feat(univ4): Refactor input and handle single swap case

Construct the uniswap v4 specific objects inside the executor
The swap test in the executor alone doesn't work anymore because of the callback data prepended to the rest of he calldata

--- don't change below this line ---
ENG-4222 Took 4 hours 0 minutes


Took 40 seconds
This commit is contained in:
Diana Carvalho
2025-02-13 18:40:10 +00:00
parent bb7c6c25a5
commit be7883affc
5 changed files with 182 additions and 140 deletions

View File

@@ -449,9 +449,9 @@ contract TychoRouter is
returns (bytes memory)
{
require(data.length >= 20, "Invalid data length");
bytes4 selector = bytes4(data[data.length - 24:data.length - 20]);
address executor = address(uint160(bytes20(data[data.length - 20:])));
bytes memory protocolData = data[:data.length - 24];
address executor = address(uint160(bytes20(data[0:20])));
bytes4 selector = bytes4(data[20:24]);
bytes memory protocolData = data[24:];
if (!executors[executor]) {
revert ExecutionDispatcher__UnapprovedExecutor();

View File

@@ -16,6 +16,7 @@ import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
import "lib/forge-std/src/console.sol";
error UniswapV4Executor__InvalidDataLength();
error UniswapV4Executor__SwapFailed();
@@ -24,108 +25,128 @@ contract UniswapV4Executor is IExecutor, V4Router {
using SafeERC20 for IERC20;
using CurrencyLibrary for Currency;
struct UniswapV4Pool {
address intermediaryToken;
uint24 fee;
int24 tickSpacing;
}
constructor(IPoolManager _poolManager) V4Router(_poolManager) {}
function swap(uint256, bytes calldata data)
function swap(uint256 amountIn, bytes calldata data)
external
payable
returns (uint256 calculatedAmount)
{
(address tokenIn, address tokenOut, bool isExactInput, uint256 amount) =
_decodeData(data);
(
address tokenIn,
address tokenOut,
uint256 amountOutMin,
bool zeroForOne,
address callbackExecutor,
bytes4 callbackSelector,
UniswapV4Executor.UniswapV4Pool[] memory pools
) = _decodeData(data);
bytes memory fullData;
if (pools.length == 0) {
console.log("problem"); // raise error
} else if (pools.length == 1) {
PoolKey memory key = PoolKey({
currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut),
currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
fee: pools[0].fee,
tickSpacing: pools[0].tickSpacing,
hooks: IHooks(address(0))
});
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN_SINGLE),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
);
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(
IV4Router.ExactInputSingleParams({
poolKey: key,
zeroForOne: zeroForOne,
amountIn: uint128(amountIn),
amountOutMinimum: uint128(amountOutMin),
hookData: bytes("")
})
);
params[1] = abi.encode(key.currency0, amountIn);
params[2] = abi.encode(key.currency1, 0);
bytes memory swapData = abi.encode(actions, params);
fullData =
abi.encodePacked(callbackExecutor, callbackSelector, swapData);
} else {
console.log("do later");
}
uint256 tokenOutBalanceBefore;
uint256 tokenInBalanceBefore;
tokenOutBalanceBefore = tokenOut == address(0)
? address(this).balance
: IERC20(tokenOut).balanceOf(address(this));
tokenInBalanceBefore = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
_executeActions(data);
executeActions(fullData);
uint256 tokenOutBalanceAfter;
uint256 tokenInBalanceAfter;
tokenOutBalanceAfter = tokenOut == address(0)
? address(this).balance
: IERC20(tokenOut).balanceOf(address(this));
tokenInBalanceAfter = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
if (isExactInput) {
calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore;
} else {
calculatedAmount = tokenInBalanceBefore - tokenInBalanceAfter;
}
calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore;
return calculatedAmount;
}
// necessary to convert bytes memory to bytes calldata
function executeActions(bytes memory unlockData) public {
poolManager.unlock(unlockData);
}
function _decodeData(bytes calldata data)
internal
public
pure
returns (
address tokenIn,
address tokenOut,
bool isExactInput,
uint256 amount
uint256 amountOutMin,
bool zeroForOne,
address callbackExecutor,
bytes4 callbackSelector,
UniswapV4Pool[] memory pools
)
{
(bytes memory actions, bytes[] memory params) =
abi.decode(data, (bytes, bytes[]));
require(data.length >= 97, "Invalid data length");
// First byte of actions determines the swap type
uint8 action = uint8(bytes1(actions[0]));
tokenIn = address(bytes20(data[0:20]));
tokenOut = address(bytes20(data[20:40]));
amountOutMin = uint256(bytes32(data[40:72]));
zeroForOne = (data[72] != 0);
callbackExecutor = address(bytes20(data[73:93]));
callbackSelector = bytes4(data[93:97]);
if (action == uint8(Actions.SWAP_EXACT_IN_SINGLE)) {
IV4Router.ExactInputSingleParams memory swapParams =
abi.decode(params[0], (IV4Router.ExactInputSingleParams));
uint256 poolsLength = (data.length - 97) / 26; // 26 bytes per pool object
pools = new UniswapV4Pool[](poolsLength);
bytes memory poolsData = data[97:];
uint256 offset = 0;
for (uint256 i = 0; i < poolsLength; i++) {
address intermediaryToken;
uint24 fee;
int24 tickSpacing;
tokenIn = swapParams.zeroForOne
? address(uint160(swapParams.poolKey.currency0.toId()))
: address(uint160(swapParams.poolKey.currency1.toId()));
tokenOut = swapParams.zeroForOne
? address(uint160(swapParams.poolKey.currency1.toId()))
: address(uint160(swapParams.poolKey.currency0.toId()));
isExactInput = true;
amount = swapParams.amountIn;
} else if (action == uint8(Actions.SWAP_EXACT_OUT_SINGLE)) {
IV4Router.ExactOutputSingleParams memory swapParams =
abi.decode(params[0], (IV4Router.ExactOutputSingleParams));
tokenIn = swapParams.zeroForOne
? address(uint160(swapParams.poolKey.currency0.toId()))
: address(uint160(swapParams.poolKey.currency1.toId()));
tokenOut = swapParams.zeroForOne
? address(uint160(swapParams.poolKey.currency1.toId()))
: address(uint160(swapParams.poolKey.currency0.toId()));
isExactInput = false;
amount = swapParams.amountOut;
} else if (action == uint8(Actions.SWAP_EXACT_IN)) {
IV4Router.ExactInputParams memory swapParams =
abi.decode(params[0], (IV4Router.ExactInputParams));
tokenIn = address(uint160(swapParams.currencyIn.toId()));
PathKey memory lastPath =
swapParams.path[swapParams.path.length - 1];
tokenOut = address(uint160(lastPath.intermediateCurrency.toId()));
isExactInput = true;
amount = swapParams.amountIn;
} else if (action == uint8(Actions.SWAP_EXACT_OUT)) {
IV4Router.ExactOutputParams memory swapParams =
abi.decode(params[0], (IV4Router.ExactOutputParams));
PathKey memory firstPath = swapParams.path[0];
tokenIn = address(uint160(firstPath.intermediateCurrency.toId()));
tokenOut = address(uint160(swapParams.currencyOut.toId()));
isExactInput = false;
amount = swapParams.amountOut;
assembly {
intermediaryToken := mload(add(poolsData, add(offset, 20)))
fee := shr(232, mload(add(poolsData, add(offset, 52))))
tickSpacing := shr(232, mload(add(poolsData, add(offset, 55))))
}
pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing);
offset += 26;
}
}

