Onboarding ERC20 tokens with OpenZeppelin SDK

Following the guide https://docs.openzeppelin.com/sdk/2.5/erc20-onboarding

I created ERC20Migrator and MyUpgradeableToken using the interactive commands below:

Versions

$ npx truffle version
Truffle v5.0.32 (core: 5.0.32)
Solidity - 0.5.11 (solc-js)
Node v10.16.0
Web3.js v1.2.1
$ npx openzeppelin --version
2.5.2

Setup project with truffle

mkdir tokenupgrade
cd tokenupgrade
npm init -y
npm i truffle
npx truffle init

Setup config for using public networks

npm i dotenv
npm i truffle-hdwallet-provider

Create .env with Infura Project ID and mnemonic (for details on how to do this see: Deploy to a public testnet using OpenZeppelin SDK)

truffle-config.js

/**
 * Use this file to configure your truffle project. It's seeded with some
 * common settings for different networks and features like migrations,
 * compilation and testing. Uncomment the ones you need or modify
 * them to suit your project as necessary.
 *
 * More information about configuration can be found at:
 *
 * truffleframework.com/docs/advanced/configuration
 *
 * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider)
 * to sign your transactions before they're sent to a remote public node. Infura accounts
 * are available for free at: infura.io/register.
 *
 * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
 * public/private key pairs. If you're publishing your code to GitHub make sure you load this
 * phrase from a file you've .gitignored so it doesn't accidentally become public.
 *
 */

// const HDWalletProvider = require('truffle-hdwallet-provider');
// const infuraKey = "fj4jll3k.....";
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();
require('dotenv').config();

const HDWalletProvider = require('truffle-hdwallet-provider');
const infuraProjectId = process.env.INFURA_PROJECT_ID;

module.exports = {
  /**
   * Networks define how you connect to your ethereum client and let you set the
   * defaults web3 uses to send transactions. If you don't specify one truffle
   * will spin up a development blockchain for you on port 9545 when you
   * run `develop` or `test`. You can ask a truffle command to use a specific
   * network from the command line, e.g
   *
   * $ truffle test --network <network-name>
   */

  networks: {
    // Useful for testing. The `development` name is special - truffle uses it by default
    // if it's defined here and no other network is specified at the command line.
    // You should run a client (like ganache-cli, geth or parity) in a separate terminal
    // tab if you use this network and you must also set the `host`, `port` and `network_id`
    // options below to some value.
    //
    development: {
     host: "127.0.0.1",     // Localhost (default: none)
     port: 8545,            // Standard Ethereum port (default: none)
     network_id: "*",       // Any network (default: none)
    },

    // Another network with more advanced options...
    // advanced: {
      // port: 8777,             // Custom port
      // network_id: 1342,       // Custom network
      // gas: 8500000,           // Gas sent with each transaction (default: ~6700000)
      // gasPrice: 20000000000,  // 20 gwei (in wei) (default: 100 gwei)
      // from: <address>,        // Account to send txs from (default: accounts[0])
      // websockets: true        // Enable EventEmitter interface for web3 (default: false)
    // },

    // Useful for deploying to a public network.
    // NB: It's important to wrap the provider as a function.
    // ropsten: {
      // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
      // network_id: 3,       // Ropsten's id
      // gas: 5500000,        // Ropsten has a lower block limit than mainnet
      // confirmations: 2,    // # of confs to wait between deployments. (default: 0)
      // timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)
      // skipDryRun: true     // Skip dry run before migrations? (default: false for public nets )
    // },
    
    kovan: {
      provider: () => new HDWalletProvider(process.env.DEV_MNEMONIC, 'https://kovan.infura.io/v3/' + infuraProjectId, 0, 5),
      network_id: 42, // eslint-disable-line camelcase
      gas: 5500000, // Ropsten has a lower block limit than mainnet
      confirmations: 2, // # of confs to wait between deployments. (default: 0)
      timeoutBlocks: 200, // # of blocks before a deployment times out  (minimum/default: 50)
      skipDryRun: true, // Skip dry run before migrations? (default: false for public nets )
    },

    // Useful for private networks
    // private: {
      // provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
      // network_id: 2111,   // This network is yours, in the cloud.
      // production: true    // Treats this network as if it was a public net. (default: false)
    // }
  },

  // Set default mocha options here, use special reporters etc.
  mocha: {
    // timeout: 100000
  },

  // Configure your compilers
  compilers: {
    solc: {
      version: "0.5.11",    // Fetch exact version from solc-bin (default: truffle's version)
      // docker: true,        // Use "0.5.1" you've installed locally with docker (default: false)
      // settings: {          // See the solidity docs for advice about optimization and evmVersion
      //  optimizer: {
      //    enabled: false,
      //    runs: 200
      //  },
      //  evmVersion: "byzantium"
      // }
    }
  }
}

