Proper Truffle integration

Hi guys !

I love the work you’re doing and really want to use the openzeppelin sdk in my project, but I ran into some issues when trying to integrate your tool with truffle.

My goal is to use the SDK without command-line, only from programmatic libraries and directly from my migrations. Migrations are very useful when using upgradeable smart contract ! Currently, I saw two options to do so:

  1. By using the @openzeppelin/cli, like shown here
  2. By using the @openzeppelin/upgrades, like documented here

But in both cases, I ran into some issues.

First, the cli solution works properly when migrating to live networks, but not so well when working on the test or dry-run network. When running truffle test, truffle raises a ganache instance and creates the test network. This means that inside my migration, the network name I receive will always be test, even if I never configured such network inside my truffle-config. In this case, the deployment crashes because it could not find a network named test inside the truffle-config.

A possible workaround is to deploy the contracts in the test suite, and make a condition to ignore migrations if network name is test.

When using the upgrades library, it will always try to recover contract artifacts from the filesystem. But once again, when running truffle test, there is no build directory generated. All the artifacts provided by the truffle-artifactor have been injected by truffle, and are not retrieved from filesystem. But @openzeppelin/upgrades cannot work without filesystem artifacts.

// Required by @openzeppelin/upgrades when running from truffle
global.artifacts = artifacts;
global.web3 = web3;

// Import dependencies from OpenZeppelin SDK programmatic library
const { Contracts, SimpleProject, ZWeb3 } = require('@openzeppelin/upgrades');
const fs = require('fs-extra');

module.exports = async function(deployer) {

    ZWeb3.initialize(web3.currentProvider);

    const extra_build_dir = `${__dirname}/../zos_build/contracts`;

    if (fs.existsSync(extra_build_dir)) {
        fs.removeSync(extra_build_dir)
    }

    fs.ensureDirSync(extra_build_dir);
    fs.writeFileSync(`${extra_build_dir}/PlaceHolder_v0.json`, JSON.stringify(artifacts.require('PlaceHolder_v0')));
    fs.writeFileSync(`${extra_build_dir}/PlaceHolder_v1.json`, JSON.stringify(artifacts.require('PlaceHolder_v1')));
    fs.writeFileSync(`${extra_build_dir}/Initializable.json`, JSON.stringify(artifacts.require('Initializable')));

    Contracts.setLocalBuildDir(extra_build_dir);

    const PlaceHolder_v0 = Contracts.getFromLocal('PlaceHolder_v0');
    const PlaceHolder_v1 = Contracts.getFromLocal('PlaceHolder_v1');

    fs.removeSync(extra_build_dir);

    /* Retrieve a couple of addresses to interact with the contracts. */
    const [creatorAddress, initializerAddress] = await ZWeb3.accounts();

    /* Create a SimpleProject to interact with OpenZeppelin programmatically. */
    const myProject = new SimpleProject('MyProject', null, { from: creatorAddress });

    const instance = await myProject.createProxy(PlaceHolder_v0, { initArgs: [42] });
    console.log('Contract\'s storage value:', (await instance.methods.value().call({ from: initializerAddress })).toString());

    await myProject.upgradeProxy(instance.address, PlaceHolder_v1, { initMethod: 'add', initArgs: [1] });
    console.log('Contract\'s storage new value:', (await instance.methods.value().call({ from: initializerAddress })).toString());

};

My workaround in this situation (that is very nasty, and isn’t working completely) is to create a temporary directory in which we dump the artifacts provided by truffle for the sdk to read them later. Once the migration is done, I can call the contract and interact with its state, but the artifact is not upgraded and the final address is not stored.

Am I doing something wrong here ? It’s working with my first solution + workaround, but I wanted to know if I missed something. It’s pretty important to be able to run truffle test as it also works with plugins like truffle-coverage and it spares a loooooot of setup time there.

Thanks !

2 Likes

Hi @mortimr,

Would the following work for your test and deploy process?

Testing

Based on Testing upgradeable projects documentation, I can test a Sample contract using truffle test though I do need to manually compile truffle compile prior to testing to generate the artifacts.

Alternatively you could just test the logic contracts (as per this Issue Comment):

