// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol"; import {OfficialStableMirrorToken} from "../../contracts/tokens/OfficialStableMirrorToken.sol"; import {CompliantUSDCTokenV2} from "../../contracts/tokens/CompliantUSDCTokenV2.sol"; import {CompliantUSDTTokenV2} from "../../contracts/tokens/CompliantUSDTTokenV2.sol"; import {StablecoinReserveVault} from "../../contracts/reserve/StablecoinReserveVault.sol"; import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol"; import {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol"; import {CWReserveVerifier} from "../../contracts/bridge/integration/CWReserveVerifier.sol"; import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol"; contract MockRouterVaultVerifierV2 is IRouterClient { uint256 public fee; bytes32 public nextMessageId = keccak256("cw-reserve-vault-v2-message"); EVM2AnyMessage internal _lastMessage; uint64 public lastDestinationChainSelector; function ccipSend( uint64 destinationChainSelector, EVM2AnyMessage memory message ) external payable returns (bytes32 messageId, uint256 fees) { lastDestinationChainSelector = destinationChainSelector; _lastMessage = message; return (nextMessageId, fee); } function getFee(uint64, EVM2AnyMessage memory) external view returns (uint256) { return fee; } function getSupportedTokens(uint64) external pure returns (address[] memory tokens) { tokens = new address[](0); } function lastMessage() external view returns (bytes memory receiver, bytes memory data, address feeToken, bytes memory extraArgs) { return (_lastMessage.receiver, _lastMessage.data, _lastMessage.feeToken, _lastMessage.extraArgs); } } contract CWReserveVerifierVaultV2IntegrationTest is Test { uint64 internal constant AVALANCHE_SELECTOR = 6433500567565415381; address internal admin = address(0xABCD); address internal user = address(0xBEEF); address internal receiveRouterL1 = address(0x138138); address internal receiveRouterL2 = address(0x4311443114); address internal peerBridge = address(0x4311443114); MockRouterVaultVerifierV2 internal router; OfficialStableMirrorToken internal officialUsdt; OfficialStableMirrorToken internal officialUsdc; CompliantUSDTTokenV2 internal compliantUsdtV2; CompliantUSDCTokenV2 internal compliantUsdcV2; StablecoinReserveVault internal vault; CWMultiTokenBridgeL1 internal l1Bridge; CWMultiTokenBridgeL2 internal l2Bridge; CWReserveVerifier internal verifier; CompliantWrappedToken internal wrappedUsdc; function setUp() public { router = new MockRouterVaultVerifierV2(); officialUsdt = new OfficialStableMirrorToken("Tether USD (Chain 138)", "USDT", 6, address(this), 0); officialUsdc = new OfficialStableMirrorToken("USD Coin (Chain 138)", "USDC", 6, address(this), 0); compliantUsdtV2 = new CompliantUSDTTokenV2(address(this), admin, 1_000_000 * 10 ** 6, true); compliantUsdcV2 = new CompliantUSDCTokenV2(address(this), admin, 1_000_000 * 10 ** 6, true); vault = new StablecoinReserveVault( admin, address(officialUsdt), address(officialUsdc), address(compliantUsdtV2), address(compliantUsdcV2) ); l1Bridge = new CWMultiTokenBridgeL1(address(router), receiveRouterL1, address(0)); l2Bridge = new CWMultiTokenBridgeL2(address(router), receiveRouterL2, address(0)); verifier = new CWReserveVerifier(admin, address(l1Bridge), address(vault), address(0)); wrappedUsdc = new CompliantWrappedToken("Wrapped cUSDC", "cWUSDC", 6, address(this)); uint256 canonicalSupply = compliantUsdcV2.totalSupply(); officialUsdc.mint(admin, canonicalSupply); vm.prank(admin); officialUsdc.approve(address(vault), canonicalSupply); vm.prank(admin); vault.seedUSDCReserve(canonicalSupply); vm.startPrank(admin); compliantUsdtV2.grantRole(compliantUsdtV2.MINTER_ROLE(), address(vault)); compliantUsdtV2.grantRole(compliantUsdtV2.PAUSER_ROLE(), address(vault)); compliantUsdtV2.transferOwnership(address(vault)); compliantUsdcV2.grantRole(compliantUsdcV2.MINTER_ROLE(), address(vault)); compliantUsdcV2.grantRole(compliantUsdcV2.PAUSER_ROLE(), address(vault)); compliantUsdcV2.transferOwnership(address(vault)); vm.stopPrank(); vm.prank(address(this)); compliantUsdcV2.transfer(user, 250e6); l1Bridge.configureSupportedCanonicalToken(address(compliantUsdcV2), true); l1Bridge.configureDestination(address(compliantUsdcV2), AVALANCHE_SELECTOR, address(l2Bridge), true); l1Bridge.setReserveVerifier(address(verifier)); l2Bridge.configureDestination(138, address(l1Bridge), true); l2Bridge.configureTokenPair(address(compliantUsdcV2), address(wrappedUsdc)); wrappedUsdc.grantRole(wrappedUsdc.MINTER_ROLE(), address(l2Bridge)); wrappedUsdc.grantRole(wrappedUsdc.BURNER_ROLE(), address(l2Bridge)); vm.prank(admin); verifier.configureToken( address(compliantUsdcV2), address(0), true, false, true ); } function testVerifierAllowsLockForV2CanonicalTokenBackedByVault() public { uint256 amount = 25e6; vm.startPrank(user); compliantUsdcV2.approve(address(l1Bridge), amount); l1Bridge.lockAndSend(address(compliantUsdcV2), AVALANCHE_SELECTOR, user, amount); vm.stopPrank(); assertEq(l1Bridge.lockedBalance(address(compliantUsdcV2)), amount); assertEq(l1Bridge.totalOutstanding(address(compliantUsdcV2)), amount); assertEq(l1Bridge.outstandingMinted(address(compliantUsdcV2), AVALANCHE_SELECTOR), amount); } function testV2CanonicalTokenCompletesFullTransportRoundTrip() public { uint256 amount = 25e6; vm.startPrank(user); compliantUsdcV2.approve(address(l1Bridge), amount); bytes32 outboundMessageId = l1Bridge.lockAndSend(address(compliantUsdcV2), AVALANCHE_SELECTOR, user, amount); vm.stopPrank(); (bytes memory receiverData, bytes memory outboundData,,) = router.lastMessage(); assertEq(abi.decode(receiverData, (address)), address(l2Bridge)); vm.prank(receiveRouterL2); l2Bridge.ccipReceive(_message(outboundMessageId, 138, address(l1Bridge), outboundData)); assertEq(wrappedUsdc.balanceOf(user), amount); assertEq(wrappedUsdc.totalSupply(), amount); assertEq(l2Bridge.mintedTotal(address(wrappedUsdc)), amount); assertEq(l2Bridge.burnedTotal(address(wrappedUsdc)), 0); vm.prank(user); bytes32 returnMessageId = l2Bridge.burnAndSend(address(wrappedUsdc), 138, user, amount); assertEq(wrappedUsdc.balanceOf(user), 0); assertEq(wrappedUsdc.totalSupply(), 0); assertEq(l2Bridge.mintedTotal(address(wrappedUsdc)), amount); assertEq(l2Bridge.burnedTotal(address(wrappedUsdc)), amount); (, bytes memory returnData,,) = router.lastMessage(); vm.prank(receiveRouterL1); l1Bridge.ccipReceive(_message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData)); assertEq(compliantUsdcV2.balanceOf(user), 250e6); assertEq(compliantUsdcV2.balanceOf(address(l1Bridge)), 0); assertEq(l1Bridge.lockedBalance(address(compliantUsdcV2)), 0); assertEq(l1Bridge.totalOutstanding(address(compliantUsdcV2)), 0); assertEq(l1Bridge.outstandingMinted(address(compliantUsdcV2), AVALANCHE_SELECTOR), 0); } 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 }); } }