totalSupply_() and _balanceOf() in src/helpers/ERC20Internals.sol read storage via inline assembly and then execute return(ptr, 0x20).
totalSupply_():
_balanceOf()
The Yul return opcode terminates the entire call frame, not just the helper. Any inheriting contract that queries these helpers midway through its logic immediately exits the outer function, skipping every statement, event, and revert that should have followed. Because they are marked internal, downstream extensions will naturally reuse them inside hooks, guarded burns, permit flows, or meta-transaction and will unknowingly drop critical logic.
Complex flows that call _balanceOf/totalSupply_ are unusable, the call appears to succeed (it even returns the stored value), but all state updates and security checks placed after the helper are silently skipped. A burn hook that first reads the balance never performs the actual burn, allowance spending routines can exit before decrementing allowances, and any invariant that relied on post-checks simply never runs. This makes extensions extremely brittle, breaks accounting invariants, and lets adversaries bypass guards by triggering the helper mid-function.
The test test_balanceHelperShortCircuitsExecutionFlow performs a low-level call to burnWithHooks (a created function to show how a realistic extension would call into the helper and unintentionally abort the caller), and then checks those flags plus balances/supply to prove the short circuit.
test/HelperViewsShortCircuit.t.sol deploys HookedToken, a derivative that:
Sets preHookEntered to true, then calls _beforeBurn, which in turn invokes _balanceOf.
Afterwards it intends to set postHookReached, call _burn, and flag burnRoutineReached.
The test test_balanceHelperShortCircuitsExecutionFlow() mints 100 tokens to alice, then (via a low-level call) invokes burnWithHooks(10 ether) and asserts:
the external call succeeded,
postHookReached and burnRoutineReached are still false, proving the function stopped right after _balanceOf,
both alice’s balance and totalSupply remain 100 ether because _burn was never reached.
Running forge test --match-test test_balanceHelperShortCircuitsExecutionFlow reproduces the issue and shows that any logic placed after the helper silently never executes.
Stop using return inside the helper views. Load the slots into return variables so Solidity performs the return,
or reimplement these helpers in Solidity entirely. This preserves control flow for callers and prevents future extensions from aborting mid-execution. Add regression tests that call the helpers before executing additional logic to ensure execution continues.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.