Fee instructions are applied to amountIn. All are wrapping instructions: they call ctx.runLoop() internally to execute the nested swap formula, then adjust amounts around the result.
Source: src/instructions/Fee.sol (part of the SwapVM instruction set on the Aqua router).
Fee scale: BPS = 1e9 (1e9 = 100%). A fee of 3000000 (3e6) = 0.3%. In the app, the swap fee is displayed to two decimals (for example, 0.30%).
Fees in v1: only the liquidity-provider fee (shown in the app as the "Swap fee") is non-zero. The protocol fee instructions on this page (_protocolFeeAmountInXD, _aquaProtocolFeeAmountInXD, _dynamicProtocolFeeAmountInXD, and _aquaDynamicProtocolFeeAmountInXD) exist in the contracts but are configured to 0 and are not surfaced in the UI. There is no on-chain fee event: fees are encoded as instructions in the strategy program and are reflected directly in the swap amounts. The LP fee auto-compounds into the maker's Aqua balance and is realized through the Pushed event. Tokens stay in the maker's wallet (or in the maker's Aqua virtual balance, an internal counter in Aqua.sol) until a taker fills; the protocol custodies none.
Constraint shared by all instructions
Must be placed before swap amounts are computed. That is, both amountIn and amountOut must be zero when this instruction executes. Violating this reverts with FeeShouldBeAppliedBeforeSwapAmountsComputation.
Quote/swap divergence: All instructions compute fee amounts in quote mode but skip the actual token transfer or Aqua pull. A quote may succeed while the swap reverts due to insufficient maker balance or missing ERC-20 approval. Do not use backward jumps to any of these instructions.
Instructions
_flatFeeAmountInXD
function _flatFeeAmountInXD(Context memory ctx, bytes calldata args) internal
Applies a flat proportional fee on amountIn. There is no token transfer; it adjusts amounts only. This is the instruction behind the non-zero LP swap fee.
| Field | Offset | Size | Description |
|---|---|---|---|
feeBps |
0 | 4 bytes (uint32) |
Fee rate; max 1e9 |
| Mode | Behavior |
|---|---|
| exactIn | Reduces amountIn by ⌈amountIn × feeBps / BPS⌉ before passing to swap formula; restores original amountIn after |
| exactOut | Passes full amountIn to swap formula; adds ⌈amountIn × feeBps / (BPS − feeBps)⌉ after |
_protocolFeeAmountInXD
function _protocolFeeAmountInXD(Context memory ctx, bytes calldata args) internal
Flat fee on amountIn with a live ERC-20 transfer from maker to a recipient.
| Field | Offset | Size | Description |
|---|---|---|---|
feeBps |
0 | 4 bytes (uint32) |
Fee rate; max 1e9 |
to |
4 | 20 bytes (address) |
Fee recipient |
The transfer (safeTransferFrom(maker, to, feeAmount)) occurs during program execution, before SwapVM's final taker-to-maker tokenIn transfer. The maker must hold sufficient tokenIn balance and have granted ERC-20 approval before swap execution.
_aquaProtocolFeeAmountInXD
function _aquaProtocolFeeAmountInXD(Context memory ctx, bytes calldata args) internal
Same as _protocolFeeAmountInXD but pulls fee from the maker's Aqua virtual balance via IAqua.pull(maker, orderHash, tokenIn, feeAmount, to) instead of a direct ERC-20 transfer. Increments ctx.swap.amountNetPulled by the fee.
| Field | Offset | Size | Description |
|---|---|---|---|
feeBps |
0 | 4 bytes (uint32) |
Fee rate; max 1e9 |
to |
4 | 20 bytes (address) |
Fee recipient |
_dynamicProtocolFeeAmountInXD
function _dynamicProtocolFeeAmountInXD(Context memory ctx, bytes calldata args) internal
Fee rate and recipient are determined at execution time by calling IProtocolFeeProvider.getFeeBpsAndRecipient(...) on an external contract via staticcall. Transfers fee via safeTransferFrom.
| Field | Offset | Size | Description |
|---|---|---|---|
feeProvider |
0 | 20 bytes (address) |
Must implement IProtocolFeeProvider; address(0) skips the fee |
Security: Uses staticcall, so the fee provider cannot mutate state. Protected by TransientLock at the orderHash level. A malicious provider can return large data causing high gas consumption; takers should verify provider trustworthiness.
Errors: FeeDynamicProtocolInvalidRecipient() if feeBps != 0 but to == address(0). FeeProtocolProviderFailedCall() if staticcall fails or returns unexpected data. FeeBpsOutOfRange(feeBps) if returned fee exceeds BPS.
_aquaDynamicProtocolFeeAmountInXD
function _aquaDynamicProtocolFeeAmountInXD(Context memory ctx, bytes calldata args) internal
Same as _dynamicProtocolFeeAmountInXD but uses IAqua.pull for the transfer instead of safeTransferFrom.
| Field | Offset | Size | Description |
|---|---|---|---|
feeProvider |
0 | 20 bytes (address) |
Must implement IProtocolFeeProvider |
Related
- FeeExperimental: experimental fee variants applied to
amountOut. Not part of the shipped v1 opcode set. - XYCSwap: a typical swap formula nested inside fee instructions.
- LimitSwap: another common nested swap formula.