Alert Source Discuss
⚠️ Review Standards Track: ERC

ERC-5639: Delegation Registry

Delegation of permissions for safer and more convenient signing operations.

Authors foobar (@0xfoobar), Wilkins Chung (@wwhchung) <wilkins@manifold.xyz>, ryley-o (@ryley-o), Jake Rockland (@jakerockland), andy8052 (@andy8052)
Created 2022-09-09

Abstract

This EIP describes the details of the Delegation Registry, a proposed protocol and ABI definition that provides the ability to link one or more delegate wallets to a vault wallet in a manner which allows the linked delegate wallets to prove control and asset ownership of the vault wallet.

Motivation

Proving ownership of an asset to a third party application in the Ethereum ecosystem is common. Users frequently sign payloads of data to authenticate themselves before gaining access to perform some operation. However, this method–akin to giving the third party root access to one’s main wallet–is both insecure and inconvenient.

Examples:

  1. In order for you to edit your profile on OpenSea, you must sign a message with your wallet.
  2. In order to access NFT gated content, you must sign a message with the wallet containing the NFT in order to prove ownership.
  3. In order to gain access to an event, you must sign a message with the wallet containing a required NFT in order to prove ownership.
  4. In order to claim an airdrop, you must interact with the smart contract with the qualifying wallet.
  5. In order to prove ownership of an NFT, you must sign a payload with the wallet that owns that NFT.

In all the above examples, one interacts with the dApp or smart contract using the wallet itself, which may be

  • inconvenient (if it is controlled via a hardware wallet or a multi-sig)
  • insecure (since the above operations are read-only, but you are signing/interacting via a wallet that has write access)

Instead, one should be able to approve multiple wallets to authenticate on behalf of a given wallet.

Problems with existing methods and solutions

Unfortunately, we’ve seen many cases where users have accidentally signed a malicious payload. The result is almost always a significant loss of assets associated with the delegate address.

In addition to this, many users keep significant portions of their assets in ‘cold storage’. With the increased security from ‘cold storage’ solutions, we usually see decreased accessibility because users naturally increase the barriers required to access these wallets.

Proposal: Use of a Delegation Registry

This proposal aims to provide a mechanism which allows a vault wallet to grant wallet, contract or token level permissions to a delegate wallet. This would achieve a safer and more convenient way to sign and authenticate, and provide ‘read only’ access to a vault wallet via one or more secondary wallets.

From there, the benefits are twofold. This EIP gives users increased security via outsourcing potentially malicious signing operations to wallets that are more accessible (hot wallets), while being able to maintain the intended security assumptions of wallets that are not frequently used for signing operations.

Improving dApp Interaction Security

Many dApps requires one to prove control of a wallet to gain access. At the moment, this means that you must interact with the dApp using the wallet itself. This is a security issue, as malicious dApps or phishing sites can lead to the assets of the wallet being compromised by having them sign malicious payloads.

However, this risk would be mitigated if one were to use a secondary wallet for these interactions. Malicious interactions would be isolated to the assets held in the secondary wallet, which can be set up to contain little to nothing of value.

Improving Multiple Device Access Security

In order for a non-hardware wallet to be used on multiple devices, you must import the seed phrase to each device. Each time a seed phrase is entered on a new device, the risk of the wallet being compromised increases as you are increasing the surface area of devices that have knowledge of the seed phrase.

Instead, each device can have its own unique wallet that is an authorized secondary wallet of the main wallet. If a device specific wallet was ever compromised or lost, you could simply remove the authorization to authenticate.

Further, wallet authentication can be chained so that a secondary wallet could itself authorize one or many tertiary wallets, which then have signing rights for both the secondary address as well as the root main address. This, can allow teams to each have their own signer while the main wallet can easily invalidate an entire tree, just by revoking rights from the root stem.

Improving Convenience

Many invididuals use hardware wallets for maximum security. However, this is often inconvenient, since many do not want to carry their hardware wallet with them at all times.

Instead, if you approve a non-hardware wallet for authentication activities (such as a mobile device), you would be able to use most dApps without the need to have your hardware wallet on hand.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Let:

  • vault represent the vault address we are trying to authenticate or prove asset ownership for.
  • delegate represent the address we want to use for signing in lieu of vault.

