Extruction

Extruction is a SwapVM instruction that delegates swap logic to an external, maker-controlled contract. It lets makers implement arbitrary pricing and control-flow logic that cannot be expressed with the built-in SwapVM instruction set, for example a proprietary pricing model that stays in the maker's own contract.

Source: src/instructions/Extruction.sol

Extruction is one of the opcodes in the Aqua instruction set (Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap, Extruction) inherited by the deployed AquaSwapVMRouter. It corresponds to the "external pricing" authoring path: instead of composing existing opcodes, the maker embeds custom logic behind IExtruction / IStaticExtruction.


Args encoding

Field Offset Size Description
target 0 20 bytes (address) External contract implementing IExtruction / IStaticExtruction
extructionArgs 20 N bytes Passed verbatim to the external contract's extruction() function

Instructions

_extruction

Solidity
1
function _extruction(Context memory ctx, bytes calldata args) internal

Calls the external target contract and applies its returned state to the swap context.

In quote mode (isStaticContext = true): calls IStaticExtruction.extruction(...) via a regular call (view function).

In swap mode (isStaticContext = false): calls IExtruction.extruction(...).

Both interfaces have the same signature:

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
function extruction(
    bool isStaticContext,
    uint256 nextPC,
    SwapQuery calldata query,
    SwapRegisters calldata swap,
    bytes calldata args,
    bytes calldata takerData
) external [view] returns (
    uint256 updatedNextPC,
    uint256 choppedLength,
    SwapRegisters memory updatedSwap
);

The returned values replace ctx.vm.nextPC and ctx.swap. choppedLength bytes are consumed from takerData.

Errors

Error Condition
ExtructionMissingTargetArg() args shorter than 20 bytes
ExtructionChoppedExceededLength(chopped, requested) takerData has fewer bytes than choppedLength

External contract requirements

Extruction relies on the SwapVM invariant that a static quote and the executed swap return identical amounts. Both IExtruction and IStaticExtruction must produce the same swap amounts for the same inputs, so that quote() and swap() agree. Non-deterministic behavior causes quote/swap inconsistency.

The target contract must be non-upgradeable. Upgradeable logic can change between the quote and the swap, which would break this invariant.

Security considerations for takers:

  • The target is a maker-controlled address; verify it is non-upgradeable or has trusted governance
  • Test quote/swap consistency before routing significant volume
  • Slippage protection in the outer swap provides a backstop but cannot prevent all inconsistencies

  • Controls — built-in control flow (jumps, deadlines)

Extruction additions

These additions expand the Extruction opcode reference with the exact register types it operates on, a minimal compilable target contract, and a definitive answer to the IExtruction vs IStaticExtruction question. Every type and signature below is taken verbatim from 1inch/swap-vm at src/instructions/Extruction.sol and src/libs/VM.sol.

Type reference: SwapQuery and SwapRegisters

Both structs are declared in src/libs/VM.sol and passed to the target as calldata. SwapQuery is read-only swap context; SwapRegisters holds the mutable amounts the instruction computes. The register the instruction must fill is the missing amount: isExactIn ? amountOut : amountIn.

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/libs/VM.sol

/// @dev Read-only swap information
struct SwapQuery {
    bytes32 orderHash;   // per-maker position/strategy identifier
    address maker;       // liquidity provider
    address taker;       // swap initiator
    address tokenIn;     // input token
    address tokenOut;    // output token
    bool    isExactIn;   // true => amountIn fixed, compute amountOut
}

/// @dev Registers used to compute the missing amount:
///      isExactIn ? amountOut : amountIn
struct SwapRegisters {
    uint256 balanceIn;        // current balance of input token
    uint256 balanceOut;       // current balance of output token
    uint256 amountIn;         // input amount being swapped
    uint256 amountOut;        // output amount being swapped
    uint256 amountNetPulled;  // net pulled from maker (fee calculations)
}

SwapQuery has six fields (the trailing isExactIn is easy to miss) and SwapRegisters has five, all uint256. The instruction receives query as calldata and receives swap as calldata, but returns an updated SwapRegisters memory — you copy, mutate the copy, and return it.

Target call signature

The dispatcher in Extruction._extruction reads the first 20 bytes of args as the target address, slices the remainder as extructionArgs, and calls the target with this exact signature (identical for both interfaces):

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
function extruction(
    bool isStaticContext,
    uint256 nextPC,
    SwapQuery calldata query,
    SwapRegisters calldata swap,
    bytes calldata args,        // args.slice(20): everything after the 20-byte target
    bytes calldata takerData    // ctx.takerArgs()
) external returns (
    uint256 updatedNextPC,      // written back to ctx.vm.nextPC (arbitrary uint256 jump)
    uint256 choppedLength,      // taker bytes to consume; MUST NOT exceed remaining takerData
    SwapRegisters memory updatedSwap
);

choppedLength is validated after the call: _extruction reverts with ExtructionChoppedExceededLength if fewer than choppedLength taker bytes remain. Return 0 unless your strategy genuinely consumes taker-supplied data.

Reference IExtruction target