LegacyToken.sol

Create LegacyToken in contracts directory

pragma solidity ^0.5.0;

import "./lib/ERC20Standard.sol";

contract LegacyToken is ERC20Standard {
    uint8 private constant DECIMALS = 18;
    string private constant NAME = "Legacy Token";
    string private constant SYMBOL = "LTC";

    constructor () ERC20Standard(NAME, SYMBOL, DECIMALS) public {
        uint256 initialSupply = 10000 * (10 ** uint256(DECIMALS));
        _mint(msg.sender, initialSupply);
    }
}

ERC20Standard.sol

Create ERC20Standard in contracts/lib directory

pragma solidity ^0.5.0;


import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol";

contract ERC20Standard is IERC20 {
  using SafeMath for uint256;

  string private _name;
  string private _symbol;
  uint8 private _decimals;
  uint256 private _totalSupply;
  mapping (address => uint256) private _balances;
  mapping (address => mapping (address => uint256)) private _allowed;

  constructor(string memory name, string memory symbol, uint8 decimals) public {
    _name = name;
    _symbol = symbol;
    _decimals = decimals;
  }

  /**
   * @return the name of the token.
   */
  function name() public view returns(string memory) {
    return _name;
  }

  /**
   * @return the symbol of the token.
   */
  function symbol() public view returns(string memory) {
    return _symbol;
  }

  /**
   * @return the number of decimals of the token.
   */
  function decimals() public view returns(uint8) {
    return _decimals;
  }

  /**
  * @dev Total number of tokens in existence
  */
  function totalSupply() public view returns (uint256) {
    return _totalSupply;
  }

  /**
  * @dev Gets the balance of the specified address.
  * @param owner The address to query the balance of.
  * @return An uint256 representing the amount owned by the passed address.
  */
  function balanceOf(address owner) public view returns (uint256) {
    return _balances[owner];
  }

  /**
   * @dev Function to check the amount of tokens that an owner allowed to a spender.
   * @param owner address The address which owns the funds.
   * @param spender address The address which will spend the funds.
   * @return A uint256 specifying the amount of tokens still available for the spender.
   */
  function allowance(
    address owner,
    address spender
  )
  public
  view
  returns (uint256)
  {
    return _allowed[owner][spender];
  }

  /**
  * @dev Transfer token for a specified address
  * @param to The address to transfer to.
  * @param value The amount to be transferred.
  */
  function transfer(address to, uint256 value) public returns (bool) {
    _transfer(msg.sender, to, value);
    return true;
  }

  /**
   * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
   * Beware that changing an allowance with this method brings the risk that someone may use both the old
   * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
   * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
   * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
   * @param spender The address which will spend the funds.
   * @param value The amount of tokens to be spent.
   */
  function approve(address spender, uint256 value) public returns (bool) {
    require(spender != address(0));

    _allowed[msg.sender][spender] = value;
    emit Approval(msg.sender, spender, value);
    return true;
  }

  /**
   * @dev Transfer tokens from one address to another
   * @param from address The address which you want to send tokens from
   * @param to address The address which you want to transfer to
   * @param value uint256 the amount of tokens to be transferred
   */
  function transferFrom(
    address from,
    address to,
    uint256 value
  )
  public
  returns (bool)
  {
    require(value <= _allowed[from][msg.sender]);

    _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);
    _transfer(from, to, value);
    return true;
  }

  /**
   * @dev Increase the amount of tokens that an owner allowed to a spender.
   * approve should be called when allowed_[_spender] == 0. To increment
   * allowed value is better to use this function to avoid 2 calls (and wait until
   * the first transaction is mined)
   * From MonolithDAO Token.sol
   * @param spender The address which will spend the funds.
   * @param addedValue The amount of tokens to increase the allowance by.
   */
  function increaseAllowance(
    address spender,
    uint256 addedValue
  )
  public
  returns (bool)
  {
    require(spender != address(0));

    _allowed[msg.sender][spender] = (
    _allowed[msg.sender][spender].add(addedValue));
    emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
    return true;
  }

  /**
   * @dev Decrease the amount of tokens that an owner allowed to a spender.
   * approve should be called when allowed_[_spender] == 0. To decrement
   * allowed value is better to use this function to avoid 2 calls (and wait until
   * the first transaction is mined)
   * From MonolithDAO Token.sol
   * @param spender The address which will spend the funds.
   * @param subtractedValue The amount of tokens to decrease the allowance by.
   */
  function decreaseAllowance(
    address spender,
    uint256 subtractedValue
  )
  public
  returns (bool)
  {
    require(spender != address(0));

    _allowed[msg.sender][spender] = (
    _allowed[msg.sender][spender].sub(subtractedValue));
    emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
    return true;
  }

  /**
  * @dev Transfer token for a specified addresses
  * @param from The address to transfer from.
  * @param to The address to transfer to.
  * @param value The amount to be transferred.
  */
  function _transfer(address from, address to, uint256 value) internal {
    require(value <= _balances[from]);
    require(to != address(0));

    _balances[from] = _balances[from].sub(value);
    _balances[to] = _balances[to].add(value);
    emit Transfer(from, to, value);
  }

  /**
   * @dev Internal function that mints an amount of the token and assigns it to
   * an account. This encapsulates the modification of balances such that the
   * proper events are emitted.
   * @param account The account that will receive the created tokens.
   * @param value The amount that will be created.
   */
  function _mint(address account, uint256 value) internal {
    require(account != address(0));
    _totalSupply = _totalSupply.add(value);
    _balances[account] = _balances[account].add(value);
    emit Transfer(address(0), account, value);
  }

  /**
   * @dev Internal function that burns an amount of the token of a given
   * account.
   * @param account The account whose tokens will be burnt.
   * @param value The amount that will be burnt.
   */
  function _burn(address account, uint256 value) internal {
    require(account != address(0));
    require(value <= _balances[account]);

    _totalSupply = _totalSupply.sub(value);
    _balances[account] = _balances[account].sub(value);
    emit Transfer(account, address(0), value);
  }

  /**
   * @dev Internal function that burns an amount of the token of a given
   * account, deducting from the sender's allowance for said account. Uses the
   * internal burn function.
   * @param account The account whose tokens will be burnt.
   * @param value The amount that will be burnt.
   */
  function _burnFrom(address account, uint256 value) internal {
    require(value <= _allowed[account][msg.sender]);

    // Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
    // this function needs to emit an event with the updated approval.
    _allowed[account][msg.sender] = _allowed[account][msg.sender].sub(
      value);
    _burn(account, value);
  }
}

