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