What is The DAO?
DAO (Decentralized Autonomous Organization) is an open-source project launched in April 2016 and aims to create a new business model for commercial and non-profit enterprises. The DAO works on the proposal of users who hold DAO tokens. They can stake Ether to receive DAO tokens and their voting power based on the amount of DAO tokens they hold. The DAO was one of the most extensive crowdfunding campaigns ever recorded on the Ethereum network, raising 150 million USD from more than 11,000 investors. However, it was exploited as a vulnerability nearly three months later, and about $60 million was stolen.
The DAO hack
Programming mistakes in the source code of The DAO were concerned before the attack happened. Experts in the field warned that the DAO’s smart contract vulnerabilities would allow users to drain all funds. While programmers were trying to fix those vulnerabilities in The DAO, the hackers made the attack and siphoned funds from The DAO.
How do hackers exploit the vulnerability in The DAO?
A minor update was committed to branch development on GitHub right before the attack occurred. The attack happened on Jun 18th, but before that, on Jun 12th, one commit to protecting against recursive calls was updated to branch develop in GitHub. The source code of The DAO below is taken from https://github.com/blockchainsllc/DAO
Let’s get into the code of The DAO: function splitDAO() in file DAO.sol is vulnerable to the recursive call.
function splitDAO(
uint _proposalID,
address _newCurator
) noEther onlyTokenholders returns (bool _success) {
// . . .
// Move ether and assign new Tokens
uint fundsToBeMoved =
(balances[msg.sender] * p.splitData[0].splitBalance) /
p.splitData[0].totalSupply;
if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
throw;
// . . .
// Burn DAO Tokens
Transfer(msg.sender, 0, balances[msg.sender]);
withdrawRewardFor(msg.sender); // be nice, and get his rewards
totalSupply -= balances[msg.sender]; // UPDATE TOTALSUPPLY AFTER SEND FUNDS
balances[msg.sender] = 0; // UPDATE USER'S BALANCE AFTER SEND FUNDS
paidOut[msg.sender] = 0;
return true;
}
Variable fundsToBeMoved is the amount of money sent to the child DAO of the attacker. The attacker will want to call this line multiple times to drain funds from DAO. After that, a reward is sent to the user in the function withdrawRewardFor(), but the attacker's balance has not been set to zero until that function is called complete.
function withdrawRewardFor(address _account) noEther internal returns (bool _success) {
if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
throw;
uint reward =
(balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
reward = rewardAccount.balance < reward ? rewardAccount.balance : reward;
if (!rewardAccount.payOut(_account, reward))
throw;
paidOut[_account] += reward;
return true;
}
// Variable paidOut is updated after function payOut is called:
function payOut(address _recipient, uint _amount) returns (bool) {
if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
throw;
if (_recipient.call.value(_amount)()) {
PayOut(_recipient, _amount);
return true;
} else {
return false;
}
}
Because both user’s balance and variable paidOut is not updated, so the second call to function withdrawRewardFor() can be called successfully. Thus, hackers need to trigger a fallback function to call function splitDAO() again with the same parameters after receiving funds from function withdrawRewardFor().
A simple schema to attack The DAO:
- Propose a split by function createProposal() in DAO.sol and wait for the voting period to end.
- Call function splitDAO() in DAO.sol
- Receive funds from The DAO by your new DAO.
- While you receive the reward from function withdrawRewardFor(), trigger fallback function to call function splitDAO() again with the same parameters.
- The DAO will send you funds and rewards before updating your balance, so you can repeat step 4 to drain all funds from The DAO.
How to fix this error?
On 12 Jun, before the attack, a minor update was committed to branch development in GitHub. It updated the variable paidOut before the function payOut() was called. But this modification is not enough to protect The DAO against the attack.
With this update, in the second function call, the variable reward will have a value of 0. But the function payOut() still transfers 0 ETH to the user, and the fallback function is still triggered.
Vulnerabilities in The DAO appear throughout the function splitDAO() and the related functions. To fix this error, the structure of the function splitDAO() will have to be fixed quite a lot which is pretty complex. I think at least the user’s balance should be updated before funds are transferred to the child DAO.
// Move ether and assign new Tokens
uint fundsToBeMoved =
(balances[msg.sender] * p.splitData[0].splitBalance) /
p.splitData[0].totalSupply;
balances[msg.sender] = 0; // UPDATE USER'S BALANCE Before SEND FUNDS
paidOut[msg.sender] = 0;
if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
throw;
Conclusion
After this attack, The DAO Community was in discussion to provide solutions to overcome the consequence of the attack. In this post, I didn’t go into the solution of The DAO, but I hope this post will help you to avoid this mistake in the future.
References
[1] Analysis of the DAO exploit, hackingdistributed.com, accessed April 13th, 2022.
[2] What is Reentrancy Attack and How to Prevent it?, techfi.tech, accessed April 13th, 2022.