Skip to content

API ReferenceSmart contracts

Reference

Smart contracts

Four contracts, Solidity 0.8.30, cancun EVM (transient storage), compiled with via_ir. Sources in contracts/src.

ContractDeployed?Holds funds?Owner powers
ExecutorYesNever between transactionsFee (≤ 1%), WHYPE allow-list, pause, ownership
OrderBookYesEscrow of open ordersKeeper allow-list, ownership
FeeCollectorYesRouter fees until convertedKeeper, treasury, target token, sweep, ownership
TokenProbeNo: injected into eth_call by state overrideNoNone

The canonical Permit2 (0x000000000022D473030F116dDEE9F6B43aC78BA3) is used, not deployed.


Executor

Executes and quotes routes by calling pools directly. One Executor serves every V2/V3/V4 factory on the chain.

Entry points

function swap(
  address tokenIn,        // ERC20, or address(0) for native HYPE (sent as msg.value)
  address tokenOut,       // ERC20, or address(0) to receive HYPE
  uint256 amountIn,
  uint256 minOut,         // checked on what `recipient` actually receives
  address recipient,
  address wrappedNative,  // allow-listed WHYPE for wrap/unwrap (ignored if no native side)
  uint256 deadline,
  Split[] calldata splits
) external payable returns (uint256 amountOut);

function swapWithPermit2(..., same 8 args ..., uint256 permitNonce, uint256 permitDeadline, bytes calldata signature)
  external returns (uint256 amountOut);   // ERC20 input pulled from msg.sender via Permit2

function quote(Split[] calldata splits) external returns (uint256 amountOut);  // call with eth_call only

Route encoding

struct Split { uint256 amountIn; Hop[] hops; }
struct Hop {
  address pool;       // the pool; KIND_REWRAP: the WHYPE to wrap into; KIND_V4: the PoolManager
  bool    zeroForOne; // token0 -> token1
  uint8   kind;       // see below
  uint16  v2FeeBps;   // KIND_V2 only: the pair's fee (30 = 0.3%)
  bytes   extra;      // KIND_V4 only: abi.encode(PoolKey, wrapper)
}
kindNameVenuesHow the hop runs
0KIND_V3Uniswap V3 and forks; also Slipstream and Algebra Integral (same swap ABI)pool.swap(this, zeroForOne, +amountIn, priceLimit, ""); the pool calls uniswapV3SwapCallback (or algebraSwapCallback) and the Executor pays exactly what is owed
1KIND_V2Uniswap V2 and forksTransfer the input to the pair, compute the output from reserves and the amount actually received (balanceOf(pair) − reserve) with v2FeeBps, call pair.swap
2KIND_REWRAPWHYPE ↔ WHYPEwithdraw the current WHYPE, deposit into hop.pool. Both must be allow-listed
3KIND_V4Uniswap v4 (with hooks)PoolManager.unlock → in unlockCallback: swap, then sync/settle the input and take the output. Native sides are unwrapped/wrapped around the hop with wrapper
4KIND_SOLIDLYSolidly family: Velodrome v2, Aerodrome, Nest classic, Ramses legacy (volatile and stable)Transfer the input to the pair, price the output with pair.getAmountOut(actualIn, tokenIn), call pair.swap

Kind 4 and the Algebra/Slipstream support are the Nest and Aero adapters; see Supported venues.

Rules checked on-chain:

  • Σ splits[i].amountIn == amountIn, each leg's first hop starts at tokenIn (or its WHYPE) and its last hop ends at tokenOut (or its WHYPE). Otherwise InvalidRoute().
  • Within a leg, hop i's output is hop i+1's input; amounts are measured by balance deltas, so fee-on-transfer tokens work at every step. A taxed input is scaled across the legs.
  • Legs run one after another; legs that share a pool see each other's impact (the API only builds pool-disjoint legs).
  • The price limit is the extreme tick, so a V3 hop is limited only by liquidity. If a pool can't take the whole input: PartialFill().

