In the entire EVM blockchain ecosystem, the transfer function of the ERC20 smart contract is probably the most used function. It's a simple function with a straightforward purpose: subtract the balance of A and add it to B. But have you ever wondered if there are any risks associated with this seemingly simple operation? There are two cases that developers need to handle cautiously, as they are usually not allowed in traditional non-blockchain applications but are valid in the blockchain context: transferring a zero amount and transferring to oneself.
Today, we will investigate the transfer-to-oneself case. What risk could there be when a person transfer tokens to himself?
On May 8th, a critical vulnerability in the GPU ERC20 contract was exploited. The attacker used this vulnerability to steal GPU tokens worth about $32K, causing the price to drop by 99.99%.
Overview
Attacker address: https://bscscan.com/address/0xcc78063840428c5ae53f3dc6d80759984788cbc0
Vulnerable contract: https://bscscan.com/address/0xf51cbf9f8e089ca48e454eb79731037a405972ce#code
Attack Tx : https://bscscan.com/tx/0x2c0ada695a507d7a03f4f308f545c7db4847b2b2c82de79e702d655d8c95dadb
Exploit Analysis
The transfer function of the ERC20 token contract deducts the specified amount from the sender's balance and adds it to the recipient's balance, provided the sender has sufficient funds. The transfer function of the GPU contract typically functions correctly, except in the case of transferring tokens to oneself, as mentioned earlier.
The issue lies in the contract caching the balances of both the sender and recipient in the senderAmount and recipientAmount variables. If the sender and recipient are the same address, the user's balance is initially deducted by the specified amount. However, it is then overwritten by the cached recipientAmount plus the transferred amount. Consequently, the user's balance increases by the transferred amount after each self-transfer.
For instance, if user A has 10 tokens and initiates a transfer of 5 tokens to themselves (A), the following occurs:
1. senderAmount = recipientAmount = 10
2. balances[A] = senderAmount - amount = 10 - 5 = 5
3. balances[A] = recipientAmount + amount = 10 + 5 = 15
As a result, A now has 15 tokens and can repeat this process to acquire an unlimited number of tokens.
The correct implementation should be adding/subtracting the balance directly without using cached variables, similar to the implementation found in the OpenZeppelin ERC20 contract.
Conclusion
The ERC20 contract is simple yet critically important because it manages all the funds within the ecosystem. Therefore, developers must exercise extreme caution when creating one. Avoid customizing the logic within the ERC20 contract whenever possible. Always check for self-transfers and zero transfers, and write test cases to ensure these scenarios are handled correctly.