On Sunday, June 3rd 2018, the account 0x0a010bd5067732bb062b52fdfb775b8f0a7648ae deposited an infeasibly-large supply of HADE Token into the decentralized exchanges Forkdelta and IDEX. After making trades, the attacker withdrew 26.5 ETH from the two exchanges.
- The HADE Token contract itself was unaffected. No fradulent transfers or changes to supply were made to the HADE contracts.
- IDEX and Forkdelta are not safe for trading HADE. The attacker effectively "tricked" the exchanges into believing she deposited tokens that she didn't actually own. Thus, the exchanges' liquidity pools inaccurately reflect the number of HADE tokens that they actually have and it's likely you will not be able to withdraw any tokens you purchase through them.
While the exchange contracts and the Hade contracts performed as expected on their own, the attacker used a series of exploits that tricked the exchanges into thinking she successfully deposited tokens into their liquidity pool, when in fact the transfers never actually succeded. Additionally, the standard open-zeppelin implementaion of approve()
allowed the attacker to give each exchange permission to transfer more tokens than she actually owned. Thus, the attack went as follows:
-
The attacker approves the exchange to transfer an infeasibly-large supply of tokens on her behalf. Here is one of those calls.
-
The attacker "deposits" the same infeasibly-large number of tokens into the exchange, here. Note the code for IDEX's depositToken function is:
function depositToken(address token, uint256 amount) {
tokens[token][msg.sender] = safeAdd(tokens[token][msg.sender], amount);
lastActiveTransaction[msg.sender] = block.number;
if (!Token(token).transferFrom(msg.sender, this, amount)) throw;
Deposit(token, msg.sender, amount, tokens[token][msg.sender]);
}
This attack exploited the fact that the HADE Token's implementaion of transferFrom does not include a return boolean denoting success or failure. Since the exchange interface only threw if the transferFrom
call returned false, the attacker was able to tell the exchange that she deposited (insert-huge-number-of) tokens, when in fact the transfers never actually succeded. For proof, the transaction history of HADE token doesn't show the above deposit.
In other words, the transferFrom()
call to HADE coin was unsuccessful, but since it didn't return false, the exchange assumed it was successful and allowed him to trade with tokens that didn't actually exist.
- Once the exchange believed that the attacker deposited that infeasibly-large supply of tokens, she was able to trade them in normal sized batches before withdrawing 27 ETH from this account later that day.
In light of this attack, my conclusions are as follows:
- Be careful with convention. Even though the HADE token itself worked as expected by rejecting the transfers, the exchange smart contracts were expecting, by convention, a boolean return value. Since they didn't receive the value "false" when the transfer failed, they assumed it succeded and allowed the attacker to trade coins that didn't actually exist.
- Rethink how the approve() function is implemented... again. A few months ago, developers fixed the so-called "race condition" on the approve() function (described in more detail here), however, users can still approve an account to transfer more tokens than they actually own. While this should ultimately be caught during the actual
transferFrom()
call, an additional check on the amount approved may help limit these fradulent smart-contract generated transfers.