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

Remove hooks from ERC20 #3838

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
50db94a
remove beforeTokenTranser and afterTokenTransfer hooks
JulissaDantes Nov 24, 2022
2e68978
Update contracts using the hooks
JulissaDantes Nov 25, 2022
bdd94d1
ERC20Snapshot changes
JulissaDantes Nov 25, 2022
e03c773
Update ERC20Snapshot mock
JulissaDantes Nov 28, 2022
616c148
Update changelog
JulissaDantes Nov 28, 2022
c2d0313
update changelog
JulissaDantes Nov 28, 2022
a81f1a5
update documentation
JulissaDantes Nov 28, 2022
caf4374
Merge branch 'next-v5.0' into refactor/erc20/mint-burn-transfer
JulissaDantes Nov 29, 2022
4f3adcb
update documentation
JulissaDantes Nov 29, 2022
bbecd36
Merge branch 'refactor/erc20/mint-burn-transfer' of https://github.co…
JulissaDantes Nov 29, 2022
054777e
Post merge fix
JulissaDantes Nov 29, 2022
b3da557
Update ERC20Votes
JulissaDantes Nov 29, 2022
27c7090
add overflow test
JulissaDantes Nov 30, 2022
b4ab7c7
Update contracts/token/ERC20/ERC20.sol
JulissaDantes Dec 2, 2022
dcbc275
Update contracts/token/ERC20/ERC20.sol
JulissaDantes Dec 2, 2022
c3350bd
Update ERC20
JulissaDantes Dec 5, 2022
8565f15
Merge branch 'refactor/erc20/mint-burn-transfer' of https://github.co…
JulissaDantes Dec 5, 2022
a56afc4
update ERC20Capped
JulissaDantes Dec 5, 2022
3ec8abf
Add format
JulissaDantes Dec 5, 2022
4213df3
update revert reason
JulissaDantes Dec 5, 2022
0fc5d3a
run lint
JulissaDantes Dec 5, 2022
975ed99
add checks for external transfer calls
JulissaDantes Dec 5, 2022
00ed155
update ERC20Test
JulissaDantes Dec 5, 2022
0d74d66
Update changelog
JulissaDantes Dec 5, 2022
7110896
Update CHANGELOG
JulissaDantes Dec 5, 2022
4e09ef8
Update CHANGELOG.md
JulissaDantes Dec 9, 2022
ed853fa
Update CHANGELOG.md
JulissaDantes Dec 9, 2022
a2bc30c
Update CHANGELOG.md
JulissaDantes Dec 9, 2022
ed66c58
update test
JulissaDantes Dec 12, 2022
5e67170
update ERC20
JulissaDantes Dec 12, 2022
fe5abf4
Handle zero address parameters
JulissaDantes Dec 12, 2022
1083de9
Update CHANGELOG.md
JulissaDantes Dec 13, 2022
a7a9ff1
Update CHANGELOG.md
JulissaDantes Dec 14, 2022
d6fdf53
Update CHANGELOG.md
JulissaDantes Dec 14, 2022
5e0688b
Update contracts/token/ERC20/ERC20.sol
JulissaDantes Dec 14, 2022
b66eb88
Update contracts/token/ERC20/ERC20.sol
JulissaDantes Dec 14, 2022
d55e714
Update contracts/token/ERC20/ERC20.sol
JulissaDantes Dec 14, 2022
8abfed7
Update unchecked reason
JulissaDantes Dec 14, 2022
278c6d0
Update contracts/token/ERC20/ERC20.sol
JulissaDantes Dec 14, 2022
56e648a
Update ERC20.sol
JulissaDantes Dec 14, 2022
2fc018e
run lint
JulissaDantes Dec 14, 2022
fa58b35
Add unchecked comment
JulissaDantes Dec 14, 2022
58a28b9
Update CHANGELOG.md
frangio Dec 14, 2022
a6ce9e1
Update CHANGELOG.md
frangio Dec 14, 2022
ee4b86a
update mock
JulissaDantes Dec 14, 2022
fc25740
Merge branch 'refactor/erc20/mint-burn-transfer' of https://github.co…
JulissaDantes Dec 14, 2022
30876ac
update docs
frangio Dec 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,30 @@
* Removed presets in favor of [OpenZeppelin Contracts Wizard](https://wizard.openzeppelin.com/).
* `TransparentUpgradeableProxy`: Removed `admin` and `implementation` getters, which were only callable by the proxy owner and thus not very useful. ([#3820](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3820))
* `ProxyAdmin`: Removed `getProxyAdmin` and `getProxyImplementation` getters. ([#3820](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3820))
* `ERC20`: Deleted `_beforeTokenTransfer` and `_afterTokenTransfer` hooks, added a new internal `_update` function for customizations, and refactored all extensions using those hooks to use `_update` instead. ([#3838](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3838))

### How to upgrade from 4.x

#### ERC20, ERC721, and ERC1155

These breaking changes will require modifications to ERC20, ERC721, and ERC1155 contracts, since the `_afterTokenTransfer` and `_beforeTokenTransfer` functions were removed. Any customization made through those hooks should now be done overriding the new `_update` function instead.

Minting and burning are implemented by `_update` and customizations should be done by overriding this function as well. `_mint` and `_burn` are no longer virtual (meaning they are not overridable) to guard against possible inconsistencies.

For example, a contract using `ERC20`'s `_beforeTokenTransfer` hook would have to be changed in the following way.

```diff
- function _beforeTokenTransfer(
+ function _update(
address from,
address to,
uint256 amount
) internal virtual override {
- super._beforeTokenTransfer(from, to, amount);
require(!condition(), "ERC20: wrong condition");
+ super._update(from, to, amount);
}
```

## Unreleased

Expand Down
144 changes: 49 additions & 95 deletions contracts/token/ERC20/ERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -216,87 +216,81 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
) internal {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_update(from, to, amount);
}

_beforeTokenTransfer(from, to, amount);

uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
/**
* @dev Transfers `amount` of tokens from `from` to `to`, or alternatively mints (or burns) if `from` (or `to`) is
* the zero address. All customizations to transfers, mints, and burns should be done by overriding this function.
*
* Emits a {Transfer} event.
*/
function _update(
address from,
address to,
uint256 amount
) internal virtual {
if (from == address(0)) {
_totalSupply += amount;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[to] += amount;
}
} else if (to == address(0)) {
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: burn amount exceeds balance");
_totalSupply -= amount;
unchecked {
// Overflow not possible: amount <= fromBalance <= totalSupply.
_balances[from] = fromBalance - amount;
}
} else {
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}
}

emit Transfer(from, to, amount);

_afterTokenTransfer(from, to, amount);
}

/** @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
/**
* @dev Creates `amount` tokens and assigns them to `account`, by transferring it from address(0).
* Relies on the `_update` mechanism
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _mint(address account, uint256 amount) internal virtual {
function _mint(address account, uint256 amount) internal {
require(account != address(0), "ERC20: mint to the zero address");

_beforeTokenTransfer(address(0), account, amount);

_totalSupply += amount;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);

_afterTokenTransfer(address(0), account, amount);
_update(address(0), account, amount);
}

/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
* @dev Destroys `amount` tokens from `account`, by transferring it to address(0).
* Relies on the `_update` mechanism.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
* NOTE: This function is not virtual, {_update} should be overridden instead
*/
function _burn(address account, uint256 amount) internal virtual {
function _burn(address account, uint256 amount) internal {
require(account != address(0), "ERC20: burn from the zero address");

_beforeTokenTransfer(account, address(0), amount);

uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount;
}

emit Transfer(account, address(0), amount);

_afterTokenTransfer(account, address(0), amount);
_update(account, address(0), amount);
}

/**
Expand Down Expand Up @@ -345,44 +339,4 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
}
}
}

/**
* @dev Hook that is called before any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* will be transferred to `to`.
* - when `from` is zero, `amount` tokens will be minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}

/**
* @dev Hook that is called after any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* has been transferred to `to`.
* - when `from` is zero, `amount` tokens have been minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens have been burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
}
15 changes: 11 additions & 4 deletions contracts/token/ERC20/extensions/ERC20Capped.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@ abstract contract ERC20Capped is ERC20 {
}

/**
* @dev See {ERC20-_mint}.
* @dev See {ERC20-_transfer}.
*/
function _mint(address account, uint256 amount) internal virtual override {
require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded");
super._mint(account, amount);
function _update(
address from,
address to,
uint256 amount
) internal virtual override {
if (from == address(0)) {
require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded");
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the rational for using ERC20.totalSupply() instead of totalSupply() ?

}

super._update(from, to, amount);
}
}
7 changes: 3 additions & 4 deletions contracts/token/ERC20/extensions/ERC20Pausable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@ import "../../../security/Pausable.sol";
*/
abstract contract ERC20Pausable is ERC20, Pausable {
/**
* @dev See {ERC20-_beforeTokenTransfer}.
* @dev See {ERC20-_update}.
*
* Requirements:
*
* - the contract must not be paused.
*/
function _beforeTokenTransfer(
function _update(
address from,
address to,
uint256 amount
) internal virtual override {
super._beforeTokenTransfer(from, to, amount);

require(!paused(), "ERC20Pausable: token transfer while paused");
super._update(from, to, amount);
}
}
9 changes: 4 additions & 5 deletions contracts/token/ERC20/extensions/ERC20Snapshot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,13 @@ abstract contract ERC20Snapshot is ERC20 {
return snapshotted ? value : totalSupply();
}

// Update balance and/or total supply snapshots before the values are modified. This is implemented
// in the _beforeTokenTransfer hook, which is executed for _mint, _burn, and _transfer operations.
function _beforeTokenTransfer(
// Update balance and/or total supply snapshots before the values are modified. This is executed
// for _mint, _burn, and _transfer operations.
function _update(
address from,
address to,
uint256 amount
) internal virtual override {
super._beforeTokenTransfer(from, to, amount);

if (from == address(0)) {
// mint
_updateAccountSnapshot(to);
Expand All @@ -140,6 +138,7 @@ abstract contract ERC20Snapshot is ERC20 {
_updateAccountSnapshot(from);
_updateAccountSnapshot(to);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

this part will not work properly if update from 0 to 0

super._update(from, to, amount);
}

function _valueAt(uint256 snapshotId, Snapshots storage snapshots) private view returns (bool, uint256) {
Expand Down
15 changes: 5 additions & 10 deletions contracts/token/ERC20/extensions/ERC20Votes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ abstract contract ERC20Votes is ERC20, Votes {
*
* Emits a {IVotes-DelegateVotesChanged} event.
*/
function _afterTokenTransfer(
function _update(
address from,
address to,
uint256 amount
) internal virtual override {
super._update(from, to, amount);
if (from == address(0)) {
require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes");
}
_transferVotingUnits(from, to, amount);
super._afterTokenTransfer(from, to, amount);
}

/**
Expand All @@ -64,12 +67,4 @@ abstract contract ERC20Votes is ERC20, Votes {
function _getVotingUnits(address account) internal view virtual override returns (uint256) {
return balanceOf(account);
}

/**
* @dev Snapshots the totalSupply after it has been increased.
*/
function _mint(address account, uint256 amount) internal virtual override {
super._mint(account, amount);
require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes");
}
}
13 changes: 3 additions & 10 deletions test/token/ERC20/ERC20.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ function shouldBehaveLikeERC20 (errorPrefix, initialSupply, initialHolder, recip
});

it('reverts', async function () {
await expectRevert(this.token.transferFrom(
tokenOwner, to, amount, { from: spender }), `${errorPrefix}: transfer to the zero address`,
await expectRevert(
this.token.transferFrom(tokenOwner, to, amount, { from: spender }),
`${errorPrefix}: transfer to the zero address`,
);
});
});
Expand Down Expand Up @@ -240,14 +241,6 @@ function shouldBehaveLikeERC20Transfer (errorPrefix, from, to, balance, transfer
});
});
});

describe('when the recipient is the zero address', function () {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since everything goes through the transfer, it cannot know if its coming from mint or burn, so transfer has a check to ensure that from and to cannot be the zero address, but if a user sends a mint to address zero it would received both from and to as the zero address. There is a test for that case on ln 206

Copy link
Collaborator

Choose a reason for hiding this comment

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

Line 206 is testing that you cannot mint to address 0. That is a valid test, but its separate from this. This tests that you cannot call transfer to the address 0.

Tests are checking for behavior correctness. Its ok (and I think its needed) to have multiple tests that are resolved by the same require in different context.

I think this test should have been kept.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if that is the cause of the coverage decrease.

it('reverts', async function () {
await expectRevert(transfer.call(this, from, ZERO_ADDRESS, balance),
`${errorPrefix}: transfer to the zero address`,
);
});
});
}

function shouldBehaveLikeERC20Approve (errorPrefix, owner, spender, supply, approve) {
Expand Down
16 changes: 8 additions & 8 deletions test/token/ERC20/ERC20.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,14 @@ contract('ERC20', function (accounts) {
);
});

it('rejects overflow', async function () {
const maxUint256 = new BN('2').pow(new BN(256)).subn(1);
await expectRevert(
this.token.mint(recipient, maxUint256),
'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)',
);
});

describe('for a non zero account', function () {
beforeEach('minting', async function () {
this.receipt = await this.token.mint(recipient, amount);
Expand Down Expand Up @@ -283,14 +291,6 @@ contract('ERC20', function (accounts) {
shouldBehaveLikeERC20Transfer('ERC20', initialHolder, recipient, initialSupply, function (from, to, amount) {
return this.token.transferInternal(from, to, amount);
});

describe('when the sender is the zero address', function () {
it('reverts', async function () {
await expectRevert(this.token.transferInternal(ZERO_ADDRESS, recipient, initialSupply),
'ERC20: transfer from the zero address',
);
});
});
});

describe('_approve', function () {
Expand Down
4 changes: 2 additions & 2 deletions test/token/ERC20/extensions/ERC20FlashMint.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable */

const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants;
const { MAX_UINT256, ZERO_ADDRESS } = constants;

const ERC20FlashMintMock = artifacts.require('ERC20FlashMintMock');
const ERC3156FlashBorrowerMock = artifacts.require('ERC3156FlashBorrowerMock');
Expand Down
2 changes: 1 addition & 1 deletion test/token/ERC20/extensions/ERC20Votes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ contract('ERC20Votes', function (accounts) {
const amount = new BN('2').pow(new BN('224'));
await expectRevert(
this.token.mint(holder, amount),
"SafeCast: value doesn't fit in 224 bits",
"ERC20Votes: total supply risks overflowing votes",
);
});

Expand Down