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

Add SafeERC20.forceApprove() #4067

Merged
merged 14 commits into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
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));
}
}
14 changes: 7 additions & 7 deletions 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