Draft ERC: Permission Registry — function-scoped delegation for agents, without custody

Hi everyone,

I’m working on a draft ERC for a registry-based delegated authorization primitive and would love feedback before opening the EIP PR.

The objective is to provide a simple primitive for users of a smart contract to give scoped permissions to call functions on a contract on their behalf. What ERC20.approve does to transfers, this proposal wants to do for every function in a contract.

Problem

A lot of DeFi and automation flows need delegated execution without custody transfer: compounding bots, forwarders, social-trading agents, wrapper contracts that let an operator manage one specific position.

Today the usual options aren’t great:

  • ERC-20 approvals are asset-scoped, not action-scoped.

  • Vault custody solves authorization by moving assets into another contract.

  • Per-protocol operator systems aren’t composable and aren’t consistently visible to wallets/indexers.

  • Fully generic execution permissions are hard to render safely.

The goal is a small registry primitive that contracts can integrate with one authorization check and that wallets/indexers can display consistently.

Proposed primitive

The registry stores one authorization blob per (user, operator, target):

auth.length == 0: no approval
auth.length == 4: expiry only = full-target approval
auth.length > 4:  uint32 expiry || sorted bytes4 selectors...

So a user can grant an operator either full approval on a target until expiry, or approval for a sorted bundle of specific selectors until expiry. Expiry is bundle-wide for each (user, operator, target) — intentionally not per-selector, to keep the blob compact.

Integration is one modifier:

modifier onlyAuthorized(address user) {
    if (msg.sender != user) {
        registry.requireAuthorizedCall(user, msg.sender, address(this), msg.sig);
    }
    _;
}

The target contract doesn’t care which mode the user picked — it just asks whether the caller is authorized for msg.sig.

Why full-target approval is first-class

An earlier draft used a pure selector mapping (user → operator → target → selector → expiry). That gives flat O(1) checks but makes broad delegation expensive — every selector is its own storage slot.

For trusted forwarders/routers, broad target-level delegation is the common case. Encoding it as an expiry-only 4-byte blob gives one compact approval, O(1) checks, and gas roughly comparable to an ERC-20 approval. Selector bundles remain available when narrower scope is wanted.

Gas tradeoff

Reference implementation, execution gas:

check grant
Old selector mapping (1 selector) ~2.1k ~35.3k
Packed full-target ~2.6k ~35.8k
Packed bundle, 2 selectors ~3.4k ~40.9k (vs ~58.3k mapping)
Packed bundle, 6 selectors ~4.5k ~54.6k (vs ~163k mapping)
Packed bundle, 20 selectors ~8.5k ~169k (vs ~531k mapping)
ERC-20 approve reference ~2.9k ~31.4k

Full-target approval is in ERC-20-approval territory. Selector bundles beat per-selector storage from 2 selectors onward but pay an O(n) check cost. In practice partial delegation tends to be “a few selectors or the whole target,” so bundles in the 2–5 range are the expected shape.

Selector sorting

Selector bundles must be strictly sorted and unique. The registry rejects unsorted/duplicate input rather than sorting onchain — sorting belongs in wallets/SDKs, and the registry gets deterministic encoding and early-exit checks.

Expiry

External API uses uint48:

  • 0 → revoked / not set

  • type(uint48).max → permanent

  • anything else → Unix timestamp

The reference implementation stores expiry compactly as uint32 (permanent sentinel uint32.max, finite timestamps valid until 2106). This keeps full-target approvals at 4 bytes.

EIP-712 permit

A selector-scoped EIP-712 permit lets an owner grant or revoke a single selector without sending a transaction themselves. Permits are intentionally selector-scoped; full-target approvals should require an explicit on-chain call so wallets can warn appropriately.

Questions for feedback

  1. Is the current event model enough for indexers, or should it carry more state? :white_check_mark:

  2. Should permitPermission stay selector-scoped only, or also support full-target? :white_check_mark:

  3. Are there better patterns to surface actions to wallets with the latest developments in clear-signing?

Thanks — looking for design feedback before turning this into a formal EIP PR.

Edit: based on the feedback below, I updated the draft and reference implementation to make full-target authorization a first-class EIP-712 permit path, via permitFullAuthorization(…), rather than treating it as a selector-permit edge case. I also added a decoded AuthorizationSet(user, operator, target, expiry, selectors) event for whole-blob authorization changes, so wallets/indexers can reconstruct state without decoding the packed storage format. Empty selectors with nonzero expiry means full-target authorization; empty selectors with zero expiry means revoked/no authorization.

1 Like

The ERC-20.approve analogy is exactly right and immediately explains the pattern without new mental models. A few connections worth making explicit:

