This is a simple contract that can receive donations and mints an ERC721 token in exchange. It demonstrates the use of zeppelinOS to make smart contracts upgradeable.
Smart contract upgradeability can be achieved by separating the storage of a contract from its logic. The user interacts with a proxy that holds the storage and delegetecalls the logic in a referenced contract. That reference can be updated, when a new logic contract should be used. The big problem with this appraoch is storage collision: When a logic contract changes a state variable through a delegatecall from the proxy, it does so in the proxy's storage scope. Hence, the logic contract could accidentally overwrite important parts of the storage of the proxy (for example the address of the referenced logic contract). There are various ways to avoid this. ZeppelinOS uses unstructured storage which allows the logic contracts to live independently of the proxy and only with minor modifications as compared to regular contracts.
Zeppelin has a lot of guides and tutorials and it gets a bit wild because many of them are outdated, but the current docs at https://docs.zeppelinos.org are very well organized and thorough. This page explains the theory behind the unstructured storage approach very well. In addition, this hidden page gives a very good low-level understanding of the various steps that are abstracted by the ZeppelinOS CLI commands.
This is new stuff, so even though most of it is well explained in the docs, I will outline all the necessary steps to create an upgradeable project using the contract in this repository as an example. The first 3 steps are pretending that we are starting with an empty slate. If you run npm install
on this repository you can jump straight to starting a local blockchain and deploying the project on it.
npm install --global zos
mkdir upgradeable-donations
cd upgradeable-donations
npm init
zos init upgradeable-donations
npm install --save zos-lib
npm install --save truffle
zos link openzeppelin-eth
openzeppelin-eth makes Zeppelins standard libraries available through on-chain upgradeable contracts. In testing you have to deploy them yourself (once), while on the mainnet they are already deployed.
Write the Donations contract. Make sure it has an initialize function instead of a constructor. You might also have to tweak the compiler version (see note below).
npm install -g ganache-cli
ganache-cli --port 9545 --deterministic
zos session --network local --from <nonAdminAddress> --expires 3600
The nonAdminAddress should be an address available in your ganache console, but not the first one (see notes below).
zos add Donations
zos push --deploy-dependencies
This creates the first logic instance of the contract which is not yet initialized. If there was a constructor that set some state variables, a proxy that would reference this logic contract would not have a way of knowing about those state changes. That is why the constructor is replaced by the initialize function, that can be called by the proxy to make sure the proxy is in on the action from the start.
zos create Donations --init initialize --args 5,AchillsPiggybank
Now we have a proxy which is what the user will be interacting with. It initializes the logic contract and sets the minDonation to 5, and the contractName to AchillsPiggybank.
We can now call some functions on that proxy contract to check if it works:
npx truffle console --network local
Donations.deployed().then(res=>{contract=res})
contract.minDonation.call().then(res=>{console.log(res)}) // 5
contract.reduceMinDonation()
contract.minDonation.call().then(res=>{console.log(res)}) // 4
contract.increaseMinDonation() // contract.increaseMinDonation is not a function!
Let's say we forgot to include increaseMinDonation and want to add this in an upgrade to our contract. We can just modify the Donations.sol contract by adding the function in:
function increaseMinDonation() public {
minDonation += 1;
}
Then run:
zos push
zos update Donations
And a new logic contract gets deployed that automatically points at the same proxy. We can verify this:
npx truffle console --network local
Donations.deployed().then(res=>{contract=res})
contract.increaseMinDonation() // works now!
contract.minDonation.call().then(res=>{num=res}) // 5
Installing ZeppelinOS as per the docs did not automatically add truffle to my repository, so I had to add it manually using npm install --save truffle
. I also ran into trouble with the Solidity compiler versions and ended up fixing it at 0.4.24 by adding this property to the exported object in truffle-config.js:
compilers: {
solc: {
version: "0.4.24" // otherwise compiler version problems
}
}
It is important to start a session with an address that is different from the default sender address in your ganache setup. This is mentioned in the docs as the "transparent proxy issue", but nevertheless a common source of error.
zos session --network local --from <nonDefaultAddress> --expires 3600
The approach to calling functions on the deployed proxy contract through truffle described in the docs, did not work for me: Instead of myContract = MyContract.at('<your-contract-address>')
I had to use MyContract.deployed().then(instance => {...})
.