Manually deploying and testing Proxy contracts

I am trying to set up an upgradeable token contract with OpenZeppelin upgrade pattern and Proxy contract. I am setting up the test environment by hand in Jest and TypeScript, as testing needs to be later integrated other packages.

I have a hard time to figure out how to set up Proxy properly, without using OpenZeppelin command-line tools. There is good documentation for the command line tools, but my needs are in my own test environment and then manually making contract deploymens and calls from a multisig wallet. I could not find any good examples of this kind of programmatic approach and tried to reverse engineer how it is supposed to work myself.

I managed to get the Proxy itself to work, as admin() an implementation() return good values. However, the actual contract reads like proxied totalSupply() calls return zero.

  1. Am I setting up the proxy somewhat wrong way? Is delegated variable space somehow messed up?

and/or

  1. Do I need to take some special considerations when testing the proxy contract and calls, or should it “just work” if I point a OpenZeppelin test enviroment crafted contract to a proxy contract address?

Below is my code and it fails on the last assert.

/**
 * Test ugprade proxy functionality and proxy transparency.
 */

import assert = require('assert');

import { accounts, contract } from '@openzeppelin/test-environment';
import { Proxy } from '@openzeppelin/upgrades';
import { ZWeb3 } from '@openzeppelin/upgrades';

import {
  BN,           // Big Number support
} from '@openzeppelin/test-helpers';

// https://etherscan.io/address/0xaf30d2a7e90d7dc361c8c4585e9bb7d2f6f15bc7#readContract
const TOKEN_1ST_TOTAL_SUPPLY = new BN('93468683899196345527500000');

// Ethereum accounts used in these tests
const [
  deployer,  // Deploys the smart contract
  owner, // Token owner - an imaginary multisig wallet
  proxyOwner, // Who owns the proxy contract - an imaginary multisig wallet
  user2 // Random dude who wants play with tokens
] = accounts;

// Loads a compiled contract using OpenZeppelin test-environment
const DawnTokenImpl = contract.fromArtifact('DawnTokenImpl');   // ERC20Pausable subclass
const DawnTokenProxy = contract.fromArtifact('DawnTokenProxy');  // AdminUpgradeabilityProxy subclass

let tokenImpl = null;  // ERC20Pausable
let token = null;  // Proxied ERC20Pausable
let proxyContract = null;  // DawnTokenProxy depoyment, AdminUpgradeabilityProxy
let proxy: Proxy = null;  // Zeppelin Proxy helper class

beforeEach(async () => {

  // Fix global usage of ZWeb3.provider in Proxy.admin() call
  // https://github.com/OpenZeppelin/openzeppelin-sdk/issues/1504
  ZWeb3.initialize(DawnTokenImpl.web3.currentProvider);

  // Here we refer the token contract directly without going through the proxy
  tokenImpl = await DawnTokenImpl.new(owner, { from: deployer });

  // Copied from
  // https://github.com/OpenZeppelin/openzeppelin-sdk/blob/master/packages/lib/test/contracts/upgradeability/AdminUpgradeabilityProxy.test.js
  const initializeData = Buffer.from('');
  proxyContract = await DawnTokenProxy.new(tokenImpl.address, proxyOwner, initializeData, { from: deployer });

  assert(proxyContract.address != null);

  // Route all token calls to go through the proxy contract
  token = await DawnTokenImpl.at(proxyContract.address);

  // We need this special Proxy helper class,
  // because Proxy smart contract is very special and we can't
  // e.g. refer to Proxy.admin() directly
  proxy = new Proxy(proxyContract.address);

  await tokenImpl.initialize(deployer, owner);
});

test('Proxy owner should be initially proxy multisig', async () => {
  assert(await proxy.admin() == proxyOwner);
});

test('Proxy should point to the first implementation ', async () => {
  assert(await proxy.implementation() == tokenImpl.address);
});

test('Proxy supply should match the original token', async () => {

  const tokenImplSupply = await tokenImpl.totalSupply();
  // Big number does not have power-assert support yet - https://github.com/power-assert-js/power-assert/issues/124
  assert(tokenImplSupply.toString() == TOKEN_1ST_TOTAL_SUPPLY.toString());

  const supply = await token.totalSupply();

  // TODO: WHEN READING SUPPLY THROUGH PROXY IT IS ZERO
  assert(supply.toString() == TOKEN_1ST_TOTAL_SUPPLY.toString());

});

Hey @miohtama! The issue is that you are calling initialize on the implementation contract, not on the proxy. Keep in mind that state is kept in the proxy, and since initialization is setting the token total supply, it needs to be called in the proxy.

In other words, you need to change:

- await tokenImpl.initialize(deployer, owner);
+ await token.initialize(deployer, owner);

Hope this helps! Also, make sure to take a look at @openzeppelin/upgrades usage, which guides you to programatically create a proxy in a simpler way.

2 Likes

Hi Santiago,

Thank for professional feedback. Of course it was this - when writing tests I had a brain fart and in my head mixed around in which contract the data is stored. Thank you for lending out a precise eye!

For anyone who reads this after the years, please find the corrected code below.

