Skip to content

Commit

Permalink
Add SafeERC20.forceApprove() (#4067)
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed Feb 24, 2023
1 parent 0fbd039 commit 8b47e96
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 103 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-terms-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`SafeERC20`: Add a `forceApprove` function to improve compatibility with tokens behaving like USDT.
13 changes: 13 additions & 0 deletions contracts/mocks/token/ERC20ForceApproveMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../../token/ERC20/ERC20.sol";

// contract that replicate USDT (0xdac17f958d2ee523a2206206994597c13d831ec7) approval beavior
abstract contract ERC20ForceApproveMock is ERC20 {
function approve(address spender, uint256 amount) public virtual override returns (bool) {
require(amount == 0 || allowance(msg.sender, spender) == 0, "USDT approval failure");
return super.approve(spender, amount);
}
}
29 changes: 18 additions & 11 deletions contracts/mocks/token/ERC20NoReturnMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@

pragma solidity ^0.8.0;

contract ERC20NoReturnMock {
mapping(address => uint256) private _allowances;
import "../../token/ERC20/ERC20.sol";

function transfer(address, uint256) public {}

function transferFrom(address, address, uint256) public {}

function approve(address, uint256) public {}
abstract contract ERC20NoReturnMock is ERC20 {
function transfer(address to, uint256 amount) public override returns (bool) {
super.transfer(to, amount);
assembly {
return(0, 0)
}
}

function setAllowance(address account, uint256 allowance_) public {
_allowances[account] = allowance_;
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
super.transferFrom(from, to, amount);
assembly {
return(0, 0)
}
}

function allowance(address owner, address) public view returns (uint256) {
return _allowances[owner];
function approve(address spender, uint256 amount) public override returns (bool) {
super.approve(spender, amount);
assembly {
return(0, 0)
}
}
}
4 changes: 1 addition & 3 deletions contracts/mocks/token/ERC20PermitNoRevertMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ pragma solidity ^0.8.0;
import "../../token/ERC20/ERC20.sol";
import "../../token/ERC20/extensions/draft-ERC20Permit.sol";

contract ERC20PermitNoRevertMock is ERC20, ERC20Permit {
constructor() ERC20("ERC20PermitNoRevertMock", "ERC20PermitNoRevertMock") ERC20Permit("ERC20PermitNoRevertMock") {}

abstract contract ERC20PermitNoRevertMock is ERC20Permit {
function permitThatMayRevert(
address owner,
address spender,
Expand Down
18 changes: 5 additions & 13 deletions contracts/mocks/token/ERC20ReturnFalseMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,18 @@

pragma solidity ^0.8.0;

contract ERC20ReturnFalseMock {
mapping(address => uint256) private _allowances;
import "../../token/ERC20/ERC20.sol";

function transfer(address, uint256) public pure returns (bool) {
abstract contract ERC20ReturnFalseMock is ERC20 {
function transfer(address, uint256) public pure override returns (bool) {
return false;
}

function transferFrom(address, address, uint256) public pure returns (bool) {
function transferFrom(address, address, uint256) public pure override returns (bool) {
return false;
}

function approve(address, uint256) public pure returns (bool) {
function approve(address, uint256) public pure override returns (bool) {
return false;
}

function setAllowance(address account, uint256 allowance_) public {
_allowances[account] = allowance_;
}

function allowance(address owner, address) public view returns (uint256) {
return _allowances[owner];
}
}
27 changes: 0 additions & 27 deletions contracts/mocks/token/ERC20ReturnTrueMock.sol

This file was deleted.

64 changes: 56 additions & 8 deletions contracts/token/ERC20/utils/SafeERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@ import "../../../utils/Address.sol";
library SafeERC20 {
using Address for address;

/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}

/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
Expand All @@ -45,20 +53,45 @@ library SafeERC20 {
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}

/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 newAllowance = token.allowance(address(this), spender) + value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
uint256 oldAllowance = token.allowance(address(this), spender);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value));
}

/**
* @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
uint256 newAllowance = oldAllowance - value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value));
}
}

/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Compatible with tokens that require the approval to be set to
* 0 before setting it to a non-zero value.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value);

if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0));
_callOptionalReturn(token, approvalCall);
}
}

/**
* @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`.
* Revert on invalid signature.
*/
function safePermit(
IERC20Permit token,
address owner,
Expand Down Expand Up @@ -87,9 +120,24 @@ library SafeERC20 {
// the target address contains contract code and also asserts for success in the low-level call.

bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
if (returndata.length > 0) {
// Return data is optional
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}

/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
// and not revert is the subcall reverts.

(bool success, bytes memory returndata) = address(token).call(data);
return
success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token));
}
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"glob": "^8.0.3",
"graphlib": "^2.1.8",
"hardhat": "^2.9.1",
"hardhat-exposed": "^0.3.1",
"hardhat-exposed": "^0.3.2",
"hardhat-gas-reporter": "^1.0.4",
"hardhat-ignore-warnings": "^0.2.0",
"keccak256": "^1.0.2",
Expand Down

0 comments on commit 8b47e96

Please sign in to comment.