Merge pull request #34 from propeller-heads/router/tnl/ENG-4034-usv3-executor
feat: UniswapV3 Executor
This commit is contained in:
@@ -379,8 +379,8 @@ contract TychoRouter is
|
|||||||
int256 amount0Delta,
|
int256 amount0Delta,
|
||||||
int256 amount1Delta,
|
int256 amount1Delta,
|
||||||
bytes calldata data
|
bytes calldata data
|
||||||
) internal view returns (uint256 amountOwed, address tokenOwed) {
|
) internal view returns (uint256 amountIn, address tokenIn) {
|
||||||
address tokenIn = address(bytes20(data[0:20]));
|
tokenIn = address(bytes20(data[0:20]));
|
||||||
address tokenOut = address(bytes20(data[20:40]));
|
address tokenOut = address(bytes20(data[20:40]));
|
||||||
uint24 poolFee = uint24(bytes3(data[40:43]));
|
uint24 poolFee = uint24(bytes3(data[40:43]));
|
||||||
|
|
||||||
@@ -389,9 +389,9 @@ contract TychoRouter is
|
|||||||
_usv3Factory, tokenIn, tokenOut, poolFee
|
_usv3Factory, tokenIn, tokenOut, poolFee
|
||||||
);
|
);
|
||||||
|
|
||||||
amountOwed =
|
amountIn =
|
||||||
amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);
|
amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);
|
||||||
|
|
||||||
return (amountOwed, tokenOwed);
|
return (amountIn, tokenIn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
83
foundry/src/executors/UniswapV3Executor.sol
Normal file
83
foundry/src/executors/UniswapV3Executor.sol
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
import "@interfaces/IExecutor.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||||
|
|
||||||
|
error UniswapV3Executor__InvalidDataLength();
|
||||||
|
|
||||||
|
contract UniswapV3Executor is IExecutor {
|
||||||
|
uint160 private constant MIN_SQRT_RATIO = 4295128739;
|
||||||
|
uint160 private constant MAX_SQRT_RATIO =
|
||||||
|
1461446703485210103287273052203988822378723970342;
|
||||||
|
|
||||||
|
// slither-disable-next-line locked-ether
|
||||||
|
function swap(uint256 amountIn, bytes calldata data)
|
||||||
|
external
|
||||||
|
payable
|
||||||
|
returns (uint256 amountOut)
|
||||||
|
{
|
||||||
|
(
|
||||||
|
address tokenIn,
|
||||||
|
address tokenOut,
|
||||||
|
uint24 fee,
|
||||||
|
address receiver,
|
||||||
|
address target,
|
||||||
|
bool zeroForOne
|
||||||
|
) = _decodeData(data);
|
||||||
|
int256 amount0;
|
||||||
|
int256 amount1;
|
||||||
|
IUniswapV3Pool pool = IUniswapV3Pool(target);
|
||||||
|
|
||||||
|
bytes memory callbackData = _makeV3CallbackData(tokenIn, tokenOut, fee);
|
||||||
|
|
||||||
|
{
|
||||||
|
(amount0, amount1) = pool.swap(
|
||||||
|
receiver,
|
||||||
|
zeroForOne,
|
||||||
|
// positive means exactIn
|
||||||
|
int256(amountIn),
|
||||||
|
zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,
|
||||||
|
callbackData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zeroForOne) {
|
||||||
|
amountOut = amount1 > 0 ? uint256(amount1) : uint256(-amount1);
|
||||||
|
} else {
|
||||||
|
amountOut = amount0 > 0 ? uint256(amount0) : uint256(-amount0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _decodeData(bytes calldata data)
|
||||||
|
internal
|
||||||
|
pure
|
||||||
|
returns (
|
||||||
|
address tokenIn,
|
||||||
|
address tokenOut,
|
||||||
|
uint24 fee,
|
||||||
|
address receiver,
|
||||||
|
address target,
|
||||||
|
bool zeroForOne
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (data.length != 84) {
|
||||||
|
revert UniswapV3Executor__InvalidDataLength();
|
||||||
|
}
|
||||||
|
tokenIn = address(bytes20(data[0:20]));
|
||||||
|
tokenOut = address(bytes20(data[20:40]));
|
||||||
|
fee = uint24(bytes3(data[40:43]));
|
||||||
|
receiver = address(bytes20(data[43:63]));
|
||||||
|
target = address(bytes20(data[63:83]));
|
||||||
|
zeroForOne = uint8(data[83]) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeV3CallbackData(address tokenIn, address tokenOut, uint24 fee)
|
||||||
|
internal
|
||||||
|
pure
|
||||||
|
returns (bytes memory)
|
||||||
|
{
|
||||||
|
return abi.encodePacked(tokenIn, tokenOut, fee);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,9 @@ contract Constants is Test {
|
|||||||
address WETH_WBTC_POOL = 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940;
|
address WETH_WBTC_POOL = 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940;
|
||||||
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
|
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
|
||||||
|
|
||||||
|
// uniswap v3
|
||||||
|
address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Deploys a dummy contract with non-empty bytecode
|
* @dev Deploys a dummy contract with non-empty bytecode
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
function testSwapSimple() public {
|
function testSwapSimple() public {
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||||
// 1 WETH -> DAI
|
// 1 WETH -> DAI
|
||||||
// (univ2)
|
// (USV2)
|
||||||
uint256 amountIn = 1 ether;
|
uint256 amountIn = 1 ether;
|
||||||
deal(WETH_ADDR, tychoRouterAddr, amountIn);
|
deal(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||||
|
|
||||||
@@ -606,4 +606,52 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
|
|
||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testUSV3Callback() public {
|
||||||
|
uint24 poolFee = 3000;
|
||||||
|
uint256 amountOwed = 1000000000000000000;
|
||||||
|
deal(WETH_ADDR, tychoRouterAddr, amountOwed);
|
||||||
|
uint256 initialPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3);
|
||||||
|
|
||||||
|
vm.startPrank(DAI_WETH_USV3);
|
||||||
|
tychoRouter.uniswapV3SwapCallback(
|
||||||
|
-2631245338449998525223,
|
||||||
|
int256(amountOwed),
|
||||||
|
abi.encodePacked(WETH_ADDR, DAI_ADDR, poolFee)
|
||||||
|
);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
uint256 finalPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3);
|
||||||
|
assertEq(finalPoolReserve - initialPoolReserve, amountOwed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapSingleUSV3() public {
|
||||||
|
// Trade 1 WETH for DAI with 1 swap on Uniswap V3
|
||||||
|
// 1 WETH -> DAI
|
||||||
|
// (USV3)
|
||||||
|
uint256 amountIn = 10 ** 18;
|
||||||
|
deal(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||||
|
|
||||||
|
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
|
||||||
|
bool zeroForOne = false;
|
||||||
|
bytes memory protocolData = encodeUniswapV3Swap(
|
||||||
|
WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne
|
||||||
|
);
|
||||||
|
bytes memory swap = encodeSwap(
|
||||||
|
uint8(0),
|
||||||
|
uint8(1),
|
||||||
|
uint24(0),
|
||||||
|
address(usv3Executor),
|
||||||
|
bytes4(0),
|
||||||
|
protocolData
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes[] memory swaps = new bytes[](1);
|
||||||
|
swaps[0] = swap;
|
||||||
|
|
||||||
|
tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps));
|
||||||
|
|
||||||
|
uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr);
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "./Constants.sol";
|
|||||||
import "./mock/MockERC20.sol";
|
import "./mock/MockERC20.sol";
|
||||||
import "@src/TychoRouter.sol";
|
import "@src/TychoRouter.sol";
|
||||||
import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol";
|
import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol";
|
||||||
|
import "../src/executors/UniswapV3Executor.sol";
|
||||||
|
|
||||||
contract TychoRouterExposed is TychoRouter {
|
contract TychoRouterExposed is TychoRouter {
|
||||||
constructor(address _permit2, address weth, address usv3Factory)
|
constructor(address _permit2, address weth, address usv3Factory)
|
||||||
@@ -34,6 +35,7 @@ contract TychoRouterTestSetup is Test, Constants {
|
|||||||
address tychoRouterAddr;
|
address tychoRouterAddr;
|
||||||
address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3);
|
address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3);
|
||||||
UniswapV2Executor public usv2Executor;
|
UniswapV2Executor public usv2Executor;
|
||||||
|
UniswapV3Executor public usv3Executor;
|
||||||
MockERC20[] tokens;
|
MockERC20[] tokens;
|
||||||
|
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
@@ -41,8 +43,9 @@ contract TychoRouterTestSetup is Test, Constants {
|
|||||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
|
|
||||||
vm.startPrank(ADMIN);
|
vm.startPrank(ADMIN);
|
||||||
|
address factoryV3 = address(0x1F98431c8aD98523631AE4a59f267346ea31F984);
|
||||||
tychoRouter =
|
tychoRouter =
|
||||||
new TychoRouterExposed(permit2Address, WETH_ADDR, address(1));
|
new TychoRouterExposed(permit2Address, WETH_ADDR, factoryV3);
|
||||||
tychoRouterAddr = address(tychoRouter);
|
tychoRouterAddr = address(tychoRouter);
|
||||||
tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER);
|
tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER);
|
||||||
tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);
|
tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);
|
||||||
@@ -55,8 +58,10 @@ contract TychoRouterTestSetup is Test, Constants {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
|
|
||||||
usv2Executor = new UniswapV2Executor();
|
usv2Executor = new UniswapV2Executor();
|
||||||
|
usv3Executor = new UniswapV3Executor();
|
||||||
vm.startPrank(EXECUTOR_SETTER);
|
vm.startPrank(EXECUTOR_SETTER);
|
||||||
tychoRouter.setExecutor(address(usv2Executor));
|
tychoRouter.setExecutor(address(usv2Executor));
|
||||||
|
tychoRouter.setExecutor(address(usv3Executor));
|
||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
|
|
||||||
vm.startPrank(BOB);
|
vm.startPrank(BOB);
|
||||||
@@ -190,4 +195,17 @@ contract TychoRouterTestSetup is Test, Constants {
|
|||||||
) internal pure returns (bytes memory) {
|
) internal pure returns (bytes memory) {
|
||||||
return abi.encodePacked(tokenIn, target, receiver, zero2one);
|
return abi.encodePacked(tokenIn, target, receiver, zero2one);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeUniswapV3Swap(
|
||||||
|
address tokenIn,
|
||||||
|
address tokenOut,
|
||||||
|
address receiver,
|
||||||
|
address target,
|
||||||
|
bool zero2one
|
||||||
|
) internal view returns (bytes memory) {
|
||||||
|
IUniswapV3Pool pool = IUniswapV3Pool(target);
|
||||||
|
return abi.encodePacked(
|
||||||
|
tokenIn, tokenOut, pool.fee(), receiver, target, zero2one
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
68
foundry/test/executors/UniswapV3Executor.t.sol
Normal file
68
foundry/test/executors/UniswapV3Executor.t.sol
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
import "@src/executors/UniswapV3Executor.sol";
|
||||||
|
import {Test} from "../../lib/forge-std/src/Test.sol";
|
||||||
|
import {Constants} from "../Constants.sol";
|
||||||
|
|
||||||
|
contract UniswapV3ExecutorExposed is UniswapV3Executor {
|
||||||
|
function decodeData(bytes calldata data)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
returns (
|
||||||
|
address inToken,
|
||||||
|
address outToken,
|
||||||
|
uint24 fee,
|
||||||
|
address receiver,
|
||||||
|
address target,
|
||||||
|
bool zeroForOne
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return _decodeData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contract UniswapV3ExecutorTest is UniswapV3ExecutorExposed, Test, Constants {
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
UniswapV3ExecutorExposed uniswapV3Exposed;
|
||||||
|
IERC20 WETH = IERC20(WETH_ADDR);
|
||||||
|
IERC20 DAI = IERC20(DAI_ADDR);
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
uint256 forkBlock = 17323404;
|
||||||
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
|
uniswapV3Exposed = new UniswapV3ExecutorExposed();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testDecodeParams() public view {
|
||||||
|
uint24 expectedPoolFee = 500;
|
||||||
|
bytes memory data = abi.encodePacked(
|
||||||
|
WETH_ADDR, DAI_ADDR, expectedPoolFee, address(2), address(3), false
|
||||||
|
);
|
||||||
|
|
||||||
|
(
|
||||||
|
address tokenIn,
|
||||||
|
address tokenOut,
|
||||||
|
uint24 fee,
|
||||||
|
address receiver,
|
||||||
|
address target,
|
||||||
|
bool zeroForOne
|
||||||
|
) = uniswapV3Exposed.decodeData(data);
|
||||||
|
|
||||||
|
assertEq(tokenIn, WETH_ADDR);
|
||||||
|
assertEq(tokenOut, DAI_ADDR);
|
||||||
|
assertEq(fee, expectedPoolFee);
|
||||||
|
assertEq(receiver, address(2));
|
||||||
|
assertEq(target, address(3));
|
||||||
|
assertEq(zeroForOne, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testDecodeParamsInvalidDataLength() public {
|
||||||
|
bytes memory invalidParams =
|
||||||
|
abi.encodePacked(WETH_ADDR, address(2), address(3));
|
||||||
|
|
||||||
|
vm.expectRevert(UniswapV3Executor__InvalidDataLength.selector);
|
||||||
|
uniswapV3Exposed.decodeData(invalidParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,7 +113,6 @@ impl SwapEncoder for UniswapV3SwapEncoder {
|
|||||||
bytes_to_address(&encoding_context.receiver)?,
|
bytes_to_address(&encoding_context.receiver)?,
|
||||||
component_id,
|
component_id,
|
||||||
zero_to_one,
|
zero_to_one,
|
||||||
encoding_context.exact_out,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(args.abi_encode_packed())
|
Ok(args.abi_encode_packed())
|
||||||
@@ -259,8 +258,6 @@ mod tests {
|
|||||||
"88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
|
"88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
|
||||||
// zero for one
|
// zero for one
|
||||||
"00",
|
"00",
|
||||||
// exact out
|
|
||||||
"00",
|
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user