UUPS Proxies: Tutorial (Solidity + JavaScript)

await upgrades.upgradeProxy(myTokenV1.address, MyTokenV2)

Adding this to myToken test gives me an Ownable error. I pasted the final code into Remix and the owner function returns the 0x0 address.

Error: VM Exception while processing transaction: reverted with reason string 'Ownable: caller is not the owner'

After spending way too much time on this, I finally figured it out. The solidity code in the tutorial is missing the __Ownable_init(); function call in its initializer. Gonna leave my passing tests here for posterity.

// test/MyToken.test.ts

import { expect } from 'chai'
import { ethers, upgrades } from 'hardhat'
import { MyTokenV1 } from '../typechain/MyTokenV1'
import { MyTokenV2 } from '../typechain/MyTokenV2'

let myTokenV1: MyTokenV1
let myTokenV2: MyTokenV2

describe('MyToken', function () {
  it('deploys', async function () {
    const MyTokenV1 = await ethers.getContractFactory('MyTokenV1')
    myTokenV1 = (await upgrades.deployProxy(MyTokenV1, { kind: 'uups' })) as MyTokenV1
  })
  it('upgrades', async function () {
    const MyTokenV2 = await ethers.getContractFactory('MyTokenV2')
    myTokenV2 = (await upgrades.upgradeProxy(myTokenV1, MyTokenV2)) as MyTokenV2
  })
  it('adds with new function and variable', async function () {
    await myTokenV2.add(21)
    expect((await myTokenV2.num()).toString()).to.equal('21')
    await myTokenV2.add(21)
    expect((await myTokenV2.num()).toString()).to.equal('42')
  })
})
// contracts/MyTokenV2.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol';
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract MyTokenV2 is Initializable, ERC20Upgradeable, UUPSUpgradeable, OwnableUpgradeable {
    uint public num;

    function initialize() public initializer {
        __ERC20_init('MyToken', 'MTK');

        __Ownable_init();

        _mint(msg.sender, 1000 * 10**decimals());
    }

    function _authorizeUpgrade(address) internal override onlyOwner {}

    function add(uint _num) public {
      num += _num;
    }
}
2 Likes

My bad, sorry about that! Thank you for debugging and finding out the cause. I've updated the code in the original post.

Thank you for updating the code. I noticed that the OpenZeppelin Contracts Wizard uses __UUPSUpgradeable_init(); in the initializer function. What does that function do? Should it be used? The tutorial also says that a constructor should not be used, but the code was updated to include one to address the UUPSUpgradeable vulnerability.

__UUPSUpgradeable_init is an empty function actually. The Wizard includes it because it follows our general recommendation which is to include all inits for all parents, so as not to require checking whether a function is empty or not, and to future-proof for the possibility that we add initialization logic at some point. So I've updated the code to reflect this recommendation.

I've also improved the part about the constructor to make things more clear.

Thanks for the feedback!

1 Like

A post was split to a new topic: How to use the variable __gep correctly in the proxy contract

When upgrading from version 2 to version 3, I notice in the video that you upgrade from mars to marsv3 by calling

const marsv3 = await hre.upgrades.upgradeProxy(mars, this.Marsv3);

I assume we update the deployed proxy and not the updated, so if we were on version 10 of the proxy, we would still upgrade this existing mars that was deployed.

Cheers

Yes that's right. The proxy always stays at the same address and that's the contract that you deploy.

1 Like

Yup, managed to upgrade my contracts via the UUPS pattern. This makes sense as the proxy uses delegateCall on the implementation. Also verified the implementation contracts on Etherscan and have the Proxy point to the implementation. Great work guys!

2 Likes

Regarding killing the upgradeability:
Can I kill it with only one upgrade by implementing an impossible check in the _authorizeUpgrade method?
For example if I upgrade to a new implementation which contains this function:

function _authorizeUpgrade(address) internal override {
    require(false, "no more upgrades allowed");
}

Or does the upgrade then fail, because of the security checks in the
function _upgradeToAndCallSecure
and I should just override the UUPS functions to directly call the upgradeToAndCall() if I want to allow the upgrade to a non upgradeable implementation?
Keep up the good work! Loving your videos as well.

1 Like

Indeed that upgrade will fail. Overriding to use the unsecured version would allow you to remove the entire upgradeability through two upgrades.

By the way, we're simplifying UUPS by removing the rollback test in the next version of OpenZeppelin Contracts, so removing upgradeability will be easier.

2 Likes

Hello, first, thanks for this great tutorial, nicely detailed :slightly_smiling_face:
I was thinking about the following :
Once the Factory and the Core are deployed, does the Core is linked to the Factory ?
Can someone else create a Factory and attach to the Core contract ?
If yes, how to prevent that ?
Thanks for your precisions.

@Coineo What do you mean by Factory and Core?

@frangio Hello, thanks for your reply.
I mean the Core contract is where the logic is stored, while the "factory" is where the data are stored.

The term we use for what you call "factory" is "proxy", and for "core" we use "implementation".

There is nothing you can do to prevent someone from attaching a proxy to your implementation contract (short of some legal licensing strategy). But it also shouldn't cause any issues if someone does this.

1 Like

Hi @frangio please I need help verifying my proxy contract on cronoscan https://cronoscan.com/address/0x59cd2E492FF59dE3D99C0E034E85c7E51d420643 .

@nonseodion Have you see the section about proxies in our verification guide?

There are instructions.

Thanks for the help I just saw it, I am having an issue with the ABI encoded arguments for the constructor. Cronoscan says " Error! Invalid constructor arguments provided. Please verify that they are in ABI-encoded format"

One thing that's really confusing is where the ERC1967 admin is used in all of this. Is the idea that it can optionally be used in _authorizeUpgrade? Seems like there's no benefit to binding this storage space to the proxy itself, though.

ERC1967 admin is not used in the UUPS pattern. It could be used but it's not needed in the same way as for Transparent Proxies.