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.
-
Spec: https://github.com/yolo-maxi/erc-approval-registry/blob/master/SPEC.md
-
Gas analysis: https://github.com/yolo-maxi/erc-approval-registry/blob/master/Gas%20analysis.md
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
-
Is the current event model enough for indexers, or should it carry more state?

-
Should
permitPermissionstay selector-scoped only, or also support full-target?
-
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.