// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol"; import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol"; import {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol"; import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol"; contract MockCanonicalBTC is ERC20 { constructor() ERC20("Bitcoin (Compliant)", "cBTC") {} function decimals() public pure override returns (uint8) { return 8; } function mint(address to, uint256 amount) external { _mint(to, amount); } } contract MockRouterBTC is IRouterClient { bytes32 public nextMessageId = keccak256("btc-message"); EVM2AnyMessage internal _lastMessage; function ccipSend( uint64 destinationChainSelector, EVM2AnyMessage memory message ) external payable returns (bytes32 messageId, uint256 fees) { destinationChainSelector; _lastMessage = message; return (nextMessageId, fees); } function getFee(uint64, EVM2AnyMessage memory) external pure returns (uint256) { return 0; } function getSupportedTokens(uint64) external pure returns (address[] memory tokens) { tokens = new address[](0); } function lastMessage() external view returns (bytes memory receiver, bytes memory data) { return (_lastMessage.receiver, _lastMessage.data); } } contract CWMultiTokenBridgeBTCTest is Test { uint64 internal constant CHAIN138_SELECTOR = 138; uint64 internal constant ETHEREUM_SELECTOR = 5009297550715157269; address internal user = address(0xBEEF); MockRouterBTC internal router138; MockRouterBTC internal routerEth; MockCanonicalBTC internal canonical; CompliantWrappedToken internal wrapped; CWMultiTokenBridgeL1 internal l1Bridge; CWMultiTokenBridgeL2 internal l2Bridge; function setUp() public { router138 = new MockRouterBTC(); routerEth = new MockRouterBTC(); canonical = new MockCanonicalBTC(); wrapped = new CompliantWrappedToken("Wrapped cBTC", "cWBTC", 8, address(this)); l1Bridge = new CWMultiTokenBridgeL1(address(router138), address(0x138138), address(0)); l2Bridge = new CWMultiTokenBridgeL2(address(routerEth), address(0x010101), address(0)); l1Bridge.configureSupportedCanonicalToken(address(canonical), true); l1Bridge.configureDestination(address(canonical), ETHEREUM_SELECTOR, address(l2Bridge), true); l2Bridge.configureDestination(CHAIN138_SELECTOR, address(l1Bridge), true); l2Bridge.configureTokenPair(address(canonical), address(wrapped)); wrapped.grantRole(wrapped.MINTER_ROLE(), address(l2Bridge)); wrapped.grantRole(wrapped.BURNER_ROLE(), address(l2Bridge)); canonical.mint(user, 2_100_000_000_000_000); } function testRoundTripPreservesSatoshiPrecision() public { uint256 amount = 125_000_000; vm.startPrank(user); canonical.approve(address(l1Bridge), amount); bytes32 outboundMessageId = l1Bridge.lockAndSend(address(canonical), ETHEREUM_SELECTOR, user, amount); vm.stopPrank(); (, bytes memory outboundData) = router138.lastMessage(); vm.prank(address(0x010101)); l2Bridge.ccipReceive(_message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData)); assertEq(wrapped.balanceOf(user), amount); assertEq(wrapped.totalSupply(), amount); vm.prank(user); bytes32 returnMessageId = l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, amount); (, bytes memory returnData) = routerEth.lastMessage(); vm.prank(address(0x138138)); l1Bridge.ccipReceive(_message(returnMessageId, ETHEREUM_SELECTOR, address(l2Bridge), returnData)); assertEq(canonical.balanceOf(user), 2_100_000_000_000_000); assertEq(wrapped.totalSupply(), 0); assertEq(l1Bridge.totalOutstanding(address(canonical)), 0); } function testPerDestinationOutstandingCapStillAppliesForBTCRoute() public { l1Bridge.setMaxOutstanding(address(canonical), ETHEREUM_SELECTOR, 100_000_000); vm.startPrank(user); canonical.approve(address(l1Bridge), 125_000_000); vm.expectRevert(bytes("CWMultiTokenBridgeL1: exceeds escrow capacity")); l1Bridge.lockAndSend(address(canonical), ETHEREUM_SELECTOR, user, 125_000_000); vm.stopPrank(); } function _message( bytes32 messageId, uint64 sourceChainSelector, address sender, bytes memory data ) internal pure returns (IRouterClient.Any2EVMMessage memory message) { IRouterClient.TokenAmount[] memory noTokens = new IRouterClient.TokenAmount[](0); message = IRouterClient.Any2EVMMessage({ messageId: messageId, sourceChainSelector: sourceChainSelector, sender: abi.encode(sender), data: data, tokenAmounts: noTokens }); } }