Revest Finance ERC-1155 callback fnftId race / config-overwrite vault drain (2022-03-27)
- Name: Revest Finance ERC-1155 callback
fnftIdrace / config-overwrite vault drain - Date: 2022-03-27
- Protocol: Revest Finance
- Chain: Ethereum
- Category: reentrancy / ERC-1155 callback reentrancy / identifier-allocation race / FNFT config overwrite / vault drain
- Loss: roughly
$2MacrossBLOCKS,ECO,RENA,LYXe, and smaller token positions; Revest said another roughly$2MofRVSTlosses were prevented by emergency transfer freezing - Summary: Revest let ERC-1155 mint callbacks run before
FNFTHandlerfinalized the nextfnftId. The attacker first minted one zero-value FNFT series with small supply, then minted a second zero-value series with huge supply to their own attack contract. During the second mint’sonERC1155Received()callback, they re-entereddepositAdditionalToFNFT()against the first series withquantity < supply, which made Revest askgetNextId()for a brand-new split series. BecausefnftsCreatedhad not yet been incremented,getNextId()returned the still-in-flight id of the huge second series.TokenVault.handleMultipleDeposits()then overwrote that live series config fromdepositAmount = 0todepositAmount = 1 token, instantly making millions of already-minted worthless FNFTs redeemable for real vault assets. The exploit was not justERC-1155 has callbacks; it wasnew-id uniqueness and series configuration were assumed before the callback window had actually closed. - Trigger / entry point:
- Revest’s
mintAddressLock()path minted ERC-1155 FNFTs throughFNFTHandler.mint()and delivered them to arbitrary recipients, including contracts that could implementonERC1155Received(). FNFTHandlerincrementedfnftsCreatedonly after_mint(...), so the callback window opened whilegetNextId()still pointed at the id being minted.depositAdditionalToFNFT()could create a fresh split series when the requestedquantitywas smaller than the source series supply.TokenVault.handleMultipleDeposits()trusted thatnewFNFTIdreferred to an actually unused series and wrote the updateddepositAmountinto that id’s config without checking whether that series already existed.
- Revest’s
- Root cause:
- Revest violated a classic callback-safety invariant: unique identifier allocation and dependent configuration writes were not finalized before an external callback could re-enter the system.
- The bug was amplified by treating
newFNFTIdas a safe blank slot purely because it came fromgetNextId(), even though nested execution could reach that helper while the original mint for the same id was still live. - Economically, this turned a tiny real deposit into mass issuance of redeemable vault claims: once the giant in-flight series inherited
depositAmount = 1, every pre-minted FNFT in that series became a withdrawal right against real assets.
- Exploit path:
- The attacker deployed contract
0xb480ac726528d1c195cd3bb32f19c92e8d928519and used it as the recipient for newly minted FNFTs so it could re-enter throughonERC1155Received(). - They first called
mintAddressLock()to create a small zero-value series, for examplefnftId = 1036, with supply2anddepositAmount = 0in the target ERC-20. - They immediately called
mintAddressLock()again to create a huge zero-value series, for examplefnftId = 1037, with supply7,700,000forECOor hundreds of thousands for other target tokens, again withdepositAmount = 0. - During the second mint, ERC-1155 delivery to the attack contract triggered
onERC1155Received()beforefnftsCreatedwas incremented. - Inside that callback, the attacker called
depositAdditionalToFNFT(1036, 1e18, 1), targeting only part of the earlier two-token series. - Because
quantity = 1was smaller than the source supply of2, Revest decided it needed a new split series and calledgetNextId()to obtain a supposedly unused id. - Since the outer mint had not yet advanced
fnftsCreated,getNextId()returned1037again: the live id of the giant series already being minted to the attacker. TokenVault.handleMultipleDeposits()then wrotedepositAmount = oldDepositAmount + amount = 1e18into series1037, effectively overwriting the giant zero-value series configuration with a real per-token claim.- Once control returned and minting finished, the attacker held millions of
1037FNFTs, each now redeemable for one whole underlying token despite only one token having been deposited through the split path. - Because the FNFTs were address-locked to the attacker, they could immediately call
withdrawFNFT()and drain the vault’s holdings ofRENA,BLOCKS,ECO,LYXe, and smaller token balances. - Revest froze
RVSTtransfers before the attacker could execute the same play against the protocol’s own token pool, preventing an additional roughly$2Mof loss.
- The attacker deployed contract
- What the attacker needed to be true:
- ERC-1155 mint recipients had to be able to run arbitrary callback code during
_mint(). fnftsCreatedhad to remain stale until after the callback, sogetNextId()could be raced.depositAdditionalToFNFT()had to support partial-supply splits that create a new series id mid-execution.TokenVaulthad to acceptnewFNFTIdas trustworthy and overwrite configuration without proving the slot was actually unused.- The targeted vaults had to contain liquid ERC-20 balances that the now-repriced FNFTs could redeem.
- ERC-1155 mint recipients had to be able to run arbitrary callback code during
- Technical anatomy worth remembering:
- This is a good reminder that
next idis itself security-critical state. If a protocol assumes id uniqueness while an external callback can still ask forgetNextId(), the id allocator becomes a custody surface. - The exploit did not require re-entering the exact same function. It only required a callback into another code path that also trusted the not-yet-finalized id namespace.
- The most important overwritten field was
depositAmount, because that number is the economic truth of one FNFT series: once raised from0to1 token, millions of pre-existing series members became valid withdrawal claims. - BlockSec later showed the incident hid an even simpler adjacent zero-day in
TokenVault.handleMultipleDeposits(): when creating a split series, the function also rewrote the old series’depositAmount, which could have enabled similar inflation without the ERC-1155 callback race if not mitigated quickly.
- This is a good reminder that
- Representative transactions and addresses:
- Revest entry contract:
0x2320A28f52334d62622cc2EaFa15DE55F9987eD9 - LockManager:
0x226124E83868812D3Dae87eB3C5F28047E1070B7 - TokenVault:
0xA81bd16Aa6F6B25e66965A2f842e9C806c0AA11F - FNFTHandler:
0xe952bda8c06481506e4731C4f54CeD2d4ab81659 - Attack contract:
0xb480ac726528d1c195cd3bb32f19c92e8d928519 - First RENA theft transaction:
0xe0b0c2672b760bef4e2851e91c69c8c0ad135c6987bbf1f43f5846d89e691428 - BLOCKS theft / dump transaction cited by Revest:
0x8e2259d6344782c61303d067418fd5c56ce52ed2db4dfab8449a60df385cbcca - ECO theft transaction cited by Revest:
0x19b10c6d38f0b911fdc0e722d681a70a56699d70559eefef3d4d6fe88276c813 - Emergency
RVSTtransfer-freeze transaction:0x18fd1a2b17109c7f2bb7244d99bcfb71d131fd9d522fdc41b8a9fb3cdded9c98 - Blocked follow-up RVST-drain attempt:
0x7bbc01ad66ba9b07fdc82afe4e0c99ffcf317680635d040427afed06b25c8c0f
- Revest entry contract:
- Response / mitigation:
- Revest soft-locked FNFT creation and withdrawal on affected deployments and froze
RVSTtransfers quickly enough to stop the attacker from repeating the exploit against the protocol’s own token. - The direct code fix was to break the classic pattern in
FNFTHandler.mint(): updatefnftsCreatedbefore_mint(...)and add guards against repeated minting of the same FNFT id. - Because BlockSec found an additional critical logic bug in
TokenVault.handleMultipleDeposits(), Revest also deployed a reduced-surfaceRevestentry contract that disabled more complex flows while mitigations were put in place. - The larger lesson is that callback-bearing token standards and
next-idhelpers must be treated as one security object; if either can observe the other half-finished, issuance becomes forgeable.
- Revest soft-locked FNFT creation and withdrawal on affected deployments and froze
Pattern tags
- reentrancy
- safe-transfer-receiver-callback-reentrancy
- receiver-hook-staged-delivery-and-single-accounting-ownership
- exit-right-consumed-state-binding
- The preserved failure was not just
ERC-1155 reentrancy; it was that Revest letseries identity still being createdandseries config now safe to overwritebe treated as the same thing during the mint callback window. - Revest is therefore a clean
receiver-hook callback plus identifier-allocation race plus receipt-meaning rewriteincident, not just a genericvault drain through callbacksanecdote.
Why the exploit worked
FNFTHandler.mint()authenticated the wrong object boundary. It treatedgetNextId()as if it already referred to a future, unused series even though ERC-1155 delivery had not finished andfnftsCreatedstill pointed at the in-flight id.depositAdditionalToFNFT()then gave the attacker a perfect bridge from that half-finished namespace into money-good vault state. A partial-supply split asked for a supposedly fresh id mid-callback, so one tiny real deposit could be rebound onto a giant zero-value series that had already been minted.TokenVault.handleMultipleDeposits()completed the economic rewrite by trustingnewFNFTIdas a blank configuration slot rather than proving the destination series was actually unused. Once it overwrotedepositAmount, millions of already-issued receipts changed from worthless placeholders into valid withdrawal rights.- Withdrawal logic honored the rewritten series config as canonical reserve truth. The exploit therefore paid because receipt issuance, receipt meaning, and vault redemption all disagreed about when a series became final.
- BlockSec’s follow-on zero-day matters because it shows the bug class was broader than one callback edge:
handleMultipleDeposits()also blurred old-series and new-series accounting authority. The callback race was the live exploit path, but the deeper failure was that issuance-splitting and vault-config mutation were not one tightly owned state machine.
Three issuance promotions this incident collapsed
- A not-yet-finalized
next idwas allowed to behave like an actually fresh issuance slot.FNFTHandler.mint()delivered ERC-1155 tokens before advancingfnftsCreated, so the callback window exposed an id that was still onlythe series currently being minted.- That should have remained an unfinished allocator state, not something sibling flows could already consume as
the next blank series.
- That half-finished allocator state was over-promoted into canonical destination-series truth for a split flow.
- During
onERC1155Received(),depositAdditionalToFNFT()askedgetNextId()for a supposedly unused split destination and received the same in-flight id. - The real authorization break was not merely
callbacks exist; it wasthe protocol treated an id still being minted as the canonical fresh destination whose economics were now safe to define or overwrite.
- During
- That rewritten destination-series story still inherited live vault redemption authority.
TokenVault.handleMultipleDeposits()trusted the returnednewFNFTIdas a blank config slot and rewrotedepositAmount, turning millions of already-minted zero-value receipts into valid claims on real vault balances.- The final authority jump was from
this looks like a new split seriestothese existing FNFTs may now redeem real escrowed assets, which is why an allocator race became an immediate vault drain rather than a harmless metadata collision.
Detection and audit pivots
- Search every ERC-1155 / ERC-721 mint or safe-delivery path for
id chosen, callback fires, counter increments later. Treat id allocators, supply maps, and per-series config writes as callback-sensitive solvency state, not as harmless bookkeeping. - For any
getNextId()ornew serieshelper, prove the returned id cannot already have supply, config, or mint-in-progress state.Came from the allocatoris not enough; reviewers should demand an explicitunused slotinvariant. - Review split, migration, deposit-additional, and receipt-upgrade paths together with minting. Revest paid out because a sibling function could create or rewrite a supposedly new series while another series with that id was still being delivered.
- Add adversarial tests where contract recipients re-enter from
onERC1155Received()/onERC721Received()into every path that can create ids, rewrite receipt economics, or move vault balances. - Property-test conservation of redeemable value: after any mint, split, or additional-deposit flow, the total withdrawable underlying across all affected series should equal actual deposited assets, especially when zero-value series and partial-supply splits are allowed.
- Audit vault write helpers like
handleMultipleDeposits()forold seriesversusnew seriesownership confusion. If one helper can update both the source and destination series economics, reviewers should assume a namespace or accounting collision is monetizable until proven otherwise.
Failure-prevention checklist
- Reserve or advance
fnftIdbefore any ERC-1155_mint()can triggeronERC1155Received(). The namespace visible during a callback must already exclude the in-flight series. - Treat
id allocated,series supply minted, andseries economics configuredas one atomic issuance object. If those steps cannot be made atomic, sibling paths should fail closed until the object is fully finalized. - Require explicit blank-slot proofs before writing new-series config: zero supply, zero config, no mint in progress, and no prior claim history for the destination id.
- Keep
receipt deliveredandreceipt meaning establishedin separate phases. A callback-visible FNFT should never still be awaiting the write that determines what each token can withdraw. - Constrain split and deposit-additional helpers so they cannot rewrite both source-series and destination-series economics without proving exact value conservation across both sides.
- Put reentrancy protection on the whole issuance / split / config authority boundary, not only on the one user-facing mint path that first looked dangerous.
- Fuzz zero-value-series flows, partial-supply splits, and contract-recipient mint callbacks together. Revest only paid because all three features shared one state machine.
- Add invariant tests that total redeemable underlying across old series plus new series never exceeds assets actually escrowed in
TokenVault, even during nested execution.
Audit prompts
- Which exact mint path exposes
getNextId()-derived ids to a receiver callback beforefnftsCreatedor equivalent namespace state advances? - Can any callback-reachable sibling function create a split series, migrate a receipt, or rewrite
depositAmountwhile that namespace is still half-finished? - What proves a supposedly fresh destination id is actually blank rather than already minted, already configured, or still being minted in the outer frame?
- Does the code ever let zero-value series be mass-minted before a later helper writes the per-token claim amount that redemption will honor?
- If a helper can update both old-series and new-series config, what invariant proves it cannot accidentally duplicate economic meaning across both?
- Do contract-recipient tests cover
mint -> callback -> depositAdditionalToFNFT -> split -> withdrawstyle sequences, or only simple mint and withdraw happy paths? - Is the reentrancy guard placed on the real shared authority surface, or can a callback simply hop into a different function that trusts the same allocator and vault state?
- What single property test proves that a tiny real deposit can never be rebound onto a large pre-minted receipt supply?
Lessons that should already be standard
Next idis not bookkeeping. In receipt systems, namespace allocation is direct custody and issuance authority.- ERC-1155 mint callbacks belong in the same threat model as withdrawals and external transfers. Issuance code is callback-exposed code.
- A receipt should not be callback-visible before its economic meaning is final. Half-configured claim tokens are already an exploit surface.
Fresh id from allocatoris not a proof of nonexistence if the allocator itself can be observed mid-transition.- Split, migration, and
deposit additionalhelpers are solvency code. They do not become safer just because they are secondary or convenience paths. - If one helper can still rewrite old-series and new-series economics after minting has started, the protocol has not actually decided what object owns claim truth yet.
- Conserving redeemable value across receipt families should be a first-class invariant, not an informal expectation.
Why this incident matters
- Revest is a sharp corpus exemplar for
reentrancy beyond balance accounting: the callback did not merely race a balance update, it raced the protocol’s series-identity allocator and then rewrote the economic meaning of millions of already-issued claims. - It is also a strong ERC-1155 lesson. Protocols often think about callbacks only at withdrawal or transfer boundaries, but issuance code that mints to contracts is just as exposed.
- The note is worth keeping alongside other overissuance incidents because the payout came from turning worthless receipt tokens into vault-valid withdrawal rights, not from manipulating a price feed or traditional borrow path.
Internal linkages
- Core recurring pattern: safe-transfer-receiver-callback-reentrancy.
- Narrower callback-specific family: safe-transfer-receiver-callback-reentrancy.
- Broader placeholder-state companion: sentinel-lane-separation-and-default-value-deauthorization — useful when the sharper question is how
blankFNFT IDs and half-initialized series slots should stay inert until configuration is provably complete. - Compare against callback-driven overissuance through stale vault state: grim-finance-spirit-lp-vault-reentrancy-2021-12-18.
- Compare against another NFT-standard callback variant that converts stale state into spendable claims: omni-protocol-doodles-nftx-safe-transfer-double-reentrancy-2022-07-10.
Control surface
-
The leverage sat in
FNFTHandler.mint()’s post-callbackfnftsCreatedincrement,depositAdditionalToFNFT()’s split-series path, andTokenVault.handleMultipleDeposits()trustingnewFNFTIdas an unused config slot. -
Revest did not just have
a reentrancy bug. It let half-finished series identity become a writable authority surface during ERC-1155 mint delivery. -
Sources:
- https://revestfinance.medium.com/revest-protocol-exploit-recovery-plan-b06ca33fbdf5
- https://blocksec.com/blog/revest-finance-vulnerabilities-more-than-re-entrancy
- https://rekt.news/revest-finance-rekt
- https://etherscan.io/address/0xb480ac726528d1c195cd3bb32f19c92e8d928519
- https://etherscan.io/tx/0xe0b0c2672b760bef4e2851e91c69c8c0ad135c6987bbf1f43f5846d89e691428
- https://etherscan.io/tx/0x19b10c6d38f0b911fdc0e722d681a70a56699d70559eefef3d4d6fe88276c813
-
Last reviewed: 2026-08-04 UTC