How to steal money in Ethereum (and how to protect your Ether)
Ethereum's smart contracts can be deceptively easy to code, but the devil is in the details. This post is about the vulnerability underneath the DAO hack and the preventative measures.
First of all, I am grateful to Daniel Nagy, my friend and an Ethereum developer, for explaining the below material concisely. This is public material but not so trivial to understand, especially when written for technical audiences, so here you go - a simple explanation of the DAO hack and the ways to design your contract to avoid being a victim of this type of attack.
You and a group of 5 friends want to get into a Drake concert, but do not want to pay for 6 tickets. How get around it? You look around the concert site and find that the people that check the tickets do not stamp or take them away - and the fence around the concert area is tall, but a chain-link type - and now the solution is trivial. You buy one ticket, agree on a secret meeting spot with your friends around the fence of the concert area, then you enter with your ticket, pass it to the first friend through the fence, then the friend enters, and so on - pretty soon the whole group is inside.
This reminds me of entering the Ripley's Aquarium from the souvenir store (i.e. from the side where by design you are supposed to spend some more money on junk before leaving) but that's a different story and a different type of a hack.
Back to the Ethereum - replace the Drake concert with one contract A, a group of friends with another contract B, and you get an approach to get way more funds (or fun) from a contract than was originally intended.
In technical terms: If a function of contract B calls contract A, and contract A starts off by sending the required money back to B, then contract B can call contract A again, from the default function, before A has a chance to stamp the ticket - this procedure can be repeated so long as the stack depth and gas permit it. That is the way the attacker stole funds from the DAO - unfortunately, once a long-term, DAO like contract is live on the chain, there is no way to change the code and Ethereum developers had to unwind the whole chain of related transactions to give money back to the DAO contributors.
Practically, this means that the early version of my futures contract, listed here, is vulnerable to this attack when the contract distributes the money.
A simplified version of the contract, found below, corrects that problem, although it is still not watertight. Note that the function that distributes the money now updates the record first, then attempts to send the money, and throws an exception if the send fails.
In the next post, I will focus on the recent developments in the Ethereum network, which have significantly affected the contract transaction processing.
First of all, I am grateful to Daniel Nagy, my friend and an Ethereum developer, for explaining the below material concisely. This is public material but not so trivial to understand, especially when written for technical audiences, so here you go - a simple explanation of the DAO hack and the ways to design your contract to avoid being a victim of this type of attack.
You and a group of 5 friends want to get into a Drake concert, but do not want to pay for 6 tickets. How get around it? You look around the concert site and find that the people that check the tickets do not stamp or take them away - and the fence around the concert area is tall, but a chain-link type - and now the solution is trivial. You buy one ticket, agree on a secret meeting spot with your friends around the fence of the concert area, then you enter with your ticket, pass it to the first friend through the fence, then the friend enters, and so on - pretty soon the whole group is inside.
This reminds me of entering the Ripley's Aquarium from the souvenir store (i.e. from the side where by design you are supposed to spend some more money on junk before leaving) but that's a different story and a different type of a hack.
Back to the Ethereum - replace the Drake concert with one contract A, a group of friends with another contract B, and you get an approach to get way more funds (or fun) from a contract than was originally intended.
In technical terms: If a function of contract B calls contract A, and contract A starts off by sending the required money back to B, then contract B can call contract A again, from the default function, before A has a chance to stamp the ticket - this procedure can be repeated so long as the stack depth and gas permit it. That is the way the attacker stole funds from the DAO - unfortunately, once a long-term, DAO like contract is live on the chain, there is no way to change the code and Ethereum developers had to unwind the whole chain of related transactions to give money back to the DAO contributors.
Practically, this means that the early version of my futures contract, listed here, is vulnerable to this attack when the contract distributes the money.
contract Oracle
{
uint public current_level;
address public owner;
function Oracle()
{
owner = msg.sender;
current_level = 2100;
}
function GetLevel() returns (uint current_level_)
{
return current_level;
}
function SetLevel(uint current_level_)
{
if (msg.sender != owner)
{
throw;
}
current_level = current_level_;
}
function GetOwner() returns (address owner_)
{
owner_ = owner;
}
}
contract FutureContract
{
address public long;
address public short;
address public oracle;
Oracle public oracleObject;
address public timer;
uint public current_level;
uint public previous_level;
uint public notional;
uint public balance_long;
uint public balance_short;
uint public collateral_margin;
uint public maintenance_margin;
uint public termination_trigger;
uint public long_required;
uint public short_required;
function FutureContract()
{
long = 0;
short = 0;
oracle = 0;
timer = 0;
long_required = 0;
short_required = 0;
current_level = 0;
previous_level = 0;
notional = 0;
balance_long = 0;
balance_short = 0;
collateral_margin = 150;
maintenance_margin = 200;
termination_trigger = 100;
long_required = 0;
short_required = 0;
}
function OfferLong(address oracle_, address timer_, uint notional_)
{
if ((long != 0)||(short != 0))
{
throw;
}
if (msg.value < notional_ * maintenance_margin)
{
throw;
}
notional = notional_;
long = msg.sender;
short = 0;
oracle = oracle_;
oracleObject = Oracle(oracle_);
timer = timer_;
balance_long = msg.value;
balance_short = 0;
long_required = 0;
short_required = 0;
current_level = oracleObject.GetLevel();
previous_level = current_level;
}
function TradeShort()
{
if ((long == 0)||(short != 0))
{
throw;
}
if (msg.value < notional * maintenance_margin)
{
throw;
}
short = msg.sender;
balance_short = msg.value;
}
function UpdateMarginRequirements()
{
if (balance_long < notional*collateral_margin)
{
long_required = notional*maintenance_margin - balance_long;
}
else
{
long_required = 0;
}
if (balance_short < collateral_margin)
{
short_required = notional*maintenance_margin - balance_short;
}
else
{
short_required = 0;
}
}
function PostMargin()
{
if (msg.sender == long)
{
balance_long += msg.value;
}
else if (msg.sender == short)
{
balance_short += msg.value;
}
else
{
throw;
}
UpdateMarginRequirements();
}
function GetBalance() constant returns (uint balance)
{
if (msg.value > 0)
{
throw;
}
if (msg.sender == long)
{
balance = balance_long;
}
else if (msg.sender == short)
{
balance = balance_short;
}
else
{
throw;
}
}
function GetRequiredMargin() constant returns (uint required)
{
if (msg.value > 0)
{
throw;
}
if (msg.sender == long)
{
required = long_required;
}
else if (msg.sender == short)
{
required = short_required;
}
else
{
throw;
}
}
function UpdateCurrentLevel()
{
if (msg.sender == timer)
{
previous_level = current_level;
current_level = oracleObject.GetLevel();
balance_long += notional*(current_level - previous_level);
balance_short -= notional*(current_level - previous_level);
if ((balance_long < notional*termination_trigger)||(balance_short < notional*termination_trigger))
{
StopOut();
}
UpdateMarginRequirements();
}
else
{
throw;
}
}
function StopOut()
{
if ((msg.sender != long)&&(msg.sender != short))
{
throw;
}
if (balance_long < 0)
{
balance_short += balance_long;
balance_long -= balance_long;
}
else if (balance_short < 0)
{
balance_long += balance_short;
balance_short -= balance_short;
}
long.send(balance_long);
short.send(balance_short);
selfdestruct(oracle);
}
}
A simplified version of the contract, found below, corrects that problem, although it is still not watertight. Note that the function that distributes the money now updates the record first, then attempts to send the money, and throws an exception if the send fails.
pragma solidity ^0.4.0; contract FutureContract { address public long; address public short; address public updater; uint public current_level; uint public notional; uint public balance_long; uint public balance_short; uint public termination_trigger; uint public maintenance_margin; uint public stopout; function FutureContract() { long = 0; short = 0; updater = 0; current_level = 0; notional = 0; balance_long = 0; balance_short = 0; termination_trigger = 100; maintenance_margin = 200; stopout = 0; } function OfferLong(address updater_, uint notional_, uint current_level_) { if ((long != 0)||(short != 0)) { throw; } if (msg.value < notional_ * maintenance_margin) { throw; } notional = notional_; long = msg.sender; short = 0; updater = updater_; balance_long = msg.value; balance_short = 0; current_level = current_level_; } function TradeShort() { if ((long == 0)||(short != 0)) { throw; } if (msg.value < notional * maintenance_margin) { throw; } short = msg.sender; balance_short = msg.value; } function PostMargin() { if (msg.sender == long) { balance_long += msg.value; } else if (msg.sender == short) { balance_short += msg.value; } else { throw; } } function GetBalance() constant returns (uint balance) { if (msg.value > 0) { throw; } if (msg.sender == long) { balance = balance_long; } else if (msg.sender == short) { balance = balance_short; } else { throw; } } function UpdateCurrentLevel( uint current_level_ ) { if (stopout == 1) { throw; } if (msg.sender == updater) { uint previous_level = current_level; current_level = current_level_; balance_long += notional*(current_level - previous_level); balance_short -= notional*(current_level - previous_level); if ((balance_long < notional*termination_trigger)||(balance_short < notional*termination_trigger)) { stopout = 1; if (balance_long < 0) { balance_short += balance_long; balance_long -= balance_long; } else if (balance_short < 0) { balance_long += balance_short; balance_short -= balance_short; } } } } function StopOut() { if ((msg.sender != long)&&(msg.sender != short)) { throw; } stopout = 1; } function ReceiveFunds() { uint256 amt = 0; if (stopout == 0) { throw; } if (msg.sender == short) { amt = balance_short; balance_short = 0; if (!short.send(amt)) throw; } if (msg.sender == long) { amt = balance_long; balance_long = 0; if (!long.send(amt)) throw; } } }
In the next post, I will focus on the recent developments in the Ethereum network, which have significantly affected the contract transaction processing.
Comments