ERC-8380: Unclonable Agent Execution Credentials

Author: Muhammad Zidan Fatonie (@mzf11125), Faisal Firdani (@zexoverz), Maulana Asykari Muhammad (@WeissCurry), Venkata ramana Komari (@Venkat5599)

ERC number: 1953, assigned by the pull request number convention.

Proposal: https://github.com/ethereum/ERCs/pull/1953 (draft)
Reference Implementation: GitHub - mzf11125/unclonable-agent-execution-credentials · GitHub

Update Log

  • 2026-08-05: Initial idea draft opened for discussion on this thread.
  • 2026-08-06: chainId removed from the nullifier preimage. Split into a chain independent nullifier and a chain bound capability commitment.
  • 2026-08-11: Design agreed for implementation with issuance binding, two sided observability, and at most once with no ordering.
  • 2026-08-16: Coupled Guard merged into the reference implementation, together with the adversarial vectors contributed on this thread.
  • 2026-08-17: Coupling promoted from a recommended profile to the normative core. Salt removed from the on chain struct. Relayed submission removed. ERC PR opened as a draft and domain separation tags settled to the assigned number.

External Reviews

  • 2026-08-05: @babyblueviper1, post 2. Unclonability and authorization soundness are orthogonal properties a system needs both of, not substitutes. On mirroring, a nullifier registry is a spend problem where the answer has to be unique rather than merely available, so redundancy without ordering is precisely what an attacker wants. Folded in. Orthogonality now leads Security Considerations, and the mirroring framing is used as written.
  • 2026-08-05: @zexoverz, post 4. The exactly once unit is per issuance, not per agent, and an orchestrator MUST NOT reissue a spent salt. Folded in as a normative salt derivation.
  • 2026-08-08: @cedricbrown, post 7. A clone carries the same salt, the same agent identity, and under executor binding the same executor key, so the Guard cannot rank the two by construction. A collision is the only on chain evidence a clone exists, and a bare revert makes a live compromise look identical to a scheduling bug. Folded in as the ordering disclaimer and collision classification.
  • 2026-08-09: @zexoverz, post 8. A revert cannot emit, so observability has to be a burn event on the accepting path plus a named error on the rejecting one. Folded in as a normative requirement.
  • 2026-08-11: @helmymekaoui-web, post 10. Introduces a fourth layer, an identity scoped cumulative bound. The draft bounds how many times a credential executes and says nothing about what consumption is worth in total. Folded in as an explicit out of scope layer.
  • 2026-08-11: @cedricbrown, posts 11 and 12, with adversarial test vectors. The cheapest attack is not winning the race for the valuable action, it is spending the credential at all. Whether the Guard checks the commitment against an issuance record is a security property, not an implementation detail. Folded in. This changed the design, not the wording.
  • 2026-08-11: @WeissCurry, post 13. Separates issuer binding from execution coupling, and flags two implementation discrepancies. Folded in. Both discrepancies are resolved below.
  • 2026-08-12: @zexoverz, posts 14 and 15. If a clone holds both salt and executor key, no interface check can distinguish them. The fixable part is preventing a burn that leaves the intended execution unavailable, so consumption and execution belong in one call. Folded in as the normative core.

Resolved Since the Original Post

  • Nullifier derivation edge cases. No legitimate recurring action can be bricked. The unit of the guarantee is the issuance, not the action, so two intentional runs of the same task receive two capabilities with distinct salts and therefore distinct nullifiers. A scheduled reissue is safe as long as the index advances, which the salt derivation now requires.
  • Nullifier preimage. Settled as keccak256(NULLIFIER_TAG, salt). Posts 8 and 9 discussed adding the agent and domain identifiers. Both are already inputs to the salt derivation and already bound by the capability commitment, so including them a second time changes no security property and creates a second place for a circuit and a Guard to disagree.
  • Hash choice. keccak256, not Poseidon. Poseidon is cheaper in circuit, but pinning a field, round counts and constants in prose is a large normative surface, and a Guard and circuit that disagree on any of it accept nothing. Proving cost is paid once per capability by the agent. A parity bug is paid by every integrator.

Outstanding Issues

  • Cross chain nullifier synchronization. A capability deliberately issued as spendable on either of two chains, with no designated home, needs real consensus on spentness. The draft binds each capability to a home chain and expects an explicit burn and reissue to move across domains. Declared out of scope rather than solved. Because the chain identifier is excluded from the nullifier preimage, a future mirroring scheme can compare nullifiers across domains without redefining the derivation.

  • Proof generation latency. Proving sits in the execution path, so an attacker who has already cloned the agent memory can begin proving at the same moment as the honest agent. Per post 7, a secondary time lock moves the race later without changing who wins it, so the draft specifies no mitigation and states the limit instead.

  • Aggregate consumption. At most once per issuance is a count on one credential. It does not bound what one identity consumes in total. A compromised agent that keeps receiving fresh capabilities spends each of them once, legitimately, and drains value without ever triggering a collision. Named as a separate layer in the draft, but nothing implements it yet.

  • Relayed submission. The Guard requires msg.sender == executor, so an agent holding no gas cannot spend through a relayer. A proper path needs an EIP-712 signature argument on execute with a domain separator over the Guard address and chain identifier, which changes the interface. Deferred until a concrete integrator asks.


