diff --git a/contracts/UniAgent.sol b/contracts/UniAgent.sol index 39a5440f..5c769bec 100644 --- a/contracts/UniAgent.sol +++ b/contracts/UniAgent.sol @@ -13,7 +13,8 @@ contract UniAgent is IUniAgent, Ownable, TokenCollector { address private constant v2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; address private constant v3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; - address payable private constant universalRouter = payable(0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B); + address private constant swapRouter02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + address payable private constant universalRouter = payable(0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD); constructor(address _owner, address _uniswapPermit2, address _allowanceTarget) Ownable(_owner) TokenCollector(_uniswapPermit2, _allowanceTarget) {} @@ -34,6 +35,7 @@ contract UniAgent is IUniAgent, Ownable, TokenCollector { // ignore return value and proceed anyway since three calls are independent tokens[i].call(abi.encodeCall(IERC20.approve, (v2Router, type(uint256).max))); tokens[i].call(abi.encodeCall(IERC20.approve, (v3Router, type(uint256).max))); + tokens[i].call(abi.encodeCall(IERC20.approve, (swapRouter02, type(uint256).max))); } } @@ -82,7 +84,7 @@ contract UniAgent is IUniAgent, Ownable, TokenCollector { // deposit directly into router if it's universal router _collect(inputToken, msg.sender, universalRouter, inputAmount, userPermit); } else { - // v2 v3 use transferFrom + // v2, v3, swapRouter02 use transferFrom _collect(inputToken, msg.sender, address(this), inputAmount, userPermit); } } @@ -101,6 +103,8 @@ contract UniAgent is IUniAgent, Ownable, TokenCollector { return v2Router; } else if (routerType == RouterType.V3Router) { return v3Router; + } else if (routerType == RouterType.SwapRouter02) { + return swapRouter02; } else if (routerType == RouterType.UniversalRouter) { return universalRouter; } diff --git a/contracts/interfaces/IUniAgent.sol b/contracts/interfaces/IUniAgent.sol index 0db7ce96..fdfb49da 100644 --- a/contracts/interfaces/IUniAgent.sol +++ b/contracts/interfaces/IUniAgent.sol @@ -17,6 +17,7 @@ interface IUniAgent { enum RouterType { V2Router, V3Router, + SwapRouter02, UniversalRouter } diff --git a/contracts/interfaces/IUniswapSwapRouter02.sol b/contracts/interfaces/IUniswapSwapRouter02.sol new file mode 100644 index 00000000..5a2c21b7 --- /dev/null +++ b/contracts/interfaces/IUniswapSwapRouter02.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface ISwapRouter02 { + function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to) external payable returns (uint256 amountOut); + + function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to) external payable returns (uint256 amountIn); + + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + + struct ExactInputParams { + bytes path; + address recipient; + uint256 amountIn; + uint256 amountOutMinimum; + } + + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 amountOut; + uint256 amountInMaximum; + } + + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); +} diff --git a/package.json b/package.json index f61a2425..ffdf8468 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "lint": "solhint \"contracts/**/*.sol\"", "compile": "forge build --force", "test-foundry-local": "DEPLOYED=false forge test --no-match-path 'test/forkMainnet/*.t.sol'", - "test-foundry-fork": "DEPLOYED=false forge test --fork-url $MAINNET_NODE_RPC_URL --fork-block-number 16075500 --match-path 'test/forkMainnet/*.t.sol'", + "test-foundry-fork": "DEPLOYED=false forge test --fork-url $MAINNET_NODE_RPC_URL --fork-block-number 17900000 --match-path 'test/forkMainnet/*.t.sol'", "gas-report-local": "yarn test-foundry-local --gas-report", "gas-report-fork": "yarn test-foundry-fork --gas-report" }, diff --git a/test/forkMainnet/UniAgent/SwapRouter02.t.sol b/test/forkMainnet/UniAgent/SwapRouter02.t.sol new file mode 100644 index 00000000..764dbd4f --- /dev/null +++ b/test/forkMainnet/UniAgent/SwapRouter02.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import { ISwapRouter02 } from "contracts/interfaces/IUniswapSwapRouter02.sol"; +import { IUniswapRouterV2 } from "contracts/interfaces/IUniswapRouterV2.sol"; +import { IUniswapV3Quoter } from "contracts/interfaces/IUniswapV3Quoter.sol"; +import { IUniAgent } from "contracts/interfaces/IUniAgent.sol"; +import { UniswapV3 } from "contracts/libraries/UniswapV3.sol"; +import { BalanceSnapshot, Snapshot } from "test/utils/BalanceSnapshot.sol"; +import { UniAgentTest } from "test/forkMainnet/UniAgent/Setup.t.sol"; + +contract SwapRouter02Test is UniAgentTest { + using BalanceSnapshot for Snapshot; + + IUniswapRouterV2 v2Router = IUniswapRouterV2(UNISWAP_V2_ADDRESS); + IUniswapV3Quoter v3Quoter = IUniswapV3Quoter(UNISWAP_V3_QUOTER_ADDRESS); + uint256 defaultOutputAmount; + uint24 defaultFee = 3000; + uint24[] v3Fees = [defaultFee]; + + function setUp() public override { + super.setUp(); + } + + function testV2SwapExactTokensForTokens() public { + // USDT -> CRV + Snapshot memory userInputToken = BalanceSnapshot.take({ owner: user, token: defaultInputToken }); + Snapshot memory recvOutputToken = BalanceSnapshot.take({ owner: recipient, token: defaultOutputToken }); + + uint256[] memory amounts = v2Router.getAmountsOut(defaultInputAmount, defaultPath); + uint256 outputAmount = amounts[amounts.length - 1]; + uint256 minOutputAmount = (defaultOutputAmount * 95) / 100; // default 5% slippage tolerance + bytes memory payload = abi.encodeCall(ISwapRouter02.swapExactTokensForTokens, (defaultInputAmount, minOutputAmount, defaultPath, recipient)); + + vm.prank(user); + uniAgent.swap(IUniAgent.RouterType.SwapRouter02, defaultInputToken, defaultInputAmount, payload, defaultUserPermit); + + userInputToken.assertChange(-int256(defaultInputAmount)); + // recipient should receive exact amount of quote from Uniswap + recvOutputToken.assertChange(int256(outputAmount)); + } + + function testV3ExactInputSingle() public { + // USDT -> CRV + Snapshot memory userInputToken = BalanceSnapshot.take({ owner: user, token: defaultInputToken }); + Snapshot memory recvOutputToken = BalanceSnapshot.take({ owner: recipient, token: defaultOutputToken }); + + bytes memory encodedPath = UniswapV3.encodePath(defaultPath, v3Fees); + defaultOutputAmount = v3Quoter.quoteExactInput(encodedPath, defaultInputAmount); + uint256 minOutputAmount = (defaultOutputAmount * 95) / 100; // default 5% slippage tolerance + bytes memory payload = abi.encodeCall( + ISwapRouter02.exactInputSingle, + ( + ISwapRouter02.ExactInputSingleParams({ + tokenIn: defaultInputToken, + tokenOut: defaultOutputToken, + fee: defaultFee, + recipient: recipient, + amountIn: defaultInputAmount, + amountOutMinimum: minOutputAmount, + sqrtPriceLimitX96: 0 + }) + ) + ); + + vm.prank(user); + uniAgent.swap(IUniAgent.RouterType.SwapRouter02, defaultInputToken, defaultInputAmount, payload, defaultUserPermit); + + userInputToken.assertChange(-int256(defaultInputAmount)); + // recipient should receive exact amount of quote from Uniswap + recvOutputToken.assertChange(int256(defaultOutputAmount)); + } +} diff --git a/test/utils/config/arbitrumMainnet.json b/test/utils/config/arbitrumMainnet.json index 3015565f..f8bf22a3 100644 --- a/test/utils/config/arbitrumMainnet.json +++ b/test/utils/config/arbitrumMainnet.json @@ -14,6 +14,7 @@ "UNISWAP_V3_ADDRESS": "0xE592427A0AEce92De3Edee1F18E0157C05861564", "UNISWAP_V3_QUOTER_ADDRESS": "0x0000000000000000000000000000000000000000", "UNISWAP_PERMIT2_ADDRESS": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "UNISWAP_SWAP_ROUTER_02_ADDRESS": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0x4648a43B2C14Da09FdF82B161150d3F634f40491", "CURVE_USDT_POOL_ADDRESS": "0x0000000000000000000000000000000000000000", "CURVE_COMPOUND_POOL_ADDRESS": "0x0000000000000000000000000000000000000000", diff --git a/test/utils/config/goerli.json b/test/utils/config/goerli.json index 8cd78cbd..d56af036 100644 --- a/test/utils/config/goerli.json +++ b/test/utils/config/goerli.json @@ -14,7 +14,8 @@ "UNISWAP_V3_ADDRESS": "0xE592427A0AEce92De3Edee1F18E0157C05861564", "UNISWAP_V3_QUOTER_ADDRESS": "0x0000000000000000000000000000000000000000", "UNISWAP_PERMIT2_ADDRESS": "0x000000000022d473030f116ddee9f6b43ac78ba3", - "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0x4648a43B2C14Da09FdF82B161150d3F634f40491", + "UNISWAP_SWAP_ROUTER_02_ADDRESS": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", "CURVE_USDT_POOL_ADDRESS": "0x0000000000000000000000000000000000000000", "CURVE_COMPOUND_POOL_ADDRESS": "0x0000000000000000000000000000000000000000", "CURVE_Y_POOL_ADDRESS": "0x0000000000000000000000000000000000000000", diff --git a/test/utils/config/local.json b/test/utils/config/local.json index 71e2f14c..3f5f478f 100644 --- a/test/utils/config/local.json +++ b/test/utils/config/local.json @@ -14,6 +14,7 @@ "UNISWAP_V3_ADDRESS": "0x000000000000000000000000000000000000000", "UNISWAP_V3_QUOTER_ADDRESS": "0x000000000000000000000000000000000000000", "UNISWAP_PERMIT2_ADDRESS": "0x000000000000000000000000000000000000000", + "UNISWAP_SWAP_ROUTER_02_ADDRESS": "0x000000000000000000000000000000000000000", "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0x000000000000000000000000000000000000000", "CURVE_USDT_POOL_ADDRESS": "0x000000000000000000000000000000000000000", "CURVE_COMPOUND_POOL_ADDRESS": "0x000000000000000000000000000000000000000", diff --git a/test/utils/config/mainnet.json b/test/utils/config/mainnet.json index bda6222c..e23e12a7 100644 --- a/test/utils/config/mainnet.json +++ b/test/utils/config/mainnet.json @@ -14,7 +14,8 @@ "UNISWAP_V3_ADDRESS": "0xE592427A0AEce92De3Edee1F18E0157C05861564", "UNISWAP_V3_QUOTER_ADDRESS": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", "UNISWAP_PERMIT2_ADDRESS": "0x000000000022d473030f116ddee9f6b43ac78ba3", - "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", + "UNISWAP_SWAP_ROUTER_02_ADDRESS": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", "CURVE_USDT_POOL_ADDRESS": "0x52EA46506B9CC5Ef470C5bf89f17Dc28bB35D85C", "CURVE_COMPOUND_POOL_ADDRESS": "0xA2B47E3D5c44877cca798226B7B8118F9BFb7A56", "CURVE_Y_POOL_ADDRESS": "0x45F783CCE6B7FF23B2ab2D70e416cdb7D6055f51",