Conditional access

Conditional access turns a swap into a permissioned one: a strategy can require the taker to hold a specific token or NFT before a fill is allowed. The access guard runs as the first instruction in the SwapVM program (SwapVM is the Aqua swap engine), so an ineligible taker is rejected and the transaction reverts before any pricing logic executes. Makers stay open and permissionless. Only takers are gated, and the gate is a per-strategy opcode rather than a protocol-wide switch.

Deployed reality. The launch taker gate is the _onlyTxOriginTokenBalanceNonZero(token) opcode in the Controls set on the deployed AquaSwapVMRouter. It reverts TxOriginTokenBalanceIsZero unless balanceOf(tx.origin) > 0. In the 1inch Aqua dApp, the program assembler applies withTxOriginAccessToken(aquaKycToken) to every strategy, so at launch each fill requires the taker to hold the Aqua KycNFT access credential, and all fills route through KYB-verified 1inch Resolvers.

Use cases: institutional whitelisting, NFT-gated liquidity, loyalty-token holders, and DAO-member-only fills. These apply when contracts are called directly and a maker sets a custom token or NFT as the gate.


How the gate works

There are two ways a fill reaches the gate:

  • In the dApp: the assembler applies withTxOriginAccessToken(aquaKycToken) to every strategy, so the taker must hold the Aqua KycNFT (the taker access credential, symbol RES, reused from Fusion). Fills are routed only through 1inch Resolvers, which are KYB-verified.

  • Direct contract calls: a maker can require the taker to hold any specific token or NFT. A Conditional Access rule can gate on a non-zero balance (hold any balance, or at least one NFT), a minimum balance, or a minimum share of total supply. The check runs on-chain first, ahead of pricing.

The guard reads tx.origin, not msg.sender. As a result, smart-contract wallets and ERC-4337 accounts cannot currently pass the gate as takers, because they execute through a contract, so msg.sender differs from tx.origin. An MPC wallet signs as an externally owned account, so tx.origin is that EOA and it can pass the gate.


Guard instruction reference

The launch gate deployed on AquaSwapVMRouter is a single opcode in the Controls set:

Instruction Args Check
_onlyTxOriginTokenBalanceNonZero token (20 bytes) balanceOf(tx.origin) > 0, else reverts TxOriginTokenBalanceIsZero

When the contracts are called directly, a Conditional Access rule can also gate takers by a minimum balance or a minimum share of total supply. These rule types come from the Controls source, with the arg encodings below. The on-chain check resolves the taker via tx.origin.

Instruction Args Check
_onlyTakerTokenBalanceGte token (20 bytes) + minAmount (32 bytes) balanceOf(taker, token) >= minAmount
_onlyTakerTokenSupplyShareGte token (20 bytes) + minShareE18 (8 bytes uint64) balanceOf(taker) / totalSupply >= minShare

At launch the dApp applies only the non-zero balance gate, against the Aqua KycNFT. The minimum-balance and supply-share rule types are available when the contracts are called directly.


Program: token-holder gate on a strategy

In the dApp, the SwapVM SDK (@1inch/swap-vm-sdk) attaches the KYC gate for you:

// @1inch/swap-vm-sdk (dApp path)
// Applied to every strategy by the assembler:
program.withTxOriginAccessToken(aquaKycToken);

At the opcode level, the guard is simply the first instruction, ahead of balance setup and pricing. The underlying strategy can be any Aqua strategy (XYCSwap, XYCConcentrate, or PeggedSwap):

Solidity
1
2
3
4
5
6
7
8
9
Program memory program = ProgramBuilder.init(_opcodes());
bytes memory bytecode = bytes.concat(
    program.build(_onlyTxOriginTokenBalanceNonZero, gateToken),   // guard first (token: 20 bytes)
    program.build(_staticBalancesXD, BalancesArgsBuilder.build(
        dynamic([tokenIn, tokenOut]),
        dynamic([uint256(1_000e18), uint256(2_000e18)])
    )),
    strategyBytecode   // any Aqua strategy program (XYCSwap / XYCConcentrate / PeggedSwap)
);

Guard first: the access instruction must precede balance setup so unauthorized callers are rejected before any state is touched.


Not available on-chain: per-strategy taker allow-lists

The source includes per-strategy taker allow-list opcodes, _whitelistSingleTaker and _whitelistMultipleTakers, which would let a maker name specific taker addresses on a strategy. These are not registered on-chain today and should be treated as not available rather than a shipped feature.


Combining with any underlying strategy

Access guards compose with any program. The gate runs before the underlying strategy (after any expiry check):

_deadline                        ← expiry first
_onlyTxOriginTokenBalanceNonZero ← then the taker gate
_dynamicBalancesXD               ← then balance setup
_flatFeeAmountInXD               ← wrapping fee
  _xycSwapXD                     ← formula inside fee loop

  • Controls: full args for the access instructions

  • Constant Product: an underlying XYC (AMM) strategy

Did you find what you need?