Summary

Agent authorization on Ethereum typically relies on function scoped boundaries or static permissions. Both assume the credential holder is the party the credential was issued to.

That assumption breaks in multi agent swarms. When building the LadingLogic autonomous trade finance network, we hit a security wall. When an orchestrator delegates a high stakes task to a specialized off chain agent, it issues an authorization credential, and a compromised agent can be cloned. A bad actor copies the memory state and replays that authorization to drain funds or duplicate actions. Nothing in a scoped permission distinguishes the original agent from its clone, because both present the same valid credential for the same permitted call.

Existing primitives do not close this. Nonces belong to the account, not to the delegated agent, so an orchestrator handing the same account to two workers gets no separation. Expiry windows shrink the replay window without removing it. Session keys authorize a class of calls rather than one call.

The property needed is that an execution capability fundamentally breaks after one use. By adapting the unconditional unclonable encryption result of Ananth and Sahai (arXiv 2607.21551), this draft brings quantum inspired unclonability to classical EVM environments through zero-knowledge nullifiers.

How It Works

  1. The orchestrator derives a per issuance salt, computes a capability commitment binding that salt to the agent identity, home chain, issuing domain, a monotonic index, and a canonical hash of the authorized action, then calls issue on chain. The capability travels to the agent off chain. The salt never reaches the chain.

  2. The agent generates a zero-knowledge proof that it knows a salt opening the commitment. Proof generation forces the exposure of a nullifier derived from the salt.

  3. A Guard contract verifies the proof, checks that the action being performed is the one the commitment binds, permanently logs the nullifier, and performs the action in the same call.

  4. If a cloned credential attempts to execute, it produces the identical nullifier. The Guard sees the duplicate and rejects the transaction with a named error.

The standard defines only the capability envelope, the nullifier derivation, and the verification interface. It does not define the proving system, the capability transport, or the policy that decided the capability should be issued.

Three Choices Worth Surfacing Early

Nullifiers derive from a hidden salt, not the transaction payload. If the nullifier were bound only to the payload, an identical legitimate subsequent task would be blocked, which breaks any recurring agent action. A settlement agent sweeping the same amount to the same address every week would brick itself on week two. Binding to a per issuance salt ensures intentional duplicate tasks receive unique capability tokens, while cloned tokens produce colliding nullifiers. The unit of the guarantee is the issuance, not the agent and not the action.

Unclonability moves from the storage layer to the execution layer. The original arXiv 2607.21551 result relies on quantum states. EVM environments are classical and data is infinitely replicable, so no property of the credential at rest can be made unclonable. This draft relocates the unclonable property to the proof of execution, where the collision is detectable on chain. The credential is still cloneable. The execution is not.

The burn is fused to the action. This is the change the thread forced, and it is the one worth arguing with. A separable spend primitive was specified first and withdrawn. Under it, any party satisfying the executor check could burn a nullifier on an action of its choosing and leave the authorized work permanently undone. At most once held perfectly and the deployment still lost. Mandatory issuance narrows that. Fusing the burn to the call closes what remains, so a burned nullifier now always means the authorized action ran.

Scope of the Security Claim

This standard guarantees at most once execution with no ordering. It is not exactly once, and it is not correct agent wins.

A clone that holds the agent memory holds the salt, and under executor binding the same executor key, so it can construct the same valid proof. Both parties can submit and the guarantee is only that one of them lands. The Guard cannot rank them by construction. Deployments that need the honest agent to win must keep the salt outside cloneable memory, in a hardware backed enclave or an external signer.

It is not an access control framework. The verifier learns that a specific single use capability was consumed, and the Guard ensures no identical capability can ever execute again.

It hides nothing. A permitted action executes on a public chain and is public. The claim is about how many times a credential can be spent, not about what the credential authorizes or who can see it.

Relationship to Neighbouring Standards

Standard Layer Relationship
Function-scoped delegation drafts Boundary Define what an agent may ever be authorized to touch. This proposal solves a different attack vector. Scoped delegation defines what an agent can reach, and this draft guarantees that a specific authorized payload executes at most once.
ERC-8354 Confidential Agent Policy Verdicts Soundness Defines how a particular authorization decision was reached, while keeping the ruleset confidential. A domain could plausibly use both, keeping the policy secret via ERC-8354 while ensuring the resulting credential cannot be replayed using this draft. Neither requires the other to function.
This draft Consumption Guarantees the resulting credential is spent at most once, and that a spend performs the issued action.
Identity-scoped cumulative bound Budget Meters total spend per agent identity so that N clones share one budget rather than multiplying it. Raised in post 10. Not defined here, and not substitutable by this draft.
ERC-8004 Identity Supplies the agent identity the capability token binds to.
ERC-7579, ERC-6900 Integration surface The Guard operates as a pre-execution hook or validation module. Dispatch must route through the Guard, for the reason in the third choice above.

