// 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 MockCanonicalBTCWithOwner is ERC20 { address internal _owner; constructor(address initialOwner) ERC20("Bitcoin (Compliant)", "cBTC") { _owner = initialOwner; } function decimals() public pure override returns (uint8) { return 8; } 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 MockRouterVerifierBTC 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 MockReserveVaultBTC { address internal trackedToken; mapping(address => uint256) public reserveBalance; mapping(address => uint256) public backingRatio; bool public tokenAdequate = true; constructor(address trackedToken_) { trackedToken = trackedToken_; } function compliantUSDT() external view returns (address) { return trackedToken; } function compliantUSDC() external pure returns (address) { return address(0xCAFE); } function setAdequacy(bool tokenAdequate_) external { tokenAdequate = tokenAdequate_; } function setBacking(address token, uint256 reserveBalance_, uint256 backingRatio_) external { reserveBalance[token] = reserveBalance_; backingRatio[token] = backingRatio_; } function getBackingRatio(address token) external view returns (uint256, uint256, uint256) { return (reserveBalance[token], ERC20(token).totalSupply(), backingRatio[token]); } function checkReserveAdequacy() external view returns (bool, bool) { return (tokenAdequate, true); } } contract MockReserveSystemBTC { 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 CWReserveVerifierBTCTest is Test { uint64 internal constant ETHEREUM_SELECTOR = 5009297550715157269; address internal user = address(0xBEEF); address internal receiveRouter = address(0x138138); address internal peerBridge = address(0x010101); address internal reserveAsset = address(0x0B7C); MockRouterVerifierBTC internal router; MockCanonicalBTCWithOwner internal canonical; CWMultiTokenBridgeL1 internal l1Bridge; MockReserveVaultBTC internal reserveVault; MockReserveSystemBTC internal reserveSystem; CWReserveVerifier internal verifier; function setUp() public { router = new MockRouterVerifierBTC(); canonical = new MockCanonicalBTCWithOwner(address(this)); l1Bridge = new CWMultiTokenBridgeL1(address(router), receiveRouter, address(0)); reserveVault = new MockReserveVaultBTC(address(canonical)); reserveSystem = new MockReserveSystemBTC(); verifier = new CWReserveVerifier(address(this), address(l1Bridge), address(reserveVault), address(reserveSystem)); canonical.setOwner(address(reserveVault)); l1Bridge.configureSupportedCanonicalToken(address(canonical), true); l1Bridge.configureDestination(address(canonical), ETHEREUM_SELECTOR, peerBridge, true); l1Bridge.setReserveVerifier(address(verifier)); verifier.configureToken(address(canonical), reserveAsset, true, true, true); canonical.mint(user, 10_000_000_000); reserveVault.setBacking(address(canonical), canonical.totalSupply(), 10_000); reserveVault.setAdequacy(true); reserveSystem.setReserveBalance(reserveAsset, canonical.totalSupply()); } function testVerifierAllowsBTCLockAtEightDecimals() public { vm.startPrank(user); canonical.approve(address(l1Bridge), 250_000_000); l1Bridge.lockAndSend(address(canonical), ETHEREUM_SELECTOR, user, 250_000_000); vm.stopPrank(); assertEq(l1Bridge.totalOutstanding(address(canonical)), 250_000_000); assertEq(l1Bridge.lockedBalance(address(canonical)), 250_000_000); } function testVerifierBlocksBTCLockWhenReserveFallsShort() public { reserveSystem.setReserveBalance(reserveAsset, canonical.totalSupply() - 1); vm.startPrank(user); canonical.approve(address(l1Bridge), 100_000_000); vm.expectRevert(CWReserveVerifier.ReserveSystemBackingInsufficient.selector); l1Bridge.lockAndSend(address(canonical), ETHEREUM_SELECTOR, user, 100_000_000); vm.stopPrank(); } }