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:
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