Skip to content

Execution Safety Design

The execution arm of the platform is the on-chain proof manager: the contract that executes batched call sequences over pool funds. It holds no standing balances and has no discretionary authority — it can only execute calls that are simultaneously:

  1. Pre-approved by policy — the call must hash to a leaf in the Merkle tree installed for the executing strategist.
  2. Correctly shaped — an on-chain decoder contract extracts the address parameters from the calldata; if the extracted bytes do not match the leaf's address constraints, the proof fails and execution reverts.
  3. Escrow-bracketed — SDK-built flows always wrap token movement in escrow withdraw/deposit pairs, so funds are only ever in the execution manager transiently.

The DeFi SDK is the layer that produces both the calldata and the policy leaves, and its design stacks several protections around that authority:

Layer Mechanism What it prevents
Leaf hash Every authorized call is a (decoder, target, valueNonZero, selector, addresses) tuple hashed into the Merkle tree Calling any unapproved function on any unapproved contract
Address packing The SDK packs the leaf's address constraints byte-for-byte as the on-chain decoder extracts them Substituting tokens, receivers, markets, or routers inside an approved function
Manager binding Most leaves pack the executing proof manager's address into their constraints Redirecting outputs to any address other than the authorized manager
Escrow flows Fixed call ordering: withdraw → scoped approve → action → sweep approve → deposit Funds being stranded in, or extracted from, the execution manager
Input validation Address checksumming, slippage floors, selector allowlists, router pinning, fail-closed resolution Malformed calldata, MEV extraction, tampered API responses
Trust boundary The SDK never imports backend or database code; all runtime data arrives through an explicit context object Backend state leaking into, or being blindly trusted by, policy generation

The rest of this page elaborates these layers.

The policy and Merkle leaf system

The leaf hash invariant

Every leaf commits to exactly one authorized call shape:

leaf = keccak256(abi.encodePacked(
    decoder,        # deployed decoder-and-sanitizer contract address
    target,         # contract being called (e.g. Aave Pool, DEX router)
    valueNonZero,   # whether native value may be sent with the call
    selector,       # 4-byte function selector
    addresses,      # packed address constraints — the decoder's output
))

This hash must reproduce identically on-chain when the proof manager verifies a call, or the call reverts. The determinism and byte layout of the encoding are pinned by dedicated tests, and the same hash structure is enforced by the on-chain proof managers and decoders. Amounts are deliberately not part of the leaf: policies constrain where funds can go, not how much moves — amount safety comes from the escrow discipline and slippage validation described below.

Address packing follows one fixed convention per protocol family: standard protocols pack 20-byte-aligned EVM addresses; protocols with custom decoders (CCTP, for example) pack raw mixed-type bytes matching their decoder's extraction exactly.

The SDK ↔ backend contract

Two types form the entire interface between the SDK and the backend — LeafContext (runtime context the backend assembles and passes in: chain, deployed contract addresses, the decoder address, the proof manager address, and bridge destinations where relevant) and LeafSpec (one policy leaf the SDK produces and returns). Neither type carries anything database-specific, and the SDK never imports backend code. This keeps the trust boundary clean in both directions: policy generation cannot silently depend on backend state, and the backend cannot inject anything into a leaf except through the explicit, typed context.

One consequence of that boundary: when the SDK declares what a strategist may configure, token fields carry symbols only, never addresses — the backend resolves symbols to addresses through its own asset records, so the SDK stays entirely database-free.

The leaf lifecycle

Every action participates in a four-step lifecycle, from strategist intent to on-chain enforcement and back:

1  declare        each action declares its configurable surface
                  (token symbols, markets, vaults) — this feeds the
                  Intent Builder UI where a strategist configures a
                  policy
        │
        ▼
2  select         the strategist's selection is captured; the backend
                  resolves symbols to concrete addresses
        │
        ▼
3  expand         the SDK expands the selection into concrete policy
                  leaves — exact target, selector, and packed address
                  constraints matching the on-chain decoder
        │
        ▼
   install        the backend hashes the leaves into a Merkle tree;
                  the root is installed on the proof manager through
                  the timelock-backed, hub-side governance channel
        │
        ▼
   enforce        at execution time, every call in a batch must carry
                  a valid Merkle proof — the on-chain decoder extracts
                  the address parameters and the leaf is recomputed
                  and verified on-chain
        │
        ▼
