ZeppelinOS Workshop
Workshop on upgradeability and EVM packages based on ZeppelinOS v2.
This repository contains a base contract, guides to deploy an upgradeable version of it, and to leverage the EVM package openzeppelin-eth
, plus a set of challenges to upgrade the base contract and play with the ZeppelinOS CLI.
Setup
All the following commands have been tested in Ubuntu Linux and OSX, and depend on nodejs
. These instructions should also work on Windows, but YMMV.
- Clone this repository and install all dependencies:
git clone git@github.com:spalladino/zos-workshop.git
cd zos-workshop
npm install
- Compile the contracts:
npx truffle compile
- Run tests to check everything is working fine:
npm test
- Start a local ganache instance for development:
./start-ganache
Working with ZeppelinOS
Deployment
To support upgradeability we first need to make some changes to our contract. The most important one is to convert the constructor into an initializer (you can check the solution in UpgradeableWallet.sol
).
Once set up, we will spin up our ZeppelinOS project and deploy it to a local ganache network using one of our local accounts. Run the following commands:
npx zos init my-wallet
npx zos add UpgradeableWallet:Wallet
npx zos session --network local --from 0x22d491bde2303f2f43325b2108d26f1eaba1e32b --expires 86400
We can now create our wallet instance, and set one of our local accounts as the owner:
npx zos create Wallet --init --args 0xffcf8fdee72ac11b5c542428b35eef5769c409f0
Linking an EVM Package
To play around with the wallet, we'll need an ERC20 contract. We can use one of the contracts provided by openzeppelin-eth
to do this.
npx zos link openzeppelin-eth --no-install
npx zos push --deploy-dependencies
Note that we'll need to deploy a local instance of openzeppelin-eth
in this case since we're on a local ganache network. On a testnet or mainnet, zos
will automatically use the deployed instances already provided by the package.
We can now create an ERC20 upgradeable contract from a logic contract provided by openzeppelin-eth
, which has the following initializer:
function initialize(
string name, string symbol, uint8 decimals, uint256 initialSupply, address initialHolder,
address[] minters, address[] pausers
) public initializer
We'll create an instance of the StandaloneERC20
contract by running the following, replacing the wallet address:
npx zos create openzeppelin-eth/StandaloneERC20 --init --args 'MyToken,MYT,8,10000000000,WALLET_ADDRESS,[],[]'
Testing our wallet
We can now fire up a truffle console to interact with our wallet and test it.
npx truffle console --network local
> wallet = UpgradeableWallet.at(WALLET_ADDRESS) // replace with actual wallet address
> token = IERC20.at(TOKEN_ADDRESS) // replace with actual token address
> someone = web3.eth.accounts[3] // any random account
> owner = '0xffcf8fdee72ac11b5c542428b35eef5769c409f0' // the same address we used when initializing the wallet
> token.balanceOf(wallet.address).then(x => x.toNumber())
> wallet.transferERC20(token.address, someone, 100, { from: owner })
> token.balanceOf(someone).then(x => x.toNumber())
But our wallet allows anyone to transfer our tokens:
> wallet.transferERC20(token.address, someone, 100, { from: someone })
> token.balanceOf(someone).then(x => x.toNumber())
Upgrading
We will now correct our contract and upgrade it to the fixed version. After fixing the Solidity code by adding a require(msg.sender == _owner);
statement, upload the new version and upgrade by running:
npx zos push
npx zos update Wallet
We can now test that the fix is in place in the exact same wallet contract by attempting the transfer again and checking that it is reverted:
> wallet.transferERC20(token.address, someone, 100, { from: someone }) // reverts
> wallet.transferERC20(token.address, someone, 100, { from: owner }) // works
Challenges
We will go through different independent challenges that will require us to upgrade the contract to cater for different requirements.
Security
Our transferERC20
function currently allows anyone to transfer the wallet's token. We need to add a check that only the owner
of the contract can do so, and upgrade to the new version. Bonus points for solving this using Ownable
from openzeppelin-eth
.
Compliance
The wallet contract's transferERC20
method returns a boolean value to notify whether the transfer was successful, following the ERC20 standard. However, the implementation is missing its return value. We need to add a return
statement to the function to avoid a return value bug.
Features
We want to extend our wallet to support not only ERC20
tokens, but also non-fungible ERC721
tokens, also known as NFTs. To do this, we need to add a new public function transferERC721
to our contract. And we can use StandaloneERC721
from openzeppelin-eth
for testing.
Improved access control
We want to allow more than a single account to manage our wallet. To do this, we'll need to add a new storage variable to keep track of those who can handle our assets, and change all access checks. Note that we need to be especially careful when modifying the storage of a contract.
Governance
To avoid having a single account with the privileges to freely upgrade our wallet smart contract, we can set up a MultiSigWallet and yield upgradeability control to it using the set-admin
command.