Back to Lessons
Lesson 09

SafeERC20 & Non-Standard Tokens

+20EFFORT
8 sections
security()
01
text

The ERC-20 standard says transfer(), approve(), and transferFrom() each return a bool. But many of the most widely-used tokens — USDT, the original BNB, OMG — were deployed before this was enforced and return NOTHING. Code that assumes a bool return can break catastrophically when it meets these tokens. This lesson is about integrating arbitrary tokens safely, the way real DeFi protocols must.

02
code
solidity
1// What EIP-20 specifies:
2function transfer(address to, uint256 value) external returns (bool);
3
4// What USDT actually deployed (no return value!):
5function transfer(address to, uint256 value) external; // returns nothing
6
7// So this very common pattern REVERTS when token == USDT:
8require(token.transfer(to, amount), "transfer failed");
9// The compiler generated code to decode a 32-byte bool,
10// but USDT returns 0 bytes -> the ABI decode fails -> revert,
11// even though the transfer itself would have succeeded.
03
text

There are two distinct failure modes to defend against. (1) Tokens that return no data: a strict bool decode reverts even on a successful transfer. (2) Tokens that return false instead of reverting on failure: if you ignore the return value, a failed transfer looks successful and your contract's accounting silently desyncs from reality.

04
note
Key Insight

Golden rule: never ignore the result of a token transfer, and never assume a bool is returned. The amount you intended to move is not proof that it moved. Treat every external token as potentially non-standard unless you deployed it yourself.

05
code
solidity
1// OpenZeppelin's SafeERC20 solves BOTH problems for you.
2import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
3import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
4
5contract Vault {
6 using SafeERC20 for IERC20;
7
8 function deposit(IERC20 token, uint256 amount) external {
9 // Tolerates tokens that return nothing on success,
10 // and reverts if a returning token returns false.
11 token.safeTransferFrom(msg.sender, address(this), amount);
12 }
13}
14
15// Under the hood SafeERC20 does a low-level call, then:
16// - if there is NO return data -> assume success (non-standard token)
17// - if there IS return data -> require it decodes to true
06
text

Approvals carry their own non-standard quirk. USDT's approve() reverts if you try to change a non-zero allowance directly to another non-zero value — you must first set it back to zero. OpenZeppelin's forceApprove() handles this by resetting to 0 and then setting the new amount, while safeIncreaseAllowance()/safeDecreaseAllowance() adjust relative to the current value and sidestep the approval race covered in the Security lesson.

07
code
solidity
1using SafeERC20 for IERC20;
2
3// BAD on USDT: a non-zero -> non-zero approve reverts
4token.approve(spender, 50);
5
6// SAFE: works on standard AND non-standard tokens
7token.forceApprove(spender, 50); // resets to 0, then sets 50
8token.safeIncreaseAllowance(spender, 50);
9token.safeDecreaseAllowance(spender, 50);
08
text

When do you NOT need SafeERC20? When you fully control the token and know it is standards-compliant — it reverts on failure and returns a proper bool. This dApp's EFFORT token is a clean OpenZeppelin ERC-20, so internal code can call it directly. SafeERC20 is the tool you reach for the moment you integrate a token you did not write.

Complete

Connect your wallet to mark this lesson as complete

Earn 20 EFFORT tokens