paint-brush
Smart Contract Security: Part 1 Reentrancy Attacksby@nkalltheway
5,121 reads
5,121 reads

Smart Contract Security: Part 1 Reentrancy Attacks

by nick256March 14th, 2018
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Ethereum is one of the two largest cryptocurrencies right now, with a market cap of more than 60B dollars, processing 600K+ transactions per day. Using its Turing-Complete high-level programming language, Solidity, people are building smart contracts daily. There is already a large amount of applications running on the main network, from Token systems to wallets, hedging contracts, lotteries etc. Where there is money involved there are malicious actors.

Company Mentioned

Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - Smart Contract Security: Part 1 Reentrancy Attacks
nick256 HackerNoon profile picture

Ethereum is one of the two largest cryptocurrencies right now, with a market cap of more than 60B dollars, processing 600K+ transactions per day. Using its Turing-Complete high-level programming language, Solidity, people are building smart contracts daily. There is already a large amount of applications running on the main network, from Token systems to wallets, hedging contracts, lotteries etc. Where there is money involved there are malicious actors.

When a smart contract is deployed on the blockchain, it can never be altered again. This is why they are called “immutable”. It is therefore extremely important to follow some basic security guidelines, do tons of testing before deploying and keeping the code as simple as possible.

In this series of articles I will try to explain some of the common vulnerabilities as I understand them and provide material, written by experts, for further studying.

Let’s first explain shortly how the call function works. It is used to invoke a function, of another or the same contract, and transfer data and/or ether to it. It does not throw in case of an exception, it just returns false - otherwise returns true. Call triggers code execution and spends all the available gas for this purpose. This is where the problems begin as you will see later. Lets see how this works in practice

I will use this simple Logger contract that contains a logNum function which can be called by anyone. It takes an integer as an argument and maps/logs that number to your address using the _myNum mapping.

Now suppose, that inside another contract I am building, I want to call this logNum function and pass the number 10 to it. The syntax, using call is:

addressOfLogger.call(bytes4(sha3("logNum(uint256)")), 10);

The 4 bytes inside the call method are used as a hash signature, which is used to point to the function we want to invoke. Practically, the first 4 bytes of the logNum’s hash match the hash of the call method and this function is called with the value 10 as an argument.

You can also send ether using the call method. Lets use this simple Test contract to understand how. This one consists of two functions. The buy function that is payable(accepts wei) and the fallback method.

You can interact with the buy function from your own contract and transfer money to it, using the call method, in almost the identical way we used it above. You just need to know the address of the Test contract. In this case you write:

addressOfTest.call.value(amount)(bytes4(sha3("buy()")));

If you wanted to transfer the ether to the fallback method you would write:

addressOfTest.call.value(amount)(); //the parenthesis is empty

The fact that the call method triggers code execution without a gas limit, unless you set one manually, makes it vulnerable to reentrancy attacks. This has led to huge financial losses in the past(DAO hack~70M $/ June 2016).

Reentrancy Attack

The following contract will be used to analyse this attack in detail. Imagine that it represents a wallet contract. You can send ether and store it there. The mapping balanceOf maps your address to the amount of ether you have stored in this wallet(in wei). The withdrawEquity function allows anyone using the wallet to withdraw their balance. This is where the BAD stuff happens.

As we saw earlier the call statement inside withdrawEquity invokes the msg.sender’s fallback function in order to send wei to him. The thing is…it has no gas limitation, so any code inside this fallback function will be executed(as long as there is remaining gas for this purpose). Now a malicious actor can deploy a smart contract looking like this:

Lets examine this one. There is a private address _owner initialised inside the constructor. It is initiated inside the constructor and indicates who the owner of the contract is/who deployed it. The other address is the address of the vulnerable wallet(here for simplicity I used 0x0). Then, an instance of that contract is created and we are almost done.

If the bad guy sent wei, from the Malicious contract to the wallet and then called his fallback function, strange things would happen. When he calls the function, the withdrawEquity function is invoked(line15).

Inside the withdrawEquity, into the require statement the call method contains an empty signature with no gas limitations

msg.sender.call.value(x)()

so it invokes the fallback method of the Malicious contract unless it fails(in case of failure the transaction is reverted).

vul.withdrawEquity();

This is a call to the withdrawEquity function again. Practically the bad guy’s contract receives the ether and then calls the withdrawEquity() again. Since the state of the first contract has not changed (the attacker’s balance is not set to zero yet) he gets paid again. And again, and again… until the execution runs out of gas or the call stack limit is reached.

So if the withdrawEquity gets called lets say 10 times, only the last one will fail because call doesn’t propagate an exception only true/false, so only the executions of the last call will be reverted. This means that the malicious guy got paid x9 times rather than 1. To put this into perspective someone could initially store 2 ether and get 18 ether back. Then do the same with the 18 ether and so on…

How to avoid This

  • transfer() and send() are safe against reentrancy attacks since they limit the code execution to 2300 gas, currently enough to log and event.
  • If you can’t avoid using call() always do the internal work (eg change balances) before using the external call.
  • In general, keep in mind that any function running external code is a threat.

Documentation and sources for further Studying

  1. Solidity Documentation
  2. Consensys Best Practices for Smart Contract Security
  3. Vitalik on security
  4. A survey of attacks on Ethereum smart contracts
  5. How to write Safe Smart Contracts-chriseth
  6. Privacy-Preserving Smart Contracts