// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test, console} from "forge-std/Test.sol"; import {CCIPWETH10Bridge} from "../contracts/ccip/CCIPWETH10Bridge.sol"; import {WETH10} from "../contracts/tokens/WETH10.sol"; import {IRouterClient} from "../contracts/ccip/IRouterClient.sol"; interface IERC20 { function approve(address spender, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); } contract MockLinkToken { mapping(address => uint256) public balanceOf; function mint(address to, uint256 amount) external { balanceOf[to] += amount; } function transfer(address to, uint256 amount) external returns (bool) { balanceOf[msg.sender] -= amount; balanceOf[to] += amount; return true; } function transferFrom(address from, address to, uint256 amount) external returns (bool) { balanceOf[from] -= amount; balanceOf[to] += amount; return true; } function approve(address spender, uint256 amount) external returns (bool) { return true; } } contract MockCCIPRouter10 is IRouterClient { mapping(bytes32 => bool) public messages; uint256 public fee = 0.001 ether; function ccipSend( uint64 destinationChainSelector, EVM2AnyMessage memory message ) external payable override returns (bytes32 messageId, uint256 fees) { messageId = keccak256(abi.encode(block.timestamp, msg.sender, message)); messages[messageId] = true; fees = fee; emit MessageSent( messageId, destinationChainSelector, msg.sender, message.receiver, message.data, message.tokenAmounts, message.feeToken, message.extraArgs ); } function getFee( uint64 destinationChainSelector, EVM2AnyMessage memory message ) external view override returns (uint256) { return fee; } function getSupportedTokens( uint64 destinationChainSelector ) external pure override returns (address[] memory) { return new address[](0); } // Note: In real CCIP, tokens are automatically transferred by the router // This mock is simplified for testing } contract CCIPWETH10BridgeTest is Test { CCIPWETH10Bridge public bridge; WETH10 public weth10; MockCCIPRouter10 public mockRouter; MockLinkToken public feeToken; address public user = address(1); address public recipient = address(2); uint64 public destinationChainSelector = 1; function setUp() public { // Deploy WETH10 weth10 = new WETH10(); // Deploy Mock LINK token feeToken = new MockLinkToken(); // Deploy Mock CCIP Router mockRouter = new MockCCIPRouter10(); // Deploy Bridge bridge = new CCIPWETH10Bridge( address(mockRouter), address(weth10), address(feeToken) ); // Setup user vm.deal(user, 10 ether); vm.prank(user); weth10.deposit{value: 5 ether}(); // Fund user with LINK feeToken.mint(user, 10 ether); } function testAddDestination() public { address receiverBridge = address(0x456); vm.prank(bridge.admin()); bridge.addDestination(destinationChainSelector, receiverBridge); (uint64 chainSelector, address receiverBridge_, bool enabled) = bridge.destinations(destinationChainSelector); assertEq(chainSelector, destinationChainSelector); assertEq(receiverBridge_, receiverBridge); assertTrue(enabled); } function testSendCrossChain() public { address receiverBridge = address(0x456); uint256 amount = 1 ether; // Add destination vm.prank(bridge.admin()); bridge.addDestination(destinationChainSelector, receiverBridge); // Approve bridge vm.prank(user); weth10.approve(address(bridge), amount); // Approve fee token vm.prank(user); feeToken.approve(address(bridge), 1 ether); // Send cross-chain vm.prank(user); bytes32 messageId = bridge.sendCrossChain( destinationChainSelector, recipient, amount ); assertTrue(messageId != bytes32(0)); assertEq(weth10.balanceOf(user), 4 ether); assertEq(weth10.balanceOf(address(bridge)), amount); } function testReceiveCrossChain() public { uint256 amount = 1 ether; address sourceSender = address(0x789); uint64 sourceChainSelector = 2; // Deposit WETH10 to bridge for testing (simulating CCIP token transfer) vm.deal(address(this), amount); weth10.deposit{value: amount}(); weth10.transfer(address(bridge), amount); // Prepare message bytes32 messageId = keccak256("test-message"); bytes memory data = abi.encode(recipient, amount, sourceSender, 1); IRouterClient.TokenAmount[] memory tokenAmounts = new IRouterClient.TokenAmount[](1); tokenAmounts[0] = IRouterClient.TokenAmount({ token: address(weth10), amount: amount, amountType: IRouterClient.TokenAmountType.Fiat }); // Simulate receive (mock router calls bridge - tokens already transferred) vm.prank(address(mockRouter)); bridge.ccipReceive( IRouterClient.Any2EVMMessage({ messageId: messageId, sourceChainSelector: sourceChainSelector, sender: abi.encode(sourceSender), data: data, tokenAmounts: tokenAmounts }) ); assertEq(weth10.balanceOf(recipient), amount); assertTrue(bridge.processedTransfers(messageId)); } function testCalculateFee() public { address receiverBridge = address(0x456); uint256 amount = 1 ether; // Add destination vm.prank(bridge.admin()); bridge.addDestination(destinationChainSelector, receiverBridge); // Calculate fee uint256 fee = bridge.calculateFee(destinationChainSelector, amount); assertEq(fee, mockRouter.fee()); } }