diff --git a/project-words.txt b/project-words.txt index 902d278..90f9b04 100644 --- a/project-words.txt +++ b/project-words.txt @@ -3,6 +3,7 @@ disbursable EpochRewardDidntChange idOS IERC20 +Masterlist MULTICALL reentrancy soldeer diff --git a/script/initial-distribution/README.md b/script/initial-distribution/README.md index 7bb3e0a..80cb1b0 100644 --- a/script/initial-distribution/README.md +++ b/script/initial-distribution/README.md @@ -1,5 +1,13 @@ # Initial Distribution +## Note well + +There's post-scriptums to this script. There are a few details that we only +caught after deploying TDEDisbursement.sol that require manual intervention. See +the end of this README for more details on those. + +## Overview + TypeScript tooling that drives the IDOS token initial distribution using the on-chain `TDEDisbursement` contract. There are two main flows: @@ -167,3 +175,49 @@ pnpm format - **`disbursement.csv`** -- ~41k rows with columns: `Wallet address`, `Token amount 10e18`, `Modality`. Used by the TDE flow. + +## Post-scriptums + +### 2026-03-04: Treasury and Staking Rewards modalities + +We didn't get these right at the start. They should have been: + +| Modality | Start | Cliff | End | +| ------------------------------ | ---------------------- | ---------------------- | ---------------------- | +| Staking Rewards Year 1 - 2 | 2026-03-05T00:00:00Z | 2026-04-05T00:00:00Z | 2028-02-05T00:00:00Z | +| Staking Rewards Year 3-6 | 2028-02-05T00:00:00Z | 2028-03-05T00:00:00Z | 2031-02-05T00:00:00Z | +| Staking Rewards Year 7 - 10 | 2031-02-05T00:00:00Z | 2031-03-05T00:00:00Z | 2035-02-05T00:00:00Z | +| Treasury | 2026-03-05T00:00:00Z | 2026-04-05T00:00:00Z | 2031-02-05T00:00:00Z | +| ------------------------------ | ---------------------- | ---------------------- | ---------------------- | + +The differences are: + +- The split on Staking Rewards is such that we can distribute different amounts + of tokens to time spans (instead of having a single sum over a single linear + vesting period). +- The treasury is end date was one month too short. + +The beneficiary for all of those is 0xd5259b6E9D8a413889953a1F3195D8F8350642dE, +idOS's main treasury wallet. + +Translating that to IDOSVesting constructor parameters, we get: + +| Modality | beneficiary | startTimestamp | durationSeconds | cliffSeconds | +| --------------------------- | ------------------------------------------ | -------------- | --------------- | ------------ | +| Staking Rewards Year 1 - 2 | 0xd5259b6E9D8a413889953a1F3195D8F8350642dE | 1772668800 | 60652800 | 2678400 | +| Staking Rewards Year 3-6 | 0xd5259b6E9D8a413889953a1F3195D8F8350642dE | 1833321600 | 94694400 | 2505600 | +| Staking Rewards Year 7 - 10 | 0xd5259b6E9D8a413889953a1F3195D8F8350642dE | 1928016000 | 126230400 | 2419200 | +| Treasury | 0xd5259b6E9D8a413889953a1F3195D8F8350642dE | 1772668800 | 155347200 | 2678400 | + +The deployed vesting contracts are: + +| Modality | Vesting contract address | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | +| Staking Rewards Year 1 - 2 | [0x03ed348892a88182e74d8e76e6f7529224032ed8](https://arbiscan.io/address/0x03ed348892a88182e74d8e76e6f7529224032ed8) | +| Staking Rewards Year 3-6 | [0xd7740bf4fbd6f7633aec11e51f9b8d7dd6c0ae40](https://arbiscan.io/address/0xd7740bf4fbd6f7633aec11e51f9b8d7dd6c0ae40) | +| Staking Rewards Year 7 - 10 | [0x21d91cedf2cf162c87f14ce988a04c35737f7e0d](https://arbiscan.io/address/0x21d91cedf2cf162c87f14ce988a04c35737f7e0d) | +| Treasury | [0x6a553c044a6a113b01be52372e8d7bc94594bbe8](https://arbiscan.io/address/0x6a553c044a6a113b01be52372e8d7bc94594bbe8) | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | + +These four contracts will be manually funded, and its transactions won't be +tracked on TDEDisbursement. diff --git a/script/initial-distribution/src/csv.test.ts b/script/initial-distribution/src/csv.test.ts index 1c94377..287f6ca 100644 --- a/script/initial-distribution/src/csv.test.ts +++ b/script/initial-distribution/src/csv.test.ts @@ -86,4 +86,21 @@ describe("loadDisbursementCsv", () => { const rows = loadDisbursementCsv(path); assert.equal(rows[0].address, getAddress(lowercase)); }); + + it("excludes modalities disbursed manually (C - 12 - 36, C - 6 - 36, Treasury, Staking Rewards)", () => { + const path = writeCsv( + [ + "Wallet address,Token amount 10e18,Modality", + `${VALID_ADDRESS},100,Masterlist`, + `${VALID_ADDRESS},200,C - 12 - 36`, + `${VALID_ADDRESS},300,C - 6 - 36`, + `${VALID_ADDRESS},400,Treasury`, + `${VALID_ADDRESS},500,Staking Rewards`, + ].join("\n"), + ); + const rows = loadDisbursementCsv(path); + assert.equal(rows.length, 1); + assert.equal(rows[0].modality, EVMModality.DIRECT); + assert.equal(rows[0].amount, 100n); + }); }); diff --git a/script/initial-distribution/src/csv.ts b/script/initial-distribution/src/csv.ts index 50dda9d..4593821 100644 --- a/script/initial-distribution/src/csv.ts +++ b/script/initial-distribution/src/csv.ts @@ -4,6 +4,7 @@ import { type Address, getAddress } from "viem"; import { type EVMModality, isKnownModality, + isScriptDisbursedModality, MODALITIES_TS_TO_EVM, type Modality, } from "./modalities.js"; @@ -47,9 +48,11 @@ export function loadDisbursementCsv(csvPath: string): DisbursementRow[] { ); } - return rows.map((row) => ({ - address: getAddress(row["Wallet address"]), - modality: MODALITIES_TS_TO_EVM[row.Modality as Modality], - amount: parseBigInt(row["Token amount 10e18"]), - })); + return rows + .filter((row) => isScriptDisbursedModality(row.Modality as Modality)) + .map((row) => ({ + address: getAddress(row["Wallet address"]), + modality: MODALITIES_TS_TO_EVM[row.Modality as Modality], + amount: parseBigInt(row["Token amount 10e18"]), + })); } diff --git a/script/initial-distribution/src/modalities.test.ts b/script/initial-distribution/src/modalities.test.ts index 9912302..4ebc1c9 100644 --- a/script/initial-distribution/src/modalities.test.ts +++ b/script/initial-distribution/src/modalities.test.ts @@ -1,7 +1,13 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { iso8601ToTimestamp } from "./lib.js"; -import { EVMModality, isKnownModality, MODALITIES, MODALITIES_TS_TO_EVM } from "./modalities.js"; +import { + EVMModality, + isKnownModality, + MODALITIES, + MODALITIES_NOT_DISBURSED_BY_OUR_SCRIPT, + MODALITIES_TS_TO_EVM, +} from "./modalities.js"; describe("isKnownModality", () => { for (const name of Object.keys(MODALITIES)) { @@ -10,6 +16,12 @@ describe("isKnownModality", () => { }); } + for (const name of MODALITIES_NOT_DISBURSED_BY_OUR_SCRIPT) { + it(`returns true for not-disbursed-by-script "${name}"`, () => { + assert.equal(isKnownModality(name), true); + }); + } + for (const unknown of ["Unknown", "", "masterlist", "DIRECT", "SM-0-12"]) { it(`returns false for "${unknown}"`, () => { assert.equal(isKnownModality(unknown), false); @@ -37,11 +49,7 @@ describe("EVMModality matches Solidity enum Modality in TDEDisbursement.sol", () for (const [ordinal, name] of solidityEnum.entries()) { it(`${name} = ${ordinal}`, () => { - assert.equal( - EVMModality[name], - ordinal, - `EVMModality.${name} should be ${ordinal}`, - ); + assert.equal(EVMModality[name], ordinal, `EVMModality.${name} should be ${ordinal}`); }); } }); @@ -83,24 +91,15 @@ describe("MODALITIES timestamps match VESTING_PARAMS_FOR_MODALITY in TDEDisburse const [vestingStartIso, cliffEndIso, vestingEndIso] = timestamps; it(`"${tsModality}" (${evmName}): vestingStart matches startTimestamp`, () => { - assert.equal( - iso8601ToTimestamp(vestingStartIso), - startTimestamp, - ); + assert.equal(iso8601ToTimestamp(vestingStartIso), startTimestamp); }); it(`"${tsModality}" (${evmName}): cliffEnd matches startTimestamp + cliffSeconds`, () => { - assert.equal( - iso8601ToTimestamp(cliffEndIso), - startTimestamp + cliffSeconds, - ); + assert.equal(iso8601ToTimestamp(cliffEndIso), startTimestamp + cliffSeconds); }); it(`"${tsModality}" (${evmName}): vestingEnd matches startTimestamp + durationSeconds`, () => { - assert.equal( - iso8601ToTimestamp(vestingEndIso), - startTimestamp + durationSeconds, - ); + assert.equal(iso8601ToTimestamp(vestingEndIso), startTimestamp + durationSeconds); }); } }); diff --git a/script/initial-distribution/src/modalities.ts b/script/initial-distribution/src/modalities.ts index df7ab6a..441d674 100644 --- a/script/initial-distribution/src/modalities.ts +++ b/script/initial-distribution/src/modalities.ts @@ -9,13 +9,24 @@ export const MODALITIES = { "SM - 12 - 36": ["2027-02-05T15:00:00Z", "2027-03-05T15:00:00Z", "2030-02-05T15:00:00Z"], "SM - 6 - 12": ["2026-08-05T15:00:00Z", "2026-09-05T15:00:00Z", "2027-08-05T15:00:00Z"], "SM - 6 - 24": ["2026-08-05T15:00:00Z", "2026-09-05T15:00:00Z", "2028-08-05T15:00:00Z"], - "Staking Rewards": ["2026-02-05T15:00:00Z", "2026-03-05T15:00:00Z", "2036-02-05T15:00:00Z"], - Treasury: ["2026-03-05T15:00:00Z", "2026-04-05T15:00:00Z", "2031-03-05T15:00:00Z"], Masterlist: null, - "C - 12 - 36": null, - "C - 6 - 36": null, } as const; -export type Modality = keyof typeof MODALITIES; +type ModalityScriptDisbursed = keyof typeof MODALITIES; + +/** Modalities disbursed manually by other processes; scripts must ignore these. */ +export const MODALITIES_NOT_DISBURSED_BY_OUR_SCRIPT = [ + "C - 12 - 36", + "C - 6 - 36", + "Treasury", + "Staking Rewards", +] as const; +type ModalityNotScriptDisbursed = (typeof MODALITIES_NOT_DISBURSED_BY_OUR_SCRIPT)[number]; + +export type Modality = ModalityScriptDisbursed | ModalityNotScriptDisbursed; + +export function isScriptDisbursedModality(m: Modality): boolean { + return !(MODALITIES_NOT_DISBURSED_BY_OUR_SCRIPT as readonly string[]).includes(m); +} // Mirrors the Solidity `enum Modality` in TDEDisbursement.sol. export const EVMModality = { @@ -32,7 +43,7 @@ export const EVMModality = { } as const; export type EVMModality = (typeof EVMModality)[keyof typeof EVMModality]; -export const MODALITIES_TS_TO_EVM: Record = { +export const MODALITIES_TS_TO_EVM: Record = { "FCL Months 2-6": 3, "SM - 0 - 12": 1, "SM - 1 - 5": 3, @@ -42,13 +53,11 @@ export const MODALITIES_TS_TO_EVM: Record = { "SM - 12 - 36": 9, "SM - 6 - 12": 6, "SM - 6 - 24": 7, - "Staking Rewards": 2, - Treasury: 5, Masterlist: 0, - "C - 12 - 36": 0, - "C - 6 - 36": 0, }; export function isKnownModality(s: string): s is Modality { - return s in MODALITIES; + return ( + s in MODALITIES || (MODALITIES_NOT_DISBURSED_BY_OUR_SCRIPT as readonly string[]).includes(s) + ); }