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.

Overview

Each auction can have one validation hook attached. The hook supports three verification methods that can be mixed per step:

  1. Pre-submitted zkTLS proof: proof submitted ahead of time via submitProof() (permissionless; anyone can relay a valid proof).
  2. Inline zkTLS proof: proof passed directly in the bid's hookData.
  3. Inline server permit: EIP-712 signed permit passed in the bid's hookData (permits have no pre-submission path; they are 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.

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 byteTypePayload (remaining bytes)
0x01Server permitabi.encode(uint256 permitStep, bytes32 nonce, uint256 deadline, bytes signature)
Any other valuezkTLS proofFirst 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 be ≤ the current step (monotonic).
  • nonce (bytes32): Single-use value chosen by the signer. Burned when the permit is consumed; reuse reverts with PermitAlreadyUsed.
  • deadline (uint256): Unix timestamp after which the permit expires.
  • signature (bytes): EIP-712 signature from the authorized signer over the ServerPermit struct.

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 step M (M ≥ N) even if step M is not permit-enabled.

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, making them eligible for all subsequent steps.

Empty hookData

If hookData is empty and the user has no pre-stored verification, the bid reverts with NotVerified(owner).

If a user has already been verified (via submitProof() or a previous proof-carrying bid), the hookData is ignored entirely. Pre-verification is checked first. Server permits never create this stored verification.

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. There is no pre-submission function for permits; only zkTLS proofs can be pre-submitted (via submitProof()).

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): Bit i set means step i requires 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): Bit i set means step i accepts server-permit signatures. A server permit submitted for a step where this bit is not set will revert with ServerPermitNotEnabled.

Admin Functions

All admin functions are restricted to the hook owner.

FunctionDescription
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)
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 BidPriceExceedsMax
unregister(user) / unregisterBatch(users)Revoke a user's stored verification
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:

  • If no wallets are configured for that step, any wallet receives a permit (open access).
  • If wallets are configured, only listed wallets receive permits.

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

# 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:

  1. If no CCA is paired, the bid passes.
  2. If sender != owner, revert with SenderNotBidOwner (no delegated bidding).
  3. If a max bid price is configured and the bid's max price exceeds it, revert with BidPriceExceedsMax.
  4. Resolve the current auction step from block number.
  5. If the step is not enabled for verification (or has neither a proof gate nor a permit gate), the bid passes.
  6. Check pre-stored verification: if the user is verified from a step ≤ current step, the bid passes.
  7. If no pre-verification exists, decode hookData according to the step's gate configuration:
    • Empty: revert ServerPermitRequired, ProofRequired, or NotVerified depending 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 > currentStep reverts PermitStepTooHigh.
    • Proof gate otherwise: decode proofStep from the first 32 bytes. If proofStep > currentStep, revert ProofStepTooHigh. Verify the zkTLS proof against proofStep's providers.
  8. On successful proof verification, store the user's verified step (monotonic: only lowers, never increases). A consumed permit stores nothing: it authorized that single bid.

Events

EventEmitted when
Registered(stepIndex, user)User's verified step is stored (zkTLS proof paths; permits emit PermitConsumed instead)
Unregistered(user)User's stored verification is revoked
SignerSet(oldSigner, newSigner)Permit signer is changed
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

ErrorCause
NotVerified(user)User has no verification and provided empty hookData
SenderNotBidOwner()Bid sender is not the bid owner
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 or submitProof() 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
BidPriceExceedsMax(bidPrice, maxAllowed)Bid's max price exceeds the configured cap
PriceNotAlignedToTick()Bid price not aligned to the auction's tick grid