Not all upgradeable contracts are following the initializer convention?

For example the ERC2771ContextUpgradeable contract inherits initializable but does not have an initializer. Instead, it has a traditional constructor function defined. Is this just because the team is still working on porting all the contracts to the upgradeable version, or is there something special about ERC2771Context?

If we want a contract to inherit ERC2771Context and also be upgradeable, what would be the recommended way to do this?

Hello @EgyptianCactus

All our upgradeable contracts inherit initializable regardless of whether or not they have an initializer. This is to avoid any linearisation issues.

ERC2771ContextUpgradeable is a very specific contract. Our position is that, while ERC2771 could be used by upgradeable contract, we feel its very likely that multiple proxies sharing the same implementation will also be sharing the same forwarder.

This means we can "hardcode" the forwarder directly in the implementation using immutable storage. This will save on gas cost when initializing the proxy, and also reduce gas costs when querying _msgSender() (which is a very frequent operation).

The workflow is to set the forwarder when deploying the implementation (through the constructor arguments) and then initialize the rest of the logic at the proxy level.

If for any reason you want to set different forwarders for different proxies ... then you should not be using the provided ERC2771ContextUpgradeable and instead write your own version (this should be really simple). Not that it would make all operations more expensive.

1 Like

I'm running into errors when trying to follow this setup.

In the contract that inherits ERC2771ContextUpgradeable, if I understood you correctly, I should have a constructor function like so:

constructor(address trustedForwarder) ERC2771ContextUpgradeable(trustedForwarder) {}

As well as an initializer function:

function initialize() initializer public {}

But when I call upgrades.deployProxy, how can I pass the trustedForwarder to the constructor? I think it's only passing the argument I gave to the initializer but not the constructor:
await upgrades.deployProxy(MyContract, [forwarder.address], { kind: 'uups' });

Edit: Solved my own problem: need to add some additional fields to the last argument:

    constructorArgs: [forwarder.address], 
    unsafeAllow: ["constructor"]

Hello @Amxx but what if I have deployed a first version (V1) contract as @EgyptianCactus mentions and then I try to upgrade the implementation creating a V2 contract that inherits V1? In that case I'm forced to use parent's constructor that tries to modify the immutable _trustedForwarder and that will fail.