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:
defiQUG
2026-04-07 23:40:52 -07:00
parent 0fb7bba07b
commit 76aa419320
289 changed files with 28367 additions and 824 deletions

View File

@@ -0,0 +1,180 @@
// 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();
}
}

View 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
});
}
}

View File

@@ -0,0 +1,135 @@
// 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 {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol";
import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol";
contract MockCanonicalBTC is ERC20 {
constructor() ERC20("Bitcoin (Compliant)", "cBTC") {}
function decimals() public pure override returns (uint8) {
return 8;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract MockRouterBTC is IRouterClient {
bytes32 public nextMessageId = keccak256("btc-message");
EVM2AnyMessage internal _lastMessage;
function ccipSend(
uint64 destinationChainSelector,
EVM2AnyMessage memory message
) external payable returns (bytes32 messageId, uint256 fees) {
destinationChainSelector;
_lastMessage = message;
return (nextMessageId, 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);
}
function lastMessage() external view returns (bytes memory receiver, bytes memory data) {
return (_lastMessage.receiver, _lastMessage.data);
}
}
contract CWMultiTokenBridgeBTCTest is Test {
uint64 internal constant CHAIN138_SELECTOR = 138;
uint64 internal constant ETHEREUM_SELECTOR = 5009297550715157269;
address internal user = address(0xBEEF);
MockRouterBTC internal router138;
MockRouterBTC internal routerEth;
MockCanonicalBTC internal canonical;
CompliantWrappedToken internal wrapped;
CWMultiTokenBridgeL1 internal l1Bridge;
CWMultiTokenBridgeL2 internal l2Bridge;
function setUp() public {
router138 = new MockRouterBTC();
routerEth = new MockRouterBTC();
canonical = new MockCanonicalBTC();
wrapped = new CompliantWrappedToken("Wrapped cBTC", "cWBTC", 8, address(this));
l1Bridge = new CWMultiTokenBridgeL1(address(router138), address(0x138138), address(0));
l2Bridge = new CWMultiTokenBridgeL2(address(routerEth), address(0x010101), address(0));
l1Bridge.configureSupportedCanonicalToken(address(canonical), true);
l1Bridge.configureDestination(address(canonical), ETHEREUM_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, 2_100_000_000_000_000);
}
function testRoundTripPreservesSatoshiPrecision() public {
uint256 amount = 125_000_000;
vm.startPrank(user);
canonical.approve(address(l1Bridge), amount);
bytes32 outboundMessageId = l1Bridge.lockAndSend(address(canonical), ETHEREUM_SELECTOR, user, amount);
vm.stopPrank();
(, bytes memory outboundData) = router138.lastMessage();
vm.prank(address(0x010101));
l2Bridge.ccipReceive(_message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
assertEq(wrapped.balanceOf(user), amount);
assertEq(wrapped.totalSupply(), amount);
vm.prank(user);
bytes32 returnMessageId = l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, amount);
(, bytes memory returnData) = routerEth.lastMessage();
vm.prank(address(0x138138));
l1Bridge.ccipReceive(_message(returnMessageId, ETHEREUM_SELECTOR, address(l2Bridge), returnData));
assertEq(canonical.balanceOf(user), 2_100_000_000_000_000);
assertEq(wrapped.totalSupply(), 0);
assertEq(l1Bridge.totalOutstanding(address(canonical)), 0);
}
function testPerDestinationOutstandingCapStillAppliesForBTCRoute() public {
l1Bridge.setMaxOutstanding(address(canonical), ETHEREUM_SELECTOR, 100_000_000);
vm.startPrank(user);
canonical.approve(address(l1Bridge), 125_000_000);
vm.expectRevert(bytes("CWMultiTokenBridgeL1: exceeds escrow capacity"));
l1Bridge.lockAndSend(address(canonical), ETHEREUM_SELECTOR, user, 125_000_000);
vm.stopPrank();
}
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
});
}
}

View File

@@ -0,0 +1,262 @@
// 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
});
}
}

View File

@@ -0,0 +1,155 @@
// 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();
}
}

View File

@@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol";
import {OfficialStableMirrorToken} from "../../contracts/tokens/OfficialStableMirrorToken.sol";
import {CompliantUSDT} from "../../contracts/tokens/CompliantUSDT.sol";
import {CompliantUSDC} from "../../contracts/tokens/CompliantUSDC.sol";
import {StablecoinReserveVault} from "../../contracts/reserve/StablecoinReserveVault.sol";
import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol";
import {CWReserveVerifier} from "../../contracts/bridge/integration/CWReserveVerifier.sol";
contract MockRouterVaultVerifier is IRouterClient {
uint256 public fee;
bytes32 public nextMessageId = keccak256("cw-reserve-vault-message");
function ccipSend(
uint64,
EVM2AnyMessage memory
) external payable returns (bytes32 messageId, uint256 fees) {
return (nextMessageId, fee);
}
function getFee(uint64, EVM2AnyMessage memory) external view returns (uint256) {
return fee;
}
function getSupportedTokens(uint64) external pure returns (address[] memory tokens) {
tokens = new address[](0);
}
}
contract CWReserveVerifierVaultIntegrationTest is Test {
uint64 internal constant AVALANCHE_SELECTOR = 6433500567565415381;
address internal user = address(0xBEEF);
address internal receiveRouter = address(0x138138);
address internal peerBridge = address(0x4311443114);
MockRouterVaultVerifier internal router;
OfficialStableMirrorToken internal officialUsdt;
OfficialStableMirrorToken internal officialUsdc;
CompliantUSDT internal compliantUsdt;
CompliantUSDC internal compliantUsdc;
StablecoinReserveVault internal vault;
CWMultiTokenBridgeL1 internal l1Bridge;
CWReserveVerifier internal verifier;
function setUp() public {
router = new MockRouterVaultVerifier();
officialUsdt = new OfficialStableMirrorToken("Tether USD (Chain 138)", "USDT", 6, address(this), 0);
officialUsdc = new OfficialStableMirrorToken("USD Coin (Chain 138)", "USDC", 6, address(this), 0);
compliantUsdt = new CompliantUSDT(address(this), address(this));
compliantUsdc = new CompliantUSDC(address(this), address(this));
vault = new StablecoinReserveVault(
address(this),
address(officialUsdt),
address(officialUsdc),
address(compliantUsdt),
address(compliantUsdc)
);
l1Bridge = new CWMultiTokenBridgeL1(address(router), receiveRouter, address(0));
verifier = new CWReserveVerifier(address(this), address(l1Bridge), address(vault), address(0));
uint256 canonicalSupply = compliantUsdc.totalSupply();
officialUsdc.mint(address(this), canonicalSupply);
officialUsdc.approve(address(vault), canonicalSupply);
vault.seedUSDCReserve(canonicalSupply);
compliantUsdc.transferOwnership(address(vault));
compliantUsdc.transfer(user, 250e6);
l1Bridge.configureSupportedCanonicalToken(address(compliantUsdc), true);
l1Bridge.configureDestination(address(compliantUsdc), AVALANCHE_SELECTOR, peerBridge, true);
l1Bridge.setReserveVerifier(address(verifier));
verifier.configureToken(
address(compliantUsdc),
address(0),
true,
false,
true
);
}
function testVerifierAllowsLockWhenRealVaultBacksExistingSupply() public {
uint256 amount = 25e6;
vm.startPrank(user);
compliantUsdc.approve(address(l1Bridge), amount);
l1Bridge.lockAndSend(address(compliantUsdc), AVALANCHE_SELECTOR, user, amount);
vm.stopPrank();
assertEq(l1Bridge.lockedBalance(address(compliantUsdc)), amount);
assertEq(l1Bridge.totalOutstanding(address(compliantUsdc)), amount);
assertEq(l1Bridge.outstandingMinted(address(compliantUsdc), AVALANCHE_SELECTOR), amount);
}
}

