Skip to content
Security
Skill

/unchecked-calls

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),

From plugin
rugproof
952 skills23 agents45 commands4 hooks
Install
$ npx -y skills add omermaksutii/RugProof --skill unchecked-calls --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/unchecked-calls

Context preview

The summary Claude sees to decide when to auto-load this skill.

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),

SKILL.md

unchecked-calls.SKILL.md
name: unchecked-calls
description: 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.

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

| Pattern | Severity | |---|---| | `.call` ignored, leads to silent fund loss | **Critical** | | Non-SafeERC20 with USDT/non-conformant token | **High** | | `target.call` without code-length check | **High** | | Try-catch swallows revert without inspection | **High** | | Multicall fail-skip on revenue path | **High** | | `.transfer` (2300 gas) on L2 / to smart-wallet receivers | **Medium** | | Best-effort fire-and-forget event hook | **Low** *(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]]
Read more
Ships withrugproof

Rugproof your code before someone else does. 🌐 Live site: omermaksutii.github.io/RugProof 📦 Latest: v1.0.0 — 45 commands · 23 agents · 45 skills · 13 MCP servers · tested, offline-first, with rule packs, a benchmark, non-EVM coverage, and post-deploy

Get the whole plugin

Other skills on rugproof.