// 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 {CWAssetReserveVerifier} from "../../contracts/bridge/integration/CWAssetReserveVerifier.sol"; contract MockCanonicalAssetToken is ERC20 { address internal _owner; constructor(string memory name_, string memory symbol_, address initialOwner) ERC20(name_, symbol_) { _owner = initialOwner; } function decimals() public pure override returns (uint8) { return 18; } 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 MockReserveAsset is ERC20 { constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} function decimals() public pure override returns (uint8) { return 18; } function mint(address to, uint256 amount) external { _mint(to, amount); } function burn(address from, uint256 amount) external { _burn(from, amount); } } contract MockRouterAssetVerifier is IRouterClient { function ccipSend( uint64, EVM2AnyMessage memory ) external payable returns (bytes32 messageId, uint256 fees) { return (bytes32(0), 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); } } contract MockReserveSystemAssetVerifier { 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 CWAssetReserveVerifierTest is Test { uint64 internal constant MAINNET_SELECTOR = 5009297550715157269; uint64 internal constant BSC_SELECTOR = 11344663589394136015; address internal user = address(0xBEEF); address internal receiveRouter = address(0x138138); address internal strictPeerBridge = address(0x010101); address internal hybridPeerBridge = address(0x020202); address internal assetVault = address(0xCAFE); MockRouterAssetVerifier internal router; MockCanonicalAssetToken internal strictCanonical; MockCanonicalAssetToken internal hybridCanonical; MockReserveAsset internal strictReserveAsset; MockReserveAsset internal hybridReserveAsset; MockReserveSystemAssetVerifier internal reserveSystem; CWMultiTokenBridgeL1 internal l1Bridge; CWAssetReserveVerifier internal verifier; function setUp() public { router = new MockRouterAssetVerifier(); strictCanonical = new MockCanonicalAssetToken("Ethereum Mainnet Gas (Compliant)", "cETH", assetVault); hybridCanonical = new MockCanonicalAssetToken("BNB Gas (Compliant)", "cBNB", address(this)); strictReserveAsset = new MockReserveAsset("Wrapped Ether", "WETH"); hybridReserveAsset = new MockReserveAsset("Wrapped BNB", "WBNB"); reserveSystem = new MockReserveSystemAssetVerifier(); l1Bridge = new CWMultiTokenBridgeL1(address(router), receiveRouter, address(0)); verifier = new CWAssetReserveVerifier(address(this), address(l1Bridge), assetVault, address(reserveSystem)); l1Bridge.configureSupportedCanonicalToken(address(strictCanonical), true); l1Bridge.configureDestination(address(strictCanonical), MAINNET_SELECTOR, strictPeerBridge, true); l1Bridge.configureSupportedCanonicalToken(address(hybridCanonical), true); l1Bridge.configureDestination(address(hybridCanonical), BSC_SELECTOR, hybridPeerBridge, true); l1Bridge.setReserveVerifier(address(verifier)); verifier.configureToken(address(strictCanonical), address(strictReserveAsset), true, false, true); verifier.configureToken(address(hybridCanonical), address(hybridReserveAsset), false, true, false); strictCanonical.mint(user, 100e18); hybridCanonical.mint(user, 75e18); strictReserveAsset.mint(assetVault, strictCanonical.totalSupply()); reserveSystem.setReserveBalance(address(hybridReserveAsset), hybridCanonical.totalSupply()); } function testStrictVerifierAllowsLockWhenVaultBalanceMatchesSupply() public { uint256 amount = 10e18; vm.startPrank(user); strictCanonical.approve(address(l1Bridge), amount); l1Bridge.lockAndSend(address(strictCanonical), MAINNET_SELECTOR, user, amount); vm.stopPrank(); assertEq(l1Bridge.lockedBalance(address(strictCanonical)), amount); assertEq(l1Bridge.totalOutstanding(address(strictCanonical)), amount); } function testStrictVerifierBlocksLockWhenVaultBalanceFallsShort() public { uint256 amount = 10e18; strictReserveAsset.burn(assetVault, 1); vm.startPrank(user); strictCanonical.approve(address(l1Bridge), amount); vm.expectRevert(CWAssetReserveVerifier.VaultBackingInsufficient.selector); l1Bridge.lockAndSend(address(strictCanonical), MAINNET_SELECTOR, user, amount); vm.stopPrank(); } function testStrictVerifierBlocksLockWhenTokenOwnerDoesNotMatchVault() public { uint256 amount = 10e18; strictCanonical.setOwner(address(0xDEAD)); vm.startPrank(user); strictCanonical.approve(address(l1Bridge), amount); vm.expectRevert(CWAssetReserveVerifier.TokenOwnerMismatch.selector); l1Bridge.lockAndSend(address(strictCanonical), MAINNET_SELECTOR, user, amount); vm.stopPrank(); } function testHybridVerifierAllowsLockWhenReserveSystemBalanceMatchesSupply() public { uint256 amount = 10e18; vm.startPrank(user); hybridCanonical.approve(address(l1Bridge), amount); l1Bridge.lockAndSend(address(hybridCanonical), BSC_SELECTOR, user, amount); vm.stopPrank(); assertEq(l1Bridge.lockedBalance(address(hybridCanonical)), amount); assertEq(l1Bridge.totalOutstanding(address(hybridCanonical)), amount); } function testHybridVerifierBlocksLockWhenReserveSystemFallsShort() public { uint256 amount = 10e18; reserveSystem.setReserveBalance(address(hybridReserveAsset), hybridCanonical.totalSupply() - 1); vm.startPrank(user); hybridCanonical.approve(address(l1Bridge), amount); vm.expectRevert(CWAssetReserveVerifier.ReserveSystemBackingInsufficient.selector); l1Bridge.lockAndSend(address(hybridCanonical), BSC_SELECTOR, user, amount); vm.stopPrank(); } }