Execution steps

  1. deadline, pause and reentrancy checks.
  2. Pull the input (transferFrom or Permit2), or wrap msg.value. Record what actually arrived.
  3. Run every leg.
  4. fee = floor(totalOut × feeBps / 10,000) → feeRecipient.
  5. Send totalOut − fee to recipient (unwrapping if tokenOut is native). Measure the recipient's balance before and after; if it grew by less than minOut: InsufficientOutput(received, minOut).
  6. emit Swapped(sender, recipient, tokenIn, tokenOut, amountIn, received, fee).

Quoting

quote(splits) runs each leg's real swaps. In the V3 callback (and in v4's unlockCallback in quote mode) the Executor reverts with the amounts instead of paying, the QuoterV2 technique, and catches the revert to continue to the next hop. V2/Solidly hops are computed from reserves / getAmountOut. quote returns the total output after the router fee. It needs no balance, approval or ownership of anything, and is meant for eth_call only. The API batches 20 of them in one Multicall3 call.

Callback security

The Executor pays pools from inside callbacks, so callbacks are the attack surface. All checks use transient storage (EIP-1153), cleared at the end of the transaction:

SlotSet before calling a poolChecked in the callback
activePoolThe pool (or PoolManager) about to be calledmsg.sender == activePool, otherwise Unauthorized()
expectedPayTokenThe hop's input tokenThe token paid is this one, not what the pool's data says
expectedPayMaxThe hop's input amountThe amount paid never exceeds it
expectedV4Keykeccak256(PoolKey, zeroForOne)The v4 swap is the one we asked for
lockedReentrancy guard

So a malicious pool (or a contract posing as one) can't get paid in another token, get paid more than the hop's input, or call back outside its own swap. unlockCallback treats every non-quote mode as a payment path and requires the active PoolManager. receive() accepts HYPE only from allow-listed WHYPEs or the active pool.

Admin

FunctionEffectGuard
setFee(feeBps, feeRecipient)Router fee and where it goesfeeBps ≤ 100; recipient ≠ 0 and ≠ Executor
setWrappedNative(token, allowed)WHYPE allow-list
setPaused(bool)Stops swap/swapWithPermit2 (and so order fills). quote keeps working
transferOwnership(newOwner) / acceptOwnership()Two-step ownership transferNew owner must accept

The owner cannot move users' funds: the Executor holds none, and there is no sweep or arbitrary-call function.

Events and errors

event Swapped(address indexed sender, address indexed recipient, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, uint256 fee);
event FeeConfigUpdated(uint16 feeBps, address feeRecipient);
event WrappedNativeUpdated(address token, bool allowed);
event PausedSet(bool paused);
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

error Expired(); error InvalidRoute(); error InvalidNativeConfig(); error PartialFill();
error InsufficientOutput(uint256 amountOut, uint256 minOut); error Unauthorized(); error TransferFailed();
error FeeTooHigh(); error IsPaused();

OrderBook

Escrow for limit orders and DCA. Full behavior: Limit orders and DCA.

constructor(Executor executor, address owner);
function create(address tokenIn, address tokenOut, uint256 total, uint256 slices, uint256 minOutPerSlice,
                uint64 interval, uint64 expiry) external payable returns (uint256 id);
function fill(uint256 id, address wrappedNative, Executor.Split[] calldata splits, uint256 fillerMinOut)
  external returns (uint256 amountOut);                      // allow-listed keepers only
function cancel(uint256 id) external;                        // owner any time; anyone after expiry; refund to owner
function cancelTo(uint256 id, address to) external;          // owner only; refund to `to`
function setKeeper(address keeper, bool allowed) external;   // owner
function transferOwnership(address) external; function acceptOwnership() external;
CheckError
tokenIn ≠ tokenOut, total ≥ slices > 0, expiry in the future, minOutPerSlice > 0, interval > 0 if slices > 1InvalidOrder()
Native: msg.value == total; ERC20: msg.value == 0 and the full total arrives (no fee-on-transfer)InvalidOrder()
fill by a non-keeperUnauthorized()
fill before nextFillAt, after expiry, or on an empty orderNotFillable()
cancel by a stranger before expiryUnauthorized()

