ID: task_050826_9
Assigned to: Robbert Abimbola (@robertocarlous)
Created: 2026-08-05
Status: Open
Repo: https://github.com/InfiniteZeroFoundation/DevNet
Base branch: develop HEAD → new branch feat/fair-launch-distributor
Roadmap ref: P4 (fair-launch distribution — no ROADMAP.md row yet, tracked via BL-18); the eligibility-computation slice (see "Explicitly out of scope" below) depends on P4-IDX1/P4-IDX2, also yours
Dates: Testnet dry-run — move fast, no external audit gate at this stage (see Context). Propose your own estimate once scoped; flag if anything below needs more than a few days.
Tracking: Issue #75 (long-running, overall effort) · Discussion #76 (progress updates, questions, PR links)
Abraham's fair-launch vision (relayed by Umer, Aug 5 2026): no ICO, no pre-sale, no VC allocation. DIN goes exclusively to validators/active network participants, distributed programmatically as an airdrop. This is a testnet dry-run — Abraham's own words: "move fast, mistakes are fixable (redeploy, adjust), no real money at stake" — deployed following the normal path: local anvil → devnet → testnet → mainnet. This is not the same mechanism as the existing per-GI validator reward emission (decay schedule, inflation guard) — that's an ongoing, separate flow. This is a one-time (or periodic-snapshot) grant.
This resolves an open fork already tracked in this repo: Developer/design/MECHANISM_DESIGN.md §9, item 14 ("ICO / token-sale mechanics — not resolved") named two options for retiring the depositAndMint faucet at mainnet — a proper on-ramp/ICO, or a validator-airdrop program. Abraham's answer picks airdrop. Full backlog entry: BL-18.
Governance note that bounds the admin design below: per DD-3 (resolved 2026-08-04), there is no on-chain multisig for protocol roles at this stage. Any multisig that exists is a plain Gnosis Safe scoped to treasury/fund custody only — not governance.
(Internal references below are for background only — you won't have access to these, everything you need is inlined in this task. Flagged as Umer-reference-only.)
This task is contract-only. It does not include eligibility computation (who qualifies, and for how much) — see "Explicitly out of scope" below for why, and what comes next.
You do not need to pull in any contracts from the 8 open P3 PRs (fee router, reward engine, staking governance, slashing taxonomy, etc.) — this contract only needs DinToken, which is already on develop. Build clean off current develop HEAD. ( pulling in other-PR contracts is fine if needed — flagging that it isn't needed here, so you don't go looking for a dependency that doesn't exist.)
Transparent Proxy pattern, matching the existing platform contracts exactly — same shape as DinValidatorStake.sol/DinToken.sol (Initializable, OwnableUpgradeable, initialize() instead of constructor args) and the same deploy pattern as foundry/script/DeployPlatform.s.sol (Upgrades.deployTransparentProxy(...)).
Important implementation note on vesting: the earlier design conversation described this as "OZ VestingWallet-style." Don't actually deploy OZ's VestingWallet/VestingWalletCliff — those are single-beneficiary contracts (one instance per recipient), which would mean deploying N contracts for N validators. What's wanted is one distributor contract holding a mapping(address => VestingPosition) internally, reusing OZ's linear-vesting math (cliff, then linear release to cliff + duration) but not the OZ contract class itself. lib/openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol is available and should be used as-is for proof verification.
State & functions (design intent — adjust as needed, but flag any deviation in your PR description):
initialize(address dinToken_, address treasuryAdmin_, uint64 cliffDuration_, uint64 vestingDuration_)—dinToken_/treasuryAdmin_addresses, cliff/vesting durations in seconds.treasuryAdmin_becomesowner(). No Gnosis Safe exists yet for testnet — pass an EOA (Umer's or a test wallet) astreasuryAdmin_for now; document in NatSpec that this should point at a Gnosis Safe once one exists, consistent with DD-3's "multisig = treasury only" model. Don't block on a Safe existing.deposit(uint256 amount)—onlyOwner, pullsamountviatransferFrom(msg.sender, address(this), amount)(treasury pre-approves), incrementstotalFunded. This is the "funded once from treasury" step — not per-user minting,DinToken.mintstaysonlyCoordinatorand is untouched.setRoot(bytes32 root_)—onlyOwner, settable only beforeopenClaims()has been called (so a snapshot can be corrected pre-launch); revert with a custom error after claims open, so a root can't be swapped out from under people who already hold valid proofs.openClaims()—onlyOwner, one-shot (revert if already open), freezes the current root, recordsvestingStart = block.timestamp(the vesting clock starts here, not at admin's leisure), setsclaimsOpen = true.claim(uint256 index, uint256 totalAmount, bytes32[] calldata proof)—nonReentrant(reuseReentrancyGuardTransient, same asDinCoordinator/DinValidatorStake). RequiresclaimsOpen. Leaf shape:keccak256(bytes.concat(keccak256(abi.encode(index, msg.sender, totalAmount))))(standard double-hashed leaf, avoids second-preimage issues) verified against the frozen root. One-time per address (revert on repeat). Creates the caller'sVestingPosition{total: totalAmount, claimed: 0}and immediately releases whatever's already vested as ofblock.timestamp(0 if still pre-cliff). IncrementstotalClaimedPrincipalbytotalAmount(the full allocation, not just what's released yet — needed for the sweep invariant below).release()—nonReentrant, callable by anyone for their own position, any number of times. No proof needed (position already exists post-claim). ReleasesvestedAmount(msg.sender) - position.claimed.vestedAmount(address account) public view returns (uint256)— standard cliff+linear formula:0beforevestingStart + cliffDuration;totalat/aftervestingStart + vestingDuration; linear interpolation between.sweepUnclaimed(address to)—onlyOwner, callable only after a deadline (vestingStart + vestingDuration + sweepGracePeriod— pick a reasonable grace period, e.g. 30 days, and make it a constant or initializer param, your call). SweepstotalFunded - totalClaimedPrincipal(i.e., allocations nobody ever claimed at all — not unreleased-but-claimed vesting, which stays claimable by its owner indefinitely) back toto. Addresses Q7's griefing concern (unclaimed allocations shouldn't sit stranded forever).- Custom errors throughout, not
require(cond, "string")— matches the gas-optimization convention already being applied elsewhere infoundry/src(see P3-6.3a inROADMAP.md). - Full NatSpec (
@notice/@param/@return) on every public/external function — matches P3-6.3b convention.
New foundry/script/DeployFairLaunchDistributor.s.sol, following DeployPlatform.s.sol's exact pattern (Upgrades.deployTransparentProxy, console.log the proxy address). Reads the existing DinToken proxy address the same way DeployPlatform.s.sol's later steps do. Don't wire this into dincli import-deployments — that's Python/dincli tooling, outside your lane; just get the contract deployable and log the address clearly enough that someone else can wire it up later.
Foundry tests using a hand-built test Merkle tree (4–8 addresses is enough) — this is what makes the contract testable end-to-end on local anvil without waiting on the real eligibility pipeline (see below). Cover at minimum:
- Happy path:
deposit→setRoot→openClaims→claim(0 vested pre-cliff) → warp past cliff →release()releases the right linear fraction → warp past full duration →release()drains remainder. - Invalid proof rejected; double
claim()rejected;setRootafteropenClaims()rejected;openClaims()called twice rejected. sweepUnclaimedbefore deadline rejected; after deadline, sweeps exactly the never-claimed portion, leaves already-claimed-but-unvested positions untouched.- Reentrancy guard present on
claim/release. - Funding invariant: can't claim more in total than
totalFunded.
Eligibility computation — deciding who qualifies and how much they get, and turning that into the (address, amount) list that generates the Merkle root — is not part of this task. Two reasons:
- It's off-chain scripting/aggregation, not contract work — outside your Solidity/Foundry lane.
- Umer's call (Aug 5 2026): fair-launch distribution should sit in P4, and the eligibility snapshot should be computed off the on-chain indexer (P4-IDX1/P4-IDX2 — also your work,
Developer/ROADMAP.md) rather than a raw RPC-log script, once that indexer exists. The snapshot script itself would then be run by "authority" (i.e., executed by whoever holds thetreasuryAdmin_key/Safe — an operational script, not on-chain logic) to compute the list and callsetRoot().
That means this task's tests use a hand-built test tree specifically so the contract can be built and verified now, without waiting on the indexer. The real indexer-backed eligibility script is the next slice in this line of work — flag it as such (don't start it) once this task is done, same convention as prior deferred-slice tasks in this repo.
Per Abraham/Umer: if anything below is ambiguous once you're in the code, stop and ask rather than picking a default silently.
- Cliff/vesting duration values. No concrete numbers have been set (e.g., "3-month cliff + 9-month linear" is just an example, not a decision). Take them as
initialize()params (not hardcoded) so this isn't a blocker — but flag in your PR that the actual testnet values need to come from Umer/Abraham beforeopenClaims()is ever called for real. - Sweep grace period. Pick a reasonable default (suggest 30 days after full vesting) but call it out explicitly as a placeholder in NatSpec.
- Contract naming.
DinFairLaunchDistributoris a suggestion, not a mandate — check it doesn't collide with anything and use your judgment on the exact name.
-
foundry/src/DinFairLaunchDistributor.sol— Merkle-claim + cliff/linear vesting, Transparent Proxy, custom errors, full NatSpec -
foundry/script/DeployFairLaunchDistributor.s.sol— deploy script followingDeployPlatform.s.sol's pattern -
foundry/test/DinFairLaunchDistributor.t.sol(or similar) — tests per section 3, hand-built Merkle fixture -
forge build && forge testgreen (plain build is fine for iteration; confirm againstvia_ir = trueprofile before opening the PR since that's the real deploy config) - PR opened from
feat/fair-launch-distributoragainstdevelop, referencing this task file andBL-18 - PR description flags: the three open questions above (with your chosen defaults called out), and explicitly notes the eligibility-computation slice is deferred pending P4-IDX1/IDX2
- This is a fresh line of work, not a continuation — no prior branch/PR to build on.
- Base branch is
developHEAD as of whenever you start; re-pull right before opening the PR in case it's moved. - If it turns out you genuinely do need a contract from one of the open P3 PRs (fee router, reward engine, etc.) once you're in the code, that's fine to pull in — Abraham's explicit go-ahead — just call out exactly which PR/commit in your PR description so review knows what's borrowed vs. new.