Eggstravaganza

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

Missing Approval Check in `depositEggToVault` Function Leads to Failed Transactions

Summary

The EggHuntGame contract contains a critical vulnerability in the depositEggToVault function that can lead to failed transactions. This vulnerability stems from the function not verifying if the player has approved the NFT transfer before attempting to transfer the NFT to the vault.

Vulnerability Details

The vulnerability exists in the EggHuntGame contract's depositEggToVault function. The function accepts one parameter:

  1. tokenId: The ID of the NFT to be deposited into the vault

The critical issue is that the function assumes the player has already approved the NFT transfer but doesn't verify this assumption. The function:

  1. Checks if the caller owns the NFT

  2. Attempts to transfer the NFT from the player to the vault

  3. Calls the vault's depositEgg function

However, if the player hasn't approved the transfer, the transferFrom call will revert, causing the entire transaction to fail. This is particularly problematic because:

Impact

  • Medium Severity: This vulnerability can lead to failed transactions and wasted gas

  • User Experience: Poor user experience as transactions fail without clear error messages

  • Gas Inefficiency: Users may waste gas on failed transactions

Proof of Concept

The vulnerability can be demonstrated with the following test case:

contract EggGameTest is Test {
EggstravaganzaNFT nft;
EggVault vault;
EggHuntGame game;
address owner;
address alice;
address bob;
error OwnableUnauthorizedAccount(address account);
function setUp() public {
owner = address(this); // The test contract is the deployer/owner.
alice = address(0x1);
bob = address(0x2);
// 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));
}
function testDepositEggWithoutApproval() public {
// 1. Mint an NFT to Alice
uint256 tokenId = 1;
vm.prank(address(game));
nft.mintEgg(alice, tokenId);
// 2. Alice attempts to deposit the NFT without approval
vm.startPrank(alice);
// 3. The transaction should revert due to lack of approval
vm.expectRevert();
game.depositEggToVault(tokenId);
// 4. Verify Alice still owns the NFT
assertEq(nft.ownerOf(tokenId), alice);
}
}

run the test

forge test -vvvv --mt testDepositEggWithoutApproval

the result as follows

Ran 1 test for test/EggHuntGameTest.t.sol:EggGameTest
[PASS] testDepositEggWithoutApproval() (gas: 101313)
Traces:
[101313] EggGameTest::testDepositEggWithoutApproval()
├─ [0] VM::prank(EggHuntGame: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a])
│ └─ ← [Return]
├─ [71582] EggstravaganzaNFT::mintEgg(ECRecover: [0x0000000000000000000000000000000000000001], 1)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: ECRecover: [0x0000000000000000000000000000000000000001], tokenId: 1)
│ └─ ← [Return] true
├─ [0] VM::startPrank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error f4844814:)
│ └─ ← [Return]
├─ [11784] EggHuntGame::depositEggToVault(1)
│ ├─ [642] EggstravaganzaNFT::ownerOf(1) [staticcall]
│ │ └─ ← [Return] ECRecover: [0x0000000000000000000000000000000000000001]
│ ├─ [5608] EggstravaganzaNFT::transferFrom(ECRecover: [0x0000000000000000000000000000000000000001], EggVault: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 1)
│ │ └─ ← [Revert] ERC721InsufficientApproval(0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 1)
│ └─ ← [Revert] ERC721InsufficientApproval(0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 1)
├─ [642] EggstravaganzaNFT::ownerOf(1) [staticcall]
│ └─ ← [Return] ECRecover: [0x0000000000000000000000000000000000000001]
├─ [0] VM::assertEq(ECRecover: [0x0000000000000000000000000000000000000001], ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ └─ ← [Return]
└─ ← [Stop]

Tools Used

  • Foundry

Mitigation

To fix this problem, implement the following changes:

Add an approval check in the depositEggToVault function:

function depositEggToVault(uint256 tokenId) external {
require(eggNFT.ownerOf(tokenId) == msg.sender, "Not owner of this egg");
// Check if the player has approved the transfer
require(
eggNFT.isApprovedForAll(msg.sender, address(this)) ||
eggNFT.getApproved(tokenId) == address(this),
"NFT transfer not approved"
);
eggNFT.transferFrom(msg.sender, address(eggVault), tokenId);
eggVault.depositEgg(tokenId, msg.sender);
}
Updates

Lead Judging Commences

m3dython Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Gas optimization

Strategy to save gas and minimize transaction costs

Support

FAQs

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