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
66 changes: 50 additions & 16 deletions script/initial-distribution/src/abis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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 = [
Expand Down
49 changes: 29 additions & 20 deletions script/initial-distribution/src/cca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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]) {
Expand All @@ -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 ──────────

Expand Down Expand Up @@ -161,7 +168,7 @@ const filledBids = tokensClaims.map((tc) => {

async function ensureTdeAllowance(totalNeeded: bigint): Promise<void> {
const currentAllowance = await soldTokenContract.read.allowance([
account.address,
tdeDisburser.account.address,
TDE_DISBURSEMENT_ADDRESS,
]);
if (currentAllowance >= totalNeeded) return;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.`);

Expand All @@ -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(
Expand All @@ -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.`);
Expand Down
19 changes: 8 additions & 11 deletions script/initial-distribution/src/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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<typeof http>, privateKey: Hex) {
const account = privateKeyToAccount(privateKey);
const walletClient = createWalletClient({ account, chain, transport });
return { account, walletClient };
}
17 changes: 7 additions & 10 deletions script/initial-distribution/src/claimAllBids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
* 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
*/

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,
Expand All @@ -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<ReturnType<typeof ccaContract.read.bids>>;
Expand Down
10 changes: 5 additions & 5 deletions script/initial-distribution/src/tde.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -48,7 +48,7 @@ if (disbursableRows.length === 0) {

await ensureAllowance(
tokenContract,
account.address,
tdeDisburser.account.address,
publicClient,
tdeDisbursementAddress,
sumOf(disbursableRows.map((r) => r.amount)),
Expand Down
21 changes: 8 additions & 13 deletions script/initial-distribution/src/tdeSetup.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down