Why You Should Verify the Tokens You Own: A Deep Dive Into 2 Vulnerable ERC20 Contracts
The prices of two crypto tokens — BeautyChain and SmartMesh — dramatically crashed this week. One or more attackers exploited bugs in their smart contracts and generated huge amounts of tokens out of thin air, massively diluting token supplies. These events could have been easily avoided. In this post, we describe the attacks, illustrate them with Solidity code examples, and argue that these hacks are a wake-up call for token investors and developers to be more cognizant and diligent about smart contract security.
On 25 April 2018, some cryptocurrency exchanges halted deposits, withdrawals, and trades of at least 2 ERC20 tokens: BeautyChain and SmartMesh, citing reports that these tokens had a security vulnerability in their smart contracts.
These contracts had already been attacked to create huge numbers of tokens out of thin air, and they suffered sharp price drops. The Bitcoin-denominated price of one of these tokens, for instance, fell more than 90% in a single hour, before trading was suspended:
This vulnerability, known as an integer overflow, is not new. It is easily preventable as it is commonly listed in smart contract security guides. Moreover, libraries that prevent such attacks are freely available. Yet, vulnerable contracts abound.
One security researcher has already located at least 10 tokens affected by this particular bug. In fact, the same vulnerability had been infamously used by scammers to massively inflate the supply of their token and sell it on exchanges. Had the developers behind these contracts been aware of these well-known exploits and acted in good faith, these attacks might not have occurred.
How the Attacks Worked
Some Solidity smart contracts are vulnerable to what is known as an integer overflow or underflow. They occur when a variable exceeds the maximum or minimum of the data type it uses. When this happens, the value wraps around the other end of the minimum or maximum range respectively.
For instance, the following Solidity function always returns true:
Mathematically, this is unintuitive, since the sum of two positive numbers is always greater than the first. Yet, since the value of max is 2256-1, which is the upper limit of the uint data type, the result of x + max wraps around to 0, and becomes 499 instead of 500 + 2256-1.
An integer underflow follows the same principle, but in reverse.
In the next two sections of this post, we illustrate how an integer overflow and underflow allowed the exploits to occur.
The SmartMesh Token Contract Bug
The vulnerable function in the SmartMesh token contract is transferProxy:
An attacker exploited that function in the transaction 0x1abab4c8…:
transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt, uint8 _v, bytes32 _r, bytes32 _s)
: 000000000000000000000000df31a499a5a8358b74564f1e2214b31bb34eb46f : 000000000000000000000000df31a499a5a8358b74564f1e2214b31bb34eb46f : 8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff : 7000000000000000000000000000000000000000000000000000000000000001 : 000000000000000000000000000000000000000000000000000000000000001b : 87790587c256045860b8fe624e5807a658424fad18c2348460e40ecf10fc8799 : 6c879b1e8a0a62f23b47aa57a3369d416dd783966bd1dda0394c04163a98d8d8
This transaction occurred at block 5499035, and at this point in the blockchain:
balances[_from] (balances[0xdf31a49…]) had a balance of 0 tokens.
balances[_to] (balances[0xdf31a49…]) had a balance of 0 tokens.
balances[msg.sender] balances[0xd6a09bd…])had a balance of 0 tokens.
_value is 0x8ffff… and _freeSmt is 0x70000…1. Notice how the addition of _from to _value perfectly overflows to 0. 0x8ffff…f + 0x70000…1 is 0. This will be helpful to understand in a minute.
Now, let’s look at the first if statement (line 5). Because balances[_from] is 0 and _feeSmt+_value is also 0 — therefore the condition of the if statement is false(0 < 0 is false) and revert() is not triggered.
This check passes — good for the hacker, bad for everyone else.
Next, let’s analyse the second if statement:
if (balances[_to] + _value < balances[_to] || balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
The contract author may have intended to perform an overflow check here. However, at this point, no overflow happens:
balances[_to] is 0 and therefore balances[_to] + _value < balances[_to] is false
balances[msg.sender] is 0 and therefore balances[msg.sender] + _feeSmt < balances[msg.sender] is false
Hence, both conditions are false and revert() is not triggered. Subsequently, balance’s state variable will be modified as such:
balances[_to] += _value; causes the owner of address _to to gain a massive number of tokens; and
balances[_from] -= _value + _feeSmt; causes the owner of _from to lose 0 tokens.
The BeautyChain Token Contract Bug
While the authors of the BeautyChain token contract used the SafeMath library, whose functions prevent overflow exploits if used correctly, they did not do so in one particular line of the batchTransfer function:
uint256 amount = uint256(cnt) * _value;
The full function is as follows:
In the transaction used to trigger this exploit, an overflow occurs when _value is multiplied with uint256(cnt). Since:
cnt == 2
_value == 0x8000000000000000000000000000000000000000000000000000000000000000
amount overflowed and became exactly 0. This means that the second require() statement did not throw an exception, and the balance state variable was massively incremented in the for-loop. Whoever exploited this was very smart to realise that a particular combination of inputs would cause the require statements or the SafeMath functions to not throw any exceptions, yet allow the exploit to occur. In contrast, a careless auditor would have simply assumed that the mere presence of SafeMath library functions in batchTransfer made it secure.
A Wake-Up Call
A common refrain in this space is that if you don’t own your private keys, you don’t own your coins. Unfortunately, this advice is not enough when it comes to tokens. This is because tokens are only as secure as the smart contracts that define them. Remember: don’t trust, but verify. You not only have to keep your private keys safe, but you also have to verify that the code which underlies your tokens is free from vulnerabilities like those described above.
Cryptocurrency exchanges are now in a tough spot, as the only way to be sure that customers’ funds are safe is to audit every single line of code for every single tradable token. This is tedious but necessary. Nor is this situation ideal for investors. The prospect of losing money because of an undiscovered and subtle bug is unsettling. Yet laypersons are unequipped to audit smart contracts, so they have little choice but to trust the developers. Ironically, financial disintermediation and trustlessness are precisely what cryptocurrencies promise, but this is the reality of the situation.
These events should serve as a wake-up call to everyone in the space. Smart contract developers must be competent and diligent to secure investors’ funds, and investors have to be fully aware of the pitfalls of poorly implemented tokens. The only way to be sure is, unfortunately, to verify the code yourself.
A brief note regarding authorship: we independently identified the bug in the SmartMesh token when we first learned about it, but the BeautyChain token bug was discovered earlier on by someone else, who then wrote about it in Mandarin. All content above, however, is in our own words.
Koh Wei Jie (@catallacticised) is a full-stack smart contract developer based in Singapore.