// 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 {CWReserveVerifier} from "../../contracts/bridge/integration/CWReserveVerifier.sol"; contract MockCanonicalOwnableToken is ERC20 { address internal _owner; constructor(address initialOwner) ERC20("Mock Canonical Stable", "MCS") { _owner = initialOwner; } function owner() external view returns (address) { return _owner; } function setOwner(address newOwner) external { _owner = newOwner; } function mint(address to, uint256 amount) external { _mint(to, amount); } } contract MockRouterVerifier is IRouterClient { uint256 public fee; bytes32 public nextMessageId = keccak256("cw-reserve-message"); EVM2AnyMessage internal _lastMessage; function ccipSend( uint64, EVM2AnyMessage memory message ) external payable returns (bytes32 messageId, uint256 fees) { fees = fee; _lastMessage = message; return (nextMessageId, fees); } 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 MockStablecoinReserveVaultForCW { address public compliantUSDT; address public compliantUSDC; mapping(address => uint256) public reserveBalance; mapping(address => uint256) public backingRatio; bool public usdtAdequate = true; bool public usdcAdequate = true; constructor(address compliantUSDT_, address compliantUSDC_) { compliantUSDT = compliantUSDT_; compliantUSDC = compliantUSDC_; } function setBacking(address token, uint256 reserveBalance_, uint256 backingRatio_) external { reserveBalance[token] = reserveBalance_; backingRatio[token] = backingRatio_; } function setAdequacy(bool usdtAdequate_, bool usdcAdequate_) external { usdtAdequate = usdtAdequate_; usdcAdequate = usdcAdequate_; } function getBackingRatio(address token) external view returns ( uint256 reserveBalance_, uint256 tokenSupply, uint256 backingRatio_ ) { reserveBalance_ = reserveBalance[token]; tokenSupply = ERC20(token).totalSupply(); backingRatio_ = backingRatio[token]; } function checkReserveAdequacy() external view returns (bool, bool) { return (usdtAdequate, usdcAdequate); } } contract MockReserveSystemForCW { mapping(address => uint256) internal reserveBalances; function setReserveBalance(address asset, uint256 amount) external { reserveBalances[asset] = amount; } function getReserveBalance(address asset) external view returns (uint256) { return reserveBalances[asset]; } } contract CWReserveVerifierTest is Test { uint64 internal constant AVALANCHE_SELECTOR = 6433500567565415381; address internal user = address(0xBEEF); address internal receiveRouter = address(0x138138); address internal peerBridge = address(0x4311443114); address internal officialReserveAsset = address(0xA0b8); MockRouterVerifier internal router; MockCanonicalOwnableToken internal canonical; CWMultiTokenBridgeL1 internal l1Bridge; MockStablecoinReserveVaultForCW internal reserveVault; MockReserveSystemForCW internal reserveSystem; CWReserveVerifier internal verifier; function setUp() public { router = new MockRouterVerifier(); canonical = new MockCanonicalOwnableToken(address(this)); l1Bridge = new CWMultiTokenBridgeL1(address(router), receiveRouter, address(0)); reserveVault = new MockStablecoinReserveVaultForCW(address(0), address(canonical)); reserveSystem = new MockReserveSystemForCW(); verifier = new CWReserveVerifier(address(this), address(l1Bridge), address(reserveVault), address(reserveSystem)); canonical.setOwner(address(reserveVault)); l1Bridge.configureSupportedCanonicalToken(address(canonical), true); l1Bridge.configureDestination(address(canonical), AVALANCHE_SELECTOR, peerBridge, true); l1Bridge.setReserveVerifier(address(verifier)); verifier.configureToken( address(canonical), officialReserveAsset, true, true, true ); canonical.mint(user, 1_000_000e6); uint256 canonicalSupply = canonical.totalSupply(); reserveVault.setBacking(address(canonical), canonicalSupply, 10000); reserveVault.setAdequacy(true, true); reserveSystem.setReserveBalance(officialReserveAsset, canonicalSupply); } function testVerifierAllowsLockWhenCanonicalReservesHealthy() public { uint256 amount = 125e6; vm.startPrank(user); canonical.approve(address(l1Bridge), amount); l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount); vm.stopPrank(); assertEq(l1Bridge.lockedBalance(address(canonical)), amount); assertEq(l1Bridge.totalOutstanding(address(canonical)), amount); assertEq(l1Bridge.outstandingMinted(address(canonical), AVALANCHE_SELECTOR), amount); } function testVerifierBlocksLockWhenVaultBackingFallsBelowPar() public { uint256 amount = 25e6; reserveVault.setBacking(address(canonical), canonical.totalSupply() - 1, 9999); reserveVault.setAdequacy(true, false); vm.startPrank(user); canonical.approve(address(l1Bridge), amount); vm.expectRevert(CWReserveVerifier.VaultBackingInsufficient.selector); l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount); vm.stopPrank(); } function testVerifierBlocksLockWhenReserveSystemBackingFallsShort() public { uint256 amount = 25e6; reserveSystem.setReserveBalance(officialReserveAsset, canonical.totalSupply() - 1); vm.startPrank(user); canonical.approve(address(l1Bridge), amount); vm.expectRevert(CWReserveVerifier.ReserveSystemBackingInsufficient.selector); l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount); vm.stopPrank(); } function testVerifierRevertLeavesEscrowAccountingUnchanged() public { uint256 amount = 25e6; reserveSystem.setReserveBalance(officialReserveAsset, canonical.totalSupply() - 1); vm.startPrank(user); canonical.approve(address(l1Bridge), amount); vm.expectRevert(CWReserveVerifier.ReserveSystemBackingInsufficient.selector); l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount); vm.stopPrank(); assertEq(l1Bridge.lockedBalance(address(canonical)), 0); assertEq(l1Bridge.totalOutstanding(address(canonical)), 0); assertEq(l1Bridge.outstandingMinted(address(canonical), AVALANCHE_SELECTOR), 0); assertEq(canonical.balanceOf(user), 1_000_000e6); } function testVerifierBlocksLockWhenTokenOwnerDoesNotMatchVault() public { uint256 amount = 25e6; canonical.setOwner(address(0xCAFE)); vm.startPrank(user); canonical.approve(address(l1Bridge), amount); vm.expectRevert(CWReserveVerifier.TokenOwnerMismatch.selector); l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount); vm.stopPrank(); } function testReleaseRemainsAvailableAfterVerifierWouldLaterFail() public { uint256 amount = 60e6; vm.startPrank(user); canonical.approve(address(l1Bridge), amount); l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount); vm.stopPrank(); reserveVault.setBacking(address(canonical), canonical.totalSupply() - 10, 9990); reserveVault.setAdequacy(true, false); reserveSystem.setReserveBalance(officialReserveAsset, canonical.totalSupply() - 10); bytes memory returnData = abi.encode(address(canonical), user, amount); vm.prank(receiveRouter); l1Bridge.ccipReceive(_message(keccak256("return"), AVALANCHE_SELECTOR, peerBridge, returnData)); assertEq(l1Bridge.lockedBalance(address(canonical)), 0); assertEq(l1Bridge.totalOutstanding(address(canonical)), 0); assertEq(canonical.balanceOf(user), 1_000_000e6); } 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 }); } }