Files
tycho-execution/foundry/src/executors/EkuboExecutor.sol
TAMARA LIPOWSKI c678f40057 fix: Take address for EkuboExecutor init
- To match the other executor inits and to not leak information about ICore into the deployment script.
- Also fix fork block of tests. This had been temporarily changed to test the Ekubo executor. For some reason, vm.rollFork is not working in this case. Skip the Ekubo integration test for now (we know it runs with the correct block, where the pool we call is actually already deployed)
2025-03-31 17:49:47 +02:00

209 lines
7.0 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IExecutor} from "@interfaces/IExecutor.sol";
import {ICallback} from "@interfaces/ICallback.sol";
import {ICore} from "@ekubo/interfaces/ICore.sol";
import {ILocker, IPayer} from "@ekubo/interfaces/IFlashAccountant.sol";
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol";
import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";
import {LibBytes} from "@solady/utils/LibBytes.sol";
import {Config, EkuboPoolKey} from "@ekubo/types/poolKey.sol";
import {MAX_SQRT_RATIO, MIN_SQRT_RATIO} from "@ekubo/types/sqrtRatio.sol";
contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer {
error EkuboExecutor__InvalidDataLength();
error EkuboExecutor__CoreOnly();
error EkuboExecutor__UnknownCallback();
ICore immutable core;
bytes4 constant LOCKED_SELECTOR = 0xb45a3c0e; // locked(uint256)
bytes4 constant PAY_CALLBACK_SELECTOR = 0x599d0714; // payCallback(uint256,address)
uint256 constant POOL_DATA_OFFSET = 56;
uint256 constant HOP_BYTE_LEN = 52;
constructor(address _core) {
core = ICore(_core);
}
function swap(uint256 amountIn, bytes calldata data)
external
payable
returns (uint256 calculatedAmount)
{
if (data.length < 92) revert EkuboExecutor__InvalidDataLength();
uint256 tokenOutOffset = data.length - HOP_BYTE_LEN;
address tokenOut =
address(bytes20(LibBytes.loadCalldata(data, tokenOutOffset)));
uint256 tokenOutBalanceBefore = _balanceOf(tokenOut);
// amountIn must be at most type(int128).MAX
_lock(bytes.concat(bytes16(uint128(amountIn)), data));
uint256 tokenOutBalanceAfter = _balanceOf(tokenOut);
// It would be better if we could somehow pass back the swapped amount from the lock but the interface doesn't offer that capability.
// Note that the current approach also prevents arbs that return less than their input because of arithmetic underflow.
calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore;
}
// We can't use the return value here since it won't get propagated (see Dispatcher.sol:_handleCallback)
function handleCallback(bytes calldata raw)
external
returns (bytes memory)
{
verifyCallback(raw);
// Without selector and locker id
bytes calldata stripped = raw[36:];
bytes4 selector = bytes4(raw[:4]);
if (selector == LOCKED_SELECTOR) {
_locked(stripped);
} else if (selector == PAY_CALLBACK_SELECTOR) {
_payCallback(stripped);
} else {
revert EkuboExecutor__UnknownCallback();
}
return "";
}
function verifyCallback(bytes calldata) public view coreOnly {}
function locked(uint256) external coreOnly {
// Without selector and locker id
_locked(msg.data[36:]);
}
function payCallback(uint256, address /*token*/ ) external coreOnly {
// Without selector and locker id
_payCallback(msg.data[36:]);
}
function _balanceOf(address token)
internal
view
returns (uint256 balance)
{
balance = token == NATIVE_TOKEN_ADDRESS
? address(this).balance
: IERC20(token).balanceOf(address(this));
}
function _lock(bytes memory data) internal {
address target = address(core);
// slither-disable-next-line assembly
assembly ("memory-safe") {
let args := mload(0x40)
// Selector of lock()
mstore(args, shl(224, 0xf83d08ba))
// We only copy the data, not the length, because the length is read from the calldata size
let len := mload(data)
mcopy(add(args, 4), add(data, 32), len)
// If the call failed, pass through the revert
if iszero(call(gas(), target, 0, args, add(len, 36), 0, 0)) {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
}
function _locked(bytes calldata swapData) internal {
// For partial swaps this is not equivalent to the given input amount
uint128 tokenInDebtAmount = 0;
int128 nextAmountIn = int128(uint128(bytes16(swapData[0:16])));
address receiver = address(bytes20(swapData[16:36]));
address tokenIn = address(bytes20(swapData[36:POOL_DATA_OFFSET]));
address nextTokenIn = tokenIn;
uint256 hopsLength = (swapData.length - POOL_DATA_OFFSET) / HOP_BYTE_LEN;
uint256 offset = POOL_DATA_OFFSET;
for (uint256 i = 0; i < hopsLength; i++) {
address nextTokenOut =
address(bytes20(LibBytes.loadCalldata(swapData, offset)));
Config poolConfig =
Config.wrap(LibBytes.loadCalldata(swapData, offset + 20));
(address token0, address token1, bool isToken1) = nextTokenIn
> nextTokenOut
? (nextTokenOut, nextTokenIn, true)
: (nextTokenIn, nextTokenOut, false);
(int128 delta0, int128 delta1) = core.swap_611415377(
EkuboPoolKey(token0, token1, poolConfig),
nextAmountIn,
isToken1,
isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO,
0
);
if (tokenInDebtAmount == 0) {
tokenInDebtAmount = uint128(isToken1 ? delta1 : delta0);
}
nextTokenIn = nextTokenOut;
nextAmountIn = -(isToken1 ? delta0 : delta1);
offset += HOP_BYTE_LEN;
}
_pay(tokenIn, tokenInDebtAmount);
core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn));
}
function _pay(address token, uint128 amount) internal {
address target = address(core);
if (token == NATIVE_TOKEN_ADDRESS) {
SafeTransferLib.safeTransferETH(target, amount);
} else {
// slither-disable-next-line assembly
assembly ("memory-safe") {
let free := mload(0x40)
// selector of pay(address)
mstore(free, shl(224, 0x0c11dedd))
mstore(add(free, 4), token)
mstore(add(free, 36), shl(128, amount))
// if it failed, pass through revert
if iszero(call(gas(), target, 0, free, 52, 0, 0)) {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
}
}
function _payCallback(bytes calldata payData) internal {
address token = address(bytes20(payData[12:32])); // This arg is abi-encoded
uint128 amount = uint128(bytes16(payData[32:48]));
SafeTransferLib.safeTransfer(token, address(core), amount);
}
// To receive withdrawals from Core
receive() external payable {}
modifier coreOnly() {
if (msg.sender != address(core)) revert EkuboExecutor__CoreOnly();
_;
}
}