Skip to main content
One atomic call is the entire product. It is proof-to-action, never proof-only:
await nullis.verifyAndExecute({ policyId, proof, publicInputs, action });
Verification and execution are one step. There is no window where a proof is accepted but the action is unchecked, and none where the action runs without a verified proof.

The exact order of checks

The contract runs these in order, and stops at the first failure — emitting an ActionRejected(reason) and a Privacy Receipt:
1

Load the active policy + version

Fetch the policy by policy_id. Reject if disabled.
2

Check the approved root & revocation state

The proof’s approved_root must match the policy’s current root. Stale-version proofs are rejected — this is how revocation works.
3

Verify the ZK proof on-chain

Run the UltraHonk verifier against the submitted proof and public inputs. This is real on-chain zero-knowledge verification.
4

Check expiry

Reject if the policy has expired.
5

Check the action context

Recompute context_hash and action_id from the submitted action and confirm they match the proof’s public inputs — recipient, amount, asset, consuming contract, and network must all line up.
6

Check amount and asset

amount ≤ max_amount, and the asset and action type must match the policy.
7

Check the nullifier and action_id are unused

If either was seen before, this is a replay → ReplayBlocked.
8

Execute atomically

Move the asset (escrow → recipient in v0), mark the nullifier spent, and emit the Privacy Receipt.

Sequence diagram

When any check fails

The contract does not silently drop the transaction. It emits a typed rejection and a Privacy Receipt describing exactly what happened:
The nullifier binds intent_nonce into action_id, and action_id is consumed on-chain. A genuinely new payment requires a new intent nonce and a new proof — you cannot reuse a proof for a second payment.

Next: claim-safety

Which of these checks the circuit proves, and which the contract enforces — the #1 rule.