Unchecked Return Values

23 January, 2025
article image
Education

Contents

The foundation of the Web3 ecosystem is built upon three core technologies: blockchain, smart contracts, and decentralized applications. While these innovations have ushered in a new era of possibilities, they’ve also become a target for cybercriminals. Hackers relentlessly seek out weaknesses, particularly within the code of smart contracts.

A seemingly insignificant oversight, like failing to verify return values, can trigger catastrophic breaches. In this exploration, we’ll delve into the root causes of this vulnerability and explore effective mitigation strategies.

What are the risks?

The return value of a function in a smart contract acts as an indicator of its success or failure. Ignoring this value is like closing our eyes to the outcome of an operation, potentially allowing the contract to work with incorrect data and lead to unforeseen consequences.

Let’s imagine the following scenario

We have two smart contracts:

  • Contract A: Its primary task is to initiate a specific action. To do this, it calls a function in contract B.
  • Contract B: Performs a specific operation required by contract A. Upon completion, it returns a value indicating the result of the operation: success, failure, or a detailed error description.

What happens if contract A ignores the return value of contract B?

Contract A continues running, assuming that the operation in contract B was successful. However, if there was actually an error, the following actions of contract A can lead to unpredictable and potentially dangerous consequences.

Why does this happen?

When smart contracts interact with each other to perform certain tasks, they use external calls. Different functions like send(), call(), and transfer() are used for this. However, each of these functions has its own way of handling errors:

  • The functions send() and call() return a true or false value, showing if the operation was successful. A common mistake made by developers is ignoring this returned value. If the external call fails, these functions won’t undo the whole transaction, they’ll just return false.
  • Unlike them, the function transfer() will automatically cancel the whole transaction if the external call fails, making it more reliable for sending Ether.

Example with Сode

Let’s look at the whitelistedTokensDistribution contract.

pragma solidity ^0.8.0;

contract whitelistedTokensDistribution {

    bool public isPayoutComplete = false;
    address public whitelistedAddress;
    uint public tokensAmount;

    function sendTokenToWhitelisted public {
        require(!isPayoutComplete);
        whitelistedAddress.send(tokensAmount);
        isPayoutComplete = true;
    }

    function withdrawLeftoverFunds public {
        require(isPayoutComplete);
        msg.sender.send(this.balance);
    }
}

The main problem with this contract is that it uses the send() function without checking if it worked. This creates a security issue: if there’s a problem sending the tokens (like not having enough gas or if there’s a bad function where the tokens should go), the contract will still think the payment was made.

Because of this, anyone can take the leftover money, even if the payment wasn’t actually sent.

Security Best Practices

  • Always check the return values of send() and call() functions. If they return false, something went wrong, and appropriate actions should be taken.
  • Prefer transfer() over send(). It is a safer option as the transaction will be reverted on failure.
  • Use events to track important actions within the contract, especially those related to transfers and errors.
  • Always consider gas consumption when performing operations in the contract. Ensure the user has enough gas to complete the transaction.
  • Leverage well-audited libraries such as OpenZeppelin’s SafeMath or similar tools that provide built-in checks for arithmetic operations and external calls.
  • Instead of direct transfers, use a withdrawal pattern where users must call a separate function to withdraw funds. This pattern allows the contract to better manage state changes and gracefully handle failures.

Regular audits of smart contracts are equally crucial for security, particularly to identify potential risks stemming from unchecked return values.

Identified Vulnerabilities

Let’s take a look at some of the victims who have been affected by this attack.

Conclusion

Telegram
Education

Contents

Telegram

Have a question?

Have a question?

Stay Connected with OXORIO

We're here to help and guide you through any inquiries you might have about blockchain security and audits.