diff --git a/script/initial-distribution/src/abis.ts b/script/initial-distribution/src/abis.ts index c284be2..c8e96d6 100644 --- a/script/initial-distribution/src/abis.ts +++ b/script/initial-distribution/src/abis.ts @@ -202,14 +202,11 @@ export const ccaAbi = [ export const tdeDisbursementAbi = [ { - type: "event", - name: "Disbursed", - inputs: [ - { name: "beneficiary", type: "address", indexed: true }, - { name: "transferTarget", type: "address", indexed: false }, - { name: "amount", type: "uint256", indexed: false }, - { name: "modality", type: "uint8", indexed: false }, - ], + type: "function", + name: "DISBURSER", + inputs: [], + outputs: [{ name: "", type: "address" }], + stateMutability: "view", }, { type: "function", @@ -218,6 +215,17 @@ export const tdeDisbursementAbi = [ outputs: [{ name: "", type: "address" }], stateMutability: "view", }, + { + type: "function", + name: "VESTING_PARAMS_FOR_MODALITY", + inputs: [{ name: "modality", type: "uint8" }], + outputs: [ + { name: "startTimestamp", type: "uint64" }, + { name: "durationSeconds", type: "uint64" }, + { name: "cliffSeconds", type: "uint64" }, + ], + stateMutability: "pure", + }, { type: "function", name: "disburse", @@ -231,27 +239,53 @@ export const tdeDisbursementAbi = [ }, { type: "function", - name: "vestingContracts", + name: "ensureVestingContractExists", inputs: [ { name: "beneficiary", type: "address" }, { name: "modality", type: "uint8" }, ], - outputs: [{ name: "vestingWallet", type: "address" }], - stateMutability: "view", + outputs: [ + { name: "vestingContract", type: "address" }, + { name: "created", type: "bool" }, + ], + stateMutability: "nonpayable", }, { type: "function", - name: "ensureVestingContractExists", + name: "vestingContracts", inputs: [ { name: "beneficiary", type: "address" }, { name: "modality", type: "uint8" }, ], - outputs: [ - { name: "vestingContract", type: "address" }, - { name: "created", type: "bool" }, + outputs: [{ name: "vestingWallet", type: "address" }], + stateMutability: "view", + }, + { + type: "event", + name: "Disbursed", + inputs: [ + { name: "beneficiary", type: "address", indexed: true }, + { name: "transferTarget", type: "address", indexed: false }, + { name: "amount", type: "uint256", indexed: false }, + { name: "modality", type: "uint8", indexed: false }, ], - stateMutability: "nonpayable", + anonymous: false, + }, + { type: "error", name: "DirectIsNotVested", inputs: [] }, + { type: "error", name: "OnlyCallableByDisburser", inputs: [] }, + { + type: "error", + name: "SafeERC20FailedOperation", + inputs: [{ name: "token", type: "address", internalType: "address" }], + }, + { + type: "error", + name: "UnknownModality", + inputs: [{ name: "modality", type: "uint8", internalType: "enum Modality" }], }, + { type: "error", name: "ZeroAddressBeneficiary", inputs: [] }, + { type: "error", name: "ZeroAddressDisburser", inputs: [] }, + { type: "error", name: "ZeroAddressToken", inputs: [] }, ] as const; export const batchCallerAbi = [ diff --git a/script/initial-distribution/src/cca.ts b/script/initial-distribution/src/cca.ts index ea8dfe2..61570a4 100644 --- a/script/initial-distribution/src/cca.ts +++ b/script/initial-distribution/src/cca.ts @@ -3,7 +3,7 @@ import "dotenv/config"; import { type Address, formatEther, getAddress, getContract, type Hex } from "viem"; import { ccaAbi, erc20Abi, tdeDisbursementAbi, trackerAbi } from "./abis.js"; import { buildExpectedEntries, type DisbursementEntry } from "./ccaEntries.js"; -import { chainSetup } from "./chains.js"; +import { chainSetup, makeWallet } from "./chains.js"; import { assertCondition, blockToTimestamp, @@ -27,14 +27,14 @@ const CCA_ADDRESS = getAddress(requireEnv("CCA_ADDRESS")); const SOLD_TOKEN_ADDRESS = getAddress(requireEnv("SOLD_TOKEN_ADDRESS")); const TDE_DISBURSEMENT_ADDRESS = getAddress(requireEnv("TDE_DISBURSEMENT_ADDRESS")); const NORMAL_PHASE_START = iso8601ToTimestamp(requireEnv("NORMAL_PHASE_START")); -const DISBURSER_PRIVATE_KEY = ensureHex(requireEnv("DISBURSER_PRIVATE_KEY")); +const TDE_DISBURSER_PRIVATE_KEY = ensureHex(requireEnv("TDE_DISBURSER_PRIVATE_KEY")); +const TRACKER_DISBURSER_PRIVATE_KEY = ensureHex(requireEnv("TRACKER_DISBURSER_PRIVATE_KEY")); const RPC_URL = requireEnv("RPC_URL"); -const { chain, account, publicClient, walletClient } = await chainSetup( - CHAIN_ID, - RPC_URL, - DISBURSER_PRIVATE_KEY, -); +const { chain, transport, publicClient } = await chainSetup(CHAIN_ID, RPC_URL); + +const tdeDisburser = makeWallet(chain, transport, TDE_DISBURSER_PRIVATE_KEY); +const trackerDisburser = makeWallet(chain, transport, TRACKER_DISBURSER_PRIVATE_KEY); const ccaContract = getContract({ address: CCA_ADDRESS, @@ -45,19 +45,19 @@ const ccaContract = getContract({ const soldTokenContract = getContract({ address: SOLD_TOKEN_ADDRESS, abi: erc20Abi, - client: walletClient, + client: tdeDisburser.walletClient, }); const tdeDisbursementContract = getContract({ address: TDE_DISBURSEMENT_ADDRESS, abi: tdeDisbursementAbi, - client: walletClient, + client: tdeDisburser.walletClient, }); const trackerContract = getContract({ address: TRACKER_TOKEN_ADDRESS, abi: trackerAbi, - client: walletClient, + client: trackerDisburser.walletClient, }); for (const contract of [ccaContract, trackerContract, soldTokenContract, tdeDisbursementContract]) { @@ -68,12 +68,19 @@ for (const contract of [ccaContract, trackerContract, soldTokenContract, tdeDisb } console.log(`✅ All contracts addresses have deployed code.`); -const onChainDisburser = getAddress(await trackerContract.read.disburser()); +const onChainTrackerDisburser = getAddress(await trackerContract.read.disburser()); +assertCondition( + onChainTrackerDisburser === trackerDisburser.account.address, + `${trackerDisburser.account.address} is not the CCADisbursementTracker disburser (expected ${onChainTrackerDisburser}).`, +); +console.log(`✅ CCADisbursementTracker disburser address matches: ${onChainTrackerDisburser}`); + +const onChainTDEDisburser = getAddress(await tdeDisbursementContract.read.DISBURSER()); assertCondition( - onChainDisburser === getAddress(account.address), - `${account.address} is not the disburser (expected ${onChainDisburser}).`, + onChainTDEDisburser === tdeDisburser.account.address, + `${tdeDisburser.account.address} is not the TDEDisbursement disburser (expected ${onChainTDEDisburser}).`, ); -console.log(`✅ Disburser address matches expected: ${onChainDisburser}`); +console.log(`✅ TDEDisbursement disburser address matches: ${onChainTDEDisburser}`); // ── 1. Fetch CCA data, resolve phase boundary, compute filled bids ────────── @@ -161,7 +168,7 @@ const filledBids = tokensClaims.map((tc) => { async function ensureTdeAllowance(totalNeeded: bigint): Promise { const currentAllowance = await soldTokenContract.read.allowance([ - account.address, + tdeDisburser.account.address, TDE_DISBURSEMENT_ADDRESS, ]); if (currentAllowance >= totalNeeded) return; @@ -217,7 +224,7 @@ async function findUnrecordedTransfer( } const logs = await soldTokenContract.getEvents.Transfer( - { from: account.address, to: entry.to }, + { from: tdeDisburser.account.address, to: entry.to }, { fromBlock, toBlock }, ); const match = logs.find((l) => l.args.value === entry.transferAmount); @@ -291,10 +298,10 @@ if (remainingEntries.length > 0) { } const remainingTokenTotal = sumOf(remainingEntries.map((e) => e.transferAmount)); - const disburserBalance = await soldTokenContract.read.balanceOf([account.address]); + const disburserBalance = await soldTokenContract.read.balanceOf([tdeDisburser.account.address]); assertCondition( disburserBalance >= remainingTokenTotal, - `${account.address} has insufficient token balance of ${soldTokenContract.address}. Has ${formatEther(disburserBalance)}, needs ${formatEther(remainingTokenTotal)}.`, + `${tdeDisburser.account.address} has insufficient token balance of ${soldTokenContract.address}. Has ${formatEther(disburserBalance)}, needs ${formatEther(remainingTokenTotal)}.`, ); console.log(`✅ Disburser has sufficient token balance.`); @@ -320,7 +327,9 @@ assertCondition( ); console.log(`✅ Sale is fully disbursed.`); -const finalDisburserBalance = await soldTokenContract.read.balanceOf([account.address]); +const finalDisburserBalance = await soldTokenContract.read.balanceOf([ + tdeDisburser.account.address, +]); const sweepTarget = await ccaContract.read.tokensRecipient(); if (finalDisburserBalance > 0n) { console.log( @@ -332,7 +341,7 @@ if (finalDisburserBalance > 0n) { ); assertCondition( - (await soldTokenContract.read.balanceOf([account.address])) === 0n, + (await soldTokenContract.read.balanceOf([tdeDisburser.account.address])) === 0n, `Sweep failed: disburser balance is not 0 after sweep. This should never happen.`, ); console.log(`✅ No remaining tokens on disburser.`); diff --git a/script/initial-distribution/src/chains.ts b/script/initial-distribution/src/chains.ts index 89fd649..7009e9a 100644 --- a/script/initial-distribution/src/chains.ts +++ b/script/initial-distribution/src/chains.ts @@ -6,7 +6,7 @@ import { http, type PublicClient, } from "viem"; -import { type PrivateKeyToAccountOptions, privateKeyToAccount } from "viem/accounts"; +import { privateKeyToAccount } from "viem/accounts"; import { arbitrum, arbitrumSepolia, sepolia } from "viem/chains"; import { assertCondition } from "./lib.js"; @@ -33,19 +33,16 @@ export async function validateRpcChainId(publicClient: PublicClient, chain: Chai ); } -export async function chainSetup( - chainId: string, - rpcUrl: string, - accountPrivateKey: Hex, - accountOptions?: PrivateKeyToAccountOptions, -) { +export async function chainSetup(chainId: string, rpcUrl: string) { const chain = resolveChain(chainId); - const account = privateKeyToAccount(accountPrivateKey, accountOptions); const transport = http(rpcUrl); const publicClient = createPublicClient({ chain, transport }); - const walletClient = createWalletClient({ account, chain, transport }); - await validateRpcChainId(publicClient, chain); + return { chain, transport, publicClient }; +} - return { chain, account, publicClient, walletClient }; +export function makeWallet(chain: Chain, transport: ReturnType, privateKey: Hex) { + const account = privateKeyToAccount(privateKey); + const walletClient = createWalletClient({ account, chain, transport }); + return { account, walletClient }; } diff --git a/script/initial-distribution/src/claimAllBids.ts b/script/initial-distribution/src/claimAllBids.ts index c23b782..f28314f 100644 --- a/script/initial-distribution/src/claimAllBids.ts +++ b/script/initial-distribution/src/claimAllBids.ts @@ -2,8 +2,8 @@ * Claim all unclaimed CCA bids: exit any unexited bids, then claim tokens (batch per owner). * * Uses the same .env as the main initial-distribution script: CHAIN_ID, RPC_URL, CCA_ADDRESS, - * and DISBURSER_PRIVATE_KEY for sending transactions. Anyone can call - * exit/claim; tokens are sent to the bid owner. + * and TRACKER_DISBURSER_PRIVATE_KEY for sending transactions. + * Anyone can call exit/claim; tokens are sent to the bid owner. * * Usage: pnpm exec tsx src/claimAllBids.ts */ @@ -11,7 +11,7 @@ import "dotenv/config"; import { type Address, getAddress, getContract } from "viem"; import { ccaAbi } from "./abis.js"; -import { chainSetup } from "./chains.js"; +import { chainSetup, makeWallet } from "./chains.js"; import { assertCondition, contractHasCode, @@ -24,19 +24,16 @@ import { const CCA_ADDRESS = getAddress(requireEnv("CCA_ADDRESS")); const CHAIN_ID = requireEnv("CHAIN_ID"); -const DISBURSER_PRIVATE_KEY = ensureHex(requireEnv("DISBURSER_PRIVATE_KEY")); +const TRACKER_DISBURSER_PRIVATE_KEY = ensureHex(requireEnv("TRACKER_DISBURSER_PRIVATE_KEY")); const RPC_URL = requireEnv("RPC_URL"); -const { chain, publicClient, walletClient } = await chainSetup( - CHAIN_ID, - RPC_URL, - DISBURSER_PRIVATE_KEY, -); +const { chain, transport, publicClient } = await chainSetup(CHAIN_ID, RPC_URL); +const trackerDisburser = makeWallet(chain, transport, TRACKER_DISBURSER_PRIVATE_KEY); const ccaContract = getContract({ address: CCA_ADDRESS, abi: ccaAbi, - client: { public: publicClient, wallet: walletClient }, + client: trackerDisburser.walletClient, }); type BidState = Awaited>; diff --git a/script/initial-distribution/src/tde.ts b/script/initial-distribution/src/tde.ts index 4bfe8e9..9d32b57 100644 --- a/script/initial-distribution/src/tde.ts +++ b/script/initial-distribution/src/tde.ts @@ -12,14 +12,14 @@ import { } from "./tdeSetup.js"; const { - tdeTimestamp, + batchConfig, nowTimestamp, - account, publicClient, - batchConfig, - tokenContract, tdeDisbursementAddress, tdeDisbursementDeploymentBlock, + tdeDisburser, + tdeTimestamp, + tokenContract, } = await setupTdeEnvironment(); const filterCurrentlyDisbursableRows = @@ -48,7 +48,7 @@ if (disbursableRows.length === 0) { await ensureAllowance( tokenContract, - account.address, + tdeDisburser.account.address, publicClient, tdeDisbursementAddress, sumOf(disbursableRows.map((r) => r.amount)), diff --git a/script/initial-distribution/src/tdeSetup.ts b/script/initial-distribution/src/tdeSetup.ts index d6556ec..38fd9c0 100644 --- a/script/initial-distribution/src/tdeSetup.ts +++ b/script/initial-distribution/src/tdeSetup.ts @@ -1,8 +1,7 @@ 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 { chainSetup, makeWallet } from "./chains.js"; import type { DisbursementRow } from "./csv.js"; import { ensureHex, @@ -18,39 +17,35 @@ export async function setupTdeEnvironment() { 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 TDE_DISBURSER_PRIVATE_KEY = ensureHex(requireEnv("TDE_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 { chain, publicClient, transport } = await chainSetup(CHAIN_ID, RPC_URL); + const tdeDisburser = makeWallet(chain, transport, TDE_DISBURSER_PRIVATE_KEY); const batchConfig: BatchCallerConfig = { publicClient, - walletClient, + walletClient: tdeDisburser.walletClient, batchCallerAddress: BATCH_CALLER_ADDRESS, }; const tdeDisbursementContract = getContract({ address: tdeDisbursementAddress, abi: tdeDisbursementAbi, - client: walletClient, + client: tdeDisburser.walletClient, }); const tokenContract = getContract({ address: await tdeDisbursementContract.read.IDOS_TOKEN(), abi: erc20Abi, - client: walletClient, + client: tdeDisburser.walletClient, }); return { - account, + tdeDisburser, publicClient, batchConfig, tokenContract,