Arbitrary transferFrom
Overview
- Severity: High
- Confidence: Medium
- Affected Versions: All
What is the Arbitrary transferFrom vulnerability?
The ERC-20 standard interface specifies a transferFrom()
function, which can be used to transfer tokens between two specified accounts. In some implementations, the from
address may be vulnerable to attacks that allow anyone to spend another user's token balance.
As a result, a best practice is to ensure that the from
address in a call to transferFrom()
is the msg.sender
value, particularly when calling an ERC-20 implementation that you did not author.
A real-world example: undisclosed live ERC-20 contracts
In 2018, a blockchain security research firm uncovered multiple undisclosed ERC-20 implementations that were vulnerable to the Arbitrary transferFrom vulnerability. These contracts did not properly check the allowance of tokens allotted to the message sender from the from
address, allowing users to spend tokens held by any address with a balance.
Further reading: New allowAnyone Bug Identified in Multiple ERC20 Smart Contracts
Technical example of vulnerable code
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract VulnerableERC20 {
mapping(address => mapping(address => uint256)) public allowed;
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(balances[_from] >= _value);
require(balances[_to] + _value > balances[_to]);
balances[_from] -= _value;
balances[_to] += _value;
allowed[_from][msg.sender] -= _value;
return true;
}
// additional ERC-20 implementation
}
The transferFrom()
function in the VulnerableERC20
contract deducts the _value
amount without checking the allowance. This allows anyone to spend the tokens controlled by any account.
Technical example of how to fix the vulnerability
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract VulnerableERC20 {
mapping(address => mapping(address => uint256)) public allowed;
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(balances[_from] >= _value);
require(balances[_to] + _value > balances[_to]);
require(allowed[_from][msg.sender] >= _value); // check allowance before deducting _value from _from's balance
balances[_from] -= _value;
balances[_to] += _value;
allowed[_from][msg.sender] -= _value;
return true;
}
// additional ERC-20 implementation
}
In the corrected code example, the allowance is enforced before allowing a third party to transfer tokens on a user's behalf.