View File

@@ -857,15 +857,22 @@ contract TychoRouterTest is TychoRouterTestSetup {
uint256 amountIn = 100 ether;
deal(USDE_ADDR, tychoRouterAddr, amountIn);
bytes memory protocolData = UniswapV4Utils.encodeExactInputSingle(
USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn)
);
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](1);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
});
// add executor and selector for callback
bytes memory protocolDataWithCallBack = abi.encodePacked(
protocolData,
bytes memory protocolData = UniswapV4Utils.encodeExactInputSingle(
USDE_ADDR,
USDT_ADDR,
uint256(1),
true,
address(usv4Executor),
SafeCallback.unlockCallback.selector,
address(usv4Executor)
pools
);
bytes memory swap = encodeSwap(
@@ -874,7 +881,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
uint24(0),
address(usv4Executor),
bytes4(0),
protocolDataWithCallBack
protocolData
);
bytes[] memory swaps = new bytes[](1);

View File

@@ -1,11 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import "../../src/executors/UniswapV4Executor.sol";
import "./UniswapV4Utils.sol";
import "@src/executors/UniswapV4Executor.sol";
import {Constants} from "../Constants.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
import {console} from "forge-std/console.sol";
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
contract UniswapV4ExecutorExposed is UniswapV4Executor {
constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {}
@@ -16,8 +17,11 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
returns (
address tokenIn,
address tokenOut,
bool isExactInput,
uint256 amount
uint256 amountOutMin,
bool zeroForOne,
address callbackExecutor,
bytes4 callbackSelector,
UniswapV4Pool[] memory pools
)
{
return _decodeData(data);
@@ -40,39 +44,58 @@ contract UniswapV4ExecutorTest is Test, Constants {
}
function testDecodeParams() public view {
uint24 expectedPoolFee = 500;
uint128 expectedAmount = 100;
uint256 minAmountOut = 100;
bool zeroForOne = true;
uint24 pool1Fee = 500;
int24 tickSpacing1 = 60;
uint24 pool2Fee = 1000;
int24 tickSpacing2 = -10;
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](2);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: pool1Fee,
tickSpacing: tickSpacing1
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDE_ADDR,
fee: pool2Fee,
tickSpacing: tickSpacing2
});
bytes memory data = UniswapV4Utils.encodeExactInputSingle(
USDE_ADDR, USDT_ADDR, expectedPoolFee, false, 1, expectedAmount
USDE_ADDR,
USDT_ADDR,
minAmountOut,
zeroForOne,
address(uniswapV4Exposed),
SafeCallback.unlockCallback.selector,
pools
);
(address tokenIn, address tokenOut, bool isExactInput, uint256 amount) =
uniswapV4Exposed.decodeData(data);
(
address tokenIn,
address tokenOut,
uint256 amountOutMin,
bool zeroForOneDecoded,
address callbackExecutor,
bytes4 callbackSelector,
UniswapV4Executor.UniswapV4Pool[] memory decodedPools
) = uniswapV4Exposed.decodeData(data);
assertEq(tokenIn, USDE_ADDR);
assertEq(tokenOut, USDT_ADDR);
assertTrue(isExactInput);
assertEq(amount, expectedAmount);
}
function testSwap() public {
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
bytes memory data = UniswapV4Utils.encodeExactInputSingle(
USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn)
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut);
assertEq(amountOutMin, minAmountOut);
assertEq(zeroForOneDecoded, zeroForOne);
assertEq(callbackExecutor, address(uniswapV4Exposed));
assertEq(callbackSelector, SafeCallback.unlockCallback.selector);
assertEq(decodedPools.length, 2);
assertEq(decodedPools[0].intermediaryToken, USDT_ADDR);
assertEq(decodedPools[0].fee, pool1Fee);
assertEq(decodedPools[0].tickSpacing, tickSpacing1);
assertEq(decodedPools[1].intermediaryToken, USDE_ADDR);
assertEq(decodedPools[1].fee, pool2Fee);
assertEq(decodedPools[1].tickSpacing, tickSpacing2);
}
}

