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
12
// 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
1234567891011121314151617181920212223
// 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
12345678910111213141516
// 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
12345
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
123456789101112131415161718
// 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
1234567891011121314151617
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
123456789
// 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
amountInA 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.
Related
Smart contract: complete function reference for
Aqua.solandAquaAppBuild an AquaApp: implement a custom AquaApp with swap logic
Strategy lifecycle: ship / dock / pull / push state machine
Virtual balances: how shared liquidity and virtual accounting work
Contract addresses: Aqua registry address across all 13 supported chains