Skip to content

Commit 023bd8a

Browse files
authored
Merge pull request #682 from privacy-ethereum/feature/distribution-task
feat(contracts): add distribution hardhat task
2 parents 0c152e3 + 3db6329 commit 023bd8a

File tree

7 files changed

+647
-106
lines changed

7 files changed

+647
-106
lines changed

packages/contracts/hardhat.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
getEtherscanApiKeys,
2222
getNetworkRpcUrls,
2323
} from "./tasks/helpers/constants";
24+
import "./tasks/runner/distribute";
2425
import "./tasks/runner/initPoll";
2526
import "./tasks/runner/merge";
2627
import "./tasks/runner/prove";

packages/contracts/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@
5454
"submitOnChain": "hardhat submitOnChain",
5555
"submitOnChain:localhost": "pnpm run submitOnChain",
5656
"submitOnChain:optimism-sepolia": "pnpm run submitOnChain --network optimism_sepolia",
57-
"upload-round-metadata": "ts-node ./scripts/uploadRoundMetadata.ts"
57+
"upload-round-metadata": "ts-node ./scripts/uploadRoundMetadata.ts",
58+
"distribute": "hardhat distribute",
59+
"distribute:localhost": "pnpm run distribute",
60+
"distribute:optimism-sepolia": "pnpm run distribute --network optimism_sepolia"
5861
},
5962
"dependencies": {
6063
"@nomicfoundation/hardhat-ethers": "^3.0.8",
@@ -71,17 +74,20 @@
7174
"maci-core": "^2.5.0",
7275
"maci-crypto": "^2.5.0",
7376
"maci-domainobjs": "^2.5.0",
74-
"solidity-docgen": "^0.6.0-beta.36"
77+
"solidity-docgen": "^0.6.0-beta.36",
78+
"zod": "3.24.1"
7579
},
7680
"devDependencies": {
7781
"@types/chai": "^4.3.11",
82+
"@types/chai-as-promised": "^8.0.2",
7883
"@types/circomlibjs": "^0.1.6",
7984
"@types/lowdb": "^1.0.15",
8085
"@types/mocha": "^10.0.10",
8186
"@types/node": "^22.2.0",
8287
"@types/snarkjs": "^0.7.8",
8388
"@types/uuid": "^10.0.0",
8489
"chai": "^4.3.10",
90+
"chai-as-promised": "^8.0.2",
8591
"hardhat-artifactor": "^0.2.0",
8692
"hardhat-contract-sizer": "^2.10.0",
8793
"sol2uml": "^2.5.20",

packages/contracts/tasks/helpers/constants/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { EDeploySteps as EMaciDeploySteps, EContracts as EMaciContracts } from "
22

33
import type { BigNumberish } from "ethers";
44

5+
export type * from "./types";
6+
57
/**
68
* Deploy steps for maci-platform related constacts
79
*/
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Interface that represents distribute hardhat task params
3+
*/
4+
export interface IDistributeArgs {
5+
/**
6+
* Project list filepath
7+
*/
8+
projectsFile: string;
9+
10+
/**
11+
* Amounts list filepath
12+
*/
13+
amountsFile: string;
14+
15+
/**
16+
* ERC-20 token address
17+
*/
18+
tokenAddress: string;
19+
20+
/**
21+
* The index of the recipient to start sending transactions from
22+
*/
23+
startIndex: number;
24+
25+
/**
26+
* Whether run command on fork or not
27+
*/
28+
dryRun: boolean;
29+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/* eslint-disable no-console */
2+
import { isAddress, Signer, ZeroAddress } from "ethers";
3+
import { task, types } from "hardhat/config";
4+
import * as z from "zod";
5+
6+
import fs from "fs";
7+
8+
import type { IDistributeArgs } from "../helpers/constants";
9+
10+
const addressSchema = z.string().refine((value) => isAddress(value) && value !== ZeroAddress, {
11+
message: "Invalid Address",
12+
});
13+
14+
const projectsSchema = z.array(
15+
z.object({
16+
recipientIndex: z.coerce.number(),
17+
payoutAddress: addressSchema,
18+
}),
19+
);
20+
21+
const amountsSchema = z.array(z.coerce.bigint());
22+
23+
// erc20,0x82aF49447D8a07e3bd95BD0d56f35241523fBab1,receiver,amount
24+
25+
/**
26+
* Distribute unallocated amounts among the specified projects
27+
*/
28+
task("distribute", "Command to distribute unallocated amounts among the specified projects")
29+
.addParam("projectsFile", "The file with projects", undefined, types.string)
30+
.addParam("amountsFile", "The file with amounts in wei to transfer", undefined, types.string)
31+
.addOptionalParam("tokenAddress", "ERC-20 token address", "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", types.string)
32+
.addOptionalParam("startIndex", "The index of the recipient to start sending transactions from", 0, types.int)
33+
.addFlag("dryRun", "Run distribute command on fork and impersonate WETH account")
34+
.setAction(async ({ projectsFile, amountsFile, tokenAddress, startIndex, dryRun }: IDistributeArgs, hre) => {
35+
const address = addressSchema.parse(tokenAddress);
36+
37+
const projects = await fs.promises
38+
.readFile(projectsFile, "utf8")
39+
.then((result) => projectsSchema.parse(JSON.parse(result)));
40+
41+
const amounts = await fs.promises
42+
.readFile(amountsFile, "utf8")
43+
.then((result) => amountsSchema.parse(JSON.parse(result)));
44+
45+
if (projects.length !== amounts.length) {
46+
throw new Error("Projects and amounts arrays must be the same length");
47+
}
48+
49+
const data = amounts.map((amount, index) => ({ ...projects[index], amount }));
50+
51+
let signer: Signer;
52+
53+
if (dryRun) {
54+
await hre.network.provider.request({
55+
method: "hardhat_impersonateAccount",
56+
params: [tokenAddress],
57+
});
58+
59+
signer = await hre.ethers.getSigner(tokenAddress);
60+
} else {
61+
[signer] = await hre.ethers.getSigners();
62+
}
63+
64+
console.log(`Using signer: ${await signer.getAddress()}`);
65+
66+
const contract = await hre.ethers.getContractAt("IERC20", address, signer);
67+
const startSignerBalance = await contract.balanceOf(signer);
68+
const startBalance = await signer.provider!.getBalance(signer);
69+
const totalAmount = data.reduce((acc, x) => acc + x.amount, 0n);
70+
71+
console.log(`Total allocated: ${totalAmount.toString()} wei`);
72+
console.log(`Signer balance: ${startBalance.toString()} wei`);
73+
console.log(`Signer token balance: ${startSignerBalance.toString()} wei`);
74+
75+
if (startSignerBalance <= totalAmount) {
76+
throw new Error("Not enough balance");
77+
}
78+
79+
// eslint-disable-next-line @typescript-eslint/prefer-for-of
80+
for (let index = startIndex; index < data.length; index += 1) {
81+
const { payoutAddress, amount } = data[index];
82+
83+
console.log(`Recipeint ${index}: transferring ${amount} wei to ${payoutAddress}`);
84+
85+
// eslint-disable-next-line no-await-in-loop
86+
const receipt = await contract.transfer(payoutAddress, amount).then((tx) => tx.wait());
87+
88+
console.log(`Sent ${amount} wei to ${payoutAddress} (recipient index ${index}) (tx: ${receipt?.hash})\n`);
89+
}
90+
91+
const endSignerBalance = await contract.balanceOf(signer);
92+
const endBalance = await signer.provider!.getBalance(signer);
93+
94+
const totalTransferred = startSignerBalance - endSignerBalance;
95+
96+
if (totalAmount !== totalTransferred) {
97+
console.warn(
98+
`Transferred amount (${totalTransferred} wei) doesn't match with total allocated amount (${totalAmount} wei)`,
99+
);
100+
}
101+
102+
console.log(`Total transferred: ${totalTransferred.toString()} wei`);
103+
console.log(`Signer balance: ${endBalance.toString()} wei`);
104+
console.log(`Signer token balance: ${endSignerBalance.toString()} wei`);
105+
console.log(`Total transaction cost: ${(startBalance - endBalance).toString()} wei`);
106+
});

0 commit comments

Comments
 (0)