ERC-5639 Delegation Registry
Review · Accounts & wallets · created 2022-09-09
A shared registry in which a vault wallet holding assets delegates proof-of-ownership authority to another wallet. A dApp can query this registry to confirm the vault wallet's asset holdings using only the delegate wallet's signature.
Why it is needed
To prove ownership of an asset on Ethereum, one must sign a message or interact with a contract directly using the wallet that holds the asset. This applies to editing a marketplace profile, accessing content restricted to NFT holders, entering an event, or claiming an airdrop. Even though the verification itself is a read-only operation, it requires signing with a wallet capable of moving assets, so falling for even one signature request presented by a malicious dApp or phishing site can result in a large loss of the assets tied to that wallet. In practice, there have been many cases of users mistakenly signing a malicious payload and losing assets. Also, many users keep most of their assets in cold storage, and the stronger the security, the higher the access barrier, making it inconvenient to sign every time with a hardware wallet or multisig. This creates a need for a way to approve multiple wallets that can authenticate on behalf of a single wallet.
What to implement
event DelegateForAll(address vault, address delegate, bool value);: Emitted when a user delegates or revokes an entire wallet.event DelegateForContract(address vault, address delegate, address contract_, bool value);: Emitted when a user delegates or revokes a specific contract.event DelegateForToken(address vault, address delegate, address contract_, uint256 tokenId, bool value);: Emitted when a user delegates or revokes a specific token.event RevokeAllDelegates(address vault);: Emitted when a user revokes all delegations at once.event RevokeDelegate(address vault, address delegate);: Emitted when a user revokes all delegations held by a specific delegate wallet.function delegateForAll(address delegate, bool value) external;: Allows a delegate wallet to act on your behalf for all contracts. If value is true it sets the delegation, if false it revokes it.function delegateForContract(address delegate, address contract_, bool value) external;: Allows a delegate wallet to act on your behalf for only a specific contract.function delegateForToken(address delegate, address contract_, uint256 tokenId, bool value) external;: Allows a delegate wallet to act on your behalf for only a single specific token.function revokeAllDelegates() external;: Revokes all delegations you have registered at once.function revokeDelegate(address delegate) external;: Revokes all permissions held by a specific delegate wallet.function revokeSelf(address vault) external;: Lets the delegate remove their own delegation for a specific vault.function getDelegationsByDelegate(address delegate) external view returns (DelegationInfo[] memory);: Returns every valid delegation a specific delegate wallet can act on, as an array of DelegationInfo structs.function getDelegatesForAll(address vault) external view returns (address[] memory);: Returns the list of wallet-level delegate wallets for a specific vault.function getDelegatesForContract(address vault, address contract_) external view returns (address[] memory);: Returns the list of contract-level delegate wallets for a specific vault and contract.function getDelegatesForToken(address vault, address contract_, uint256 tokenId) external view returns (address[] memory);: Returns the list of delegate wallets for a specific token in a specific vault.function getContractLevelDelegations(address vault) external view returns (ContractDelegation[] memory delegations);: Returns all contract-level delegations for a specific vault, as an array of ContractDelegation structs.function getTokenLevelDelegations(address vault) external view returns (TokenDelegation[] memory delegations);: Returns all token-level delegations for a specific vault, as an array of TokenDelegation structs.function checkDelegateForAll(address delegate, address vault) external view returns (bool);: Returns true if the given address is delegated to act for the entire vault.function checkDelegateForContract(address delegate, address vault, address contract_) external view returns (bool);: Returns true if the given address is delegated for a specific contract or for the entire vault.function checkDelegateForToken(address delegate, address vault, address contract_, uint256 tokenId) external view returns (bool);: Returns true if the given address is delegated for a specific token, that token's contract, or the entire vault.
In depth
Three delegation scopes
The registry defines a DelegationType enum with four values, NONE, ALL, CONTRACT, and TOKEN, and splits delegation into three write functions by scope. delegateForAll(delegate, value) delegates across all contracts controlled by the vault, delegateForContract(delegate, contract_, value) delegates a specific contract, and delegateForToken(delegate, contract_, tokenId, value) delegates a single specific token. If value is true the delegation is set, if false it is revoked. The Rationale explains that this granularity lets a vault control asset security with precision, and makes it possible to expose only the relevant assets to a third-party wallet in a limited way.
Delegation checks and inclusion relationships
Dapps or smart contracts check delegation status using the return values of checkDelegateForAll, checkDelegateForContract, and checkDelegateForToken. There is an inclusion relationship between the levels. For a token, the delegate can act if there is a delegation at the token level, the contract level of the contract the token belongs to, or the full-vault level. For a contract, a contract-level or full-vault-level delegation is enough. When checking at the smart contract level, to save gas it is expected that the dapp hints the contract with the delegation level the delegate wallet holds, so the contract can verify using the most gas-efficient check function.
Three ways to revoke
A vault wallet can call revokeDelegate(delegate) to withdraw all permissions from a specific delegate wallet, or revokeAllDelegates() to withdraw every delegation it has registered at once. Conversely, the delegate side can also call revokeSelf(vault) to remove itself from a specific vault's delegation list. Revocations are recorded via the RevokeDelegate and RevokeAllDelegates events, and the setting versus clearing of a delegation is distinguished by the value field (true for set, false for revoke) in the DelegateForAll, DelegateForContract, and DelegateForToken events.
Why on-chain enumeration was chosen
The read functions let delegation relationships be listed directly on chain. getDelegationsByDelegate returns every delegation a specific delegate wallet can act on as an array of DelegationInfo structs, getDelegatesForAll, getDelegatesForContract, and getDelegatesForToken return the list of delegate wallets at each level for a given vault, and getContractLevelDelegations and getTokenLevelDelegations return all contract-level and token-level delegations for a vault. The Rationale cites, as the reason for accepting the extra gas cost of on-chain enumeration, that a dapp frontend can look up the delegations a connected wallet can access and offer a UI picker. Without enumeration, users would have to type in the vault address manually, or the dapp would have to index every delegation event.
Multi-device security and delegation chains
The Motivation proposes that instead of entering a seed phrase on every device, users create a unique wallet per device and register it as an approved secondary wallet of the main wallet. If a given device's wallet is leaked or lost, only that wallet's authentication rights need to be removed. Wallet authentication can also form a chain: if a secondary wallet in turn approves one or more tertiary wallets, those tertiary wallets gain signing authority for both the secondary address and the top-level main address. Team members can each hold their own signer, while the main wallet can invalidate the entire tree just by revoking permission at the root. Hardware wallet users can approve a non-hardware wallet, such as a mobile device, for authentication, letting them use most dapps without carrying their hardware wallet around.
An independently deployed immutable registry
The interface comments in the specification describe this registry as an immutable contract deployed as a standalone primitive. Newly launched projects can read existing delegations, from cold wallets to hot wallets, registered in this registry and incorporate that authority into their own flows. Once a user registers a delegation, later projects can read and build on the same record.
Currently in Review status
This document is a Standards Track ERC created on September 9, 2022, and its current status is Review. Since it is still a proposal under review, interface names and behavioral rules may change going forward, and it has not yet been finalized as a standard. Developers who want to implement this registry or integrate it into a dapp should first check the latest version and status of the source document.
Things to watch out for
- Read-only authentication use: This proposal's core purpose is to authenticate wallet control and asset ownership more securely in cases where the main wallet is not needed and the main wallet's assets do not need to move. The source document suggests viewing this as a way to perform read only authentication.