Skip to content

feat(scanner): add unchecked-return value detection plugin - #107

Open
TaiYang-cs wants to merge 1 commit into
VeridionLabs:mainfrom
TaiYang-cs:unchecked-return-plugin
Open

feat(scanner): add unchecked-return value detection plugin#107
TaiYang-cs wants to merge 1 commit into
VeridionLabs:mainfrom
TaiYang-cs:unchecked-return-plugin

Conversation

@TaiYang-cs

@TaiYang-cs TaiYang-cs commented Sep 10, 2026

Copy link
Copy Markdown

Closes #16

What this adds

A new scanner plugin, unchecked-return, that catches the class of bug behind
SWC-104: a call that reports failure by returning false instead of reverting,
where the return value is then thrown away.

That failure mode is quiet and expensive. address.send() and address.call()
do not revert when the callee fails — they hand back a boolean. If nobody checks
it, a payout that never happened looks exactly like one that did, and the books
drift with no transaction to point at. The same applies to ERC-20
transfer / transferFrom / approve, where returning a boolean is part of the
interface but plenty of tokens return false rather than reverting.

How it decides

For every call site the plugin asks one question: is the returned boolean
actually consumed?

  • Low-level calls (.call, .send, .delegatecall, .staticcall) are
    reported as HIGH with 0.9 confidence when the result is not wrapped in
    require / assert / if / while / for, not returned to the caller, and
    not assigned to a variable that is verified later.
  • ERC-20 calls are reported as MEDIUM with 0.65 confidence. That family is
    heuristic by nature, so it does not pretend to the same certainty.

Detection runs against a copy of the source in which comments and string
literals are blanked out while character offsets are preserved. Line numbers stay
exact, and commented-out code or "to.call(...)" inside a string can never
produce a finding. Multi-line statements and expression receivers such as
IERC20(token).transfer(...) are handled, not just plain token.transfer(...).

// reported
recipient.send(amount);
token.transfer(to, amount);

// not reported
require(recipient.send(amount), "send failed");
(bool ok, ) = recipient.call{value: amount}("");
require(ok, "call failed");
token.safeTransfer(to, amount);

Two decisions worth flagging

.transfer() is ambiguous, so it is disambiguated by argument count.
payable(x).transfer(1 ether) is a native ETH transfer and reverts on failure —
flagging it would be a false positive. token.transfer(to, amount) returns a
boolean that is routinely ignored. Rather than guess from the receiver's name,
the plugin counts arguments: one means native and is skipped, two means ERC-20
and is checked. SafeERC20's safe* helpers never match at all.

Built-in plugins are loaded dynamically rather than imported. A static import
in plugin-registry.ts would pull plugins/unchecked-return/src/index.ts into
scanner-core's compilation, which fails under rootDir: ./src (TS6059) — and it
would contradict the note in ARCHITECTURE.md that scanner-core has zero
knowledge of individual plugins. So BUILTIN_PLUGIN_SPECIFIERS maps plugin ids
to module specifiers, loadBuiltinPlugins() imports each one and instantiates
whatever satisfies IRulePlugin, and registerBuiltins() registers them. A
plugin package that cannot be resolved is logged and skipped instead of throwing,
so the registry stays usable in a partial install.

Verification

31 tests cover the plugin — positive, negative and edge cases — plus 7 for the
registry. tsc --noEmit, eslint --max-warnings 0 and prettier --check are
all clean.

Implements the unchecked-return scanner plugin (SWC-104) that detects
low-level calls (.call/.send/.delegatecall/.staticcall) and ERC-20
transfers whose boolean return value is discarded.

- Add plugins/unchecked-return with IRulePlugin implementation and 31 tests
- Register the plugin in scanner-core via a lazy built-in plugin loader
- Declare the new workspace package in scanner-core deps and pnpm-lock
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scanner: Implement Unchecked Return Value detection plugin

1 participant