Authors: Vlad Estoup & Sebastian Fabry
Security is inherent at the protocol level, and many hacks and attack vectors are actively prevented through safe design best practices, audited and battle-tested code, and consistent testing. Phishing attacks however come not from finding an exploit in the code, but by manipulating the user’s behavior. So how can Web3 developers help keep users safe from this type of attack?
Let’s dive into a specific type of phishing exploit known as the “zero transfer attack.” This is a phishing technique where the attacker tricks the user into mistakenly sending funds to the attacker’s address, because it resembles a trusted address in the user’s transaction history. According to Coinbase, this approach has led to a loss of $19m in victim funds from various wallet providers between late November 2022 and February 2023, which is just the tip of the iceberg since it is difficult to distinguish between intentional and unintentional transactions.
This research aims to highlight a behavior in the implementation of the ERC-20 token standard that has been recently exploited in the wild through address poisoning attacks. The motivations for publishing this are twofold:
It is possible for any account to make a transaction that transfers zero tokens to another account. This transaction would be executed and recorded on the blockchain. An attacker would seek to use a zero transfer to leverage users’ tendencies to rely on transaction history and quickly validate wallet addresses by comparing the first and last several characters. By polluting the transaction history of users’ wallets, attackers are able to take advantage of those who quickly copy/paste addresses and conduct frequent transactions.
This phishing attack exemplifies how a behavior with no “direct” security impact can still lead to large financial consequences. An attacker would take advantage of this by following these three steps:
The ERC-20 standard defines a set of rules and guidelines for implementing a token contract on the Ethereum blockchain. One of the core functions of an ERC-20 token contract is the transferFrom function, which allows a third party to transfer tokens from one address to another on behalf of the token owner. It requires approval from the token owner, which is usually set by calling the approve function which sets an allowance for the spender. This function can be regarded as the backbone of the ERC-20 DeFi ecosystem.
To quote directly from the ERC-20 standard regarding the transferFrom function:
The function SHOULD throw unless the _from account has deliberately authorized the sender of the message via some mechanism.
Transfers of 0 values MUST be treated as normal transfers and fire the Transfer event.
The OpenZeppelin implementation does its best to conform to these statements. Zero-value transfers are allowed in OpenZeppelin’s ERC-20, but because any address technically has a default allowance of 0 for any token, they can be executed from and to any address.
The reason for this lies in how the _spendAllowance function evaluates the approval between the caller and the sender of the funds. There is a require statement that evaluates the currentAllowance against the amount requested by the sender, at which point it will return “ERC20: insufficient allowance” if the allowance is not sufficient for the transfer.
That being said, there is a clear distinction between must and should in the specification. While the ERC-20 standard mandates that the function should throw unless the _from account has deliberately authorized the sender of the message via some mechanism, the implementation does not throw in the case of a zero-value transfer, because the currentAllowance would be zero, and thus equal to the amount. This aspect of the implementation highlights the subtle difference between should and must, as the must statement is strictly adhered to while the should statement is not interpreted as restrictively.
Executing transferFrom with a value of 0 for any chosen from and to addresses will be successful, even if the approval between the from address (the sender of funds) and address(this) (the caller of the transferFrom function) has been explicitly set to 0 (not approved). This behavior is true for most ERC-20 contracts (Uniswap, Compound Finance, Solmate and many others behave this way) which means that there are almost no safeguards in place to prevent zero-value transactions.
This allows an attacker to execute transferFrom instructions from arbitrary addresses with an amount of 0. These transactions trigger a Transfer event, as with any other transfer. In this case, it would include the victim’s address, the attacker’s address, and a value of 0. If an off-chain client happened to be listening to this event (which is often the case for wallets or any application tracking transaction data), they would be polluted by this malicious data. This leads to the potential attack vector illustrated above.
Some researchers have proposed an on-chain solution to address the ERC-20 standard address poisoning attack. The proposed solution involves modifying the require check within the _spendAllowance function to evaluate that the currentAllowance is greater than or equal to the maximum of amount and 1, which would require the Approval to be non-zero.
This approach would swiftly address the issue of address poisoning attacks, which have become increasingly common in recent times. However, this implementation deviates from the statement “Transfers of 0 values MUST be treated as normal transfers and fire the Transfer event.” If this change was made, a currentAllowance of 0 would not satisfy this check.
It is important to understand that ERC-20 is final and cannot be updated, and even if it could, it would not affect the behavior of the many non-upgradeable contracts on the blockchain.
The issue at hand is not with triggering the event itself, but rather with how off-chain tools display these events and how users rely on that information. Instead of trying to change a longstanding standard, it may be more effective to make changes to tools (e.g., block explorers, wallets, exchanges) by defaulting to not display or interact with zero-value transfers unless a toggle is activated.
Any changes on OpenZeppelin’s end to disallow or restrict zero-value transactions within its implementation could greatly impact its current use cases, perhaps even breaking many of them. Restricting this implementation risks creating more issues than the benefits it would bring to the community.
We invite suggestions as to how the ERC-20 implementation could be improved to better protect users against address poisoning while adhering to ERC-20 and without impacting the vast DeFi ecosystem.
The main way to address this issue, as of right now, is through continuous education and more secure UX patterns. By verifying addresses during transfers of tokens, and not relying on wallets’ built-in mechanisms for automatically pasting addresses in the to field, users can be one step ahead of malicious actors who take advantage of the “0 transfer”.
As for how clients listen to these events, it makes sense to monitor for Transfer events when the _value is zero and treat these differently off-chain, perhaps by raising a notification or avoiding using the data to prepopulate address fields in the future. As an additional layer of security, blockchain-based address books that uniquely and distinctively identify each account can prevent accidental transfers to undesirable addresses.
Implementing these guidelines can help improve the security of Web3 users. As has been shown, the security of the end user relies not only on the secure implementation of contract standards, but also the UX patterns of applications and wallets.
OpenZeppelin has built the most-trusted open-source libraries for blockchain developers: Contracts version 4.8. Repositories are tested, secure, optimized and gas-efficient for Web3 developers to build and deploy dApps, DeFi and Web3 projects.
These considerations are being shared in a continued effort to improve security best practices across the blockchain developer ecosystem. For security advisory, smart contract design, audits and incident response – get in touch with the OpenZeppelin security team.