Talking about blockchain (24): Security vulnerabilities of smart contracts

Talking about blockchain (24): Security vulnerabilities of smart contracts

Smart contracts are becoming the most important programming language in the next decade. Its emergence has overturned the centralized programming method of nearly 30 years, bringing an open, transparent, tamper-free, and trustworthy operating environment to the entire society, but at the same time it also faces unprecedented technical challenges and security vulnerabilities that are difficult to prevent. Some of these challenges and vulnerabilities come from the design itself, while others come from a new, distributed operating environment. This article will sort out the security vulnerabilities that have appeared or are known in the history of Ethereum smart contracts to warn you who are or will be working on smart contracts.

Note: This article uses Solidity as the programming language for smart contracts.

Let’s look at the following two pieces of code:

 address addr = 0x6c8f2a135f6ed072de4503bd7c4999a1a17f824b;
 if(!addr.call.value(20 ether)()){
throw;
}

as well as:

 address addr = 0x6c8f2a135f6ed072de4503bd7c4999a1a17f824b;
if(!addr.send(20 ether)){
throw;
}

Both of these codes send 20 ethers to the contract address 0x6c8f…. The second code has no vulnerability, but the first code has a serious security vulnerability. Why?

Let's first look at the difference between addr.call.value()() (note: there are two brackets, the first bracket is the assignment of how much ether to transfer, and the second bracket is the method call) and addr.send(). Both send ether to a certain address, and both are new message calls. The difference is that the gaslimit of these two calls is different. send() gives 0 gas (equivalent to call.gas(0).value()()), while call.value()() gives all (currently remaining) gas.

Note: If the fallback function needs to be called without any gas, EVM will automatically adjust the gas to no more than 2300.

Security vulnerability 1: fallback function

When we call a smart contract, if the specified function cannot be found, or if we do not specify which function to call (such as sending ether), the fallback function will be called.

The fallback function is designed not to do too much, and a reasonable approach is to print some logs (or events) in the fallback function to notify the client (usually web3.js) of some relevant information. So if you don't send any gas to the call (just like send()), the system will default to an upper limit of 2300 gas to execute the fallback function.

But when you send ether via addr.call.value()(), the situation is different. Like send(), the fallback function will be called, but the available gas passed to the fallback function is all the remaining gas (which may be a lot), and then the fallback function can do a lot of things (such as writing storage, calling a new smart contract again, etc.). A well-designed fallback for malicious purposes can do many things that harm the system.

So to avoid this security vulnerability, the conclusion is: always use send() to send ether instead of call.value() .

Security vulnerability 2: recursive

Look at the following code:

 function withdrawBalance() {
     amountToWithdraw = userBalances[msg.sender];
     if(amountToWithdraw > 0){
         if (!(msg.sender.call.value(amountToWithdraw)())) { throw; }
         userBalances[msg.sender] = 0;
}
}

This is a code that allows users to withdraw deposits from your smart contract at one time. For example, your contract account has a total of 1,000 ethers, and a user has 10 ethers. Because the code has a serious recursive call vulnerability, the user can easily withdraw all 1,000 ethers in your account.

First, this code uses addr.call.value()() to send ether instead of send(), which provides enough gas for the fallback function call. You can take away all the ether by writing the fallback function as follows:

 function () {
     address addr = 0x6c8f2a135f6ed072de4503bd7c4999a1a17f824b;
if(COUNT<100){
         addr.call("withdrawBalance");
COUNT++;
}
}

In this fallback code, when the counter is less than 100, the withdrawBalance function is called recursively. In this case,

 msg.sender.call.value(amountToWithdraw)()

Will be called 100 times, taking away 100*10 ether.

So when writing a smart contract, you need to consider that it may be called recursively. In this case, we can adjust the code like this to prevent problems caused by recursive calls:

 function withdrawBalance() {
     amountToWithdraw = userBalances[msg.sender];
     userBalances[msg.sender] = 0;
     if(amountToWithdraw > 0){
         if (!(msg.sender.call.value(amountToWithdraw)())) {
              userBalances[msg.sender] = amountToWithdraw;
throw;
}
}
}

Security vulnerability 3: call depth limit

The call depth is limited to 1024. In EVM, a smart contract can call other smart contracts through message calls, and the called smart contract can continue to call other contracts through message calls, or even call back (recursive). The depth of nested calls is limited to 1024.

