Liquidity Provider and Taker Guide

Solidity-native reference for liquidity providers and takers who interact directly with the Aqua registry and AquaApp contracts, with no SDK required.

Position = strategy. This Solidity-native guide uses strategy, the on-chain name for what the 1inch dApp shows as a position — the same object. The code identifiers keep that name (strategyHash, the Strategy struct, the /strategies endpoint).

Audience. The maker side of this guide is permissionless: anyone can approve, ship(), and dock() with no credential. The taker side is gated at launch: filling strategies — including every strategy created through the 1inch dApp — requires the per-chain Aqua KycNFT (the Aqua resolver NFT), issued to KYB-verified 1inch Resolvers. If you are a resolver or B2B integrator who wants to fill Aqua liquidity, request contract whitelisting first via csm@1inch.com — see Becoming a resolver for the request checklist. This applies to existing 1inch resolvers too: Aqua is enabled only on request, after manual review — unlike Limit Order, Fusion, and Fusion+ contracts, it cannot be added self-service in the Resolver profile.

Strategy model

Immutable: once shipped, a strategy's parameters and initial allocation are fixed and cannot be edited in place.
Self-custodial: your tokens stay in your wallet under a revocable allowance to Aqua; the protocol holds zero tokens and takes no custody.
Rebalancing: call dock() then ship() to change parameters (no token transfers needed).
Simpler integration: immutability means fewer moving parts for takers and integrators to reason about.


For liquidity providers

1. Approve tokens to Aqua (one-time per token)

Solidity
1
2
// Grant max allowance — Aqua only pulls when a swap executes
token.approve(address(aqua), type(uint256).max);

Aqua never takes custody. The allowance is drawn only when pull() fires during a swap, and only for the exact fill amount. Your tokens stay in your wallet. The allowance is granted per token and per chain, and you can revoke it at any time by setting it back to 0. Granting type(uint256).max is an optional convenience; you can instead approve a bounded amount.


2. Ship a strategy

Encode the strategy struct for your target AquaApp (each AquaApp defines its own struct shape), then call ship():

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Example: XYCSwap two-token strategy
XYCSwap.Strategy memory strategy = XYCSwap.Strategy({
    maker:  msg.sender,
    token0: DAI,
    token1: USDC,
    feeBps: 30,    // 0.3%
    salt:   bytes32(0)
});

address[] memory tokens  = new address[](2);
tokens[0] = DAI;
tokens[1] = USDC;

uint256[] memory amounts = new uint256[](2);
amounts[0] = 1000e18;   // 1000 DAI
amounts[1] = 1000e6;    // 1000 USDC

bytes32 strategyHash = aqua.ship(
    address(xycSwapApp),
    abi.encode(strategy),
    tokens,
    amounts
);

On success, the strategy is registered and available to fill immediately, and the Shipped event is emitted. No tokens move.


3. Check virtual balances

Virtual balances are an internal counter in Aqua.sol; they track a strategy's committed liquidity without moving any tokens out of your wallet.

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Validate tokens are in the strategy and read balances
(uint256 balanceDAI, uint256 balanceUSDC) = aqua.safeBalances(
    maker,
    address(xycSwapApp),
    strategyHash,
    DAI,
    USDC
);

// Raw read without validation (use for debugging)
(uint248 raw, uint8 count) = aqua.rawBalances(
    maker,
    address(xycSwapApp),
    strategyHash,
    DAI
);

4. Dock a strategy

Docking revokes the strategy's virtual balances and stops it from filling swaps. Your tokens are already in your wallet, so dock() moves nothing.

Solidity
1
2
3
4
5
address[] memory tokens = new address[](2);
tokens[0] = DAI;
tokens[1] = USDC;

aqua.dock(address(xycSwapApp), strategyHash, tokens);

Emits Docked(maker, app, strategyHash).


5. Update parameters (dock, then re-ship)

Strategies are immutable once shipped. To change any parameter (fee, amounts, pair), dock the old strategy and ship a new one. This is how a rebalance works: dock() followed by ship().

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. Dock existing strategy
aqua.dock(address(xycSwapApp), strategyHash, tokens);

// 2. Ship with updated parameters — no token transfer needed
XYCSwap.Strategy memory newStrategy = XYCSwap.Strategy({
    maker:  msg.sender,
    token0: DAI,
    token1: USDC,
    feeBps: 10,    // updated to 0.1%
    salt:   bytes32(0)
});

bytes32 newHash = aqua.ship(
    address(xycSwapApp),
    abi.encode(newStrategy),
    tokens,
    amounts
);

The new strategyHash differs from the old one, so integrators referencing the old hash must update.


For takers

Most strategies require the Aqua resolver NFT. A strategy's program can include a Conditional Access rule that requires the taker to hold a specific token or NFT, checked on-chain before the fill. Every strategy created through the 1inch dApp carries a tx.origin token-balance gate bound to the Aqua KycNFT (the Aqua resolver NFT) — the taker credential minted to KYB-verified 1inch Resolvers — so the swap reverts unless tx.origin holds that token. Because the check reads tx.origin, contract wallets, multisigs, and ERC-4337 bundlers cannot pass it. When you call a strategy directly on-chain, you must satisfy whatever access rule that strategy encodes. Resolvers and B2B integrators request contract whitelisting via csm@1inch.com — see Becoming a resolver. This applies to existing 1inch resolvers too — Aqua is enabled only on request, after manual review, and cannot be added self-service in the Resolver profile.

1. Implement the callback interface

When calling a callback-based AquaApp, your contract must implement the app's callback interface and call push() inside it to deliver the input token:

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract Trader is IAquaAppSwapCallback {
    function aquaAppSwapCallback(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 amountOut,
        address maker,
        address app,
        bytes32 strategyHash,
        bytes calldata takerData
    ) external override {
        // Transfer tokenIn to complete the swap (requires token approval)
        // This is the ONLY appropriate use of push() - during swap execution
        IERC20(tokenIn).forceApprove(aqua, amountIn);
        aqua.push(maker, app, strategyHash, tokenIn, amountIn);
    }
}

2. Execute a swap

Call the AquaApp's swap function directly. There is no signature round-trip: the taker callback fires automatically during execution.

Solidity
1
2
3
4
5
6
7
8
9
// swapExactIn — swap a fixed input amount
uint256 amountOut = xycSwapApp.swapExactIn(
    strategy,
    true,              // zeroForOne: token0 → token1
    1e18,              // amountIn (1 DAI)
    0.99e18,           // amountOutMin (slippage tolerance)
    address(this),     // recipient
    ""                 // takerData (forwarded to callback)
);

Swap reverts if:

  • amountOut < amountOutMin (slippage exceeded)

  • The maker's real wallet balance is insufficient (pull() reverts)

  • The callback does not deliver the correct amountIn

  • A Conditional Access rule on the strategy is not satisfied


Key invariants

Property Behavior
Self-custodial Tokens never leave the maker's wallet until pull() fires, and then only for the exact fill amount. The protocol holds zero tokens.
Atomic fill A swap completes fully or reverts, with no partial fills and no bad debt
Coverage If the maker's real balance is below the strategy's virtual commitment, pull() reverts and the fill fails. The strategy keeps quoting; it is never paused on-chain or liquidated.
Fee auto-compounding Input tokens delivered via push() immediately expand the strategy's virtual balance

Swap fees accrue to the maker and are not guaranteed; there is no guaranteed yield, APY, or return. Aqua is self-custodial and non-custodial, but smart-contract risk and the risk of granting a token allowance still apply.


Did you find what you need?