Install the OpenZeppelin Contracts Ethereum Package (used by ERC20Standard.sol)

npm i @openzeppelin/contracts-ethereum-package

Deploy LegacyToken

Use truffle console to deploy LegacyToken to Kovan (can use any network)

$ npx truffle console --network kovan
truffle(kovan)> compile

Compiling your contracts...
===========================
> Compiling ./contracts/LegacyToken.sol
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/lib/ERC20Standard.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol
> Artifacts written to /mnt/c/Users/andre/Documents/projects/forum/tokenupgrade/build/contracts
> Compiled successfully using:
   - solc: 0.5.11+commit.c082d0b4.Emscripten.clang

truffle(kovan)> owner = (await web3.eth.getAccounts())[1]
truffle(kovan)> legacyToken = await LegacyToken.new({ from: owner })
truffle(kovan)> legacyToken.address
'0x2Fa8e999Ef74099Ac4C16eBbfFB410059BCe3De9'

Setup OpenZeppelin SDK

In a new terminal setup project with OpenZeppelin SDK

npm i @openzeppelin/cli
npx openzeppelin init

ModernToken.sol

Create ModernToken in contracts directory

pragma solidity ^0.5.2;

import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/drafts/ERC20Migrator.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Mintable.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Detailed.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol";

contract ModernToken is Initializable, ERC20Detailed, ERC20Mintable, Ownable {
    /**
     * @dev Initialization function.
     * @dev This function will initialize the new upgradeable ERC20 contract
     * and will set up the ERC20 migrator.
     */
    function initialize(ERC20Detailed legacyToken, ERC20Migrator _migrator ) initializer public {
        ERC20Mintable.initialize(address(_migrator)); // ERC20 implementation
        ERC20Detailed.initialize(legacyToken.name(), legacyToken.symbol(), legacyToken.decimals());
        Ownable.initialize(msg.sender);
    }
}

Link OpenZeppelin Contracts - Ethereum Package

