Empty Code Detection Remediation
Overview
A low-level call to an address with no deployed code returns true and empty return data — it succeeds without doing anything. Code that treats that success as confirmation (a token transfer completed, a bridge minted, a hook ran) proceeds on a false premise. The fix is to require code at the target before relying on a low-level call’s success, and to use a wrapper that also checks return data for token interactions.
Related Detector: Empty Code Detection
Recommended Fix
Before (Vulnerable)
function relay(address token, address to, uint256 amount) external {
(bool ok, ) = token.call(
abi.encodeWithSelector(IERC20.transfer.selector, to, amount)
);
require(ok, "transfer failed"); // true even if token has no code
emit Relayed(token, to, amount); // logged though nothing moved
}
After (Fixed)
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract SafeRelay {
using SafeERC20 for IERC20;
function relay(address token, address to, uint256 amount) external {
require(token.code.length > 0, "token not deployed");
IERC20(token).safeTransfer(to, amount); // reverts on missing return data
emit Relayed(token, to, amount);
}
}
The explicit code.length check rejects codeless targets, and safeTransfer additionally rejects tokens that return false or no data.
Alternative Mitigations
Require non-empty return data — when a call is expected to return a value, check returndatasize() (or decode the bool) rather than trusting the success flag alone. A codeless target returns zero bytes, which fails the decode.
Pin trusted targets — for contracts that interact with a known set of addresses (a specific token, a specific bridge endpoint), store those addresses as immutable or in admin-controlled config and validate against the list, instead of accepting an arbitrary target from calldata.
Common Mistakes
Relying on SafeERC20 with a non-existent token — SafeERC20 guards against missing/false return data, but a codeless address produces no return data, which it correctly rejects only because of that check; pair it with the explicit code.length check when the address itself is untrusted, so the failure mode is unambiguous.
Checking code existence on the wrong chain — in cross-chain designs, validate that the target is deployed on the chain where the call executes; an address that has code on the source chain may have none on the destination.