ERC-7484 - Registry Extension for ERC-7579

Created 2023-08-14
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This proposal standardizes the interface and functionality of Module Registries, allowing modular smart accounts to verify the security of modules using a Registry Adapter. It also provides a reference implementation of a Singleton Module Registry.

Motivation

ERC-4337 standardizes the execution flow of contract accounts and ERC-7579 standardizes the modular implementation of these accounts, allowing any developer to build modules for these modular accounts (hereafter Smart Accounts). However, adding third-party modules into Smart Accounts unchecked opens up a wide range of attack vectors.

One solution to this security issue is to create a Module Registry that stores security attestations on Modules and allows Smart Accounts to query these attestations before using a module. This standard aims to achieve two things:

  1. Standardize the interface and required functionality of Module Registries.
  2. Standardize the functionality of Adapters that allow Smart Accounts to query Module Registries.

This ensures that Smart Accounts can securely query Module Registries and handle the Registry behavior correctly, irrespective of their architecture, execution flows and security assumptions. This standard also provides a reference implementation of a Singleton Module Registry that is ownerless and can be used by any Smart Account. While we see many benefits of the entire ecosystem using this single Module Registry (see Rationale), we acknowledge that there are tradeoffs to using a singleton and thus this standard does not require Smart Accounts to use the reference implementation. Hence, this standard ensures that Smart Accounts can query any Module Registry that implements the required interface and functionality, reducing integration overhead and ensuring interoperability for Smart Accounts.

Specification

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

Definitions

Required Registry functionality

The core interface for a Registry is as follows:

interface IERC7484Registry {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               Check with internal attester(s)              */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
    function check(address module) external view;

    function checkForAccount(address smartAccount, address module) external view;

    function check(address module, uint256 moduleType) external view;

    function checkForAccount(
        address smartAccount,
        address module,
        uint256 moduleType
    )
        external
        view;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                   Set internal attester(s)                 */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function trustAttesters(uint8 threshold, address[] calldata attesters) external;


    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*              Check with external attester(s)               */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function check(
        address module,
        address[] calldata attesters,
        uint256 threshold
    )
        external
        view;

    function check(
        address module,
        uint256 moduleType,
        address[] calldata attesters,
        uint256 threshold
    )
        external
        view;
}

The Registry MUST also implement the following functionality:

The Registry SHOULD also implement the following additional functionality:

check functions

check functions with moduleType

Functions with internal attester(s)

trustAttesters

Adapter behavior

A Smart Account MUST implement the following Adapter functionality either natively in the account or as a module. This Adapter functionality MUST ensure that:

Additionally, the Adapter SHOULD implement the following functionality:

Example: Adapter flow using check Adapter flow using check()

Rationale

Attestations

Attestations are onchain assertions made about a module. These assertions could pertain to the security of a module (similar to a regular smart contract audit), whether a module adheres to a certain standard or any other kinds of statements about these modules. While some of these assertions can feasibly be verified onchain, the majority of them cannot be.

One example of this would be determining what storage slots a specific module can write to, which might be useful if a smart account uses DELEGATECALL to invoke the module. This assertion is practically infeasible to verify onchain, but can easily be verified off-chain. Thus, an attester could perform this check off-chain and publish an attestation onchain that attests to the fact that a given module can only write to its designated storage slots.

While attestations are always certain kinds of assertions made about a module, this proposal purposefully allows the attestation data to be any kind of data or pointer to data. This ensures that any kind of data can be used as an assertion, from a simple boolean flag specifying that a module is secure to a complex proof of runtime module behaviour.

Singleton Registry

In order for attestations to be queryable onchain, they need to be stored in some sort of list in a smart contract. This proposal includes the reference implementation of an ownerless Singleton Registry that functions as the source of truth for attestations.

The reasons for proposing a Singleton Registry are the following:

Security: A Singleton Registry creates greater security by focusing account integrations into a single source of truth where a maximum number of security entities are attesting. This has a number of benefits: a) it increases the maximum potential quantity and type of attestations per module and b) removes the need for accounts to verify the authenticity and security of different registries, focusing trust delegation to the onchain entities making attestations. The result is that accounts are able to query multiple attesters with lower gas overhead in order to increase security guarantees and there is no additional work required by accounts to verify the security of different registries.

Interoperability: A Singleton Registry not only creates a greater level of “attestation liquidity”, but it also increases module liquidity and ensures a greater level of module interoperability. Developers need only deploy their module to one place to receive attestations and maximise module distribution to all integrated accounts. Attesters can also benefit from previous auditing work by chaining attestations and deriving ongoing security from these chains of dependencies. This allows for benefits such as traversing through the history of attestations or version control by the developer.

However, there are obviously tradeoffs for using a singleton. A Singleton Registry creates a single point of failure that, if exploited, could lead to serious consequences for smart accounts. The most serious attack vector of these would be the ability for an attacker to attest to a malicious module on behalf of a trusted attester. One tradeoff here is that using multiple registries, changes in security attestations (for example a vulnerability is found and an attestation is revoked) are slower to propagate across the ecosystem, giving attackers an opportunity to exploit vulnerabilities for longer or even find and exploit them after seeing an issue pointed out in a specific Registry but not in others.

Due to being a singleton, the Registry needs to be very flexible and thus likely less computationally efficient in comparison to a narrow, optimised Registry. This means that querying a Singleton Registry is likely to be more computationally (and by extension gas) intensive than querying a more narrow Registry. The tradeoff here is that a singleton makes it cheaper to query attestations from multiple parties simultaneously. So, depending on the Registry architectures, there is an amount of attestations to query (N) after which using a flexible singleton is actually computationally cheaper than querying N narrow registries. However, the reference implementation has also been designed with gas usage in mind and it is unlikely that specialised registries will be able to significantly decrease gas beyond the reference implementations benchmarks.

