ERC20 operations can be unsafe due to different implementations and vulnerabilities in the standard.
It is therefore recommended to always either use OpenZeppelin's SafeERC20
library or at least to wrap each operation in a require
statement.
To circumvent ERC20's approve
functions race-condition vulnerability use
OpenZeppelin's SafeERC20
library's safe{Increase|Decrease}Allowance
functions.
In case the vulnerability is of no danger for your implementation, provide enough documentation explaining the reasonings.
🤦 Bad:
IERC20(token).transferFrom(msg.sender, address(this), amount);
🚀 Good (using OpenZeppelin's SafeERC20
):
import {SafeERC20} from "openzeppelin/token/utils/SafeERC20.sol";
// ...
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
🚀 Good (using require
):
bool success = IERC20(token).transferFrom(msg.sender, address(this), amount);
require(success, "ERC20 transfer failed");
There exist ERC20 tokens that charge a fee for every transfer()
or
transferFrom()
.
If this tokens are unsupported, ensure there is proper documentation about it.
🤦 Bad:
IERC20(token).transferFrom(msg.sender, address(this), amount);
internalBalance += amount;
🚀 Good:
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
IERC20(token).transferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = IERC20(token).balanceOf(address(this));
require(balanceAfter - amount == balanceBefore, "feeOnTransfer token not supported");
internalBalance += amount;
Avoid floating pragmas for non-library contracts.
While floating pragmas make sense for libraries to allow them to be included with multiple different versions of applications, it may be a security risk for application implementations.
A known vulnerable compiler version may accidentally be selected or security tools might fall-back to an older compiler version ending up checking a different EVM compilation that is ultimately deployed on the blockchain.
It is recommended to pin to a concrete compiler version.
🤦 Bad:
pragma solidity ^0.8.0;
🚀 Good:
pragma solidity 0.8.4;
Contracts implementing access control's, e.g. owner
, should consider
implementing a Two-Step Transfer pattern.
Otherwise it's possible that the role mistakenly transfers ownership to the wrong address, resulting in a loss of the role.
🤦 Bad:
address owner;
// ...
function setOwner(address newOwner) external {
require(msg.sender == owner, "!owner");
emit NewOwner(newOwner);
owner = newOwner;
}
🚀 Good:
address owner;
address pendingOwner;
// ...
function setPendingOwner(address newPendingOwner) external {
require(msg.sender == owner, "!owner");
emit NewPendingOwner(newPendingOwner);
pendingOwner = newPendingOwner;
}
function acceptOwnership() external {
require(msg.sender == pendingOwner, "!pendingOwner");
emit NewOwner(pendingOwner);
owner = pendingOwner;
pendingOwner = address(0);
}
The usage of deprecated library functions should be discouraged.
This issue is mostly related to OpenZeppelin libraries.
🤦 Bad:
use SafeERC20 for IERC20;
// ...
IERC20(token).safeApprove(spender, value);
🚀 Good:
use SafeERC20 for IERC20;
// ...
IERC20(token).safeIncreaseAllowance(spender, value);
Functions in solmate
's SafeTransferLib
library do not check whether a token
has code at all. This responsibility is delegated to the caller.
As a call to an address with no code will be a no-op, since low-level calls to
non-contracts always return true, a transfer of tokens using solmate
's
SafeTransferLib
will succeed if the token does not have any code.
Therefore, it is recommended to verify that a contract exists before using any
SafeTransferLib
functions.
🤦 Bad:
// Note that this function is for demonstration purposes only and should not be used as is.
function _fetchTokens(address token, address from, uint amount) internal {
ERC20(token).safeTransferFrom(from, amount);
}
🚀 Good:
// Note that this function is for demonstration purposes only and should not be used as is.
function _fetchTokens(address token, address from, uint amount) internal {
require(token.code.length != 0, "Token does not exist");
ERC20(token).safeTransferFrom(from, amount);
}