Initializing admin in the constructor of a UUPS proxy contract

From what I understand, for UUPS proxies: the proxy contract is ERC1967Proxy.sol from OpenZeppelin Contracts (why doesn't OpenZeppelin Contracts-Upgradeable have this proxy contract?) and the implementation contract should inherit from UUPSUpgradeable.sol in OpenZeppelin Contracts-Upgradeable

But it seems like a malicious actor could easily initialize the the proxy before the deployer/admin. I'd like to initialize the admin in the constructor of the proxy contract. It seems like I can use the admin slot: 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 and then permission the initialize function to the admin — would this be safe? Why isn't it set this way by default?

I'm creating a factory for upgradeable smart contracts, which is why I need to work with ERC1967Proxy.sol directly as opposed to the hardhat pulgin (and I'm using Foundry)

Also, if the UUPSUpgradeable.sol is meant to be used for the implementation contract, and ERC1967Proxy.sol is meant to be used in the proxy, why are there 2 __gap's in UUPSUpgradeable.sol (one in the contract itself and one in ERC1967UpgradeUpgradeable.sol, which it inherits from) but none in ERC1967Proxy.sol, where the actual storage is?

Lastly, when making e.g. an upgradeable ERC-20, why do people (including OZ staff in tutorial videos) inherit from both Initializable.sol and ERC20Upgradeable.sol? ERC20Upgradeable.sol is already initializable

EDIT: I see that you can set the admin in with an encode call in the data arg in the proxy's constructor. Still curious about the other questions though!

why doesn't OpenZeppelin Contracts-Upgradeable have this proxy contract?

ERC1967Proxy.sol is the proxy contract which is meant to be deployed as-is, so it is not part of the upgradeable library. The upgradeable library @openzeppelin/contracts-upgradeable is meant to be used in your implementation contract, which needs to follow the rules for upgradeability such as using initializers and avoiding storage layout conflicts.

But it seems like a malicious actor could easily initialize the the proxy before the deployer/admin. I'd like to initialize the admin in the constructor of the proxy contract. It seems like I can use the admin slot: 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 and then permission the initialize function to the admin — would this be safe? Why isn't it set this way by default?

The admin slot is only used for Transparent proxies, not UUPS proxies. When deploying a UUPS proxy with ERC1967Proxy.sol, you should include the _data parameter for the constructor to call the initializer function in the implementation logic, so that a malicious actor cannot initialize the proxy before you.

Also, if the UUPSUpgradeable.sol is meant to be used for the implementation contract, and ERC1967Proxy.sol is meant to be used in the proxy, why are there 2 __gap's in UUPSUpgradeable.sol (one in the contract itself and one in ERC1967UpgradeUpgradeable.sol, which it inherits from) but none in ERC1967Proxy.sol, where the actual storage is?

Storage gaps should be included in implementation contracts and in the library contracts, but not in the proxy contract itself. When you call functions in the proxy, it delegates calls to the implementation logic but any usage of storage variables actually occur in the context of the proxy.

In general, there should not be any storage variables in the proxy contract itself (ERC1967Proxy.sol), since that could lead to collisions with the implementation's storage variables.

1 Like