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) returns (bytes memory)
{ {
require(data.length >= 20, "Invalid data length"); require(data.length >= 20, "Invalid data length");
bytes4 selector = bytes4(data[data.length - 24:data.length - 20]); address executor = address(uint160(bytes20(data[0:20])));
address executor = address(uint160(bytes20(data[data.length - 20:]))); bytes4 selector = bytes4(data[20:24]);
bytes memory protocolData = data[:data.length - 24]; bytes memory protocolData = data[24:];
if (!executors[executor]) { if (!executors[executor]) {
revert ExecutionDispatcher__UnapprovedExecutor(); 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 {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol"; import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol"; import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
import "lib/forge-std/src/console.sol";
error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__InvalidDataLength();
error UniswapV4Executor__SwapFailed(); error UniswapV4Executor__SwapFailed();
@@ -24,108 +25,128 @@ contract UniswapV4Executor is IExecutor, V4Router {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
using CurrencyLibrary for Currency; using CurrencyLibrary for Currency;
struct UniswapV4Pool {
address intermediaryToken;
uint24 fee;
int24 tickSpacing;
}
constructor(IPoolManager _poolManager) V4Router(_poolManager) {} constructor(IPoolManager _poolManager) V4Router(_poolManager) {}
function swap(uint256, bytes calldata data) function swap(uint256 amountIn, bytes calldata data)
external external
payable payable
returns (uint256 calculatedAmount) 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 tokenOutBalanceBefore;
uint256 tokenInBalanceBefore;
tokenOutBalanceBefore = tokenOut == address(0) tokenOutBalanceBefore = tokenOut == address(0)
? address(this).balance ? address(this).balance
: IERC20(tokenOut).balanceOf(address(this)); : IERC20(tokenOut).balanceOf(address(this));
tokenInBalanceBefore = tokenIn == address(0) executeActions(fullData);
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
_executeActions(data);
uint256 tokenOutBalanceAfter; uint256 tokenOutBalanceAfter;
uint256 tokenInBalanceAfter;
tokenOutBalanceAfter = tokenOut == address(0) tokenOutBalanceAfter = tokenOut == address(0)
? address(this).balance ? address(this).balance
: IERC20(tokenOut).balanceOf(address(this)); : IERC20(tokenOut).balanceOf(address(this));
tokenInBalanceAfter = tokenIn == address(0) calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore;
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
if (isExactInput) {
calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore;
} else {
calculatedAmount = tokenInBalanceBefore - tokenInBalanceAfter;
}
return calculatedAmount; return calculatedAmount;
} }
// necessary to convert bytes memory to bytes calldata
function executeActions(bytes memory unlockData) public {
poolManager.unlock(unlockData);
}
function _decodeData(bytes calldata data) function _decodeData(bytes calldata data)
internal public
pure pure
returns ( returns (
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
bool isExactInput, uint256 amountOutMin,
uint256 amount bool zeroForOne,
address callbackExecutor,
bytes4 callbackSelector,
UniswapV4Pool[] memory pools
) )
{ {
(bytes memory actions, bytes[] memory params) = require(data.length >= 97, "Invalid data length");
abi.decode(data, (bytes, bytes[]));
// First byte of actions determines the swap type tokenIn = address(bytes20(data[0:20]));
uint8 action = uint8(bytes1(actions[0])); 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)) { uint256 poolsLength = (data.length - 97) / 26; // 26 bytes per pool object
IV4Router.ExactInputSingleParams memory swapParams = pools = new UniswapV4Pool[](poolsLength);
abi.decode(params[0], (IV4Router.ExactInputSingleParams)); bytes memory poolsData = data[97:];
uint256 offset = 0;
for (uint256 i = 0; i < poolsLength; i++) {
address intermediaryToken;
uint24 fee;
int24 tickSpacing;
tokenIn = swapParams.zeroForOne assembly {
? address(uint160(swapParams.poolKey.currency0.toId())) intermediaryToken := mload(add(poolsData, add(offset, 20)))
: address(uint160(swapParams.poolKey.currency1.toId())); fee := shr(232, mload(add(poolsData, add(offset, 52))))
tokenOut = swapParams.zeroForOne tickSpacing := shr(232, mload(add(poolsData, add(offset, 55))))
? address(uint160(swapParams.poolKey.currency1.toId())) }
: address(uint160(swapParams.poolKey.currency0.toId())); pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing);
isExactInput = true; offset += 26;
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;
} }
} }

View File

@@ -857,15 +857,22 @@ contract TychoRouterTest is TychoRouterTestSetup {
uint256 amountIn = 100 ether; uint256 amountIn = 100 ether;
deal(USDE_ADDR, tychoRouterAddr, amountIn); deal(USDE_ADDR, tychoRouterAddr, amountIn);
bytes memory protocolData = UniswapV4Utils.encodeExactInputSingle( UniswapV4Executor.UniswapV4Pool[] memory pools =
USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn) 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 protocolData = UniswapV4Utils.encodeExactInputSingle(
bytes memory protocolDataWithCallBack = abi.encodePacked( USDE_ADDR,
protocolData, USDT_ADDR,
uint256(1),
true,
address(usv4Executor),
SafeCallback.unlockCallback.selector, SafeCallback.unlockCallback.selector,
address(usv4Executor) pools
); );
bytes memory swap = encodeSwap( bytes memory swap = encodeSwap(
@@ -874,7 +881,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
uint24(0), uint24(0),
address(usv4Executor), address(usv4Executor),
bytes4(0), bytes4(0),
protocolDataWithCallBack protocolData
); );
bytes[] memory swaps = new bytes[](1); bytes[] memory swaps = new bytes[](1);

View File

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

View File

@@ -7,40 +7,31 @@ library UniswapV4Utils {
function encodeExactInputSingle( function encodeExactInputSingle(
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
uint24 fee, uint256 amountOutMin,
bool zeroForOne, bool zeroForOne,
uint24 tickSpacing, address callbackExecutor,
uint128 amountIn bytes4 callbackSelector,
UniswapV4Executor.UniswapV4Pool[] memory pools
) public pure returns (bytes memory) { ) public pure returns (bytes memory) {
PoolKey memory key = PoolKey({ bytes memory encodedPools;
currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut),
currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
fee: fee,
tickSpacing: int24(tickSpacing),
hooks: IHooks(address(0))
});
bytes memory actions = abi.encodePacked( for (uint256 i = 0; i < pools.length; i++) {
uint8(Actions.SWAP_EXACT_IN_SINGLE), encodedPools = abi.encodePacked(
uint8(Actions.SETTLE_ALL), encodedPools,
uint8(Actions.TAKE_ALL) 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);
} }
} }