Merge branch 'refs/heads/main' into feat/bebop-rfq-encoder-and-executor

# Conflicts:
#	config/executor_addresses.json
#	foundry/scripts/deploy-executors.js
#	foundry/test/TychoRouterSequentialSwap.t.sol
#	foundry/test/assets/calldata.txt
#	src/encoding/models.rs
#	tests/common/mod.rs

Took 21 minutes
This commit is contained in:
Diana Carvalho
2025-08-08 14:40:03 +01:00
54 changed files with 5428 additions and 659 deletions

View File

@@ -9,8 +9,12 @@ 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";
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";
@@ -21,11 +25,13 @@ contract EkuboExecutor is
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;
@@ -33,12 +39,19 @@ contract EkuboExecutor is
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 _permit2)
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)
@@ -141,19 +154,40 @@ contract EkuboExecutor is
Config poolConfig =
Config.wrap(LibBytes.loadCalldata(swapData, offset + 20));
(address token0, address token1, bool isToken1) = nextTokenIn
> nextTokenOut
? (nextTokenOut, nextTokenIn, true)
: (nextTokenIn, nextTokenOut, false);
(
address token0,
address token1,
bool isToken1,
SqrtRatio sqrtRatioLimit
) = nextTokenIn > nextTokenOut
? (nextTokenOut, nextTokenIn, true, MAX_SQRT_RATIO)
: (nextTokenIn, nextTokenOut, false, MIN_SQRT_RATIO);
// slither-disable-next-line calls-loop
(int128 delta0, int128 delta1) = core.swap_611415377(
EkuboPoolKey(token0, token1, poolConfig),
nextAmountIn,
isToken1,
isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO,
0
);
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);
@@ -166,6 +200,45 @@ contract EkuboExecutor is
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
{

View File

@@ -0,0 +1,31 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import {SignedOrder} from "./IStructs.sol";
import {IReactorCallback} from "./IReactorCallback.sol";
/// @notice Interface for order execution reactors
interface IReactor {
/// @notice Execute a single order
/// @param order The order definition and valid signature to execute
function execute(SignedOrder calldata order) external payable;
/// @notice Execute a single order using the given callback data
/// @param order The order definition and valid signature to execute
function executeWithCallback(
SignedOrder calldata order,
bytes calldata callbackData
) external payable;
/// @notice Execute the given orders at once
/// @param orders The order definitions and valid signatures to execute
function executeBatch(SignedOrder[] calldata orders) external payable;
/// @notice Execute the given orders at once using a callback with the given callback data
/// @param orders The order definitions and valid signatures to execute
/// @param callbackData The callbackData to pass to the callback
function executeBatchWithCallback(
SignedOrder[] calldata orders,
bytes calldata callbackData
) external payable;
}

View File

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "./IStructs.sol";
/// @notice Callback for executing orders through a reactor.
interface IReactorCallback {
/// @notice Called by the reactor during the execution of an order
/// @param resolvedOrders Has inputs and outputs
/// @param fillData The fillData specified for an order execution
/// @dev Must have approved each token and amount in outputs to the msg.sender
function reactorCallback(
ResolvedOrder[] memory resolvedOrders,
bytes memory fillData
) external;
}

View File

@@ -0,0 +1,114 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
/// @dev external struct including a generic encoded order and swapper signature
/// The order bytes will be parsed and mapped to a ResolvedOrder in the concrete reactor contract
struct SignedOrder {
bytes order;
bytes sig;
}
struct OrderInfo {
// The address of the reactor that this order is targeting
// Note that this must be included in every order so the swapper
// signature commits to the specific reactor that they trust to fill their order properly
address reactor;
// The address of the user which created the order
// Note that this must be included so that order hashes are unique by swapper
address swapper;
// The nonce of the order, allowing for signature replay protection and cancellation
uint256 nonce;
// The timestamp after which this order is no longer valid
uint256 deadline;
// Custom validation contract
address additionalValidationContract;
// Encoded validation params for additionalValidationContract
bytes additionalValidationData;
}
/// @dev tokens that need to be sent from the swapper in order to satisfy an order
struct InputToken {
address token;
uint256 amount;
// Needed for dutch decaying inputs
uint256 maxAmount;
}
/// @dev tokens that need to be received by the recipient in order to satisfy an order
struct OutputToken {
address token;
uint256 amount;
address recipient;
}
/// @dev generic concrete order that specifies exact tokens which need to be sent and received
struct ResolvedOrder {
OrderInfo info;
InputToken input;
OutputToken[] outputs;
bytes sig;
bytes32 hash;
}
struct DutchOutput {
address token;
uint256 startAmount;
uint256 endAmount;
address recipient;
}
struct DutchInput {
address token;
uint256 startAmount;
uint256 endAmount;
}
struct ExclusiveDutchOrder {
OrderInfo info;
uint256 decayStartTime;
uint256 decayEndTime;
address exclusiveFiller;
uint256 exclusivityOverrideBps;
DutchInput input;
DutchOutput[] outputs;
}
struct DutchOrder {
OrderInfo info;
uint256 decayStartTime;
uint256 decayEndTime;
address exclusiveFiller;
uint256 exclusivityOverrideBps;
DutchInput input;
DutchOutput[] outputs;
}
struct CosignerData {
// The time at which the DutchOutputs start decaying
uint256 decayStartTime;
// The time at which price becomes static
uint256 decayEndTime;
// The address who has exclusive rights to the order until decayStartTime
address exclusiveFiller;
// The amount in bps that a non-exclusive filler needs to improve the outputs by to be able to fill the order
uint256 exclusivityOverrideBps;
// The tokens that the swapper will provide when settling the order
uint256 inputAmount;
// The tokens that must be received to satisfy the order
uint256[] outputAmounts;
}
struct V2DutchOrder {
// generic order information
OrderInfo info;
// The address which must cosign the full order
address cosigner;
// The tokens that the swapper will provide when settling the order
DutchInput baseInput;
// The tokens that must be received to satisfy the order
DutchOutput[] baseOutputs;
// signed over by the cosigner
CosignerData cosignerData;
// signature from the cosigner over (orderHash || cosignerData)
bytes cosignature;
}

View File

@@ -0,0 +1,164 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "./IReactor.sol";
import "./IReactorCallback.sol";
import {
SafeERC20,
IERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import {TychoRouter} from "../TychoRouter.sol";
error UniswapXFiller__AddressZero();
error UniswapXFiller__BatchExecutionNotSupported();
contract UniswapXFiller is AccessControl, IReactorCallback {
using SafeERC20 for IERC20;
// UniswapX V2DutchOrder Reactor
IReactor public immutable reactor;
address public immutable tychoRouter;
address public immutable nativeAddress;
// keccak256("NAME_OF_ROLE") : save gas on deployment
bytes32 public constant REACTOR_ROLE =
0x39dd1d7269516fc1f719706a5e9b05cdcb1644978808b171257d9a8eab55dd57;
bytes32 public constant EXECUTOR_ROLE =
0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63;
event Withdrawal(
address indexed token, uint256 amount, address indexed receiver
);
constructor(
address _tychoRouter,
address _reactor,
address _native_address
) {
if (_tychoRouter == address(0)) revert UniswapXFiller__AddressZero();
if (_reactor == address(0)) revert UniswapXFiller__AddressZero();
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(REACTOR_ROLE, address(_reactor));
tychoRouter = _tychoRouter;
reactor = IReactor(_reactor);
// slither-disable-next-line missing-zero-check
nativeAddress = _native_address;
}
function execute(SignedOrder calldata order, bytes calldata callbackData)
external
onlyRole(EXECUTOR_ROLE)
{
reactor.executeWithCallback(order, callbackData);
}
function reactorCallback(
ResolvedOrder[] calldata resolvedOrders,
bytes calldata callbackData
) external onlyRole(REACTOR_ROLE) {
require(
resolvedOrders.length == 1,
UniswapXFiller__BatchExecutionNotSupported()
);
ResolvedOrder memory order = resolvedOrders[0];
bool tokenInApprovalNeeded = bool(uint8(callbackData[0]) == 1);
bool tokenOutApprovalNeeded = bool(uint8(callbackData[1]) == 1);
bytes calldata tychoCalldata = bytes(callbackData[2:]);
// The TychoRouter will take the input tokens from the filler
if (tokenInApprovalNeeded) {
// Native ETH input is not supported by UniswapX
IERC20(order.input.token).forceApprove(
tychoRouter, type(uint256).max
);
}
// slither-disable-next-line low-level-calls
(bool success, bytes memory result) = tychoRouter.call(tychoCalldata);
if (!success) {
revert(
string(
result.length > 0
? result
: abi.encodePacked("Execution failed")
)
);
}
if (tokenOutApprovalNeeded) {
// Multiple outputs are possible when taking fees - but token itself should
// not change.
OutputToken memory output = order.outputs[0];
if (output.token != nativeAddress) {
IERC20 token = IERC20(output.token);
token.forceApprove(address(reactor), type(uint256).max);
} else {
// With native ETH - the filler is responsible for transferring back
// to the reactor.
Address.sendValue(payable(address(reactor)), output.amount);
}
}
}
/**
* @dev Allows granting roles to multiple accounts in a single call.
*/
function batchGrantRole(bytes32 role, address[] memory accounts)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
for (uint256 i = 0; i < accounts.length; i++) {
_grantRole(role, accounts[i]);
}
}
/**
* @dev Allows withdrawing any ERC20 funds.
*/
function withdraw(IERC20[] memory tokens, address receiver)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
if (receiver == address(0)) revert UniswapXFiller__AddressZero();
for (uint256 i = 0; i < tokens.length; i++) {
// slither-disable-next-line calls-loop
uint256 tokenBalance = tokens[i].balanceOf(address(this));
if (tokenBalance > 0) {
emit Withdrawal(address(tokens[i]), tokenBalance, receiver);
tokens[i].safeTransfer(receiver, tokenBalance);
}
}
}
/**
* @dev Allows withdrawing any NATIVE funds.
*/
function withdrawNative(address receiver)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
if (receiver == address(0)) revert UniswapXFiller__AddressZero();
uint256 amount = address(this).balance;
if (amount > 0) {
emit Withdrawal(address(0), amount, receiver);
Address.sendValue(payable(receiver), amount);
}
}
/**
* @dev Allows this contract to receive native token with empty msg.data from contracts
*/
// slither-disable-next-line locked-ether
receive() external payable {
require(msg.sender.code.length != 0);
}
}