Skip to main content

Consolidation and QuoteRegistry

Sources: contracts/src/vault/FeeVault.sol (additive IFeeVaultConsolidation surface), contracts/src/consolidation/QuoteRegistry.sol and contracts/src/consolidation/executors/. Tests: contracts/test/consolidation/** (registry, executors, vault paths, invariants, a real-adapter template suite) and contracts/test/e2e/**.

Consolidation​

Turns a beneficiary's balance in any registry-routed asset into USDT (BSC-USD 0x55d398326f99059fF775485246999027B3197955, 18 decimals), the only settlement asset, inside the vault. The executor must pull exactly amountIn, and the vault credits the measured USDT delta, which must be at least minOut.

Entry pointCalled byBound
consolidate(id, assetIn, amountIn, minOut, deadline, signature)anyone (a relayer), with the owner's Consolidate signaturethe signed minOut
creatorConsolidate(launchToken, assetIn, amountIn)the launch's creator: ZkPadFactory.tokenDeploymentInfo(launchToken).deployer (the msg.sender of deployToken)QuoteRegistry.creatorMinOut: Chainlink price × (1 − maxDeviationBps), amountIn ≤ maxSwapSize, feed younger than 1 hour; that minOut must be at least minCreatorShield (else CreatorAmountTooSmall); amountIn ≤ launchCredit(launchToken, assetIn) (else ExceedsLaunchCredit)
creatorConsolidateAndShield(launchToken, assetIn, amountIn, shieldAmount)the launch's creatoras above (skipped when amountIn == 0); then, if a template is waiting, shields shieldAmount USDT with it. 0 = the launch's whole USDT credit, or nothing while that is below minCreatorShield; an explicit amount must be ≥ minCreatorShield (ShieldAmountTooSmall) and ≤ the launch's USDT credit
  • The id is always the launch's own beneficiaryId; the creator never chooses an id or a destination. Without a template the USDT stays in the vault.
  • Launch credit. launchCredit(launchToken, asset) is the launch locker's depositFromLaunch credits, plus the USDT its creator consolidations produced, less what its creator already consolidated or shielded, held as shares of a per-(id, asset) pool. Every owner-side debit (claim, signed consolidation, fallback sweep) shrinks the pool pro rata, so a launch's credit never exceeds the balance it draws on and never reaches funds credited after the owner withdrew. Donations and other launches' credits are never reachable by a creator.
  • Whole balance. amountIn = 2^255 | quotedAmountIn swaps the balance at execution and scales minOut by balance / quotedAmountIn when less is left (the signed rate holds, so a creator consolidation landing first cannot make it revert). amountIn = type(uint256).max also swaps the whole balance but keeps minOut absolute. The FeeVault of the superseded 2026-10-04 deployment (never funded) predates the quoted form: it accepts only type(uint256).max and reads 2^255 | quotedAmountIn as a literal amount, so that request reverts there.
  • Trust. The creator path's bound (the registry's feeds and executors, and which registry the vault uses) is owner configuration. A malicious owner with a colluding creator can drain launch-credited balances through a bad feed or executor; the signed path is bounded by the beneficiary's own minOut. See trust assumptions.
  • consolidate shares nonces[id] with every owner action and counts as owner activity. A relayer batches multicall([registerStealth?, consolidate (nonce n), claim (nonce n + 1)]).
  • Creator calls are not owner activity: they never delay the fallback sweep.
  • Tokens without a price feed (USDC, USD1, FDUSD and XAUt in the mainnet config) can only be consolidated by the beneficiary. MAX_ORACLE_AGE is 1 hour; a USDT/USD feed, when configured, gets its own 25-hour bound (MAX_USDT_ORACLE_AGE); without one USDT is priced at $1.

Shield templates​

Entry pointDescription
registerShieldTemplates(id, bytes[] templates, deadline, signature)Owner-signed. Appends 1..32 templates (FIFO). Each must start with the account's id. Each is pinned to the current shieldAdapter, which must be set and allow-listed (ShieldAdapterNotSet); it is used only while that adapter is still configured and allow-listed, so a later change can block it, never redirect it.
clearShieldTemplates(id, deadline, signature)Owner-signed. Discards every unused template (e.g. after rotating Railgun wallets). rotateOwner and finalizeBind already discard them.
shieldTemplateCount(id) / shieldTemplateAt(id, i)Unused templates; index 0 is the next one used.

A template is exactly the RailgunShieldAdapter payload abi.encode(bytes32 id, ShieldRequest) with preimage.value = 0. The vault passes it unchanged with the gross amount (no relayer fee), and the adapter sets value to that amount. A template that pins a non-zero value only works for exactly that amount. Malformed templates (an npk outside the SNARK field, a wrong token) are rejected by the adapter at use time. The whole call then reverts, so neither the USDT nor the template is lost, and the owner can clear them. The same holds when the adapter's sanctions screen refuses the submitter (SubmitterSanctioned).

Each template is used at most once. The SDK's buildShieldTemplates uses fresh note randomness and a fresh shield key per template.

Typed data​

Consolidate(bytes32 beneficiaryId,address assetIn,uint256 amountIn,uint256 minOut,uint256 nonce,uint256 deadline)
RegisterShieldTemplates(bytes32 beneficiaryId,bytes32 templatesHash,uint256 nonce,uint256 deadline)
ClearShieldTemplates(bytes32 beneficiaryId,uint256 nonce,uint256 deadline)
templatesHash = keccak256(keccak256(t_0) ‖ keccak256(t_1) ‖ … ‖ keccak256(t_n-1))

All three use the FeeVault domain and share nonces[id]. See EIP-712.

Events​

EventWhen
Consolidated(id, assetIn, executor, amountIn, amountOut, byCreator)Every consolidation. byCreator separates the oracle-bounded path.
ShieldTemplatesRegistered(id, added, available)available is the unused count afterwards.
ShieldTemplatesCleared(id, removed)
ShieldedFromTemplate(id, adapter, amount, templateIndex)A creator-triggered shield. It is not a Claimed event, so indexers must debit USDT here.
ConsolidationConfigSet(registry, usdt, factory), ShieldAdapterSet(adapter), MinCreatorShieldSet(amount)Admin.

QuoteRegistry​

QuoteRegistry(owner, usdt, factory, guardian) is Ownable2Step (on mainnet, currently owned by the deployer EOA; a multisig is planned). It holds the factory's admin role (factory.setAdmin(registry, true)) and mirrors every quote's {enabled, minStartingTick, maxStartingTick} into factory.setQuoteToken.

struct QuoteConfig {
uint8 tier; // 1 curated, 2 fast-track
bool enabled; // new launches may pair with it
int24 minStartingTick; // token0 orientation, multiples of 200
int24 maxStartingTick;
address executor; // IRouteExecutor; zero only for USDT
bytes route; // executor-specific
address priceFeed; // Chainlink token/USD; zero = no creator path
uint16 maxDeviationBps; // creator path bound; Tier-2 depth probe bound
uint256 maxSwapSize; // creator path cap; Tier 2 requires > 0
}
FunctionWho
setQuote(token, config)owner (Tier 1, or overwrite)
setTier2Approval(token, configHash) then admitTier2(token, config)owner pre-approves the exact hashConfig; anyone executes the admission checks with their own tokens
disable(token)owner or guardian (stops new launches; never re-enables)
removeQuote(token), setFactory, setGuardianowner
quoteTokens(), quoteConfig(token), routeOf(token), creatorMinOut(token, amountIn), oracleQuote(token, amountIn), probeAmount(token)views

Tier-2 admission checks: decimals ∈ [6, 18]; a fee-on-transfer round trip of 10^(decimals−2); executor.routeOutput(token, route) == USDT; a small and a maxSwapSize sale along the route whose rates differ by at most maxDeviationBps; and, when a feed is set, both sales within the oracle bound. These are sanity rails. The owner's review is the real gate, because KYC-gated, rebasing or upgradeable-to-fee-on-transfer tokens cannot be detected on-chain.

Route executors​

ExecutorRoute encodingNotes
InfinityCLRouteExecutor(CLPoolManager, WBNB)abi.encode(PoolKey[]), 1..4 hopsOne vault.lock; native-BNB pools are unwrapped and wrapped transparently. No owner.
PancakeV3RouteExecutor(SwapRouter, V3Factory)V3 packed path token (20) ‖ fee (3) ‖ token (20) …, 1..4 hopsrouteOutput checks that every hop's pool exists and has liquidity. No owner.

Mainnet routes in contracts/config/56.json: WBNB, USDC, USD1, FDUSD and BTCB go straight to USDT on the 0.01% V3 pools, CAKE on the 0.25% pool, and ETH and XAUt via WBNB.

SDK​

readQuoteRegistry, readCreatorMinOut, readOracleQuote, consolidateAllAmountIn, effectiveConsolidateMinOut, buildConsolidate, buildRegisterShieldTemplates, buildClearShieldTemplates, encodeConsolidateCall, encodeCreatorConsolidateCall, encodeCreatorConsolidateAndShieldCall, encodeRegisterShieldTemplatesCall, encodeClearShieldTemplatesCall, readShieldTemplateCount, readLaunchCredit, readMinCreatorShield. Beneficiary clients price a consolidation with readOracleQuote for one whole unit of every configured quote (the same calls for every visitor) and scale locally, so neither the balance nor the held asset reaches the RPC.