Unclonability and authorization soundness are orthogonal. This standard guarantees that a specific authorized payload executes at most once. It makes no claim about whether that payload should have been authorized in the first place. A replayed credential from a compromised agent and a correctly issued credential encoding a genuinely bad decision are indistinguishable to the Guard, because both present a valid, previously unseen nullifier. A deployment needs boundary, soundness, consumption, and budget independently, and none of the four substitutes for another. Adopting this draft alone does not make an agent safe.

What Changed Since the Original Post

For anyone returning to the thread, three things are materially different from the version at the top of this history.

The separable spend primitive is gone. consume(cap, proof) burned a nullifier on its own call. It is replaced by mandatory issuance plus execute(cap, proof, target, callData), which checks the action against the commitment, burns, and performs the call atomically. A Guard must not expose any path that marks a nullifier consumed without performing the committed action. The residual, stated plainly, is a clone holding both the salt and the executor key triggering the authorized action early. That is a key management boundary, and coupling only turns the outcome from task dead into task done early.

The salt is out of calldata. It was described as a private witness while sitting in the Solidity struct, which made it public in the mempool ahead of inclusion and handed any observer the ability to destroy the capability before the honest transaction was mined. Strictly worse than a leaked salt, because it leaked on every spend. It is now a private witness of the circuit only.

Observability is normative and two sided. The first and only successful spend emits a burn event. Every subsequent attempt reverts with CredentialAlreadySpent. The highest issued index per agent is exposed so a collision can be classified: one at an index the orchestrator never issued indicates a clone, one at an index it did issue indicates a reissue bug on the orchestrator side.

Twenty tests in the reference implementation, including every adversarial vector contributed here.

Feedback I Am Specifically Looking For

  1. Integration shape. The draft requires dispatch to route through the Guard so the burn cannot be separated from the action. That may conflict with how ERC-7579 and ERC-6900 modules expect to return a verdict and let the account dispatch. If you build on either, does this create real friction, and is there a formulation that keeps the coupling guarantee without the Guard owning dispatch?

  2. Cross-chain mirroring. For anyone working on cross-chain agent execution, how would you handle nullifier registry mirroring where a capability is deliberately issued without a designated home domain? Post 2 framed this as a spend problem rather than an availability problem, which is why the draft declines to solve it. Is there an ordering guarantee here that is not simply a bridge with extra steps?

  3. The relocation itself. Is binding unclonability to the proof of execution rather than the credential the right move in a classical setting, or is there a construction that gets closer to the original quantum property?

  4. The residual. Coupling bounds a clone to performing the authorized action early rather than destroying the capability. Is bounding the damage the right stopping point for a standard at this layer, or should the draft say more about detecting a premature but valid spend?

Reviews on the PR or here are equally welcome. Thanks to everyone who pushed on this, particularly for the adversarial vectors, which changed the design rather than the wording.

The ERC-8354 characterization is accurate – CAPV keeps the ruleset confidential, this keeps the execution credential from being replayed, and neither one needs the other to function. Good example of neighbouring standards actually staying in their lane instead of overlapping.

One thing worth naming explicitly in the “Relationship to Neighbouring Standards” section: this guarantees exactly-once execution of a specific authorized payload, but says nothing about whether that payload should have been authorized in the first place. A compromised agent replaying a stolen credential and a legitimate agent executing a genuinely bad decision produce the same on-chain shape from this standard’s point of view – both are “a valid, unreplayed nullifier consumed once.” That’s not a gap in this proposal (scope says as much – “not an access control framework”), just worth being explicit that unclonability and soundness are orthogonal properties a system needs both of, not substitutes for each other.

On the cross-chain nullifier-registry-mirroring question: we’ve dealt with a structurally adjacent problem – avoiding a single point of trust for verifying a signed artifact – by publishing to multiple independent relays/nodes rather than one authoritative registry, and letting a verifier check any of them (or recompute locally from the signature). Doesn’t solve your race-condition problem directly since a nullifier registry needs actual consensus on “has this been spent,” not just availability – but if the failure mode you’re most worried about is a single mirror going down or lying, redundant independent mirrors with local recomputability might be worth a look even if full cross-chain consensus stays the harder open problem.

Thanks, this is a useful distinction and I am going to make it explicit rather than leave it implied by the scope section.

On unclonability vs soundness. You put it better than the current text does. The nullifier answers “has this capability been consumed”, not “should this capability have been issued”. An attacker replaying a stolen credential and a well behaved agent executing a genuinely bad decision terminate in the same on chain shape, a valid previously unseen nullifier consumed once, because the standard only ever inspects the consumption side. I will add wording along these lines to Relationship to Neighbouring Standards:

Unclonability and authorization soundness are orthogonal. This standard guarantees that a specific authorized payload executes at most once. It makes no claim about whether that payload should have been authorized in the first place. Systems needing both properties must source them from different layers, and neither is a substitute for the other.

That also gives a cleaner three layer picture of where the neighbouring drafts sit. Function scoped delegation constrains what an agent may ever be authorized to touch. ERC-8354 governs how a particular authorization decision was reached while keeping the ruleset confidential. This draft only governs consumption of the resulting credential. A domain missing any of the three has a real gap, and the gaps do not cover for one another.

