Snowman Merkle Airdrop

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

Protocol fee collector private key is publicly derivable, allowing unauthorized fee collection and collector takeover

Root + Impact

Impact

An attacker can derive the private key corresponding to the configured fee collector address and impersonate the collector.

The attacker can then call collectFee() to transfer both WETH fees and native ETH held by Snow to the attacker-controlled collector address. The attacker can subsequently call changeCollector() to permanently redirect future fee collection to an attacker-controlled address.

This results in unauthorized protocol-fee theft and persistent redirection of future fees.

Description

  • Description

    # Root + Impact

    ## Description

    The Snow contract stores a fee collector address in `s_collector` and restricts fee collection and collector changes through `onlyCollector`.

    The deployed collector address is configured using `makeAddr("collector")`. Foundry's `makeAddr()` deterministically derives its private key as:

    `uint256(keccak256(abi.encodePacked("collector")))`

    Therefore, the private key corresponding to the configured collector address is publicly computable by anyone.

    An attacker can derive this private key, impersonate the configured collector, call `collectFee()`, and receive the WETH and native ETH accumulated by the Snow contract. The attacker can then call `changeCollector()` and replace the collector with an attacker-controlled address, redirecting future fees as well.Risk

Likelihood:

  • Likelihood

    High

    - The collector private key is deterministically derived from the publicly known string "collector".

    - The private key can therefore be calculated without compromising any account, secret, oracle, or external dependency.

    - Once the vulnerable collector configuration is deployed, an attacker can immediately authenticate as the collector.

    - No victim interaction is required.

  • Risk — Likelihood

    - The collector private key is deterministically derived from the publicly known string `"collector"`.

    - The private key can therefore be calculated without compromising any account, secret, oracle, or external dependency.

    - Once the vulnerable collector configuration is deployed, an attacker can immediately authenticate as the collector.

    - No victim interaction is required.

    Risk — Impact

    - The attacker can call `collectFee()` and withdraw the WETH accumulated as protocol fees.

    - The attacker can also receive the native ETH accumulated by the Snow contract.

    - The attacker can call `changeCollector()` and replace the collector with an attacker-controlled address.

    - Future protocol fees can subsequently be redirected to the attacker's collector address.

Impact:

  • Impact

    An attacker can derive the private key corresponding to the configured fee collector address and impersonate the collector.

    The attacker can then call collectFee() to transfer both WETH fees and native ETH held by Snow to the attacker-controlled collector address. The attacker can subsequently call changeCollector() to permanently redirect future fee collection to an attacker-controlled address.

    This results in unauthorized protocol-fee theft and persistent redirection of future fees.

Proof of Concept

Proof of Concept
function testCollectorPrivateKeyIsPubliclyDerivable() public {
// The vulnerable collector private key is publicly derivable.
uint256 collectorPrivateKey =
uint256(keccak256(abi.encodePacked("collector")));
​
address derivedCollector = vm.addr(collectorPrivateKey);
​
assertEq(derivedCollector, snow.getCollector());
​
// Fund protocol fees through the WETH path.
uint256 wethFee = 5 ether;
weth.mint(address(this), wethFee);
weth.approve(address(snow), wethFee);
snow.buySnow{value: 0}(wethFee / FEE);
​
// Fund protocol fees through the native ETH path.
uint256 nativeAmount = 5 ether;
snow.buySnow{value: nativeAmount / 1}(nativeAmount / FEE);
​
// Impersonate the publicly derivable collector.
vm.prank(derivedCollector);
​
// Unauthorized attacker-controlled account can collect protocol fees.
snow.collectFee();
​
// Protocol fee balances are drained.
assertEq(address(snow).balance, 0);
assertEq(weth.balanceOf(address(snow)), 0);
​
// The attacker can permanently replace the collector.
vm.prank(derivedCollector);
snow.changeCollector(address(this));
​
assertEq(snow.getCollector(), address(this));
}
​
Note: The evidence package already records the repository PoC
`testK5_collectorKeyPublic_feesDrainable()` as PASS. Use that existing
PoC/evidence rather than claiming the above as a newly executed test.
Recommended Mitigation
The fee collector must be configured with a genuinely secret private key.
​
Do not derive privileged accounts from public, deterministic strings such as `"collector"`.
​
Generate the collector key securely outside the contract/deployment code and provide only the resulting public address to the deployment configuration.
​
For an existing deployment, rotate the collector to a securely controlled address using the legitimate administrative procedure.
​
​
​
## Root Cause
​
The collector account is created from a publicly known deterministic value:
​
`makeAddr("collector")`
​
This does not generate a secret private key. The resulting private key is completely reproducible by anyone who knows the string `"collector"`.
​
The resulting address is subsequently used as the privileged `s_collector` account.
Root Cause — Solidity
// Root cause: the collector private key is deterministically derived
// from a publicly known string.
​
address public collector = makeAddr("collector");
​
// Foundry makeAddr() internally derives the private key as:
//
// privateKey = uint256(keccak256(abi.encodePacked(name)));
// addr = vm.addr(privateKey);
//
// Therefore:
//
// privateKey = uint256(keccak256("collector"))
//
// is publicly computable by any attacker.
​
​
​
​
​

Recommended Mitigation

Recommended Mitigation — Diff
- address public collector = makeAddr("collector");
+ address public collector = <SECURELY_GENERATED_DEPLOYMENT_ADDRESS>;
Important Scope Note
The evidence package states that deployment scripts, including `DeploySnow.s.sol`, are out of scope for this contest. It also records that the bundled deployment script aborts because of nested `vm.startBroadcast()`.
​
Therefore, this finding is technically demonstrated but has a contest-scope risk. The report should not claim that the official/sanctioned deployment is live and vulnerable unless that is independently verified.
​
The evidence package classifies G as technically surviving but Low/QA because of the scope/deployment limitation.
​
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 3 days 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!