A Delegation Registry must implement IDelegationRegistry

/**
 * @title An immutable registry contract to be deployed as a standalone primitive
 * @dev New project launches can read previous cold wallet -> hot wallet delegations
 * from here and integrate those permissions into their flow
 */
interface IDelegationRegistry {
    /// @notice Delegation type
    enum DelegationType {
        NONE,
        ALL,
        CONTRACT,
        TOKEN
    }

    /// @notice Info about a single delegation, used for onchain enumeration
    struct DelegationInfo {
        DelegationType type_;
        address vault;
        address delegate;
        address contract_;
        uint256 tokenId;
    }

    /// @notice Info about a single contract-level delegation
    struct ContractDelegation {
        address contract_;
        address delegate;
    }

    /// @notice Info about a single token-level delegation
    struct TokenDelegation {
        address contract_;
        uint256 tokenId;
        address delegate;
    }

    /// @notice Emitted when a user delegates their entire wallet
    event DelegateForAll(address vault, address delegate, bool value);

    /// @notice Emitted when a user delegates a specific contract
    event DelegateForContract(address vault, address delegate, address contract_, bool value);

    /// @notice Emitted when a user delegates a specific token
    event DelegateForToken(address vault, address delegate, address contract_, uint256 tokenId, bool value);

    /// @notice Emitted when a user revokes all delegations
    event RevokeAllDelegates(address vault);

    /// @notice Emitted when a user revoes all delegations for a given delegate
    event RevokeDelegate(address vault, address delegate);

    /**
     * -----------  WRITE -----------
     */

    /**
     * @notice Allow the delegate to act on your behalf for all contracts
     * @param delegate The hotwallet to act on your behalf
     * @param value Whether to enable or disable delegation for this address, true for setting and false for revoking
     */
    function delegateForAll(address delegate, bool value) external;

    /**
     * @notice Allow the delegate to act on your behalf for a specific contract
     * @param delegate The hotwallet to act on your behalf
     * @param contract_ The address for the contract you're delegating
     * @param value Whether to enable or disable delegation for this address, true for setting and false for revoking
     */
    function delegateForContract(address delegate, address contract_, bool value) external;

    /**
     * @notice Allow the delegate to act on your behalf for a specific token
     * @param delegate The hotwallet to act on your behalf
     * @param contract_ The address for the contract you're delegating
     * @param tokenId The token id for the token you're delegating
     * @param value Whether to enable or disable delegation for this address, true for setting and false for revoking
     */
    function delegateForToken(address delegate, address contract_, uint256 tokenId, bool value) external;

    /**
     * @notice Revoke all delegates
     */
    function revokeAllDelegates() external;

    /**
     * @notice Revoke a specific delegate for all their permissions
     * @param delegate The hotwallet to revoke
     */
    function revokeDelegate(address delegate) external;

    /**
     * @notice Remove yourself as a delegate for a specific vault
     * @param vault The vault which delegated to the msg.sender, and should be removed
     */
    function revokeSelf(address vault) external;

    /**
     * -----------  READ -----------
     */

    /**
     * @notice Returns all active delegations a given delegate is able to claim on behalf of
     * @param delegate The delegate that you would like to retrieve delegations for
     * @return info Array of DelegationInfo structs
     */
    function getDelegationsByDelegate(address delegate) external view returns (DelegationInfo[] memory);

    /**
     * @notice Returns an array of wallet-level delegates for a given vault
     * @param vault The cold wallet who issued the delegation
     * @return addresses Array of wallet-level delegates for a given vault
     */
    function getDelegatesForAll(address vault) external view returns (address[] memory);

    /**
     * @notice Returns an array of contract-level delegates for a given vault and contract
     * @param vault The cold wallet who issued the delegation
     * @param contract_ The address for the contract you're delegating
     * @return addresses Array of contract-level delegates for a given vault and contract
     */
    function getDelegatesForContract(address vault, address contract_) external view returns (address[] memory);

