Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ disbursable
EpochRewardDidntChange
idOS
IERC20
Masterlist
MULTICALL
reentrancy
soldeer
Expand Down
54 changes: 54 additions & 0 deletions script/initial-distribution/README.md
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
pkoch marked this conversation as resolved.

## Overview

TypeScript tooling that drives the IDOS token initial distribution using the
on-chain `TDEDisbursement` contract. There are two main flows:

Expand Down Expand Up @@ -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.
17 changes: 17 additions & 0 deletions script/initial-distribution/src/csv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
13 changes: 8 additions & 5 deletions script/initial-distribution/src/csv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type Address, getAddress } from "viem";
import {
type EVMModality,
isKnownModality,
isScriptDisbursedModality,
MODALITIES_TS_TO_EVM,
type Modality,
} from "./modalities.js";
Expand Down Expand Up @@ -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"]),
}));
}
35 changes: 17 additions & 18 deletions script/initial-distribution/src/modalities.test.ts
Original file line number Diff line number Diff line change
@@ -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)) {
Expand All @@ -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);
Expand Down Expand Up @@ -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}`);
});
}
});
Expand Down Expand Up @@ -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);
});
}
});
31 changes: 20 additions & 11 deletions script/initial-distribution/src/modalities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -32,7 +43,7 @@ export const EVMModality = {
} as const;
export type EVMModality = (typeof EVMModality)[keyof typeof EVMModality];

export const MODALITIES_TS_TO_EVM: Record<Modality, EVMModality> = {
export const MODALITIES_TS_TO_EVM: Record<ModalityScriptDisbursed, EVMModality> = {
"FCL Months 2-6": 3,
"SM - 0 - 12": 1,
"SM - 1 - 5": 3,
Expand All @@ -42,13 +53,11 @@ export const MODALITIES_TS_TO_EVM: Record<Modality, EVMModality> = {
"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)
);
Comment thread
pkoch marked this conversation as resolved.
}