Hardhat-deploy: OpenZeppelin UUPS proxy support

Created on 28 Jun 2021  ·  13Comments  ·  Source: wighawag/hardhat-deploy

I see that OpenZeppelinTransparentProxy is supported. Are there any plans to add UUPS proxy support in the near future?

enhancement

Most helpful comment

Planning to support natively but for now it is still possible to use them with hardhat-deploy. the proxy jist need to have a dummy/unused owner arg in its constructor to remain compatible.

All 13 comments

Planning to support natively but for now it is still possible to use them with hardhat-deploy. the proxy jist need to have a dummy/unused owner arg in its constructor to remain compatible.

the proxy jist need to have a dummy/unused owner arg in its constructor to remain compatible.

Would that not make using UUPS no longer beneficial? The reason to use UUPS is to save gas on transaction calls because the proxy doesn't need to check if it is the admin making the call.

Maybe I'm missing it, could you elaborate with some pseudo-code how to use hardhat-deploy with uups proxies (with the unused argument)?

Thanks

It will not affect gas, it is just a dummy arg so that hardhat-deploy can deploy it without change of code. the constructor does not need to do anything with that argument.
Going to see if I can make the change to make it work without any change soon. it should be a small change where you can specify the constructor arg for the proxy or maybe simply specifiy that it is a UUPS proxy

Thanks @wighawag, got around to this finally.

For those interested I created this contract:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

// Kept for backwards compatibility with older versions of Hardhat and Truffle plugins.
contract UUPSProxy is ERC1967Proxy {
  constructor(
    address _logic,
    address, // This is completely unused by the uups proxy, required to remain compatible with hardhat deploy: https://github.com/wighawag/hardhat-deploy/issues/146
    bytes memory _data
  ) payable ERC1967Proxy(_logic, _data) {}
}

And then my deployments looks something like this (proxyContract: "UUPSProxy" being the important part):

  await deploy(CONTRACT, {
    from: deployer,
    proxy: {
      proxyContract: "UUPSProxy",
      // ... your other config, initializers etc
    },
    log: true,
  });

@JasoonS Please can you clarify the purpose of that UUPSProxy contract code? Is it just a dummy contract so that hardhat-deploy knows the interface of the actual proxy's constructor? Or does it actually relate somehow to the proxy contract which gets deployed?

What exactly is the role of admin, I get an error no matter if's there, the deployer (=upgrade admin) or if I omit the field.

proxy: {
      proxyContract: "UUPSProxy",
      execute: {
        methodName: "initialize",
        args: [admin],
      },
    },

@marceljay Please (always) paste the error(s) you get :-)

Hi @aspiers , I haven't dug into hardhat-deploy deeply so please correct me @wighawag. But from what I understand it knows how to deploy transparent proxies. And transparent proxies are deployed with an admin that manages those proxies. In UUPS contracts you don't have an external contract (proxy admin) managing the upgrades, it is rather in the implementation itself - so isn't passed into the proxy constructor.

So to make this work with hardhat-deploy we deploy it with the same constructor as a transparent proxy, but we just ignore the admin field and get the same effect.

Seems to be working for me @marceljay

Disclaimer my code snippet isn't audited :sweat_smile: - I take no responsibility for badly configured proxies :laughing:

@marceljay Ah, yes, I can guess your issue. I edited my original response.

The following code is what to do on the implementation. So I have a function on the implementation (not the proxy) called initialize that takes in 1 argument (the admin).

execute: {
  methodName: "initialize",
  args: [admin],
},

@marceljay Ah, yes, I can guess your issue. I edited my original response.

The following code is what to do on the implementation. So I have a function on the implementation (not the proxy) called initialize that takes in 1 argument (the admin).

execute: {
  methodName: "initialize",
  args: [admin],
},

Yeah now it makes perfect sense, saw you edited the answer :)

Do you know if the ERC1967Proxy contract is the same that's used by OZ upgrades plugin?

@JasoonS Thanks a lot for the explanations but I still don't fully get it. Above you posted a contract starting:

contract UUPSProxy is ERC1967Proxy {

Does that actually get deployed for real by the sample await deploy code you posted above, or is it just a dummy contract so that hardhat-deploy knows the interface of the actual proxy's constructor, or something else? You wrote:

we deploy it with the same constructor as a transparent proxy

but if it's really deploying that then I don't see how it could possibly work, because that contract inherits from ERC1967Proxy not UUPSUpgradeable, so where would the actual UUPS functionality come from?

Does that actually get deployed for real by the sample await deploy code you posted above

Yes, it actually deploys it.

because that contract inherits from ERC1967Proxy not UUPSUpgradeable

It is supposed to use the ERC1967Proxy (both UUPS and transparent proxies from openzeppelin use erc1967, https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups). The UUPSUpgradeable contract is used by the implementation contract (it isn't part of the proxy).

so where would the actual UUPS functionality come from?

By design the implementation contract holds the UUPS functionality, (which is good because it is more gas efficient since the logic isn't in the proxy itself, but bad because it is possible to upgrade to a non-compatible contract by mistake and break upgradeability).

Also, I'm definitely not an expert on proxies, this is just from my own research.

@JasoonS commented on September 1, 2021 9:18 PM:

because that contract inherits from ERC1967Proxy not UUPSUpgradeable

It is supposed to use the ERC1967Proxy (both UUPS and transparent proxies from openzeppelin use erc1967, docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups).

Right. And ERC-1967 doesn't do much except specify the storage location of the implementation address, beacon address, and admin address. BTW fun fact I noticed: in conforming to ERC-1967, the OZ UUPS implementation actually violates EIP1822 which specifies a slightly different storage location for the implementation address.

The UUPSUpgradeable contract is used by the implementation contract (it isn't part of the proxy).

D'oh, of course! Thanks for helping me see what I was stupidly missing here.

so where would the actual UUPS functionality come from?

By design the implementation contract holds the UUPS functionality, (which is good because it is more gas efficient since the logic isn't in the proxy itself, but bad because it is possible to upgrade to a non-compatible contract by mistake and break upgradeability).

Yep, so the implementation has to inherit from UUPSUpgradeable.

Thanks a lot again for this great help!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lcswillems picture lcswillems  ·  14Comments

tennox picture tennox  ·  4Comments

smartcontracts picture smartcontracts  ·  20Comments

lepidotteri picture lepidotteri  ·  5Comments

jsidorenko picture jsidorenko  ·  3Comments