I do not prefer using OpenZeppelin JS SDK, as it adds too many layers of indirection and abstraction over the contracts, making very difficult to understand what’s going under the hood. Truffle suffers from this even more. Thus, try to work directly with Solidity and web3 objects whenever it is possible.

/**
 * Test ugprade proxy functionality and proxy transparency.
 */

import assert = require('assert');

import { accounts, contract } from '@openzeppelin/test-environment';
import { Proxy } from '@openzeppelin/upgrades';
import { ZWeb3 } from '@openzeppelin/upgrades';

import {
  BN,           // Big Number support
} from '@openzeppelin/test-helpers';

// https://etherscan.io/address/0xaf30d2a7e90d7dc361c8c4585e9bb7d2f6f15bc7#readContract
const TOKEN_1ST_TOTAL_SUPPLY = new BN('93468683899196345527500000');

// Ethereum accounts used in these tests
const [
  deployer,  // Deploys the smart contract
  owner, // Token owner - an imaginary multisig wallet
  proxyOwner, // Who owns the proxy contract - an imaginary multisig wallet
  user2 // Random dude who wants play with tokens
] = accounts;

// Loads a compiled contract using OpenZeppelin test-environment
const DawnTokenImpl = contract.fromArtifact('DawnTokenImpl');   // ERC20Pausable subclass
const DawnTokenProxy = contract.fromArtifact('DawnTokenProxy');  // AdminUpgradeabilityProxy subclass

let tokenImpl = null;  // ERC20Pausable
let token = null;  // Proxied ERC20Pausable
let proxyContract = null;  // DawnTokenProxy depoyment, AdminUpgradeabilityProxy
let proxy: Proxy = null;  // Zeppelin Proxy helper class

beforeEach(async () => {

  // Fix global usage of ZWeb3.provider in Proxy.admin() call
  // https://github.com/OpenZeppelin/openzeppelin-sdk/issues/1504
  ZWeb3.initialize(DawnTokenImpl.web3.currentProvider);

  // This is the first implementation contract - v1 for the smart contarct code.
  // Here we refer the token contract directly without going through the proxy.
  tokenImpl = await DawnTokenImpl.new(owner, { from: deployer });

  // Proxy contract will
  // 1. Store all data, current implementation and future implementations
  // 2. Have a mechanism for proxy owner to change the implementation pointer to a new smart contract
  //
  // Note that this means that you can never call tokenImpl contract directly - because if you call it directly
  // all the memory (data) is missing as it is hold on the proxy contract
  //
  // Copied from
  // https://github.com/OpenZeppelin/openzeppelin-sdk/blob/master/packages/lib/test/contracts/upgradeability/AdminUpgradeabilityProxy.test.js
  const initializeData = Buffer.from('');
  proxyContract = await DawnTokenProxy.new(tokenImpl.address, proxyOwner, initializeData, { from: deployer });

  assert(proxyContract.address != null);

  // Route all token calls to go through the proxy contract
  token = await DawnTokenImpl.at(proxyContract.address);

  // We need this special Proxy helper class,
  // because Proxy smart contract is very special and we can't
  // e.g. refer to Proxy.admin() directly
  proxy = new Proxy(proxyContract.address);

  // This is the constructor in OpenZeppelin upgradeable pattern
  await token.initialize(deployer, owner);
});

test('Proxy owner should be initially proxy multisig', async () => {
  assert(await proxy.admin() == proxyOwner);
});

test('Proxy should point to the first implementation ', async () => {
  assert(await proxy.implementation() == tokenImpl.address);
});

test('Proxy supply should match the original token', async () => {
  const supply = await token.totalSupply();

  // We get a different supply than above
  assert(supply.toString() == TOKEN_1ST_TOTAL_SUPPLY.toString());

});

test("Token should allow transfer", async () => {
  const amount = new BN("1") * new BN("1e18");  // Transfer 1 whole token
  await token.transfer(user2, amount, { from: owner });
  const balanceAfter = await token.balanceOf(user2);
  assert(balanceAfter.toString() == amount.toString());
});

test("Token tranfers are disabled after pause", async () => {
  const amount = new BN("1") * new BN("1e18");  // Transfer 1 whole token
  // Pause
  await token.pause({ from: owner });
  assert(await token.paused());
  // Transfer tokens fails after the pause
  assert.rejects(async () => {
    await token.transfer(user2, amount, { from: owner });
  });
});

test("Proxy should allow upgrade", async () => {
  const amount = new BN("1") * new BN("1e18");  // Transfer 1 whole token
  await token.transfer(user2, amount, { from: owner });
  const balanceAfter = await token.balanceOf(user2);
  assert(balanceAfter.toString() == amount.toString());
});
2 Likes

This is good feedback, thanks! We will probably be reviewing the upgrades JS SDK interface in the mid-term, especially around removing global state and other bad practices that have accumulated over time, to make it easier to use.

1 Like

If you have feedback my favorite SDK has been web3.py and now dead Populus.

With Populus, we tried to make sure that for every task one does, there is a command line or programmatic (in this case Python script) way to describe how to accomplish the goal.

This ensures that the people who are building their own tooling, e.g. with server-side backends or now web browser frontends, can also reuse the code. For example, you do not want to pull in some code that is CLI specific when you are dealing with things like SQL models - a common use case when your integrating Ethereum to centralised systems.

2 Likes