feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- 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
This commit is contained in:
339
test/bridge/CWMultiTokenBridge.t.sol
Normal file
339
test/bridge/CWMultiTokenBridge.t.sol
Normal file
@@ -0,0 +1,339 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
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 {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol";
|
||||
import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol";
|
||||
import {CCIPRelayRouter} from "../../contracts/relay/CCIPRelayRouter.sol";
|
||||
|
||||
contract MockCanonicalToken is ERC20 {
|
||||
constructor() ERC20("Mock Canonical", "MCAN") {}
|
||||
|
||||
function mint(address to, uint256 amount) external {
|
||||
_mint(to, amount);
|
||||
}
|
||||
}
|
||||
|
||||
contract MockRouter is IRouterClient {
|
||||
uint256 public fee;
|
||||
bytes32 public nextMessageId = keccak256("message");
|
||||
EVM2AnyMessage internal _lastMessage;
|
||||
uint64 public lastDestinationChainSelector;
|
||||
|
||||
function setFee(uint256 newFee) external {
|
||||
fee = newFee;
|
||||
}
|
||||
|
||||
function ccipSend(
|
||||
uint64 destinationChainSelector,
|
||||
EVM2AnyMessage memory message
|
||||
) external payable returns (bytes32 messageId, uint256 fees) {
|
||||
fees = fee;
|
||||
if (message.feeToken == address(0)) {
|
||||
require(msg.value >= fees, "native fee");
|
||||
}
|
||||
|
||||
lastDestinationChainSelector = destinationChainSelector;
|
||||
_lastMessage = message;
|
||||
|
||||
emit MessageSent(
|
||||
nextMessageId,
|
||||
destinationChainSelector,
|
||||
msg.sender,
|
||||
message.receiver,
|
||||
message.data,
|
||||
message.tokenAmounts,
|
||||
message.feeToken,
|
||||
message.extraArgs
|
||||
);
|
||||
|
||||
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 MockReceiveBridge {
|
||||
bytes public lastData;
|
||||
|
||||
function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external {
|
||||
lastData = message.data;
|
||||
}
|
||||
}
|
||||
|
||||
contract CWMultiTokenBridgeTest is Test {
|
||||
uint64 internal constant CHAIN138_SELECTOR = 138;
|
||||
uint64 internal constant AVALANCHE_SELECTOR = 6433500567565415381;
|
||||
|
||||
address internal user = address(0xBEEF);
|
||||
|
||||
MockRouter internal router138;
|
||||
MockRouter internal routerAvax;
|
||||
MockCanonicalToken internal canonical;
|
||||
CompliantWrappedToken internal wrapped;
|
||||
CWMultiTokenBridgeL1 internal l1Bridge;
|
||||
CWMultiTokenBridgeL2 internal l2Bridge;
|
||||
|
||||
function setUp() public {
|
||||
router138 = new MockRouter();
|
||||
routerAvax = new MockRouter();
|
||||
canonical = new MockCanonicalToken();
|
||||
wrapped = new CompliantWrappedToken("Wrapped Canonical", "cWMOCK", 6, address(this));
|
||||
|
||||
l1Bridge = new CWMultiTokenBridgeL1(address(router138), address(0x138138), address(0));
|
||||
l2Bridge = new CWMultiTokenBridgeL2(address(routerAvax), address(0x4311443114), address(0));
|
||||
|
||||
l1Bridge.configureSupportedCanonicalToken(address(canonical), true);
|
||||
l1Bridge.configureDestination(address(canonical), AVALANCHE_SELECTOR, address(l2Bridge), true);
|
||||
l2Bridge.configureDestination(CHAIN138_SELECTOR, address(l1Bridge), true);
|
||||
l2Bridge.configureTokenPair(address(canonical), address(wrapped));
|
||||
|
||||
wrapped.grantRole(wrapped.MINTER_ROLE(), address(l2Bridge));
|
||||
wrapped.grantRole(wrapped.BURNER_ROLE(), address(l2Bridge));
|
||||
|
||||
canonical.mint(user, 1_000_000e18);
|
||||
}
|
||||
|
||||
function testRoundTripLockMintBurnRelease() public {
|
||||
uint256 amount = 125e6;
|
||||
|
||||
vm.startPrank(user);
|
||||
canonical.approve(address(l1Bridge), amount);
|
||||
bytes32 outboundMessageId =
|
||||
l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
|
||||
vm.stopPrank();
|
||||
|
||||
assertEq(canonical.balanceOf(address(l1Bridge)), amount);
|
||||
assertEq(canonical.balanceOf(user), 1_000_000e18 - amount);
|
||||
assertEq(l1Bridge.lockedBalance(address(canonical)), amount);
|
||||
assertEq(l1Bridge.totalOutstanding(address(canonical)), amount);
|
||||
assertEq(l1Bridge.outstandingMinted(address(canonical), AVALANCHE_SELECTOR), amount);
|
||||
|
||||
(bytes memory receiverData, bytes memory outboundData,,) = router138.lastMessage();
|
||||
assertEq(abi.decode(receiverData, (address)), address(l2Bridge));
|
||||
assertEq(outboundMessageId, router138.nextMessageId());
|
||||
|
||||
vm.prank(address(0x4311443114));
|
||||
l2Bridge.ccipReceive(_message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
|
||||
assertEq(wrapped.balanceOf(user), amount);
|
||||
assertEq(wrapped.totalSupply(), amount);
|
||||
assertEq(l2Bridge.mintedTotal(address(wrapped)), amount);
|
||||
assertEq(l2Bridge.burnedTotal(address(wrapped)), 0);
|
||||
|
||||
vm.prank(user);
|
||||
bytes32 returnMessageId = l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, amount);
|
||||
assertEq(wrapped.balanceOf(user), 0);
|
||||
assertEq(wrapped.totalSupply(), 0);
|
||||
assertEq(l2Bridge.mintedTotal(address(wrapped)), amount);
|
||||
assertEq(l2Bridge.burnedTotal(address(wrapped)), amount);
|
||||
|
||||
(, bytes memory returnData,,) = routerAvax.lastMessage();
|
||||
vm.prank(address(0x138138));
|
||||
l1Bridge.ccipReceive(_message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData));
|
||||
|
||||
assertEq(canonical.balanceOf(user), 1_000_000e18);
|
||||
assertEq(canonical.balanceOf(address(l1Bridge)), 0);
|
||||
assertEq(l1Bridge.lockedBalance(address(canonical)), 0);
|
||||
assertEq(l1Bridge.totalOutstanding(address(canonical)), 0);
|
||||
assertEq(l1Bridge.outstandingMinted(address(canonical), AVALANCHE_SELECTOR), 0);
|
||||
}
|
||||
|
||||
function testLockAndSendRespectsMaxOutstanding() public {
|
||||
uint256 amount = 125e6;
|
||||
|
||||
l1Bridge.setMaxOutstanding(address(canonical), AVALANCHE_SELECTOR, 100e6);
|
||||
|
||||
vm.startPrank(user);
|
||||
canonical.approve(address(l1Bridge), amount);
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL1: exceeds escrow capacity"));
|
||||
l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSupportedCanonicalTokenCannotBeDisabledWhileFundsOutstanding() public {
|
||||
uint256 amount = 50e6;
|
||||
|
||||
vm.startPrank(user);
|
||||
canonical.approve(address(l1Bridge), amount);
|
||||
l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
|
||||
vm.stopPrank();
|
||||
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL1: token still locked"));
|
||||
l1Bridge.configureSupportedCanonicalToken(address(canonical), false);
|
||||
}
|
||||
|
||||
function testWithdrawTokenCannotDrainLockedCanonicalEscrow() public {
|
||||
uint256 amount = 50e6;
|
||||
|
||||
vm.startPrank(user);
|
||||
canonical.approve(address(l1Bridge), amount);
|
||||
l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
|
||||
vm.stopPrank();
|
||||
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL1: amount locked"));
|
||||
l1Bridge.withdrawToken(address(canonical), user, 1);
|
||||
|
||||
canonical.mint(address(l1Bridge), 10e6);
|
||||
l1Bridge.withdrawToken(address(canonical), user, 10e6);
|
||||
assertEq(canonical.balanceOf(user), 1_000_000e18 - amount + 10e6);
|
||||
assertEq(canonical.balanceOf(address(l1Bridge)), amount);
|
||||
}
|
||||
|
||||
function testReleaseRequiresOutstandingBalance() public {
|
||||
bytes memory returnData = abi.encode(address(canonical), user, uint256(1));
|
||||
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL1: outstanding underflow"));
|
||||
vm.prank(address(0x138138));
|
||||
l1Bridge.ccipReceive(_message(keccak256("no-lock"), AVALANCHE_SELECTOR, address(l2Bridge), returnData));
|
||||
}
|
||||
|
||||
function testL2PauseBlocksMintAndBurn() public {
|
||||
uint256 amount = 20e6;
|
||||
|
||||
vm.startPrank(user);
|
||||
canonical.approve(address(l1Bridge), amount);
|
||||
bytes32 outboundMessageId =
|
||||
l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
|
||||
vm.stopPrank();
|
||||
|
||||
(, bytes memory outboundData,,) = router138.lastMessage();
|
||||
|
||||
l2Bridge.setTokenPaused(address(wrapped), true);
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL2: token paused"));
|
||||
vm.prank(address(0x4311443114));
|
||||
l2Bridge.ccipReceive(_message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
|
||||
|
||||
l2Bridge.setTokenPaused(address(wrapped), false);
|
||||
vm.prank(address(0x4311443114));
|
||||
l2Bridge.ccipReceive(_message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
|
||||
|
||||
l2Bridge.setTokenPaused(address(wrapped), true);
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL2: token paused"));
|
||||
vm.prank(user);
|
||||
l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, amount);
|
||||
}
|
||||
|
||||
function testL2CirculatingSupplyTracksMintMinusBurn() public {
|
||||
uint256 amount = 35e6;
|
||||
|
||||
vm.startPrank(user);
|
||||
canonical.approve(address(l1Bridge), amount);
|
||||
bytes32 outboundMessageId =
|
||||
l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
|
||||
vm.stopPrank();
|
||||
|
||||
(, bytes memory outboundData,,) = router138.lastMessage();
|
||||
|
||||
vm.prank(address(0x4311443114));
|
||||
l2Bridge.ccipReceive(_message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
|
||||
assertEq(l2Bridge.circulatingSupply(address(wrapped)), amount);
|
||||
|
||||
vm.prank(user);
|
||||
l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, 10e6);
|
||||
assertEq(l2Bridge.circulatingSupply(address(wrapped)), amount - 10e6);
|
||||
}
|
||||
|
||||
function testL2RejectsReplayOfInboundMintMessage() public {
|
||||
uint256 amount = 15e6;
|
||||
|
||||
vm.startPrank(user);
|
||||
canonical.approve(address(l1Bridge), amount);
|
||||
bytes32 outboundMessageId =
|
||||
l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
|
||||
vm.stopPrank();
|
||||
|
||||
(, bytes memory outboundData,,) = router138.lastMessage();
|
||||
|
||||
vm.prank(address(0x4311443114));
|
||||
l2Bridge.ccipReceive(_message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
|
||||
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL2: replayed"));
|
||||
vm.prank(address(0x4311443114));
|
||||
l2Bridge.ccipReceive(_message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
|
||||
}
|
||||
|
||||
function testFrozenL2ConfigCannotBeRewritten() public {
|
||||
CompliantWrappedToken wrapped2 = new CompliantWrappedToken("Wrapped Canonical 2", "cWMOCK2", 6, address(this));
|
||||
|
||||
l2Bridge.freezeTokenPair(address(canonical));
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL2: token pair frozen"));
|
||||
l2Bridge.configureTokenPair(address(canonical), address(wrapped2));
|
||||
|
||||
l2Bridge.freezeDestination(CHAIN138_SELECTOR);
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL2: destination frozen"));
|
||||
l2Bridge.configureDestination(CHAIN138_SELECTOR, address(0x1234), true);
|
||||
}
|
||||
|
||||
function testL1RejectsReplayOfReturnMessage() public {
|
||||
uint256 amount = 25e6;
|
||||
|
||||
vm.startPrank(user);
|
||||
canonical.approve(address(l1Bridge), amount);
|
||||
l1Bridge.lockAndSend(address(canonical), AVALANCHE_SELECTOR, user, amount);
|
||||
vm.stopPrank();
|
||||
|
||||
bytes32 returnMessageId = keccak256("return-replay");
|
||||
bytes memory returnData = abi.encode(address(canonical), user, amount);
|
||||
|
||||
vm.prank(address(0x138138));
|
||||
l1Bridge.ccipReceive(_message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData));
|
||||
|
||||
vm.expectRevert(bytes("CWMultiTokenBridgeL1: replayed"));
|
||||
vm.prank(address(0x138138));
|
||||
l1Bridge.ccipReceive(_message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData));
|
||||
}
|
||||
|
||||
function testRelayRouterAcceptsThreeFieldPayload() public {
|
||||
CCIPRelayRouter relayRouter = new CCIPRelayRouter();
|
||||
MockReceiveBridge receiveBridge = new MockReceiveBridge();
|
||||
|
||||
relayRouter.authorizeBridge(address(receiveBridge));
|
||||
relayRouter.grantRelayerRole(address(this));
|
||||
|
||||
IRouterClient.TokenAmount[] memory noTokens = new IRouterClient.TokenAmount[](0);
|
||||
IRouterClient.Any2EVMMessage memory message = IRouterClient.Any2EVMMessage({
|
||||
messageId: keccak256("three-field"),
|
||||
sourceChainSelector: CHAIN138_SELECTOR,
|
||||
sender: abi.encode(address(0xCAFE)),
|
||||
data: abi.encode(address(canonical), user, uint256(42)),
|
||||
tokenAmounts: noTokens
|
||||
});
|
||||
|
||||
relayRouter.relayMessage(address(receiveBridge), message);
|
||||
assertEq(keccak256(receiveBridge.lastData()), keccak256(message.data));
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user