4  reverse        leaves map back to the original selection, so an
                  installed tree can be reconstructed, reviewed, and
                  edited on the rebalance page

Steps 1–4 are SDK responsibilities; installation and enforcement are the backend's and the contracts' — see policy gating in the execution pipeline for the off-chain pre-flight and Arkonix Extensions for the on-chain half. The expansion step is fail-closed: if a required contract address is missing from the context, the action emits no leaves at all rather than a leaf with a guessed or defaulted address.

The escrow-flow discipline

Flows compose a single action's transaction into an ordered multi-call sequence executed atomically by the proof manager. Each emitted call carries a canonical function signature that must correspond to a policy leaf — including the escrow steps themselves — so the entire sequence, not just the DeFi action, is policy-verified.

The canonical five-call sequence

The core safety primitive is a fixed sequence of exactly five calls, in this order:

1  escrow withdraw   BalanceSheet.withdraw — pull exactly the input
                     amount from pool escrow to the proof manager;
                     funds exist in the manager only from this point
2  scoped approve    ERC-20 approve of the input token, scoped to the
                     action's target contract and the exact amount
3  the action        the DeFi call itself — swap, supply, bridge, ...
4  sweep approve     approve the escrow contract to pull the output
                     token
5  escrow deposit    BalanceSheet.deposit — sweep the FULL output
                     balance (including any positive slippage) back
                     into pool escrow; nothing is ever stranded in
                     the proof manager

This is why a compromised execution credential has a bounded blast radius: the manager holds funds only between steps 1 and 5 of an atomically executed, policy-verified batch; approvals are scoped to one target and one amount; and the terminal sweep leaves no residual balance or approval an attacker could later exploit. The exact call count and ordering are pinned by tests (see verified properties).

Flow shapes

A unified flow builder derives one of four shapes from an action's declared escrow behavior:

Withdraw leg Deposit leg Shape Used for
yes yes full five-call sequence swaps, PT trades
yes no withdraw → approve → action bridge-out and other exits where funds leave the chain or escrow
no yes action → approve → deposit bridge-in (claimed funds swept straight into escrow)
no no the action alone direct actions with no token movement (e.g. Hyperliquid orders via precompile)

Two safety nuances are worth noting:

  • Where the expected output is known, the deposit step uses the real minimum-output amount rather than a sweep-everything sentinel, because the escrow deposit performs an exact transfer.
  • Flows may insert bracketing calls around the action for protocols that need a standing authorization — the Morpho pattern is grant → act → revoke within a single transaction, so an operator authorization exists only for the duration of that transaction. Every inserted call must independently pass the Merkle policy: defense in depth, not a bypass.

Bridging follows the same discipline split across chains: the outbound leg is withdraw → approve → burn (no deposit-back; the funds leave the source chain), and the inbound leg is mint → approve → deposit, sweeping the bridged funds directly into escrow on arrival. Destination claiming is permissionless; an asynchronous attestation service watches the burn, polls the issuer's attestation API, and fails with a timeout rather than waiting forever.

Untrusted quote services

Aggregator integrations (Odos, OKX DEX, 1inch, Pendle, Enso) pair the on-chain action with an off-chain quoting service. API responses are treated as untrusted input and pass a validation gate before anything is signed:

strategist amount ──▶ quote service ──▶ aggregator API
                              │
                    quote (route, minimum out, price impact)
                              ▼
             validation gate: router allowlist, selector
             whitelist, slippage floor, checksum pass
                              ▼
             transaction request → flow builder → proof manager

Concretely: Enso rejects any returned bundle whose selector is not on an accepted-selector whitelist and exact-matches returned router and factory addresses against pinned canonical values (non-canonical targets are accepted only where a Merkle leaf independently pins them); Pendle verifies the API-returned router against the canonical router address; Odos re-derives the minimum output locally and pads slippage so the aggregator's executor cannot pocket positive slippage; OKX enforces a hard price-impact cap. And even if a compromised aggregator API defeated all of these, it still could not push the executor outside its Merkle policy — the on-chain leaf check is the final gate.

Safety pattern catalogue