https://github.com/OpenZeppelin/openzeppelin-sdk/issues/702#issuecomment-464418760
Alternatively, you can just test your contracts as you would do with regular truffle tests, since the proxies don’t introduce any difference in their behaviour. Just remember to run initialize on your contracts after you have created them!

Sample.sol

pragma solidity ^0.5.0;

contract Sample {
  function greet() public pure returns (string memory) {
    return "A sample";
  }
}

Sample.test.js

const { TestHelper } = require('@openzeppelin/cli');
const { Contracts, ZWeb3 } = require('@openzeppelin/upgrades');

ZWeb3.initialize(web3.currentProvider);

const Sample = Contracts.getFromLocal('Sample');

require('chai').should();

contract('Sample', function () {

  beforeEach(async function () {
    this.project = await TestHelper();
  })

  it('should create a proxy', async function () {
    const proxy = await this.project.createProxy(Sample);
    const result = await proxy.methods.greet().call();
    result.should.eq('A sample');
  })
})

Test

Manually compile before running truffle test

$ truffle compile

Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/Sample.sol
> Artifacts written to /mnt/c/Users/andre/Documents/projects/forum/oztruffle/build/contracts
> Compiled successfully using:
   - solc: 0.5.8+commit.23d335f2.Emscripten.clang
$ truffle test
Using network 'test'.


Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



  Contract: Sample
    ✓ should create a proxy (416ms)


  1 passing (459ms)

Deploy (Migrations)

Based on the Truffle Migrate example: https://github.com/OpenZeppelin/openzeppelin-sdk/tree/master/examples/truffle-migrate I was able to migrate the Sample contract and interact with it using both truffle console and the OpenZeppelin CLI (call).

2_deploy.js

// Load zos scripts and truffle wrapper function
const { scripts, ConfigManager } = require('@openzeppelin/cli');
const { add, push, create } = scripts;

async function deploy(options) {
  // Register Sample in the zos project
  add({ contractsData: [{ name: 'Sample', alias: 'Sample' }] });

  // Push implementation contracts to the network
  await push(options);

  // Create an instance of Sample
  await create(Object.assign({ contractAlias: 'Sample' }, options));
}

module.exports = function(deployer, networkName, accounts) {
  deployer.then(async () => {
    const { network, txParams } = await ConfigManager.initNetworkConfiguration({ network: networkName, from: accounts[1] })
    await deploy({ network, txParams })
  })
}

truffle-config.js

Uncomment the development network in truffle-config.js

    development: {
     host: "127.0.0.1",     // Localhost (default: none)
     port: 8545,            // Standard Ethereum port (default: none)
     network_id: "*",       // Any network (default: none)
    },

Deploy

$ truffle migrate

Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/Sample.sol
> Artifacts written to /mnt/c/Users/andre/Documents/projects/forum/oztruffle/build/contracts
> Compiled successfully using:
   - solc: 0.5.8+commit.23d335f2.Emscripten.clang



Starting migrations...
======================
> Network name:    'development'
> Network id:      1571113019294
> Block gas limit: 0x6691b7


1_initial_migration.js
======================

   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0x16fd71e6ce76737f3ce777946abf873b504329ded5cae86cf7b89951211b9073
   > Blocks: 0            Seconds: 0
   > contract address:    0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec
   > block number:        45
   > block timestamp:     1571122651
   > account:             0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
   > balance:             96.44318818
   > gas used:            261393
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00522786 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00522786 ETH


2_deploy.js
===========
0x902719F192Aa5240632f704Aa7A94BaB61B86550

   > Saving migration to chain.
   -------------------------------------
   > Total cost:                   0 ETH


Summary
=======
> Total deployments:   1
> Final cost:          0.00522786 ETH

Interact

Interact using truffle console

$ truffle console
truffle(development)> sample = await Sample.deployed()
undefined
truffle(development)> sample.address
'0x902719F192Aa5240632f704Aa7A94BaB61B86550'
truffle(development)> await sample.greet()
'A sample'

Interact using oz call (oz is an alias for openzeppelin)

$ oz call
? Pick a network development
? Pick an instance Sample at 0x902719F192Aa5240632f704Aa7A94BaB61B86550
? Select which function greet()
✓ Method 'greet()' returned: A sample
A sample
1 Like

Hi @mortimr,

Did the above reply work for you? Feel free to ask more questions.