A minimal, compilable target that reads amountIn from the registers, applies a proprietary (here: fixed) price, and writes the missing amount back. It obeys both invariants: the price is immutable (the target is non-upgradeable, so logic cannot change between quote and swap), and the logic is a single view function — one implementation serves both the quote (STATICCALL) and swap (CALL) paths, which is the tightest possible guarantee that quote() == swap() return identical amounts.

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

// Illustrative import paths (in-repo these are relative to src/):
import { IExtruction, IStaticExtruction } from "./instructions/Extruction.sol";
import { SwapQuery, SwapRegisters } from "./libs/VM.sol";

/// @notice Fixed-price target. `priceE18` is immutable => non-upgradeable logic.
///         A single `view` implementation satisfies BOTH interface selectors,
///         so the quote path and the swap path are provably identical.
contract FixedPriceExtruction is IExtruction, IStaticExtruction {
    error RecomputeDetected();

    uint256 public immutable priceE18; // tokenOut per tokenIn, 1e18-scaled

    constructor(uint256 priceE18_) {
        priceE18 = priceE18_;
    }

    function extruction(
        bool,                       // isStaticContext (unused: logic is identical either way)
        uint256 nextPC,
        SwapQuery calldata query,
        SwapRegisters calldata swap,
        bytes calldata,             // args
        bytes calldata              // takerData
    ) external view override(IExtruction, IStaticExtruction)
        returns (uint256 updatedNextPC, uint256 choppedLength, SwapRegisters memory updatedSwap)
    {
        updatedSwap = swap;         // copy read-only registers into memory
        if (query.isExactIn) {
            require(swap.amountOut == 0, RecomputeDetected());       // no backward-jump recompute
            updatedSwap.amountOut = swap.amountIn * priceE18 / 1e18; // round down: maker-favorable
        } else {
            require(swap.amountIn == 0, RecomputeDetected());
            // round up amountIn: maker-favorable
            updatedSwap.amountIn = (swap.amountOut * 1e18 + priceE18 - 1) / priceE18;
        }
        updatedNextPC = nextPC;     // linear fall-through, no jump
        choppedLength = 0;          // consume no taker data
    }
}

The require(amount == 0) recompute guard mirrors the pattern in first-party instructions such as PeggedSwap (PeggedSwapRecomputeDetected). Because Extruction alone can set an arbitrary uint256 nextPC, a maker could jump backward into this instruction; the guard makes any such re-entry with an already-populated register revert instead of silently double-pricing, preserving quote/swap consistency.

IExtruction vs IStaticExtruction: one interface or two?

They are two distinct interfaces, both declared insrc/instructions/Extruction.sol. IStaticExtruction is a real source symbol — not a typo or an alias for IExtruction. The two differ only in state mutability:

Interface Mutability Called during Call type
IExtruction non-view (state-modifying) swap() CALL
IStaticExtruction view quote() STATICCALL

The dispatcher selects between them at runtime on ctx.vm.isStaticContext:

// src/instructions/Extruction.sol — _extruction(...)
if (ctx.vm.isStaticContext) {
    (ctx.vm.nextPC, choppedLength, ctx.swap) =
        IStaticExtruction(target).extruction(/* ...view path (quote)... */);
} else {
    (ctx.vm.nextPC, choppedLength, ctx.swap) =
        IExtruction(target).extruction(/* ...state-modifying path (swap)... */);
}

Both declarations have the identical function name and parameter list, so they compute the same 4-byte selector — the view keyword does not affect selector calculation. This is the mechanism that lets one deployed contract serve both paths: a single extruction implementation answers both the IStaticExtruction STATICCALL and the IExtruction CALL.

Whether a single target implements both a view (quote) path and a non-view (swap) path is a choice left to the target author, and the safe choice is to implement exactly oneviewfunction (as the reference above does). A view implementation is callable under both STATICCALL and CALL and cannot diverge, so it cannot violate the identical-amounts invariant. If instead you deploy a genuinely state-modifying IExtruction path alongside a separate view IStaticExtruction path, you become responsible for proving the two return bit-identical amounts for every input — the source header of Extruction.sol states this as a CRITICAL SECURITY REQUIREMENT, and any drift breaks quote/swap consistency.

Custom pricing completeness (Path C)

Extruction is the external pricing authoring path — Path C — for makers whose pricing cannot be expressed by composing the built-in AMM opcodes (XYCSwap, XYCConcentrate, Decay, PeggedSwap). Instead of writing a curve as an opcode program, the maker embeds arbitrary Solidity behind IExtruction / IStaticExtruction and points one opcode at it. This section is the shipping checklist for a Path C strategy: the two hard requirements, the minimal interface surface, where the opcode sits in the deployed table, how it composes with native opcodes, and the invariants to fuzz. Every claim below is verified against src/instructions/Extruction.sol and src/opcodes/AquaOpcodes.sol at v1.0.1.

The two hard requirements

Both requirements are load-bearing. Violating either breaks the SwapVM guarantee that a static quote() and the executed swap() return identical amounts, and the source header of Extruction.sol marks them a CRITICAL SECURITY REQUIREMENT.

