Strategy lifecycle

A strategy moves through three states, driven by four entry points on Aqua.sol: ship(), pull(), push(), and dock(). Swap execution itself is handled by SwapVM, the Aqua swap engine. Understanding this lifecycle is essential before building on Aqua.

Shipping a strategy never moves the maker's tokens. They stay in the maker's wallet under a revocable, per-chain, per-token ERC-20 allowance to Aqua.sol, and move only when a taker fills a swap atomically. The protocol holds zero tokens. If the maker's wallet no longer covers the strategy, the strategy simply stops filling; this is an economic effect, not an on-chain pause or liquidation.


State machine

[Not Shipped]
     |
     |  maker calls ship(app, strategy, tokens, amounts)
     |  -> emits Shipped event
     v
[Active / Shipped]
     |
     |  takers call quote() / swap() on the app
     |    -> pull() fires: output tokens go maker -> taker
     |    -> push() fires: input tokens go taker -> maker
     |
     |  maker calls dock(app, strategyHash, tokens)
     |  -> emits Docked event
     v
[Docked]
     |
     |  maker ships again with new params
     |  (a new strategy; a docked strategy is never reactivated in place)
     v
back to [Active / Shipped]

The four entry points

ship(app, strategy, tokens, amounts)

Registers a strategy and allocates virtual balances so it can be filled. The maker's tokens are not transferred.

  • app: the deployed app contract that executes swaps against this strategy. Aqua ships a single deployed app, the AquaSwapVMRouter, which serves every strategy type. The type (xyc, concentrated, or pegged) is selected by the program bytes inside the strategy, not by the app address.

  • strategy: ABI-encoded strategy struct (its shape is defined by the app's program).

  • tokens: ordered list of token addresses to allocate.

  • amounts: virtual amounts to allocate per token (parallel to tokens).

On success:

  • balances[maker][app][strategyHash][token] is populated for each token.

  • The Shipped(maker, app, strategyHash, strategy) event is emitted, followed by a Pushed event per token for the initial allocation.

  • strategyHash is derived as keccak256(abi.encode(strategy)) and is immutable.

The maker's tokens are not transferred. Shipping only relies on the ERC-20 allowance granted to Aqua.sol. For how virtual balances work, see Virtual Balances.


pull(maker, strategyHash, token, amount, to)

Called by the app during a swap to move the maker's output token to the taker.

  • Transfers amount of token from the maker's wallet to to. Reverts if the maker's wallet balance is insufficient.

  • Decrements balances[maker][app][strategyHash][token].

  • Emits Pulled(maker, app, strategyHash, token, amount).

Makers never call pull() directly. The app calls it as part of swap().


push(maker, app, strategyHash, token, amount)

Called by the app (via the IAquaAppSwapCallback callback) to receive the taker's input token.

  • Transfers amount of token from the taker (the caller of swap()) to maker.

  • Increments balances[maker][app][strategyHash][token]. LP swap fees auto-compound here.

  • Emits Pushed(maker, app, strategyHash, token, amount).


dock(app, strategyHash, tokens)

Revokes virtual balances and stops the strategy from filling, without moving any tokens.

  • For each token in tokens, zeroes balances[maker][app][strategyHash][token].

  • The tokens are already in the maker's wallet (they were never transferred to the registry), so dock() only clears the virtual accounting entries.

  • Emits Docked(maker, app, strategyHash).

A strategy is immutable, so dock() does not edit it. To change any parameter, dock the current strategy and ship a new one. Re-encoding the same struct reproduces the same strategyHash, while changing any field produces a new hash (see Re-shipping patterns below).


Events

The four Aqua.sol entry points emit Shipped, Pulled, Pushed, and Docked. On each fill, the AquaSwapVMRouter additionally emits Swapped(orderHash, maker, taker, tokenIn, tokenOut, amountIn, amountOut), where orderHash equals the strategy's strategyHash. These events are documented alongside the registry (Aqua) and router references.

The @1inch/aqua-sdk package exports decoders for the four registry events:

TypeScript
1
2
3
4
5
6
7
import { ShippedEvent, DockedEvent, PulledEvent, PushedEvent } from "@1inch/aqua-sdk";

const shipped = ShippedEvent.fromLog(log);
// { maker, app, strategyHash, strategy }

const pulled = PulledEvent.fromLog(log);
// { maker, app, strategyHash, token, amount }

Re-shipping patterns

Because strategies are immutable, updating a parameter requires a dock-then-ship cycle:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. Dock the old strategy
await wallet.sendTransaction(aqua.dock({
  app, strategyHash: oldHash, tokens: [USDC, WETH]
}));

// 2. Ship with updated params (e.g., a new fee)
// feePercent is a decimal in [0,1]; 0.0030 = 0.30%
const newStrategyData = { ...strategyData, feePercent: 0.0030 };
const newStrategy     = encodeAbiParameters([...], [newStrategyData]);
await wallet.sendTransaction(aqua.ship({
  app, strategy: new HexString(newStrategy),
  amountsAndTokens: [...]
}));

The new strategyHash will differ from the old one because feePercent changed. Any integration or strategy reference that points at the old hash is now stale, so integrators must update to the new hash.


Reentrancy

The nonReentrantStrategy() modifier on AquaApp locks per (maker, strategyHash) using transient storage. Two different strategies owned by the same maker can execute concurrently; the same (maker, strategyHash) pair cannot re-enter itself mid-swap.


Execution lifecycle flow

Execution order  (actor -- message --> target):

 1. Maker     --  approve() token balance           -->  Aqua
 2. Maker     --  ship() allocation to Strategy      -->  Aqua
               [ Aqua records the maker allocation scoped to the Strategy ]

 3. Taker     --  trigger swap against Strategy       -->  Aqua
 4. Aqua      --  validate Strategy authorization     -->  Strategy
 5. Strategy  --  hand off Order for execution        -->  SwapVM
 6. SwapVM    --  begin Program execution             -->  Program

    loop [ for each instruction ]
 7.   Program --  dispatch opcode, read/write ctx      -->  Program

    opt [ Path C: Extruction opcode ]
 8.   Program --  call external contract              -->  External Contract
 9.   External Contract  --  return result            -->  Program
10.   Program --  write result into ctx               -->  Program

11. Program   --  return computed amounts             -->  SwapVM
12. SwapVM    --  return swap output                  -->  Aqua
13. Aqua      --  credit & debit balances atomically       (internal)
14. Aqua      --  deliver tokens                      -->  Taker

Taker access gate (checked at swap time). At launch every dApp strategy carries the Controls opcode _onlyTxOriginTokenBalanceNonZero, which reverts TxOriginTokenBalanceIsZero unless balanceOf(tx.origin) > 0. It is evaluated at swap time, not ship time, so a strategy can be live yet untradeable until a permitted taker holds the credential. Because it reads tx.origin, smart-contract wallets, multisigs and ERC-4337 bundlers cannot pass it as takers today. Permitted takers at launch are KYB-verified 1inch Resolvers. See Access, resolvers & Pathfinder.

Did you find what you need?