We do not have a reliable USD price for the recorded assets yet.
0x5499178919c79086fd580d6c5f332a4253244d91OptimismNebula Revelation lost 1773100000000000000000000000 NBL on Optimism in transaction 0xf4fc3b638f1a377cf22b729199a9aeb27fc62fe2983a65c4d14b99ee5c5b2328 at block 115293069. The attacker first prepared a helper contract and staged NblBox NFT #737, then used a public Uniswap V3 flash loan to fund a stake, reentered NblNftStake.withdrawNft, withdrew the same NBL-backed position twice, repaid the flash loan, and realized profit in USDT and WETH.
The root cause is a checks-effects-interactions violation in NblNftStake.withdrawNft at 0x5499178919c79086fd580d6c5f332a4253244d91. The function transfers the staked NFT to msg.sender with safeTransferFrom before clearing the slot fields that authorize withdrawal, so an attacker-controlled ERC721 receiver can reenter the same slot while it is still live.
NblNftStake stores each user position as StakeInfo { nftTokenId, inscriptionId, nblStakeAmount, begin }. A slot becomes valuable after the user opens it, deposits an NblBox NFT, and deposits NBL into the same slot. The slot state is then used to determine whether withdrawNft can release both the NFT and the associated NBL.
NblBoxNft at is a standard ERC721. Its verified source shows calls , and invokes the recipient's hook when the recipient is a contract. That callback surface matters because sends the staked NFT to , which the attacker intentionally made into a contract capable of executing arbitrary logic during receipt.
0xf4fc3b638f1a377cf22b729199a9aeb27fc62fe2983a65c4d14b99ee5c5b23280x534e1a8a89548c44be7aba1c3c27951801940c10safeTransferFrom_safeTransfer_safeTransferonERC721ReceivedwithdrawNftmsg.senderThe incident also used public liquidity rather than privileged funding. The exploit transaction borrowed NBL from the Uniswap V3 pool at 0xfAF037caAfA9620bFAebc04C298Bf4A104963613, seeded the vulnerable stake slot, extracted duplicate NBL value, repaid the flash loan, then swapped residual NBL into USDT and WETH.
The vulnerability class is reentrancy through an ERC721 safe-transfer callback. Verified NblNftStake source shows the critical ordering inside withdrawNft:
function withdrawNft(uint256 _index) public {
StakeInfo[] storage stakes = userStakeInfo[msg.sender];
uint tokenid = stakes[_index].nftTokenId;
uint amount = stakes[_index].nblStakeAmount;
nft.safeTransferFrom(address(this), msg.sender, tokenid);
if (stakes[_index].inscriptionId > 0) {
inscription.safeTransferFrom(address(this), msg.sender, stakes[_index].inscriptionId);
}
uint discount = calcDiscount(stakes[_index].begin, amount);
nbl.safeTransfer(community, discount);
nbl.safeTransfer(msg.sender, SafeMath.sub(amount, discount));
stakes[_index].nftTokenId = 0;
stakes[_index].inscriptionId = 0;
stakes[_index].nblStakeAmount = 0;
stakes[_index].begin = 0;
}
The invariant that should hold is: a live stake slot must be invalidated before any external interaction that can reenter, so one slot can authorize at most one NFT release and one NBL refund. withdrawNft breaks that invariant by making the external ERC721 callback reachable before slot invalidation. Because the slot still contains the original nftTokenId and nblStakeAmount during the callback, a malicious receiver can transfer the NFT back into the stake contract and call withdrawNft again on the same slot.
The verified victim source and the exploit trace line up cleanly. The source establishes that withdrawNft performs the external safeTransferFrom first and only later clears nftTokenId, inscriptionId, nblStakeAmount, and begin. The call trace for the seed exploit transaction then shows the exact reentrant sequence on Optimism block 115293069.
First, the attacker-created transient contract opened slot 0, deposited NFT #737, and deposited NBL into that slot. The trace contains calls from the transient contract to the victim for unlockSlot, depositNft(737, 0), and depositNbl(0, 1773100000000000000000000000).
Next, the transient contract called withdrawNft(0). Inside that first withdrawal, the victim called the NFT contract to safe-transfer token 737 to the transient contract. The trace then shows the NFT contract invoking onERC721Received on the transient contract during that transfer:
0xfc3b... -> 0x5499... input 0x79347371...(withdrawNft slot 0)
0x5499... -> 0x534e... input 0x42842e0e...(safe transfer NFT #737)
0x534e... -> 0xfc3b... input 0x150b7a02...(onERC721Received callback)
During that callback, the transient contract transferred NFT #737 back into NblNftStake and immediately called withdrawNft(0) again before the outer call had cleared slot 0. The nested trace shows the second withdrawal on the same slot, followed by another safe transfer of NFT #737 back to the transient contract. Because slot 0 was still live, the second call passed the same checks and released the same NBL-backed position a second time.
After the duplicate withdrawal, the attacker re-deposited the NFT so the outer call could finish without reverting, repaid the flash-loan principal plus fee, then swapped the residual NBL. The seed balance diff confirms the economic outcome: NblNftStake lost exactly 1773100000000000000000000000 NBL, the attacker EOA gained 164967658585 USDT and 6900257340231423430 WETH, and there was no remaining repayment obligation because the flash loan was fully repaid inside the transaction.
The attacker sequence is fully reconstructible from public chain data and used no privileged capability.
Transaction 0xd3cfde9265321cefd1e0dda4fd4162d23f305c7a830e200f207111b4b829c8d8 deployed helper contract 0xe4d41bdd6459198b33cc795ff280cee02d91087b. Deployment artifacts and the helper reconstruction summary tie that contract directly to attacker EOA 0x1fd0a6a5e232eeba8020a40535ad07013ec4ef12.
Transaction 0x5e908fcf767eb0ded20edddbae13a9b86ee463a78d19d3c9bd319a5b4b8aa0de transferred NFT #737 from the attacker EOA into the helper. That staged the NFT inside attacker-controlled contracts before the exploit transaction.
Transaction 0xf4fc3b638f1a377cf22b729199a9aeb27fc62fe2983a65c4d14b99ee5c5b2328 executed the exploit. The helper created transient contract 0xfc3b08555b1c328ecf8b8a0ccd85679bf59bba4c, moved NFT #737 into it, triggered the flash loan, and let the transient contract perform the vulnerable stake sequence. The transient contract was the contract that received the NFT during withdrawNft, ran the callback reentrancy, extracted the duplicate NBL value, and then returned value back through the helper to the attacker EOA. This is an ACT sequence because any unprivileged actor with one NFT and public liquidity could reproduce the same callback-based double withdrawal.
The measurable protocol loss was 1773100000000000000000000000 NBL from NblNftStake. The direct economic effect is visible in the balance diff for the seed exploit transaction, where the victim's NBL balance moved from 1773100000000000000000000000 to 0.
The attacker EOA ended the transaction with positive proceeds in two assets:
164967658585 USDT6900257340231423430 WETHThe native balance delta for the attacker EOA was negative only by gas and L1 fee components (3477615881738895 wei), so the exploit remained net profitable after execution costs.
0xf4fc3b638f1a377cf22b729199a9aeb27fc62fe2983a65c4d14b99ee5c5b23280xd3cfde9265321cefd1e0dda4fd4162d23f305c7a830e200f207111b4b829c8d80x5e908fcf767eb0ded20edddbae13a9b86ee463a78d19d3c9bd319a5b4b8aa0deNblNftStake at 0x5499178919c79086fd580d6c5f332a4253244d91NblBoxNft at 0x534e1a8a89548c44be7aba1c3c27951801940c100x770b279678105b378f90055aad1b22d2a0306bafNblNftStake/workspace/session/artifacts/collector/seed/10/0x534e1a8a89548c44be7aba1c3c27951801940c10/src/Contract.sol/workspace/session/artifacts/collector/iter_1/tx/10/0xf4fc3b638f1a377cf22b729199a9aeb27fc62fe2983a65c4d14b99ee5c5b2328/call_trace.json/workspace/session/artifacts/collector/iter_1/tx/10/0xf4fc3b638f1a377cf22b729199a9aeb27fc62fe2983a65c4d14b99ee5c5b2328/receipt.json/workspace/session/artifacts/collector/seed/10/0xf4fc3b638f1a377cf22b729199a9aeb27fc62fe2983a65c4d14b99ee5c5b2328/balance_diff.json