The EggVault
contract has a critical front-running vulnerability in its deposit process due to the separation between the NFT transfer and recording the depositor's ownership. This allows attackers to monitor the mempool and claim ownership of NFTs that have been transferred to the vault but not yet registered.
The EggVault
contract lacks the standard onERC721Received
hook. The deposit mechanism relies on a two-step process implicitly (user approves game/vault, user initiates transfer, then depositEgg
is called). Specifically, in EggHuntGame.depositEggToVault
, the eggNFT.transferFrom
happens first, sending the NFT to the vault. After this transfer is successful but before the eggVault.depositEgg(tokenId, msg.sender)
call within the same function completes, the NFT resides in the vault but isn't officially recorded. The EggVault.depositEgg
function itself is public
and takes the depositor
address as an argument. An attacker monitoring the mempool can see the successful transferFrom
to the vault. They can then immediately call EggVault.depositEgg(tokenId, attackerAddress)
before the original transaction's call to depositEgg
executes. Because require(eggNFT.ownerOf(tokenId) == address(this))
will pass (the NFT is in the vault) and require(!storedEggs[tokenId])
will pass (it hasn't been recorded yet), the attacker's transaction can succeed, recording them as the eggDepositor
.
POC:
An attacker can steal NFTs deposited into the vault by front-running the depositEgg
call and registering themselves as the depositor. They can then use withdrawEgg
to take the NFT.
Manual code review
Forge testing framework to validate the vulnerability
Implement onERC721Received
: The EggVault
should inherit ERC721Holder
(or manually implement onERC721Received
).
Modify Deposit Flow: Users should call safeTransferFrom
on the EggstravaganzaNFT
contract, transferring the NFT directly to the EggVault
. The onERC721Received
function in the vault will then automatically be called. Inside onERC721Received
, record the _from
address (the original owner transferring the NFT) as the depositor and the _tokenId
.
Remove depositEgg
: The separate depositEgg
function becomes unnecessary and should be removed.
Adjust EggHuntGame.depositEggToVault
: This function might be removed entirely if users interact directly with the NFT contract and vault for deposits. Alternatively, it could be kept but would need the user to have approved the Vault contract first, and then this function would simply call eggNFT.safeTransferFrom(msg.sender, address(eggVault), tokenId)
.
Front-running depositEgg allows deposit ownership hijacking.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.