Eggstravaganza

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

Direct deposit into Egg Vault can lead to loss of NFT

Summary

Directly depositing into the Egg Vault and using an incorrect depositor address, can lead to complete loss of owned NFT.

Vulnerability Details

Depositing into the Egg Vault can be done either externally through the Egg Hunt Game (with the "depositEggToVault()" function) or directly with the public "depositEgg()" function. The depositEgg() function takes a tokenId and depositor address:

function depositEgg(uint256 tokenId, address depositor) public {
// function code
}

If depositing through the game contract, it will provide the correct owner of the NFT (the msg.sender), whereas calling this function directly in the vault and having the user provide a depositor address by themselves, can lead to a completely loss of the owned NFT by incorrectly typing out a depositor address and essentially crediting another address as the owner.

This flaw can be tested with the following Foundry test:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import "forge-std/Test.sol";
import "../src/EggstravaganzaNFT.sol";
import "../src/EggVault.sol";
import "../src/EggHuntGame.sol";
/*
_.----._ _.---.
.-' `-.-' `. How do I deposit my egge?
.' .:''':.`.
.' .:'''':. .' .----. `.
.-./ .' .----. / .-. \ `.
/.-. / .-. \ \ ' O ' | \
|| `. | ' O '/ \ `-' / |
|| ( \ \ `-'/ `-.__ / `.
\`-' ) .-' -- ) `.
`-' _.' ( _.-' _/\ \
`. /\_ `-.____..-' .-' _/ /
`. \_ `-._ _.-'_.-' .'
`--.._ `-._ `-.__..-'_.-' .'
.-' `-._ `--.__..-' _.----'`.
/ `---.......-' _ \ \
/ ( `-._.-` )
/ / _ .- _.-'
( `-._.-' ) (_ .' \
`-._ -. (_.-' |
`. _) __..---'
| `-._) ''...__ .-. __...'''__..---'
*/
contract EggGameTestDeposit 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));
}
// Directly deposit an egg into the vault with the wrong depositor
function testDepositEggDirectlyWithIncorrectDepositor() public {
// Egg threshold 100 to guarantee find
game.setEggFindThreshold(100);
// Start the game with a duration.
uint256 duration = 200;
game.startGame(duration);
// Alice searches for an egg
vm.prank(alice);
game.searchForEgg();
// Check that the egg counter has increased and alice's egg count increased.
assertEq(game.eggCounter(), 1);
assertEq(game.eggsFound(alice), 1);
// Alice decides to deposit her egg into the vault directly but incorrectly types Bob as the depositor
vm.startPrank(alice);
// Alice transfers ownership of NFT to the vault
nft.transferFrom(alice, address(vault), 1);
// Alice goes to deposit egg but accidentally puts Bobs address rather than her own
vault.depositEgg(1, bob);
vm.stopPrank();
// The deposited egg is now credited to Bob who was not the original owner/depositor
assertEq(vault.eggDepositors(1), bob);
// Alice decides to withdraw egg and now can't because she accidentally put Bob as the depositor
vm.startPrank(alice);
vm.expectRevert("Not the original depositor");
vault.withdrawEgg(1);
// Bob, despite not being the original owner/depositor of the egg, CAN withdraw the egg
vm.startPrank(bob);
vault.withdrawEgg(1);
// After withdrawal, Bob is credited as the owner, Alice has lost the NFT
assertEq(nft.ownerOf(1), bob);
}
}

Impact

Loss of NFT due to incorrect depositor address.

Tools Used

Manual review. Foundry testing.

Recommendations

We can add a check in the Egg Vault to see if depositing an egg is coming from the Game Contract or from a user directly, in which case we will make sure the user is providing a correct depositor address and thus still allowing egg deposits to happen through the game contract or directly into the vault.

To do this, we will do the following:

Our EggVault contract will have the address and type of the EggstravaganzaNFT, which has a public address variable of the EggHuntGame contract. Due to it being public, it will automatically have a getter function added to it allowing us to view it from our Egg Vault. We can put this check into our depositEgg() function after the initial require checks.

// Check if the sender is NOT the game contract
if (msg.sender != eggNFT.gameContract()) {
// If it's not, require that the sender is adding the correct depositor (themselves)
require(msg.sender == depositor, "Only the depositor can deposit");
// If it passes, set the depositor to the sender
depositor = msg.sender;
}

