Safe-Transfer Receiver Callback Reentrancy
- Name: safe-transfer receiver callback reentrancy
- Category: exploit pattern / reentrancy / ERC-721 callback / ERC-1155 callback / ERC-3525 receiver-hook callback / token-delivery callback trust
- Summary: ERC-721, ERC-1155, and ERC-3525
safedelivery methods are not passive asset moves. They hand control to a receiver hook throughonERC721Received,onERC1155Received, oronERC1155BatchReceivedbefore the calling protocol finishes its own state transition. The recurring review mistake is to collapse three different promotions into one vaguethe safe transfer is still part of this actionstory:a receiver-hook callback edge is live at all,the protocol has already published one settled collateral / issuance / identity story before that hook runs, andsome resumed or callback-reachable path still deserves to spend against that story before delivery fully closes. Protocols lose money whensafeTransferFrom()or_safeMint()is treated like a safer transfer while collateral flags, id allocators, series configs, mint caps, issuance ownership, or withdrawal rights remain half-finished across that callback window. The receiver may be attacker-controlled, but that is not required: protocols also break when they safe-transfer assets into their own wrapper / receiver hook and let both the callback and the outer frame account for the same economic event. - What it is:
- A protocol transfers or mints ERC-721 / ERC-1155 / ERC-3525 assets to a contract recipient using a
safepath. - The standard invokes a receiver callback before the outer protocol has sealed its own accounting or identity state.
- That callback may re-enter from an attacker-controlled recipient, or it may execute protocol-owned receiver logic that independently credits value while the outer frame is still live.
- The protocol resumes after the callback and finalizes writes that are now stale, contradictory, or economically invalid.
- The payout comes from converting that stale state into extra withdrawals, extra issuance, forgotten collateral constraints, duplicate minting, or rewritten claim semantics.
- A protocol transfers or mints ERC-721 / ERC-1155 / ERC-3525 assets to a contract recipient using a
- Typical preconditions:
- The vulnerable path sends NFTs or receipt tokens to contract recipients, or safe-transfers wrapped positions into the protocol’s own receiver hook.
- ERC-3525 and similar semi-fungible wrappers are especially easy to misread because only some branches may use ERC-721-style safe-transfer semantics.
- The protocol relies on
safeTransferFrom()/_safeMint()/ batch-safe delivery during solvency-critical or issuance-critical flows. - Important state such as
usingAsCollateral,nextId, supply / cap counters, per-series configuration, orthis transfer is already being accounted forcontext is updated only after the callback. - A sibling entrypoint or receiver-hook code path reachable during the callback still trusts that state as already final.
- The resulting claims can later be redeemed, borrowed against, or withdrawn for real assets.
- Common vulnerable surfaces:
- NFT-collateral withdrawal and liquidation paths in money markets
- ERC-1155 or ERC-721 issuance flows where id allocation, cap accounting, or claim configuration is finalized after mint delivery
- wrapper and reserve products that safe-transfer ERC-721-like or ERC-3525 positions into the protocol itself and also mint claims inside the receiver hook
- vault, auction, or migration code that mints receipt tokens before fixing the economic meaning of those receipts
- split / clone / wrapper flows where
getNextId()orcurrentSupply()is assumed unique or final inside a callback window - any codebase that read the word
safeand stopped treating the call as an attacker-controlled external interaction or a protocol-self callback boundary
- Exploit sequence:
- The attacker arranges for a protocol to deliver ERC-721, ERC-1155, or ERC-3525 assets through a
safepath, either to an attacker-controlled recipient or into a protocol-owned receiver hook that will also run callback logic. - The protocol reaches a
safeTransferFrom()or_safeMint()call before sealing a critical state transition. - The token standard invokes the receiver hook.
- Inside that hook, attacker-controlled code or protocol-owned receiver logic touches a path that still trusts the old collateral state, old id namespace, old cap state, or old claim configuration.
- The attacker mints, withdraws, liquidates, splits, or causes duplicate accounting against that stale view.
- Control returns to the outer frame, which now writes state as if the callback had not already consumed or repurposed the same authority.
- The attacker later cashes the inconsistent state out into real assets, solvency bypass, or mass-issued claims.
- The attacker arranges for a protocol to deliver ERC-721, ERC-1155, or ERC-3525 assets through a
- Variants:
- Collateral-flag desynchronization — a withdrawal or liquidation callback runs before
usingAsCollateralor equivalent debt-gating state is cleared, as in Omni. - Identifier-allocation race — a mint callback runs before
next idorfnftsCreatedis advanced, so another path can reuse the supposedly fresh id and rewrite its economics, as in Revest. - Protocol-self receiver double-accounting — the protocol safe-transfers a position into its own receiver hook, the hook mints or credits value immediately, and the still-live outer frame later mints again for the same incoming transfer, as in Solv’s BRO exploit.
- Mint-cap / issuance-window bypass — a receiver callback re-enters issuance before loop counters, per-wallet caps, or supply guards are fully reflected in state.
- Receipt-meaning rewrite — the callback does not merely withdraw value; it changes what an already minted receipt token means, so a huge existing supply suddenly becomes redeemable.
- Collateral-flag desynchronization — a withdrawal or liquidation callback runs before
- Boundary tests that keep this pattern distinct:
- Classify it here when the decisive callback surface is the standardized ERC-721 / ERC-1155 receiver hook itself. If the exploit would disappear by replacing
safedelivery with a design that finalized state before any receiver callback, this note should do the analytical work. - Stay with broader reentrancy when the callback surface is not specifically a standardized receiver hook but an ERC-777 token hook, raw ETH refund, flash-loan callback, bridge timeout handler, or arbitrary plugin / strategy call.
- Pivot to read-only reentrancy when the callback-bearing protocol mostly stays internally consistent and the loss happens because another protocol reads transient upstream state as if it were final.
- Pivot to shared-standard entrypoint invariant drift when the sharper question is not callback timing but one inherited standard surface still bypassing a moved backing invariant even without a receiver hook race.
- Classify it here when the decisive callback surface is the standardized ERC-721 / ERC-1155 receiver hook itself. If the exploit would disappear by replacing
Three receiver-hook promotions reviewers should not collapse
-
A standardized receiver-hook callback edge is live at all
safeTransferFrom(),_safeMint(), ERC-1155 batch delivery, and ERC-3525 full-position transfer all answer the weak first question:can receiver-controlled code run before this action is over?- Teams still get hurt by stopping here and treating
safeas if the hard accounting work were already done.
-
The protocol had already published one settled collateral / issuance / identity story before that hook ran
- This stronger question is whether collateral flags, id allocators, cap counters, receipt meaning, entitlement consumption, transfer-accounted markers, and series configuration already described a state the system would defend as coherent if execution paused at the callback edge.
- Safe-transfer exploits usually live in the gap between
receiver hook can runandthe protocol had actually finished proving one settled state story.
-
Any resumed or callback-reachable path still deserved to spend against that story before delivery fully closed
- Even if some partial state looks locally plausible, it still needs its own boundary before a sibling mint, withdraw, liquidation, split, wrap, or receiver-side credit path may treat that state as money-good authority.
- The recurring mistake is not only unfinished accounting, but the later silent promotion from
delivery is mid-transitiontoa callback path may still cash out, mint, rewrite meaning, or reuse authority right now.
What sound design looks like
- The protocol treats
safetoken delivery as an external call boundary, not as a housekeeping detail. - Before any receiver hook can run, collateral flags, id allocators, cap counters, receipt configuration, and transfer-accounted context already describe a state that would be safe if execution stopped there.
Function exitis too late; the real boundary isevery receiver-hook edge. - If the protocol must expose a callback window, all sibling paths that could be re-entered authenticate that the relevant state transition is still live and in the expected stage rather than silently spending against a half-published state story.
- When the protocol is itself the receiver, the outer transfer flow and the receiver hook are designed as one state machine with one clearly owned accounting boundary.
next idhelpers, split-series creation, and receipt-configuration writes are treated as one security object rather than scattered across pre- and post-callback code.- Minted receipt tokens are not allowed to exist in a callback-visible half-configured state where their economic meaning can still be overwritten.
- A healthy review can point to one exact place where the incoming safe-transfer becomes economically recognized, and every other callback-reachable path should already see that same recognition state or fail closed. Receiver-hook staged delivery and single-accounting ownership is the sharper positive-design companion when the practical question becomes
what exactly should already be final before delivery, and which one component should own accounting for the transfer?
Detection heuristics
- Grep for
safeTransferFrom,_safeMint,_mint(...); _doSafeTransferAcceptanceCheck(...), ERC-1155 batch-safe acceptance checks, and any ERC-3525 full-value branch that silently falls back to ERC-721-style safe-transfer semantics in solvency- or issuance-critical code. - Trace what state is still pending after those calls: collateral toggles, supply updates, per-user limits, id counters, config writes, entitlement consumption, or
transfer already accounted formarkers. - Ask what other functions a recipient contract can call back into before that pending state lands.
- If the protocol safe-transfers into itself, review the receiver hook and the outer transfer function together and prove that exactly one of them owns issuance / accounting for the incoming position.
- Split branch review by transfer shape. Solv is a good reminder that the vulnerable path may exist only on
transfer whole positionwhile partial-split paths look fine and hide the callback-bearing branch during normal review. - Treat any
getNextId()-style helper as dangerous if it can run while a mint for that same namespace is still in flight. - For NFT lending and wrapper systems, explicitly test contract recipients rather than only EOA recipients; many bugs stay invisible in happy-path UI flows.
- Trace the monetization rail after duplicate accounting. Receiver-hook bugs often pay only because a downstream pool, exchange, reserve, or borrowing path still honors the callback-minted receipt as money-good inventory.
Defenses
- Finalize all critical effects before standardized receiver callbacks can fire.
- Use reentrancy guards on the whole authority boundary, not just on the most obvious top-level function.
- Separate
economic meaning of the receiptfromdelivery of the receipt; configure first, deliver second. - Avoid deriving unique ids, split ids, or supply rights from mutable counters that remain callback-visible.
- When the protocol is also the receiver, ensure the receiver hook cannot independently mint, credit, or settle an event the outer frame still plans to account for.
- Prefer pull-style claims or staged withdrawals when direct safe-delivery would otherwise expose half-finished solvency state.
- Add explicit invariant tests for contract-recipient callbacks in audit harnesses and local-fork exploit simulations.
- High-signal incidents:
- [[../../../incidents/callbacks-reentrancy/omni-protocol-doodles-nftx-safe-transfer-double-reentrancy-2022-07-10|Omni Protocol Doodles / NFTX
safeTransferFromdouble reentrancy (2022-07-10)]] — the NFT-lending exemplar where ERC-721 withdrawal and liquidation callbacks made Omni first honor and then forget NFT-backed debt. - [[../../../incidents/callbacks-reentrancy/revest-finance-erc1155-callback-fnftid-race-config-overwrite-vault-drain-2022-03-27|Revest Finance ERC-1155 callback
fnftIdrace / config-overwrite vault drain (2022-03-27)]] — the issuance-side exemplar where ERC-1155 mint delivery raced the FNFT id allocator and rewrote the economics of millions of already minted receipts. - [[../../../incidents/accounting-issuance-claims/solv-protocol-bitcoinreserveoffering-erc3525-onerc721received-double-mint-reserve-drain-2026-03-05|Solv Protocol BitcoinReserveOffering ERC-3525
onERC721Received()double-mint / reserve drain (2026-03-05)]] — the protocol-self receiver-hook exemplar where one incoming wrapped position was minted once inonERC721Received()and again by the still-live outermint()frame.
- [[../../../incidents/callbacks-reentrancy/omni-protocol-doodles-nftx-safe-transfer-double-reentrancy-2022-07-10|Omni Protocol Doodles / NFTX
- Why this pattern matters:
- It preserves a narrower lesson than generic
reentrancy: the danger is not onlyexternal control came back, buta standards-blessed token-delivery path was mistaken for a safe bookkeeping step. - It also helps keep ERC-721 / ERC-1155 incidents from being flattened into ERC-777 mental models. The callback is on the recipient side of
safedelivery, which changes where the attacker-controlled contract sits in the flow. - Just as importantly, the recipient does not have to be an attacker-owned contract. Solv shows that protocol-owned receiver hooks can become the duplicate-accounting path if the outer frame and the callback both think they own mint authority for the same transfer.
- Finally, it is a useful audit forcing function: if receipt delivery, collateral release, or split-series issuance can reach a contract, the token standard itself may be part of the protocol’s live control plane.
- It preserves a narrower lesson than generic
Failure-prevention checklist
- Treat every ERC-721 / ERC-1155
safedelivery as an untrusted external interaction, even when the destination is a protocol-owned wrapper, escrow, or receiver helper. - Keep the three receiver-hook promotions explicit in code and review:
callback edge exists,one settled collateral / issuance / identity story exists, anda resumed path may spend against that storyare different gates, not one genericsafe transfer in progressstate. - Finalize collateral flags, entitlement-consumed state, id allocators, per-series configuration, mint caps, and supply counters before any receiver hook can run.
- If state cannot be finalized first, gate every callback-reachable sibling entrypoint on an explicit stage variable so the hook cannot reuse stale authority from the outer frame.
- Keep
receipt meaning establishedandreceipt deliveredas separate phases; do not let callback-visible tokens exist before their economics are fully bound. - Ensure protocol-owned receiver hooks and the outer transfer frame have one clear accounting owner for the incoming asset. A transfer should never be mintable, claimable, or credited by both.
- Fuzz contract-recipient paths, not just EOAs, for withdrawals, liquidations, minting, splitting, wrapping, and migration flows that use
safeTransferFrom()or_safeMint(). - Add whole-boundary reentrancy guards where appropriate, especially when multiple top-level functions share the same collateral or issuance state machine.
- For NFT lending systems, prove that a withdrawal or liquidation callback cannot leave debt checks trusting a pre-callback
usingAsCollateralview once control returns. - For ERC-1155 / ERC-721 issuance systems, prove that
nextId, cap counters, and per-id configuration cannot be observed in a half-advanced state during the acceptance hook. - Prefer pull-based claims, staged withdrawals, or post-configuration delivery when a direct safe-transfer path would otherwise expose unfinished authority.
Audit prompts
- Which exact lines call
safeTransferFrom(),_safeMint(), or ERC-1155 acceptance checks inside solvency-critical or issuance-critical flows? - What state is still pending after that call: collateral toggles, debt gating, id allocation, supply accounting, cap enforcement, configuration writes, or duplicate-accounting guards?
- Can the receiver hook call any sibling entrypoint that still trusts that pending state as already final?
- If the destination is a protocol-owned receiver, what invariant proves the hook and the outer frame cannot both credit the same incoming transfer?
- Are there any
getNextId()-,currentSupply()-, orisUsingAsCollateral()-style reads whose security meaning changes if they are observed inside the callback window? - Do contract-recipient tests cover both attacker-controlled receivers and protocol-self receiver flows, or only happy-path EOAs?
- If a liquidation or withdrawal transfers NFTs out before debt-state finalization, what prevents the callback from restoring collateral elsewhere and returning to a stale cleanup step?
- If a mint callback can re-enter, what prevents it from reusing the same id namespace, cap room, or receipt configuration before the outer frame advances them?
- Is the reentrancy guard placed on the actual authority boundary, or only on one function while sibling functions remain callback-reachable?
- What single invariant proves
token delivery successcannot diverge fromprotocol accounting finalized exactly once?
Lessons that should already be standard
Safein ERC-721 / ERC-1155 / ERC-3525 meansreceiver hook will run, notthis transfer is operationally harmless.Receiver hook is live,one settled collateral / issuance story exists, anda resumed path may still spend against that storyare different security claims.- Receiver callbacks are part of the protocol control plane whenever delivery happens before accounting is sealed.
- Protocol-owned hooks deserve the same suspicion as attacker-owned hooks. Self-reentrancy and duplicate accounting are still reentrancy problems.
- NFT lending systems do not escape callback risk just because they avoid ERC-777. ERC-721 receiver semantics are enough to break collateral accounting.
- Id allocators, cap counters, and per-token configuration are solvency objects when a callback can mint or rewrite claims against them before the outer frame finishes.
- A token should not become visible to a callback before its economic meaning is final. Half-configured receipts are an exploit surface, not a harmless intermediate state.
- If a protocol cannot explain who owns accounting for an incoming safe-transfer at every step of the state machine, it probably has a double-credit or stale-cleanup path waiting to happen.
Internal linkages
- Parent family: reentrancy — use the broader note when the callback surface is not specifically a standardized ERC-721 / ERC-1155 receiver hook.
- Positive-design companion: receiver-hook-staged-delivery-and-single-accounting-ownership — use it when the sharper question is what exactly should already be final before delivery and which one component should own accounting for the transfer.
- Adjacent family: read-only reentrancy — use that note when another protocol turns transient callback-window state into false price or solvency truth.
- Adjacent family: shared-standard entrypoint invariant drift — use that note when the sharper failure is a stale inherited standard entrypoint rather than a receiver-callback race.
- Best concrete cases so far: omni-protocol-doodles-nftx-safe-transfer-double-reentrancy-2022-07-10 for the collateral-withdraw / liquidation version, revest-finance-erc1155-callback-fnftid-race-config-overwrite-vault-drain-2022-03-27 for the issuance / id-allocation version, and solv-protocol-bitcoinreserveoffering-erc3525-onerc721received-double-mint-reserve-drain-2026-03-05 for the protocol-self receiver double-accounting version.
Control surface
-
The leverage sits in the gap between
a receiver-hook callback can run,the protocol has published one collateral / issuance / identity story, andsome callback-reachable path is still allowed to spend against that story. -
In these incidents, the protocol did not merely make an unsafe external call in the abstract; it let a standards-mandated receiver callback observe or reuse unfinished solvency or issuance authority.
-
The sharp review question is not only
does safe delivery happen here?butwhat exact state story had already become money-good before the hook, and what exact path was still allowed to treat it that way during the hook? -
Sources:
- omni-protocol-doodles-nftx-safe-transfer-double-reentrancy-2022-07-10
- revest-finance-erc1155-callback-fnftid-race-config-overwrite-vault-drain-2022-03-27
- solv-protocol-bitcoinreserveoffering-erc3525-onerc721received-double-mint-reserve-drain-2026-03-05
- https://eips.ethereum.org/EIPS/eip-721
- https://eips.ethereum.org/EIPS/eip-1155
- https://eips.ethereum.org/EIPS/eip-3525
- https://samczsun.com/the-dangers-of-surprising-code/
- https://scs.owasp.org/SCWE/SCSVS-COMM/SCWE-138/
-
Last reviewed: 2026-07-30 UTC