Strategy

A strategy is the unit of liquidity allocation in Aqua. It is the on-chain artifact a maker ships to declare, "I will provide this liquidity, under these pricing rules." A taker references a strategy when asking, "Swap against this liquidity on these terms." Every protocol operation (ship, dock, pull, push, quote, and swap) acts on a strategy.

Position = strategy. This page uses strategy, the on-chain and API name for what the 1inch dApp shows as a position. They are the same object; the code identifiers cannot be renamed (strategyHash, the Strategy struct/DTO, the /strategies endpoint).


What a strategy is

A strategy is an ABI-encoded structure, opaque bytes from Aqua's perspective, that carries all parameters defining its swap behavior. The registry treats the strategy body as opaque bytes and never parses them. Aqua manages only balance accounting; all pricing and execution logic lives inside the AquaApp.

When built with SwapVM, the Aqua swap engine, the strategy is the ABI-encoded form of a SwapVM Order. The Order wraps a Program, an ordered sequence of opcodes and instructions that AquaSwapVMRouter (the deployed SwapVM router) executes at swap time. Aqua does not interpret the Program; it only accounts for virtual balances keyed by the strategy hash.

Program (opcodes + args)  →  SwapVM Order  →  Aqua Strategy (ABI-encoded)

This separation between accounting (managed by Aqua.sol) and swap logic (implemented by AquaApps or SwapVM Programs) is the core architectural principle. The same virtual balance infrastructure can back any strategy type, such as constant product AMMs, concentrated liquidity, or pegged and reward-bearing curves, without Aqua itself needing to understand any of them.


Identity: how a strategy is addressed

Every strategy is uniquely identified by the triple (maker, app, strategyHash):

  • maker — the EOA or contract that shipped the liquidity.

  • app — the deployed AquaApp contract that governs swap logic. For SwapVM strategies this is AquaSwapVMRouter, the single router that serves all three strategy types (xyc, concentrated, and pegged); the type is selected by the program inside the strategy bytes, not by a separate app contract. Developers can also deploy a custom AquaApp, which defines its own strategy struct shape.

  • strategyHash — a bytes32 derived from the hash of the strategy's immutable parameters. Because the hash encodes all parameters, the same inputs always produce the same hash, and any parameter change produces a different hash entirely.

Solidity
1
bytes32 strategyHash = keccak256(abi.encode(strategy));

The (maker, app, strategyHash) triple is the key Aqua uses throughout the registry. It is how ship(), dock(), pull(), and push() locate the correct virtual balance slot.


Registry: what liquidity backs a strategy

Inside Aqua.sol, virtual balances are tracked in a four-level nested mapping:

Solidity
1
2
3
4
Maker Address
    → Application Address
        → Strategy Hash (bytes32)
            → Token Address → Balance

The storage slot balances[maker][app][strategyHash][token] holds the virtual balance for each token in the strategy. These are claims against the maker's revocable ERC-20 allowance to Aqua.sol, not custody balances. The allowance is granted per chain and per token, and the tokens stay in the maker's wallet at all times; the protocol itself holds zero tokens.

Shipping a strategy populates these slots. Docking empties them. Because each strategy holds only a claim on a slice of the maker's total allowance, one per-token approval on a chain can back many strategies on that chain in parallel. This is the "shared" in Aqua's shared liquidity layer.

Read balances via:

  • rawBalances — raw read, does not validate that the token belongs to the strategy.

  • safeBalances — reverts if the queried token is not registered in the strategy.

See Virtual Balances for the full on-chain layout and packed struct details.


Swap logic: how a strategy prices and fills swaps

Swap logic is entirely the responsibility of the AquaApp. Aqua itself has no knowledge of pricing curves, fee calculations, or AMM invariants. When a taker calls swap(), the AquaApp runs its own logic (a constant product formula, a pegged curve, or a custom quote) and then calls pull() and push() on Aqua.sol to complete the transfer.

For apps that use SwapVM, the execution logic is encoded as a bytecode Program: an ordered sequence of instructions the engine interprets against swap-state registers. The Program is wrapped in a SwapVM Order, which is ABI-encoded to produce the strategy. It is the Program instructions, not the strategy container, that SwapVM executes. The strategy is only the registered, immutable wrapper. See Order, SwapVM Instructions, and Program Model for the composition rules and bytecode format.

All strategies, regardless of which AquaApp implements them, must preserve the core invariants: symmetry, additivity, quote/swap consistency, monotonicity, rounding in the maker's favor, balance sufficiency, and strategy liveness.