View File

@@ -0,0 +1,195 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol";
import {OfficialStableMirrorToken} from "../../contracts/tokens/OfficialStableMirrorToken.sol";
import {CompliantUSDCTokenV2} from "../../contracts/tokens/CompliantUSDCTokenV2.sol";
import {CompliantUSDTTokenV2} from "../../contracts/tokens/CompliantUSDTTokenV2.sol";
import {StablecoinReserveVault} from "../../contracts/reserve/StablecoinReserveVault.sol";
import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol";
import {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol";
import {CWReserveVerifier} from "../../contracts/bridge/integration/CWReserveVerifier.sol";
import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol";
contract MockRouterVaultVerifierV2 is IRouterClient {
uint256 public fee;
bytes32 public nextMessageId = keccak256("cw-reserve-vault-v2-message");
EVM2AnyMessage internal _lastMessage;
uint64 public lastDestinationChainSelector;
function ccipSend(
uint64 destinationChainSelector,
EVM2AnyMessage memory message
) external payable returns (bytes32 messageId, uint256 fees) {
lastDestinationChainSelector = destinationChainSelector;
_lastMessage = message;
return (nextMessageId, fee);
}
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 CWReserveVerifierVaultV2IntegrationTest is Test {
uint64 internal constant AVALANCHE_SELECTOR = 6433500567565415381;
address internal admin = address(0xABCD);
address internal user = address(0xBEEF);
address internal receiveRouterL1 = address(0x138138);
address internal receiveRouterL2 = address(0x4311443114);
address internal peerBridge = address(0x4311443114);
MockRouterVaultVerifierV2 internal router;
OfficialStableMirrorToken internal officialUsdt;
OfficialStableMirrorToken internal officialUsdc;
CompliantUSDTTokenV2 internal compliantUsdtV2;
CompliantUSDCTokenV2 internal compliantUsdcV2;
StablecoinReserveVault internal vault;
CWMultiTokenBridgeL1 internal l1Bridge;
CWMultiTokenBridgeL2 internal l2Bridge;
CWReserveVerifier internal verifier;
CompliantWrappedToken internal wrappedUsdc;
function setUp() public {
router = new MockRouterVaultVerifierV2();
officialUsdt = new OfficialStableMirrorToken("Tether USD (Chain 138)", "USDT", 6, address(this), 0);
officialUsdc = new OfficialStableMirrorToken("USD Coin (Chain 138)", "USDC", 6, address(this), 0);
compliantUsdtV2 = new CompliantUSDTTokenV2(address(this), admin, 1_000_000 * 10 ** 6, true);
compliantUsdcV2 = new CompliantUSDCTokenV2(address(this), admin, 1_000_000 * 10 ** 6, true);
vault = new StablecoinReserveVault(
admin,
address(officialUsdt),
address(officialUsdc),
address(compliantUsdtV2),
address(compliantUsdcV2)
);
l1Bridge = new CWMultiTokenBridgeL1(address(router), receiveRouterL1, address(0));
l2Bridge = new CWMultiTokenBridgeL2(address(router), receiveRouterL2, address(0));
verifier = new CWReserveVerifier(admin, address(l1Bridge), address(vault), address(0));
wrappedUsdc = new CompliantWrappedToken("Wrapped cUSDC", "cWUSDC", 6, address(this));
uint256 canonicalSupply = compliantUsdcV2.totalSupply();
officialUsdc.mint(admin, canonicalSupply);
vm.prank(admin);
officialUsdc.approve(address(vault), canonicalSupply);
vm.prank(admin);
vault.seedUSDCReserve(canonicalSupply);
vm.startPrank(admin);
compliantUsdtV2.grantRole(compliantUsdtV2.MINTER_ROLE(), address(vault));
compliantUsdtV2.grantRole(compliantUsdtV2.PAUSER_ROLE(), address(vault));
compliantUsdtV2.transferOwnership(address(vault));
compliantUsdcV2.grantRole(compliantUsdcV2.MINTER_ROLE(), address(vault));
compliantUsdcV2.grantRole(compliantUsdcV2.PAUSER_ROLE(), address(vault));
compliantUsdcV2.transferOwnership(address(vault));
vm.stopPrank();
vm.prank(address(this));
compliantUsdcV2.transfer(user, 250e6);
l1Bridge.configureSupportedCanonicalToken(address(compliantUsdcV2), true);
l1Bridge.configureDestination(address(compliantUsdcV2), AVALANCHE_SELECTOR, address(l2Bridge), true);
l1Bridge.setReserveVerifier(address(verifier));
l2Bridge.configureDestination(138, address(l1Bridge), true);
l2Bridge.configureTokenPair(address(compliantUsdcV2), address(wrappedUsdc));
wrappedUsdc.grantRole(wrappedUsdc.MINTER_ROLE(), address(l2Bridge));
wrappedUsdc.grantRole(wrappedUsdc.BURNER_ROLE(), address(l2Bridge));
vm.prank(admin);
verifier.configureToken(
address(compliantUsdcV2),
address(0),
true,
false,
true
);
}
function testVerifierAllowsLockForV2CanonicalTokenBackedByVault() public {
uint256 amount = 25e6;
vm.startPrank(user);
compliantUsdcV2.approve(address(l1Bridge), amount);
l1Bridge.lockAndSend(address(compliantUsdcV2), AVALANCHE_SELECTOR, user, amount);
vm.stopPrank();
assertEq(l1Bridge.lockedBalance(address(compliantUsdcV2)), amount);
assertEq(l1Bridge.totalOutstanding(address(compliantUsdcV2)), amount);
assertEq(l1Bridge.outstandingMinted(address(compliantUsdcV2), AVALANCHE_SELECTOR), amount);
}
function testV2CanonicalTokenCompletesFullTransportRoundTrip() public {
uint256 amount = 25e6;
vm.startPrank(user);
compliantUsdcV2.approve(address(l1Bridge), amount);
bytes32 outboundMessageId =
l1Bridge.lockAndSend(address(compliantUsdcV2), AVALANCHE_SELECTOR, user, amount);
vm.stopPrank();
(bytes memory receiverData, bytes memory outboundData,,) = router.lastMessage();
assertEq(abi.decode(receiverData, (address)), address(l2Bridge));
vm.prank(receiveRouterL2);
l2Bridge.ccipReceive(_message(outboundMessageId, 138, address(l1Bridge), outboundData));
assertEq(wrappedUsdc.balanceOf(user), amount);
assertEq(wrappedUsdc.totalSupply(), amount);
assertEq(l2Bridge.mintedTotal(address(wrappedUsdc)), amount);
assertEq(l2Bridge.burnedTotal(address(wrappedUsdc)), 0);
vm.prank(user);
bytes32 returnMessageId = l2Bridge.burnAndSend(address(wrappedUsdc), 138, user, amount);
assertEq(wrappedUsdc.balanceOf(user), 0);
assertEq(wrappedUsdc.totalSupply(), 0);
assertEq(l2Bridge.mintedTotal(address(wrappedUsdc)), amount);
assertEq(l2Bridge.burnedTotal(address(wrappedUsdc)), amount);
(, bytes memory returnData,,) = router.lastMessage();
vm.prank(receiveRouterL1);
l1Bridge.ccipReceive(_message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData));
assertEq(compliantUsdcV2.balanceOf(user), 250e6);
assertEq(compliantUsdcV2.balanceOf(address(l1Bridge)), 0);
assertEq(l1Bridge.lockedBalance(address(compliantUsdcV2)), 0);
assertEq(l1Bridge.totalOutstanding(address(compliantUsdcV2)), 0);
assertEq(l1Bridge.outstandingMinted(address(compliantUsdcV2), AVALANCHE_SELECTOR), 0);
}
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
});
}
}

View File