Module Types

Modules can be of different types and it can be important for an account to ensure that a module is of a certain type. For example, if an account wants to install a module that handles the validation logic of the account, then it might want to ensure that attesters have confirmed that the module is indeed capable of performing this validation logic. Otherwise, the account might be at risk of installing a module that is not capable of performing the validation logic, which could lead to an account being rendered unusable.

Nonetheless, the Registry itself does not need to care what specific module types mean. Instead, attesters can provide these types and the Registry can store them.

Related work

The reference implementation of the Registry is heavily inspired by the Ethereum Attestation Service. The specific use-case of this proposal, however, required some custom modifications and additions to EAS, meaning that using the existing EAS contracts as the Module Registry was sub-optimal. However, it would be possible to use EAS as a Module Registry with some modifications.

Backwards Compatibility

No backward compatibility issues found.

Reference Implementation

Adapter.sol

contract Adapter {
    IRegistry registry;

    function checkModule(address module) internal {
        // Check module attestation on Registry
        registry.check(module);
    }

    function checkModuleWithModuleTypeAndAttesters(address module, address[] memory attesters, uint256 threshold,  uint16 moduleType) internal {
        // Check list of module attestations on Registry
        registry.check(module, attesters, threshold, moduleType);
    }

}

Account.sol

Note: This is a specific example that complies to the Specification above, but this implementation is not binding.

contract Account is Adapter {
    ...

    // installs a module
    function installModule(
        uint256 moduleTypeId,
        address module,
        bytes calldata initData
    )
        external
        payable
    {
        checkModule(module);
        ...
    }

    // executes a module
    function executeFromExecutor(
        ModeCode mode,
        bytes calldata executionCalldata
    )
        external
        payable
        returns (bytes[] memory returnData)
    {
        checkModule(module);
        ...
    }

    ...
}

Registry

/**
* @dev this implementation is unoptimized in order to make the reference implementation shorter to read
* @dev some function implementations are missing for brevity
*/
contract Registry is IERC7484Registry {
    ...

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               Check with internal attester(s)              */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
    function check(address module) external view {
        (address[] calldata attesters, uint256 threshold) = _getAttesters(msg.sender);

        uint256 validCount = 0;
        for (uint256 i = 0; i < attesters.length; i++) {
            bool isValid = _check(module, attesters[i]);
            if (isValid) validCount++;
        }
        if (validCount < threshold) revert AttestationThresholdNotMet();
    }

    function checkForAccount(address smartAccount, address module) external view {
        (address[] calldata attesters, uint256 threshold) = _getAttesters(smartAccount);

        ...
    }

    function check(address module, uint256 moduleType) external view {
        (address[] calldata attesters, uint256 threshold) = _getAttesters(msg.sender);

        uint256 validCount = 0;
        for (uint256 i = 0; i < attesters.length; i++) {
            bool isValid = _check(module, attesters[i]);
            if (isValid) validCount++;

            AttestationRecord storage attestation = _getAttestation(module, attester);
            if (attestation.moduleType != moduleType) revert ModuleTypeMismatch();
        }
        if (validCount < threshold) revert AttestationThresholdNotMet();
    }

    function checkForAccount(
        address smartAccount,
        address module,
        uint256 moduleType
    )
        external
        view {
        (address[] calldata attesters, uint256 threshold) = _getAttesters(smartAccount);

        ...
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                   Set internal attester(s)                 */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function trustAttesters(uint8 threshold, address[] calldata attesters) external {
        ...
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*              Check with external attester(s)               */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function check(
        address module,
        address[] calldata attesters,
        uint256 threshold
    )
        external
        view
    {
        uint256 validCount = 0;
        for (uint256 i = 0; i < attesters.length; i++) {
            bool isValid = _check(module, attesters[i]);
            if (isValid) validCount++;
        }
        if (validCount < threshold) revert AttestationThresholdNotMet();
    }

    function check(
        address module,
        uint256 moduleType,
        address[] calldata attesters,
        uint256 threshold
    )
        external
        view
    {
        uint256 validCount = 0;
        for (uint256 i = 0; i < attesters.length; i++) {
            bool isValid = _check(module, attesters[i]);
            if (isValid) validCount++;

            AttestationRecord storage attestation = _getAttestation(module, attester);
            if (attestation.moduleType != moduleType) revert ModuleTypeMismatch();
        }
        if (validCount < threshold) revert AttestationThresholdNotMet();
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                         Internal                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function _check(address module, address attester) external view returns (bool isValid){
        AttestationRecord storage attestation = _getAttestation(module, attester);

        uint48 expirationTime = attestation.expirationTime;
        uint48 attestedAt =
            expirationTime != 0 && expirationTime < block.timestamp ? 0 : attestation.time;
        if (attestedAt == 0) return;

        uint48 revokedAt = attestation.revocationTime;
        if (revokedAt != 0) return;

        isValid = true;
    }

    function _getAttestation(
        address module,
        address attester
    )
        internal
        view
        virtual
        returns (AttestationRecord storage)
    {
        return _moduleToAttesterToAttestations[module][attester];
    }

    function _getAttesters(
        address account
    )
        internal
        view
        virtual
        returns (address[] calldata attesters, uint256 threshold)
    {
        ...
    }

    ...
}

Security Considerations

Needs discussion.

Copyright

Copyright and related rights waived via CC0.