On mirroring. Agreed that it does not port directly, and the reason it does not is worth naming. Publishing a signed artifact to redundant relays is an availability problem, so any honest copy is sufficient and duplication is harmless. A nullifier registry is a spend problem, where the answer has to be unique rather than merely available, so redundancy without ordering is precisely what an attacker wants.

Where your suggestion does land is the read path. My current leaning is to remove the cross chain race by construction rather than solve it: bind the nullifier preimage to a home domain (chainId plus a domain identifier), so a capability token is only ever spendable against one registry. Moving a capability across domains then requires an explicit burn on the source and reissue on the destination, which converts a silent double spend race into an ordinary bridging step with an ordering the bridge already enforces. Redundant independent mirrors are genuinely useful there for the destination’s proof of non spend read, where availability and local recomputability are what you actually need, and a lying mirror gets caught because the claim is checkable against the source registry.

The case that stays hard is a capability deliberately issued as spendable on either of two chains with no designated home. That needs real consensus on spentness, and I only see two honest options: a shared settlement layer both domains read from, or an optimistic registry with a challenge window and bonded relayers, which reintroduces latency into a path I am already worried about latency in. I am inclined to declare the either or case out of scope for a first draft and require a home domain.

Curious whether you have hit cases where the “any honest copy is enough” model held up against an adversary who benefits from a stale read rather than an unavailable one.

The orthogonality cut is the right one, and pinning it explicitly the way you and babyblue landed on it removes the main way this gets misread. Consumption is not authorization, and saying so up front saves a lot of thread later.

One thing worth nailing down on the recurring-action question. If the nullifier derives from a hidden salt rather than the payload, a legitimate recurring action that reuses the same salt collides with itself, not just a clone. So the exactly-once unit is really per-issuance, not per-agent. That points at the orchestrator issuing a fresh salt, or a salt plus a monotonic index, for each authorized execution, and the spec being explicit that reuse of a spent salt is indistinguishable from a clone by design. Otherwise an honest agent that retries or runs a scheduled action bricks itself.

On where I can help, the two open gaps are the Guard interface and a reference implementation. I have built the nullifier registry plus proof verification shape for the confidential-verdict work, so I can port a minimal Guard interface (register nullifier, verify proof, reject on collision) plus a small reference impl with the double-spend negative test, if that is useful. It stays your draft, I just fill the engineering side. Tell me the nullifier preimage layout you want (salt, agentId, chainId, domain) and I will wire it to match.

Yes on per-issuance, and I want to state it as a normative requirement rather than a note. The exactly-once unit is the issuance, not the agent and not the action. Reuse of a spent salt is indistinguishable from a clone by design, so the orchestrator MUST NOT reissue a salt, and the spec should say that in those words. I would rather an honest orchestrator hit a hard revert than have the property quietly weaken.

The safe way to get that is derivation rather than randomness: salt = HKDF(issuerSecret, agentId || homeDomainId || capabilityIndex) with a monotonic index per (agent, domain). Fresh salts from an RNG work until the RNG does not, and a repeated salt is silent until the second spend reverts. A monotonic index makes accidental reuse structurally impossible and gives the orchestrator a gap-detectable issuance ledger for free.

On the preimage layout, one correction. Do not put chainId in the nullifier preimage. It is the intuitive move and it inverts the property. If nullifier = H(salt, agentId, chainId, domain), then a clone replaying on chain B computes a different nullifier, chain B’s registry has never seen it, and the replay succeeds. Chain binding has to be an acceptance check, not a preimage ingredient.

So, split into two hashes:

NULLIFIER_TAG  = keccak256("ERC-XXXX/nullifier/v1")
CAPABILITY_TAG = keccak256("ERC-XXXX/capability/v1")

private:  salt
public:   capabilityCommitment, nullifier, agentId,
          homeChainId, homeDomainId, capabilityIndex,
          actionCommitment, executor, expiry

nullifier            = H(NULLIFIER_TAG, salt)
capabilityCommitment = H(CAPABILITY_TAG, salt, agentId,
                         homeChainId, homeDomainId,
                         capabilityIndex, actionCommitment)

The nullifier is chain-independent on purpose, so the same salt maps to the same nullifier everywhere and a mirrored registry stays coherent. Everything else binds through the commitment, which the circuit proves consistent with the same salt. The Guard then enforces homeChainId == block.chainid, so a clone on the wrong chain fails the home check rather than sailing past a fresh nullifier.

Two consequences worth having in the spec text. A publicly computable nullifier would let anyone burn a capability before the honest agent spends it, so salt secrecy is load-bearing for liveness and not just for unclonability. And the nullifier gives at-most-once, not “the right agent wins”: a clone holding the memory holds the salt, so both parties can race and the standard only guarantees one of them lands. Executor binding does not help there, since the clone shares the identity. That is a real limit, and the mitigations are expiry plus orchestrator revocation of unconsumed capabilities, not the nullifier.

