📋 What Is It?
Unchecked External Calls occur when a contract makes a low-level call to another address-to send ether, invoke a function, or move tokens-and then ignores whether that call actually succeeded. On the EVM, not every failure reverts: send, call, delegatecall, and staticcall signal failure by returning false instead of throwing. If you never read that boolean, a failed call looks identical to a successful one-so the contract marches forward on a false assumption, stranding funds or crediting phantom balances.
⚠️ Common Exploits
- Ignored call bool:
call{value:}return dropped; failed ETH transfer looks fine - Unchecked send:
send()returns false, never read, payment marked "paid" - Assumed ERC-20 revert: non-standard token returns false → phantom deposit credited
- USDT-class no-return tokens: strict bool decode reverts and bricks the integration
- Masked delegatecall: success flag discarded; a reverting impl treated as success
- Push-loop griefing: one reverting recipient blocks a whole payout loop
- 2300-gas stipend:
transfer/sendbreak valid contract recipients
🔴 Attack Flow
↓
2. Become the recipient, the token, or the depositor
↓
3. Make the call fail (reject ETH / return false)
↓
4. Victim updates state as if it succeeded
↓
5. IMPACT: Stuck funds, phantom credit, corrupted books!
❌ Vulnerable Code
✅ Secure Code
✓ Prevention Checklist
- Capture and require the bool of every low-level call
- Use SafeERC20 for transfer/transferFrom/approve
- Follow Checks-Effects-Interactions ordering
- Prefer pull-over-push for multi-recipient payments
- Verify returned data, not just the success flag
- Guard delegatecall with an
impl.code.length > 0check - Measure real balance delta for fee-on-transfer tokens
- Prefer checked
call+ reentrancy guard overtransfer/send - Surface failures with custom errors, never swallow them
- Treat the compiler "return value not used" warning as an error
- Detect and handle partial failures in batch calls
- Assert internal accounting matches on-chain balances
🔍 Detection & Tools
Analysis Tools:
Secure Libraries:
How to Test:
- Run Slither detectors:
unchecked-lowlevel,unchecked-send,unchecked-transfer - Route every transfer through a Rejector contract (no payable fallback)
- Test with a token that returns
falsewithout reverting - Test with a USDT-class token that returns no data
- Test with a fee-on-transfer / deflationary token
- Add invariant tests: internal balances == actual balances
🌍 Real-World Incident Classes
- Silent-transfer-failure accounting bugs: a withdrawal zeroes the internal balance then sends ETH with an unchecked
send/call; an incompatible recipient makes it fail and funds are stranded while the books say "paid". - Non-standard token integrations: vaults/DEXes assuming strict ERC-20 credit phantom deposits for tokens that return
falseor no data - the reason SafeERC20 exists. - Masked delegatecall / proxy failures: executors and multicalls that discard the success flag record upgrades or actions that never happened.
- Push-payment DoS: a single reverting or gas-heavy recipient in a distribution loop bricks payouts for everyone else.
Note: unchecked calls are among the most frequently reported findings in Solidity audits. Exact loss figures vary by report; the durable lesson is that a single missing require can either strand or steal funds.
📌 Quick Tips
- DO NOT discard a low-level call's return value
- DO NOT assume ERC-20 reverts on failure
- DO NOT update state before confirming the call
- DO capture and require the success bool
- DO use SafeERC20 for all token movements
- DO prefer pull-over-push payments
- DO validate returned data and contract existence