@@ -0,0 +1,124 @@
// 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 {USDWPublicWrapVault} from "../../contracts/bridge/integration/USDWPublicWrapVault.sol";
import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol";
contract MockNativeUSDW is ERC20 {
uint8 private immutable _decimalsValue;
constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
_decimalsValue = decimals_;
}
function decimals() public view override returns (uint8) {
return _decimalsValue;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract USDWPublicWrapVaultTest is Test {
address internal admin = address(0xA11CE);
address internal operator = address(0xB0B);
address internal user = address(0xCAFE);
address internal bridgeUser = address(0xD00D);
MockNativeUSDW internal nativeUsdw;
MockNativeUSDW internal strayToken;
CompliantWrappedToken internal wrappedUsdw;
USDWPublicWrapVault internal vault;
function setUp() public {
vm.startPrank(admin);
nativeUsdw = new MockNativeUSDW("USD DWIN", "USDW", 18);
strayToken = new MockNativeUSDW("Stray", "STRAY", 18);
wrappedUsdw = new CompliantWrappedToken("Wrapped cUSDW", "cWUSDW", 6, admin);
vault = new USDWPublicWrapVault(admin, address(nativeUsdw), address(wrappedUsdw));
vault.grantRole(vault.RESERVE_OPERATOR_ROLE(), operator);
wrappedUsdw.grantRole(wrappedUsdw.MINTER_ROLE(), address(vault));
wrappedUsdw.grantRole(wrappedUsdw.BURNER_ROLE(), address(vault));
vm.stopPrank();
nativeUsdw.mint(user, 100e18);
nativeUsdw.mint(operator, 100e18);
strayToken.mint(address(vault), 5e18);
}
function testWrapNormalizesNativeAmountToWrappedDecimals() public {
vm.startPrank(user);
nativeUsdw.approve(address(vault), 5e18);
uint256 wrappedAmount = vault.wrap(5e18, user);
vm.stopPrank();
assertEq(wrappedAmount, 5e6);
assertEq(wrappedUsdw.balanceOf(user), 5e6);
assertEq(nativeUsdw.balanceOf(address(vault)), 5e18);
assertEq(nativeUsdw.balanceOf(user), 95e18);
}
function testUnwrapBurnsWrappedAndReleasesNativeUsd() public {
vm.startPrank(user);
nativeUsdw.approve(address(vault), 7e18);
vault.wrap(7e18, user);
uint256 unwrapped = vault.unwrap(2e6, user);
vm.stopPrank();
assertEq(unwrapped, 2e18);
assertEq(wrappedUsdw.balanceOf(user), 5e6);
assertEq(nativeUsdw.balanceOf(user), 95e18);
assertEq(nativeUsdw.balanceOf(address(vault)), 5e18);
}
function testBridgeMintedSupplyCanUnwrapAgainstSeededLiquidity() public {
vm.prank(operator);
nativeUsdw.approve(address(vault), 20e18);
vm.prank(operator);
vault.seedLiquidity(20e18);
vm.prank(admin);
wrappedUsdw.mint(bridgeUser, 3e6);
vm.prank(bridgeUser);
uint256 released = vault.unwrap(3e6, bridgeUser);
assertEq(released, 3e18);
assertEq(nativeUsdw.balanceOf(bridgeUser), 3e18);
assertEq(nativeUsdw.balanceOf(address(vault)), 17e18);
assertEq(wrappedUsdw.balanceOf(bridgeUser), 0);
}
function testWrapRejectsNonCanonicalNativeAmount() public {
vm.startPrank(user);
nativeUsdw.approve(address(vault), 1e18 + 1);
vm.expectRevert(abi.encodeWithSelector(USDWPublicWrapVault.NonCanonicalAmount.selector, 1e18 + 1));
vault.wrap(1e18 + 1, user);
vm.stopPrank();
}
function testPauseBlocksWrapAndUnwrap() public {
vm.prank(admin);
vault.pause();
vm.startPrank(user);
nativeUsdw.approve(address(vault), 1e18);
vm.expectRevert();
vault.wrap(1e18, user);
vm.stopPrank();
}
function testRecoverNonUnderlyingTokenButProtectNativeReserve() public {
vm.prank(admin);
vault.recoverNonUnderlyingToken(address(strayToken), admin, 5e18);
assertEq(strayToken.balanceOf(admin), 5e18);
vm.prank(admin);
vm.expectRevert(USDWPublicWrapVault.UnderlyingTokenProtected.selector);
vault.recoverNonUnderlyingToken(address(nativeUsdw), admin, 1);
}
}

View File

@@ -0,0 +1,271 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../../../contracts/bridge/atomic/AtomicBridgeCoordinator.sol";
import "../../../contracts/bridge/atomic/AtomicFeePolicy.sol";
import "../../../contracts/bridge/atomic/AtomicFulfillerRegistry.sol";
import "../../../contracts/bridge/atomic/AtomicLiquidityVault.sol";
import "../../../contracts/bridge/atomic/AtomicObligationEscrow.sol";
import "../../../contracts/bridge/atomic/AtomicQuoteEngine.sol";
import "../../../contracts/bridge/atomic/AtomicSettlementRouter.sol";
import "../../../contracts/bridge/atomic/AtomicSlashingManager.sol";
import "../../../contracts/bridge/atomic/AtomicTypes.sol";
import "../../../contracts/bridge/atomic/interfaces/IAtomicSettlementAdapter.sol";
import "../../dbis/MockMintableToken.sol";
contract MockAtomicSettlementAdapter is IAtomicSettlementAdapter {
address public lastToken;
uint256 public lastAmount;
address public lastRecipient;
bytes public lastData;
function executeSettlement(
bytes32 obligationId,
address token,
uint256 amount,
address recipient,
bytes calldata data
) external payable returns (bytes32 settlementId) {
lastToken = token;
lastAmount = amount;
lastRecipient = recipient;
lastData = data;
settlementId = keccak256(abi.encode(obligationId, token, amount, recipient, data, block.timestamp));
}
}
contract AtomicBridgeCoordinatorTest is Test {
bytes32 internal constant MOCK_SETTLEMENT_MODE = keccak256("MOCK_SETTLEMENT_MODE");
MockMintableToken internal sourceToken;
MockMintableToken internal destinationToken;
MockMintableToken internal bondToken;
AtomicLiquidityVault internal vault;
AtomicFulfillerRegistry internal registry;
AtomicFeePolicy internal feePolicy;
AtomicObligationEscrow internal escrow;
AtomicSettlementRouter internal router;
AtomicSlashingManager internal slashingManager;
AtomicBridgeCoordinator internal coordinator;
AtomicQuoteEngine internal quoteEngine;
MockAtomicSettlementAdapter internal settlementAdapter;
address internal user = address(0x1111);
address internal fulfiller = address(0x2222);
address internal protocolTreasury = address(0x3333);
bytes32 internal corridorId;
function setUp() public {
sourceToken = new MockMintableToken("Chain 138 USD", "cUSDC", 6, address(this));
destinationToken = new MockMintableToken("Mainnet Wrapped USD", "cWUSDC", 6, address(this));
bondToken = new MockMintableToken("Bond USDC", "bUSDC", 6, address(this));
vault = new AtomicLiquidityVault(address(this));
registry = new AtomicFulfillerRegistry(address(bondToken), address(this));
feePolicy = new AtomicFeePolicy(address(this));
escrow = new AtomicObligationEscrow(address(this));
router = new AtomicSettlementRouter(address(this));
slashingManager = new AtomicSlashingManager(address(registry), address(this));
coordinator = new AtomicBridgeCoordinator(
address(vault),
address(registry),
address(escrow),
address(router),
address(feePolicy),
address(slashingManager),
protocolTreasury,
address(this)
);
quoteEngine = new AtomicQuoteEngine(
address(coordinator),
address(vault),
address(registry),
address(feePolicy)
);
settlementAdapter = new MockAtomicSettlementAdapter();
vault.grantRole(vault.COORDINATOR_ROLE(), address(coordinator));
vault.grantRole(vault.RECONCILER_ROLE(), address(coordinator));
registry.grantRole(registry.COORDINATOR_ROLE(), address(coordinator));
registry.grantRole(registry.SLASHER_ROLE(), address(slashingManager));
escrow.grantRole(escrow.COORDINATOR_ROLE(), address(coordinator));
router.grantRole(router.COORDINATOR_ROLE(), address(coordinator));
slashingManager.grantRole(slashingManager.COORDINATOR_ROLE(), address(coordinator));
router.setAdapter(MOCK_SETTLEMENT_MODE, address(settlementAdapter));
corridorId = coordinator.getCorridorId(138, 1, address(sourceToken), address(destinationToken));
coordinator.configureCorridor(
AtomicTypes.CorridorConfig({
enabled: true,
degraded: false,
sourceChain: 138,
destinationChain: 1,
assetIn: address(sourceToken),
assetOut: address(destinationToken),
maxNotional: 500_000e6,
maxReservedBps: 5_000,
targetBuffer: 10_000e6,
maxSettlementBacklog: 250_000e6,
maxOracleDriftBps: 500,
fulfilmentTimeout: 1 days,
settlementTimeout: 2 days,
defaultSettlementMode: MOCK_SETTLEMENT_MODE
})
);
feePolicy.setCorridorPolicy(corridorId, 100, 50, 12_000, 1_000, 1 days, 2 days);
vault.setTargetBuffer(corridorId, address(destinationToken), 10_000e6);
registry.setFulfillerActive(fulfiller, true);
registry.setCorridorAuthorization(fulfiller, corridorId, true);
destinationToken.mint(address(this), 100_000e6);
destinationToken.approve(address(vault), type(uint256).max);
vault.fundCorridor(corridorId, address(destinationToken), 100_000e6);
bondToken.mint(fulfiller, 100_000e6);
vm.startPrank(fulfiller);
bondToken.approve(address(registry), type(uint256).max);
registry.depositBond(50_000e6);
vm.stopPrank();
sourceToken.mint(user, 50_000e6);
vm.prank(user);
sourceToken.approve(address(escrow), type(uint256).max);
}
function testCreateIntentReservesDestinationLiquidity() public {
bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 1 hours);
AtomicTypes.AtomicObligation memory obligation = coordinator.getObligation(obligationId);
assertEq(uint8(obligation.status), uint8(AtomicTypes.ObligationStatus.IntentCreated));
assertEq(obligation.sourceEscrow, 1_000e6);
assertEq(obligation.destinationReserve, 900e6);
AtomicTypes.CorridorLiquidityState memory state =
vault.getCorridorLiquidityState(corridorId, address(destinationToken));
assertEq(state.totalLiquidity, 100_000e6);
assertEq(state.reservedLiquidity, 900e6);
assertEq(state.freeLiquidity, 99_100e6);
}
function testQuoteReflectsExecutionReadyPath() public {
AtomicTypes.AtomicQuote memory q = quoteEngine.quote(corridorId, 1_000e6, 900e6, fulfiller);
assertEq(uint8(q.routeClass), uint8(AtomicTypes.RouteClass.ExecutionReady));
assertTrue(q.fulfillerAuthorized);
assertTrue(q.fulfillerBondSufficient);
assertEq(q.protocolFee, 5e6);
assertEq(q.fulfillerFee, 10e6);
assertEq(q.requiredBond, 1_080e6);
}
function testCommitFailsWhenBondIsInsufficient() public {
address weakFulfiller = address(0x4444);
registry.setFulfillerActive(weakFulfiller, true);
registry.setCorridorAuthorization(weakFulfiller, corridorId, true);
bondToken.mint(weakFulfiller, 100e6);
vm.startPrank(weakFulfiller);
bondToken.approve(address(registry), type(uint256).max);
registry.depositBond(100e6);
vm.stopPrank();
bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 1 hours);
vm.prank(weakFulfiller);
vm.expectRevert(AtomicFulfillerRegistry.InsufficientAvailableBond.selector);
coordinator.submitCommitment(obligationId, bytes32(0));
}
function testSuccessfulSettlementReleasesBondAndReplenishesLiquidity() public {
bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 1 hours);
vm.prank(fulfiller);
coordinator.submitCommitment(obligationId, bytes32(0));
assertEq(destinationToken.balanceOf(user), 900e6);
AtomicTypes.CorridorLiquidityState memory postFulfill =
vault.getCorridorLiquidityState(corridorId, address(destinationToken));
assertEq(postFulfill.totalLiquidity, 99_100e6);
assertEq(postFulfill.settlementBacklog, 900e6);
bytes memory settlementData = abi.encodePacked(bytes32(uint256(123)));
bytes32 settlementId = coordinator.initiateSettlement(obligationId, settlementData);
assertTrue(settlementId != bytes32(0));
assertEq(settlementAdapter.lastToken(), address(sourceToken));
assertEq(settlementAdapter.lastAmount(), 985e6);
assertEq(settlementAdapter.lastRecipient(), user);
assertEq(sourceToken.balanceOf(protocolTreasury), 5e6);
assertEq(sourceToken.balanceOf(fulfiller), 10e6);
destinationToken.mint(address(this), 900e6);
destinationToken.approve(address(vault), 900e6);
coordinator.confirmSettlement(obligationId, 900e6);
AtomicTypes.AtomicObligation memory obligation = coordinator.getObligation(obligationId);
assertEq(uint8(obligation.status), uint8(AtomicTypes.ObligationStatus.Settled));
assertEq(registry.lockedBond(fulfiller), 0);
AtomicTypes.CorridorLiquidityState memory postSettlement =
vault.getCorridorLiquidityState(corridorId, address(destinationToken));
assertEq(postSettlement.totalLiquidity, 100_000e6);
assertEq(postSettlement.settlementBacklog, 0);
}
function testExpiredIntentRefundsAndReleasesReservation() public {
uint256 payerBalanceBefore = sourceToken.balanceOf(user);
bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 30 minutes);
vm.warp(block.timestamp + 31 minutes);
coordinator.refundExpiredIntent(obligationId);
AtomicTypes.AtomicObligation memory obligation = coordinator.getObligation(obligationId);
assertEq(uint8(obligation.status), uint8(AtomicTypes.ObligationStatus.Refunded));
assertEq(sourceToken.balanceOf(user), payerBalanceBefore);
AtomicTypes.CorridorLiquidityState memory state =
vault.getCorridorLiquidityState(corridorId, address(destinationToken));
assertEq(state.reservedLiquidity, 0);
}
function testSettlementTimeoutSlashesBondAndDegradesCorridor() public {
bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 1 hours);
vm.prank(fulfiller);
coordinator.submitCommitment(obligationId, bytes32(0));
coordinator.initiateSettlement(obligationId, abi.encodePacked(bytes32(uint256(456))));
uint256 treasuryBondBefore = bondToken.balanceOf(protocolTreasury);
vm.warp(block.timestamp + 3 days);
coordinator.handleSettlementTimeout(obligationId);
AtomicTypes.AtomicObligation memory obligation = coordinator.getObligation(obligationId);
assertEq(uint8(obligation.status), uint8(AtomicTypes.ObligationStatus.Slashed));
AtomicTypes.CorridorConfig memory cfg = coordinator.getCorridorConfig(corridorId);
assertTrue(cfg.degraded);
assertEq(bondToken.balanceOf(protocolTreasury), treasuryBondBefore + 1_080e6);
}
function testConcurrentReservationsDoNotOverReserveLiquidity() public {
_createIntent(10_000e6, 40_000e6, block.timestamp + 1 hours);
_createIntent(10_000e6, 10_000e6, block.timestamp + 1 hours);
vm.expectRevert(AtomicBridgeCoordinator.ReservedLiquidityLimitExceeded.selector);
_createIntent(10_000e6, 5_000e6, block.timestamp + 1 hours);
}
function _createIntent(uint256 amountIn, uint256 minAmountOut, uint256 deadline) internal returns (bytes32) {
vm.prank(user);
return coordinator.createIntent(
AtomicBridgeCoordinator.CreateIntentParams({
sourceChain: 138,
destinationChain: 1,
assetIn: address(sourceToken),
assetOut: address(destinationToken),
amountIn: amountIn,
minAmountOut: minAmountOut,
recipient: user,
deadline: deadline,
routeId: corridorId
})
);
}
}