On your offer, yes, and thank you. The reuse I want is the executor binding from CAPV verbatim: msg.sender == executor or a relayed EIP-712 signature checked through SignatureChecker, with the nullifier doing the signature replay protection. That problem is already solved next door and there is no reason to solve it twice.

I have written up the full engineering scope so you are not reverse-engineering intent from forum posts. Interfaces, circuit constraints, and a test plan with the double-spend and recurring-action cases spelled out. The negative test I care most about is the honest recurring action: two issuances at different indices both succeed, and the same salt at the same index reverts on the second spend.

Good correction, and you are right. Baking chainId into the nullifier preimage would fork the nullifier per chain, so the same credential could be spent once on each chain and the global double-spend property breaks. Chain binding belongs as an acceptance predicate, the verifier checks the proof commits to the intended chainId as a public input, while the nullifier stays chain-independent so it is spent once everywhere. I will follow that.

On the reuse, yes, I will take the executor-binding path from CAPV verbatim, the signature-or-sender check with the single-use nullifier, and shape the Guard interface around it. Send me the scope writeup and I will start on the interface plus a reference skeleton.

The proof-latency item in your Outstanding Issues is larger than a timing window, because the nullifier fixes how many executions happen and says nothing about which one. A clone that copied agent memory carries the same salt, the same agentId, and under the executor-binding path being ported from CAPV the same executor key, so the Guard cannot rank the two by construction, which is the indistinguishability the draft opened with. The clone also holds the structural advantage in that race, since it is not waiting on the agent’s own reasoning loop, so a secondary time lock moves the race later without changing who wins it. Written precisely the guarantee is at most once with no ordering, and that belongs beside the exactly-once claim in Scope of the Security Claim.

What compensates for it is already in the design and currently discarded: a nullifier collision is the only on-chain evidence that a clone exists, and the Guard answers it with a plain revert that the salt rule makes indistinguishable from an orchestrator reissuing a spent index, so a live compromise and a scheduling bug look identical. Emitting a Collision event with agentId and capabilityIndex costs one event and lets the orchestrator classify it unaided, because with HKDF over a monotonic index a collision on an index it never issued is a clone while one on an index it did issue is its own bug; freezing further issuance for that agentId is stronger containment but turns any reissue bug into a halt, which is why the event is the right default and the freeze belongs in orchestrator policy.

Exactly-once is a count, and the property an operator actually needs here is the alarm that fires when the count is contested.

Nullifier layout first, since you asked for it concretely.

nullifier = H(salt, agentId, domainId)

salt is the per-issuance secret, HKDF-derived with a monotonic index so a salt can never be reused across issuances, which is where the exactly-once unit actually lives. agentId binds the credential to the identity. domainId scopes it, the same way CAPV scopes its nullifier, so the credential is spent per domain and not across unrelated ones. chainId is deliberately not in the preimage, per your correction. The nullifier stays chain-independent so it is spent once globally, and chain binding is an acceptance predicate, the proof commits to the intended chainId as a public input and the verifier checks it, so a proof made for one chain cannot be replayed on another while the nullifier still cannot be cloned across chains.

On Cedric’s collision point, the instinct is right and the mechanism needs one correction. You cannot emit a Collision event from the rejecting path, because a revert discards its logs, so an event on the second spend never lands. And not reverting to make the event land weakens the guarantee, the clone would partially execute. The way to get clone-detection without giving up the atomic reject is two-sided. The first spend emits the nullifier-burn event, the positive trace a watcher keys on, and the second spend reverts with a named error, CredentialAlreadySpent(nullifier), not a silent revert. An orchestrator simulating or reading the revert data sees exactly which credential collided and can act on it, and the clone still never runs. So the observability comes from the burn event plus the named error, not from an event on a path that cannot emit one.

If that shape works for both of you I will wire the Guard interface to it. Executor binding from CAPV, single-use nullifier with the layout above, and the named CredentialAlreadySpent error.

@zexoverz @cedricbrown

Thank you both for the incredibly sharp feedback. This precisely addresses several of the outstanding issues from my initial draft.

On Proof Latency and the Security Claim (Thanks, @cedricbrown)

Cedric, your analysis of the race condition is spot on. You are entirely right that a clone holds a structural advantage by skipping the agent’s reasoning loop, and that a time lock only delays the race without changing the winner. I will update the “Scope of the Security Claim” to explicitly define the guarantee as at most once with no ordering, rather than “exactly-once.”

Your point about framing the collision not just as a failure, but as the primary on-chain alarm for a live compromise, is exactly the right mental model for the orchestrator’s policy layer.

On Collision Observability and EVM Mechanics (@zexoverz)

Zexoverz, your correction regarding the EVM revert behavior is perfect. Cedric’s instinct for an alarm was exactly right, but you are completely correct that any Collision event emitted on the rejecting path would be wiped out by the state revert.

Your two-sided approach is the ideal solution:

  1. The first valid execution emits the positive NullifierBurned event.
  2. Any subsequent attempt (whether a malicious clone or a benign scheduling bug) triggers a hard revert with the custom error CredentialAlreadySpent(nullifier).

This ensures we don’t weaken the guarantee by allowing partial execution, while still giving orchestrators the exact revert data they need during simulation to freeze the compromised agentId.