View File

@@ -7,40 +7,31 @@ library UniswapV4Utils {
function encodeExactInputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint256 amountOutMin,
bool zeroForOne,
uint24 tickSpacing,
uint128 amountIn
address callbackExecutor,
bytes4 callbackSelector,
UniswapV4Executor.UniswapV4Pool[] memory pools
) public pure returns (bytes memory) {
PoolKey memory key = PoolKey({
currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut),
currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
fee: fee,
tickSpacing: int24(tickSpacing),
hooks: IHooks(address(0))
});
bytes memory encodedPools;
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN_SINGLE),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
for (uint256 i = 0; i < pools.length; i++) {
encodedPools = abi.encodePacked(
encodedPools,
pools[i].intermediaryToken,
bytes3(pools[i].fee),
pools[i].tickSpacing
);
}
return abi.encodePacked(
tokenIn,
tokenOut,
amountOutMin,
zeroForOne,
callbackExecutor,
bytes4(callbackSelector),
encodedPools
);
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(
IV4Router.ExactInputSingleParams({
poolKey: key,
zeroForOne: zeroForOne,
amountIn: amountIn,
amountOutMinimum: 0,
hookData: bytes("")
})
);
params[1] = abi.encode(key.currency0, amountIn);
params[2] = abi.encode(key.currency1, 0);
return abi.encode(actions, params);
}
}