A fill sends exactly the slice (the last slice takes the remainder), approves the Executor for that exact amount, and calls Executor.swap(..., minOut = max(slice floor, fillerMinOut), recipient = owner, deadline = now). State is updated before the external call.

Events: OrderCreated, OrderFilled, OrderCancelled, KeeperSet.


FeeCollector

The Executor's feeRecipient. Converts collected fees into one target token for the treasury.

constructor(Executor executor, address owner, address keeper, address treasury, address target); // target 0 = native HYPE
function convert(address token, uint256 amount, uint256 minOut, address wrappedNative, Executor.Split[] calldata splits)
  external;                                    // keeper or owner: swap `amount` of `token` into `target`, output to treasury
function sweep(address token, uint256 amount) external;            // owner: send as-is to the treasury
function configure(address keeper, address treasury, address target) external;  // owner
function transferOwnership(address) external; function acceptOwnership() external;

Output always goes to the treasury, never to the caller, so a compromised keeper key can at worst convert at a bad price within minOut; the API's keeper guards minOut with a price EMA (see Fees).

Events: Converted(token, amountIn, amountOut), ConfigUpdated(keeper, treasury, target).


TokenProbe

Never deployed. The API injects its bytecode at a fixed address with an eth_call state override, plus 1 HYPE of balance, and calls:

function probe(address ex, address token, address wrappedNative, Executor.Split[] calldata buy, Executor.Hop[] calldata sellHops)
  external returns (Result memory);
struct Result { uint256 quotedBuy; uint256 received; uint256 transferSent; uint256 transferReceived;
                uint256 quotedSell; uint256 sold; bool sellOk; }

It buys through the Executor, transfers to a second address, and sells back, measuring each step. It uses low-level calls so tokens that don't return a bool (USDT-style) work. See Token safety.


Deployment

contracts/script/Deploy.s.sol deploys all three and wires them:

EnvRequiredDefaultUsed for
WRAPPED_NATIVES✅Comma-separated WHYPEs to allow-list
FEE_BPS5Router fee
FEE_RECIPIENTthe FeeCollectorSet it to bypass the FeeCollector
KEEPERdeployerOrderBook keeper and FeeCollector keeper
TREASURYdeployerFeeCollector output
DEPLOYER_KEY--private-key

Then contracts/script/verify.sh verifies them on the explorer. Full runbook: Running and deploying.

Tests

cd contracts && forge test:

  • Fork tests against Elysium testnet pinned at block 24,094,443 (FORK_BLOCK to override): Executor, OrderBook, fee-on-transfer, FeeCollector, V4 with hooks, Permit2, launchpad.
  • Adapter fork tests (test/Adapters.fork.t.sol): Aerodrome volatile, stable and 2-hop, and Slipstream on a Base fork; Algebra (Camelot) on an Arbitrum fork. Quote equals the executed amount.
  • Mainnet launchpad fork tests (test/Launchpads.mainnet.fork.t.sol, skipped without ELYSIUM_MAINNET_RPC_URL; MAINNET_FORK_BLOCK to pin): buy and sell through the Kleos tax hook, ElysPump and Ascend (dynamic fee, ERC20 WHYPE) with exact quotes and an empty Executor.
  • Liquidity replay (test/LiquidityV3.fork.t.sol): the V3 add transactions the API built (fixture) replayed byte for byte on a fork; position minted with exactly the computed liquidity and range.
  • Liquidity on Slipstream and Algebra (test/LiquidityL2.fork.t.sol): mints built by the API's encoders replayed on Aero (Base) and Camelot (Arbitrum) forks.
  • Fork-free fuzz and invariant tests (test/Invariants.t.sol) with mocks of honest and hostile venues (greedy pool, forged callback, partial fill, 5% tax token). Invariants: the Executor ends every call with zero balance; each swap equals its quote; the fee is exact; the OrderBook's escrow equals the sum of open orders.
lane.markets · Make the most of every move.