In our EggVault.sol, we will now have a completed depositEgg() function of:

function depositEgg(uint256 tokenId, address depositor) public {
require(eggNFT.ownerOf(tokenId) == address(this), "NFT not transferred to vault");
require(!storedEggs[tokenId], "Egg already deposited");
// Check if the sender is NOT the game contract
if (msg.sender != eggNFT.gameContract()) {
// If it's not, require that the sender is adding the correct depositor (themselves)
require(msg.sender == depositor, "Only the depositor can deposit");
// If it passes, set the depositor to the sender
depositor = msg.sender;
}
storedEggs[tokenId] = true;
eggDepositors[tokenId] = depositor;
emit EggDeposited(depositor, tokenId);
}

If called through the Egg Hunt Game contract, it will provide the necessary details such as the depositor address. If called directly, it will do a check that if the sender is not the game contract, that the provided depositor in the call is the msg.sender, so as not to have an incorrect address.

This will allow eggs to be deposited either through the game contract or directly by a user into the vault and prevent an incorrect depositor being credited and NFT's being lost.

With the fix, we can do some tests to make sure that depositing works normally, either through the game contract or directly into vault and that it will revert if putting in an incorrect depositor when doing a direct deposit:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import "forge-std/Test.sol";
import "../src/EggstravaganzaNFT.sol";
import "../src/EggVault.sol";
import "../src/EggHuntGame.sol";
/*
_.----._ _.---.
.-' `-.-' `. My egges now deposit correctly! :)
.' .:''':.`.
.' .:'''':. .' .----. `.
.-./ .' .----. / .-. \ `.
/.-. / .-. \ \ ' O ' | \
|| `. | ' O '/ \ `-' / |
|| ( \ \ `-'/ `-.__ / `.
\`-' ) .-' -- ) `.
`-' _.' ( _.-' _/\ \
`. /\_ `-.____..-' .-' _/ /
`. \_ `-._ _.-'_.-' .'
`--.._ `-._ `-.__..-'_.-' .'
.-' `-._ `--.__..-' _.----'`.
/ `---.......-' _ \ \
/ ( `-._.-` )
/ / _ .- _.-'
( `-._.-' ) (_ .' \
`-._ -. (_.-' |
`. _) __..---'
| `-._) ''...__ .-. __...'''__..---'
*/
contract EggGameTestDepositWithFix 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));
}
modifier setGameAndFindEgg() {
// Egg threshold 100 to guarantee find
game.setEggFindThreshold(100);
// Start the game with a duration.
uint256 duration = 200;
game.startGame(duration);
// Alice searches for an egg
vm.prank(alice);
game.searchForEgg();
// Check that the egg counter has increased and alice's egg count increased.
assertEq(game.eggCounter(), 1);
assertEq(game.eggsFound(alice), 1);
_;
}
function testDepositThroughGameContractWorksNormal() public setGameAndFindEgg {
// Alice deposits egg into vault through game contract
vm.startPrank(alice);
// Alice approves the game to transfer her NFT
nft.approve(address(game), 1);
// Alice goes to deposit egg
game.depositEggToVault(1);
vm.stopPrank();
// The deposited egg is now credit to Alice
assertEq(vault.eggDepositors(1), alice);
}
function testDepositDirectlyIntoEggVaultWorksNormal() public setGameAndFindEgg {
// Alice deposits egg into vault directly
vm.startPrank(alice);
// Alice transfers ownership of NFT to the vault
nft.transferFrom(alice, address(vault), 1);
// Alice goes to deposit egg
vault.depositEgg(1, alice);
vm.stopPrank();
// The deposited egg is now credit to Alice
assertEq(vault.eggDepositors(1), alice);
}
function testDepositDirectlyIntoVaultWithIncorrectDepositorReverts() public setGameAndFindEgg {
// Alice deposits egg into vault directly
vm.startPrank(alice);
// Alice transfers ownership of NFT to the vault
nft.transferFrom(alice, address(vault), 1);
// Alice goes to deposit egg but accidentally puts Bobs address rather than her own
vm.expectRevert("Only the depositor can deposit");
vault.depositEgg(1, bob);
vm.stopPrank();
}
}
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.