Files
smom-dbis-138/test/bridge/trustless/EnhancedSwapRouterV2.t.sol
defiQUG 76aa419320 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
2026-04-07 23:40:52 -07:00

687 lines
24 KiB
Solidity

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