ERC-6823 - Token Mapping Slot Retrieval Extension

Created 2023-03-29
Status Draft
Category ERC
Type Standards Track
Authors
  • qdqd (@qd-qd) <qdqdqdqdqd at protonmail.com>

Requires

Abstract

The aim of this proposal is to enhance the precision of off-chain simulations for transactions that involve contracts complying with the ERC-20, ERC-721, or ERC-1155 standards. To achieve this, a method is proposed for obtaining the reserved storage slot of the mapping responsible to track ownership of compliant tokens. The proposed extension offers a standardized entry point that allows for identifying the reserved storage slot of a mapping in a compatible manner. This not only facilitates capturing state changes more precisely but also enables external tools and services to do so without requiring expertise in the particular implementation details.

Motivation

To understand the rationale behind this proposal, it's important to remember how values and mapping are stored in the storage layout. This procedure is language-agnostic; it can be applied to multiple programming languages beyond Solidity, including Vyper.

The storage layout is a way to persistently store data in Ethereum smart contracts. In the EVM, storage is organized as a key-value store, where each key is a 32-byte location, and each value is a 32-byte word. When you define a state variable in a contract, it is assigned to a storage location. The location is determined by the variable's position in the contract's storage structure. The first variable in the contract is assigned to location 0, the second to location 1, and so on. Multiple values less than 32 bytes can be grouped to fit in a single slot if possible.

Due to their indeterminate size, mappings utilize a specialized storage arrangement. Instead of storing mappings "in between" state variables, they are allocated to occupy 32 bytes only, and their elements are stored in a distinct storage slot computed through a keccak-256 hash. The location of the value corresponding to a mapping key k is determined by concatenating h(k) and p and performing a keccak-256 hash. The value of p is the position of the mapping in the storage layout, which depends on the order and the nature of the variables initialized before the mapping. It can't be determined in a universal way as you have to know how the implementation of the contract is done.

Due to the nature of the mapping type, it is challenging to simulate transactions that involve smart contracts because the storage layout for different contracts is unique to their specific implementation, etched by their variable requirements and the order of their declaration. Since the storage location of a value in a mapping variable depends on this implementation-sensitive storage slot, we cannot guarantee similarity on the off-chain simulation version that an on-chain attempted interaction will result in.

This hurdle prevents external platforms and tools from capturing/validating changes made to the contract's state with certainty.

That's why transaction simulation relies heavily on events. However, this approach has limitations, and events should only be informative and not relied upon as the single source of truth. The state is and must be the only source of truth. Furthermore, it is impossible to know the shape of the storage deterministically and universally, which prevents us from verifying the source of truth that is storage, forcing us to rely on information emitted from the application layer.

Specification

The keywords "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.

The proposal suggests an extension to the ERC-20/ERC-721/ERC-1155 standards that allows retrieving the reserved storage slot for the mapping type in any compliant smart-contract implementation in a deterministic manner. This method eliminates the reliance on events and enhances the precision of the data access from storage. The proposed extension therefore enables accurate off-chain simulations. The outcome is greater transparency and predictability at no extra cost for the caller, and a negigleable increase in the deployment cost of the contract.

The proposed extension is a single function that returns the reserved storage slot for the mapping type in any ERC-20/ERC-721/ERC-1155 compliant smart-contract implementation. The function is named getTokenLocationRoot and is declared as follows:

abstract contract ERC20Extension is ERC20 {
    function getTokenLocationRoot() external pure virtual returns (bytes32 slot) {
        assembly {
            slot := <mapping_name>.slot
        }
    }
}

abstract contract ERC721Extension is ERC721 {
    function getTokenLocationRoot() external pure virtual returns (bytes32 slot) {
        assembly {
            slot := <mapping_name>.slot
        }
    }
}

abstract contract ERC1155Extension is ERC1155 {
    function getTokenLocationRoot() external pure virtual returns (bytes32 slot) {
        assembly {
            slot := <mapping_name>.slot
        }
    }
}

For these contracts, off-chain callers can use the getTokenLocationRoot() function to find the reserved storage slot for the mapping type. This function returns the reserved storage slot for the mapping type in the contract. This location is used to calculate where all the values of the mapping will be stored. Knowing this value makes it possible to determine precisely where each value of the mapping will be stored, regardless of the contract's implementation. The caller can use this slot to calculate the storage slot for a specific token ID and compare the value to the expected one to verify the action stated by the event. In the case of a ERC-721 mint, the caller can compare the value of the storage slot to the address of the token's owner. In the case of a ERC-20 transfer, the caller can compare the value of the storage slot to the address of the token's new owner. In the case of a ERC-1155 burn, the caller can compare the value of the storage slot to the zero address. The off-chain comparison can be performed with any of the many tools available. In addition, it could perhaps allow storage to be proven atomically by not proving the entire state but only a location -- to track ownership of a specific token, for example.

The name of the function is intentionally generic to allow the same implementation for all the different token standards. Once implemented universally, the selector derived from the signature of this function will be a single, universal entry point that can be used to directly read the slots in the storage responsible of the ownership, of any token contract. This will make off-chain simulations significantly more accurate, and the events will be used for informational purposes only.

Contract implementers MUST implement the getTokenLocationRoot() function in their contracts. The function MUST return the reserved storage slot for the mapping type in the contract. The function SHOULD be declared as external pure.

Rationale

The idea behind the implementation was to find an elegant and concise way that avoided any breaking changes with the current standard. Moreover, since gas consumption is crucial, it was inconceivable to find an implementation that would cost gas to the final user. In this case, the addition of a function increases the deployment cost of the contract in a minimal way, but its use is totally free for the external actors.

The implementation is minimalist in order to be as flexible as possible while being directly compatible with the main programming languages used today to develop smart-contracts for the EVM.

Backwards Compatibility

No backward compatibility issues have been found.

Reference Implementation

abstract contract ERC20Extension is ERC20 {
    function getTokenLocationRoot() external pure virtual returns (bytes32 slot) {
        assembly {
            slot := <mapping_name>.slot
        }
    }
}

abstract contract ERC721Extension is ERC721 {
    function getTokenLocationRoot() external pure virtual returns (bytes32 slot) {
        assembly {
            slot := <mapping_name>.slot
        }
    }
}

abstract contract ERC1155Extension is ERC1155 {
    function getTokenLocationRoot() external pure virtual returns (bytes32 slot) {
        assembly {
            slot := <mapping_name>.slot
        }
    }

Security Considerations

No security issues are raised by the implementation of this extension.

Copyright

Copyright and related rights waived via CC0.