    /**
     * @notice Returns an array of contract-level delegates for a given vault's token
     * @param vault The cold wallet who issued the delegation
     * @param contract_ The address for the contract holding the token
     * @param tokenId The token id for the token you're delegating
     * @return addresses Array of contract-level delegates for a given vault's token
     */
    function getDelegatesForToken(address vault, address contract_, uint256 tokenId)
        external
        view
        returns (address[] memory);

    /**
     * @notice Returns all contract-level delegations for a given vault
     * @param vault The cold wallet who issued the delegations
     * @return delegations Array of ContractDelegation structs
     */
    function getContractLevelDelegations(address vault)
        external
        view
        returns (ContractDelegation[] memory delegations);

    /**
     * @notice Returns all token-level delegations for a given vault
     * @param vault The cold wallet who issued the delegations
     * @return delegations Array of TokenDelegation structs
     */
    function getTokenLevelDelegations(address vault) external view returns (TokenDelegation[] memory delegations);

    /**
     * @notice Returns true if the address is delegated to act on the entire vault
     * @param delegate The hotwallet to act on your behalf
     * @param vault The cold wallet who issued the delegation
     */
    function checkDelegateForAll(address delegate, address vault) external view returns (bool);

    /**
     * @notice Returns true if the address is delegated to act on your behalf for a token contract or an entire vault
     * @param delegate The hotwallet to act on your behalf
     * @param contract_ The address for the contract you're delegating
     * @param vault The cold wallet who issued the delegation
     */
    function checkDelegateForContract(address delegate, address vault, address contract_)
        external
        view
        returns (bool);

    /**
     * @notice Returns true if the address is delegated to act on your behalf for a specific token, the token's contract or an entire vault
     * @param delegate The hotwallet to act on your behalf
     * @param contract_ The address for the contract you're delegating
     * @param tokenId The token id for the token you're delegating
     * @param vault The cold wallet who issued the delegation
     */
    function checkDelegateForToken(address delegate, address vault, address contract_, uint256 tokenId)
        external
        view
        returns (bool);
}

Checking Delegation

A dApp or smart contract would check whether or not a delegate is authenticated for a vault by checking the return value of checkDelegateForAll.

A dApp or smart contract would check whether or not a delegate can authenticated for a contract associated with a by checking the return value of checkDelegateForContract.

A dApp or smart contract would check whether or not a delegate can authenticated for a specific token owned by a vault by checking the return value of checkDelegateForToken.

A delegate can act on a token if they have a token level delegation, contract level delegation (for that token’s contract) or vault level delegation.

A delegate can act on a contract if they have contract level delegation or vault level delegation.

For the purposes of saving gas, it is expected if delegation checks are performed at a smart contract level, the dApp would provide a hint to the smart contract which level of delegation the delegate has so that the smart contract can verify with the Delegation Registry using the most gas efficient check method.

Rationale

Allowing for vault, contract or token level delegation

In order to support a wide range of delegation use cases, the proposed specification allows a vault to delegate all assets it controls, assets of a specific contract, or a specific token. This ensures that a vault has fine grained control over the security of their assets, and allows for emergent behavior around granting third party wallets limited access only to assets relevant to them.

On-chain enumeration

In order to support ease of integration and adoption, this specification has chosen to include on-chain enumeration of delegations and incur the additional gas cost associated with supporting enumeration. On-chain enumeration allows for dApp frontends to identify the delegations that any connected wallet has access to, and can provide UI selectors.

Without on-chain enumeration, a dApp would require the user to manually input the vault, or would need a way to index all delegate events.

Security Considerations

The core purpose of this EIP is to enhance security and promote a safer way to authenticate wallet control and asset ownership when the main wallet is not needed and assets held by the main wallet do not need to be moved. Consider it a way to do ‘read only’ authentication.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

foobar (@0xfoobar), Wilkins Chung (@wwhchung) <wilkins@manifold.xyz>, ryley-o (@ryley-o), Jake Rockland (@jakerockland), andy8052 (@andy8052), "ERC-5639: Delegation Registry [DRAFT]," Ethereum Improvement Proposals, no. 5639, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5639.