ERC2771Context: TypeError: Immutable variables cannot be read during contract creation time

Hi,

I’m trying to integrate Meta Transactions and when I inherit a contract with ERC2771Context I get the following error during compile time:

Compiling 24 files with 0.8.0
TypeError: Immutable variables cannot be read during contract creation time, which means they cannot be read in the constructor or any function or modifier called from it.
  --> @openzeppelin/contracts/metatx/ERC2771Context.sol:18:29:
   |
18 |         return forwarder == _trustedForwarder;
   |                             ^^^^^^^^^^^^^^^^^

:computer: Environment

HardHat: v2.3.0
@openzeppelin/contracts: v4.1.0

:memo:Details

:1234: Code to reproduce

BadgeRoles.sol

import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
import "@openzeppelin/contracts/metatx/MinimalForwarder.sol";

contract BadgeRoles is AccessControlEnumerable, Pausable, ERC2771Context {
    /// @dev Roles
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant TEMPLATER_ROLE = keccak256("TEMPLATER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    constructor(MinimalForwarder forwarder) ERC2771Context(address(forwarder)) {
        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());

        _setupRole(ADMIN_ROLE, _msgSender());
        _setupRole(TEMPLATER_ROLE, _msgSender());
        _setupRole(PAUSER_ROLE, _msgSender());
    }

MakerBadges.sol

import "./BadgeRoles.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract MakerBadges is BadgeRoles, ERC721Enumerable {
    /// @dev Libraries
    using Counters for Counters.Counter;
    using MerkleProof for bytes32[];

    Counters.Counter private _templateIdTracker;

    string public baseTokenURI;

    bytes32[] private roots;

    struct BadgeTemplate {
        string name;
        string description;
        string image;
    }

    mapping(uint256 => BadgeTemplate) public templates;
    mapping(uint256 => uint256) public templateQuantities;

    /// @dev Events
    event NewTemplate(uint256 indexed templateId, string name, string description, string image);
    event TemplateUpdated(uint256 indexed templateId, string name, string description, string image);
    event BadgeActivated(uint256 indexed tokenId, uint256 indexed templateId);

    constructor(MinimalForwarder forwarder) ERC721("MakerBadges", "MAKER") BadgeRoles(forwarder) {
        baseTokenURI = "https://badges.makerdao.com/token/";
    }

Tried different approaches but I get the same error.

Source Code: https://github.com/naszam/maker-badges/pull/3

1 Like

Unfortunately immutable state variables cannot be read at construction time. This is a limitation of the compiler (though there are plans to make it possible in the future). See https://github.com/ethereum/solidity/issues/10463 if you’re interested in the details.

2 Likes

Hi @cameel,

Thanks for your response!

I’m wondering @Amxx if OZ plans to remove the immutable keyword in the meantime to allow the community to experiment with meta txs and build on top of it or if you’d recommend any workaround to use it in the meantime.

Thanks!

@naszam This error only happens if you try to read the forwarder, from its “immutable slot”, during construction. The error comes from the fact that you are using _msgSender() in your constructor. You should either keep using msg.sender in the constructor (I don’t think meta-tx construction is a thing) … or pass the admin as a constructor argument and use that to setup your roles.

Immutable variable offer significant gas saving (particularly since the Berlin upgrade). So far the drawback as always been minimal and avoidable compared to the benefits.

2 Likes

Hi @Amxx,

Thanks for your response!

I’ve tried using the msg.sender in the constructor but I still have the forwarder issue.
Do you have any plans to release a version without immutable keyword?
Also I’ve noticed that is not specified any visibility for the _trustedForwarder.
Thanks!

1 Like

I’ve run into the same problem.

I’m using AccessControl in the constructor, which in turn calls _msgSender. The constructor is the correct place to do this access control, as the contract shouldnt exist without the correct access control. Doing this with an initialise function should also be avoided if possible.

It basically means that constructor use of AccessControl is not compatible with inheriting from ERC2771. Can the library be improved to allow for this compatiblity? I expect many people will be using AccessControl in the constructor.

Same thing with Ownable

ERC2771Context no longer uses an immutable variable in the latest release (4.3), so you will no longer run into this problem.

That said, I was happy to see today that reading of immutable variables during construction was merged in Solidity, so starting with the next Solidity release this problem should no longer arise.

1 Like

The immutable keyword seems to be back in the latest version (4.5), and I also have the same problem now (inheriting from Ownable, and ERC2771Context. Ownable calls _msgSender() in the constructor). I'm also using solidity 0.8.9. What is the recommended workaround?

@EgyptianCactus Your contract needs to inherit ERC2771Context first to ensure the immutable variable is initialized before it's read.

So you should write contract C is ERC2771Context, Ownable instead of contract C is Ownable, ERC2771Context.

4 Likes

That makes sense. Thank you so much!

But now it's back.

It's better to remove immutable because like everything else in life, something or someone that was once trusted may one day no longer be trustworthy.

This is definitely true about humans, but I wouldn't apply the same reasoning to code (contracts) so lightly. It is true that a contract could have a bug discovered and lose its trustworthiness, but if you want the ability to replace it if that happens you are forced to introduce humans in the loop to make that decision, which I think is a strictly worse situation in terms of trust assumptions.

But it wouldn't just be anyone who could change the address of trusted forwarder in your contract... it'd be the admin of your contract (which could be yourself) that would do it when it's required. Contracts that use AccessControl would have an account that has DEFAULT_ADMIN_ROLE to maintain it. Updating runtime variables when required is the job of that trusted admin. _trustedForwarder, which is the address of an external entity, is one of those variables that should be updateable if it's ever required.