- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
263 lines
9.3 KiB
Solidity
263 lines
9.3 KiB
Solidity
// 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
|
|
});
|
|
}
|
|
}
|