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 13 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* 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, and refactor all extensions using those hooks for customization, by using `transfer` instead. ([#3838](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3838))
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved

## Unreleased

Expand Down
4 changes: 2 additions & 2 deletions contracts/mocks/ERC20SnapshotMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ contract ERC20SnapshotMock is ERC20Snapshot {
}

function mint(address account, uint256 amount) public {
_mint(account, amount);
_transfer(address(0), account, amount);
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
}

function burn(address account, uint256 amount) public {
_burn(account, amount);
_transfer(account, address(0), amount);
}
}
4 changes: 2 additions & 2 deletions contracts/mocks/ERC20VotesMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ contract ERC20VotesMock is ERC20Votes {
constructor(string memory name, string memory symbol) ERC20(name, symbol) EIP712(name, "1") {}

function mint(address account, uint256 amount) public {
_mint(account, amount);
_transfer(address(0), account, amount);
}

function burn(address account, uint256 amount) public {
_burn(account, amount);
_transfer(account, address(0), amount);
}

function getChainId() external view returns (uint256) {
Expand Down
124 changes: 29 additions & 95 deletions contracts/token/ERC20/ERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -214,89 +214,63 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
* Will mint (or burn) if `from` (or `to`) is address(0).
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
*
* Requirements:
* Emits a {Transfer} event.
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
*/
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");

_beforeTokenTransfer(from, to, amount);
require(from != address(0) || to != address(0), "ERC20: invalid transfer operation");
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved

uint256 fromBalance = _balances[from];
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
frangio marked this conversation as resolved.
Show resolved Hide resolved
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.
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)) {
require(fromBalance >= amount, "ERC20: burn amount exceeds balance");
_totalSupply -= amount;
unchecked {
_balances[from] -= amount;
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
_balances[from] -= amount;
_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 `_transfer` mechanism
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
* Note: this function is not virtual, {_transfer} should be overridden instead.
*
* - `account` cannot be the zero address.
*/
function _mint(address account, uint256 amount) internal virtual {
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);
function _mint(address account, uint256 amount) internal {
_transfer(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 `_transfer` mechanism.
*
* Emits a {Transfer} event with `to` set to the zero address.
* Note: this function is not virtual, {_transfer} should be overridden instead
*
* Requirements:
* Emits a {Transfer} event with `to` set to the zero address.
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint256 amount) internal virtual {
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);
function _burn(address account, uint256 amount) internal {
_transfer(account, address(0), amount);
}

/**
Expand Down Expand Up @@ -345,44 +319,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 {}
}
10 changes: 7 additions & 3 deletions contracts/token/ERC20/extensions/ERC20Capped.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ abstract contract ERC20Capped is ERC20 {
}

/**
* @dev See {ERC20-_mint}.
* @dev See {ERC20-_transfer}.
*/
function _mint(address account, uint256 amount) internal virtual override {
function _transfer(
address,
address account,
uint256 amount
) internal virtual override {
require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded");
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
super._mint(account, amount);
super._transfer(address(0), account, amount);
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
}
}
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-_transfer}.
*
* Requirements:
*
* - the contract must not be paused.
*/
function _beforeTokenTransfer(
function _transfer(
address from,
address to,
uint256 amount
) internal virtual override {
super._beforeTokenTransfer(from, to, amount);

require(!paused(), "ERC20Pausable: token transfer while paused");
super._transfer(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 _transfer(
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._transfer(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 _transfer(
address from,
address to,
uint256 amount
) internal virtual override {
super._transfer(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");
}
}
23 changes: 0 additions & 23 deletions test/token/ERC20/ERC20.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,21 +148,6 @@ function shouldBehaveLikeERC20 (errorPrefix, initialSupply, initialHolder, recip
});
});
});

describe('when the recipient is the zero address', function () {
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
const amount = initialSupply;
const to = ZERO_ADDRESS;

beforeEach(async function () {
await this.token.approve(spender, amount, { from: tokenOwner });
});

it('reverts', async function () {
await expectRevert(this.token.transferFrom(
tokenOwner, to, amount, { from: spender }), `${errorPrefix}: transfer to the zero address`,
);
});
});
});

describe('when the token owner is the zero address', function () {
Expand Down Expand Up @@ -240,14 +225,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
19 changes: 9 additions & 10 deletions test/token/ERC20/ERC20.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,14 @@ contract('ERC20', function (accounts) {
const amount = new BN(50);
it('rejects a null account', async function () {
await expectRevert(
this.token.mint(ZERO_ADDRESS, amount), 'ERC20: mint to the zero address',
this.token.mint(ZERO_ADDRESS, amount), 'ERC20: invalid transfer operation',
);
});

it('rejects overflow', async function () {
const maxUint256 = new BN('2').pow(new BN(256)).subn(1);
await expectRevert.unspecified(
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
this.token.mint(recipient, maxUint256),
);
});

Expand Down Expand Up @@ -236,7 +243,7 @@ contract('ERC20', function (accounts) {
describe('_burn', function () {
it('rejects a null account', async function () {
await expectRevert(this.token.burn(ZERO_ADDRESS, new BN(1)),
'ERC20: burn from the zero address');
'ERC20: invalid transfer operation');
});

describe('for a non zero account', function () {
Expand Down Expand Up @@ -283,14 +290,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