On the Nullifier Layout (@zexoverz)

The proposed layout is extremely clean:

$\text{nullifier} = H(\text{salt}, \text{agentId}, \text{domainId})$

Keeping chainId out of the preimage and strictly enforcing it as a public input to the proof is an elegant way to handle the cross-chain dilemma I noted in the draft. It achieves global single-use without forcing us to design a complex cross-chain state mirroring protocol.

Next Steps

If that shape works for both of you, you have my full green light to wire up the Guard interface to this spec (Executor binding from CAPV, the single-use nullifier layout above, and the named CredentialAlreadySpent error). I will get to work updating the ERC text to reflect the updated security scope and the new architecture.

1 Like

@mzf11125 @cedricbrown @zexoverz — the orthogonality framing in this thread is the reason
I’m posting rather than lurking, so let me stay inside it and offer a neighbouring lane
rather than a critique.

Cedric’s ordering point is the one I want to pick up. Once the guarantee is stated
precisely as at most once, with no ordering, a question follows that I don’t think the
thread has answered yet: the clone wins the race — what did it win?

Your layer bounds how many times a credential is consumed. It says nothing about
what consumption is worth in total, and it correctly declines to: that’s authorization,
and you’ve already fenced it off. But an operator needs both numbers, and the second one
has to come from somewhere.

I ran the scenario against a gate I have deployed on Base Sepolia, because I’d rather show
a receipt than argue from a diagram. Two executions under the same agent identity:

amount nonce commitment result
legitimate agent 4 000 000 000 000 000 wei 1001 0x7e5dc801…9e71 success
clone 1 wei 1002 0xc10d1c6b…fc57 revertedover effective cap

The part that matters for this thread: nothing was replayed. The clone presents a
different action, a different salt, a different commitment, a freshly signed verdict and an
unused nonce. A nullifier registry would have seen two perfectly legitimate consumptions and
accepted both. It still gets nothing, because the bound is welded to the identity, not to
the credential:

require(spent[agentId] + amount <= effectiveCap(agentId), "over effective cap");

effectiveCap is the minimum along the agent’s lineage, and spent accumulates across every
execution regardless of how many credentials were issued. So N clones of one agent share one
budget rather than multiplying it — which is the opposite of the intuition credential-based
systems invite, where N credentials naturally reads as N budgets.

That slots next to the three layers you set out in #3 rather than into any of them:

  1. function-scoped delegation — what an agent may ever be authorized to touch
  2. ERC-8354 — how a particular decision was reached, ruleset confidential
  3. this draft — that the resulting credential is consumed at most once
  4. (what I’m describing) — a standing cumulative bound on the identity itself, which caps
    what winning the race is worth, and cascades containment once your alarm fires

On that last part: Cedric put freezing further issuance in orchestrator policy, which is the
honest place for it given your scope. It can also be a protocol primitive — in the registry I
use, freezing an agent deactivates its entire subtree through an ancestor walk, so a
CredentialAlreadySpent revert can be answered with one transaction rather than a policy
document. Not a proposal for your spec; just noting the effector exists if you ever want to
point at one.

Where this stops, so you don’t have to find it yourselves. It bounds one identity, not
the aggregate consumption of a lineage. A parent and its child each spending up to their own
cap can together exceed the parent’s — I’ve measured that and it’s written into my own
deployment notes as a stated limit, not a footnote. So this composes with your layer for the
single-agent clone case, which is the case your draft opens with; a clone that goes on to
spawn further agents needs the cascade, not the cap.

Everything above is reproducible — the registry and gate are verified on Sourcify and the two
transactions are linked. Happy to be told this belongs in a different lane entirely, or that
someone’s already covering it.

The salt-secrecy line in #5, that a publicly computable nullifier would let anyone burn a capability before the honest agent spends it, is the one item in this thread still open, and the two-sided observability in #8 sharpens it rather than closing it. Once the guarantee is written as at most once with no ordering, the clone’s cheapest move is not to win the race for the valuable action, it is to spend the credential at all. It holds the same salt, the same agentId and the same executor key, so it can consume the issuance the moment it has it, and the honest agent’s later attempt reverts with CredentialAlreadySpent. The count is preserved exactly as specified and the deployment still loses, because the orchestrator issued that capability ahead of the decision about whether to use it, and the clone is the party that never runs the decision.

What determines how bad that is, is a question the draft currently leaves to the implementer. If the Guard accepts any proof whose public inputs are internally consistent, a leaked salt is sufficient on its own, since the clone can form its own capabilityCommitment over a null actionCommitment and burn the nullifier on nothing. If the Guard instead requires that commitment to be bound to an issuance record or an issuer signature, the clone is confined to the exact authorized action and the attack degrades from an arbitrary burn to a premature one. Those are different security properties and the choice reads as normative, so it belongs in the interface rather than in the reference implementation.

