Skip to content

Commit 315f426

Browse files
Aniket-Enggnventuro
authored andcommitted
Improved SafeERC20 allowance handling (#1407)
* signing prefix added * Minor improvement * Tests changed * Successfully tested * Minor improvements * Minor improvements * Revert "Dangling commas are now required. (#1359)" This reverts commit a688977. * updates * fixes #1404 * approve failing test * suggested changes done * ISafeERC20 removed * allowance methods in library * Improved SafeERC20 tests. * Fixed test coverage.
1 parent 67dac7a commit 315f426

File tree

3 files changed

+142
-35
lines changed

3 files changed

+142
-35
lines changed

contracts/mocks/SafeERC20Helper.sol

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ pragma solidity ^0.4.24;
33
import "../token/ERC20/IERC20.sol";
44
import "../token/ERC20/SafeERC20.sol";
55

6-
contract ERC20FailingMock is IERC20 {
7-
function totalSupply() public view returns (uint256) {
8-
return 0;
9-
}
6+
contract ERC20FailingMock {
7+
uint256 private _allowance;
108

119
function transfer(address, uint256) public returns (bool) {
1210
return false;
@@ -20,19 +18,13 @@ contract ERC20FailingMock is IERC20 {
2018
return false;
2119
}
2220

23-
function balanceOf(address) public view returns (uint256) {
24-
return 0;
25-
}
26-
2721
function allowance(address, address) public view returns (uint256) {
2822
return 0;
2923
}
3024
}
3125

32-
contract ERC20SucceedingMock is IERC20 {
33-
function totalSupply() public view returns (uint256) {
34-
return 0;
35-
}
26+
contract ERC20SucceedingMock {
27+
uint256 private _allowance;
3628

3729
function transfer(address, uint256) public returns (bool) {
3830
return true;
@@ -46,12 +38,12 @@ contract ERC20SucceedingMock is IERC20 {
4638
return true;
4739
}
4840

49-
function balanceOf(address) public view returns (uint256) {
50-
return 0;
41+
function setAllowance(uint256 allowance_) public {
42+
_allowance = allowance_;
5143
}
5244

5345
function allowance(address, address) public view returns (uint256) {
54-
return 0;
46+
return _allowance;
5547
}
5648
}
5749

@@ -62,10 +54,12 @@ contract SafeERC20Helper {
6254
IERC20 private _succeeding;
6355

6456
constructor() public {
65-
_failing = new ERC20FailingMock();
66-
_succeeding = new ERC20SucceedingMock();
57+
_failing = IERC20(new ERC20FailingMock());
58+
_succeeding = IERC20(new ERC20SucceedingMock());
6759
}
6860

61+
// Using _failing
62+
6963
function doFailingTransfer() public {
7064
_failing.safeTransfer(address(0), 0);
7165
}
@@ -78,6 +72,16 @@ contract SafeERC20Helper {
7872
_failing.safeApprove(address(0), 0);
7973
}
8074

75+
function doFailingIncreaseAllowance() public {
76+
_failing.safeIncreaseAllowance(address(0), 0);
77+
}
78+
79+
function doFailingDecreaseAllowance() public {
80+
_failing.safeDecreaseAllowance(address(0), 0);
81+
}
82+
83+
// Using _succeeding
84+
8185
function doSucceedingTransfer() public {
8286
_succeeding.safeTransfer(address(0), 0);
8387
}
@@ -86,7 +90,23 @@ contract SafeERC20Helper {
8690
_succeeding.safeTransferFrom(address(0), address(0), 0);
8791
}
8892

89-
function doSucceedingApprove() public {
90-
_succeeding.safeApprove(address(0), 0);
93+
function doSucceedingApprove(uint256 amount) public {
94+
_succeeding.safeApprove(address(0), amount);
95+
}
96+
97+
function doSucceedingIncreaseAllowance(uint256 amount) public {
98+
_succeeding.safeIncreaseAllowance(address(0), amount);
99+
}
100+
101+
function doSucceedingDecreaseAllowance(uint256 amount) public {
102+
_succeeding.safeDecreaseAllowance(address(0), amount);
103+
}
104+
105+
function setAllowance(uint256 allowance_) public {
106+
ERC20SucceedingMock(_succeeding).setAllowance(allowance_);
107+
}
108+
109+
function allowance() public view returns (uint256) {
110+
return _succeeding.allowance(address(0), address(0));
91111
}
92112
}

contracts/token/ERC20/SafeERC20.sol

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import "./IERC20.sol";
1010
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
1111
*/
1212
library SafeERC20 {
13+
14+
using SafeMath for uint256;
15+
1316
function safeTransfer(
1417
IERC20 token,
1518
address to,
@@ -38,6 +41,32 @@ library SafeERC20 {
3841
)
3942
internal
4043
{
44+
// safeApprove should only be called when setting an initial allowance,
45+
// or when resetting it to zero. To increase and decrease it, use
46+
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
47+
require((value == 0) || (token.allowance(msg.sender, spender) == 0));
4148
require(token.approve(spender, value));
4249
}
50+
51+
function safeIncreaseAllowance(
52+
IERC20 token,
53+
address spender,
54+
uint256 value
55+
)
56+
internal
57+
{
58+
uint256 newAllowance = token.allowance(address(this), spender).add(value);
59+
require(token.approve(spender, newAllowance));
60+
}
61+
62+
function safeDecreaseAllowance(
63+
IERC20 token,
64+
address spender,
65+
uint256 value
66+
)
67+
internal
68+
{
69+
uint256 newAllowance = token.allowance(address(this), spender).sub(value);
70+
require(token.approve(spender, newAllowance));
71+
}
4372
}

test/token/ERC20/SafeERC20.test.js

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,85 @@ contract('SafeERC20', function () {
1010
this.helper = await SafeERC20Helper.new();
1111
});
1212

13-
it('should throw on failed transfer', async function () {
14-
await shouldFail.reverting(this.helper.doFailingTransfer());
15-
});
13+
describe('with token that returns false on all calls', function () {
14+
it('reverts on transfer', async function () {
15+
await shouldFail.reverting(this.helper.doFailingTransfer());
16+
});
1617

17-
it('should throw on failed transferFrom', async function () {
18-
await shouldFail.reverting(this.helper.doFailingTransferFrom());
19-
});
18+
it('reverts on transferFrom', async function () {
19+
await shouldFail.reverting(this.helper.doFailingTransferFrom());
20+
});
2021

21-
it('should throw on failed approve', async function () {
22-
await shouldFail.reverting(this.helper.doFailingApprove());
23-
});
22+
it('reverts on approve', async function () {
23+
await shouldFail.reverting(this.helper.doFailingApprove());
24+
});
2425

25-
it('should not throw on succeeding transfer', async function () {
26-
await this.helper.doSucceedingTransfer();
27-
});
26+
it('reverts on increaseAllowance', async function () {
27+
await shouldFail.reverting(this.helper.doFailingIncreaseAllowance());
28+
});
2829

29-
it('should not throw on succeeding transferFrom', async function () {
30-
await this.helper.doSucceedingTransferFrom();
30+
it('reverts on decreaseAllowance', async function () {
31+
await shouldFail.reverting(this.helper.doFailingDecreaseAllowance());
32+
});
3133
});
3234

33-
it('should not throw on succeeding approve', async function () {
34-
await this.helper.doSucceedingApprove();
35+
describe('with token that returns true on all calls', function () {
36+
it('doesn\'t revert on transfer', async function () {
37+
await this.helper.doSucceedingTransfer();
38+
});
39+
40+
it('doesn\'t revert on transferFrom', async function () {
41+
await this.helper.doSucceedingTransferFrom();
42+
});
43+
44+
describe('approvals', function () {
45+
context('with zero allowance', function () {
46+
beforeEach(async function () {
47+
await this.helper.setAllowance(0);
48+
});
49+
50+
it('doesn\'t revert when approving a non-zero allowance', async function () {
51+
await this.helper.doSucceedingApprove(100);
52+
});
53+
54+
it('doesn\'t revert when approving a zero allowance', async function () {
55+
await this.helper.doSucceedingApprove(0);
56+
});
57+
58+
it('doesn\'t revert when increasing the allowance', async function () {
59+
await this.helper.doSucceedingIncreaseAllowance(10);
60+
});
61+
62+
it('reverts when decreasing the allowance', async function () {
63+
await shouldFail.reverting(this.helper.doSucceedingDecreaseAllowance(10));
64+
});
65+
});
66+
67+
context('with non-zero allowance', function () {
68+
beforeEach(async function () {
69+
await this.helper.setAllowance(100);
70+
});
71+
72+
it('reverts when approving a non-zero allowance', async function () {
73+
await shouldFail.reverting(this.helper.doSucceedingApprove(20));
74+
});
75+
76+
it('doesn\'t revert when approving a zero allowance', async function () {
77+
await this.helper.doSucceedingApprove(0);
78+
});
79+
80+
it('doesn\'t revert when increasing the allowance', async function () {
81+
await this.helper.doSucceedingIncreaseAllowance(10);
82+
});
83+
84+
it('doesn\'t revert when decreasing the allowance to a positive value', async function () {
85+
await this.helper.doSucceedingDecreaseAllowance(50);
86+
});
87+
88+
it('reverts when decreasing the allowance to a negative value', async function () {
89+
await shouldFail.reverting(this.helper.doSucceedingDecreaseAllowance(200));
90+
});
91+
});
92+
});
3593
});
3694
});

0 commit comments

Comments
 (0)