What is ERC-1820: Pseudo-introspection Registry Contract?

What is ERC-1820: Pseudo-introspection Registry Contract?

As I was writing a post about ERC777, I had to explain ERC1820 as well, and in that process, I realized that ERC1820 needs a separate article. So let’s delve deeper into ERC-1820.

You might be wondering why is it called a registry contract. Well, any contract or EOA can register which interface they support, and this can be checked by other contracts/EOAs.

We already had an ERC that is ERC165 for this purpose but that was supposed to be implemented by every contract separately, while ERC1820 does it differently, let’s see how it works but first, if you want to know more about the technical details of ERC165 then head over to my ERC721 article.

Now as we know ERC165 is implemented by every contract separately, ERC1820 comes with the idea of having a single registry contract on every chain, with the same contract address, and every other contract will have to register whatever interface they are implementing, within this registry contract. Later on, any user can directly query from the registry contract whether a particular interface has been implemented by a certain contract or not, and which smart contract handles its implementation. It’ll be more clear as we move forward.

It contains 4 main actors, manager, implementer, target, and user.

The flow is something like this :

Manager — The function getManager returns the current manager for an address, if there’s no manager set, it returns the address itself, meaning every address has the manager right of itself. Now, either an address or its manager can change the manager using the setManager function.

    function setManager(address _addr, address _newManager) external {
        require(getManager(_addr) == msg.sender, "Not the manager");
        managers[_addr] = _newManager == _addr ? address(0) : _newManager;
        emit ManagerChanged(_addr, _newManager);
    }

    function getManager(address _addr) public view returns (address) {
        if (managers[_addr] == address(0)) {
            return _addr;
        } else {
            return managers[_addr];
        }
   }

The manager can now call the setInterfaceImplementer function and set the implementer for a target address. Now let’s see what’s inside this function.

function setInterfaceImplementer(
        address _addr,
        bytes32 _interfaceHash,
        address _implementer
    ) external {
        address addr = _addr == address(0) ? msg.sender : _addr;
        require(getManager(addr) == msg.sender, "Not the manager");

        require(
            !isERC165Interface(_interfaceHash),
            "Must not be an ERC165 hash"
        );
        if (_implementer != address(0) && _implementer != msg.sender) {
            require(
                ERC1820ImplementerInterface(_implementer)
                    .canImplementInterfaceForAddress(_interfaceHash, addr) ==
                    ERC1820_ACCEPT_MAGIC,
                "Does not implement the interface"
            );
        }
        interfaces[addr][_interfaceHash] = _implementer;
        emit InterfaceImplementerSet(addr, _interfaceHash, _implementer);
    }

The setInterfaceImplementer starts with defining the target address, it checks if the target address has been passed, if not then the msg.sender becomes the target. It only proceeds if the caller is a manager for the given address.

Now comes an important part where the function makes sure that this is not an ERC165 Interface ID, and reverts otherwise.

This is done by checking if the given interface ID ends with 28 zeroes. The reason for this is the fact that an ERC165 interface is a 4-byte value while the ERC1820 interface ID is a 32-byte value.

Learn more about the way we calculate ERC165 interface IDs here.

On the other hand ERC1820 interface ID is just the keccak256 hash of the interface name.

    function interfaceHash(string calldata _interfaceName)
        external
        pure
        returns (bytes32)
    {
        return keccak256(abi.encodePacked(_interfaceName));
    }

Now that we know the difference between both the interface IDs, we can look at the function

function isERC165Interface(bytes32 _interfaceHash)
        internal
        pure
        returns (bool)
    {
        return
            _interfaceHash &
                0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF ==
            0;
    }

This function will return true if the passed interface ID ends with 28 zeroes. Indicating that the interface ID is an ERC165 compatible.

If the given ID is an ERC165 ID the function reverts, otherwise it checks if the implementer’s address is a zero address or if the msg.sender itself is the implementer, if both of these conditions are false, it has a require statement to check whether or not the implementer’s address has implemented the given interface ID.

It does so by calling the function canImplementInterfaceForAddress() to the given implementer’s address. This function should return the ERC1820_ACCEPT_MAGIC if it implements the interface.

    function canImplementInterfaceForAddress(bytes32 interfaceHash, address addr) external view returns(bytes32);

Once this check is passed, the interfaces mapping is updated with the target address, interface ID, and the implementer address. And finally, we emit the event InterfaceImplementerSet.

Now let’s look at the getInterfaceImplementer function.

function getInterfaceImplementer(address _addr, bytes32 _interfaceHash)
        external
        view
        returns (address)
    {
        address addr = _addr == address(0) ? msg.sender : _addr;
        if (isERC165Interface(_interfaceHash)) {
            bytes4 erc165InterfaceHash = bytes4(_interfaceHash);
            return
                implementsERC165Interface(addr, erc165InterfaceHash)
                    ? addr
                    : address(0);
        }
        return interfaces[addr][_interfaceHash];
    }

