Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ERC20 opt-in migration contract #1054

Conversation

facuspagnuolo
Copy link
Contributor

Fixes zeppelinos/zos-lib#185

🚀 Description

This PR adds a new contract to migrate ERC20 tokens with an opt-in strategy.
Additionally, I'm extracting the behavior expected of the StandardToken so we can start reusing it in different test files.

  • 📘 I've reviewed the OpenZeppelin Contributor Guidelines
  • ✅ I've added tests where applicable to test my new functionality.
  • 📖 I've made sure that my contracts are well-documented.
  • 🎨 I've run the JS/Solidity linters and fixed any issues (npm run lint:all:fix).

@facuspagnuolo facuspagnuolo force-pushed the feature/add_erc20_optin_migration_contract branch from 0d06f87 to 624cf45 Compare June 29, 2018 12:08
@facuspagnuolo facuspagnuolo force-pushed the feature/add_erc20_optin_migration_contract branch 2 times, most recently from db9db8e to 6da80e5 Compare July 12, 2018 20:54
Copy link
Contributor

@spalladino spalladino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! There are a few changes I'd require though, and left a few thoughts as well. One pending question: what's the status on the new upcoming version of StandardToken with the _mint function? Can we count on that one for the next release, or is it planned for 2.0 @frangio?

@@ -1,469 +1,11 @@
import assertRevert from '../../helpers/assertRevert';
import shouldBehaveLikeStandardToken from './behaviors/StandardToken.behavior';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a typo in behavior, it should be behaviour

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jokes aside, all other behaviour files are using british spelling; we should remain consistent with that.


