707 lines
25 KiB
Solidity
707 lines
25 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 testQuoteConfiguredProviderQuotesSingleDodoRoute() public view {
|
|
RouteTypesV2.ProviderQuote memory quote =
|
|
router.quoteConfiguredProvider(address(weth), address(usdt), 1 ether, RouteTypesV2.Provider.Dodo);
|
|
|
|
assertEq(uint256(quote.provider), uint256(RouteTypesV2.Provider.Dodo));
|
|
assertEq(quote.target, address(dodoProvider));
|
|
assertEq(quote.amountOut, 1_800 ether);
|
|
assertEq(quote.estimatedGas, 140000);
|
|
assertTrue(quote.executable);
|
|
assertEq(abi.decode(quote.providerData, (address)), address(0xA11CE));
|
|
}
|
|
|
|
function testQuoteConfiguredProviderRevertsWhenProviderDisabled() public {
|
|
router.setProviderEnabled(RouteTypesV2.Provider.Dodo, false);
|
|
|
|
vm.expectRevert(EnhancedSwapRouterV2.ProviderDisabled.selector);
|
|
router.quoteConfiguredProvider(address(weth), address(usdt), 1 ether, RouteTypesV2.Provider.Dodo);
|
|
}
|
|
|
|
function testQuoteConfiguredProviderRevertsWhenRouteMissing() public {
|
|
vm.expectRevert(EnhancedSwapRouterV2.RouteNotConfigured.selector);
|
|
router.quoteConfiguredProvider(address(weth), address(dai), 1 ether, RouteTypesV2.Provider.Dodo);
|
|
}
|
|
|
|
function testQuoteConfiguredProvidersStillDiscoversConfiguredRoutes() public view {
|
|
RouteTypesV2.ProviderQuote[] memory quotes =
|
|
router.quoteConfiguredProviders(address(weth), address(usdt), 1 ether);
|
|
|
|
assertEq(quotes.length, 1);
|
|
assertEq(uint256(quotes[0].provider), uint256(RouteTypesV2.Provider.Dodo));
|
|
assertEq(quotes[0].amountOut, 1_800 ether);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|