View File

@@ -0,0 +1,230 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import "../../../contracts/bridge/trustless/EnhancedSwapRouterV2.sol";
import "../../../contracts/bridge/trustless/RouteTypesV2.sol";
import "../../../contracts/bridge/trustless/adapters/UniswapV3RouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/BalancerRouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/CurveRouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/OneInchRouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/pilot/Chain138PilotDexVenues.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract PilotVenueToken is ERC20 {
constructor(string memory name_, string memory symbol_, uint256 supply) ERC20(name_, symbol_) {
_mint(msg.sender, supply);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract Chain138PilotDexVenuesTest is Test {
PilotVenueToken internal weth;
PilotVenueToken internal usdt;
PilotVenueToken internal usdc;
PilotVenueToken internal dai;
Chain138PilotUniswapV3Router internal uniswapRouter;
Chain138PilotBalancerVault internal balancerVault;
Chain138PilotCurve3Pool internal curvePool;
Chain138PilotOneInchAggregationRouter internal oneInchRouter;
EnhancedSwapRouterV2 internal router;
address internal user = address(0x1234);
bytes32 internal constant BALANCER_POOL_ID = keccak256("chain138-pilot-balancer-weth-usdc");
function setUp() public {
weth = new PilotVenueToken("Wrapped Ether", "WETH", 10_000 ether);
usdt = new PilotVenueToken("Tether", "USDT", 50_000_000 * 1e6);
usdc = new PilotVenueToken("USD Coin", "USDC", 50_000_000 * 1e6);
dai = new PilotVenueToken("Dai", "DAI", 10_000_000 ether);
uniswapRouter = new Chain138PilotUniswapV3Router();
balancerVault = new Chain138PilotBalancerVault();
curvePool = new Chain138PilotCurve3Pool(address(usdt), address(usdc), address(0), 4);
oneInchRouter = new Chain138PilotOneInchAggregationRouter();
router = new EnhancedSwapRouterV2(address(weth), address(usdt), address(usdc), address(dai));
router.setProviderAdapter(RouteTypesV2.Provider.UniswapV3, address(new UniswapV3RouteExecutorAdapter()));
router.setProviderAdapter(RouteTypesV2.Provider.Balancer, address(new BalancerRouteExecutorAdapter()));
router.setProviderAdapter(RouteTypesV2.Provider.Curve, address(new CurveRouteExecutorAdapter()));
router.setProviderAdapter(RouteTypesV2.Provider.OneInch, address(new OneInchRouteExecutorAdapter()));
router.setProviderEnabled(RouteTypesV2.Provider.OneInch, true);
weth.approve(address(uniswapRouter), type(uint256).max);
usdt.approve(address(uniswapRouter), type(uint256).max);
usdc.approve(address(uniswapRouter), type(uint256).max);
weth.approve(address(balancerVault), type(uint256).max);
usdt.approve(address(balancerVault), type(uint256).max);
usdc.approve(address(balancerVault), type(uint256).max);
usdt.approve(address(curvePool), type(uint256).max);
usdc.approve(address(curvePool), type(uint256).max);
weth.approve(address(oneInchRouter), type(uint256).max);
usdt.approve(address(oneInchRouter), type(uint256).max);
usdc.approve(address(oneInchRouter), type(uint256).max);
uniswapRouter.seedPair(address(weth), address(usdt), 3000, 100 ether, 210_000 * 1e6);
uniswapRouter.seedPair(address(weth), address(usdc), 3000, 100 ether, 210_000 * 1e6);
balancerVault.seedPool(BALANCER_POOL_ID, address(weth), address(usdc), 100 ether, 210_000 * 1e6, 30);
curvePool.fund(500_000 * 1e6, 500_000 * 1e6, 0);
oneInchRouter.seedRoute(address(weth), address(usdt), 100 ether, 210_000 * 1e6, 35);
router.setProviderRoute(
address(weth),
address(usdt),
RouteTypesV2.Provider.UniswapV3,
address(uniswapRouter),
abi.encode(bytes(""), uint24(3000), address(uniswapRouter), false),
true
);
router.setProviderRoute(
address(weth),
address(usdc),
RouteTypesV2.Provider.Balancer,
address(balancerVault),
abi.encode(BALANCER_POOL_ID),
true
);
router.setProviderRoute(
address(usdt),
address(usdc),
RouteTypesV2.Provider.Curve,
address(curvePool),
abi.encode(int128(0), int128(1), false),
true
);
router.setProviderRoute(
address(weth),
address(usdt),
RouteTypesV2.Provider.OneInch,
address(oneInchRouter),
abi.encode(address(oneInchRouter), address(oneInchRouter), bytes("")),
true
);
weth.mint(user, 10 ether);
usdt.mint(user, 50_000 * 1e6);
}
function testUniswapPilotQuotesAndExecutes() public {
uint256 quote = uniswapRouter.quoteExactInputSingle(address(weth), address(usdt), 3000, 1 ether, 0);
assertGt(quote, 0);
vm.startPrank(user);
weth.approve(address(router), 1 ether);
RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
legs[0] = RouteTypesV2.RouteLeg({
provider: RouteTypesV2.Provider.UniswapV3,
tokenIn: address(weth),
tokenOut: address(usdt),
amountSource: RouteTypesV2.AmountSource.UserInput,
minAmountOut: quote - 1,
target: address(uniswapRouter),
providerData: abi.encode(bytes(""), uint24(3000), address(uniswapRouter), false)
});
RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
chainId: block.chainid,
inputToken: address(weth),
outputToken: address(usdt),
amountIn: 1 ether,
minAmountOut: quote - 1,
recipient: user,
deadline: block.timestamp + 300,
legs: legs
});
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertEq(amountOut, quote);
}
function testBalancerPilotExecutes() public {
vm.startPrank(user);
weth.approve(address(router), 1 ether);
RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
legs[0] = RouteTypesV2.RouteLeg({
provider: RouteTypesV2.Provider.Balancer,
tokenIn: address(weth),
tokenOut: address(usdc),
amountSource: RouteTypesV2.AmountSource.UserInput,
minAmountOut: 1_000 * 1e6,
target: address(balancerVault),
providerData: abi.encode(BALANCER_POOL_ID)
});
RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
chainId: block.chainid,
inputToken: address(weth),
outputToken: address(usdc),
amountIn: 1 ether,
minAmountOut: 1_000 * 1e6,
recipient: user,
deadline: block.timestamp + 300,
legs: legs
});
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertGt(amountOut, 0);
}
function testCurvePilotExecutes() public {
vm.startPrank(user);
usdt.approve(address(router), 10_000 * 1e6);
RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
legs[0] = RouteTypesV2.RouteLeg({
provider: RouteTypesV2.Provider.Curve,
tokenIn: address(usdt),
tokenOut: address(usdc),
amountSource: RouteTypesV2.AmountSource.UserInput,
minAmountOut: 9_900 * 1e6,
target: address(curvePool),
providerData: abi.encode(int128(0), int128(1), false)
});
RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
chainId: block.chainid,
inputToken: address(usdt),
outputToken: address(usdc),
amountIn: 10_000 * 1e6,
minAmountOut: 9_900 * 1e6,
recipient: user,
deadline: block.timestamp + 300,
legs: legs
});
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertGt(amountOut, 0);
}
function testOneInchPilotExecutes() public {
vm.startPrank(user);
weth.approve(address(router), 1 ether);
RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
legs[0] = RouteTypesV2.RouteLeg({
provider: RouteTypesV2.Provider.OneInch,
tokenIn: address(weth),
tokenOut: address(usdt),
amountSource: RouteTypesV2.AmountSource.UserInput,
minAmountOut: 1_000 * 1e6,
target: address(oneInchRouter),
providerData: abi.encode(address(oneInchRouter), address(oneInchRouter), bytes(""))
});
RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
chainId: block.chainid,
inputToken: address(weth),
outputToken: address(usdt),
amountIn: 1 ether,
minAmountOut: 1_000 * 1e6,
recipient: user,
deadline: block.timestamp + 300,
legs: legs
});
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertGt(amountOut, 0);
}
}

