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
9 changes: 9 additions & 0 deletions script/initial-distribution/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ SOLD_TOKEN_ADDRESS=
# Address of the TDEDisbursement contract (used for all CCA disbursements)
TDE_DISBURSEMENT_ADDRESS=

# Block number at which TDEDisbursement was deployed (used as event scan start)
TDE_DISBURSEMENT_DEPLOYMENT_BLOCK=

# Address of the BatchCaller contract (used for EIP-7702 batch execution)
BATCH_CALLER_ADDRESS=

# TDE date/time (ISO 8601 Zulu). Before this, only vested disbursements run; DIRECT recipients are skipped.
TDE_DATETIME=2026-03-05T15:00:00Z

# Timestamp when the Normal phase starts (ISO 8601 Zulu, e.g. 2025-06-01T00:00:00Z).
# Bids in blocks at or after this timestamp are Normal phase; earlier blocks are Whale phase.
NORMAL_PHASE_START=
16 changes: 9 additions & 7 deletions script/initial-distribution/src/findPendingRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,23 @@ export function findPendingRows(
rows: DisbursementRow[],
logs: { beneficiary: Address; modality: number; amount: bigint }[],
): DisbursementRow[] {
const counts = new Map<string, number>();
const logCountsByKey = new Map<string, number>();
for (const log of logs) {
const key = disbursementKey(log.beneficiary, log.modality, log.amount);
counts.set(key, (counts.get(key) ?? 0) + 1);

logCountsByKey.set(key, (logCountsByKey.get(key) ?? 0) + 1);
}

const pending: DisbursementRow[] = [];
const pendingRows: DisbursementRow[] = [];
for (const row of rows) {
const key = disbursementKey(row.address, row.modality, row.amount);
const remaining = counts.get(key) ?? 0;

const remaining = logCountsByKey.get(key) ?? 0;
if (remaining > 0) {
counts.set(key, remaining - 1);
logCountsByKey.set(key, remaining - 1);
} else {
pending.push(row);
pendingRows.push(row);
}
}
return pending;
return pendingRows;
}
138 changes: 45 additions & 93 deletions script/initial-distribution/src/tde.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,59 @@
import "dotenv/config";
import { encodeFunctionData, getAddress, getContract } from "viem";
import { nonceManager } from "viem/accounts";
import { erc20Abi, tdeDisbursementAbi } from "./abis.js";
import {
type BatchCall,
type BatchCallerConfig,
clearDelegation,
ensureDelegation,
executeInGasFilledBatches,
} from "./batch.js";
import { chainSetup } from "./chains.js";
import { clearDelegation, ensureDelegation } from "./batch.js";
import { type DisbursementRow, loadDisbursementCsv } from "./csv.js";
import { findPendingRows } from "./findPendingRows.js";
import { ensureHex, paginatedGetEvents, receiptFor, requiredArgs, requireEnv } from "./lib.js";

// --- Config ---

const CHAIN_ID = requireEnv("CHAIN_ID");
const TDE_DISBURSEMENT_ADDRESS = getAddress(requireEnv("TDE_DISBURSEMENT_ADDRESS"));
const TDE_DISBURSEMENT_DEPLOYMENT_BLOCK = BigInt(requireEnv("TDE_DISBURSEMENT_DEPLOYMENT_BLOCK"));
const BATCH_CALLER_ADDRESS = getAddress(requireEnv("BATCH_CALLER_ADDRESS"));
const DISBURSER_PRIVATE_KEY = ensureHex(requireEnv("DISBURSER_PRIVATE_KEY"));
const RPC_URL = requireEnv("RPC_URL");

const { account, publicClient, walletClient } = await chainSetup(
CHAIN_ID,
RPC_URL,
DISBURSER_PRIVATE_KEY,
{ nonceManager },
);