The second half of it is recovery, which has no stated path. Under HKDF over a monotonic index a burned index is permanently unusable, so a capability killed this way cannot be re-authorized where it stood, and the orchestrator has to reissue at the next index. That is the same classification you already get from the collision, an index it never issued means a clone and an index it did issue means its own bug, so the reissue rule costs nothing to state and without it an honest orchestrator that takes a burn has a dead capability and no specified way to replace it. It also has to agree with an identity-scoped bound of the kind in #10 on one point, whether a burn that executed nothing still consumes budget, because if it does then the cheapest denial of service against the whole agent is a series of dust spends.

At most once bounds what an attacker can do with a credential. It says nothing about what an attacker can do to one.

@zexoverz since you are wiring the Guard interface, the negative-test half is written and yours to take: GitHub - renezander030/unclonable-credential-guard-tests: Adversarial test vectors for the Guard interface in the Unclonable Agent Execution Credentials ERC draft: grief burn on a leaked salt, issuance binding, missing recovery rule, no-ordering as an assertion. · GitHub

Eight Foundry tests against the shape you settled in #8, no circuit required. The verifier is mocked as a salt check, which accepts exactly the statements the real circuit accepts, whoever holds the salt can prove and nobody else can, so a clone qualifies by construction and no test turns on the mock being weak. It covers the two cases the draft asked for in #5, two issuances at different indices both succeeding and the same salt at the same index reverting on the second spend, plus the burn event and the named error on their respective paths.

The part worth your attention before you fix the interface is that requireIssuance is an immutable in there rather than an argument. With it off, a clone burns the credential on a null action and the honest task can never run, which is the liveness case from my last post. With it on, that specific burn reverts as CommitmentNotIssued and the honest agent still spends. So whether the Guard checks the capabilityCommitment against an issuance record is a security property rather than an implementation detail, and it is currently unstated.

One result cuts against the easy conclusion. Issuance binding does not close it, it narrows it: the clone spends the authorized action early instead, and that test passes too. Worth knowing before the interface hardens around the assumption that binding is sufficient.

Take any of it without attribution. It is faster to hand you the tests than to have you write them twice.

@cedricbrown I think #11 helps separate another property that had been bundled into “single use”: preventing reuse is different from preventing someone from destroying a valid issuance before its intended execution.

Trying to map where the thread has landed:

Question Where the discussion seems to be
Was the action correctly authorized? Outside this draft; @babyblueviper1 and @mzf11125 separated authorization soundness from consumption.
Can the same issuance execute twice? No. The intended guarantee is now at-most-once, per issuance.
Which holder wins a race? Unspecified. As @cedricbrown noted in #7, the Guard cannot distinguish the original from a clone holding the same state.
Can a collision be observed? @zexoverz #8 gives the current shape: first spend emits the burn event, later attempts revert with CredentialAlreadySpent.
How much can one identity consume overall? @helmymekaoui-web #10 describes this as a neighbouring identity-scoped budget layer.
Can a valid issuance be destroyed before the intended action? This still seems open, and #11 makes the distinction explicit.

One question I have is whether the remaining issue is slightly broader than issuance binding.

Suppose the Guard requires capabilityCommitment to correspond to an actual issuer-authorized issuance. That removes the arbitrary-null-action case Cedric describes: possession of the salt no longer lets the clone invent a different commitment.

But does it prevent the credential from being burned without the committed action actually taking place?

The current interface exposes consume(cap, proof) as its own primitive. If consumption and execution are separable, a clone that holds the salt and executor key can still submit the issued capability directly, consume the nullifier, and leave the intended execution unavailable afterwards.

So perhaps there are two decisions rather than one:

issuer binding
→ was this exact capability actually issued?

execution coupling
→ can this capability only become consumed as part of the action it commits to?

If the second property is intended, does it belong in the ERC interface?

For example, should consume() only be callable from an authorized execution module/hook, with the burn and target call occurring in the same transaction and reverting together? Or is the ERC deliberately defining consumption independently from whether the downstream action completes, with grief recovery left to the orchestrator?

That choice also seems to affect the recovery question from #11.

If premature consumption is an accepted failure mode, I think it would help to state the recovery transition explicitly:

issued[i] → burned[i] → reissue[i+1]

rather than leaving a killed issuance without a standardized successor rule.

There are also two implementation details I noticed while comparing the current repo with the thread that may just be pre-implementation artifacts:

  • #8/#9 discuss the nullifier layout as H(salt, agentId, domainId), while the current SPEC.md uses H(NULLIFIER_TAG, salt).

  • salt is described as the private witness, but it is still present in the Solidity Capability calldata while the Noir circuit treats it as private input.

Are those intentional changes from the forum discussion, or simply pieces that have not been reconciled yet?

If the objective is to keep the ERC narrowly about consumption, my tentative boundary would be:

  • Normative: issuance binding, at-most-once semantics, and the conditions under which consumption is allowed to occur.
  • Orchestrator policy: what to do after detecting a contested/burned issuance, including freeze and reissue.

That would still leave the identity-level budget in #10 as a separate composable layer rather than pulling it into this ERC.

1 Like

Thanks Cedric, and you’re right, issuance binding only narrows it. Let me split the two cases in your suite.

