Eggstravaganza

First Flight #37
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: low
Valid

Direct NFT transfers to `EggVault` can result in permanent asset loss

Description::

This vulnerability arises as a side effect of a valid mitigation applied to prevent front-running attacks. That mitigation restricts the EggVault::depositEgg() function so that only the EggHuntGame contract can call it—preventing malicious actors from claiming NFTs they don't own.

However, once this restriction is in place, it introduces a critical new scenario:

If a user manually transfers their NFT to the EggVault using transferFrom(), they can no longer register or withdraw it. The NFT becomes permanently trapped in the vault.

Since EggVault does not implement onERC721Received() and lacks any rescue mechanism, this results in irreversible NFT loss for the user, even without any malicious behavior.

Impact:

A legitimate user who mistakenly sends their NFT directly to the EggVault using transferFrom() will lose access to their asset permanently. Since the vault only allows the game contract to call depositEgg(), the user cannot register the token or retrieve it through any available function.

This creates a critical loss-of-access scenario:

  • No way to withdraw the NFT.

  • No feedback or revert to prevent the mistake.

  • No fallback mechanism such as onERC721Received or admin rescue.

While not caused by an attacker, the impact is real and irreversible, affecting the core user experience and the safety of owned assets. It undermines user trust and may require off-chain intervention or administrative rescue, which contradicts the principles of trustless smart contracts.

Proof of Concept:

Test that simulates a manual transfer of an NFT to the vault, bypassing the intended game flow, and verifies that the NFT becomes unrecoverable.

function test_nftGetsTrappedByManualTransfer() public gameStarted aliceHasANft {
// Simulate Alice acting in this scenario
vm.startPrank(alice);
// Get the current egg ID (last minted token)
uint256 eggId = game.eggCounter();
// Alice transfers the NFT directly to the vault, skipping the game logic
nft.transferFrom(alice, address(vault), eggId);
// Now Alice attempts to register the NFT in the vault, but is unauthorized
vm.prank(alice);
vm.expectRevert("Unauthorized caller");
vault.depositEgg(eggId, alice);
// The NFT is stuck in the vault with no way to recover it
assertEq(nft.ownerOf(eggId), address(vault));
}

Result:

Ran 1 test for test/EggHuntGamesTest2.t.sol:EggGameTest2
[PASS] test_frontRunningAttack() (gas: 263762)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.67ms (1.73ms CPU time)

Tools Used

Manual review, Foundry

Recommended Mitigation:

Modify the EggstravaganzaNFT contract to block all direct transfers to the vault, unless initiated by the game contract:

+ address public immutable eggVaultAddress;
- constructor(string memory _name, string memory _symbol)
+ constructor(string memory _name, string memory _symbol, address _eggVaultAddress)
ERC721(_name, _symbol) Ownable(msg.sender)
{
+ require(_eggVaultAddress != address(0), "Invalid vault address");
+ eggVaultAddress = _eggVaultAddress
}
+ function transferFrom(address from, address to, uint256 tokenId) public override {
+ if (to == eggVaultAddress) {
+ require(msg.sender == gameContract, "Use game contract to deposit NFTs");
+ }
+ super.transferFrom(from, to, tokenId);
+ }
Updates

Lead Judging Commences

m3dython Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Unsafe direct token transfer

Users can transfer NFTs directly to the vault using standard ERC721 transferFrom(), bypassing the registration

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.