286 lines
9.3 KiB
Solidity
286 lines
9.3 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.26;
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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, PoolKey} from "@ekubo/types/poolKey.sol";
|
|
import {
|
|
MAX_SQRT_RATIO,
|
|
MIN_SQRT_RATIO,
|
|
SqrtRatio
|
|
} from "@ekubo/types/sqrtRatio.sol";
|
|
import {RestrictTransferFrom} from "../RestrictTransferFrom.sol";
|
|
import "@openzeppelin/contracts/utils/Address.sol";
|
|
|
|
contract EkuboExecutor is
|
|
IExecutor,
|
|
ILocker,
|
|
IPayer,
|
|
ICallback,
|
|
RestrictTransferFrom
|
|
{
|
|
error EkuboExecutor__AddressZero();
|
|
error EkuboExecutor__InvalidDataLength();
|
|
error EkuboExecutor__CoreOnly();
|
|
error EkuboExecutor__UnknownCallback();
|
|
|
|
ICore immutable core;
|
|
address immutable mevResist;
|
|
|
|
uint256 constant POOL_DATA_OFFSET = 57;
|
|
uint256 constant HOP_BYTE_LEN = 52;
|
|
|
|
bytes4 constant LOCKED_SELECTOR = 0xb45a3c0e; // locked(uint256)
|
|
bytes4 constant PAY_CALLBACK_SELECTOR = 0x599d0714; // payCallback(uint256,address)
|
|
|
|
uint256 constant SKIP_AHEAD = 0;
|
|
|
|
using SafeERC20 for IERC20;
|
|
|
|
constructor(address _core, address _mevResist, address _permit2)
|
|
RestrictTransferFrom(_permit2)
|
|
{
|
|
core = ICore(_core);
|
|
|
|
if (_mevResist == address(0)) {
|
|
revert EkuboExecutor__AddressZero();
|
|
}
|
|
mevResist = _mevResist;
|
|
}
|
|
|
|
function swap(uint256 amountIn, bytes calldata data)
|
|
external
|
|
payable
|
|
returns (uint256 calculatedAmount)
|
|
{
|
|
if (data.length < 92) revert EkuboExecutor__InvalidDataLength();
|
|
|
|
// amountIn must be at most type(int128).MAX
|
|
calculatedAmount =
|
|
uint256(_lock(bytes.concat(bytes16(uint128(amountIn)), data)));
|
|
}
|
|
|
|
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]);
|
|
|
|
bytes memory result = "";
|
|
if (selector == LOCKED_SELECTOR) {
|
|
int128 calculatedAmount = _locked(stripped);
|
|
result = abi.encodePacked(calculatedAmount);
|
|
} else if (selector == PAY_CALLBACK_SELECTOR) {
|
|
_payCallback(stripped);
|
|
} else {
|
|
revert EkuboExecutor__UnknownCallback();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function verifyCallback(bytes calldata) public view coreOnly {}
|
|
|
|
function locked(uint256) external coreOnly {
|
|
// Without selector and locker id
|
|
int128 calculatedAmount = _locked(msg.data[36:]);
|
|
// slither-disable-next-line assembly
|
|
assembly ("memory-safe") {
|
|
mstore(0, calculatedAmount)
|
|
return(0x10, 16)
|
|
}
|
|
}
|
|
|
|
function payCallback(
|
|
uint256,
|
|
address /*token*/
|
|
)
|
|
external
|
|
coreOnly
|
|
{
|
|
// Without selector and locker id
|
|
_payCallback(msg.data[36:]);
|
|
}
|
|
|
|
function _lock(bytes memory data) internal returns (uint128 swappedAmount) {
|
|
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())
|
|
}
|
|
|
|
returndatacopy(0, 0, 16)
|
|
swappedAmount := shr(128, mload(0))
|
|
}
|
|
}
|
|
|
|
function _locked(bytes calldata swapData) internal returns (int128) {
|
|
int128 nextAmountIn = int128(uint128(bytes16(swapData[0:16])));
|
|
uint128 tokenInDebtAmount = uint128(nextAmountIn);
|
|
TransferType transferType = TransferType(uint8(swapData[16]));
|
|
address receiver = address(bytes20(swapData[17:37]));
|
|
address tokenIn = address(bytes20(swapData[37:57]));
|
|
|
|
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,
|
|
SqrtRatio sqrtRatioLimit
|
|
) = nextTokenIn > nextTokenOut
|
|
? (nextTokenOut, nextTokenIn, true, MAX_SQRT_RATIO)
|
|
: (nextTokenIn, nextTokenOut, false, MIN_SQRT_RATIO);
|
|
|
|
PoolKey memory pk = PoolKey(token0, token1, poolConfig);
|
|
|
|
int128 delta0;
|
|
int128 delta1;
|
|
|
|
if (poolConfig.extension() == mevResist) {
|
|
(delta0, delta1) = abi.decode(
|
|
_forward(
|
|
mevResist,
|
|
abi.encode(
|
|
pk,
|
|
nextAmountIn,
|
|
isToken1,
|
|
sqrtRatioLimit,
|
|
SKIP_AHEAD
|
|
)
|
|
),
|
|
(int128, int128)
|
|
);
|
|
} else {
|
|
// slither-disable-next-line calls-loop
|
|
(delta0, delta1) = core.swap_611415377(
|
|
pk, nextAmountIn, isToken1, sqrtRatioLimit, SKIP_AHEAD
|
|
);
|
|
}
|
|
|
|
nextTokenIn = nextTokenOut;
|
|
nextAmountIn = -(isToken1 ? delta0 : delta1);
|
|
|
|
offset += HOP_BYTE_LEN;
|
|
}
|
|
|
|
_pay(tokenIn, tokenInDebtAmount, transferType);
|
|
core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn));
|
|
return nextAmountIn;
|
|
}
|
|
|
|
function _forward(address to, bytes memory data)
|
|
internal
|
|
returns (bytes memory result)
|
|
{
|
|
address target = address(core);
|
|
|
|
// slither-disable-next-line assembly
|
|
assembly ("memory-safe") {
|
|
// We will store result where the free memory pointer is now, ...
|
|
result := mload(0x40)
|
|
|
|
// But first use it to store the calldata
|
|
|
|
// Selector of forward(address)
|
|
mstore(result, shl(224, 0x101e8952))
|
|
mstore(add(result, 4), to)
|
|
|
|
// We only copy the data, not the length, because the length is read from the calldata size
|
|
let len := mload(data)
|
|
mcopy(add(result, 36), add(data, 32), len)
|
|
|
|
// If the call failed, pass through the revert
|
|
if iszero(call(gas(), target, 0, result, add(36, len), 0, 0)) {
|
|
returndatacopy(result, 0, returndatasize())
|
|
revert(result, returndatasize())
|
|
}
|
|
|
|
// Copy the entire return data into the space where the result is pointing
|
|
mstore(result, returndatasize())
|
|
returndatacopy(add(result, 32), 0, returndatasize())
|
|
|
|
// Update the free memory pointer to be after the end of the data, aligned to the next 32 byte word
|
|
mstore(
|
|
0x40,
|
|
and(add(add(result, add(32, returndatasize())), 31), not(31))
|
|
)
|
|
}
|
|
}
|
|
|
|
function _pay(address token, uint128 amount, TransferType transferType)
|
|
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))
|
|
mstore(add(free, 52), shl(248, transferType))
|
|
|
|
// 4 (selector) + 32 (token) + 16 (amount) + 1 (transferType) = 53
|
|
if iszero(call(gas(), target, 0, free, 53, 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]));
|
|
TransferType transferType = TransferType(uint8(payData[48]));
|
|
_transfer(address(core), transferType, token, amount);
|
|
}
|
|
|
|
// To receive withdrawals from Core
|
|
receive() external payable {}
|
|
|
|
modifier coreOnly() {
|
|
if (msg.sender != address(core)) revert EkuboExecutor__CoreOnly();
|
|
_;
|
|
}
|
|
}
|