Last updated: March 11, 2025
SCWE-004: Uncaught Exceptions
Stable Version v0.0.1
This content is in the version-(v0.0.1) and still under active development, so it is subject to change any time (e.g. structure, IDs, content, URLs, etc.).
Send Feedback
Relationships
Description
Uncaught exceptions occur when a smart contract fails to properly handle errors, leading to unintended behaviors, lost funds, or denial of service (DoS). In Solidity, low-level calls such as call()
, delegatecall()
, and send()
return a boolean instead of reverting automatically, requiring explicit checks. Ignoring return values or using assert()
instead of require()
can result in vulnerabilities that attackers can exploit.
Common issues with uncaught exceptions:
- Ignoring return values of call()
, delegatecall()
, send()
- Unchecked external contract calls
- Incorrect use of assert()
instead of require()
- Failure to catch errors in try/catch
blocks
- Check Return Values: Always verify the return values of low-level calls to ensure execution succeeded.
- Use
require()
for Input Validation: require()
should be used for checking user input, while assert()
should only be used for invariants.
- Implement Try/Catch: Use
try/catch
when interacting with external contracts to prevent silent failures.
- Use Safe Libraries: Utilize OpenZeppelin’s
SafeERC20
for secure token transfers.
- Minimize External Dependencies: Avoid relying on external contract behavior unless necessary.
Examples
Example 1: Uncaught Exception:
pragma solidity ^0.8.0;
contract UncheckedCall {
function execute(address target, bytes memory data) public payable {
target.call{value: msg.value}(data); // ❌ No success check
}
}
In the above example, call()
returns a boolean indicating success, but it is ignored, which can lead to silent failures.
Refactored to Handle Exceptions Properly:
pragma solidity ^0.8.0;
contract SafeCall {
function execute(address target, bytes memory data) public payable {
(bool success, bytes memory returnData) = target.call{value: msg.value}(data);
require(success, "Call execution failed"); // ✅ Handle failure
}
}
In this improved version, the contract checks whether call()
was successful, preventing execution from continuing on failure.
Example 2: Bad Practice (Using assert() Instead of require())
- Problem: Using assert() for input validation instead of require().
pragma solidity ^0.8.0;
contract WrongAssertion {
function withdraw(uint256 amount) public {
assert(amount > 0); // ❌ Assert should not be used for input validation
// Withdraw logic here
}
}
Why is this vulnerable?
- assert()
is meant for invariants, not input validation.
- If assert()
fails, it consumes all remaining gas and does not return gas to the caller.
pragma solidity ^0.8.0;
contract CorrectValidation {
function withdraw(uint256 amount) public {
require(amount > 0, "Invalid amount"); // ✅ Proper validation
// Withdraw logic here
}
}
- Now, a proper require()
statement prevents execution if amount is invalid.
Example 3: Bad Practice (Failure to Catch Errors in Try/Catch)
- Problem: Not using try/catch for external contract calls.
pragma solidity ^0.8.0;
interface ExternalContract {
function riskyFunction() external;
}
contract NoErrorHandling {
function callExternal(address contractAddr) public {
ExternalContract(contractAddr).riskyFunction(); // ❌ No error handling
}
}
Why is this vulnerable?
- If riskyFunction() fails, the transaction completely reverts.
- There is no way to recover or take alternative action.
Better Practice (Using Try/Catch for External Calls)
pragma solidity ^0.8.0;
interface ExternalContract {
function riskyFunction() external;
}
contract SafeExternalCall {
function callExternal(address contractAddr) public {
try ExternalContract(contractAddr).riskyFunction() {
// ✅ Success case
} catch {
revert("External call failed"); // ✅ Catch and handle failure
}
}
}
- Now, errors are properly caught, and alternative actions can be taken.