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'