contract('OptInERC20Migration', function ([_, owner, recipient, anotherAccount]) {
contract.only('OptInERC20Migration', function ([_, owner, recipient, anotherAccount]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this .only, I think it was the cause of coverage falling to about 7% :-P

@@ -56,17 +55,12 @@ contract OptInERC20Migration is StandardToken {
*/
function migrateTokenTo(address _to, uint256 _amount) public {
_mint(_to, _amount);
legacyToken.transferFrom(msg.sender, BURN_ADDRESS, _amount);
legacyToken.safeTransferFrom(msg.sender, BURN_ADDRESS, _amount);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider swapping the two calls. I couldn't find any attack vector, but I find it good practice to decrement balance before incrementing it, and not the other way around.

Copy link
Contributor Author

@facuspagnuolo facuspagnuolo Jul 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point but note that we are not decreasing and incrementing the same balance here. Given that we need to call a foreign contract, I went with the check-effects-interactions pattern instead.

@@ -1,4 +1,4 @@
import shouldBehaveLikeBurnableToken from './BurnableToken.behaviour';
import shouldBehaveLikeBurnableToken from './behaviors/BurnableToken.behaviour';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave this refactor for a different PR, otherwise we are risking a lot of conflicts while we review this one. If we do decide to keep it here, then I'd use behaviours for consistency 🇬🇧.

});

describe('migrate', function () {
describe('when the approved balance is higher or equal to the owned balance', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test to check that the balance of the owner in legacyToken is zero. This applies also to the other describe blocks. You may want to add a single shouldHaveTransferred that sums up that:

  • the balance of the owner in the new contract was updated
  • the balance of the owner in the old contract was updated
  • the balance of 0xdead in the old contract was updated
  • total supply for the new token was updated

@facuspagnuolo facuspagnuolo force-pushed the feature/add_erc20_optin_migration_contract branch 6 times, most recently from 1cd0f47 to 37083f4 Compare July 19, 2018 01:05
@facuspagnuolo
Copy link
Contributor Author

Comments addressed, please take another look @spalladino

@frangio frangio added feature New contracts, functions, or helpers. contracts Smart contract code. labels Jul 20, 2018
@frangio frangio mentioned this pull request Jul 20, 2018
Copy link
Contributor

@frangio frangio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babel has been removed (#1074) and we're no longer using ES6 modules. Please change from import statements to const xyz = require('xyz'); statements!

* @dev Migrates the total balance of the token holder to this token contract
* @dev This function will burn the old token balance and mint the same balance in the new token contract
*/
function migrate() public {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested renaming:

  • migratemigrateAll
  • migrateTokenmigrate

* @param _amount uint256 representing the amount of tokens to be migrated
* @param _to address the recipient that will receive the new minted tokens
*/
function migrateTokenTo(address _to, uint256 _amount) public {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove migrateTokenTo, and put all the migration logic in migrate. migrateTokenTo is like a migration + a transfer, and keeping those things separate sounds more simpler and correct to me.

Copy link
Contributor Author

@facuspagnuolo facuspagnuolo Jul 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm not following, do you prefer holding all the logic together in migrate or separated somehow?

/**
* @title MigratableERC20
* @dev This strategy carries out an optional migration of the token balances. This migration is performed and paid for
* @dev by the token holders. The new token contract starts with no initial supply and no balances. The only way to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the @dev tag from all but the first line here.

* @dev "mint" the new tokens is for users to "turn in" their old ones. This is done by first approving the amount they
* @dev want to migrate via `ERC20.approve(newTokenAddress, amountToMigrate)` and then calling a function of the new
* @dev token called `migrateTokens`. The old tokens are sent to a burn address, and the holder receives an equal amount
* @dev in the new contract.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These inline docs explain how the contract works but not how it's used. I'd add a note saying at least that it's an abstract contract that must be combined with StandardToken or the user's own token implementation (with a _mint internal function), and if we have a link to a guide for how to use it I'd put that here as well!

@facuspagnuolo
Copy link
Contributor Author

@frangio I had already changed all the import statements in favour of require ones. I'm not finding any imports left, please point them out in case you see one.

@facuspagnuolo facuspagnuolo force-pushed the feature/add_erc20_optin_migration_contract branch 2 times, most recently from 2263d32 to 33fa961 Compare July 27, 2018 09:17
@frangio frangio added this to the v2.0 milestone Aug 17, 2018
@frangio frangio self-assigned this Aug 29, 2018
@frangio frangio force-pushed the feature/add_erc20_optin_migration_contract branch from 37a2110 to 2b7b3e1 Compare September 7, 2018 12:49
@frangio frangio dismissed stale reviews from spalladino and themself September 7, 2018 13:10

Refactor

@frangio frangio requested a review from nventuro September 7, 2018 13:10
@frangio frangio requested a review from spalladino September 7, 2018 13:39
* `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 here forever.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider replacing here for there

* is migrated, and remain here forever.
*
* Although this contract can be used in many different scenarios, the main
* motivation was to provide a way of how an ERC20 token can be migrated to an
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider replacing a way of how an ERC20 token can be migrated to an for a way to migrate ERC20 tokens into an

}

/**
* @dev Burns a given amount of the old token contract for a token holder and mints the same amount of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider rephrasing; tokens aren't burned, but instead transfered here.

*/
function migrate(address account, uint256 amount) public {
_newToken.mint(account, amount);
_legacyToken.safeTransferFrom(account, this, amount);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd swap these two statements, for clarity (i.e. first transfer to migrator, then mint new tokens).


/**
* @dev Migrates the total balance of the token holder to this token contract
* @dev This function will burn the old token balance and mint the same balance in the new token contract
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, burn is not accurate.

});

describe('beginMigration', function () {
it('rejects zero address for token', async function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per #1157, this should read something like 'reverts with a null new token address'

});

describe('once migration began', function () {
beforeEach('', async function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the empty block description

await this.legacyToken.approve(this.migrator.address, amount, { from: owner });
});

beforeEach('migrating token', async function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This describe has two beforeEach blocks, which are not independent: I'm not sure if Mocha specifies in which order they'll be run (or even if both will be run), but we should avoid this situation IMO.

describe('migrateAll', function () {
const baseAmount = totalSupply;

describe('when the approved balance is higher or equal to the owned balance', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this only tests for equal to balance, not higher

await this.legacyToken.approve(this.migrator.address, baseAmount, { from: owner });
});

describe('when the amount is lower or equal to the one approved', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only tests for equal

spalladino
spalladino previously approved these changes Sep 7, 2018
Copy link
Contributor

@spalladino spalladino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to merge. I added several notes on comments and the tests, but nothing too important, and can be reviewed later. The code itself is solid. The only thing I'd consider before merging is whether we want to allow migrations with amount=zero (not sure if I'm for or against this, just throwing it out for consideration).


/**
* @title ERC20Migrator
* @dev This contract can be used to to migrate an ERC20 token from one
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to to migrate

ERC20Mintable private _newToken;

/**
* @dev Constructor function. It initializes the new token contract
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It initializes the new token contract

legacy

_legacyToken = legacyToken;
}

function beginMigration(ERC20Mintable newToken) public {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing natspec docs in this function

* 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
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add an example here of how to set up the ERC20 migration. Something like:

const migrator = await ERC20Migrator.new(legacyToken.address);
await newToken.addMinter(migrator.address);
await migrator.beginMigration(newToken.address);

* @param account uint256 representing the amount of tokens to be migrated
* @param amount address the recipient that will receive the new minted tokens
*/
function migrate(address account, uint256 amount) public {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we require that amount > 0? Just asking

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The (soft) policy we currently have is that no-op transactions (barring events being emitted) are allowed, to prevent burdening the caller with the responsibility of checking for non-zero.

describe('migrateAll', function () {
const baseAmount = totalSupply;

describe('when the approved balance is higher or equal to the owned balance', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this block is only testing approved balance equal to the owned balance, not higher to. There is another block missing, where approved is amount + 1.

await this.legacyToken.approve(this.migrator.address, baseAmount, { from: owner });
});

describe('when the amount is lower or equal to the one approved', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, it's testing equal only

it('updates the total supply', async function () {
const currentSupply = await this.newToken.totalSupply();
currentSupply.should.be.bignumber.equal(amount);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three tests (mints amount, burns, updates) are repeated here and in the previous block. I'd add a helper that is parameterized with amount and runs the three tests, to avoid code duplication.

.use(require('chai-bignumber')(BigNumber))
.should();

contract('ERC20Migrator', function ([_, owner, recipient, anotherAccount]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a test for two subsequent migrations of the same account

*/
function migrate(address account, uint256 amount) public {
_newToken.mint(account, amount);
_legacyToken.safeTransferFrom(account, this, amount);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since minting happens before transferring, there could be some odd behaviours if the legacy and new token are the same (you could migrate a near-infinite amount). But I'm not sure that's a scenario we even want to consider.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I guess we could make sure legacyToken and newToken are not the same contract?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if there is any point in it, as one could be a proxy, so you have no way to compare. I mentioned it just because it's generally a good practice to decrement (ie transfer) before incrementing (ie minting).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that, but thought there was value in preventing a simple user error.

…y into feature/add_erc20_optin_migration_contract
@frangio
Copy link
Contributor

frangio commented Sep 7, 2018

Tests passed locally.

@frangio frangio merged commit 92133be into OpenZeppelin:master Sep 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contracts Smart contract code. feature New contracts, functions, or helpers.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Explore new models to support legacy tokens onboarding mechanisms
4 participants