You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Epic: #1229 · Spec: §5.3, §6.2 · Type: CODE (Solidity) · Estimate: 0.7 day · Depends on: T2.1
🚨 Blocker
Current contracts/MerkleClaim.sol (deployed name PLOTAirdrop) has NO owner, NO deadline, NO sweep/withdraw path. If T6.3 transfers released_pool PLOT into that contract and users don't claim, those tokens are stuck forever — T6.5 cannot execute.
RE1 round-6 caught this. Must be deployed BEFORE T6.2 settlement contract goes live.
Scope
Modify contracts/MerkleClaim.sol + bootstrap Foundry test infrastructure (no framework exists yet — contracts/ only contains the source file, no foundry.toml).
Part A — Contract changes
(a) Rename contract PLOTAirdrop → MerkleClaim
The filename is already MerkleClaim.sol and the spec consistently uses "MerkleClaim" terminology. Rename the contract declaration to match. Update doc comment block (@title MerkleClaim). Eliminates downstream naming ambiguity.
(b) Add owner / admin
address public immutable owner set in constructor (deployer = owner)
onlyOwner modifier
(c) Add claim deadline as DIRECT constructor argument
uint256 public immutable claimDeadline set in constructor
Operator computes at T6.2 deploy time as (intended claim-open timestamp) + getClaimWindowSeconds(AIRDROP_CONFIG) — the helper from T2.1 ([T2.1] Update lib/airdrop/config.ts (PROD + TEST configs) #1236) resolves PROD's CLAIM_WINDOW_DAYS × 86400 OR TEST modes' CLAIM_WINDOW_SECONDS to a single integer-seconds value. Contract itself is config-agnostic; it just accepts a unix timestamp.
Modify claim() to revert if block.timestamp > claimDeadline
NO internal CAMPAIGN_END + 30d computation (RE1 round-7)
Part B — Bootstrap Foundry test framework (RE1 round-12)
contracts/ currently has no test infrastructure. Bootstrap it:
Install Foundry (via foundryup if not installed)
forge init --no-commit at repo root OR create the structure manually:
contracts/
├── MerkleClaim.sol
├── test/
│ └── MerkleClaim.t.sol ← Foundry test
├── script/
│ └── DeployMerkleClaim.s.sol ← deploy script (used at T6.2)
foundry.toml ← config
remappings.txt ← if needed
foundry.toml minimal config (RE1 round-14: scope all Foundry paths under contracts/ to avoid colliding with the existing TypeScript lib/ directory which has 39 entries of app code):
[profile.default]
src = "contracts"test = "contracts/test"script = "contracts/script"out = "contracts/out"cache_path = "contracts/cache"libs = ["contracts/lib"] # NOT "lib" — that is the TypeScript app dirsolc = "0.8.20"
forge install writes dependencies to contracts/lib/ rather than polluting the app source tree.
If team prefers committing vendored Foundry deps for reproducibility, remove contracts/lib/ from gitignore. Default: gitignored + setup script runs forge install.
.env.example — repo already has /Users/cho/Projects/plotlink/.env.example (Supabase + Filebase + other app vars, ~3.7KB). DO NOT overwrite. Two acceptable options (pick at PR time, document choice):
Option (a) — Append a Foundry/deploy section to existing .env.example (recommended for single-source):
# -----------------------------------------------------------------------------
# Foundry / Contract Deploy (§T2.7 MerkleClaim)
# -----------------------------------------------------------------------------
BASE_RPC_URL=https://mainnet.base.org
DEPLOYER_PRIVATE_KEY= # NEVER commit real key — set in shell only
PLOT_TOKEN_ADDRESS=0x4F567DACBF9D15A6acBe4A47FC2Ade0719Fb63C4
Option (b) — Create contracts/.env.example (recommended if contract ops are isolated from app):
Same content as above, scoped under contracts/ folder
forge commands run from contracts/ directory pick up local .env
Either way: confirm git diff does NOT touch existing .env.example Supabase/Filebase keys. If accidentally clobbered, restore from git checkout -- .env.example before commit.
Setup script (RE1 round-23 — required if contracts/lib/ stays gitignored): create contracts/setup.sh (or add to root Makefile / package.json script) with the exact forge install commands so a fresh clone can build:
Document in repo README or docs/airdrop-contracts.md: "Run bash contracts/setup.sh once after fresh clone before testing/deploying."
Security: never commit .env, never hardcode DEPLOYER_PRIVATE_KEY. Anvil-generated test addresses (deterministic accounts) for tests — no real funds. broadcast/ folder contains tx data → gitignored.
Part C — Unit tests in contracts/test/MerkleClaim.t.sol
Required:
test_claim_BeforeDeadline_Succeeds — eligible address claims within window
test_claim_AfterDeadline_Reverts — claim attempt after claimDeadline reverts
test_claim_InvalidProof_Reverts — bad Merkle proof reverts
test_claim_DoubleClaim_Reverts — already-claimed address can't claim again
test_sweep_BeforeDeadline_Reverts — sweep before deadline reverts with "Claim window still open"
Epic: #1229 · Spec: §5.3, §6.2 · Type: CODE (Solidity) · Estimate: 0.7 day · Depends on: T2.1
🚨 Blocker
Current
contracts/MerkleClaim.sol(deployed namePLOTAirdrop) has NO owner, NO deadline, NO sweep/withdraw path. If T6.3 transfersreleased_poolPLOT into that contract and users don't claim, those tokens are stuck forever — T6.5 cannot execute.RE1 round-6 caught this. Must be deployed BEFORE T6.2 settlement contract goes live.
Scope
Modify
contracts/MerkleClaim.sol+ bootstrap Foundry test infrastructure (no framework exists yet —contracts/only contains the source file, nofoundry.toml).Part A — Contract changes
(a) Rename contract
PLOTAirdrop→MerkleClaimThe filename is already
MerkleClaim.soland the spec consistently uses "MerkleClaim" terminology. Rename the contract declaration to match. Update doc comment block (@title MerkleClaim). Eliminates downstream naming ambiguity.(b) Add owner / admin
address public immutable ownerset in constructor (deployer = owner)onlyOwnermodifier(c) Add claim deadline as DIRECT constructor argument
uint256 public immutable claimDeadlineset in constructor(intended claim-open timestamp) + getClaimWindowSeconds(AIRDROP_CONFIG)— the helper from T2.1 ([T2.1] Update lib/airdrop/config.ts (PROD + TEST configs) #1236) resolves PROD'sCLAIM_WINDOW_DAYS × 86400OR TEST modes'CLAIM_WINDOW_SECONDSto a single integer-seconds value. Contract itself is config-agnostic; it just accepts a unix timestamp.claim()to revert ifblock.timestamp > claimDeadlineCAMPAIGN_END + 30dcomputation (RE1 round-7)(d) Add sweep function
(e) Add event
Constructor signature
Part B — Bootstrap Foundry test framework (RE1 round-12)
contracts/currently has no test infrastructure. Bootstrap it:Install Foundry (via
foundryupif not installed)forge init --no-commitat repo root OR create the structure manually:foundry.tomlminimal config (RE1 round-14: scope all Foundry paths undercontracts/to avoid colliding with the existing TypeScriptlib/directory which has 39 entries of app code):forge installwrites dependencies tocontracts/lib/rather than polluting the app source tree..gitignoreadditions:If team prefers committing vendored Foundry deps for reproducibility, remove
contracts/lib/from gitignore. Default: gitignored + setup script runsforge install..env.example— repo already has/Users/cho/Projects/plotlink/.env.example(Supabase + Filebase + other app vars, ~3.7KB). DO NOT overwrite. Two acceptable options (pick at PR time, document choice):Option (a) — Append a Foundry/deploy section to existing
.env.example(recommended for single-source):Option (b) — Create
contracts/.env.example(recommended if contract ops are isolated from app):contracts/folderforgecommands run fromcontracts/directory pick up local.envEither way: confirm
git diffdoes NOT touch existing.env.exampleSupabase/Filebase keys. If accidentally clobbered, restore fromgit checkout -- .env.examplebefore commit.Setup script (RE1 round-23 — required if
contracts/lib/stays gitignored): createcontracts/setup.sh(or add to rootMakefile/package.jsonscript) with the exactforge installcommands so a fresh clone can build:Document in repo README or
docs/airdrop-contracts.md: "Runbash contracts/setup.shonce after fresh clone before testing/deploying."Security: never commit
.env, never hardcodeDEPLOYER_PRIVATE_KEY. Anvil-generated test addresses (deterministic accounts) for tests — no real funds.broadcast/folder contains tx data → gitignored.Part C — Unit tests in
contracts/test/MerkleClaim.t.solRequired:
test_claim_BeforeDeadline_Succeeds— eligible address claims within windowtest_claim_AfterDeadline_Reverts— claim attempt afterclaimDeadlinerevertstest_claim_InvalidProof_Reverts— bad Merkle proof revertstest_claim_DoubleClaim_Reverts— already-claimed address can't claim againtest_sweep_BeforeDeadline_Reverts— sweep before deadline reverts with "Claim window still open"test_sweep_AfterDeadline_BySomeone_Reverts— non-owner sweep revertstest_sweep_AfterDeadline_ByOwner_Succeeds— owner sweep after deadline transfers full balancetest_sweep_DoubleSweep_TransfersZero— second sweep after first sends 0 (or reverts; document choice)Run via
forge test -vv. Gas snapshot viaforge snapshot→ claim path within +5% of v1.What NOT to add (per spec intent + R21)
setMerkleRootCAMPAIGN_END + 30dcomputationMigration of T6.5
After this lands, T6.5 simplifies: operator calls
sweepUnclaimed(deadAddress)once. Already reflected in #1266.Acceptance
PLOTAirdrop→MerkleClaimfoundry.toml+contracts/test/exists)foundry.tomllibs = ["contracts/lib"](NOT["lib"]) — no collision with TypeScript applib/dirforge install(if invoked) writes tocontracts/lib/, notlib/— verify viagit statusshows no untracked files in repolib/contracts/setup.sh(or equivalent script entry) exists withforge installcommands so fresh clone can builddocs/airdrop-contracts.mddocuments the setup step.gitignoreincludescontracts/out/,contracts/cache/,contracts/lib/,broadcast/,.env,.env.local.env.exampledocuments required env vars; no real keys committedforge test -vv)Dependencies
T2.1 (#1236 defines claim window config +
getClaimWindowSeconds(AIRDROP_CONFIG)helper — operator uses the helper for deadline computation at T6.2; contract itself is config-agnostic)Blocks
T6.2 (#1263), T6.3 (#1264 closed - merged), T6.5 (#1266) — without this, settlement is one-way and tokens get stranded.