Consider the following code:

 function sendether(){
     address addr = 0x6c8f2a135f6ed072de4503bd7c4999a1a17f824b;
addr.send(20 ether);
     //you think the send should return true
var thesendok = true;
     //do something regarding send returns ok
...
}

And the other party's fallback function is defined as:

 function(){
//do nothing
}

You think your code is safe because the other party has clearly defined the fallback method. But you are wrong. The attacker only needs to create 1023 nested calls and then call sendether() to make add.send(20 ether) fail and other executions succeed. The code is as follows:

 function hack(){
var count = 0;
while(count < 1023){
         this.hack();//this keyword makes it a message call
count++;
}
if(count==1023){
         thecallingaddr.call("sendether");
}
}

So in order to solve the depth limit problem, the correct way to write it should be to check whether the call return is correct every time the call depth increases, as follows:

 function sendether(){
     address addr = 0x6c8f2a135f6ed072de4503bd7c4999a1a17f824b;
     if(!addr.send(20 ether)){
         throw; //somebody hacks me
}
     //you think the send should return true
var thesendok = true;
     //do something regarding send returns ok
...
}

Now that we know about these three basic vulnerabilities in smart contracts, let’s take a look at how hackers used them to steal Ether in the famous The DAO incident, causing the most devastating incident in the history of Ethereum development.

The DAO Vulnerability

The DAO vulnerability is a combination of the first and second vulnerabilities above. Look at the following code:

 function splitDAO(
uint _proposalID,
     address _newCurator)noEther onlyTokenholders returns (bool _success){
...
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;
...
     withdrawRewardFor(msg.sender);
    totalSupply -= balances[msg.sender];
balances[msg.sender] = 0;
paidOut[msg.sender] = 0;
return true;
}

The hacker transferred multiple copies of Ether by calling the following code multiple times:

 p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender)

How did he do it? It’s simple! When the contract is executed:

 withdrawRewardFor(msg.sender);

When it does, it will enter the corresponding function:

 function withdrawRewardFor(address _account)
    noEther internal returns (bool _success){
...
if(!rewardAccount.payOut(_account,reward)) //Vulnerability code throw;
...
}

The payOut function is defined as follows:

 function payOut(address _recipient, uint _amount) returns (bool){
...
if(_recipient.call.value(_amount)) //vulnerability code PayOut(_recipient, _amount);
return true;
}else{
return false;
}
}

You may have understood that this is almost the same as the example we gave earlier. The code sends ether through addr.call.value()() instead of send(), which leaves room for hackers. Hackers only need to create a fallback function and call splitDAO() again in the function.

Maybe you are thinking that if you had read this article a few months earlier, you could have become the hacker of The DAO and transferred millions of dollars worth of Ethereum. In fact, The DAO's vulnerability was obvious at the code level, so it was discovered and warned online before The DAO was implemented, but it was not taken seriously until the attack actually happened!

The DAO incident has had a significant impact on the entire Ethereum community, and the security of smart contracts has become the most important and urgent issue. Only by letting everyone understand the various possible security vulnerabilities in smart contract programming can it become more and more secure in the future!

<<:  Enterprise Ethereum Alliance may adopt a new governance model

>>:  European Parliament: Regulators should give blockchain transactions legal status (download the full report)

Recommend

Is goldfish eyes good?

Are goldfish eyes good for physiognomy? We all kn...

Strong self-discipline, stick to what you should stick to

There are always some people in our lives who hav...

Bitcoin China CEO Bobby Lee attended the TechCrunch Summit to promote Bitcoin

At 4 p.m. on June 9, 2015, Bobby Lee, CEO of Bitc...

Is it true that people with big cheekbones are blessed?

A lot of information can be revealed from a perso...

How to lose money and change your luck

No matter how good-looking you are, it’s useless ...

Is it true that people with small mouths and thin lips often tell lies?

Lying is not a good behavior. People who like to ...

What evil will be revealed after all the schemes are done?

What evil will be revealed after all the schemes ...

Technical analysis of Bitcoin trend after Trump's attack

The gunfire last weekend not only shocked the wor...

Three types of bad luck lines on your hands

Three types of bad luck lines on your hands Lifel...