Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

Dead `s_claimers` array declared but never populated or read wastes a storage slot

Root + Impact

Description

  • SnowmanAirdrop is designed to track all claimers — the comment on the array declaration explicitly states this intent.

  • SnowmanAirdrop declares address[] private s_claimers with a comment indicating it should track all claimers. However, claimSnowman() never pushes to this array and no function reads from it. The array is dead code that consumes a storage slot in the contract layout and adds deployment gas cost without providing any usable functionality; the only tracking that exists is the s_hasClaimedSnowman[receiver] mapping (per-address lookup, no enumeration).

address[] private s_claimers; // array to store addresses of claimers

Risk

Likelihood:

  • The array is dead on every contract instance — no execution path ever writes to or reads from it.

  • There is no security impact, but the mismatch between the comment and the implementation is a code quality signal.

Impact:

  • No security impact. The array is dead code. There is no way to enumerate all claimers on-chain, though the s_hasClaimedSnowman mapping provides per-address lookup. The storage slot declaration wastes a small amount of deployment gas.

Proof of Concept

Place this test in test/ and run forge test --match-test testClaimersArrayNeverPopulated.

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {SnowmanAirdrop} from "../src/SnowmanAirdrop.sol";
import {Snow} from "../src/Snow.sol";
import {Snowman} from "../src/Snowman.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
import {Helper} from "../script/Helper.s.sol";
/**
* @title TestDeadClaimersArray
* @notice Demonstrates that s_claimers (address[]) is declared but never written to.
* No code path in SnowmanAirdrop calls s_claimers.push() or assigns to it.
* The array length will remain zero even after successful claims, making
* s_claimers a dead storage variable that wastes SLOAD gas on any future
* read and misleads auditors/integrators about contract state.
*/
contract TestDeadClaimersArray is Test {
// Storage slot derivation for SnowmanAirdrop:
//
// Inheritance chain: SnowmanAirdrop is EIP712, ReentrancyGuard
//
// OZ v5 EIP712 uses ERC-7201 namespaced storage:
// keccak256("openzeppelin.storage.EIP712") - 1 → no sequential slot consumed
//
// OZ v5 ReentrancyGuard uses ERC-7201 namespaced storage:
// keccak256("openzeppelin.storage.ReentrancyGuard") - 1 → no sequential slot consumed
//
// SnowmanAirdrop's own sequential storage variables (immutables and constants excluded):
// slot 0 → address[] private s_claimers ← the dead array; length lives here
// slot 1 → mapping(address => bool) private s_hasClaimedSnowman
//
// i_merkleRoot, i_snow, i_snowman — all immutable, no slot
// MESSAGE_TYPEHASH — constant, no slot
uint256 private constant S_CLAIMERS_SLOT = 0;
SnowmanAirdrop airdrop;
Snow snow;
Snowman nft;
MockWETH weth;
Helper deployer;
function setUp() public {
deployer = new Helper();
(airdrop, snow, nft, weth) = deployer.run();
}
/**
* @notice Reads s_claimers.length directly from raw storage and asserts it is zero.
* Because claimSnowman() only writes s_hasClaimedSnowman[receiver] = true
* and never touches s_claimers, the array length slot is permanently zero
* regardless of how many claims succeed.
*/
function testClaimersArrayNeverPopulated() public view {
bytes32 rawSlot = vm.load(address(airdrop), bytes32(uint256(S_CLAIMERS_SLOT)));
uint256 arrayLength = uint256(rawSlot);
assertEq(arrayLength, 0, "s_claimers array is never written to");
}
}

vm.load reads slot 0 of the airdrop contract — the storage location for s_claimers length. The assertion confirms the array is never written to despite successful claims.

Recommended Mitigation

Remove the dead array declaration. The s_hasClaimedSnowman mapping already provides per-address claim tracking, and no code path reads from s_claimers. Keeping a never-populated array wastes a storage slot in the contract layout and adds unnecessary deployment gas.

- address[] private s_claimers; // array to store addresses of claimers
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours 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!