Requirement Why it is mandatory
The external target contract must be non-upgradeable Upgradeable logic can change between the quote and the swap. The taker quotes against one implementation and settles against another, so the amounts diverge. Pin pricing behind immutable state or an already-frozen implementation.
quote() and swap()must return identical amounts The IStaticExtruction path (quote, STATICCALL) and the IExtruction path (swap, CALL) must compute the same SwapRegisters for the same inputs. Any non-determinism — block state reads, mutable storage, divergent code paths — is an inconsistency the router cannot detect for you.

The tightest way to satisfy both at once is the single-view-function pattern shown in the reference target above: one view implementation is callable under both STATICCALL and CALL and cannot diverge, so it satisfies the identical-amounts invariant by construction.

Minimal interface surface

A Path C target implements exactly one function. IExtruction (non-view) and IStaticExtruction (view) are two distinct interfaces declared in Extruction.sol; they share an identical name and parameter list, so they compute the same 4-byte selector, and a single implementation answers both. The minimal surface is:

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
function extruction(
    bool isStaticContext,       // false => swap (CALL), true => quote (STATICCALL)
    uint256 nextPC,             // current program counter; return updatedNextPC
    SwapQuery calldata query,   // read-only swap context (6 fields)
    SwapRegisters calldata swap,// current registers (5 x uint256)
    bytes calldata args,        // extructionArgs: args after the 20-byte target
    bytes calldata takerData    // remaining taker args
) external [view] returns (
    uint256 updatedNextPC,
    uint256 choppedLength,      // taker bytes consumed; 0 unless you read takerData
    SwapRegisters memory updatedSwap
);

Fill the missing amount only: query.isExactIn ? updatedSwap.amountOut : updatedSwap.amountIn. See the type reference and reference target above for the full SwapQuery/SwapRegisters layout and a compilable example.

Opcode index 32: the register fold

In the deployed AquaSwapVMRouter's opcode table (AquaOpcodes._opcodes()), Extruction._extruction is opcode index 32. When the VM reaches that opcode, _extruction:

  1. reads the first 20 bytes of args as the target address (reverts ExtructionMissingTargetArg if args is shorter);
  2. calls the target — IStaticExtruction in quote mode, IExtruction in swap mode — selected on ctx.vm.isStaticContext;
  3. folds the return values into the live VM state: updatedSwap replaces ctx.swap, updatedNextPC replaces ctx.vm.nextPC, and choppedLength bytes are consumed from the taker args (reverts ExtructionChoppedExceededLength if fewer remain).

Because the returned SwapRegisters becomes the VM's working registers, whatever your target writes into amountIn/amountOut is what the rest of the program — and the final settlement — sees. The target does not return a price; it returns the swap registers.

Composition: native opcodes stay reachable

Extruction is one entry in the same opcode table as every first-party instruction — it does not replace the VM, it plugs custom pricing into it. A single program (the program half of order.data = hooks || program) can interleave the Extruction opcode with native opcodes in one run:

  • Controls_jump, _deadline, and the taker-balance guards remain available for control flow and preconditions around your pricing call.
  • Fee — the flat, protocol, and dynamic-protocol fee opcodes run in the same program, so a Path C strategy can price with custom logic and still take fees through native opcodes.

Extruction is the only opcode that can set an arbitrary uint256 nextPC. Makers must not use a backward jump into an Extruction instruction: re-executing the pricing on already-populated registers breaks consistency between quote() and swap(). Defend against it in the target with a recompute guard (revert when the register you are about to fill is already non-zero), mirroring PeggedSwap's PeggedSwapRecomputeDetected.

Invariant and fuzz-test checklist

Fuzz these properties against your target before routing significant volume. Only the first is enforced by the protocol's call structure; the rest are author responsibilities that the router cannot check for you.

Property What to assert
quote == swap For identical (isStaticContext-aside, nextPC, query, swap, args, takerData), the IStaticExtruction and IExtruction paths return bit-identical (updatedNextPC, choppedLength, updatedSwap). The single-view-function pattern makes this hold by construction; assert it anyway if you split the paths.
Monotonicity A larger amountIn never yields a smaller amountOut (exact-in), and a larger amountOut never demands a smaller amountIn (exact-out). A sane pricing curve is non-decreasing in the fixed leg.
No wrapped / spurious output Amounts are uint256 and cannot be negative — so guard every subtraction and division against underflow and against silently rounding to 0. Round consistently in the maker-favorable direction (down for amountOut, up for amountIn).
Reverts on bad args Malformed input must revert rather than return garbage: short args (< 20 bytes) surface as ExtructionMissingTargetArg upstream; a choppedLength exceeding remaining taker bytes surfaces as ExtructionChoppedExceededLength; out-of-range extructionArgs should revert inside your target.
Recompute guard Re-entry with an already-populated register reverts, so a backward jump into the instruction cannot double-price the same leg.

The router guarantees only that the quote and the swap dispatch to the same target with the same inputs. Correctness of the pricing math, its monotonicity, its rounding direction, and its revert behavior on bad input are all the target author's responsibility — Path C is an advanced, use-at-your-own-risk surface, and takers are expected to validate a strategy's quote/swap consistency before routing through it.

Did you find what you need?