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
194 lines
6.5 KiB
Solidity
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);
|
|
}
|
|
}
|