Validation Hook
Smart contract reference for UmiaValidationHook: bid verification, hookData format, and server permits
The UmiaValidationHook is the smart contract that enforces per-step bid eligibility during a Tailored Auction. It is called by the CCA on every bid submission and decides whether the bidder is allowed to participate in the current auction step. Bid Validation charts every path a bid takes through it.
Overview
Each auction can have one validation hook attached. The hook accepts two credentials, which can be mixed per step. Both travel in the bid's hookData: validate() is the only entry point, and nothing is submitted ahead of a bid.
- Inline zkTLS proof: proof passed directly in the bid's
hookData. On an ordinary step it registers the wallet for later bids. - Inline server permit: EIP-712 signed permit passed in the bid's
hookData, verified and consumed during the bid transaction.
Verification is monotonic: a user verified at step N is automatically eligible for steps N, N+1, N+2, and beyond. This models tiered access where earlier tiers are supersets of later ones.
Interface discovery (ERC165)
The hook follows CIP-1, the CCA standard for validation hooks, so an integrator can identify it from its address alone rather than from a trusted list. Read validationHook() off the auction, then call supportsInterface:
| Interface | ID | Meaning |
|---|---|---|
IERC165 | 0x01ffc9a7 | Supports introspection |
IValidationHook | 0x22c44b5f | Is a CCA validation hook (has validate) |
IUmiaValidationHook | 0xbff343b3 | Is this hook, with the gating reads and proof relays below |
IMaxBidPriceValidationHook | 0x2268a4c3 | Exposes a bid price cap via maxBidPrice() (0 = no cap) |
IGatedValidationHook | 0x6d417064 | Early bidding is gated until expirationBlock() (0 = never gated) |
IVerifyEveryBidHook | 0x98e51cb6 | Can require a fresh credential on every bid at a step (verifiesEveryBid, authorizesWithoutCredential) |
IUmiaValidationHook covers the permissionless surface only: the reads that describe the gate (cca, getSteps, isStepEnabled, isStepPermitEnabled, getStepProviders, isVerified, signer, stepMaxBidAmount, zkBidTotal, isPermitNonceUsed, identityOwner). The owner-only admin functions are outside it, so routine admin changes never shift the ID. Removing the proof relays (submitProof, submitProofBatch) did change it; hooks deployed before still report the earlier ID.
IMaxBidPriceValidationHook is Uniswap's interface for a price-capped hook, and our maxBidPrice() matches it in both signature and meaning (0 = no cap), so generic CCA tooling can read our cap. One difference: Uniswap's cap is immutable, ours can be changed by the hook owner, so read it fresh rather than caching it.
The cap rejects with MaxBidPriceExceeded(), whose selector matches Uniswap's hook, so a client that knows the standard can decode the failure without this contract's ABI.
IGatedValidationHook is the gating half of Uniswap's IGatedERC1155ValidationHook, the interface their auction UI reads to learn that early bidding is restricted: it blocks bidding while block.number < expirationBlock() and lifts the restriction on its own once that block passes. The ID is theirs even though we skip the ERC1155 ownership check it upstream inherits, because Solidity excludes inherited selectors when computing an interface ID. expirationBlock() returns the end of the last gated step, or 0 when no step gates. The hook owner can move the gate mid-auction, so read it fresh rather than caching it.
hookData Format
When a user submits a bid, the CCA forwards a hookData bytes payload to validate(). The step's gate configuration decides which credential is required; the first-byte type flag only picks between them when a step has both a proof gate and a permit gate configured:
| First byte | Type | Payload (remaining bytes) |
|---|---|---|
0x01 | Server permit | abi.encode(uint256 permitStep, bytes32 nonce, uint256 deadline, bytes signature) |
| Any other value | zkTLS proof | First 32 bytes: uint256 proofStep (the step the proof targets), followed by abi.encode(Reclaim.Proof) |
A missing, wrong-kind, or truncated payload is rejected with the gate's own error (ServerPermitRequired / ProofRequired) rather than an opaque decode failure.
Server permit inline (0x01)
hookData = 0x01 ++ abi.encode(permitStep, nonce, deadline, signature)- permitStep (
uint256): The 0-indexed auction step the permit was signed for. Must equal the current step on steps that verify every bid; ordinary steps accept ≤ the current step (monotonic). - nonce (
bytes32): Single-use value chosen by the signer. Burned when the permit is consumed; reuse reverts withPermitAlreadyUsed. - deadline (
uint256): Unix timestamp after which the permit expires. - signature (
bytes): EIP-712 signature from the authorized signer over theServerPermitstruct.
The hook enforces monotonic inline verification: permitStep must be ≤ the current auction step. The permit bitmap is checked against permitStep, and the EIP-712 signature is verified for permitStep. This allows a user who obtained a permit at step N to bid at a later step M that also accepts permits. On a step that takes only proofs, a permit reverts ProofRequired.
zkTLS proof inline (default)
hookData = abi.encodePacked(uint256 proofStep) ++ abi.encode(Reclaim.Proof)If the first byte is anything other than 0x01, the hookData is treated as a step-prefixed zkTLS proof:
- proofStep (
uint256, first 32 bytes): The 0-indexed auction step the proof was originally verified for. - proof (remaining bytes): ABI-encoded
Reclaim.Proof.
The hook enforces monotonic inline verification: proofStep must be ≤ the current auction step. The proof is verified against the proofStep's provider set, not the current step's. This allows a user who verified at step N (with provider X) to bid at step M (M ≥ N) even if step M uses different providers.
After verification, the user is registered from proofStep, so later ordinary steps that take proofs accept their bids without a credential.
Empty hookData
If hookData is empty and the user has no pre-stored verification, the bid reverts with NotVerified(owner).
On an ordinary step that takes proofs, a user registered by an earlier proof bids without a credential and the hookData is ignored. Standing verification is checked first. Server permits never create it.
Server Permit System
Server permits use EIP-712 typed signatures to gate access. A trusted backend signer issues permits to eligible wallets, and the contract verifies the signature onchain.
EIP-712 Domain
EIP712Domain(
string name, // "UmiaValidationHook"
string version, // "1"
uint256 chainId, // Bound at deployment
address verifyingContract // Hook contract address
)Permit Struct
ServerPermit(
address wallet, // The wallet being permitted
uint256 step, // The auction step index
bytes32 nonce, // Single-use nonce, burned on consumption
uint256 deadline // Unix timestamp expiry
)Submission Path
Permits are inline-only: include the permit in the bid's hookData with the 0x01 prefix. The permit is verified and its nonce burned during the bid transaction, and it authorizes that single bid only. Unlike zkTLS proofs, a permit does not register the user: every bid in a permit-gated step needs a fresh permit and nonce, because the off-chain signer is the source of truth for per-wallet bid caps.
Step Configuration
Each step can independently be configured for zkTLS verification, server permit verification, or both.
Bitmaps
The hook uses two bitmaps to track which steps enforce verification:
- Step enabled bitmap (
_stepEnabledBitmap): Bitiset means stepirequires verification of any kind (zkTLS or server permit). If a step is not enabled, any wallet can bid without verification. - Step permit enabled bitmap (
_stepPermitEnabledBitmap): Bitiset means stepiaccepts server-permit signatures. A server permit submitted for a step where this bit is not set will revert withServerPermitNotEnabled.
Admin Functions
All admin functions are restricted to the hook owner.
| Function | Description |
|---|---|
enableStep(stepIndex, providerHashes, providerIds) | Enable zkTLS verification for a step with specific provider requirements |
enableStepBatch(stepIndices, providerHashesPerStep, providerIdsPerStep) | Batch variant of enableStep |
disableStep(stepIndex) / disableStepBatch(stepIndices) | Disable verification for a step (anyone can bid) and clear its verify-every-bid flag |
enableStepPermit(stepIndex) | Enable server-permit verification for a step |
disableStepPermit(stepIndex) | Disable server-permit verification for a step |
setSigner(address) | Set the authorized EIP-712 signer (set to address(0) to disable all permits) |
addStepProviders(stepIndices, hashes, ids) | Add required zkTLS provider hashes to steps |
removeStepProviders(stepIndices, hashes) | Remove provider hashes from steps |
setStepProviders(stepIndex, hashes, ids) / setStepProvidersBatch(...) | Replace a step's provider set atomically |
setMaxBidPrice(maxBidPrice) | Cap the max price a bid may carry (0 disables the cap); bids above it revert MaxBidPriceExceeded |
unregister(user) / unregisterBatch(users) | Clear a user's standing registration; any valid registration proof can register them again |
clearIdentity(providerHash, identityHash) | Release a claimed provider-scoped identity |
setCCA(address) | Pair the hook with a CCA contract (called by owner or router; effectively one-time, subsequent calls are no-ops) |
Permit Wallet Lists
The server-side permit signing service maintains an off-chain wallet allowlist per auction step. When a wallet requests a permit:
- An exact allowlist row at the active permit step overrides that step's policy with the wallet's own cap.
- Otherwise, a configured default policy applies: open-to-all grants screened wallets the default cap; a whitelist-gated policy grants no permit. Earlier rows never override a configured default policy.
- Only when the active step has no default policy can the highest eligible earlier permit-step row supply the cap. Verify-every-bid steps accept only their own row. A zero cap blocks issuance.
The Hub describes eligible earlier-round entries only for rounds that do not require verification on every bid; verify-every-bid rounds use their own allowlist rows. A configured default policy still prevents earlier-row fallback.
Every wallet on an auction's allowlist shows as Angel next to its bids on the hub, unless an operator assigns it a label for that auction (currently Fund; one label per wallet, covering every step). A wallet that bid through zkTLS shows a zkTLS tag instead of Angel, but never instead of a label; wallets outside the allowlist that did not use zkTLS carry no tag. The operator note and the per-wallet cap never leave the admin side.
Wallet lists are managed via the Umia CLI:
# List permitted wallets
umia permit-wallets-list --auction 0x... --step 0
# Add a single wallet
umia permit-wallets-add --auction 0x... --step 0 --wallet 0xabc...
# Bulk add from file (one address per line)
umia permit-wallets-add --auction 0x... --step 0 --file wallets.txt
# Label the added wallets (--label none clears it; omit to keep the label)
umia permit-wallets-add --auction 0x... --step 0 --file funds.txt --label fund
# Replace entire list atomically
umia permit-wallets-set --auction 0x... --step 0 --file wallets.txt
# Remove a wallet
umia permit-wallets-remove --auction 0x... --step 0 --wallet 0xabc...Validation Flow
When validate() is called on a bid:
- If no CCA is paired, the bid passes.
- Require the caller to be the paired CCA. The bid sender may differ from the owner; credentials and caps bind to the owner, while the CCA collects the sender's funds.
- If a max bid price is configured and the bid's max price exceeds it, revert with
MaxBidPriceExceeded. - Resolve the current auction step from block number.
- If the step is not enabled for verification, the bid passes. An enabled step with no credential gates also passes unless it verifies every bid, in which case it fails closed.
- On ordinary proof-gated steps, accept pre-stored verification from a step ≤ current step, subject to zkTLS caps. Permit-only steps and steps that verify every bid ignore that status.
- If a credential is required, decode
hookDataaccording to the step's gate configuration:- Empty: revert
ServerPermitRequired,ProofRequired, orNotVerifieddepending on which gates the step has. - Permit gate (payload prefixed
0x01, or the step is permit-only): verify and consume the server permit inline;permitStep > currentSteprevertsPermitStepTooHigh. - Proof gate otherwise: decode
proofStepfrom the first 32 bytes. IfproofStep > currentStep, revertProofStepTooHigh. Verify the zkTLS proof againstproofStep's providers.
- Empty: revert
- On steps that verify every bid, require
proofSteporpermitStepto equal the current step, and never accept such a step's proof or permit at any other step. Proof timestamps must be within the last ten minutes and cannot be in the future. The signed proof context must bebid:<chain>:<lowercase auction>:<step>:<amount in base units>:<nonempty nonce>. The hook consumes the proof identifier; permits consume their nonce. - On ordinary steps, successful proof verification stores the step the user is verified from. Registered users bid without a credential on later ordinary steps that take proofs, so it changes only through
unregister. A permit-only step still takes a permit from them. Bid proofs never create standing registration; neither do permits.
Enable setVerifyEveryBid(step, true) on steps that verify every bid after configuring their accepted credentials, in the same multicall batch and behind requireStepNotStarted(step) so the step never starts half-configured. Leave it off for early-bid steps so inline registration and later bids keep their existing flow. With both gates configured, each bid can supply either credential. Each proof request needs its own context, bid:<chain>:<lowercase auction>:<step>:<amount in base units>:<nonce> (bidProofContextMessage in @umia/types).
Proof verification uses the beacon's witness data without consuming its public replay registry. A copied call to Reclaim verifyProof cannot burn the pending bid proof. Early-bid registration remains reusable for bidding within its auction. Registration is idempotent, while bid proofs remain single-use. Registration proofs require a signed register:<chain>:<lowercase auction>:<nonempty nonce> context and cannot register the same owner in another auction.
A relayer can fund and submit a bid for the credential's owner. Refunds and token claims still go to the stored owner. The permit's signed amount cannot be changed, and another address cannot be substituted as owner. Bid price remains a sender-supplied value subject to the hook's price cap. Someone copying a complete bid can still fund the full authorized amount for the same owner and consume the credential; proof-only consumption is prevented, but public-mempool ordering is not guaranteed.
These changes require a compatible hook deployed with the auction. Existing auctions have immutable hook addresses and cannot acquire this behavior from an API update.
Events
| Event | Emitted when |
|---|---|
Registered(stepIndex, user) | User's verified step is stored (ordinary zkTLS proof paths only) |
Unregistered(user) | User's standing registration is cleared |
SignerSet(oldSigner, newSigner) | Permit signer is changed |
VerifyEveryBidSet(stepIndex, required) | A step started or stopped verifying every bid |
StepPermitEnabled(stepIndex) | Server permit enabled for a step |
StepPermitDisabled(stepIndex) | Server permit disabled for a step |
StepEnabled(stepIndex) | Verification enabled for a step |
StepDisabled(stepIndex) | Verification disabled for a step |
StepProviderSet(stepIndex, providerHash, providerId) | Provider added to or set for a step |
StepProviderRemoved(stepIndex, providerHash) | Provider removed from a step |
ProofVerified(user, stepIndex, proofIdentifier) | zkTLS proof verified |
PermitConsumed(user, stepIndex, nonce) | Server permit verified and its nonce burned |
IdentityClaimed(...) / IdentityCleared(providerHash, identityHash) | Provider-scoped identity claimed / released |
MaxBidPriceSet(maxBidPrice) | Max bid price cap changed |
CCASet(cca) | Hook paired with its CCA |
Error Reference
| Error | Cause |
|---|---|
NotVerified(user) | Required credential is absent; steps that verify every bid ignore prior verification |
CredentialStepMismatch(credentialStep, currentStep) | A credential on a step that verifies every bid targets another step |
BidProofExpired() / BidProofFromFuture() | A bid proof is too old or future-dated |
BidProofContextMismatch() | Missing or incorrect chain, auction, step, amount, or nonce in signed bid context |
ProofAlreadyUsed(bytes32) | This bid proof was already consumed |
NoGateConfigured(stepIndex) | A step that verifies every bid has no credential gate |
ProofRequired(stepIndex) | Step has a zkTLS gate but the payload isn't a valid proof envelope |
ServerPermitRequired(stepIndex) | Step has a permit gate but the payload isn't a valid permit envelope |
ServerPermitNotEnabled(stepIndex) | Server permit submitted for a step that doesn't accept permits |
SignerNotSet() | No signer configured (signer is address(0)) |
ExpiredDeadline() | Permit deadline has passed |
InvalidSignature() | EIP-712 signature doesn't match the configured signer |
PermitAlreadyUsed(nonce) | Permit nonce was already consumed |
PermitStepTooHigh(permitStep, currentStep) | Inline permit targets a future step (permitStep > currentStep) |
StepIndexOutOfBounds(stepIndex) | Step index exceeds the auction's step count |
NoCCA() | Admin call before a CCA is paired (bids simply pass in that state) |
ProofStepTooHigh(proofStep, currentStep) | Inline proof targets a future step (proofStep > currentStep) |
ProviderHashMismatch(expected, actual) | zkTLS proof provider doesn't match step requirements |
IdentityAlreadyClaimed(providerHash, identityHash, existingUser) | Another wallet already claimed this provider-scoped identity |
MaxBidPriceExceeded() | Bid's max price exceeds the configured cap. Selector matches Uniswap's MaxBidPriceValidationHook |
PriceNotAlignedToTick() | Bid price not aligned to the auction's tick grid |