Ten patterns recur across every protocol module:

  1. Mandatory address checksumming. Every address input is EIP-55 validated before it enters calldata or leaf packing.
  2. Pinned selectors with a self-check. Action metadata stores literal 4-byte selectors, and calldata encoders recompute the selector from the canonical signature at build time — signature/selector drift fails fast. Enso additionally rejects API calldata with unrecognized selectors.
  3. Slippage validation against MEV. PancakeSwap V3 actions hard-reject a zero or negative minimum output (an explicit anti-sandwich guard); Odos pads slippage against executor capture; OKX caps price impact; Pendle's guard is enforced inside the router calldata itself.
  4. Leaf packing mirrors the on-chain decoder byte-for-byte. The core invariant: the leaf hash must reproduce on-chain or execution reverts.
  5. Manager binding scopes leaves. Most leaves pack the proof manager's address into their constraints, so authorization is scoped to a single manager and outputs cannot be redirected.
  6. Fail-closed on unresolved context. Leaf expansion emits nothing when a required contract is absent; constructors raise descriptive errors when required addresses (vault, market, pool, chain) are unresolved — never a silently malformed call.
  7. Router-address allowlisting. API-returned targets are verified against pinned canonical routers; exceptions exist only where a Merkle leaf independently pins the target.
  8. The SDK is database-agnostic. All runtime resolution flows through the explicit leaf context; the SDK declares which deployment-manifest keys the backend must resolve and never reaches into backend state.
  9. Approval hygiene. Token approvals are emitted only when needed and tightly scoped (asset, spender, amount); native tokens skip approvals; escrow flows scope the action approval to the exact router and amount; standing authorizations are granted and revoked within one transaction.
  10. No key custody, no gas guessing. The SDK never handles private keys and never estimates gas itself — gas is estimated at execution time by the platform that signs, and signing lives in the KMS/HSM layer.

Protocol modules add their own guards on top: Aave E-Mode rejects out-of-range category values, Morpho refuses to act without an explicitly resolved market and packs the full market tuple (loan, collateral, oracle, interest-rate model, manager) into its leaves, PancakeSwap actions never attach native value, and Hyperliquid actions validate coins and order parameters against known asset maps.

Continuously verified properties

The SDK ships a test suite of roughly 450 tests covering every protocol module, all flow builders, all type models, and both base-class contracts. It runs on every change, alongside lint and strict static type checking — so the type contracts described on these pages are machine-enforced, not aspirational. The safety-critical properties pinned by dedicated tests:

Property
The escrow flow is exactly five calls, in the fixed canonical order
Emitted calldata begins with the correct function selector
The leaf hash is deterministic and byte-exact, including the address packing
Policy inputs distinguish FREE (unconstrained) from fixed-set constraints correctly
New protocol modules auto-register with the action registry
Registry policy, deployment-manifest, and configuration resolution behave as specified
The Morpho grant → act → revoke authorization bracket is produced correctly
Aggregator swaps survive escrow wrapping, including native-token paths

Worked example: Aave V3 supply

The Aave V3 module shows the complete pattern every protocol follows. Its actions cover supply, withdraw, borrow, repay, collateral toggles, and E-Mode — each mapping 1:1 to one Pool function, with the Pool address resolved per chain from the deployment manifest.

Calldata derivation. Constructing a supply action checksums every address and coerces numeric inputs; encoding recomputes the 4-byte selector from the canonical Solidity signature and ABI-encodes the parameters behind it. Supply and repay declare a token approval scoped to the Pool and the exact amount; withdraw and borrow do not. The receiving address defaults to the proof manager, so interest-bearing tokens accrue to — and withdrawals return to — the manager the policy authorizes, inside an escrow-bracketed flow.

Leaf derivation. The supply action declares its configurable surface as a list of supply-token symbols (borrow declares borrow-token symbols). Expansion resolves the Pool address from the context — returning no leaves at all if it is missing (fail closed) — and emits one leaf per selected token, whose packed constraints bind both the asset and the proof manager, exactly matching what the on-chain decoder extracts from supply calldata. Actions with different shapes pack accordingly: the collateral-toggle leaf binds only the asset, and the E-Mode leaf carries no address constraints at all (its calldata has no address parameters). The reverse mapping slices token addresses back out of the leaves so an installed policy can be reconstructed for review.

The result: a strategist who selected USDC and WETH for supply gets a policy under which the execution arm can supply exactly those two assets to exactly that Pool with proceeds bound to exactly that manager — and nothing else.