Using Dependencies
In Getting Started, we learned how to set up a new OpenZeppelin project, deploy a simple contract, and upgrade it. Now, we will build a more interesting project with multiple contracts, leveraging the OpenZeppelin Contracts Ethereum Package. We will learn about linking Ethereum Packages, and writing upgradeable contracts.
What We Will Build
We will write a TokenExchange
contract, that will allow any user to purchase an ERC20 token in exchange for ETH, at a fixed exchange rate. We will write TokenExchange
ourselves, but leverage the ERC20 implementation from OpenZeppelin Contracts
Before we get started, make sure to initialize a new project:
$ mkdir token-exchange && cd token-exchange
$ npm init -y
$ npm install @openzeppelin/cli
$ npx openzeppelin init
The full code for this project is available in our Github repo. |
Linking the Contracts Ethereum Package
We will first get ourselves an ERC20 token. Instead of coding one from scratch, we will use the one provided by the OpenZeppelin Contracts Ethereum Package. An Ethereum Package is a set of contracts set up to be easily included in an OpenZeppelin project, with the added bonus that the contracts' code is already deployed in the Ethereum network. This is a more secure code distribution mechanism, and also helps you save gas upon deployment.
Check out this article to learn more about Ethereum Packages. |
To link the OpenZeppelin Contracts Ethereum Package into your project, simply run the following:
$ npx oz link @openzeppelin/contracts-ethereum-package
This command will download the Ethereum Package (bundled as a regular npm package), and connect it to your OpenZeppelin project. We now have all of OpenZeppelin contracts at our disposal, so let’s create an ERC20 token!
Make sure you install @openzeppelin/contracts-ethereum-package and not the vanilla @openzeppelin/contracts . The latter is set up for general usage, while @openzeppelin/contracts-ethereum-package is tailored for being used with OpenZeppelin Upgrades. This means that its contracts are already set up to be upgradeable.
|
Creating an ERC20 Token
Let’s deploy an ERC20 token contract to our development network. Make sure to have a Ganache instance running, or start one by running:
$ npx ganache-cli --deterministic
For setting up the token, we will be using the ERC20PresetMinterPauser implementation provided by the OpenZeppelin package. We will initialize the instance with the token metadata (name, symbol), and then mint a large initial supply for one of our accounts.
The unlocked accounts and transaction hashes may differ from the ones shown here. Run a brand-new Ganache in --deterministic mode to get the same ones.
|
$ npx oz deploy
No contracts found to compile.
? Choose the kind of deployment upgradeable
? Pick a network development
? Pick a contract to deploy @openzeppelin/contracts-ethereum-package/ERC20PresetMinterPauserUpgradeSafe
✓ Deploying @openzeppelin/contracts-ethereum-package dependency to network dev-1589440867048
All implementations are up to date
? Call a function to initialize the instance after creating it? Yes
? Select which function initialize(name: string, symbol: string)
? name: string: MyToken
? symbol: string: MYT
✓ Setting everything up to create contract instances
✓ Instance created at 0x59d3631c86BbE35EF041872d502F218A39FBa150
To upgrade this instance run 'oz upgrade'
0x59d3631c86BbE35EF041872d502F218A39FBa150
Let’s break down what we did in the command above. We first chose to create an instance of the ERC20PresetMinterPauserUpgradeSafe
contract from the @openzeppelin/contracts-ethereum-package
package we had linked before, and to create it in the local development
network. We are then instructing the CLI to initialize it with the initial values needed to set up our token. This requires us to choose the appropriate initialize
function, and input all the required arguments. The OpenZeppelin CLI will then atomically deploy and initialize the new instance in a single transaction.
We now have a working ERC20 token contract in our development network.
Next we get the accounts we have setup
$ npx oz accounts
? Pick a network development
Accounts for dev-1589440867048:
Default: 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
All:
- 0: 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
- 1: 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0
...
Then we mint 100 MYT to our default account
$ npx oz send-tx
? Pick a network development
? Pick an instance ERC20PresetMinterPauserUpgradeSafe at 0x59d3631c86BbE35EF041872d502F218A39FBa150
? Select which function mint(to: address, amount: uint256)
? to: address: 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
? amount: uint256: 100000000000000000000
✓ Transaction successful. Transaction hash: 0xff24cce7801e424db6e5a7076d4a9c677ef76df320fe04e2c39e537d09029ec0
Events emitted:
- Transfer(0x0000000000000000000000000000000000000000, 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1, 100000000000000000000)
We can check that the initial supply was properly allocated by using the balance
command. Make sure to use the address where your ERC20 token instance was created.
$ npx oz balance --erc20 0x59d3631c86BbE35EF041872d502F218A39FBa150
? Enter an address to query its balance 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1
? Pick a network development
Balance: 100 MYT
100000000000000000000
Great! We can now write an exchange contract and connect it to this token when we deploy it.
Writing the Exchange Contract
In order to transfer an amount of tokens every time it receives ETH, our exchange contract will need to store the token contract address and the exchange rate in its state. We will set these two values during initialization, when we deploy the instance with oz deploy
.
Because we’re writing upgradeable contracts we cannot use Solidity constructor
s. Instead, we need to use initializers. An initializer is just a regular Solidity function, with an additional check to ensure that it can be called only once.
To make coding initializers easy, OpenZeppelin Upgrades provides a base Initializable
contract, that includes an initializer
modifier that takes care of this. You will first need to install it:
$ npm install @openzeppelin/upgrades
Now, let’s write our exchange contract in contracts/TokenExchange.sol
, using an initializer to set its initial state:
// contracts/TokenExchange.sol
pragma solidity ^0.6.2;
// Import base Initializable contract
import "@openzeppelin/upgrades/contracts/Initializable.sol";
// Import the IERC20 interface and and SafeMath library
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
contract TokenExchange is Initializable {
using SafeMath for uint256;
// Contract state: exchange rate and token
uint256 public rate;
IERC20 public token;
// Initializer function (replaces constructor)
function initialize(uint256 _rate, IERC20 _token) public initializer {
rate = _rate;
token = _token;
}
// Send tokens back to the sender using predefined exchange rate
receive() external payable {
uint256 tokens = msg.value.mul(rate);
token.transfer(msg.sender, tokens);
}
}
Note the usage of the initializer
modifier in the initialize
method. This guarantees that once we have deployed our contract, no one can call into that function again to alter the token or the rate.
Let’s now create and initialize our new TokenExchange
contract:
$ npx oz deploy
✓ Compiled contracts with solc 0.6.7 (commit.b8d736ae)
? Choose the kind of deployment upgradeable
? Pick a network development
? Pick a contract to deploy TokenExchange
✓ Added contract TokenExchange
✓ Contract TokenExchange deployed
All implementations have been deployed
? Call a function to initialize the instance after creating it? Yes
? Select which function initialize(_rate: uint256, _token: address)
? _rate: uint256: 10
? _token: address: 0x59d3631c86BbE35EF041872d502F218A39FBa150
✓ Instance created at 0x67B5656d60a809915323Bf2C40A8bEF15A152e3e
To upgrade this instance run 'oz upgrade'
0x67B5656d60a809915323Bf2C40A8bEF15A152e3e
Our exchange is almost ready! We only need to fund it, so it can send tokens to purchasers. Let’s do that using the oz send-tx
command, to transfer the full token balance from our own account to the exchange contract. Make sure to replace the recipient of the transfer with the TokenExchange
address you got from the previous command.
$ npx oz send-tx
? Pick a network development
? Pick an instance ERC20PresetMinterPauserUpgradeSafe at 0x59d3631c86BbE35EF041872d502F218A39FBa150
? Select which function transfer(recipient: address, amount: uint256)
? recipient: address: 0x67B5656d60a809915323Bf2C40A8bEF15A152e3e
? amount: uint256: 100e18
✓ Transaction successful. Transaction hash: 0xdfec4bb86d995ab6f48696bf09685e0bee9949f19eb0dc6c6425e2ea3a37ef49
Events emitted:
- Transfer(0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1, 0x67B5656d60a809915323Bf2C40A8bEF15A152e3e, 100000000000000000000)
All set! We can start playing with our brand new token exchange.
Using Our Exchange
Now that we have initialized our exchange contract and seeded it with funds, we can test it out by purchasing tokens. Our exchange contract will send tokens back automatically when we send ETH to it, so let’s test it by using the oz transfer
command. This command allows us to send funds to any address; in this case, we will use it to send ETH to our TokenExchange
instance:
$ npx oz transfer
? Pick a network development
? Choose the account to send transactions from (1) 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0
? Enter the receiver account 0x67B5656d60a809915323Bf2C40A8bEF15A152e3e
? Enter an amount to transfer 0.1 ether
✓ Funds sent. Transaction hash: 0xfcd74514cd9629a26ec25f637f5dd958dbdfa151594c1a3a6671dcf1d889a50e
Make sure you replace the receiver account with the corresponding address where your TokenExchange was created.
|
We can now use oz balance
again, to check the token balance of the address that made the purchase. Since we sent 0.1 ETH, and we used a 1:10 exchange rate, we should see a balance of 1 MYT (MyToken).
$ npx oz balance --erc20 0x59d3631c86BbE35EF041872d502F218A39FBa150
? Enter an address to query its balance 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0
? Pick a network development
Balance: 1 MYT
1000000000000000000
Success! We have our exchange up and running, gathering ETH in exchange for our tokens. But how can we collect the funds we earned…?
Upgrading the Exchange
We forgot to add a method to withdraw the funds from the token exchange contract! While this would typically mean that the funds are locked in there forever, we can upgrade the contract with the OpenZeppelin CLI to add a way to collect those funds.
While upgrading a contract is certainly useful in situations like this, where you need to fix a bug or add a missing feature, it could still be used to change the rules of the game. For instance, you could upgrade the token exchange contract to alter the rate at any time. Because of this, it is important to have appropriate Project Governance in place. |
Let’s modify the TokenExchange
contract to add a withdraw
method, only callable by an owner
.
// contracts/TokenExchange.sol
contract TokenExchange is Initializable {
uint256 public rate;
IERC20 public token;
+ address public owner;
// (existing functions not shown here for brevity)
+ function withdraw() public {
+ require(
+ msg.sender == owner,
+ "Address not allowed to call this function"
+ );
+ msg.sender.transfer(address(this).balance);
+ }
}
When modifying your contract, you will have to place the owner
variable after the other variables (learn more about this restriction). Don’t worry if you forget about it, the CLI will check this for you when you try to upgrade.
If you are familiar with OpenZeppelin Contracts, you may be wondering why we didn’t simply extend from Ownable and used the onlyOwner modifier. The issue is OpenZeppelin Upgrades does not support extending from now contracts in an upgrade (if they declare their own state variables). Again, the CLI will alert you if you attempt to do this. Refer to the Upgrades documentation for more info.
|
The only thing missing is actually setting the owner
of the contract. To do this, we can add another function that we will call when upgrading, making sure it can only be called once:
// contracts/TokenExchange.sol
contract TokenExchange is Initializable {
uint256 public rate;
IERC20 public token;
address public owner;
// (existing functions not shown here for brevity)
function withdraw() public {
require(
msg.sender == owner,
"Address not allowed to call this function"
);
msg.sender.transfer(address(this).balance);
}
+ // To be run during upgrade, ensuring it can never be called again
+ function setOwner(address _owner) public {
+ require(owner == address(0), "Owner already set, cannot modify!");
+ owner = _owner;
+ }
}
First we compile the contract
$ npx oz compile
✓ Compiled contracts with solc 0.6.7 (commit.b8d736ae)
We can now upgrade our token exchange contract to this new version, and call setOwner
during the upgrade process. The OpenZeppelin CLI will take care of making the upgrade and the call atomically in a single transaction.
$ npx oz upgrade
? Pick a network development
? Which instances would you like to upgrade? Choose by name
? Pick an instance to upgrade TokenExchange
? Call a function on the instance after upgrading it? Yes
? Select which function setOwner(_owner: address)
? _owner: address: 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1
Nothing to compile, all contracts are up to date.
- New variable 'address owner' was added in contract TokenExchange in contracts/TokenExchange.sol:1 at the end of the contract.
See https://docs.openzeppelin.com/upgrades/2.6//writing-upgradeable#modifying-your-contracts for more info.
✓ Contract TokenExchange deployed
All implementations have been deployed
✓ Instance upgraded at 0x67B5656d60a809915323Bf2C40A8bEF15A152e3e. Transaction receipt: 0xb742c40f939daab145552880e7e07a696570684af1b6aef03d196d39ea61c019
✓ Instance at 0x67B5656d60a809915323Bf2C40A8bEF15A152e3e upgraded
There! We can now call withdraw
from our default address to extract all ETH sent to the exchange.
$ npx oz send-tx
? Pick a network development
? Pick an instance TokenExchange at 0x67B5656d60a809915323Bf2C40A8bEF15A152e3e
? Select which function withdraw()
✓ Transaction successful. Transaction hash: 0x4797d95b1892c9a0cb264bb430fcb425b7cb220ef33cb759496d1dd1dc05a31a
You can also upgrade dependencies from an Ethereum Package. Upon a new release of @openzeppelin/contracts-ethereum-package , if you want to update your ERC20 to include the latest fixes, you can just oz link the new version and use oz upgrade to get your instance to the newest code.
|
Wrapping Up
We have built a more complex setup in this tutorial, and learned several concepts along the way. We introduced Ethereum Packages as dependencies for our projects, allowing us to spin up a new token with little effort.
We also presented some limitations of how Upgrades works, such as initializer methods as a replacement for constructors, and preserving the storage layout when modifying our source code. We also learned how to run a function as a migration when upgrading a contract.