This formalises delegationRef in the agent trust stack

Recent work on ERC-8004 and ERC-8263 has been building toward a validation subject hash — a canonical claim type binding principal, agentId, delegationRef, codeMeasurement, policyHash, inputHash, outputHash, chainId, nonce into a single verifiable record. The delegationRef field has been intentionally underspecified because no formal on-chain registry existed to point it at. The Permission Registry is that registry. A delegationRef would resolve to the (user, operator, target) tuple stored here, giving verifiers a public, recomputable record of what the agent was authorized to call before any input was committed.

Where it sits in the stack

The Permission Registry answers “what is this agent authorized to do” — upstream of input provenance (WYRIWE, L2) and upstream of execution commitment (OCP, L3). It doesn’t replace identity (L1) or attestation (L4); it scopes what the agent identity is permitted to act on. That’s a load-bearing gap the current stack leaves to convention. This makes it formal.

On permitPermission for full-target approvals

Yes, full-target approval is the common case for trusted agent forwarders and should be a first-class path in permitPermission, not a secondary encoding. The three-mode encoding (empty / 4 bytes / longer) is clean for storage, but the permit interface should reflect the same priority: full-target first, selector bundles as the scoped variant. Matching the encoding hierarchy in the interface reduces integration friction.

On event design for indexers

Emitting the full (user, operator, target, expiry, selectors[]) in the event rather than encoding into bytes keeps indexers simple, they can reconstruct the authorization state without decoding the packed format. Worth the slight calldata cost for the composability gain.

This belongs in the Composition Note v2 scope conversation currently underway across ERC-8004, ERC-8263, ERC-8274 , flagging it to that group now.

Thanks, this is exactly the kind of connection I was hoping to surface.

I agree with the delegationRef framing. The registry gives that field something concrete to resolve to: a public (user, operator, target) authorization record, with either full-target scope or a decoded selector bundle. That makes the agent’s delegated authority verifiable before reasoning about input provenance or execution commitments.

I’ve updated the draft and reference implementation based on your feedback:

• full-target authorization is now a first-class EIP-712 permit path: permitFullAuthorization(…)

• selector permits remain the narrow scoped path

• whole-blob changes now emit AuthorizationSet(user, operator, target, expiry, selectors) for indexers/wallets

• the packed bytes remain an implementation detail, not something every integration has to decode independently

I think this makes the hierarchy clearer: full-target authorization for trusted agent/forwarder relationships, selector bundles for narrower delegation, and decoded events for the composability layer around it.

Appreciate you flagging the ERC-8004 / ERC-8263 / ERC-8274 connection — I’ll keep the delegationRef use case explicit as the draft moves toward an EIP PR.

1 Like

Fran23 — the delegationRef framing Tiago outlined is exactly where this connects to OCP.

The Permission Registry answers what an agent was authorized to do. OCP (ERC-8281) answers what the agent actually did — observation → digest → on-chain commitment → independently verifiable. These are sequential questions, not competing ones. Authorization is upstream of commitment, and the registry gives the delegationRef field in the verification record something concrete and recomputable to point at.

One thing worth making explicit in the spec as you move toward the EIP PR: the authorization state at commit time matters. A commitment anchored under OCP is permanently verifiable, but the authorization that scoped the agent’s action at that moment should be equally recomputable. If the registry stores the (user, operator, target) authorization with an expiry, a verifier needs to be able to reconstruct what was authorized at the timestamp of the OCP commitment — not just what is authorized today. The decoded AuthorizationSet event you added handles this for indexers, but it’s worth stating explicitly that the event log is the canonical historical record for this purpose.

Following this thread. Good timing on the EIP-712 permit path — that’s the right shape for the agent delegation case.

— Damon Zwicker / ERC-8281

1 Like

Authorization and verification are sequential, not competing. “What was the agent authorized to do” has to be answerable before “what did the agent actually do” means anything.

The commit-time point matters specifically for ERC-8275 integration. If a node operator delegates submitCommit / submitReveal authority to an automated agent via the registry, the settlement contract needs to verify the authorization was valid at the commit deadline, not at verification time, which could be weeks later during the challenge window. The event log being the canonical record is what makes that reconstruction possible.

Two things worth making explicit in the spec:

  1. That the AuthorizationSet event log is canonical for historical authorization state (Damon’s point)

  2. That timestamp-scoped authorization queries are a first-class use case, the permit approach should clearly define which timestamp anchors the authorization for settlement contexts

Also still want to understand the onlyAuthorized modifier: does it read msg.sig directly or expect an explicit selector from the integrating contract? That determines whether 8274 and 8275 can call into it cleanly without wrapper logic.

1 Like