const batchConfig: BatchCallerConfig = {
import { sumOf } from "./lib.js";
import { EVMModality } from "./modalities.js";
import {
disburseAll,
ensureAllowance,
fetchDisbursementLogs,
setupTdeEnvironment,
} from "./tdeSetup.js";

const {
tdeTimestamp,
nowTimestamp,
account,
publicClient,
walletClient,
batchCallerAddress: BATCH_CALLER_ADDRESS,
};
batchConfig,
tokenContract,
tdeDisbursementAddress,
tdeDisbursementDeploymentBlock,
} = await setupTdeEnvironment();

const tdeDisbursementContract = getContract({
address: TDE_DISBURSEMENT_ADDRESS,
abi: tdeDisbursementAbi,
client: walletClient,
});

const tokenContract = getContract({
address: await tdeDisbursementContract.read.IDOS_TOKEN(),
abi: erc20Abi,
client: walletClient,
});

// --- Helpers ---

async function ensureAllowance(totalNeeded: bigint): Promise<void> {
const allowance = await tokenContract.read.allowance([account.address, TDE_DISBURSEMENT_ADDRESS]);

if (allowance >= totalNeeded) return;

await receiptFor(
publicClient,
await tokenContract.write.approve([TDE_DISBURSEMENT_ADDRESS, totalNeeded]),
);
}

async function disburseAll(pending: DisbursementRow[]): Promise<void> {
const calls: BatchCall[] = pending.map(({ address, modality, amount }) => ({
target: TDE_DISBURSEMENT_ADDRESS,
data: encodeFunctionData({
abi: tdeDisbursementAbi,
functionName: "disburse",
args: [address, amount, modality],
}),
}));

await executeInGasFilledBatches(batchConfig, calls, "disbursed");
}

// --- Main ---
const filterCurrentlyDisbursableRows =
nowTimestamp < tdeTimestamp
? (rows: DisbursementRow[]) => rows.filter((r) => r.modality !== EVMModality.DIRECT)
: (rows: DisbursementRow[]) => rows;

const allRows = loadDisbursementCsv("disbursement.csv");
const latestBlock = await publicClient.getBlockNumber();
const logs = (
await paginatedGetEvents(
(r) =>
publicClient.getContractEvents({
address: TDE_DISBURSEMENT_ADDRESS,
abi: tdeDisbursementAbi,
eventName: "Disbursed",
...r,
}),
TDE_DISBURSEMENT_DEPLOYMENT_BLOCK,
latestBlock,
)
).map((l) => requiredArgs(l));
const logs = await fetchDisbursementLogs(
publicClient,
tdeDisbursementAddress,
tdeDisbursementDeploymentBlock,
);
const pendingRows = findPendingRows(allRows, logs);
const disbursableRows = filterCurrentlyDisbursableRows(pendingRows);
const alreadyDisbursed = allRows.length - pendingRows.length;
const skipped = pendingRows.length - disbursableRows.length;

console.error(`${alreadyDisbursed} already disbursed`);
if (skipped > 0) console.error(`${skipped} skipped (DIRECT, before TDE)`);

