Add ERC20 opt-in migration contract (#1054)
* Extract standard token behaviuor to reuse it in other tests * Add opt in ERC20 migration contract * Make migration contract not to depend from standard token * Changes based on feedback * Improve MigratableERC20 inline documentation * move behaviors to behaviors directory * refactor MigratableERC20 into ERC20Migrator * fix errors * change expectEvent to support multiple events with same name * fix tests * update documentation * rename MigratableERC20 files to ERC20Migrator * move to drafts * test beginMigration * rename to ERC20Migrator * missing semicolon (╯°□°)╯︵ ┻━┻ * add non-zero check * improve documentation based on review comments * improve test descriptions * improve docs * add getters * fix contract * improve testspull/1311/head
parent
4b33eaefa2
commit
92133be7ea
@ -0,0 +1,101 @@ |
||||
pragma solidity ^0.4.24; |
||||
|
||||
import "../token/ERC20/IERC20.sol"; |
||||
import "../token/ERC20/ERC20Mintable.sol"; |
||||
import "../token/ERC20/SafeERC20.sol"; |
||||
import "../math/Math.sol"; |
||||
|
||||
|
||||
/** |
||||
* @title ERC20Migrator |
||||
* @dev This contract can be used to migrate an ERC20 token from one |
||||
* contract to another, where each token holder has to opt-in to the migration. |
||||
* To opt-in, users must approve for this contract the number of tokens they |
||||
* want to migrate. Once the allowance is set up, anyone can trigger the |
||||
* migration to the new token contract. In this way, token holders "turn in" |
||||
* their old balance and will be minted an equal amount in the new token. |
||||
* The new token contract must be mintable. For the precise interface refer to |
||||
* OpenZeppelin's ERC20Mintable, but the only functions that are needed are |
||||
* `isMinter(address)` and `mint(address, amount)`. The migrator will check |
||||
* that it is a minter for the token. |
||||
* The balance from the legacy token will be transfered to the migrator, as it |
||||
* is migrated, and remain there forever. |
||||
* Although this contract can be used in many different scenarios, the main |
||||
* motivation was to provide a way to migrate ERC20 tokens into an upgradeable |
||||
* version of it using ZeppelinOS. To read more about how this can be done |
||||
* using this implementation, please follow the official documentation site of |
||||
* ZeppelinOS: https://docs.zeppelinos.org/docs/erc20_onboarding.html |
||||
* Example of usage: |
||||
* ``` |
||||
* const migrator = await ERC20Migrator.new(legacyToken.address); |
||||
* await newToken.addMinter(migrator.address); |
||||
* await migrator.beginMigration(newToken.address); |
||||
* ``` |
||||
*/ |
||||
contract ERC20Migrator { |
||||
using SafeERC20 for IERC20; |
||||
|
||||
/// Address of the old token contract |
||||
IERC20 private _legacyToken; |
||||
|
||||
/// Address of the new token contract |
||||
ERC20Mintable private _newToken; |
||||
|
||||
/** |
||||
* @param legacyToken address of the old token contract |
||||
*/ |
||||
constructor(IERC20 legacyToken) public { |
||||
require(legacyToken != address(0)); |
||||
_legacyToken = legacyToken; |
||||
} |
||||
|
||||
/** |
||||
* @dev Returns the legacy token that is being migrated. |
||||
*/ |
||||
function legacyToken() public view returns (IERC20) { |
||||
return _legacyToken; |
||||
} |
||||
|
||||
/** |
||||
* @dev Returns the new token to which we are migrating. |
||||
*/ |
||||
function newToken() public view returns (IERC20) { |
||||
return _newToken; |
||||
} |
||||
|
||||
/** |
||||
* @dev Begins the migration by setting which is the new token that will be |
||||
* minted. This contract must be a minter for the new token. |
||||
* @param newToken the token that will be minted |
||||
*/ |
||||
function beginMigration(ERC20Mintable newToken) public { |
||||
require(_newToken == address(0)); |
||||
require(newToken != address(0)); |
||||
require(newToken.isMinter(this)); |
||||
|
||||
_newToken = newToken; |
||||
} |
||||
|
||||
/** |
||||
* @dev Transfers part of an account's balance in the old token to this |
||||
* contract, and mints the same amount of new tokens for that account. |
||||
* @param account whose tokens will be migrated |
||||
* @param amount amount of tokens to be migrated |
||||
*/ |
||||
function migrate(address account, uint256 amount) public { |
||||
_legacyToken.safeTransferFrom(account, this, amount); |
||||
_newToken.mint(account, amount); |
||||
} |
||||
|
||||
/** |
||||
* @dev Transfers all of an account's allowed balance in the old token to |
||||
* this contract, and mints the same amount of new tokens for that account. |
||||
* @param account whose tokens will be migrated |
||||
*/ |
||||
function migrateAll(address account) public { |
||||
uint256 balance = _legacyToken.balanceOf(account); |
||||
uint256 allowance = _legacyToken.allowance(account, this); |
||||
uint256 amount = Math.min(balance, allowance); |
||||
migrate(account, amount); |
||||
} |
||||
} |
@ -0,0 +1,156 @@ |
||||
const { assertRevert } = require('../helpers/assertRevert'); |
||||
|
||||
const ERC20Mock = artifacts.require('ERC20Mock'); |
||||
const ERC20Mintable = artifacts.require('ERC20Mintable'); |
||||
const ERC20Migrator = artifacts.require('ERC20Migrator'); |
||||
|
||||
const BigNumber = web3.eth.BigNumber; |
||||
|
||||
require('chai') |
||||
.use(require('chai-bignumber')(BigNumber)) |
||||
.should(); |
||||
|
||||
contract('ERC20Migrator', function ([_, owner, recipient, anotherAccount]) { |
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; |
||||
|
||||
const totalSupply = 200; |
||||
|
||||
it('reverts with a null legacy token address', async function () { |
||||
await assertRevert(ERC20Migrator.new(ZERO_ADDRESS)); |
||||
}); |
||||
|
||||
describe('with tokens and migrator', function () { |
||||
beforeEach('deploying tokens and migrator', async function () { |
||||
this.legacyToken = await ERC20Mock.new(owner, totalSupply); |
||||
this.migrator = await ERC20Migrator.new(this.legacyToken.address); |
||||
this.newToken = await ERC20Mintable.new(); |
||||
}); |
||||
|
||||
it('returns legacy token', async function () { |
||||
(await this.migrator.legacyToken()).should.be.equal(this.legacyToken.address); |
||||
}); |
||||
|
||||
describe('beginMigration', function () { |
||||
it('reverts with a null new token address', async function () { |
||||
await assertRevert(this.migrator.beginMigration(ZERO_ADDRESS)); |
||||
}); |
||||
|
||||
it('reverts if not a minter of the token', async function () { |
||||
await assertRevert(this.migrator.beginMigration(this.newToken.address)); |
||||
}); |
||||
|
||||
it('succeeds if it is a minter of the token', async function () { |
||||
await this.newToken.addMinter(this.migrator.address); |
||||
await this.migrator.beginMigration(this.newToken.address); |
||||
}); |
||||
|
||||
it('reverts the second time it is called', async function () { |
||||
await this.newToken.addMinter(this.migrator.address); |
||||
await this.migrator.beginMigration(this.newToken.address); |
||||
await assertRevert(this.migrator.beginMigration(this.newToken.address)); |
||||
}); |
||||
}); |
||||
|
||||
describe('once migration began', function () { |
||||
beforeEach('beginning migration', async function () { |
||||
await this.newToken.addMinter(this.migrator.address); |
||||
await this.migrator.beginMigration(this.newToken.address); |
||||
}); |
||||
|
||||
it('returns new token', async function () { |
||||
(await this.migrator.newToken()).should.be.equal(this.newToken.address); |
||||
}); |
||||
|
||||
describe('migrateAll', function () { |
||||
const baseAmount = totalSupply; |
||||
|
||||
describe('when the approved balance is equal to the owned balance', function () { |
||||
const amount = baseAmount; |
||||
|
||||
beforeEach('approving the whole balance to the new contract', async function () { |
||||
await this.legacyToken.approve(this.migrator.address, amount, { from: owner }); |
||||
}); |
||||
|
||||
beforeEach('migrating token', async function () { |
||||
const tx = await this.migrator.migrateAll(owner); |
||||
this.logs = tx.receipt.logs; |
||||
}); |
||||
|
||||
it('mints the same balance of the new token', async function () { |
||||
const currentBalance = await this.newToken.balanceOf(owner); |
||||
currentBalance.should.be.bignumber.equal(amount); |
||||
}); |
||||
|
||||
it('burns a given amount of old tokens', async function () { |
||||
const currentBurnedBalance = await this.legacyToken.balanceOf(this.migrator.address); |
||||
currentBurnedBalance.should.be.bignumber.equal(amount); |
||||
|
||||
const currentLegacyTokenBalance = await this.legacyToken.balanceOf(owner); |
||||
currentLegacyTokenBalance.should.be.bignumber.equal(0); |
||||
}); |
||||
|
||||
it('updates the total supply', async function () { |
||||
const currentSupply = await this.newToken.totalSupply(); |
||||
currentSupply.should.be.bignumber.equal(amount); |
||||
}); |
||||
}); |
||||
|
||||
describe('when the approved balance is lower than the owned balance', function () { |
||||
const amount = baseAmount - 1; |
||||
|
||||
beforeEach('approving part of the balance to the new contract', async function () { |
||||
await this.legacyToken.approve(this.migrator.address, amount, { from: owner }); |
||||
await this.migrator.migrateAll(owner); |
||||
}); |
||||
|
||||
it('migrates only approved amount', async function () { |
||||
const currentBalance = await this.newToken.balanceOf(owner); |
||||
currentBalance.should.be.bignumber.equal(amount); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('migrate', function () { |
||||
const baseAmount = 50; |
||||
|
||||
beforeEach('approving tokens to the new contract', async function () { |
||||
await this.legacyToken.approve(this.migrator.address, baseAmount, { from: owner }); |
||||
}); |
||||
|
||||
describe('when the amount is equal to the one approved', function () { |
||||
const amount = baseAmount; |
||||
|
||||
beforeEach('migrate token', async function () { |
||||
({ logs: this.logs } = await this.migrator.migrate(owner, amount)); |
||||
}); |
||||
|
||||
it('mints that amount of the new token', async function () { |
||||
const currentBalance = await this.newToken.balanceOf(owner); |
||||
currentBalance.should.be.bignumber.equal(amount); |
||||
}); |
||||
|
||||
it('burns a given amount of old tokens', async function () { |
||||
const currentBurnedBalance = await this.legacyToken.balanceOf(this.migrator.address); |
||||
currentBurnedBalance.should.be.bignumber.equal(amount); |
||||
|
||||
const currentLegacyTokenBalance = await this.legacyToken.balanceOf(owner); |
||||
currentLegacyTokenBalance.should.be.bignumber.equal(totalSupply - amount); |
||||
}); |
||||
|
||||
it('updates the total supply', async function () { |
||||
const currentSupply = await this.newToken.totalSupply(); |
||||
currentSupply.should.be.bignumber.equal(amount); |
||||
}); |
||||
}); |
||||
|
||||
describe('when the given amount is higher than the one approved', function () { |
||||
const amount = baseAmount + 1; |
||||
|
||||
it('reverts', async function () { |
||||
await assertRevert(this.migrator.migrate(owner, amount)); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -1,5 +1,5 @@ |
||||
const { assertRevert } = require('../../helpers/assertRevert'); |
||||
const expectEvent = require('../../helpers/expectEvent'); |
||||
const { assertRevert } = require('../../../helpers/assertRevert'); |
||||
const expectEvent = require('../../../helpers/expectEvent'); |
||||
|
||||
const BigNumber = web3.BigNumber; |
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; |
@ -1,4 +1,5 @@ |
||||
const { expectThrow } = require('../../helpers/expectThrow'); |
||||
const { expectThrow } = require('../../../helpers/expectThrow'); |
||||
const expectEvent = require('../../../helpers/expectEvent'); |
||||
|
||||
const BigNumber = web3.BigNumber; |
||||
|
@ -1,5 +1,5 @@ |
||||
const { assertRevert } = require('../../helpers/assertRevert'); |
||||
const expectEvent = require('../../helpers/expectEvent'); |
||||
const { assertRevert } = require('../../../helpers/assertRevert'); |
||||
const expectEvent = require('../../../helpers/expectEvent'); |
||||
|
||||
const BigNumber = web3.BigNumber; |
||||
|
Loading…
Reference in new issue