It also starts with the same logic, as the setter function. The difference comes in when the passed interface ID is an ERC165 ID. Instead of reverting it calls the implementsERC165Interface function, which we’ll talk about in a bit. In the opposite case, it just returns the value from interfaces mapping, and that’s how the caller confirms whether the interface is implemented or not by that address.

Now let’s move a step back and see what happens when the interface ID is an ERC165 ID.

To start with, first, we’ll list all the related functions for ERC165.

function updateERC165Cache(address _contract, bytes4 _interfaceId)
        external
    {
        interfaces[_contract][_interfaceId] = implementsERC165InterfaceNoCache(
            _contract,
            _interfaceId
        )
            ? _contract
            : address(0);
        erc165Cached[_contract][_interfaceId] = true;
    }

    function implementsERC165Interface(address _contract, bytes4 _interfaceId)
        public
        view
        returns (bool)
    {
        if (!erc165Cached[_contract][_interfaceId]) {
            return implementsERC165InterfaceNoCache(_contract, _interfaceId);
        }
        return interfaces[_contract][_interfaceId] == _contract;
    }

    function implementsERC165InterfaceNoCache(
        address _contract,
        bytes4 _interfaceId
    ) public view returns (bool) {
        uint256 success;
        uint256 result;

        (success, result) = noThrowCall(_contract, ERC165ID);
        if (success == 0 || result == 0) {
            return false;
        }

        (success, result) = noThrowCall(_contract, INVALID_ID);
        if (success == 0 || result != 0) {
            return false;
        }

        (success, result) = noThrowCall(_contract, _interfaceId);
        if (success == 1 && result == 1) {
            return true;
        }
        return false;
    }

The first function, updateERC165Cache is used to directly record the interface ID into the registry using the erc165Cached mapping. We call it cache.

The cache is nothing but just a term we use to describe that an ERC165 ID is being stored directly in the storage of the registry contract so that the registry wouldn’t need to call the target contract to query the same.

The function implementsERC165Interface is used to check whether the given address has implemented the given interface or not.

It first checks if the ID is cached in the mapping, if it is then the target address stored in the erc165Cached mapping simply gets returned, if not then it calls the implementsERC165InterfaceNoCache.

implementsERC165InterfaceNoCache function calls the given address directly, the same way ERC165 works in general. It returns either true or false based on the data received.

This is how ERC1820 is backward compatible with ERC165.

Find the complete contract here.

Now that you know the logic of this contract, you might be wondering how can we have a single contract on every chain. Who will deploy that contract? And how do we all agree on the fact that which contract will be considered to be the universal registry contract on the respective Blockchain?

Things get interesting now as we get to know about the deployment process of ERC1820.

To understand this part you should have a bit of knowledge about the ECDSA signing mechanism used in Ethereum Blockchain. Not going into much detail, I’ll provide some gist of it.

A transaction consists of these parameters Nonce, Gas price, Gas limit, Recipient, Value, Data, v, r, and s. In a normal scenario, we sign a transaction with our private keys, and get the v, r, s parameters, and send all of it to the network, where v,r, and s are used to verify that the transaction is signed by the correct private key.

But in the case of keyless deployment, we don’t sign a transaction, instead, we provide arbitrary values for v, r, and s. This means we don’t have any private key and we just generated a signature without actually knowing the private key. We will never know the private key, as r, s, v cannot be reverted to get the private key.

For a better understanding, let’s look at the actual transaction object created for the deployment of ERC1820.

const rawTransaction = {
  nonce: 0,
  gasPrice: 100000000000,
  value: 0,
  data: '0x' + contracts.ERC1820Registry.ERC1820Registry.bin,
  gasLimit: 800000,
  v: 27,
  r: '0x1820182018201820182018201820182018201820182018201820182018201820',
  s: '0x1820182018201820182018201820182018201820182018201820182018201820'
};

You can find it here.

As we see in this transaction, the values of v, r, and s are explicitly written by a human.

Now with these random inputs, we can fetch the address, that’s going to send this tx. Remember we can’t get the private key from these data.

The next step is to fund the recovered address so that it can pay for the transaction fees.

Then this transaction is sent to the blockchain. This transaction will deploy the contract and no one has the private key for the EOA used to deploy it.

We can do this on all EVM chains, and get the registry contract deployed on the same address across all chains which is 0x1820a4b7618bde71dce8cdc73aab6c95905fad24

Now you know everything you need to know about ERC1820. Join us if you have any questions or discussion points, here.