How to Build and Deploy a Full Stack Upgradeable ERC-20 dApp

Quality crypto development resources are hard to find and even harder to understand. Moreover, some guides show you how to write smart contracts while others show you how to build front end dApps to interface with smart contracts, but few synthesize these parts into a coherent, usable app. To address this gap in educational material, in this article we will build a full stack application that works out of the box.

Writing readable code is core to the design philosophy at Carbon-12 labs. In a perfect world, end users of our products will never interact with the bit-machinery that takes place underneath the hood. Like the silent engines that purr in the smoothest cars, our code silently powers our products. Our goal is that if one were to actually look at our code base, then reading our code would be as pleasant as Reading a Murakami novel (insert IQ84 plug).

Even if we could turn back, we’d probably never end up back where we started

1Q84

We view the art of programming not as an isolated stroke of genius but as a continual process. Coding is never “finished”, and most of our day is spent first re-reading past code and refactoring. When we are not maintaining, we are planning. Like a General, we carefully plan our attack before we type a single line of code. We care a lot about clean design, but ultimately God is in the details. Every day, we preach to our engineers that quality is the result of a million selfless acts of care. These principles have guided us in designing the Carbon hybrid stablecoin.


Learning how to write smart contracts: old philosophy, new context

The skills required of a smart contract developer are really no different than a typical software developer, except for one catch: permanence. Once a smart contract is deployed to production, it exists…forever…meaning that any technical debt persists. This is why the concept of ‘upgradeability’ is so important to us.

Regardless of what type of smart contract we are building, whether it be a token or an exchange or an Oracle, we must have at the foundation some mechanism for upgradeability. Designing for upgradeability at the outset is the major departure from traditional programming; modern software developers can continually push code to a server powering a web app, but once a smart contract is deployed it is technically unchangeable.

