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:
180
test/bridge/CWAssetReserveVerifier.t.sol
Normal file
180
test/bridge/CWAssetReserveVerifier.t.sol
Normal 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();
|
||||
}
|
||||
}
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
135
test/bridge/CWMultiTokenBridgeBTC.t.sol
Normal file
135
test/bridge/CWMultiTokenBridgeBTC.t.sol
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
262
test/bridge/CWReserveVerifier.t.sol
Normal file
262
test/bridge/CWReserveVerifier.t.sol
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
155
test/bridge/CWReserveVerifierBTC.t.sol
Normal file
155
test/bridge/CWReserveVerifierBTC.t.sol
Normal 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();
|
||||
}
|
||||
}
|
||||
98
test/bridge/CWReserveVerifierVaultIntegration.t.sol
Normal file
98
test/bridge/CWReserveVerifierVaultIntegration.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
195
test/bridge/CWReserveVerifierVaultV2Integration.t.sol
Normal file
195
test/bridge/CWReserveVerifierVaultV2Integration.t.sol
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
124
test/bridge/USDWPublicWrapVault.t.sol
Normal file
124
test/bridge/USDWPublicWrapVault.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
271
test/bridge/atomic/AtomicBridgeCoordinator.t.sol
Normal file
271
test/bridge/atomic/AtomicBridgeCoordinator.t.sol
Normal 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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
230
test/bridge/trustless/Chain138PilotDexVenues.t.sol
Normal file
230
test/bridge/trustless/Chain138PilotDexVenues.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
686
test/bridge/trustless/EnhancedSwapRouterV2.t.sol
Normal file
686
test/bridge/trustless/EnhancedSwapRouterV2.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user