agentleFS
iktok90-designiktok90-design/ai-smart-contract-auditor/skills/unchecked-calls/SKILL.md

unchecked-calls

Skill36 starsChanged 5 months agoMITView on GitHub

Detect ignored external-call return values — silent failures from low-level call/delegatecall/staticcall, ignored ERC20 transfer return values, return-data length issues. Activate on `.call`, `.delegatecall`, `.staticcall`, `.send`, `transfer`/`transferFrom` (without SafeERC20), and any function returning `bool` whose return is discarded.

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 0 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Always require(ok, "call failed").

    line 24
  2. Very specific:

    Use OZ SafeERC20's safeTransfer which handles missing return values.

    line 31
  3. Very specific:

    If permit reverts (because it was already used), the consuming transferFrom continues with a stale allowance.

    line 54
  4. Very specific:

    Always check (bool ok, bytes memory ret) = target.call(...) and require(ok, ...).

    line 76
  5. Very specific:

    After .call to dynamic addresses, check target.code.length > 0 before relying on return.

    line 78

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Unchecked external calls detection

When this applies

  • Low-level call, delegatecall, staticcall, send
  • ERC-20 transfer / transferFrom / approve (non-SafeERC20)
  • Calls to user-provided addresses
  • Multi-call patterns that don't propagate failures
  • Try-catch swallowing all errors

Detection patterns

Ignored .call return (HIGH)

(bool ok,) = target.call(data);   // ← ok unused
// or worse:
target.call(data);                 // ← Solidity ≥0.5 still allows this with warning

Always require(ok, "call failed"); unless an intentional best-effort.

ERC-20 without SafeERC20 (HIGH)

USDT and other non-conformant tokens don't return bool. Naive call:

IERC20(usdt).transfer(to, amt);   // ← reverts on USDT due to ABI mismatch

Use OZ SafeERC20's safeTransfer which handles missing return values.

Return-data check missing (HIGH)

Even compliant ERC20 returning false instead of reverting is silently passed:

bool ok = token.transfer(to, amt);   // ← ok unused, returns false on failure

try/catch swallows everything (MEDIUM-HIGH)

try external.call() { /* … */ }
catch { /* silently ignored */ }

Without inspecting catch (bytes memory reason), you lose all info; transitioning a critical revert into a silent success is a bug.

Address with no code (HIGH on call)

(bool ok,) = target.call(data);
require(ok);

ok = true even if target is an EOA with no contract — call returns true. Add target.code.length > 0 check.

Permit followed by transferFrom — permit silently revertable (MEDIUM)

If permit reverts (because it was already used), the consuming transferFrom continues with a stale allowance. Use try/catch only to skip the permit reuse case.

Multicall fail-skip (HIGH)

Some routers continue on per-call failure. If a treasury sweeps via multicall, a single failed call could drop revenue.

selfdestruct / transfer no longer guaranteed on L2s with reduced gas

Forwarding 2300 gas is brittle on Berachain/Arbitrum/etc. — see [[dos-vectors]].

Severity rubric

PatternSeverity
.call ignored, leads to silent fund lossCritical
Non-SafeERC20 with USDT/non-conformant tokenHigh
target.call without code-length checkHigh
Try-catch swallows revert without inspectionHigh
Multicall fail-skip on revenue pathHigh
.transfer (2300 gas) on L2 / to smart-wallet receiversMedium
Best-effort fire-and-forget event hookLow (document intent)

Remediation patterns

  • Always check (bool ok, bytes memory ret) = target.call(...) and require(ok, ...).
  • For ERC-20: use OZ SafeERC20 everywhere.
  • After .call to dynamic addresses, check target.code.length > 0 before relying on return.
  • Replace transfer / send with (bool ok,) = to.call{value: amt}("") + check.
  • For best-effort hooks (event-style), document with a comment and consider returning a status code rather than silently succeeding.

False-positive notes

  • A .call whose only side-effect-on-failure is a UX message can be best-effort — Info.
  • Solady / Solmate sometimes use bare call with custom assembly checks — verify the surrounding bytes are inspected.

Related

  • [[token-compatibility]] — non-conformant ERC20s
  • [[dos-vectors]]
  • [[reentrancy]]

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.