new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Smart Contract Auditor · all subjects

Reentrancy

5 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

Is this withdraw function reentrant?

The canonical vulnerable pattern: `function withdraw(uint amount) { require(balances[msg.sender] >= amount); (bool ok,) = msg.sender.call{value: amount}(""); require(ok); balances[msg.sender] -= amount; }`. State is updated AFTER the external call, so a receiver contract's `receive()` re-enters `withdraw()` while the balance is still intact and drains the contract in a loop. Detection signal: any `call{value:}`, `transfer`, or `send` that precedes a state write to the same variable the call depends on. Fix: checks-effects-interactions (zero/decrement the balance before the call), or OpenZeppelin `ReentrancyGuard` (`nonReentrant` modifier), or pull-over-push so users claim funds instead of being sent them. This is exactly the 2016 The DAO bug (~3.6M ETH, led to the Ethereum hard fork).

How did cross-function reentrancy drain Cream Finance in 2021?

Reentrancy guards that protect a single function fail when the attacker re-enters through a DIFFERENT function that reads the stale state. Cream Finance (August 2021, ~$18.8M) lost funds to the AMP token's ERC-777-style hook: `_tokensReceived` fired on transfer, letting the attacker re-enter a borrow function before the first borrow's collateral accounting settled, borrowing repeatedly against the same collateral. Detection signal: shared state (balances, collateral, exchange rates) mutated in function A and read in function B, where A makes an external call mid-update. A `nonReentrant` mutex covering all functions touching that state is the fix, not per-function guards. ERC-777 `transfer`/`transferFrom` hooks are reentrancy vectors even for "safe-looking" tokens — treat hook-capable tokens like external calls.

What is read-only reentrancy and why do Curve pools have it?

Read-only reentrancy: the callback target doesn't re-enter a state-mutating function; it calls a VIEW function that returns stale state mid-transaction. Classic case: Curve's `remove_liquidity` sends ETH (triggering a callback) before updating pool internals; during that window `get_virtual_price()` returns an inflated value. Protocols using Curve LP tokens as collateral (and reading `get_virtual_price` for pricing) could be exploited — this pattern forced many integrations to patch in 2022-2023 even though Curve itself was safe. Detection: your contract (or an integrated one) reads another protocol's view function for pricing while that protocol can hand control flow to an attacker (ETH transfers, token hooks). Fix: use reentrancy-aware price oracles, Curve's later `price_oracle`-style TWAP getters, or a reentrancy lock wrapper around the read.

Does nonReentrant protect against cross-contract reentrancy?

No. OpenZeppelin's `ReentrancyGuard` only serializes calls within ONE contract instance. If contract A (guarded) calls contract B, and B calls back into a different guarded function of A, the guard works; but if the attacker re-enters a THIRD contract C that shares derived state with A (e.g., a pricing module, a vault A reads shares from), no guard helps. Real-world shape: vault share-price manipulations where the attacker re-enters the oracle/accounting contract, not the vault holding the guard. Audit approach: map the full call graph across protocol boundaries; any external call in a state-update window must be flagged even behind `nonReentrant`. Also note the gas footgun: `ReentrancyGuard` pre-5.x costs an extra SSTORE per guarded call; transient-storage guards (EIP-1153, post-Cancun) are cheaper — as of early 2026 OZ ships both variants.

Are ETH transfer() and send() still safe against reentrancy?

`transfer()`/`send()` forward only 2300 gas, historically enough to log but not re-enter. After several hard forks changed opcode gas costs (Istanbul 2019 made SLOAD 800 gas; Berlin 2021 repriced cold/warm access), 2300 gas no longer guarantees a callback can't mutate state — and future repricing could break the assumption again. Consensys and OpenZeppelin have advised against relying on the 2300 stipend since 2019. Rule: never treat `transfer()` as a reentrancy defense; use checks-effects-interactions plus a guard regardless of which send primitive you use. Detection signal: code that deliberately chose `transfer()` with a comment like "safe, only 2300 gas" — that's an audit finding about intent, not about the line itself. Prefer `call{value:}` with CEI and a `nonReentrant` lock, since `transfer` also breaks multisig/contract wallets that need gas in `receive()`.

Give your agent this brain