Files
tycho-execution/foundry/src/executors/UniswapV4Executor.sol
Diana Carvalho 4d0f5cec64 fix(univ4): Append callback data instead of prepending
Bring back tests on the executor level. This way the executor can actually be used alone

--- don't change below this line ---
ENG-4222 Took 12 minutes
2025-02-14 12:02:07 +00:00

194 lines
6.5 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol";
import {
IERC20,
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {
Currency, CurrencyLibrary
} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
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";
error UniswapV4Executor__InvalidDataLength();
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 amountIn, bytes calldata data)
external
payable
returns (uint256 calculatedAmount)
{
(
address tokenIn,
address tokenOut,
uint256 amountOutMin,
bool zeroForOne,
address callbackExecutor,
bytes4 callbackSelector,
UniswapV4Executor.UniswapV4Pool[] memory pools
) = _decodeData(data);
bytes memory swapData;
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, amountOutMin);
swapData = abi.encode(actions, params);
} else {
PathKey[] memory path = new PathKey[](pools.length);
for (uint256 i = 0; i < pools.length; i++) {
path[i] = PathKey({
intermediateCurrency: Currency.wrap(pools[i].intermediaryToken),
fee: pools[i].fee,
tickSpacing: pools[i].tickSpacing,
hooks: IHooks(address(0)),
hookData: bytes("")
});
}
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
);
bytes[] memory params = new bytes[](3);
Currency currencyIn = Currency.wrap(tokenIn);
params[0] = abi.encode(
IV4Router.ExactInputParams({
currencyIn: currencyIn,
path: path,
amountIn: uint128(amountIn),
amountOutMinimum: uint128(amountOutMin)
})
);
params[1] = abi.encode(currencyIn, amountIn);
params[2] = abi.encode(Currency.wrap(tokenOut), amountOutMin);
swapData = abi.encode(actions, params);
}
bytes memory fullData =
abi.encodePacked( swapData, callbackExecutor, callbackSelector);
uint256 tokenOutBalanceBefore;
tokenOutBalanceBefore = tokenOut == address(0)
? address(this).balance
: IERC20(tokenOut).balanceOf(address(this));
executeActions(fullData);
uint256 tokenOutBalanceAfter;
tokenOutBalanceAfter = tokenOut == address(0)
? address(this).balance
: IERC20(tokenOut).balanceOf(address(this));
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)
public
pure
returns (
address tokenIn,
address tokenOut,
uint256 amountOutMin,
bool zeroForOne,
address callbackExecutor,
bytes4 callbackSelector,
UniswapV4Pool[] memory pools
)
{
if(data.length < 123) {
revert UniswapV4Executor__InvalidDataLength();
}
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]);
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;
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;
}
}
function _pay(Currency token, address payer, uint256 amount)
internal
override
{
IERC20(Currency.unwrap(token)).safeTransfer(
address(poolManager), amount
);
}
function msgSender() public view override returns (address) {
return address(this);
}
}