if (pendingRows.length === 0) {
console.error(`All ${allRows.length} disbursements already recorded on-chain, nothing to do.`);
if (disbursableRows.length === 0) {
console.error("Nothing to do.");
} else {
console.error(
`${allRows.length - pendingRows.length} already disbursed (by event logs), disbursing ${pendingRows.length}...`,
);
console.error(`Disbursing ${disbursableRows.length}...`);

await ensureAllowance(
tokenContract,
account.address,
publicClient,
tdeDisbursementAddress,
sumOf(disbursableRows.map((r) => r.amount)),
);
await ensureDelegation(batchConfig);
await ensureAllowance(pendingRows.reduce((sum, row) => sum + row.amount, 0n));
await disburseAll(pendingRows);
await disburseAll(batchConfig, tdeDisbursementAddress, disbursableRows);
await clearDelegation(batchConfig);
Comment thread
pkoch marked this conversation as resolved.
}
118 changes: 118 additions & 0 deletions script/initial-distribution/src/tdeSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { type Address, encodeFunctionData, getAddress, getContract, type PublicClient } from "viem";
import { nonceManager } from "viem/accounts";
import { erc20Abi, tdeDisbursementAbi } from "./abis.js";
import { type BatchCallerConfig, executeInGasFilledBatches } from "./batch.js";
import { chainSetup } from "./chains.js";
import type { DisbursementRow } from "./csv.js";
import {
ensureHex,
iso8601ToTimestamp,
paginatedGetEvents,
receiptFor,
requiredArgs,
requireEnv,
} from "./lib.js";

export async function setupTdeEnvironment() {
const CHAIN_ID = requireEnv("CHAIN_ID");
const tdeDisbursementAddress = getAddress(requireEnv("TDE_DISBURSEMENT_ADDRESS"));
const tdeDisbursementDeploymentBlock = BigInt(requireEnv("TDE_DISBURSEMENT_DEPLOYMENT_BLOCK"));
const BATCH_CALLER_ADDRESS = getAddress(requireEnv("BATCH_CALLER_ADDRESS"));
const DISBURSER_PRIVATE_KEY = ensureHex(requireEnv("DISBURSER_PRIVATE_KEY"));
const RPC_URL = requireEnv("RPC_URL");

const tdeTimestamp = iso8601ToTimestamp(requireEnv("TDE_DATETIME"));
const nowTimestamp = BigInt(Math.floor(Date.now() / 1000));

const { account, publicClient, walletClient } = await chainSetup(
CHAIN_ID,
RPC_URL,
DISBURSER_PRIVATE_KEY,
{ nonceManager },
);

const batchConfig: BatchCallerConfig = {
publicClient,
walletClient,
batchCallerAddress: BATCH_CALLER_ADDRESS,
};

const tdeDisbursementContract = getContract({
address: tdeDisbursementAddress,
abi: tdeDisbursementAbi,
client: walletClient,
});

const tokenContract = getContract({
address: await tdeDisbursementContract.read.IDOS_TOKEN(),
abi: erc20Abi,
client: walletClient,
});

return {
account,
publicClient,
batchConfig,
tokenContract,
tdeDisbursementAddress,
tdeDisbursementDeploymentBlock,
tdeTimestamp,
nowTimestamp,
};
}

export async function ensureAllowance(
tokenContract: Awaited<ReturnType<typeof setupTdeEnvironment>>["tokenContract"],
accountAddress: Address,
publicClient: PublicClient,
tdeDisbursementAddress: Address,
totalNeeded: bigint,
): Promise<void> {
const allowance = await tokenContract.read.allowance([accountAddress, tdeDisbursementAddress]);

if (allowance >= totalNeeded) return;

await receiptFor(
publicClient,
await tokenContract.write.approve([tdeDisbursementAddress, totalNeeded]),
);
}

export async function disburseAll(
batchConfig: BatchCallerConfig,
tdeDisbursementAddress: Address,
pending: DisbursementRow[],
): Promise<void> {
await executeInGasFilledBatches(
batchConfig,
pending.map(({ address, modality, amount }) => ({
target: tdeDisbursementAddress,
data: encodeFunctionData({
abi: tdeDisbursementAbi,
functionName: "disburse",
args: [address, amount, modality],
}),
})),
"disbursed",
);
}

export async function fetchDisbursementLogs(
publicClient: PublicClient,
tdeDisbursementAddress: Address,
tdeDisbursementDeploymentBlock: bigint,
) {
return (
await paginatedGetEvents(
(r) =>
publicClient.getContractEvents({
address: tdeDisbursementAddress,
abi: tdeDisbursementAbi,
eventName: "Disbursed",
...r,
}),
tdeDisbursementDeploymentBlock,
await publicClient.getBlockNumber(),
)
).map((l) => requiredArgs(l));
}
80 changes: 0 additions & 80 deletions script/initial-distribution/src/vesting.ts

This file was deleted.