This is a critical logic issue that allows unauthorized withdrawal of NFTs from the vault. It compromises user trust and directly enables asset theft.
The depositEgg()
function in EggVault
lacks access control, allowing any user to assign themselves as the depositor of any NFT already transferred to the vault. As a result, malicious actors can withdraw NFTs they do not own. This leads to critical asset theft by exploiting a logical flaw in the depositor-tracking mechanism.
The function depositEgg(uint256 tokenId, address depositor)
is market public
and has no checks to ensure that the depositor
address is valid or matches msg.sender
. The only checks it performs are:
That the NFT is currently held by the vault.
That the egg has not already been marked as deposited.
Code:
If a user legitimately transfers an NFT to the vault, but doesn't call depositEgg()
immediately, an attacker can observe the vault balance on-chain and front-run or follow-up with a call to depositEgg()
using themselves as the depositor
. This results in a situation where they can later call withdrawEgg()
and withdraw an NFT they never owned.
This vulnerability enables the total loss of user NFTs by allowing any actor to arbitrarily assign themselves as the depositor of an NFT after it has been transferred to the vault. As a result, legitimate NFT owners are forced to fully trust that no one else will hijack their deposit in the interim. The contract's lack of access control on depositor assignment introduces a critical flaw that can be exploited repeatedly by attackers who monitor the vault and intercept or front-run deposits, effectively enabling them to steal NFTs.
Manual code review
Foundry + Anvil
Forge scripting for local proof of concept
I created a Forge script that simulates the attack locally using Foundry and Anvil. It demonstrates:
A victim mints and transfers the egg NFT to the vault
An attacker calls depositEgg()
with their own address
The attacker then successfully withdraws the egg
Script: ExploitDeposit.s.sol
Console Logs
This proves the vulnerability is real and exploitable in a default configuration.
Require msg.sender
to equal depositor
: require(msg.sender == depositor, "Sender must be depositor");
Or, remove the depositor
parameter and set: eggDepositors[tokenId] = msg.sender;
Consider designing the contract to automatically handle approval and deposit in the same call, removing the two-step process which introduces this race condition.
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.