Smart contracts are the building block of the decentralized web (source: https://blockgeeks.com/guides/smart-contracts/)

(Delayed) Upgradeability Proxies

Upgradeability Proxy Architecture (source: https://blog.zeppelinos.org/proxy-patterns/)

There has been a lot of existing work done on upgradeable ‘proxy’ smart contracts — proxies are essentially book keeping contracts that keep track of different logic implementations. For example, suppose that a smart contract Foo_version_0 is initially deployed with a single function, “bar()” that prints “Ooo, look at me!”. A proxy Foo contract initially points to Foo_version_0, such that if a user were to call Foo.bar(), then the Foo() proxy would fetch the bar() logic from Foo_version_0 and print “Ooo, look at me!”. Now, imagine that the Foo-Lab development team wants to update the Foo logic to Foo_version_1. Some users have reported feeling uncomfortable with Foo_version_0.bar()’s original message, so the version 1 bar() function now prints “Hello, world!”. The Foo proxy contract can now upgrade its logic to version 1 such that any user call to Foo.bar() now fetches the bar() logic from Foo_version_1 and prints “Hello, world!”.

The great feature of using proxy contracts is that the end user never needs to change the address of the Foo contract that they make calls to. The user can trust that all calls to the Foo proxy contract will be delegated to the latest logic contract’s implementation of bar().

Upgradeable logic is paramount to defending against inevitable bugs in smart contracts deployed on the blockchain

As you can see, programming smart contracts is not actually that different from typical programming. The fundamental best practices of modularity and separation of concerns still hold true! Early on, we turned to these principles and separated our logic contracts from our storage contracts. Our logic and can be upgraded in the face of nasty bugs without changing the storage state and sacrificing our users’ trust. In summary, a proxy will execute function calls according to how the latest logic implements it and crucially, these calls will modify the proxy’s storage. Just remember that the proxy executes calls in the context of the proxy contract.

In this article we introduce a unique way to upgrade smart contracts via a time-delayed locked where the actual upgrade will not take place until a certain amount of time has passed. Think of this like the “commit-push” pattern that Git is famous for.

So YOU want to build an upgradeable, future-proof ERC-20 Token? (On to the fun stuff)

Carbon-12 Labs believes that stablecoins will form the fluid base layer of the future digital economy. Let’s dive into the code now: at the heart of every stablecoin is a really robust token. So, the topic of this inaugural blog is to guide readers gently in building an upgradeable ERC20 token. At the end of this guide you will be able to deploy a basic ERC20 token with a simple dApp interface thrown in for fun.


The Roadmap

  1. Token implementation V0: This will be a Standard ERC20 capable of minting new tokens, burning tokens, and of course transferring
  2. Token implementation V1: We’ll add a few security features to V0 such as pausability and defense against double-spend attacks
  3. Token proxy: From the user’s point of view, they will have no clue that they are actually interacting with and using a ‘proxy’ contract as opposed to one of the V0/V1 token contracts, but the proxy is the key to enabling flexible upgradeability
  4. Deploying our smart contracts on a testnet: This part can be tricky, so we’ll use Infura for convenience and I’ll show you how to write useful deployment scripts
  5. dApp time: We will use React, Redux, and Material-UI to build a simple, elegant wallet to interface with our smart contracts!

Ready? Let’s get started.

0. Token Storage

To really drive home the point, in the name of modularity and adherance to the principles of the one true God — clean code — I will separate smart contract logic from storage. This will appear more useful when we begin building the proxy contract, which should use the same type of storage as its implementation contracts. This storage contract is admittedly very simple, but is inspired by this excellent post on Eternal Storage. The gist of the article is that data-copying on-chain is expensive so isolating data storage from the logic supports future upgrades.

All Tokens and Token Proxies will inherit from the TokenStorage class:

pragma solidity ^0.4.24;
import "./AllowanceSheet.sol";
import "./BalanceSheet.sol";
/**
* @title TokenStorage
*/
contract TokenStorage {
/**
Storage
*/
BalanceSheet public balances;
AllowanceSheet public allowances;
/**
* @dev a TokenStorage consumer can set its storages only once, on construction
*
**/
constructor (address _balances, address _allowances) public {
balances = BalanceSheet(_balances);
allowances = AllowanceSheet(_allowances);
}
/**
* @dev claim ownership of balance sheet passed into constructor.
**/
function claimBalanceOwnership() public {
balances.claimOwnership();
}
/**
* @dev claim ownership of allowance sheet passed into constructor.
**/
function claimAllowanceOwnership() public {
allowances.claimOwnership();
}
}

(TokenStorage.sol)

Any Token that inherits from TokenStorage will store references to a BalanceSheet and an AllowanceSheet, whose code follows. Note that although BalanceSheet and AllowanceSheet are isolated from the Token logic, their addresses cannot be changed after construction. This is a design decision and has important tradeoffs: modifiable storage addresses prevent against disaster scenarios or hacks that corrupt storage contracts, but they also force end-users to place trust in storage contract owners. We choose in this example to make storage non-upgradeable, but logic upgradeable.

pragma solidity ^0.4.24;
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import '../../helpers/Ownable.sol';
/**
* @title BalanceSheet
* @notice A wrapper around the balanceOf mapping.
*/
contract BalanceSheet is Ownable {
using SafeMath for uint256;
mapping (address => uint256) public balanceOf;
uint256 public totalSupply;
function addBalance(address _addr, uint256 _value) public onlyOwner {
balanceOf[_addr] = balanceOf[_addr].add(_value);
}
function subBalance(address _addr, uint256 _value) public onlyOwner {
balanceOf[_addr] = balanceOf[_addr].sub(_value);
}
function setBalance(address _addr, uint256 _value) public onlyOwner {
balanceOf[_addr] = _value;
}
function addTotalSupply(uint256 _value) public onlyOwner {
totalSupply = totalSupply.add(_value);
}
function subTotalSupply(uint256 _value) public onlyOwner {
totalSupply = totalSupply.sub(_value);
}
function setTotalSupply(uint256 _value) public onlyOwner {
totalSupply = _value;
}
}

(BalanceSheet.sol: a wrapper around the ERC20 balanceOf mapping)

pragma solidity ^0.4.24;
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import '../../helpers/Ownable.sol';
/**
* @title AllowanceSheet
* @notice A wrapper around an allowance mapping.
*/
contract AllowanceSheet is Ownable {
using SafeMath for uint256;
mapping (address => mapping (address => uint256)) public allowanceOf;
function addAllowance(address _tokenHolder, address _spender, uint256 _value) public onlyOwner {
allowanceOf[_tokenHolder][_spender] = allowanceOf[_tokenHolder][_spender].add(_value);
}
function subAllowance(address _tokenHolder, address _spender, uint256 _value) public onlyOwner {
allowanceOf[_tokenHolder][_spender] = allowanceOf[_tokenHolder][_spender].sub(_value);
}
function setAllowance(address _tokenHolder, address _spender, uint256 _value) public onlyOwner {
allowanceOf[_tokenHolder][_spender] = _value;
}
}

(AllowanceSheet.sol: a wrapper around the ERC20 allowance mapping)

  1. Token_V0

Token_V0 is an ERC20 token that includes the ability for the contract owner to mint new tokens and any token owner to burn tokens. Token_V0 inherits TokenStorage and therefore stores references to its storage classes.

Now is a good time to mention why we choose to implement two-phase ownership transfer. Many smart contracts inherit the common Ownable contract from the OpenZeppelin project to enforce user permissions. This contract provides for the concept of a single owner, who can unilaterally transfer ownership to a different address. However, if the owner of a contract makes a mistake in entering the address of an intended new owner, then the contract can become irrecoverably unowned. This is analogous to the well-known “Locked Ether” scenario resulting in several million US dollars (over 7000 Ether) irretrievably locked at address 0x0.

pragma solidity ^0.4.24;
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions". This adds two-phase
* ownership control to OpenZeppelin's Ownable class. In this model, the original owner
* designates a new owner but does not actually transfer ownership. The new owner then accepts
* ownership and completes the transfer.
*/
contract Ownable {
address public owner;
address public pendingOwner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor() public {
owner = msg.sender;
pendingOwner = address(0);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyPendingOwner() {
require(msg.sender == pendingOwner);
_;
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param _newOwner The address to transfer ownership to.
*/
function transferOwnership(address _newOwner) public onlyOwner {
require(_newOwner != address(0));
pendingOwner = _newOwner;
}
/**
* @dev Allows the pendingOwner address to finalize the transfer.
*/
function claimOwnership() onlyPendingOwner public {
emit OwnershipTransferred(owner, pendingOwner);
owner = pendingOwner;
pendingOwner = address(0);
}
}

(Ownable.sol: two-stage ownership transfer)

With a two-stage ownership model, the original owner designates a new “pending” owner but does not yet transfer ownership. The new owner must accepts ownership to complete the transfer

pragma solidity ^0.4.24;
import "./dataStorage/TokenStorage.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import "openzeppelin-solidity/contracts/AddressUtils.sol";
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import '../helpers/Ownable.sol';
/**
* @title Token_V0
* @notice A basic ERC20 token with modular data storage
*/
contract Token_V0 is ERC20, TokenStorage, Ownable {
using SafeMath for uint256;
/** Events */
event Mint(address indexed to, uint256 value);
event Burn(address indexed burner, uint256 value);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor (address _balances, address _allowances) public 
TokenStorage(_balances, _allowances) {}
/** Modifiers **/
/** Functions **/
function mint(address _to, uint256 _amount) public onlyOwner {
return _mint(_to, _amount);
}
function burn(uint256 _amount) public {
_burn(msg.sender, _amount);
}
function approve(address _spender, uint256 _value) 
public returns (bool) {
allowances.setAllowance(msg.sender, _spender, _value);
emit Approval(msg.sender, _spender, _value);
return true;
}
function transfer(address _to, uint256 _amount) public returns (bool) {
require(_to != address(0),"to address cannot be 0x0");
require(_amount <= balanceOf(msg.sender),"not enough balance to transfer");
balances.subBalance(msg.sender, _amount);
balances.addBalance(_to, _amount);
emit Transfer(msg.sender, _to, _amount);
return true;
}
function transferFrom(address _from, address _to, uint256 _amount) 
public returns (bool) {
require(_amount <= allowance(_from, msg.sender),"not enough allowance to transfer");
require(_to != address(0),"to address cannot be 0x0");
require(_amount <= balanceOf(_from),"not enough balance to transfer");

allowances.subAllowance(_from, msg.sender, _amount);
balances.addBalance(_to, _amount);
balances.subBalance(_from, _amount);
emit Transfer(_from, _to, _amount);
return true;
}
/**
* @notice Implements balanceOf() as specified in the ERC20 standard.
*/
function balanceOf(address who) public view returns (uint256) {
return balances.balanceOf(who);
}
/**
* @notice Implements allowance() as specified in the ERC20 standard.
*/
function allowance(address owner, address spender) public view returns (uint256) {
return allowances.allowanceOf(owner, spender);
}
/**
* @notice Implements totalSupply() as specified in the ERC20 standard.
*/
function totalSupply() public view returns (uint256) {
return balances.totalSupply();
}
/** Internal functions **/
function _burn(address _tokensOf, uint256 _amount) internal {
require(_amount <= balanceOf(_tokensOf),"not enough balance to burn");
// no need to require value <= totalSupply, since that would imply the
// sender's balance is greater than the totalSupply, which *should* be an assertion failure
balances.subBalance(_tokensOf, _amount);
balances.subTotalSupply(_amount);
emit Burn(_tokensOf, _amount);
emit Transfer(_tokensOf, address(0), _amount);
}
function _mint(address _to, uint256 _amount) internal {
balances.addTotalSupply(_amount);
balances.addBalance(_to, _amount);
emit Mint(_to, _amount);
emit Transfer(address(0), _to, _amount);
}
}

(Token_V0.sol: Burnable, Mintable, standard ERC20 token)

2. Token_V1

Let’s make an upgraded version of Token_V0 that includes a couple security upgrades. We add protection against double-spend attacks and include an emergency-brake ‘pause’ mechanism.

ERC20 double-spend attack vulnerability: The standard ERC-20 interface has a design flaw: if some user Alice wants to change the allowance granted to another user Bob, then Alice checks if Bob has already spent his allowance before issuing the transaction to change Bob’s allowance. However, Bob can still spend the original allowance before the transaction changing the allowance is mined, which thus allows Bob to spend both the pre-change and post-change allowances. In order to have a high probability of successfully spending the pre-change allowance after the victim has verified that it is not yet spent, the attacker waits until the transaction to change the allowance is issued, then issues a spend transaction with an unusually high gas price to ensure that the spend transaction is mined before the allowance change. More details on this flaw are available here and here

To make it more convenient to change allowances, we upgrade to Token_V1 which includes increaseApproval() and decreaseApproval() methods that add or subtract to the existing allowances rather than overwriting them. This effectively moves the check of whether Bob has spent its allowance to the time that the transaction is mined, removing Bob’s ability to double spend.

Since the approve() method is required in the ERC20 standard, Token_V1 disables users from calling this unsafe method by default using a Lockable contract. Any method that has a whenUnlocked modifier is disabled by default and can only be called if the contract owner ‘unlocks’ locked methods.

pragma solidity ^0.4.24;
import './Ownable.sol';
/**
* @title Lockable
* @dev Base contract which allows children to lock certain methods from being called by clients.
* Locked methods are deemed unsafe by default, but must be implemented in children functionality to adhere by
* some inherited standard, for example.
*/
contract Lockable is Ownable {
// Events
event Unlocked();
event Locked();
// Fields
bool public isMethodEnabled = false;
// Modifiers
/**
* @dev Modifier that disables functions by default unless they are explicitly enabled
*/
modifier whenUnlocked() {
require(isMethodEnabled);
_;
}
// Methods
/**
* @dev called by the owner to enable method
*/
function unlock() onlyOwner public {
isMethodEnabled = true;
emit Unlocked();
}
/**
* @dev called by the owner to disable method, back to normal state
*/
function lock() onlyOwner public {
isMethodEnabled = false;
emit Locked();
}
}

(Lockable.sol: locks whenUnlocked methods by default, owner can call unlock())

Secondly, we make Token_V1 pausable, meaning that if the contract owner detects malicious activity, then they can pause the contract to buy time to evaluate and defend against any attacks.

pragma solidity ^0.4.24;
import "./Ownable.sol";
/**
* @title Pausable
* @dev Base contract which allows children to implement an emergency stop mechanism. Identical to OpenZeppelin version
* except that it uses local Ownable contract
*/
contract Pausable is Ownable {
event Pause();
event Unpause();
bool public paused = false;
/**
* @dev Modifier to make a function callable only when the contract is not paused.
*/
modifier whenNotPaused() {
require(!paused);
_;
}
/**
* @dev Modifier to make a function callable only when the contract is paused.
*/
modifier whenPaused() {
require(paused);
_;
}
/**
* @dev called by the owner to pause, triggers stopped state
*/
function pause() onlyOwner whenNotPaused public {
paused = true;
emit Pause();
}
/**
* @dev called by the owner to unpause, returns to normal state
*/
function unpause() onlyOwner whenPaused public {
paused = false;
emit Unpause();
}
}

(Pausable.sol: supports pausing of significant state-changing activity, effectively implementing an ‘emergency brake’ on the contract)

pragma solidity ^0.4.24;
import './Token_V0.sol';
import "../helpers/Lockable.sol";
import "../helpers/Pausable.sol";
/**
* @title Token_V1
* @notice Adds pausability and disables approve() to defend against double-spend attacks in addition
* to inherited Token_V0 behavior
*/
contract Token_V1 is Token_V0, Pausable, Lockable {
using SafeMath for uint256;
/** Events */
constructor (address _balances, address _allowances) public 
Token_V0(_balances, _allowances) {}
/** Modifiers **/
/** Functions **/
function mint(address _to, uint256 _amount) public whenNotPaused {
super.mint(_to, _amount);
}
function burn(uint256 _amount) public whenNotPaused {
super.burn(_amount);
}
/**
* @notice Implements ERC-20 standard approve function. Locked or disabled by default to protect against
* double spend attacks. To modify allowances, clients should call safer increase/decreaseApproval methods.
* Upon construction, all calls to approve() will revert unless this contract owner explicitly unlocks approve()
*/
function approve(address _spender, uint256 _value)
public whenNotPaused whenUnlocked returns (bool) {
super.approve(_spender, _value);
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
* @notice increaseApproval should be used instead of approve when the user's allowance
* is greater than 0. Using increaseApproval protects against potential double-spend attacks
* by moving the check of whether the user has spent their allowance to the time that the transaction
* is mined, removing the user's ability to double-spend
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/
function increaseApproval(address _spender, uint256 _addedValue)
public whenNotPaused returns (bool) {
increaseApprovalAllArgs(_spender, _addedValue, msg.sender);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
* @notice decreaseApproval should be used instead of approve when the user's allowance
* is greater than 0. Using decreaseApproval protects against potential double-spend attacks
* by moving the check of whether the user has spent their allowance to the time that the transaction
* is mined, removing the user's ability to double-spend
* @param _spender The address which will spend the funds.
* @param _subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseApproval(address _spender, uint256 _subtractedValue)
public whenNotPaused returns (bool) {
decreaseApprovalAllArgs(_spender, _subtractedValue, msg.sender);
return true;
}
function transfer(address _to, uint256 _amount) public whenNotPaused returns (bool) {
super.transfer(_to, _amount);
}
/**
* @notice Initiates a transfer operation between address `_from` and `_to`. Requires that the
* message sender is an approved spender on the _from account.
* @dev When implemented, it should use the transferFromConditionsRequired() modifier.
* @param _to The address of the recipient. This address must not be blacklisted.
* @param _from The address of the origin of funds. This address _could_ be blacklisted, because
* a regulator may want to transfer tokens out of a blacklisted account, for example.
* In order to do so, the regulator would have to add themselves as an approved spender
* on the account via `addBlacklistAddressSpender()`, and would then be able to transfer tokens out of it.
* @param _amount The number of tokens to transfer
* @return `true` if successful
*/
function transferFrom(address _from, address _to, uint256 _amount)
public whenNotPaused returns (bool) {
super.transferFrom(_from, _to, _amount);
}
/** Internal functions **/

function decreaseApprovalAllArgs(address _spender, uint256 _subtractedValue, address _tokenHolder) internal {
uint256 oldValue = allowances.allowanceOf(_tokenHolder, _spender);
if (_subtractedValue > oldValue) {
allowances.setAllowance(_tokenHolder, _spender, 0);
} else {
allowances.subAllowance(_tokenHolder, _spender, _subtractedValue);
}
emit Approval(_tokenHolder, _spender, allowances.allowanceOf(_tokenHolder, _spender));
}
function increaseApprovalAllArgs(address _spender, uint256 _addedValue, address _tokenHolder) internal {
allowances.addAllowance(_tokenHolder, _spender, _addedValue);
emit Approval(_tokenHolder, _spender, allowances.allowanceOf(_tokenHolder, _spender));
}
}

(Token_V1.sol: adds pausability and defends against a double-spend attack vulnerability)

3. TokenProxy

One of the advantages of Ethereum is that every transaction made to a contract is immutable on a public ledger we call the blockchain. This enables smart contracts to enforce a verified set of promises and is the main reason why there is so much excitement about their potential for forming the foundation of the next great digital revolution.

But the disadvantage is that you cannot change the source code of your smart contract after it’s been deployed. Developers working on centralized applications (like Facebook, or Airbnb) are used to frequent updates in order to fix bugs or introduce new features. This is impossible to do on Ethereum with traditional patterns.

A proxy architecture pattern is such that all message calls go through a Proxy contract that will redirect them to the latest deployed contract logic. To upgrade, a new version of your contract is deployed, and the Proxy is updated to reference the new contract address.

A proxy contract uses the delegatecall opcode to forward function calls to a target contract which can be updated. As delegatecall retains the state of the function call, the target contract’s logic can be updated and the state will remain in the proxy contract for the updated target contract’s logic to use. As with delegatecall, the msg.sender will remain that of the caller of the proxy contract.

pragma solidity ^0.4.24;
import "./dataStorage/TokenStorage.sol";
import "zos-lib/contracts/upgradeability/UpgradeabilityProxy.sol";
import '../helpers/Ownable.sol';
/**
* @title TokenProxy
* @notice A proxy contract that serves the latest implementation of TokenProxy.
*/
contract TokenProxy is UpgradeabilityProxy, TokenStorage, Ownable {
constructor(address _implementation, address _balances, address _allowances)
UpgradeabilityProxy(_implementation)
TokenStorage(_balances, _allowances) public {}
/**
* @dev Upgrade the backing implementation of the proxy.
* Only the admin can call this function.
* @param newImplementation Address of the new implementation.
*/
function upgradeTo(address newImplementation) public onlyOwner {
_upgradeTo(newImplementation);
}
/**
* @return The address of the implementation.
*/
function implementation() public view returns (address) {
return _implementation();
}
}

(TokenProxy.sol)

I will deploy TokenProxy on the blockchain initially using Token_V0 as its logic, meaning that users can call approve() on our Token and it won’t be pausable. But, the contract owner can upgrade to Token_V1 to add these changes to the token. Importantly, the end-user will ALWAYS make calls to the address of the token proxy even if the underlying logic changes.

4. DelayedUpgradeabilityProxy

We can make our upgradeable proxy contracts even more future-proof by introducing a time-delay between when a proxy announces an upgrade and when the upgrade actually takes place. This time-delay allows developers time to vet and audit code before it gets permanently published. This also hopefully enables a better user experience since users will know exactly when code changes are set to take place.

The time-delayed nature of our upgradeability proxies limits the contract owner’s ability to make discretionary upgrades. What this means is that the owner cannot upgrade contracts whenever they want to, giving end-users time to adjust to upgraded logic, and ideally this is paired with a strong governance system that allows the network to ‘vote’ on pending upgrades. This approach is also described in Consensys’ best practices as a ‘speed bump’ that delays contract actions. Our delayed upgradeability proxy and a token proxy that uses the delayed upgrade mechanism follow:

pragma solidity ^0.4.24;
import "zos-lib/contracts/upgradeability/UpgradeabilityProxy.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
/** 
* @title DelayedUpgradeabilityProxy
* @notice Implements an upgradeability proxy with the option of
* introducing pending implementations.
*/
contract DelayedUpgradeabilityProxy is UpgradeabilityProxy {
using SafeMath for uint256;
address public pendingImplementation;
bool public pendingImplementationIsSet;
uint256 public pendingImplementationApplicationDate; // Date on which to switch all contract calls to the new implementation
uint256 public UPGRADE_DELAY = 4 weeks;
event PendingImplementationChanged(address indexed oldPendingImplementation, address indexed newPendingImplementation);
constructor(address _implementation) public UpgradeabilityProxy(_implementation) {}
/**
* @notice Sets the pending implementation address of the proxy.
* This function is internal--uses of this proxy should wrap this function
* with a public function in order to make it externally callable.
* @param implementation Address of the new implementation.
*/
function _setPendingUpgrade(address implementation) internal {
address oldPendingImplementation = pendingImplementation;
pendingImplementation = implementation;
pendingImplementationIsSet = true;
emit PendingImplementationChanged(oldPendingImplementation, implementation);
pendingImplementationApplicationDate = block.timestamp.add(UPGRADE_DELAY);
}
/**
* @notice Overrides the _willFallback() function of Proxy, which enables some code to
* be executed prior to the fallback function. In this case, the purpose of this code
* is to automatically switch the implementation to the pending implementation if the
* wait period of UPGRADE_DELAY (28 days) has been satisfied.
*/
function _willFallback() internal {
if (pendingImplementationIsSet && block.timestamp > pendingImplementationApplicationDate) {
_upgradeTo(pendingImplementation);
pendingImplementationIsSet = false;
super._willFallback();
}
else {
super._willFallback();
}
}
}

(DelayedUpgradeabilityProxy.sol: generic delayed upgradeable proxy arbitrarily upgrades a contract after 4 weeks)

pragma solidity ^0.4.24;
import "./dataStorage/TokenStorage.sol";
import '../upgradeability/DelayedUpgradeabilityProxy.sol';
import '../helpers/Ownable.sol';
/**
* @title TokenProxyDelayed
* @notice A proxy contract that serves the latest implementation of TokenProxy. This proxy
* upgrades only after a set amount of time (denominated in blocks mined)
*/
contract TokenProxyDelayed is DelayedUpgradeabilityProxy, TokenStorage, Ownable {
constructor(address _implementation, address _balances, address _allowances)
DelayedUpgradeabilityProxy(_implementation)
TokenStorage(_balances, _allowances) public {}
/**
* @dev Upgrade the backing implementation of the proxy.
* Only the admin can call this function.
* @param newImplementation Address of the new implementation.
*/
function upgradeTo(address newImplementation) public onlyOwner {
_setPendingUpgrade(newImplementation);
}
/**
* @return The address of the implementation.
*/
function implementation() public view returns (address) {
return _implementation();
}
}

(TokenProxyDelayed.sol: Upgradeable Token Proxy contract that uses the delayed upgrade mechanism)

5. Time to Deploy our Contracts!

First, we deploy our base data structure classes (BalanceSheet, AllowanceSheet) in advance so that we can provide them to our top-level constructors.

Secondly, we’ll deploy our Token and TokenProxy classes. Since we intend for users to interact only with our upgradeable proxy classes, we will pass in empty data storage contracts into the Token constructors and pass the real contracts to the proxy. This serves our intention for the Token contracts to be “implementation” contracts that exist as models of the logic that proxy’s should delegate functionality to.

Finally, before we can have some fun with our newly deployed contracts, we have to do a couple important setup steps. Remember how our storage contracts are isolated from our proxy/token contracts? Moreover, observe that BalanceSheet and AllowanceSheet are Ownable contracts, thereby protecting them from being manipulated by any average Joe. Therefore, we must make the Proxy contract the owner of both storage contracts so that it can manipulate its own balance and allowance mappings! This is the point of 5_setup.js which executes two-stage ownership transfer of the data storages to the proxy contract.

I wrote deployment scripts in migrations/ that you can run to deploy these smart contracts. I deployed these smart contracts on Ropsten using Infura, which you can do as well by running (you will need to create a .env file with your MetaMask mnemonic and Infura API key!):

truffle migrate --network ropsten

6. Wallet dApp

I built a simple front end for users to interact with the UpgradeableERC20 smart contracts. This dApp works with the initially deployed TokenProxy that uses the Token_V0 logic, but it can be easily configured to work with the upgraded Token_V1 logic. It also is configured to work if the contracts are deployed on Ropsten, as opposed to the Mainnet.

The dApp has three sections: Dashboard, Profile, and Admin. Normal users can use the Dashboard to transfer or burn tokens. The contract owner can use the Admin portal to mint new tokens; if a non-owner tries to mint new tokens their transaction will revert.

The user must sign in via the Profile portal using MetaMask, which includes a secure identity vault, providing a user interface to manage your identities on different sites and sign blockchain transactions.

Admin Portal
Dashboard
Profile

Final Points

Smart contract repo is here including unit tests

Wallet dApp repo is here

Check out Carbon’s latest product, a fiat-stablecoin ramp at fiat.carbon.money

I recently transitioned from my role as a corporate bond trader to work on product design at Carbon, where we are building a stablecoin using a new hybrid approach. If you enjoyed this tutorial, want to collaborate on a future project, or have questions, then please do not hesitate to contact me at team@carbon.money or check out open positions at https://angel.co/carbonmoney/jobs! Happy coding!