Skip to content

Commit 41aa39a

Browse files
authored
Add no-return-data ERC20 support to SafeERC20. (#1655)
* Add no-return-data ERC20 support to SafeERC20. * Add changelog entry. * Replace abi.encodeWithSignature for encodeWithSelector. * Remove SafeERC20 test code duplication. * Replace assembly for abi.decode. * Fix linter errors.
1 parent 0dded49 commit 41aa39a

File tree

4 files changed

+136
-88
lines changed

4 files changed

+136
-88
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* `ERC20`: added internal `_approve(address owner, address spender, uint256 value)`, allowing derived contracts to set the allowance of arbitrary accounts. ([#1609](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1609))
77
* `ERC20Metadata`: added internal `_setTokenURI(string memory tokenURI)`. ([#1618](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1618))
88
* `ERC20Snapshot`: create snapshots on demand of the token balances and total supply, to later retrieve and e.g. calculate dividends at a past time. ([#1617](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1617))
9+
* `SafeERC20`: `ERC20` contracts with no return value (i.e. that revert on failure) are now supported. ([#1655](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/))
910

1011
### Improvements:
1112
* Upgraded the minimum compiler version to v0.5.2: this removes many Solidity warnings that were false positives. ([#1606](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1606))

contracts/mocks/SafeERC20Helper.sol

+37-31
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.5.2;
33
import "../token/ERC20/IERC20.sol";
44
import "../token/ERC20/SafeERC20.sol";
55

6-
contract ERC20FailingMock {
6+
contract ERC20ReturnFalseMock {
77
uint256 private _allowance;
88

99
// IERC20's functions are not pure, but these mock implementations are: to prevent Solidity from issuing warnings,
@@ -31,7 +31,7 @@ contract ERC20FailingMock {
3131
}
3232
}
3333

34-
contract ERC20SucceedingMock {
34+
contract ERC20ReturnTrueMock {
3535
mapping (address => uint256) private _allowances;
3636

3737
// IERC20's functions are not pure, but these mock implementations are: to prevent Solidity from issuing warnings,
@@ -62,62 +62,68 @@ contract ERC20SucceedingMock {
6262
}
6363
}
6464

65-
contract SafeERC20Helper {
66-
using SafeERC20 for IERC20;
65+
contract ERC20NoReturnMock {
66+
mapping (address => uint256) private _allowances;
6767

68-
IERC20 private _failing;
69-
IERC20 private _succeeding;
68+
// IERC20's functions are not pure, but these mock implementations are: to prevent Solidity from issuing warnings,
69+
// we write to a dummy state variable.
70+
uint256 private _dummy;
7071

71-
constructor () public {
72-
_failing = IERC20(address(new ERC20FailingMock()));
73-
_succeeding = IERC20(address(new ERC20SucceedingMock()));
72+
function transfer(address, uint256) public {
73+
_dummy = 0;
7474
}
7575

76-
function doFailingTransfer() public {
77-
_failing.safeTransfer(address(0), 0);
76+
function transferFrom(address, address, uint256) public {
77+
_dummy = 0;
7878
}
7979

80-
function doFailingTransferFrom() public {
81-
_failing.safeTransferFrom(address(0), address(0), 0);
80+
function approve(address, uint256) public {
81+
_dummy = 0;
8282
}
8383

84-
function doFailingApprove() public {
85-
_failing.safeApprove(address(0), 0);
84+
function setAllowance(uint256 allowance_) public {
85+
_allowances[msg.sender] = allowance_;
8686
}
8787

88-
function doFailingIncreaseAllowance() public {
89-
_failing.safeIncreaseAllowance(address(0), 0);
88+
function allowance(address owner, address) public view returns (uint256) {
89+
return _allowances[owner];
9090
}
91+
}
92+
93+
contract SafeERC20Wrapper {
94+
using SafeERC20 for IERC20;
95+
96+
IERC20 private _token;
9197

92-
function doFailingDecreaseAllowance() public {
93-
_failing.safeDecreaseAllowance(address(0), 0);
98+
constructor (IERC20 token) public {
99+
_token = token;
94100
}
95101

96-
function doSucceedingTransfer() public {
97-
_succeeding.safeTransfer(address(0), 0);
102+
function transfer() public {
103+
_token.safeTransfer(address(0), 0);
98104
}
99105

100-
function doSucceedingTransferFrom() public {
101-
_succeeding.safeTransferFrom(address(0), address(0), 0);
106+
function transferFrom() public {
107+
_token.safeTransferFrom(address(0), address(0), 0);
102108
}
103109

104-
function doSucceedingApprove(uint256 amount) public {
105-
_succeeding.safeApprove(address(0), amount);
110+
function approve(uint256 amount) public {
111+
_token.safeApprove(address(0), amount);
106112
}
107113

108-
function doSucceedingIncreaseAllowance(uint256 amount) public {
109-
_succeeding.safeIncreaseAllowance(address(0), amount);
114+
function increaseAllowance(uint256 amount) public {
115+
_token.safeIncreaseAllowance(address(0), amount);
110116
}
111117

112-
function doSucceedingDecreaseAllowance(uint256 amount) public {
113-
_succeeding.safeDecreaseAllowance(address(0), amount);
118+
function decreaseAllowance(uint256 amount) public {
119+
_token.safeDecreaseAllowance(address(0), amount);
114120
}
115121

116122
function setAllowance(uint256 allowance_) public {
117-
ERC20SucceedingMock(address(_succeeding)).setAllowance(allowance_);
123+
ERC20ReturnTrueMock(address(_token)).setAllowance(allowance_);
118124
}
119125

120126
function allowance() public view returns (uint256) {
121-
return _succeeding.allowance(address(0), address(0));
127+
return _token.allowance(address(0), address(0));
122128
}
123129
}

contracts/token/ERC20/SafeERC20.sol

+28-6
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,58 @@ import "../../math/SafeMath.sol";
55

66
/**
77
* @title SafeERC20
8-
* @dev Wrappers around ERC20 operations that throw on failure.
8+
* @dev Wrappers around ERC20 operations that throw on failure (when the token
9+
* contract returns false). Tokens that return no value (and instead revert or
10+
* throw on failure) are also supported, non-reverting calls are assumed to be
11+
* successful.
912
* To use this library you can add a `using SafeERC20 for ERC20;` statement to your contract,
1013
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
1114
*/
1215
library SafeERC20 {
1316
using SafeMath for uint256;
1417

1518
function safeTransfer(IERC20 token, address to, uint256 value) internal {
16-
require(token.transfer(to, value));
19+
callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
1720
}
1821

1922
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
20-
require(token.transferFrom(from, to, value));
23+
callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
2124
}
2225

2326
function safeApprove(IERC20 token, address spender, uint256 value) internal {
2427
// safeApprove should only be called when setting an initial allowance,
2528
// or when resetting it to zero. To increase and decrease it, use
2629
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
2730
require((value == 0) || (token.allowance(address(this), spender) == 0));
28-
require(token.approve(spender, value));
31+
callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
2932
}
3033

3134
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
3235
uint256 newAllowance = token.allowance(address(this), spender).add(value);
33-
require(token.approve(spender, newAllowance));
36+
callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
3437
}
3538

3639
function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
3740
uint256 newAllowance = token.allowance(address(this), spender).sub(value);
38-
require(token.approve(spender, newAllowance));
41+
callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
42+
}
43+
44+
/**
45+
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
46+
* on the return value: the return value is optional (but if data is returned, it must equal true).
47+
* @param token The token targeted by the call.
48+
* @param data The call data (encoded using abi.encode or one of its variants).
49+
*/
50+
function callOptionalReturn(IERC20 token, bytes memory data) private {
51+
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
52+
// we're implementing it ourselves.
53+
54+
// solhint-disable-next-line avoid-low-level-calls
55+
(bool success, bytes memory returndata) = address(token).call(data);
56+
require(success);
57+
58+
if (returndata.length > 0) {
59+
require(abi.decode(returndata, (bool)));
60+
}
3961
}
4062
}

test/token/ERC20/SafeERC20.test.js

+70-51
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,110 @@
11
const { shouldFail } = require('openzeppelin-test-helpers');
22

3-
const SafeERC20Helper = artifacts.require('SafeERC20Helper');
3+
const ERC20ReturnFalseMock = artifacts.require('ERC20ReturnFalseMock');
4+
const ERC20ReturnTrueMock = artifacts.require('ERC20ReturnTrueMock');
5+
const ERC20NoReturnMock = artifacts.require('ERC20NoReturnMock');
6+
const SafeERC20Wrapper = artifacts.require('SafeERC20Wrapper');
47

58
contract('SafeERC20', function () {
6-
beforeEach(async function () {
7-
this.helper = await SafeERC20Helper.new();
8-
});
9-
109
describe('with token that returns false on all calls', function () {
10+
beforeEach(async function () {
11+
this.wrapper = await SafeERC20Wrapper.new((await ERC20ReturnFalseMock.new()).address);
12+
});
13+
1114
it('reverts on transfer', async function () {
12-
await shouldFail.reverting(this.helper.doFailingTransfer());
15+
await shouldFail.reverting(this.wrapper.transfer());
1316
});
1417

1518
it('reverts on transferFrom', async function () {
16-
await shouldFail.reverting(this.helper.doFailingTransferFrom());
19+
await shouldFail.reverting(this.wrapper.transferFrom());
1720
});
1821

1922
it('reverts on approve', async function () {
20-
await shouldFail.reverting(this.helper.doFailingApprove());
23+
await shouldFail.reverting(this.wrapper.approve(0));
2124
});
2225

2326
it('reverts on increaseAllowance', async function () {
24-
await shouldFail.reverting(this.helper.doFailingIncreaseAllowance());
27+
await shouldFail.reverting(this.wrapper.increaseAllowance(0));
2528
});
2629

2730
it('reverts on decreaseAllowance', async function () {
28-
await shouldFail.reverting(this.helper.doFailingDecreaseAllowance());
31+
await shouldFail.reverting(this.wrapper.decreaseAllowance(0));
2932
});
3033
});
3134

3235
describe('with token that returns true on all calls', function () {
33-
it('doesn\'t revert on transfer', async function () {
34-
await this.helper.doSucceedingTransfer();
36+
beforeEach(async function () {
37+
this.wrapper = await SafeERC20Wrapper.new((await ERC20ReturnTrueMock.new()).address);
3538
});
3639

37-
it('doesn\'t revert on transferFrom', async function () {
38-
await this.helper.doSucceedingTransferFrom();
40+
shouldOnlyRevertOnErrors();
41+
});
42+
43+
describe('with token that returns no boolean values', function () {
44+
beforeEach(async function () {
45+
this.wrapper = await SafeERC20Wrapper.new((await ERC20NoReturnMock.new()).address);
3946
});
4047

41-
describe('approvals', function () {
42-
context('with zero allowance', function () {
43-
beforeEach(async function () {
44-
await this.helper.setAllowance(0);
45-
});
48+
shouldOnlyRevertOnErrors();
49+
});
50+
});
51+
52+
function shouldOnlyRevertOnErrors () {
53+
it('doesn\'t revert on transfer', async function () {
54+
await this.wrapper.transfer();
55+
});
56+
57+
it('doesn\'t revert on transferFrom', async function () {
58+
await this.wrapper.transferFrom();
59+
});
60+
61+
describe('approvals', function () {
62+
context('with zero allowance', function () {
63+
beforeEach(async function () {
64+
await this.wrapper.setAllowance(0);
65+
});
4666

47-
it('doesn\'t revert when approving a non-zero allowance', async function () {
48-
await this.helper.doSucceedingApprove(100);
49-
});
67+
it('doesn\'t revert when approving a non-zero allowance', async function () {
68+
await this.wrapper.approve(100);
69+
});
5070

51-
it('doesn\'t revert when approving a zero allowance', async function () {
52-
await this.helper.doSucceedingApprove(0);
53-
});
71+
it('doesn\'t revert when approving a zero allowance', async function () {
72+
await this.wrapper.approve(0);
73+
});
5474

55-
it('doesn\'t revert when increasing the allowance', async function () {
56-
await this.helper.doSucceedingIncreaseAllowance(10);
57-
});
75+
it('doesn\'t revert when increasing the allowance', async function () {
76+
await this.wrapper.increaseAllowance(10);
77+
});
5878

59-
it('reverts when decreasing the allowance', async function () {
60-
await shouldFail.reverting(this.helper.doSucceedingDecreaseAllowance(10));
61-
});
79+
it('reverts when decreasing the allowance', async function () {
80+
await shouldFail.reverting(this.wrapper.decreaseAllowance(10));
6281
});
82+
});
6383

64-
context('with non-zero allowance', function () {
65-
beforeEach(async function () {
66-
await this.helper.setAllowance(100);
67-
});
84+
context('with non-zero allowance', function () {
85+
beforeEach(async function () {
86+
await this.wrapper.setAllowance(100);
87+
});
6888

69-
it('reverts when approving a non-zero allowance', async function () {
70-
await shouldFail.reverting(this.helper.doSucceedingApprove(20));
71-
});
89+
it('reverts when approving a non-zero allowance', async function () {
90+
await shouldFail.reverting(this.wrapper.approve(20));
91+
});
7292

73-
it('doesn\'t revert when approving a zero allowance', async function () {
74-
await this.helper.doSucceedingApprove(0);
75-
});
93+
it('doesn\'t revert when approving a zero allowance', async function () {
94+
await this.wrapper.approve(0);
95+
});
7696

77-
it('doesn\'t revert when increasing the allowance', async function () {
78-
await this.helper.doSucceedingIncreaseAllowance(10);
79-
});
97+
it('doesn\'t revert when increasing the allowance', async function () {
98+
await this.wrapper.increaseAllowance(10);
99+
});
80100

81-
it('doesn\'t revert when decreasing the allowance to a positive value', async function () {
82-
await this.helper.doSucceedingDecreaseAllowance(50);
83-
});
101+
it('doesn\'t revert when decreasing the allowance to a positive value', async function () {
102+
await this.wrapper.decreaseAllowance(50);
103+
});
84104

85-
it('reverts when decreasing the allowance to a negative value', async function () {
86-
await shouldFail.reverting(this.helper.doSucceedingDecreaseAllowance(200));
87-
});
105+
it('reverts when decreasing the allowance to a negative value', async function () {
106+
await shouldFail.reverting(this.wrapper.decreaseAllowance(200));
88107
});
89108
});
90109
});
91-
});
110+
}

0 commit comments

Comments
 (0)