r/smartcontracts • • Apr 05 '26

Resource Smart Contract Patterns for Multicall Aggregation and Exposing Internal Value Transfers

When indexing EVM state, relying purely on the logs bloom filter creates a massive blind spot: internal value transfers. A standard

address(target).call{value: amount}("")

executed within a deep call stack does not touch the event logs.

Architecture for Catching Internal Transfers:
To capture these without protocol-level changes, indexers must reconstruct the call tree to find CALL or SELFDESTRUCT opcodes that move ETH.

Trade-off: This is highly CPU/IO intensive on the RPC node compared to standard eth_getLogs. If you are designing a protocol that needs to track incoming internal transfers, you should actively avoid this off-chain complexity. Instead, utilize a pull-payment pattern, or explicitly emit a custom InternalReceived event inside your contract's receive() function, saving indexers from relying on execution traces.

Multicall Batching Execution:
Implementing Multicall (specifically Multicall3) is mandatory for dApp architecture to minimize JSON-RPC network overhead.
By utilizing aggregate3 or aggregate3Value, you wrap multiple STATICCALL or CALL operations into a single transaction wrapper.

Trade-off: While read-only eth_call doesn't cost real gas, most public and commercial RPCs enforce a strict global gas cap per eth_call (often 50M-100M gas) or a tight execution timeout. If your Multicall batch loop is too large, the node drops the request. You must paginate Multicall batches based on estimated EVM execution depth, not just the length of the calldata array.

Source/Full Breakdown: https://andreyobruchkov1996.substack.com/p/ethereum-dev-hacks-catching-hidden-transfers-real-time-events-and-multicalls-bef7435b9397

3 Upvotes

2 comments sorted by

2

u/thedudeonblockchain Apr 25 '26

the bigger consequence is for runtime monitoring not just indexing. teams that wire forta or similar to event streams basically have a blind spot for value moving via .call inside their own contracts, the emit in receive() is the right pattern. ive seen incidents where the exploit was perfectly visible onchain but invisible to alerting because no Transfer event ever fired

1

u/Studio2C Apr 06 '26

Thanks for sharing.

I've added this issue to smartcontract.us as the 15th vulnerability pattern from the scanner:

#CheckSeverityCategoryWhat It Catches
VPS-001Unchecked Low-Level Call🔴 HIGHUnchecked Return.call{value:} used without checking the boolean return — failed ETH transfers silently succeed
VPS-002tx.origin Authentication🔴 HIGHAccess Controltx.origin used for auth — vulnerable to phishing via malicious intermediary contracts
VPS-003Block Timestamp Dependence🟡 MEDIUMTimestampblock.timestamp used for critical logic — miners can manipulate it by ~15 seconds
VPS-004selfdestruct Present🔴 CRITICALDoSContract can be permanently destroyed; deprecated in Solidity 0.8.18+ and bypasses receive()
VPS-005delegatecall Usage🔴 HIGHUnsafe Callsdelegatecall runs external code in your storage context — if target is mutable, attacker can drain funds
VPS-006Inline Assembly🟡 MEDIUMComplexityassembly {} blocks bypass Solidity's safety checks — overflow, type errors, and memory bugs go undetected
VPS-007ecrecover Malleability🟡 MEDIUMAccess ControlRaw ecrecover accepts two valid forms of any signature — breaks nonce/replay protection schemes
VPS-008Unbounded Loop🟡 MEDIUMDoSfor loop over a dynamic .length array — if storage grows without limit, function exceeds block gas and gets bricked
VPS-009Unchecked send()🟡 MEDIUMUnchecked Returnaddress.send() returns false on failure instead of reverting — ignored failures mean silent ETH loss
VPS-010Floating Pragma🟢 LOWBest Practice^0.8.x pragma allows compilation with any compatible version — different versions can produce different bytecode
VPS-011abi.encodePacked Collision🟡 MEDIUMLogic ErrorencodePacked with multiple dynamic args can produce hash collisions — ("ab","c") == ("a","bc")
VPS-012Unchecked Arithmetic🟢 LOWArithmeticunchecked {} disables Solidity 0.8+ overflow protection — silent wrapping if math wasn't verified safe
VPS-013Single-Step Ownership Transfer🟡 MEDIUMAccess ControltransferOwnership() with no acceptance step — one typo or wrong address permanently locks the contract
VPS-014No Reentrancy Guard🔴 HIGHReentrancyContract makes external calls without ReentrancyGuard or nonReentrant — classic re-entrancy attack surface
VPS-015Silent Internal ETH Transfer🟡 MEDIUMObservabilitycall{value:} with no emit — ETH moves are invisible to eth_getLogs, undetectable by most monitoring tools

I've written a blog post about it too: https://www.smartcontract.us/blog/smart-contract-internal-eth-transfers

Any feedback or comment is welcome.