View File

@@ -0,0 +1,686 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import "../../../contracts/bridge/trustless/EnhancedSwapRouterV2.sol";
import "../../../contracts/bridge/trustless/IntentBridgeCoordinatorV2.sol";
import "../../../contracts/bridge/trustless/RouteTypesV2.sol";
import "../../../contracts/bridge/trustless/adapters/DodoRouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/DodoV3RouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/UniswapV3RouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/BalancerRouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/adapters/CurveRouteExecutorAdapter.sol";
import "../../../contracts/bridge/trustless/interfaces/IBalancerVault.sol";
import "../../../contracts/bridge/trustless/interfaces/IBridgeIntentExecutor.sol";
import "../../../contracts/bridge/trustless/interfaces/ISwapRouter.sol";
import "../../../contracts/liquidity/interfaces/ILiquidityProvider.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockERC20V2 is ERC20 {
constructor(string memory name_, string memory symbol_, uint256 supply) ERC20(name_, symbol_) {
_mint(msg.sender, supply);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract MockDodoProviderV2 is ILiquidityProvider {
mapping(address => mapping(address => bool)) public supported;
mapping(address => mapping(address => uint256)) public quoteAmount;
function setSupport(address tokenIn, address tokenOut, bool isSupported) external {
supported[tokenIn][tokenOut] = isSupported;
}
function setQuote(address tokenIn, address tokenOut, uint256 amountOut) external {
quoteAmount[tokenIn][tokenOut] = amountOut;
}
function getQuote(address tokenIn, address tokenOut, uint256) external view returns (uint256 amountOut, uint256 slippageBps) {
if (!supported[tokenIn][tokenOut]) return (0, 10000);
return (quoteAmount[tokenIn][tokenOut], 30);
}
function executeSwap(address tokenIn, address tokenOut, uint256 amountIn, uint256) external returns (uint256 amountOut) {
require(supported[tokenIn][tokenOut], "unsupported");
amountOut = quoteAmount[tokenIn][tokenOut];
ERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
ERC20(tokenOut).transfer(msg.sender, amountOut);
}
function supportsTokenPair(address tokenIn, address tokenOut) external view returns (bool) {
return supported[tokenIn][tokenOut];
}
function providerName() external pure returns (string memory) {
return "MockDodo";
}
function estimateGas(address, address, uint256) external pure returns (uint256) {
return 140000;
}
}
contract MockUniswapQuoterV2 {
mapping(bytes32 => uint256) public quotes;
function setQuote(address tokenIn, address tokenOut, uint24 fee, uint256 amountOut) external {
quotes[keccak256(abi.encode(tokenIn, tokenOut, fee))] = amountOut;
}
function quoteExactInputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint256,
uint160
) external view returns (uint256) {
return quotes[keccak256(abi.encode(tokenIn, tokenOut, fee))];
}
function quoteExactInput(bytes calldata, uint256) external pure returns (uint256) {
return 0;
}
}
contract MockUniswapRouterV2 {
mapping(bytes32 => uint256) public quotes;
function setQuote(address tokenIn, address tokenOut, uint24 fee, uint256 amountOut) external {
quotes[keccak256(abi.encode(tokenIn, tokenOut, fee))] = amountOut;
}
function exactInputSingle(
ISwapRouter.ExactInputSingleParams calldata params
) external returns (uint256 amountOut) {
amountOut = quotes[keccak256(abi.encode(params.tokenIn, params.tokenOut, params.fee))];
ERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn);
ERC20(params.tokenOut).transfer(params.recipient, amountOut);
}
function exactInput(
ISwapRouter.ExactInputParams calldata
) external pure returns (uint256) {
revert("path unsupported in mock");
}
}
contract MockBalancerVaultV2 {
bytes32 public poolId;
address[] public tokens;
uint256[] public balances;
function setPool(bytes32 newPoolId, address tokenA, address tokenB, uint256 balanceA, uint256 balanceB) external {
poolId = newPoolId;
tokens = [tokenA, tokenB];
balances = [balanceA, balanceB];
}
function swap(
IBalancerVault.SingleSwap memory singleSwap,
IBalancerVault.FundManagement memory funds,
uint256,
uint256
) external returns (uint256 amountCalculated) {
require(singleSwap.poolId == poolId, "bad pool");
ERC20(singleSwap.assetIn).transferFrom(msg.sender, address(this), singleSwap.amount);
amountCalculated = (singleSwap.amount * balances[1]) / balances[0];
amountCalculated = (amountCalculated * 9950) / 10000;
ERC20(singleSwap.assetOut).transfer(funds.recipient, amountCalculated);
}
function getPool(bytes32) external pure returns (address poolAddress, uint8 specialization) {
return (address(0xBEEF), 0);
}
function queryBatchSwap(
IBalancerVault.SwapKind,
IBalancerVault.SingleSwap[] memory,
address[] memory
) external pure returns (int256[] memory assetDeltas) {
assetDeltas = new int256[](0);
}
function getPoolTokens(bytes32 requestedPoolId)
external
view
returns (
address[] memory,
uint256[] memory,
uint256
)
{
require(requestedPoolId == poolId, "bad pool");
return (tokens, balances, block.number);
}
}
contract MockCurvePoolV2 {
mapping(bytes32 => uint256) public quotes;
function setQuote(int128 i, int128 j, uint256 amountOut) external {
quotes[keccak256(abi.encode(i, j))] = amountOut;
}
function exchange(int128 i, int128 j, uint256 dx, uint256) external returns (uint256) {
uint256 amountOut = quotes[keccak256(abi.encode(i, j))];
dx;
return amountOut;
}
function exchange_underlying(int128 i, int128 j, uint256 dx, uint256) external returns (uint256) {
uint256 amountOut = quotes[keccak256(abi.encode(i, j))];
dx;
return amountOut;
}
function get_dy(int128 i, int128 j, uint256) external view returns (uint256) {
return quotes[keccak256(abi.encode(i, j))];
}
function coins(uint256) external pure returns (address) {
return address(0);
}
}
contract MockBridgeIntentExecutorV2 is IBridgeIntentExecutor {
event Bridged(bytes32 indexed bridgeType, address indexed token, uint256 amount, address recipient);
function validateBridge(
bytes32,
bytes calldata,
address token,
uint256 amount,
address recipient
) external pure returns (bool ok, string memory reason) {
if (token == address(0) || amount == 0 || recipient == address(0)) {
return (false, "invalid bridge request");
}
return (true, "");
}
function executeBridge(
bytes32 bridgeType,
bytes calldata,
address token,
uint256 amount,
address recipient
) external payable returns (bytes32 referenceId) {
ERC20(token).transferFrom(msg.sender, address(this), amount);
emit Bridged(bridgeType, token, amount, recipient);
return keccak256(abi.encode(bridgeType, token, amount, recipient));
}
}
contract MockD3ApproveV2 {
function claimTokens(address token, address who, address dest, uint256 amount) external {
IERC20(token).transferFrom(who, dest, amount);
}
}
contract MockD3ApproveProxyV2 {
address public immutable _DODO_APPROVE_;
mapping(address => bool) public allowedProxy;
constructor(address approve) {
_DODO_APPROVE_ = approve;
}
function setAllowedProxy(address proxy, bool allowed) external {
allowedProxy[proxy] = allowed;
}
function claimTokens(address token, address who, address dest, uint256 amount) external {
require(allowedProxy[msg.sender], "NOT_ALLOWED_PROXY");
MockD3ApproveV2(_DODO_APPROVE_).claimTokens(token, who, dest, amount);
}
}
interface IMockD3SwapCallbackV2 {
function d3MMSwapCallBack(address token, uint256 value, bytes calldata data) external;
}
contract MockD3MMV2 {
mapping(address => mapping(address => uint256)) public quoteAmount;
function setQuote(address tokenIn, address tokenOut, uint256 amountOut) external {
quoteAmount[tokenIn][tokenOut] = amountOut;
}
function querySellTokens(
address fromToken,
address toToken,
uint256 fromAmount
) external view returns (uint256 payFromAmount, uint256 receiveToAmount, uint256 vusdAmount, uint256 swapFee, uint256 mtFee) {
payFromAmount = fromAmount;
receiveToAmount = quoteAmount[fromToken][toToken];
vusdAmount = 0;
swapFee = 0;
mtFee = 0;
}
function sellToken(
address to,
address fromToken,
address toToken,
uint256 fromAmount,
uint256 minReceiveAmount,
bytes calldata data
) external returns (uint256 receiveToAmount) {
receiveToAmount = quoteAmount[fromToken][toToken];
require(receiveToAmount >= minReceiveAmount, "insufficient output");
IMockD3SwapCallbackV2(msg.sender).d3MMSwapCallBack(fromToken, fromAmount, data);
IERC20(toToken).transfer(to, receiveToAmount);
}
}
contract MockD3ProxyV2 {
address public immutable _DODO_APPROVE_PROXY_;
struct SwapCallbackData {
bytes data;
address payer;
}
constructor(address approveProxy) {
_DODO_APPROVE_PROXY_ = approveProxy;
}
function sellTokens(
address pool,
address to,
address fromToken,
address toToken,
uint256 fromAmount,
uint256 minReceiveAmount,
bytes calldata data,
uint256 deadLine
) external payable returns (uint256 receiveToAmount) {
require(deadLine >= block.timestamp, "expired");
SwapCallbackData memory swapData = SwapCallbackData({data: data, payer: msg.sender});
receiveToAmount = MockD3MMV2(pool).sellToken(
to,
fromToken,
toToken,
fromAmount,
minReceiveAmount,
abi.encode(swapData)
);
}
function d3MMSwapCallBack(address token, uint256 value, bytes calldata callbackData) external {
SwapCallbackData memory decoded = abi.decode(callbackData, (SwapCallbackData));
MockD3ApproveProxyV2(_DODO_APPROVE_PROXY_).claimTokens(token, decoded.payer, msg.sender, value);
}
}
contract EnhancedSwapRouterV2Test is Test {
EnhancedSwapRouterV2 internal router;
IntentBridgeCoordinatorV2 internal coordinator;
DodoRouteExecutorAdapter internal dodoAdapter;
DodoV3RouteExecutorAdapter internal dodoV3Adapter;
UniswapV3RouteExecutorAdapter internal uniswapAdapter;
BalancerRouteExecutorAdapter internal balancerAdapter;
CurveRouteExecutorAdapter internal curveAdapter;
MockBridgeIntentExecutorV2 internal bridgeExecutor;
MockERC20V2 internal weth;
MockERC20V2 internal usdt;
MockERC20V2 internal usdc;
MockERC20V2 internal dai;
MockDodoProviderV2 internal dodoProvider;
MockUniswapQuoterV2 internal uniswapQuoter;
MockUniswapRouterV2 internal uniswapRouter;
MockBalancerVaultV2 internal balancerVault;
MockCurvePoolV2 internal curvePool;
MockD3ApproveV2 internal d3Approve;
MockD3ApproveProxyV2 internal d3ApproveProxy;
MockD3MMV2 internal d3Pool;
MockD3ProxyV2 internal d3Proxy;
address internal user = address(0x1111);
function setUp() public {
weth = new MockERC20V2("Wrapped Ether", "WETH", 1_000_000 ether);
usdt = new MockERC20V2("Tether", "USDT", 1_000_000 ether);
usdc = new MockERC20V2("USD Coin", "USDC", 1_000_000 ether);
dai = new MockERC20V2("Dai", "DAI", 1_000_000 ether);
dodoProvider = new MockDodoProviderV2();
uniswapQuoter = new MockUniswapQuoterV2();
uniswapRouter = new MockUniswapRouterV2();
balancerVault = new MockBalancerVaultV2();
curvePool = new MockCurvePoolV2();
dodoAdapter = new DodoRouteExecutorAdapter();
dodoV3Adapter = new DodoV3RouteExecutorAdapter();
uniswapAdapter = new UniswapV3RouteExecutorAdapter();
balancerAdapter = new BalancerRouteExecutorAdapter();
curveAdapter = new CurveRouteExecutorAdapter();
bridgeExecutor = new MockBridgeIntentExecutorV2();
d3Approve = new MockD3ApproveV2();
d3ApproveProxy = new MockD3ApproveProxyV2(address(d3Approve));
d3Pool = new MockD3MMV2();
d3Proxy = new MockD3ProxyV2(address(d3ApproveProxy));
router = new EnhancedSwapRouterV2(address(weth), address(usdt), address(usdc), address(dai));
coordinator = new IntentBridgeCoordinatorV2(address(router));
router.setProviderAdapter(RouteTypesV2.Provider.Dodo, address(dodoAdapter));
router.setProviderAdapter(RouteTypesV2.Provider.DodoV3, address(dodoV3Adapter));
router.setProviderAdapter(RouteTypesV2.Provider.UniswapV3, address(uniswapAdapter));
router.setProviderAdapter(RouteTypesV2.Provider.Balancer, address(balancerAdapter));
router.setProviderAdapter(RouteTypesV2.Provider.Curve, address(curveAdapter));
coordinator.setBridgeExecutor(bytes32("CCIP"), address(bridgeExecutor));
weth.mint(user, 100 ether);
usdt.mint(address(dodoProvider), 1_000_000 ether);
usdc.mint(address(dodoProvider), 1_000_000 ether);
usdc.mint(address(uniswapRouter), 1_000_000 ether);
usdc.mint(address(balancerVault), 1_000_000 ether);
usdc.mint(address(curveAdapter), 1_000_000 ether);
usdt.mint(address(d3Pool), 1_000_000 ether);
dodoProvider.setSupport(address(weth), address(usdt), true);
dodoProvider.setQuote(address(weth), address(usdt), 1_800 ether);
dodoProvider.setSupport(address(usdt), address(usdc), true);
dodoProvider.setQuote(address(usdt), address(usdc), 1_790 ether);
d3Pool.setQuote(address(weth), address(usdt), 2_100 ether);
d3Pool.setQuote(address(usdt), address(weth), 0.095 ether);
d3ApproveProxy.setAllowedProxy(address(d3Proxy), true);
router.setProviderRoute(
address(weth),
address(usdt),
RouteTypesV2.Provider.Dodo,
address(dodoProvider),
abi.encode(address(0xA11CE)),
true
);
router.setProviderRoute(
address(usdt),
address(usdc),
RouteTypesV2.Provider.Dodo,
address(dodoProvider),
abi.encode(address(0xB0B)),
true
);
uniswapQuoter.setQuote(address(weth), address(usdc), 3000, 1_700 ether);
uniswapRouter.setQuote(address(weth), address(usdc), 3000, 1_700 ether);
router.setProviderRoute(
address(weth),
address(usdc),
RouteTypesV2.Provider.UniswapV3,
address(uniswapRouter),
abi.encode(bytes(""), uint24(3000), address(uniswapQuoter), false),
true
);
bytes32 poolId = keccak256("balancer-weth-usdc");
balancerVault.setPool(poolId, address(weth), address(usdc), 100 ether, 160_000 ether);
router.setProviderRoute(
address(weth),
address(usdc),
RouteTypesV2.Provider.Balancer,
address(balancerVault),
abi.encode(poolId),
true
);
curvePool.setQuote(0, 1, 1_000 ether);
router.setProviderRoute(
address(usdt),
address(usdc),
RouteTypesV2.Provider.Curve,
address(curvePool),
abi.encode(int128(0), int128(1), false),
true
);
}
function _singleLegPlan(
RouteTypesV2.Provider provider,
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 minAmountOut,
address target,
bytes memory providerData
) internal view returns (RouteTypesV2.RoutePlan memory plan) {
RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
legs[0] = RouteTypesV2.RouteLeg({
provider: provider,
tokenIn: tokenIn,
tokenOut: tokenOut,
amountSource: RouteTypesV2.AmountSource.UserInput,
minAmountOut: minAmountOut,
target: target,
providerData: providerData
});
plan = RouteTypesV2.RoutePlan({
chainId: block.chainid,
inputToken: tokenIn,
outputToken: tokenOut,
amountIn: amountIn,
minAmountOut: minAmountOut,
recipient: user,
deadline: block.timestamp + 1 hours,
legs: legs
});
}
function testDodoDirectExecution() public {
vm.startPrank(user);
weth.approve(address(router), 1 ether);
RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
RouteTypesV2.Provider.Dodo,
address(weth),
address(usdt),
1 ether,
1_700 ether,
address(dodoProvider),
abi.encode(address(0xA11CE))
);
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertEq(amountOut, 1_800 ether);
assertEq(usdt.balanceOf(user), 1_800 ether);
}
function testUniswapV3Execution() public {
vm.startPrank(user);
weth.approve(address(router), 1 ether);
RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
RouteTypesV2.Provider.UniswapV3,
address(weth),
address(usdc),
1 ether,
1_650 ether,
address(uniswapRouter),
abi.encode(bytes(""), uint24(3000), address(uniswapQuoter), false)
);
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertEq(amountOut, 1_700 ether);
assertEq(usdc.balanceOf(user), 1_700 ether);
}
function testDodoV3DirectExecution() public {
vm.startPrank(user);
weth.approve(address(router), 1 ether);
RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
RouteTypesV2.Provider.DodoV3,
address(weth),
address(usdt),
1 ether,
2_000 ether,
address(d3Proxy),
abi.encode(address(d3Pool))
);
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertEq(amountOut, 2_100 ether);
assertEq(usdt.balanceOf(user), 2_100 ether);
}
function testBalancerExecution() public {
vm.startPrank(user);
weth.approve(address(router), 1 ether);
RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
RouteTypesV2.Provider.Balancer,
address(weth),
address(usdc),
1 ether,
1_500 ether,
address(balancerVault),
abi.encode(keccak256("balancer-weth-usdc"))
);
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertGt(amountOut, 0);
assertEq(usdc.balanceOf(user), amountOut);
}
function testCurveStableStableExecutionOnly() public {
usdt.mint(user, 2_000 ether);
vm.startPrank(user);
usdt.approve(address(router), 1_100 ether);
RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
RouteTypesV2.Provider.Curve,
address(usdt),
address(usdc),
1_100 ether,
900 ether,
address(curvePool),
abi.encode(int128(0), int128(1), false)
);
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertEq(amountOut, 1_000 ether);
assertEq(usdc.balanceOf(user), amountOut);
}
function testInvalidProviderDataRejected() public {
vm.startPrank(user);
weth.approve(address(router), 1 ether);
RouteTypesV2.RoutePlan memory plan = _singleLegPlan(
RouteTypesV2.Provider.Dodo,
address(weth),
address(usdt),
1 ether,
1_700 ether,
address(dodoProvider),
hex"1234"
);
vm.expectRevert();
router.executeRoute(plan);
vm.stopPrank();
}
function testMultiLegExecutionUsesPreviousLegOutput() public {
vm.startPrank(user);
weth.approve(address(router), 1 ether);
RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](2);
legs[0] = RouteTypesV2.RouteLeg({
provider: RouteTypesV2.Provider.Dodo,
tokenIn: address(weth),
tokenOut: address(usdt),
amountSource: RouteTypesV2.AmountSource.UserInput,
minAmountOut: 1_700 ether,
target: address(dodoProvider),
providerData: abi.encode(address(0xA11CE))
});
legs[1] = RouteTypesV2.RouteLeg({
provider: RouteTypesV2.Provider.Dodo,
tokenIn: address(usdt),
tokenOut: address(usdc),
amountSource: RouteTypesV2.AmountSource.PreviousLeg,
minAmountOut: 1_750 ether,
target: address(dodoProvider),
providerData: abi.encode(address(0xB0B))
});
RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
chainId: block.chainid,
inputToken: address(weth),
outputToken: address(usdc),
amountIn: 1 ether,
minAmountOut: 1_750 ether,
recipient: user,
deadline: block.timestamp + 1 hours,
legs: legs
});
uint256 amountOut = router.executeRoute(plan);
vm.stopPrank();
assertEq(amountOut, 1_790 ether);
assertEq(usdc.balanceOf(user), 1_790 ether);
}
function testIntentBridgeCoordinatorExecutesSourcePlanAndBridge() public {
vm.startPrank(user);
weth.approve(address(coordinator), 1 ether);
RouteTypesV2.RouteLeg[] memory sourceLegs = new RouteTypesV2.RouteLeg[](1);
sourceLegs[0] = RouteTypesV2.RouteLeg({
provider: RouteTypesV2.Provider.Dodo,
tokenIn: address(weth),
tokenOut: address(usdt),
amountSource: RouteTypesV2.AmountSource.UserInput,
minAmountOut: 1_700 ether,
target: address(dodoProvider),
providerData: abi.encode(address(0xA11CE))
});
RouteTypesV2.RoutePlan memory sourcePlan = RouteTypesV2.RoutePlan({
chainId: block.chainid,
inputToken: address(weth),
outputToken: address(usdt),
amountIn: 1 ether,
minAmountOut: 1_700 ether,
recipient: address(coordinator),
deadline: block.timestamp + 1 hours,
legs: sourceLegs
});
RouteTypesV2.RoutePlan memory destinationPlan = RouteTypesV2.RoutePlan({
chainId: block.chainid + 1,
inputToken: address(usdt),
outputToken: address(usdt),
amountIn: 1_800 ether,
minAmountOut: 1_800 ether,
recipient: user,
deadline: block.timestamp + 2 hours,
legs: new RouteTypesV2.RouteLeg[](0)
});
RouteTypesV2.BridgeIntentPlan memory intent = RouteTypesV2.BridgeIntentPlan({
sourcePlan: sourcePlan,
bridgeType: bytes32("CCIP"),
bridgeData: abi.encode(address(bridgeExecutor), "CCIP"),
destinationPlan: destinationPlan,
recipient: user,
deadline: block.timestamp + 1 hours
});
(bytes32 bridgeReference,) = coordinator.executeIntent(intent);
vm.stopPrank();
assertTrue(bridgeReference != bytes32(0));
assertEq(usdt.balanceOf(address(bridgeExecutor)), 1_800 ether);
}
}

View File

@@ -51,11 +51,11 @@ contract MockDODOPool {
function _QUOTE_TOKEN_() external view returns (address) { return quoteToken; }
function getMidPrice() external view returns (uint256) { return midPrice; }
function sellBase(uint256 amount) external returns (uint256) {
function sellBase(address) external returns (uint256) {
return sellOutAmount;
}
function sellQuote(uint256 amount) external returns (uint256) {
function sellQuote(address) external returns (uint256) {
return sellOutAmount;
}