Skip to content

Commit e94109f

Browse files
committed
feat: Add SafeERC20 (#469)
1 parent 2247847 commit e94109f

File tree

10 files changed

+740
-36
lines changed

10 files changed

+740
-36
lines changed

.solcover.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const skipFiles = [
33
'test',
44
'acl/ACLSyntaxSugar.sol',
55
'common/DepositableStorage.sol', // Used in tests that send ETH
6+
'common/SafeERC20.sol', // solidity-coverage fails on assembly if (https://github.com/sc-forks/solidity-coverage/issues/287)
67
'common/UnstructuredStorage.sol' // Used in tests that send ETH
78
]
89

contracts/common/SafeERC20.sol

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Inspired by AdEx (https://github.com/AdExNetwork/adex-protocol-eth/blob/b9df617829661a7518ee10f4cb6c4108659dd6d5/contracts/libs/SafeERC20.sol)
2+
// and 0x (https://github.com/0xProject/0x-monorepo/blob/737d1dc54d72872e24abce5a1dbe1b66d35fa21a/contracts/protocol/contracts/protocol/AssetProxy/ERC20Proxy.sol#L143)
3+
4+
pragma solidity ^0.4.24;
5+
6+
import "../lib/token/ERC20.sol";
7+
8+
9+
library SafeERC20 {
10+
// Before 0.5, solidity has a mismatch between `address.transfer()` and `token.transfer()`:
11+
// https://github.com/ethereum/solidity/issues/3544
12+
bytes4 private constant TRANSFER_SELECTOR = 0xa9059cbb;
13+
14+
string private constant ERROR_TOKEN_BALANCE_REVERTED = "SAFE_ERC_20_BALANCE_REVERTED";
15+
string private constant ERROR_TOKEN_ALLOWANCE_REVERTED = "SAFE_ERC_20_ALLOWANCE_REVERTED";
16+
17+
function invokeAndCheckSuccess(address _addr, bytes memory _calldata)
18+
private
19+
returns (bool)
20+
{
21+
bool ret;
22+
assembly {
23+
let ptr := mload(0x40) // free memory pointer
24+
25+
let success := call(
26+
gas, // forward all gas
27+
_addr, // address
28+
0, // no value
29+
add(_calldata, 0x20), // calldata start
30+
mload(_calldata), // calldata length
31+
ptr, // write output over free memory
32+
0x20 // uint256 return
33+
)
34+
35+
if gt(success, 0) {
36+
// Check number of bytes returned from last function call
37+
switch returndatasize
38+
39+
// No bytes returned: assume success
40+
case 0 {
41+
ret := 1
42+
}
43+
44+
// 32 bytes returned: check if non-zero
45+
case 0x20 {
46+
// Only return success if returned data was true
47+
// Already have output in ptr
48+
ret := eq(mload(ptr), 1)
49+
}
50+
51+
// Not sure what was returned: don't mark as success
52+
default { }
53+
}
54+
}
55+
return ret;
56+
}
57+
58+
function staticInvoke(address _addr, bytes memory _calldata)
59+
private
60+
view
61+
returns (bool, uint256)
62+
{
63+
bool success;
64+
uint256 ret;
65+
assembly {
66+
let ptr := mload(0x40) // free memory pointer
67+
68+
success := staticcall(
69+
gas, // forward all gas
70+
_addr, // address
71+
add(_calldata, 0x20), // calldata start
72+
mload(_calldata), // calldata length
73+
ptr, // write output over free memory
74+
0x20 // uint256 return
75+
)
76+
77+
if gt(success, 0) {
78+
ret := mload(ptr)
79+
}
80+
}
81+
return (success, ret);
82+
}
83+
84+
/**
85+
* @dev Same as a standards-compliant ERC20.transfer() that never reverts (returns false).
86+
* Note that this makes an external call to the token.
87+
*/
88+
function safeTransfer(ERC20 _token, address _to, uint256 _amount) internal returns (bool) {
89+
bytes memory transferCallData = abi.encodeWithSelector(
90+
TRANSFER_SELECTOR,
91+
_to,
92+
_amount
93+
);
94+
return invokeAndCheckSuccess(_token, transferCallData);
95+
}
96+
97+
/**
98+
* @dev Same as a standards-compliant ERC20.transferFrom() that never reverts (returns false).
99+
* Note that this makes an external call to the token.
100+
*/
101+
function safeTransferFrom(ERC20 _token, address _from, address _to, uint256 _amount) internal returns (bool) {
102+
bytes memory transferFromCallData = abi.encodeWithSelector(
103+
_token.transferFrom.selector,
104+
_from,
105+
_to,
106+
_amount
107+
);
108+
return invokeAndCheckSuccess(_token, transferFromCallData);
109+
}
110+
111+
/**
112+
* @dev Same as a standards-compliant ERC20.approve() that never reverts (returns false).
113+
* Note that this makes an external call to the token.
114+
*/
115+
function safeApprove(ERC20 _token, address _spender, uint256 _amount) internal returns (bool) {
116+
bytes memory approveCallData = abi.encodeWithSelector(
117+
_token.approve.selector,
118+
_spender,
119+
_amount
120+
);
121+
return invokeAndCheckSuccess(_token, approveCallData);
122+
}
123+
124+
/**
125+
* @dev Static call into ERC20.balanceOf().
126+
* Reverts if the call fails for some reason (should never fail).
127+
*/
128+
function staticBalanceOf(ERC20 _token, address _owner) internal view returns (uint256) {
129+
bytes memory balanceOfCallData = abi.encodeWithSelector(
130+
_token.balanceOf.selector,
131+
_owner
132+
);
133+
134+
(bool success, uint256 tokenBalance) = staticInvoke(_token, balanceOfCallData);
135+
require(success, ERROR_TOKEN_BALANCE_REVERTED);
136+
137+
return tokenBalance;
138+
}
139+
140+
/**
141+
* @dev Static call into ERC20.allowance().
142+
* Reverts if the call fails for some reason (should never fail).
143+
*/
144+
function staticAllowance(ERC20 _token, address _owner, address _spender) internal view returns (uint256) {
145+
bytes memory allowanceCallData = abi.encodeWithSelector(
146+
_token.allowance.selector,
147+
_owner,
148+
_spender
149+
);
150+
151+
(bool success, uint256 allowance) = staticInvoke(_token, allowanceCallData);
152+
require(success, ERROR_TOKEN_ALLOWANCE_REVERTED);
153+
154+
return allowance;
155+
}
156+
}

contracts/common/VaultRecoverable.sol

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import "../lib/token/ERC20.sol";
88
import "./EtherTokenConstant.sol";
99
import "./IsContract.sol";
1010
import "./IVaultRecoverable.sol";
11+
import "./SafeERC20.sol";
1112

1213

1314
contract VaultRecoverable is IVaultRecoverable, EtherTokenConstant, IsContract {
15+
using SafeERC20 for ERC20;
16+
1417
string private constant ERROR_DISALLOWED = "RECOVER_DISALLOWED";
1518
string private constant ERROR_VAULT_NOT_CONTRACT = "RECOVER_VAULT_NOT_CONTRACT";
19+
string private constant ERROR_TOKEN_TRANSFER_FAILED = "RECOVER_TOKEN_TRANSFER_FAILED";
1620

1721
/**
1822
* @notice Send funds to recovery Vault. This contract should never receive funds,
@@ -27,8 +31,9 @@ contract VaultRecoverable is IVaultRecoverable, EtherTokenConstant, IsContract {
2731
if (_token == ETH) {
2832
vault.transfer(address(this).balance);
2933
} else {
30-
uint256 amount = ERC20(_token).balanceOf(this);
31-
ERC20(_token).transfer(vault, amount);
34+
ERC20 token = ERC20(_token);
35+
uint256 amount = token.staticBalanceOf(this);
36+
require(token.safeTransfer(vault, amount), ERROR_TOKEN_TRANSFER_FAILED);
3237
}
3338
}
3439

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
pragma solidity 0.4.24;
2+
3+
import "../../common/SafeERC20.sol";
4+
import "../../lib/token/ERC20.sol";
5+
6+
7+
contract SafeERC20Mock {
8+
using SafeERC20 for ERC20;
9+
event Result(bool result);
10+
11+
function transfer(ERC20 token, address to, uint256 amount) external returns (bool) {
12+
bool result = token.safeTransfer(to, amount);
13+
emit Result(result);
14+
return result;
15+
}
16+
17+
function transferFrom(ERC20 token, address from, address to, uint256 amount) external returns (bool) {
18+
bool result = token.safeTransferFrom(from, to, amount);
19+
emit Result(result);
20+
return result;
21+
}
22+
23+
function approve(ERC20 token, address spender, uint256 amount) external returns (bool) {
24+
bool result = token.safeApprove(spender, amount);
25+
emit Result(result);
26+
return result;
27+
}
28+
29+
function allowance(ERC20 token, address owner, address spender) external view returns (uint256) {
30+
return token.staticAllowance(owner, spender);
31+
}
32+
33+
function balanceOf(ERC20 token, address owner) external view returns (uint256) {
34+
return token.staticBalanceOf(owner);
35+
}
36+
}

contracts/test/mocks/TokenMock.sol

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Stripped from https://github.com/OpenZeppelin/openzeppelin-solidity/blob/a9f910d34f0ab33a1ae5e714f69f9596a02b4d91/contracts/token/ERC20/StandardToken.sol
1+
// Modified from https://github.com/OpenZeppelin/openzeppelin-solidity/blob/a9f910d34f0ab33a1ae5e714f69f9596a02b4d91/contracts/token/ERC20/StandardToken.sol
22

33
pragma solidity 0.4.24;
44

@@ -8,14 +8,18 @@ import "../../lib/math/SafeMath.sol";
88
contract TokenMock {
99
using SafeMath for uint256;
1010
mapping (address => uint256) private balances;
11+
mapping (address => mapping (address => uint256)) private allowed;
1112
uint256 private totalSupply_;
13+
bool private allowTransfer_;
1214

15+
event Approval(address indexed owner, address indexed spender, uint256 value);
1316
event Transfer(address indexed from, address indexed to, uint256 value);
1417

1518
// Allow us to set the inital balance for an account on construction
1619
constructor(address initialAccount, uint256 initialBalance) public {
1720
balances[initialAccount] = initialBalance;
1821
totalSupply_ = initialBalance;
22+
allowTransfer_ = true;
1923
}
2024

2125
function totalSupply() public view returns (uint256) { return totalSupply_; }
@@ -29,12 +33,31 @@ contract TokenMock {
2933
return balances[_owner];
3034
}
3135

36+
/**
37+
* @dev Function to check the amount of tokens that an owner allowed to a spender.
38+
* @param _owner address The address which owns the funds.
39+
* @param _spender address The address which will spend the funds.
40+
* @return A uint256 specifying the amount of tokens still available for the spender.
41+
*/
42+
function allowance(address _owner, address _spender) public view returns (uint256) {
43+
return allowed[_owner][_spender];
44+
}
45+
46+
/**
47+
* @dev Set whether the token is transferable or not
48+
* @param _allowTransfer Should token be transferable
49+
*/
50+
function setAllowTransfer(bool _allowTransfer) public {
51+
allowTransfer_ = _allowTransfer;
52+
}
53+
3254
/**
3355
* @dev Transfer token for a specified address
3456
* @param _to The address to transfer to.
3557
* @param _value The amount to be transferred.
3658
*/
3759
function transfer(address _to, uint256 _value) public returns (bool) {
60+
require(allowTransfer_);
3861
require(_value <= balances[msg.sender]);
3962
require(_to != address(0));
4063

@@ -43,4 +66,41 @@ contract TokenMock {
4366
emit Transfer(msg.sender, _to, _value);
4467
return true;
4568
}
69+
70+
/**
71+
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
72+
* Beware that changing an allowance with this method brings the risk that someone may use both the old
73+
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
74+
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
75+
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
76+
* @param _spender The address which will spend the funds.
77+
* @param _value The amount of tokens to be spent.
78+
*/
79+
function approve(address _spender, uint256 _value) public returns (bool) {
80+
// Assume we want to protect for the race condition
81+
require(allowed[msg.sender][_spender] == 0);
82+
83+
allowed[msg.sender][_spender] = _value;
84+
emit Approval(msg.sender, _spender, _value);
85+
return true;
86+
}
87+
88+
/**
89+
* @dev Transfer tokens from one address to another
90+
* @param _from address The address which you want to send tokens from
91+
* @param _to address The address which you want to transfer to
92+
* @param _value uint256 the amount of tokens to be transferred
93+
*/
94+
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
95+
require(allowTransfer_);
96+
require(_value <= balances[_from]);
97+
require(_value <= allowed[_from][msg.sender]);
98+
require(_to != address(0));
99+
100+
balances[_from] = balances[_from].sub(_value);
101+
balances[_to] = balances[_to].add(_value);
102+
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
103+
emit Transfer(_from, _to, _value);
104+
return true;
105+
}
46106
}

0 commit comments

Comments
 (0)