You would think calling a few functions on an ERC-20 token is the simplest thing to do, right? Unfortunately I have some bad news, it's not. There are several things to consider and some errors are still pretty common. Let's start with the easy ones.
Let's take a very common token: ... Now to interact with this token, let's import the IERC20.sol and just use it:

How to securely handle ERC-20 interactions
// incorrect version
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol";
function interactWithToken(uint256 sendAmount) {
// some code
IERC20 token = IERC20(tokenAddress);
token.transferFrom(msg.sender, address(this), sendAmount);
}function transferFrom(address _from, address _to, uint _value) returns (bool) {
if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value >= balances[_to]) {
balances[_to] += _value;
balances[_from] -= _value;
allowed[_from][msg.sender] -= _value;
Transfer(_from, _to, _value);
return true;
} else { return false; }
}You can see, in contrast to the DAI token, it doesn't revert the call. Now instead of reverting on failure, our token transfer returns false. But we don't look at the return value in our code. Essentially anyone could interact now with our contract where our contract thinks a token transfer was successful while really nothing was transferred. Ouch!
ZRX is still ERC-20 compliant, as it's nowhere defined that the ERC-20 contract has to revert on failure. There are pros and cons with both approaches. Our solution to fix the code example is obviously to just check the return value. A simple require(token.transferFrom(msg.sender, address(this), sendAmount), "Token transfer failed!"); will be enough to fix it. The same thing is true for any function in the contract, they all return false on failure or revert, so always handle both cases.
Error handling within the contract
Most of the times tokens just revert on failure. The advantage is that even broken code like our first attempt still securely interacts with this token. This is also the reason why OpenZeppelin has chosen to do this in their ERC20 reference implementation and why I recommend you do it this way.
But there's definitely an argument to be made for the return value. If you know the token you're interacting with returns false on failure, or you just want to add extra functionality for those tokens, you can do something like
function interactWithToken(uint256 sendAmount) {
IERC20 token = IERC20(tokenAddress);
bool success = token.transferFrom(msg.sender, address(this), sendAmount);
if (success) {
// handle success case
} else {
// handle failure case without reverting
}
}The advantage here obviously being that we still allow successful transactions even for failed token transfers.
What about error handling if the token reverts on failure?
This used to be more complicated, but since Solidity 0.6 it's actually not that difficult anymore. Now they support try/catch:

function interactWithToken(uint256 sendAmount) {
IERC20 token = IERC20(tokenAddress);
bool success;
try token.transferFrom(msg.sender, address(this), sendAmount) returns (bool _success) {
success = _success;
} catch Error(string memory /*reason*/) {
success = false;
// special handling depending on error message possible
} catch (bytes memory /*lowLevelData*/) {
success = false;
}
if (success) {
// handle success case
} else {
// handle failure case without reverting
}
}This way you can do error handling for both versions of the ERC-20 contracts.
How to support all tokens
So that's technically it. Now you support ERC-20 compliant tokens. Unfortunately as it turns out, there are quite a few tokens out there that look like ERC-20, but don't behave like it. This is because of the missing return value bug. As it turns out, OpenZeppelin's reference implementation had a bug for some amount of time. They reverted on failure, but they didn't return true on success. Quite a few tokens are affected including big names like USDT, OmiseGo and BNB.
Unfortunately if you are now expecting a bool return, but no value is returned, our contracts compiled with Solidity 0.4.22 or higher will correctly revert. This bug has even affected Uniswap in the past.
So how do other projects handle this? Let's look at the Compound version:
function doTransferOut(address payable to, uint amount) internal {
EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying);
token.transfer(to, amount);
bool success;
assembly {
switch returndatasize()
case 0 { // This is a non-standard ERC-20
success := not(0) // set success to true
}
case 32 { // This is a complaint ERC-20
returndatacopy(0, 0, 32)
success := mload(0) // Set `success = returndata` of external call
}
default { // This is an excessively non-compliant ERC-20, revert.
revert(0, 0)
}
}
require(success, "TOKEN_TRANSFER_OUT_FAILED");
}For a known token that returns no data, a successful call is conventionally treated as success. Empty return data alone does not prove that balances changed: the token and the application’s accounting rules still need review. Use a maintained wrapper rather than treating arbitrary low-level calls as trusted token transfers.
With the advancements of Solidity, we can simplify this code. This is how Uniswap is doing it:
function safeTransfer(address token, address to, uint value) internal {
// bytes4(keccak256(bytes('transfer(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED');
}This implementation is only slightly different as the abi.decode will work for other data.lengths as well, not only 32. But this shouldn't make a difference to you. We can also easily change it to support error handling:
// OpenZeppelin Contracts 5.x; import IERC20 and SafeERC20 as below.
function safeTransferNoRevert(
IERC20 token, address to, uint256 value
) internal returns (bool) {
return SafeERC20.trySafeTransfer(token, to, value);
}
What should you do? (tl;dr)
So what's the best way to go about it now? Well you can simply use the OpenZeppelin SafeERC20 implementation.
This is a wrapper library around ERC-20 calls. Don't be confused, this is not for creating your own token, but for securely interacting with existing ones. The implementation of SafeERC20 is essentially like the above Uniswap version. You can use it like this:
2026 update: The final example uses the current OpenZeppelin Contracts 5.x wrapper. Match your compiler to the installed package and lock its version. Fee-on-transfer and rebasing tokens may require balance-based accounting; callback-capable tokens also require reentrancy-safe application logic. The sample only demonstrates the transfer call.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TestContract {
using SafeERC20 for IERC20;
// Demonstration only: add accounting and withdrawals before holding funds.
function safeInteractWithToken(IERC20 token, uint256 sendAmount) external {
token.safeTransferFrom(msg.sender, address(this), sendAmount);
}
}



Join the conversation
Comments are hosted by Disqus and load only when you choose to enable them.