-
Notifications
You must be signed in to change notification settings - Fork 11.9k
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
Add ERC20 opt-in migration contract #1054
Conversation
0d06f87
to
624cf45
Compare
db9db8e
to
6da80e5
Compare
There was a problem hiding this 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'; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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]) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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 () { |
There was a problem hiding this comment.
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
1cd0f47
to
37083f4
Compare
Comments addressed, please take another look @spalladino |
There was a problem hiding this 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested renaming:
migrate
→migrateAll
migrateToken
→migrate
* @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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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!
@frangio I had already changed all the |
2263d32
to
33fa961
Compare
…_optin_migration_contract
37a2110
to
2b7b3e1
Compare
contracts/drafts/ERC20Migrator.sol
Outdated
* `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. |
There was a problem hiding this comment.
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
contracts/drafts/ERC20Migrator.sol
Outdated
* 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 |
There was a problem hiding this comment.
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
contracts/drafts/ERC20Migrator.sol
Outdated
} | ||
|
||
/** | ||
* @dev Burns a given amount of the old token contract for a token holder and mints the same amount of |
There was a problem hiding this comment.
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.
contracts/drafts/ERC20Migrator.sol
Outdated
*/ | ||
function migrate(address account, uint256 amount) public { | ||
_newToken.mint(account, amount); | ||
_legacyToken.safeTransferFrom(account, this, amount); |
There was a problem hiding this comment.
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).
contracts/drafts/ERC20Migrator.sol
Outdated
|
||
/** | ||
* @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 |
There was a problem hiding this comment.
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.
test/drafts/ERC20Migrator.test.js
Outdated
}); | ||
|
||
describe('beginMigration', function () { | ||
it('rejects zero address for token', async function () { |
There was a problem hiding this comment.
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'
test/drafts/ERC20Migrator.test.js
Outdated
}); | ||
|
||
describe('once migration began', function () { | ||
beforeEach('', async function () { |
There was a problem hiding this comment.
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
test/drafts/ERC20Migrator.test.js
Outdated
await this.legacyToken.approve(this.migrator.address, amount, { from: owner }); | ||
}); | ||
|
||
beforeEach('migrating token', async function () { |
There was a problem hiding this comment.
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.
test/drafts/ERC20Migrator.test.js
Outdated
describe('migrateAll', function () { | ||
const baseAmount = totalSupply; | ||
|
||
describe('when the approved balance is higher or equal to the owned balance', function () { |
There was a problem hiding this comment.
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
test/drafts/ERC20Migrator.test.js
Outdated
await this.legacyToken.approve(this.migrator.address, baseAmount, { from: owner }); | ||
}); | ||
|
||
describe('when the amount is lower or equal to the one approved', function () { |
There was a problem hiding this comment.
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
There was a problem hiding this 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).
contracts/drafts/ERC20Migrator.sol
Outdated
|
||
/** | ||
* @title ERC20Migrator | ||
* @dev This contract can be used to to migrate an ERC20 token from one |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to to migrate
contracts/drafts/ERC20Migrator.sol
Outdated
ERC20Mintable private _newToken; | ||
|
||
/** | ||
* @dev Constructor function. It initializes the new token contract |
There was a problem hiding this comment.
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
contracts/drafts/ERC20Migrator.sol
Outdated
_legacyToken = legacyToken; | ||
} | ||
|
||
function beginMigration(ERC20Mintable newToken) public { |
There was a problem hiding this comment.
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
contracts/drafts/ERC20Migrator.sol
Outdated
* 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 | ||
*/ |
There was a problem hiding this comment.
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);
contracts/drafts/ERC20Migrator.sol
Outdated
* @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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
test/drafts/ERC20Migrator.test.js
Outdated
describe('migrateAll', function () { | ||
const baseAmount = totalSupply; | ||
|
||
describe('when the approved balance is higher or equal to the owned balance', function () { |
There was a problem hiding this comment.
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
.
test/drafts/ERC20Migrator.test.js
Outdated
await this.legacyToken.approve(this.migrator.address, baseAmount, { from: owner }); | ||
}); | ||
|
||
describe('when the amount is lower or equal to the one approved', function () { |
There was a problem hiding this comment.
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
test/drafts/ERC20Migrator.test.js
Outdated
it('updates the total supply', async function () { | ||
const currentSupply = await this.newToken.totalSupply(); | ||
currentSupply.should.be.bignumber.equal(amount); | ||
}); |
There was a problem hiding this comment.
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]) { |
There was a problem hiding this comment.
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
contracts/drafts/ERC20Migrator.sol
Outdated
*/ | ||
function migrate(address account, uint256 amount) public { | ||
_newToken.mint(account, amount); | ||
_legacyToken.safeTransferFrom(account, this, amount); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
Tests passed locally. |
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.npm run lint:all:fix
).