Empty Code Detection
Detects external calls to user-controlled addresses without verifying that the target has deployed code, where silent success masks failed interactions.
Empty Code Detection
Overview
Remediation Guide: How to Fix Empty Code Detection
The empty code detection detector identifies external calls to addresses that may not have deployed code. In the EVM, a low-level call to an address with no code silently succeeds (returns true) without executing anything. If a contract relies on the success of that call to confirm that an operation completed (such as a token transfer or a bridge relay), the silent success causes the contract to proceed as if the operation succeeded when nothing actually happened.
Sigvex traces the target address of each external call to determine if it comes from user input or untrusted storage, then checks whether an EXTCODESIZE validation precedes the call.
Why This Is an Issue
Cross-chain bridges are particularly vulnerable: if a bridge contract calls a wrapped token contract at an address that does not exist on the destination chain, the call succeeds silently, and the bridge records the transfer as complete without actually minting tokens. DEX routers face a similar risk when routing through non-existent pool contracts.
Token transfer wrappers like SafeERC20 partially mitigate this by checking return data length, but contracts that use raw call without SafeERC20 are exposed. Proxy contracts that delegatecall to an uninitialized implementation address will also silently succeed, leaving the proxy in an undefined state.
How to Resolve
// Before: Vulnerable — no code check before call
function executeCall(address target, bytes calldata data) external {
(bool success, ) = target.call(data);
require(success, "Call failed"); // Succeeds even if target has no code!
}
// After: Fixed — validate code exists
function executeCall(address target, bytes calldata data) external {
require(target.code.length > 0, "Target has no code");
(bool success, ) = target.call(data);
require(success, "Call failed");
}
Examples
Vulnerable Code
contract VulnerableBridge {
function relayTransfer(address token, address recipient, uint256 amount) external {
// If token address has no code on this chain, call silently succeeds
(bool success, ) = token.call(
abi.encodeWithSelector(IERC20.transfer.selector, recipient, amount)
);
require(success, "Transfer failed");
emit TransferRelayed(token, recipient, amount); // Logged but transfer never happened
}
}
Fixed Code
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract SafeBridge {
using SafeERC20 for IERC20;
function relayTransfer(address token, address recipient, uint256 amount) external {
require(token.code.length > 0, "Token contract not deployed");
IERC20(token).safeTransfer(recipient, amount); // Reverts if no return data
emit TransferRelayed(token, recipient, amount);
}
}
Sample Sigvex Output
{
"detector_id": "empty-code-detection",
"severity": "critical",
"confidence": 0.82,
"description": "External CALL at offset 0xb4 targets a user-controlled address without EXTCODESIZE validation. If the target has no deployed code, the call silently succeeds.",
"location": { "function": "relayTransfer(address,address,uint256)", "offset": 180 }
}
Detection Methodology
- External call identification: Locates
CALL,DELEGATECALL, andSTATICCALLinstructions with user-controlled or storage-loaded target addresses. - EXTCODESIZE check detection: Searches the execution path before each call for
EXTCODESIZEon the target address followed by a zero comparison. - Return data validation: Checks whether the contract validates return data length (as SafeERC20 does), which serves as a proxy for code existence.
- Address source classification: Distinguishes hardcoded addresses (low risk), user-supplied addresses (high risk), and storage-loaded addresses (medium risk).
Limitations
False positives:
- Contracts that use SafeERC20 or similar wrappers may be flagged if the wrapper call is not recognized.
- Addresses stored in immutable variables set during construction are safe but may be classified as storage-sourced.
False negatives:
- Calls through intermediate contracts (routers, multicall) where the final target is user-controlled may not be traced.
- Assembly-level calls with non-standard patterns may be missed.
Related Detectors
- Unchecked Call — detects missing return value checks
- Arbitrary External Calls — detects calls to user-controlled addresses