Configuration: authorization and permissions

Each strategy carries two configuration headers that govern who can swap against it and under what conditions:

  • MakerTraits — a 256-bit packed header the engine reads before executing. It carries authorization flags, hook indexes, receiver address, epoch and nonce fields, and time bounds. The useAquaInsteadOfSignature flag is particularly important: when set to true, the engine skips signature verification and treats the strategy as authorized by virtue of being shipped to Aqua's registry. When false, the strategy functions as an isolated SwapVM order authorized by an EIP-712 signature.

  • TakerTraits — a variable-length payload the taker supplies at execution time to parameterize the swap (recipient override and extension data slices). Aqua fills are all-or-nothing; there are no order-level partial fills.


Lifecycle

Strategies pass through three states:

[Not shipped]
     |
     |  ship(app, strategy, tokens, amounts)
     v
[Active]
     |
     |  pull() / push() execute on every swap
     |
     |  dock(app, strategyHash, tokens)
     v
[Docked]
     |
     |  re-ship with updated params
     v
back to [Active]
State Description
Not shipped The strategy struct exists off-chain but no registry slot is populated. It cannot be swapped against.
Active The maker called ship(); balances[maker][app][strategyHash][token] is populated; takers can swap via pull() and push().
Docked The maker called dock(); virtual balances are revoked and registry slots are zeroed. No tokens move; the maker's wallet balance is unaffected. The hash remains known and can be re-shipped with fresh balances.

A shipped strategy is completely immutable. Parameters, execution logic, and configuration headers cannot change after shipping. To modify anything, dock the current strategy and ship a new one with the updated struct. Because a different struct always produces a different strategyHash, this design removes entire classes of parameter-manipulation vulnerabilities: immutable strategies have smaller attack surfaces, predictable behavior, and are fully auditable by hash alone.

Activation is pure configuration

ship() and dock() involve no token transfers, only virtual accounting updates. A maker can go from zero liquidity to a live, swappable strategy in a single transaction, with no withdrawal delay and no opportunity cost from exiting existing strategies. This lets a maker allocate or revoke liquidity in minutes rather than through a lengthy deposit-and-withdraw cycle.


Completing a swap: pull() and push()

When a taker executes a swap, the AquaApp completes the fill through two operations on Aqua.sol:

Operation Caller Effect
pull() AquaApp Decreases virtual balance; transfers output token directly from maker's wallet to taker
push() AquaApp Increases virtual balance; transfers input token from taker to maker's wallet

pull() checks the maker's actual wallet balance at execution time and reverts if insufficient, so every fill is atomic. A swap either completes in full or fails cleanly: no partial fills, no bad debt, no protocol insolvency.

push() is auto-compounding by design. When input tokens arrive at the maker's wallet, the virtual balance for that token is immediately incremented, expanding available liquidity without any manual rebalancing step. Earned swap fees and received tokens instantly become available liquidity.

Illiquidity behavior

If the maker's wallet balance falls below their virtual commitment, pull() reverts. Aqua keeps quoting prices from virtual balances (it does not check real balances at quote time), which preserves price continuity, while swap execution fails until the maker's wallet is replenished. Underfunded strategies simply stop filling; this is temporary illiquidity, not bad debt, an on-chain pause, or protocol insolvency.

Makers are strongly advised to dock() strategies that become chronically underfunded. During an illiquid period, if prices move unfavorably, the first executable swap when liquidity returns may lock in those adverse price movements, analogous to impermanent loss in a constant product AMM. For strategies with balance invariants such as the constant-product rule x * y = k, these movements remain bounded by the same mathematical limits as traditional pools.


Reentrancy

The nonReentrantStrategy modifier on AquaApp locks per (maker, strategyHash) using transient storage. Two strategies owned by the same maker can execute concurrently; the same strategy cannot re-enter itself during execution. See IAquaAppSwapCallback for the callback interface that completes the push() side of a swap.


Observability

The registry emits the following events over the strategy lifecycle:

Event Emitted when
Shipped Strategy is activated via ship()
Docked Strategy is closed via dock()
Pulled Output tokens are transferred from maker during a swap
Pushed Input tokens are delivered to maker during a swap

On each fill, AquaSwapVMRouter additionally emits Swapped(orderHash, maker, taker, tokenIn, tokenOut, amountIn, amountOut), where orderHash equals the strategy's strategyHash.

The @1inch/aqua-sdk package parses these via ShippedEvent.fromLog(log), DockedEvent.fromLog(log), and equivalent helpers.

Did you find what you need?