Snowman Merkle Airdrop

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

Redundant Fee Computation in buySnow Inflates Gas Cost

Root + Impact

Description

  • Not a security vulnerability — no funds, access control, or correctness are at risk. The impact is purely economic: every call to buySnow recomputes s_buyFee * amount more times than necessary, meaning every buyer (on both the ETH path and the WETH path) pays extra gas for the same outcome. Over a high volume of purchases, this adds up to real, avoidable cost passed directly onto users.

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

Compute totalFee = s_buyFee * amount once, store it in a local variable, and reuse it in both branches. This drops the operation from 2×SLOAD + 2×MUL down to 1×SLOAD + 1×MUL, regardless of which branch executes.

contract BuySnowGasComparisonTest is Test {
GasComparisonBuySnow token;
MockWETH weth;
address buyerEth = makeAddr("buyerEth");
address buyerWeth = makeAddr("buyerWeth");
uint256 constant FEE = 0.01 ether; // must match s_buyFee in the contract
function setUp() public {
weth = new MockWETH();
token = new GasComparisonBuySnow(address(weth));
// Fund ETH buyers
vm.deal(buyerEth, 100 ether);
// Fund + approve WETH buyer
weth.mint(buyerWeth, 100 ether);
vm.prank(buyerWeth);
weth.approve(address(token), type(uint256).max);
}
/// @notice Compares gas cost via the ETH-payment branch (msg.value == totalFee).
function test_GasComparison_ETHPath() public {
uint256 amount = 5;
uint256 cost = FEE * amount;
uint256 gasBeforeOriginal = gasleft();
vm.prank(buyerEth);
token.buySnowOriginal{value: cost}(amount);
uint256 gasUsedOriginal = gasBeforeOriginal - gasleft();
uint256 gasBeforeOptimized = gasleft();
vm.prank(buyerEth);
token.buySnowOptimized{value: cost}(amount);
uint256 gasUsedOptimized = gasBeforeOptimized - gasleft();
console.log("=== ETH path, amount:", amount, "===");
console.log("Original gas used:", gasUsedOriginal);
console.log("Optimized gas used:", gasUsedOptimized);
console.log("Gas saved:", gasUsedOriginal - gasUsedOptimized);
assertLt(gasUsedOptimized, gasUsedOriginal);
}
/// @notice Compares gas cost via the WETH-payment branch (else / safeTransferFrom).
/// This branch computes totalFee twice in the original (once in the failed
/// `if` check, once again inside safeTransferFrom), so the saving here is
/// expected to be at least as large as the ETH path.
function test_GasComparison_WETHPath() public {
uint256 amount = 5;
uint256 gasBeforeOriginal = gasleft();
vm.prank(buyerWeth);
token.buySnowOriginal(amount); // msg.value = 0 -> falls into else branch
uint256 gasUsedOriginal = gasBeforeOriginal - gasleft();
uint256 gasBeforeOptimized = gasleft();
vm.prank(buyerWeth);
token.buySnowOptimized(amount);
uint256 gasUsedOptimized = gasBeforeOptimized - gasleft();
console.log("=== WETH path, amount:", amount, "===");
console.log("Original gas used:", gasUsedOriginal);
console.log("Optimized gas used:", gasUsedOptimized);
console.log("Gas saved:", gasUsedOriginal - gasUsedOptimized);
assertLt(gasUsedOptimized, gasUsedOriginal);
}
}

Recommended Mitigation

Compute s_buyFee * amount once into a local variable (totalFee) at the top of the function, and reference that cached value in both the if check and the else branch's safeTransferFrom call, rather than re-deriving it in each branch.

- remove this code
function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
+ add this code
function buySnow(uint256 amount) external payable canFarmSnow {
uint256 totalFee = s_buyFee * amount; // single SLOAD + single MUL, reused below
if (msg.value == totalFee) {
_mint(msg.sender, amount);
} else {
i_weth.safeTransferFrom(msg.sender, address(this), totalFee);
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!