Blog

Classic Bug: Unchecked return value of low-level calls



Blog Article: Unchecked Return Values in Solidity – The Silent Vulnerability

Introduction to Unchecked Return Values in Solidity
Smart contracts are the backbone of decentralized applications on Ethereum. Developers use various methods to interact with other contracts, from high-level calls to low-level functions like call(), delegatecall(), and staticcall(). Although these low-level functions offer enhanced flexibility, they also pose significant risks if their return values are not properly checked.
One of the most dangerous vulnerabilities in Solidity is neglecting to verify the boolean return value from low-level calls. Unlike high-level function calls, which automatically revert on failure, low-level calls return false if something goes wrong and allow execution to continue—potentially leading to silent failures and catastrophic security breaches.
Understanding Low-Level Call Functions
Solidity provides several low-level functions for external interactions:
  • call(): Sends Ether and calls functions in external contracts.
  • delegatecall(): Similar to call(), but executes code in the context of the calling contract.
  • staticcall(): Executes external code without modifying state.
Each of these functions returns a boolean indicating success or failure. If unchecked, a failed call can cause a critical flaw without triggering an automatic revert.
Consider the following dangerous pattern:
In this example, the return value from call() is ignored. If the transfer fails (for reasons such as insufficient gas or a recipient without a proper fallback function), the function continues executing as if the call succeeded.
The Vulnerability Explained
Examine this simplified snippet:
Here, the contract marks the recipient as "claimed" before attempting to send Ether. If the call() fails, the recipient remains marked as claimed even though no funds were transferred—effectively causing a silent failure that can be exploited.
For example, in a refund scenario:
An attacker could force the refund to fail (by using a contract without a fallback or by controlling gas limits), leaving the refund marked as processed while the funds remain trapped.
This vulnerability is particularly dangerous because it doesn’t require sophisticated manipulation—simply controlling gas or using a malicious contract can trigger the failure.
For a deeper understanding of how these vulnerabilities impact real-world contracts, explore our comprehensive audit services at Bailsec.
How to Fix Unchecked Return Values
Properly handling return values from low-level calls is straightforward but critical for security. Here are several solutions:
Solution 1: Check the Return Value and Revert on Failure
Solution 2: Use the OpenZeppelin Address Library
The OpenZeppelin Address library offers a safer alternative. Its sendValue() function checks the return value and reverts on failure:
Solution 3: Follow the Checks-Effects-Interactions Pattern
A common practice to mitigate reentrancy is to use the Checks-Effects-Interactions pattern. However, note the subtle pitfall:
This pattern shows that if the call fails, the recipient is still marked as claimed, meaning they lose funds even though the transfer did not occur. In such cases, it might be preferable to update state only after a successful external interaction or to implement a rollback mechanism.
For additional insights and best practices, review our detailed workflow documentation at Bailsec.
Best Practices for Handling Low-Level Calls
To avoid vulnerabilities from unchecked return values, consider these guidelines:
  1. Always Check Return Values:
  2. Never ignore the boolean returned by low-level calls.
  3. Use require Statements:
  4. Immediately revert the transaction if the call fails.
  5. Plan State Changes Carefully:
  6. Update state variables only after successful interactions or use rollback mechanisms.
  7. Leverage Battle-Tested Libraries:
  8. Use libraries like OpenZeppelin's Address library that handle return checks for you.
  9. Implement Comprehensive Tests:
  10. Create test cases that cover edge scenarios and various gas limits.
  11. Conduct Thorough Security Audits:
  12. Have experienced professionals review your code to catch subtle vulnerabilities. Our audit services at Bailsec can help you safeguard your contracts.
Beyond the Basic Fix: Additional Security Considerations
Even after handling return values correctly, consider the following:
  • Gas Stipends and Out-of-Gas Scenarios:
  • When transferring Ether, be mindful of gas limits. For instance:
  • Reentrancy Risks:
  • External calls can open up reentrancy vulnerabilities. While the nonReentrant modifier is useful, always analyze the risk in context.
For a comprehensive review of reentrancy protection and other smart contract security measures, visit our security reviews page.
Real-World Examples: Learning from Past Mistakes
Historical vulnerabilities illustrate the dangers of unchecked return values:
  • The DAO Hack:
  • Although primarily a reentrancy attack, unchecked return values contributed to the overall vulnerability.
  • Parity Multisig Wallets:
  • Flaws in handling return values resulted in the loss of millions of dollars’ worth of Ether.
  • EtherDelta Smart Contract:
  • An unchecked call led to silent failures that caused users to lose funds.
For more detailed case studies and analyses, read our blog where we delve into these incidents.
Conclusion: Don't Let Silent Failures Steal Your Funds
Unchecked return values in low-level calls represent one of the most insidious vulnerabilities in Solidity programming. Without proper checks, a failed external call can go unnoticed—resulting in silent fund losses and security breaches.
As smart contract developers, remaining vigilant against such vulnerabilities is critical. By rigorously checking return values, adhering to secure design patterns, and leveraging robust libraries and audits, you can build more secure and reliable DeFi applications.
Remember: in smart contracts, what you don’t check can hurt you the most. For professional security assessments and to ensure your smart contracts are resilient against such vulnerabilities, consider consulting with the experts at Bailsec through our integrated security services.