If the clone holds both the salt and the executor key, then by definition the credential is cloned and no interface check can tell the two apart. That is a key-management boundary, not something the Guard can close. What it should still guarantee there is that the authorized action executes at most once, which the nullifier already gives.

The part that is actually fixable is the one you point at, consume being a separable primitive. A clone shouldn’t be able to submit consume(cap, proof), burn the nullifier, and leave the intended execution unavailable. So consumption and execution should be coupled in one call, the Guard burns the nullifier as part of executing the action, never on its own. That removes the burn-without-execute grief and the early-consume race.

Recovery should mean re-issuance under a fresh salt so the old nullifier stays dead, not reviving a burned one. I’ll take the negative-test half and run your premature-spend and missing-recovery vectors against this, then reply on the repo.

1 Like

Ran your vectors @cedricbrown, the grief burn reproduces on the implementation. consume takes no issuance and the nullifier is action-independent, so a clone that holds the salt burns it on any action. Put up a PR on the reference impl that closes it with mandatory issuance and a coupled consume/execute, and adapted your tests alongside. Coupled guard + adversarial vectors that close the grief burn by zexoverz · Pull Request #1 · mzf11125/unclonable-agent-execution-credentials · GitHub Thanks again, they made the gap concrete.

Thanks @cedricbrown and @zexoverz for the insights

I’ve merged the pull request on the reference implementation, I will create the PR next on ERCs so we have a number here. Thanks again for the contribution

Thanks for merging, and congrats on the number.

On the spec question I left open, my take is that mandatory issuance belongs in the core rather than beside it. Without it the nullifier can be burned on an action nobody ever authorized, and that is the whole grief burn rather than an edge of it. It is also cheap, one check against a commitment you already compute.

Coupling consume and execute is the bigger change and I would treat it separately. It moves where the action runs, which is more invasive than adding a check, and it constrains how integrators structure their executor. Worth doing, but it deserves its own discussion rather than riding along with the issuance fix.

Either way it is your call as author. Ping me when the ERCs PR is up and I will follow it.

1 Like

Catching up on this thread, apologies for the late reply:

@helmymekaoui-web — thanks for post 10, and for bringing a live gate instead of a diagram. The identity-scoped cumulative bound is folded in as its own layer, the Budget row in Relationship to Neighbouring Standards, explicitly not defined by this draft and not substitutable by it. Your framing is the one that stuck: this draft bounds how many times a credential executes, your layer bounds what that execution is worth in total, and an operator needs both numbers from different places. The lineage gap you flagged yourself, a parent and child each spending up to their own cap and together exceeding the parent’s, stays out of scope for the same reason the rest of your layer does. It is a real limit on the composition, not on either piece alone, and it is worth stating in the spec text as a known seam rather than leaving people to find it the way you did.

@WeissCurry — thanks for post 13, and specifically for separating issuer binding from execution coupling. That is exactly the split Faisal and I ended up shipping. Mandatory issuance closes the arbitrary-null-action case, coupling closes the separable-consume case, and both landed in the PR merged Aug 16. On your two discrepancies: the nullifier layout is settled as keccak256(NULLIFIER_TAG, salt), not H(salt, agentId, domainId). Both identifiers are already inputs to the salt derivation and already bound through the capability commitment, so carrying them in the nullifier a second time added no security property and gave the circuit and the Guard a second place to disagree. And the salt is out of calldata now, private witness of the circuit only, so it no longer sits exposed in the mempool ahead of inclusion. Both were real gaps, not artifacts you imagined. Your proposed recovery transition, issued[i] → burned[i] → reissue[i+1], is the shape the spec now states explicitly rather than leaving implied.

@zexoverz — thanks for the PR.

On mandatory issuance, agreed without qualification. That one closes an actual gap in the guarantee rather than adding defense in depth, and it costs a comparison against a commitment the Guard already computes. No real argument against putting it in the core.

On coupling, I hear the separation you’re drawing, and I want to say plainly why I promoted it anyway instead of leaving it as a profile. A Guard that offers coupling as optional does not actually deliver the guarantee the spec claims. Scope of the Security Claim says a burned nullifier means the authorized action ran. If coupling is a recommendation an integrator can skip, that sentence is only true for integrators who opted in, and every other deployment is back to the separable-consume grief burn with a paragraph telling them not to do that. A SHOULD sitting next to the exact property the standard is named after reads as a standard that does not stand behind its own headline claim.

The friction you’re pointing at is real, and it is the same one I flagged as open feedback item 1. Coupling requires dispatch to route through the Guard, and that is a genuine collision with how 7579 and 6900 modules expect to hand back a verdict and let the account dispatch on its own. Making coupling normative does not resolve that tension, it forces it into the open instead of letting implementers quietly route around it. I would rather have that argument now, with the incompatibility visible in the spec text, than ship a version where it stays invisible until someone builds against both and hits the gap in production.

If there is a formulation that keeps the coupling guarantee without the Guard literally owning dispatch, for example an inline verdict the account is required to consume atomically rather than the Guard performing the call itself, that would resolve this properly instead of trading one problem for another. Open to being wrong on where I landed this.

ERCs PR is up at ethereum/ERCs#1953. Will ping you there.