npx openzeppelin link @openzeppelin/contracts-ethereum-package

Create ERC20Migrator and ModernToken

Create ERC20Migrator on Kovan network and call initialize(legacyToken: address) function

$ npx openzeppelin create ERC20Migrator
✓ Compiling contracts with Truffle, using settings from truffle.js file
Truffle output:

Compiling your contracts...
===========================
> Compiling ./contracts/LegacyToken.sol
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/ModernToken.sol
> Compiling ./contracts/lib/ERC20Standard.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/GSN/Context.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/access/Roles.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/access/roles/MinterRole.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/drafts/ERC20Migrator.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/math/Math.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Detailed.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Mintable.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/SafeERC20.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/utils/Address.sol
> Compiling @openzeppelin/upgrades/contracts/Initializable.sol
> Artifacts written to /mnt/c/Users/andre/Documents/projects/forum/tokenupgrade/build/contracts
> Compiled successfully using:
   - solc: 0.5.11+commit.c082d0b4.Emscripten.clang


? Pick a network kovan
✓ Added contract ERC20Migrator
✓ Linked dependency @openzeppelin/contracts-ethereum-package 2.2.3
✓ Contract ERC20Migrator deployed
All contracts have been deployed
? Do you want to call a function on the instance after creating it? Yes
? Select which function initialize(legacyToken: address)
? legacyToken (address): 0x2Fa8e999Ef74099Ac4C16eBbfFB410059BCe3De9
✓ Setting everything up to create contract instances
✓ Instance created at 0x3248603b264BBC096cb8eE9014673cbB756f90BD
0x3248603b264BBC096cb8eE9014673cbB756f90BD

Create ModernToken on Kovan network and call initialize(legacyToken: address, _migrator: address) function

$ npx openzeppelin create ModernToken
✓ Compiling contracts with Truffle, using settings from truffle.js file
Truffle output:

Compiling your contracts...
===========================
> Compiling ./contracts/LegacyToken.sol
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/ModernToken.sol
> Compiling ./contracts/lib/ERC20Standard.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/GSN/Context.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/access/Roles.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/access/roles/MinterRole.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/drafts/ERC20Migrator.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/math/Math.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Detailed.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Mintable.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/SafeERC20.sol
> Compiling @openzeppelin/contracts-ethereum-package/contracts/utils/Address.sol
> Compiling @openzeppelin/upgrades/contracts/Initializable.sol
> Artifacts written to /mnt/c/Users/andre/Documents/projects/forum/tokenupgrade/build/contracts
> Compiled successfully using:
   - solc: 0.5.11+commit.c082d0b4.Emscripten.clang


? Pick a network kovan
✓ Added contract ModernToken
✓ Contract ModernToken deployed
All contracts have been deployed
? Do you want to call a function on the instance after creating it? Yes
? Select which function initialize(legacyToken: address, _migrator: address)
? legacyToken (address): 0x2Fa8e999Ef74099Ac4C16eBbfFB410059BCe3De9
? _migrator (address): 0x3248603b264BBC096cb8eE9014673cbB756f90BD
✓ Instance created at 0x9df6559Df837B366842dd54944dF44d49ee75dE5
0x9df6559Df837B366842dd54944dF44d49ee75dE5

Migrate token

$ npx truffle console --network kovan
truffle(kovan)> erc20Migrator = await ERC20Migrator.at('0x3248603b264BBC096cb8eE9014673cbB756f90BD')
truffle(kovan)> modernToken = await ModernToken.at('0x9df6559Df837B366842dd54944dF44d49ee75dE5')
truffle(kovan)> owner = (await web3.eth.getAccounts())[1]
truffle(kovan)> legacyToken = await LegacyToken.at('0x2Fa8e999Ef74099Ac4C16eBbfFB410059BCe3De9')
truffle(kovan)> erc20Migrator.beginMigration(modernToken.address, { from: owner })
truffle(kovan)> balance = await legacyToken.balanceOf(owner)
truffle(kovan)> legacyToken.approve(erc20Migrator.address, balance, { from: owner })
truffle(kovan)> erc20Migrator.migrateAll(owner, { from: owner })
truffle(kovan)> (await legacyToken.balanceOf(owner)).toString()
'0'
truffle(kovan)> (await legacyToken.balanceOf(erc20Migrator.address)).toString()
'10000000000000000000000'
truffle(kovan)> (await modernToken.balanceOf(owner, { from: owner })).toString()
'10000000000000000000000'
1 Like