From e207a612a83592ee300935adb3afa71fd69786ac Mon Sep 17 00:00:00 2001 From: 0xmad <0xmad@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:28:11 -0500 Subject: [PATCH] feat(contracts): payout strategy - [x] Use tally as payout strategy - [x] Add deposit/claim/withdraw functions - [x] Add tests --- .../contracts/interfaces/IPayoutStrategy.sol | 63 +++ .../contracts/contracts/interfaces/IPoll.sol | 7 +- packages/contracts/contracts/maci/Poll.sol | 10 +- packages/contracts/contracts/maci/Tally.sol | 263 ++++++++++++ .../contracts/contracts/maci/TallyFactory.sol | 24 ++ .../contracts/contracts/mocks/MockERC20.sol | 12 + packages/contracts/package.json | 1 + .../tasks/deploy/maci/07-tallyFactory.ts | 53 +++ .../contracts/tasks/deploy/poll/01-poll.ts | 11 +- packages/contracts/tests/Maci.test.ts | 12 +- packages/contracts/tests/Poll.test.ts | 40 +- packages/contracts/tests/Tally.test.ts | 386 ++++++++++++++++++ packages/contracts/tests/constants.ts | 45 +- packages/contracts/tests/utils.ts | 69 +++- pnpm-lock.yaml | 28 +- 15 files changed, 951 insertions(+), 73 deletions(-) create mode 100644 packages/contracts/contracts/interfaces/IPayoutStrategy.sol create mode 100644 packages/contracts/contracts/maci/Tally.sol create mode 100644 packages/contracts/contracts/maci/TallyFactory.sol create mode 100644 packages/contracts/contracts/mocks/MockERC20.sol create mode 100644 packages/contracts/tasks/deploy/maci/07-tallyFactory.ts create mode 100644 packages/contracts/tests/Tally.test.ts diff --git a/packages/contracts/contracts/interfaces/IPayoutStrategy.sol b/packages/contracts/contracts/interfaces/IPayoutStrategy.sol new file mode 100644 index 00000000..90697038 --- /dev/null +++ b/packages/contracts/contracts/interfaces/IPayoutStrategy.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IPoll } from "./IPoll.sol"; + +/// @title IPayoutStrategy +/// @notice Interface responsible for payout strategy +interface IPayoutStrategy { + /// @notice Strategy initialization params + struct StrategyInit { + /// @notice The cooldown duration for withdrawal extra funds + uint256 cooldownTime; + /// @notice The max contribution amount + uint256 maxContribution; + /// @notice The payout token + address payoutToken; + /// @notice The poll address + address poll; + } + + /// @notice Claim params + struct Claim { + /// @notice The index of the vote option to verify the correctness of the tally + uint256 index; + /// @notice The voice credit options received for recipient + uint256 voiceCreditsPerOption; + /// @notice Flattened array of the tally + uint256 tallyResult; + /// @notice The sum of tally results squares + uint256 totalVotesSquares; + /// @notice The total amount of voice credits spent + uint256 totalSpent; + /// @notice Corresponding proof of the tally result + uint256[][] tallyResultProof; + /// @notice The respective salt in the results object in the tally.json + uint256 tallyResultSalt; + /// @notice Depth of the vote option tree + uint8 voteOptionTreeDepth; + /// @notice hashLeftRight(number of spent voice credits, spent salt) + uint256 spentVoiceCreditsHash; + /// @notice hashLeftRight(merkle root of the no spent voice + uint256 perVOSpentVoiceCreditsHash; + } + + /// @notice Total deposited amount + function totalAmount() external view returns (uint256); + + /// @notice The cooldown timeout + function cooldown() external view returns (uint256); + + /// @notice Deposit amount + /// @param amount The amount + function deposit(uint256 amount) external; + + /// @notice Withdraw extra amount + /// @param receivers The receivers addresses + /// @param amounts The amounts + function withdrawExtra(address[] calldata receivers, uint256[] calldata amounts) external; + + /// @notice Claim funds for recipient + /// @param params The claim params + function claim(Claim calldata params) external; +} diff --git a/packages/contracts/contracts/interfaces/IPoll.sol b/packages/contracts/contracts/interfaces/IPoll.sol index 04c2696c..fcd3c0d4 100644 --- a/packages/contracts/contracts/interfaces/IPoll.sol +++ b/packages/contracts/contracts/interfaces/IPoll.sol @@ -4,8 +4,9 @@ pragma solidity ^0.8.20; import { IPoll as IPollBase } from "maci-contracts/contracts/interfaces/IPoll.sol"; import { IOwnable } from "./IOwnable.sol"; +import { IRecipientRegistry } from "./IRecipientRegistry.sol"; -/// @title IPollBase +/// @title IPoll /// @notice Poll interface interface IPoll is IPollBase, IOwnable { /// @notice The initialization function. @@ -14,4 +15,8 @@ interface IPoll is IPollBase, IOwnable { /// @notice Set the poll registry. /// @param registryAddress The registry address function setRegistry(address registryAddress) external; + + /// @notice Get the poll registry. + /// @return registry The poll registry + function getRegistry() external returns (IRecipientRegistry); } diff --git a/packages/contracts/contracts/maci/Poll.sol b/packages/contracts/contracts/maci/Poll.sol index 9389234d..a889a5fc 100644 --- a/packages/contracts/contracts/maci/Poll.sol +++ b/packages/contracts/contracts/maci/Poll.sol @@ -5,6 +5,7 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Poll as BasePoll } from "maci-contracts/contracts/Poll.sol"; import { ICommon } from "../interfaces/ICommon.sol"; +import { IRecipientRegistry } from "../interfaces/IRecipientRegistry.sol"; /// @title Poll /// @notice A Poll contract allows voters to submit encrypted messages @@ -90,8 +91,7 @@ contract Poll is Ownable, BasePoll, ICommon { _; } - /// @notice A modifier that causes the function to revert if the voting period is - /// over + /// @notice A modifier that causes the function to revert if the voting period is over modifier isWithinVotingDeadline() override { uint256 secondsPassed = block.timestamp - initTime; @@ -110,6 +110,12 @@ contract Poll is Ownable, BasePoll, ICommon { emit SetRegistry(registryAddress); } + /// @notice Get the poll registry. + /// @return registry The poll registry + function getRegistry() public view returns (IRecipientRegistry) { + return IRecipientRegistry(registry); + } + /// @notice The initialization function. function init() public override onlyOwner isRegistryInitialized { initTime = block.timestamp; diff --git a/packages/contracts/contracts/maci/Tally.sol b/packages/contracts/contracts/maci/Tally.sol new file mode 100644 index 00000000..25ea9485 --- /dev/null +++ b/packages/contracts/contracts/maci/Tally.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Tally as TallyBase } from "maci-contracts/contracts/Tally.sol"; + +import { IPoll } from "../interfaces/IPoll.sol"; +import { IPayoutStrategy } from "../interfaces/IPayoutStrategy.sol"; +import { IRecipientRegistry } from "../interfaces/IRecipientRegistry.sol"; + +/// @title Tally - poll tally and payout strategy +/// @notice The Tally contract is used during votes tallying and by users to verify the tally results. +/// @notice Allows users to deposit and claim rewards for recipients +contract Tally is TallyBase, IPayoutStrategy, Pausable { + using SafeERC20 for IERC20; + + /// @notice The max voice credits (MACI allows 2 ** 32 voice credits max) + uint256 private constant MAX_VOICE_CREDITS = 10 ** 9; + + /// @notice The alpha precision (needed for allocated amount calculation) + uint256 private constant ALPHA_PRECISION = 10 ** 18; + + /// @notice The payout token + IERC20 public token; + + /// @notice The poll registry + IRecipientRegistry public registry; + + /// @notice The total amount of funds deposited + uint256 public totalAmount; + + /// @notice The max contribution amount + uint256 public maxContributionAmount; + + /// @notice The voice credit factor (needed for allocated amount calculation) + uint256 public voiceCreditFactor; + + /// @notice The cooldown duration for withdrawal extra funds + uint256 public cooldown; + + /// @notice Initialized or not + bool internal initialized; + + /// @notice custom errors + error CooldownPeriodNotOver(); + error VotingPeriodNotOver(); + error VotingPeriodOver(); + error InvalidBudget(); + error NoProjectHasMoreThanOneVote(); + error InvalidWithdrawal(); + error AlreadyInitialized(); + error NotInitialized(); + error InvalidPoll(); + + /// @notice Create a new Tally contract + /// @param verifierContract The Verifier contract + /// @param vkRegistryContract The VkRegistry contract + /// @param pollContract The Poll contract + /// @param mpContract The MessageProcessor contract + /// @param tallyOwner The owner of the Tally contract + /// @param pollMode The mode of the poll + constructor( + address verifierContract, + address vkRegistryContract, + address pollContract, + address mpContract, + address tallyOwner, + Mode pollMode + ) payable TallyBase(verifierContract, vkRegistryContract, pollContract, mpContract, tallyOwner, pollMode) { + } + + /// @notice A modifier that causes the function to revert if the cooldown period is not over + modifier afterCooldown() { + (uint256 deployTime, uint256 duration) = poll.getDeployTimeAndDuration(); + uint256 secondsPassed = block.timestamp - deployTime; + + if (secondsPassed <= duration + cooldown) { + revert CooldownPeriodNotOver(); + } + + _; + } + + /// @notice A modifier that causes the function to revert if the voting period is over + modifier beforeVotingDeadline() { + (uint256 deployTime, uint256 duration) = poll.getDeployTimeAndDuration(); + uint256 secondsPassed = block.timestamp - deployTime; + + if (secondsPassed > duration) { + revert VotingPeriodOver(); + } + + _; + } + + /// @notice A modifier that causes the function to revert if the voting period is not over + modifier afterVotingDeadline() { + (uint256 deployTime, uint256 duration) = poll.getDeployTimeAndDuration(); + uint256 secondsPassed = block.timestamp - deployTime; + + if (secondsPassed <= duration) { + revert VotingPeriodNotOver(); + } + + _; + } + + /// @notice A modifier that causes the function to revert if the strategy is not initialized + modifier isInitialized() { + if (!initialized) { + revert NotInitialized(); + } + + _; + } + + /// @notice Initialize tally with strategy params + /// @param params The strategy initialization params + function init(IPayoutStrategy.StrategyInit calldata params) public onlyOwner { + if (initialized) { + revert AlreadyInitialized(); + } + + if (params.poll != address(poll)) { + revert InvalidPoll(); + } + + initialized = true; + cooldown = params.cooldownTime; + registry = IPoll(params.poll).getRegistry(); + token = IERC20(params.payoutToken); + maxContributionAmount = params.maxContribution; + voiceCreditFactor = params.maxContribution / MAX_VOICE_CREDITS; + } + + /// @notice Pause contract calls (deposit, claim, withdraw) + function pause() public onlyOwner { + _pause(); + } + + /// @notice Unpause contract calls (deposit, claim, withdraw) + function unpause() public onlyOwner { + _unpause(); + } + + /// @inheritdoc IPayoutStrategy + function deposit(uint256 amount) public isInitialized whenNotPaused beforeVotingDeadline { + totalAmount += amount; + + token.safeTransferFrom(msg.sender, address(this), amount); + } + + /// @inheritdoc IPayoutStrategy + function withdrawExtra( + address[] calldata receivers, + uint256[] calldata amounts + ) public override isInitialized onlyOwner whenNotPaused afterCooldown { + uint256 amountLength = amounts.length; + uint256 totalFunds = totalAmount; + uint256 sum = 0; + + for (uint256 index = 0; index < amountLength; ) { + uint256 amount = amounts[index]; + sum += amount; + + if (sum > totalFunds) { + revert InvalidWithdrawal(); + } + + totalAmount -= amount; + + unchecked { + index++; + } + } + + for (uint256 index = 0; index < amountLength; ) { + uint256 amount = amounts[index]; + address receiver = receivers[index]; + + if (amount > 0) { + token.safeTransfer(receiver, amount); + } + + unchecked { + index++; + } + } + } + + /// @inheritdoc IPayoutStrategy + function claim( + IPayoutStrategy.Claim calldata params + ) public override isInitialized whenNotPaused afterVotingDeadline { + uint256 amount = getAllocatedAmount( + params.voiceCreditsPerOption, + params.totalVotesSquares, + params.totalSpent, + params.tallyResult + ); + totalAmount -= amount; + + bool isValid = super.verifyTallyResult( + params.index, + params.tallyResult, + params.tallyResultProof, + params.tallyResultSalt, + params.voteOptionTreeDepth, + params.spentVoiceCreditsHash, + params.perVOSpentVoiceCreditsHash + ); + + if (!isValid) { + revert InvalidTallyVotesProof(); + } + + IRecipientRegistry.Recipient memory recipient = registry.getRecipient(params.index); + + token.safeTransfer(recipient.recipient, amount); + } + + /// @notice Get allocated token amount (without verification). + /// @param voiceCreditsPerOption The voice credit options received for recipient + /// @param totalVotesSquares The sum of tally results squares + /// @param totalSpent The total amount of voice credits spent + /// @param tallyResult The tally result for recipient + function getAllocatedAmount( + uint256 voiceCreditsPerOption, + uint256 totalVotesSquares, + uint256 totalSpent, + uint256 tallyResult + ) internal view returns (uint256) { + uint256 alpha = calculateAlpha(totalVotesSquares, totalSpent); + uint256 quadratic = alpha * voiceCreditFactor * tallyResult * tallyResult; + uint256 totalSpentCredits = voiceCreditFactor * voiceCreditsPerOption; + uint256 linearPrecision = ALPHA_PRECISION * totalSpentCredits; + uint256 linearAlpha = alpha * totalSpentCredits; + + return ((quadratic + linearPrecision) - linearAlpha) / ALPHA_PRECISION; + } + + /// @notice Calculate the alpha for the capital constrained quadratic formula + /// @dev page 17 of https://arxiv.org/pdf/1809.06421.pdf + /// @param totalVotesSquares The sum of tally results squares + /// @param totalSpent The total amount of voice credits spent + function calculateAlpha(uint256 totalVotesSquares, uint256 totalSpent) internal view returns (uint256) { + uint256 budget = token.balanceOf(address(this)); + uint256 contributions = totalSpent * voiceCreditFactor; + + if (budget < contributions) { + revert InvalidBudget(); + } + + if (totalVotesSquares <= totalSpent) { + revert NoProjectHasMoreThanOneVote(); + } + + return ((budget - contributions) * ALPHA_PRECISION) / (voiceCreditFactor * (totalVotesSquares - totalSpent)); + } +} diff --git a/packages/contracts/contracts/maci/TallyFactory.sol b/packages/contracts/contracts/maci/TallyFactory.sol new file mode 100644 index 00000000..57d05f79 --- /dev/null +++ b/packages/contracts/contracts/maci/TallyFactory.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { TallyFactory as BaseTallyFactory } from "maci-contracts/contracts/TallyFactory.sol"; + +import { Tally } from "./Tally.sol"; + +/// @title TallyFactory +/// @notice A factory contract which deploys Tally contracts. +contract TallyFactory is BaseTallyFactory { + /// @inheritdoc BaseTallyFactory + function deploy( + address verifier, + address vkRegistry, + address poll, + address messageProcessor, + address owner, + Mode mode + ) public virtual override returns (address tallyAddress) { + // deploy Tally for this Poll + Tally tally = new Tally(verifier, vkRegistry, poll, messageProcessor, owner, mode); + tallyAddress = address(tally); + } +} diff --git a/packages/contracts/contracts/mocks/MockERC20.sol b/packages/contracts/contracts/mocks/MockERC20.sol new file mode 100644 index 00000000..62fedec7 --- /dev/null +++ b/packages/contracts/contracts/mocks/MockERC20.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @title MockERC20 +/// @notice A mock ERC20 contract that mints 100,000,000,000,000,000 tokens to the deployer +contract MockERC20 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) { + _mint(msg.sender, 100e21); + } +} diff --git a/packages/contracts/package.json b/packages/contracts/package.json index d2594c5c..31c95bfc 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -56,6 +56,7 @@ "lowdb": "^1.0.0", "maci-contracts": "^2.3.0", "maci-core": "^2.2.0", + "maci-crypto": "^2.2.0", "maci-domainobjs": "^2.2.0", "solidity-docgen": "^0.6.0-beta.36" }, diff --git a/packages/contracts/tasks/deploy/maci/07-tallyFactory.ts b/packages/contracts/tasks/deploy/maci/07-tallyFactory.ts new file mode 100644 index 00000000..ca41cacf --- /dev/null +++ b/packages/contracts/tasks/deploy/maci/07-tallyFactory.ts @@ -0,0 +1,53 @@ +import { ContractStorage, Deployment, type IDeployParams } from "maci-contracts"; + +import { EDeploySteps, EContracts } from "../../helpers/constants"; + +const deployment = Deployment.getInstance(); +const storage = ContractStorage.getInstance(); + +/** + * Deploy step registration and task itself + */ +deployment.deployTask(EDeploySteps.TallyFactory, "Deploy tally factory").then((task) => + task.setAction(async ({ incremental }: IDeployParams, hre) => { + deployment.setHre(hre); + const deployer = await deployment.getDeployer(); + + const tallyFactoryContractAddress = storage.getAddress(EContracts.TallyFactory, hre.network.name); + + if (incremental && tallyFactoryContractAddress) { + // eslint-disable-next-line no-console + console.log(`Skipping deployment of the ${EContracts.TallyFactory} contract`); + return; + } + + const poseidonT3ContractAddress = storage.mustGetAddress(EContracts.PoseidonT3, hre.network.name); + const poseidonT4ContractAddress = storage.mustGetAddress(EContracts.PoseidonT4, hre.network.name); + const poseidonT5ContractAddress = storage.mustGetAddress(EContracts.PoseidonT5, hre.network.name); + const poseidonT6ContractAddress = storage.mustGetAddress(EContracts.PoseidonT6, hre.network.name); + + const linkedTallyFactoryContract = await hre.ethers.getContractFactory( + "contracts/maci/TallyFactory.sol:TallyFactory", + { + signer: deployer, + libraries: { + PoseidonT3: poseidonT3ContractAddress, + PoseidonT4: poseidonT4ContractAddress, + PoseidonT5: poseidonT5ContractAddress, + PoseidonT6: poseidonT6ContractAddress, + }, + }, + ); + + const tallyFactoryContract = await deployment.deployContractWithLinkedLibraries({ + contractFactory: linkedTallyFactoryContract, + }); + + await storage.register({ + id: EContracts.TallyFactory, + contract: tallyFactoryContract, + args: [], + network: hre.network.name, + }); + }), +); diff --git a/packages/contracts/tasks/deploy/poll/01-poll.ts b/packages/contracts/tasks/deploy/poll/01-poll.ts index 8677b5a1..9e742ada 100644 --- a/packages/contracts/tasks/deploy/poll/01-poll.ts +++ b/packages/contracts/tasks/deploy/poll/01-poll.ts @@ -2,7 +2,7 @@ import { ContractStorage, Deployment, EMode } from "maci-contracts"; import { PubKey } from "maci-domainobjs"; -import { type Poll, type MACI } from "../../../typechain-types"; +import { type Poll, type MACI, Tally } from "../../../typechain-types"; import { EContracts, EDeploySteps, REGISTRY_TYPES, TRegistryManager, TRegistry } from "../../helpers/constants"; const deployment = Deployment.getInstance(); @@ -31,7 +31,11 @@ deployment.deployTask(EDeploySteps.Poll, "Deploy poll").then((task) => throw new Error("Need to deploy VkRegistry contract first"); } - const { MACI__factory: MACIFactory, Poll__factory: PollFactory } = await import("../../../typechain-types"); + const { + MACI__factory: MACIFactory, + Poll__factory: PollFactory, + Tally__factory: TallyFactory, + } = await import("../../../typechain-types"); const maciContract = await deployment.getContract({ name: EContracts.MACI, abi: MACIFactory.abi }); const pollId = await maciContract.nextPollId(); @@ -113,8 +117,9 @@ deployment.deployTask(EDeploySteps.Poll, "Deploy poll").then((task) => address: messageProcessorContractAddress, }); - const tallyContract = await deployment.getContract({ + const tallyContract = await deployment.getContract({ name: EContracts.Tally, + abi: TallyFactory.abi, address: tallyContractAddress, }); diff --git a/packages/contracts/tests/Maci.test.ts b/packages/contracts/tests/Maci.test.ts index 2df89b5c..e03d4591 100644 --- a/packages/contracts/tests/Maci.test.ts +++ b/packages/contracts/tests/Maci.test.ts @@ -9,9 +9,9 @@ import { MACI, Poll__factory as PollFactory, Poll as PollContract } from "../typ import { NOTHING_UP_MY_SLEEVE, STATE_TREE_DEPTH, - duration, - initialVoiceCreditBalance, - messageBatchSize, + DURATION, + INITIAL_VOICE_CREDIT_BALANCE, + MESSAGE_BATCH_SIZE, treeDepths, } from "./constants"; import { deployTestContracts } from "./utils"; @@ -36,7 +36,7 @@ describe("Maci", () => { [owner, user] = await getSigners(); const contracts = await deployTestContracts({ - initialVoiceCreditBalance, + initialVoiceCreditBalance: INITIAL_VOICE_CREDIT_BALANCE, stateTreeDepth: STATE_TREE_DEPTH, signer: owner, }); @@ -46,7 +46,7 @@ describe("Maci", () => { // deploy on chain poll const tx = await maciContract.deployPoll( - duration, + DURATION, treeDepths, coordinator.pubKey.asContractParam(), verifierContract, @@ -66,7 +66,7 @@ describe("Maci", () => { pollContract = PollFactory.connect(pollContracts.poll, owner); // deploy local poll - const p = maciState.deployPoll(BigInt(deployTime + duration), treeDepths, messageBatchSize, coordinator); + const p = maciState.deployPoll(BigInt(deployTime + DURATION), treeDepths, MESSAGE_BATCH_SIZE, coordinator); expect(p.toString()).to.eq(pollId.toString()); // publish the NOTHING_UP_MY_SLEEVE message const messageData = [NOTHING_UP_MY_SLEEVE]; diff --git a/packages/contracts/tests/Poll.test.ts b/packages/contracts/tests/Poll.test.ts index 299a4406..75580e26 100644 --- a/packages/contracts/tests/Poll.test.ts +++ b/packages/contracts/tests/Poll.test.ts @@ -1,24 +1,20 @@ import { expect } from "chai"; import { encodeBytes32String, ZeroAddress, type Signer } from "ethers"; -import hardhat from "hardhat"; -import { deployContract, Deployment, deployPoseidonContracts, genEmptyBallotRoots, getSigners } from "maci-contracts"; +import { deployContract, genEmptyBallotRoots, getSigners } from "maci-contracts"; import { Keypair, Message, PubKey } from "maci-domainobjs"; -import { type Poll, Poll__factory as PollFactory, PollFactory as PollFactoryContract } from "../typechain-types"; +import type { Poll } from "../typechain-types"; import { DEFAULT_SR_QUEUE_OPS, STATE_TREE_DEPTH, treeDepths } from "./constants"; -import { timeTravel } from "./utils"; +import { deployTestPoll, timeTravel } from "./utils"; describe("Poll", () => { - let pollFactory: PollFactoryContract; let pollContract: Poll; let owner: Signer; let user: Signer; const { pubKey: coordinatorPubKey } = new Keypair(); - const deployment = Deployment.getInstance(hardhat); - const emptyBallotRoots = genEmptyBallotRoots(STATE_TREE_DEPTH); const emptyBallotRoot = emptyBallotRoots[treeDepths.voteOptionTreeDepth]; const duration = 100; @@ -32,34 +28,8 @@ describe("Poll", () => { before(async () => { [owner, user] = await getSigners(); - const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = - await deployPoseidonContracts(owner); - - const contractFactory = await hardhat.ethers.getContractFactory("contracts/maci/PollFactory.sol:PollFactory", { - signer: owner, - libraries: { - PoseidonT3: PoseidonT3Contract, - PoseidonT4: PoseidonT4Contract, - PoseidonT5: PoseidonT5Contract, - PoseidonT6: PoseidonT6Contract, - }, - }); - - pollFactory = await deployment.deployContractWithLinkedLibraries({ contractFactory, signer: owner }); - - const pollAddress = await pollFactory.deploy.staticCall( - duration, - treeDepths, - coordinatorPubKey.asContractParam(), - await owner.getAddress(), - emptyBallotRoot, - ); - await pollFactory - .deploy("100", treeDepths, coordinatorPubKey.asContractParam(), await owner.getAddress(), emptyBallotRoot) - .then((tx) => tx.wait()); - - pollContract = PollFactory.connect(pollAddress, owner); + pollContract = await deployTestPoll({ signer: owner, emptyBallotRoot, treeDepths, duration, coordinatorPubKey }); }); it("should fail if unauthorized user tries to set the poll registry", async () => { @@ -105,6 +75,8 @@ describe("Poll", () => { .to.emit(pollContract, "SetRegistry") .withArgs(address); + expect(await pollContract.getRegistry()).to.equal(address); + await expect(pollContract.connect(owner).setRegistry(address)).to.be.revertedWithCustomError( pollContract, "RegistryAlreadyInitialized", diff --git a/packages/contracts/tests/Tally.test.ts b/packages/contracts/tests/Tally.test.ts new file mode 100644 index 00000000..1929941d --- /dev/null +++ b/packages/contracts/tests/Tally.test.ts @@ -0,0 +1,386 @@ +import { expect } from "chai"; +import { encodeBytes32String, parseUnits, Signer } from "ethers"; +import { + getSigners, + deployContract, + EMode, + MessageProcessor, + MockVerifier, + VkRegistry, + MessageProcessor__factory as MessageProcessorFactory, + IVerifyingKeyStruct, +} from "maci-contracts"; +import { genTreeProof } from "maci-crypto"; +import { Keypair, Message, PCommand, PubKey } from "maci-domainobjs"; + +import { + Tally, + ERC20, + Poll, + IRecipientRegistry, + MACI, + Poll__factory as PollFactory, + Tally__factory as TallyFactory, +} from "../typechain-types"; + +import { + INITIAL_VOICE_CREDIT_BALANCE, + MESSAGE_BATCH_SIZE, + PER_VO_SPENT_VOICE_CREDITS, + STATE_TREE_DEPTH, + TALLY_RESULTS, + TEST_PROCESS_VK, + TEST_TALLY_VK, + TOTAL_SPENT_VOICE_CREDITS, + treeDepths, +} from "./constants"; +import { deployTestContracts, timeTravel } from "./utils"; + +describe("Tally", () => { + let payoutToken: ERC20; + let poll: Poll; + let tally: Tally; + let maci: MACI; + let messageProcessor: MessageProcessor; + let registry: IRecipientRegistry; + let verifier: MockVerifier; + let vkRegistry: VkRegistry; + let owner: Signer; + let user: Signer; + let project: Signer; + + let ownerAddress: string; + let projectAddress: string; + + const maxRecipients = TALLY_RESULTS.tally.length; + + const cooldownTime = 1_000; + const metadataUrl = encodeBytes32String("url"); + const maxContribution = parseUnits("5", 18); + const duration = 100; + const keypair = new Keypair(); + + const emptyClaimParams = { + index: 0, + voiceCreditsPerOption: 0, + tallyResult: 0, + totalVotesSquares: 0, + totalSpent: 0, + tallyResultProof: [], + tallyResultSalt: 0, + voteOptionTreeDepth: 0, + spentVoiceCreditsHash: 0, + perVOSpentVoiceCreditsHash: 0, + }; + + before(async () => { + [owner, user, project] = await getSigners(); + [ownerAddress, , projectAddress] = await Promise.all([owner.getAddress(), user.getAddress(), project.getAddress()]); + + payoutToken = await deployContract("MockERC20", owner, true, "Payout token", "PT"); + + const contracts = await deployTestContracts({ + initialVoiceCreditBalance: INITIAL_VOICE_CREDIT_BALANCE, + stateTreeDepth: STATE_TREE_DEPTH, + signer: owner, + }); + maci = contracts.maciContract; + verifier = contracts.mockVerifierContract; + vkRegistry = contracts.vkRegistryContract; + + await vkRegistry.setVerifyingKeys( + STATE_TREE_DEPTH, + treeDepths.intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + MESSAGE_BATCH_SIZE, + EMode.QV, + TEST_PROCESS_VK.asContractParam() as IVerifyingKeyStruct, + TEST_TALLY_VK.asContractParam() as IVerifyingKeyStruct, + ); + + await maci + .deployPoll(duration, treeDepths, keypair.pubKey.asContractParam(), verifier, vkRegistry, EMode.QV) + .then((tx) => tx.wait()); + + registry = await deployContract("SimpleRegistry", owner, true, maxRecipients, metadataUrl, ownerAddress); + + await maci.setPollRegistry(0n, registry).then((tx) => tx.wait()); + await maci.initPoll(0n).then((tx) => tx.wait()); + + const pollContracts = await maci.getPoll(0n); + poll = PollFactory.connect(pollContracts.poll, owner); + tally = TallyFactory.connect(pollContracts.tally, owner); + messageProcessor = MessageProcessorFactory.connect(pollContracts.messageProcessor, owner); + + const messages: [Message, PubKey][] = []; + for (let i = 0; i < 2; i += 1) { + const command = new PCommand(1n, keypair.pubKey, 0n, 9n, 1n, 0n, BigInt(i)); + const signature = command.sign(keypair.privKey); + const sharedKey = Keypair.genEcdhSharedKey(keypair.privKey, keypair.pubKey); + const message = command.encrypt(signature, sharedKey); + messages.push([message, keypair.pubKey]); + } + + await poll + .publishMessageBatch( + messages.map(([m]) => m.asContractParam()), + messages.map(([, k]) => k.asContractParam()), + ) + .then((tx) => tx.wait()); + }); + + it("should not allow to deposit/claim/withdraw before initialization", async () => { + await expect(tally.deposit(1n)).to.be.revertedWithCustomError(tally, "NotInitialized"); + await expect(tally.withdrawExtra([], [])).to.be.revertedWithCustomError(tally, "NotInitialized"); + await expect(tally.claim(emptyClaimParams)).to.be.revertedWithCustomError(tally, "NotInitialized"); + }); + + it("should not allow non-owner to initialize tally", async () => { + await expect( + tally.connect(user).init({ + cooldownTime, + maxContribution: parseUnits("5", await payoutToken.decimals()), + poll, + payoutToken, + }), + ).to.be.revertedWithCustomError(tally, "OwnableUnauthorizedAccount"); + }); + + it("should not allow to initialize with different poll", async () => { + await expect( + tally.init({ + cooldownTime, + poll: project, + maxContribution: parseUnits("5", await payoutToken.decimals()), + payoutToken, + }), + ).to.be.revertedWithCustomError(tally, "InvalidPoll"); + }); + + it("should initialize tally properly", async () => { + const receipt = await tally + .init({ + cooldownTime, + poll, + maxContribution: parseUnits("5", await payoutToken.decimals()), + payoutToken, + }) + .then((tx) => tx.wait()); + + expect(receipt?.status).to.equal(1); + }); + + it("should not allow to initialize tally twice", async () => { + await expect( + tally.init({ + cooldownTime, + poll, + maxContribution: parseUnits("5", await payoutToken.decimals()), + payoutToken, + }), + ).to.be.revertedWithCustomError(tally, "AlreadyInitialized"); + }); + + it("should deposit funds properly", async () => { + const [decimals, initialBalance] = await Promise.all([payoutToken.decimals(), payoutToken.balanceOf(owner)]); + const ownerAmount = parseUnits(TOTAL_SPENT_VOICE_CREDITS.spent, decimals); + const userAmount = parseUnits("2", decimals); + + await payoutToken.approve(user, userAmount).then((tx) => tx.wait()); + await payoutToken.transfer(user, userAmount); + + await payoutToken.approve(tally, ownerAmount).then((tx) => tx.wait()); + await tally.deposit(ownerAmount).then((tx) => tx.wait()); + + await payoutToken + .connect(user) + .approve(tally, userAmount) + .then((tx) => tx.wait()); + await tally + .connect(user) + .deposit(userAmount) + .then((tx) => tx.wait()); + + const [tokenBalance, totalAmount] = await Promise.all([payoutToken.balanceOf(tally), tally.totalAmount()]); + + expect(totalAmount).to.equal(tokenBalance); + expect(initialBalance - tokenBalance).to.equal(initialBalance - ownerAmount - userAmount); + }); + + it("should not withdraw extra if cooldown period is not over", async () => { + await expect(tally.withdrawExtra([owner, user], [1n])).to.be.revertedWithCustomError( + tally, + "CooldownPeriodNotOver", + ); + }); + + it("should not allow non-owner to withdraw funds", async () => { + await expect(tally.connect(user).withdrawExtra([owner, user], [1n])).to.be.revertedWithCustomError( + tally, + "OwnableUnauthorizedAccount", + ); + }); + + it("should not allow non-owner to pause/unpause", async () => { + await expect(tally.connect(user).pause()).to.be.revertedWithCustomError(tally, "OwnableUnauthorizedAccount"); + + await expect(tally.connect(user).unpause()).to.be.revertedWithCustomError(tally, "OwnableUnauthorizedAccount"); + }); + + it("should not allow to call functions if contract is paused", async () => { + try { + await tally.pause().then((tx) => tx.wait()); + + await expect(tally.deposit(1n)).to.be.revertedWithCustomError(tally, "EnforcedPause"); + + await expect(tally.withdrawExtra([owner, user], [1n])).to.be.revertedWithCustomError(tally, "EnforcedPause"); + + await expect(tally.claim(emptyClaimParams)).to.be.revertedWithCustomError(tally, "EnforcedPause"); + } finally { + await tally.unpause().then((tx) => tx.wait()); + } + }); + + it("should not allow to claim before voting deadline", async () => { + await expect(tally.claim(emptyClaimParams)).to.be.revertedWithCustomError(tally, "VotingPeriodNotOver"); + }); + + it("should not claim funds for the project if proof generation is failed", async () => { + await timeTravel(cooldownTime + duration, owner); + + const totalVotesSquares = TALLY_RESULTS.tally.reduce((acc, x) => acc + BigInt(x) * BigInt(x), 0n); + const voteOptionTreeDepth = 3; + const tallyResults = TALLY_RESULTS.tally.map((x) => BigInt(x)); + + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let index = 0; index < TALLY_RESULTS.tally.length; index += 1) { + const tallyResultProof = genTreeProof(index, tallyResults, voteOptionTreeDepth); + + const params = { + index, + voiceCreditsPerOption: PER_VO_SPENT_VOICE_CREDITS.tally[index], + tallyResult: TALLY_RESULTS.tally[index], + totalVotesSquares, + totalSpent: TOTAL_SPENT_VOICE_CREDITS.spent, + tallyResultProof, + tallyResultSalt: TALLY_RESULTS.salt, + voteOptionTreeDepth, + spentVoiceCreditsHash: TOTAL_SPENT_VOICE_CREDITS.commitment, + perVOSpentVoiceCreditsHash: PER_VO_SPENT_VOICE_CREDITS.commitment, + }; + + // eslint-disable-next-line no-await-in-loop + await expect(tally.claim(params)).to.be.revertedWithCustomError(tally, "InvalidTallyVotesProof"); + } + }); + + it("should claim funds properly for the project", async () => { + await timeTravel(cooldownTime + duration, owner); + + const totalVotesSquares = TALLY_RESULTS.tally.reduce((acc, x) => acc + BigInt(x) * BigInt(x), 0n); + const voteOptionTreeDepth = 3; + const tallyResults = TALLY_RESULTS.tally.map((x) => BigInt(x)); + + await poll.mergeMaciState().then((tx) => tx.wait()); + await poll.mergeMessageAqSubRoots(4).then((tx) => tx.wait()); + await poll.mergeMessageAq().then((tx) => tx.wait()); + + await messageProcessor.processMessages(0n, [0, 0, 0, 0, 0, 0, 0, 0]); + await tally.tallyVotes( + 16314492373388736967790160553643622877652062027311228078826397128502181670780n, + [0, 0, 0, 0, 0, 0, 0, 0], + ); + + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let index = 0; index < TALLY_RESULTS.tally.length; index += 1) { + // eslint-disable-next-line no-await-in-loop + await registry + .addRecipient({ + id: encodeBytes32String(index.toString()), + metadataUrl, + recipient: projectAddress, + }) + .then((tx) => tx.wait()); + + const tallyResultProof = genTreeProof(index, tallyResults, voteOptionTreeDepth); + + const params = { + index, + voiceCreditsPerOption: PER_VO_SPENT_VOICE_CREDITS.tally[index], + tallyResult: TALLY_RESULTS.tally[index], + totalVotesSquares, + totalSpent: TOTAL_SPENT_VOICE_CREDITS.spent, + tallyResultProof, + tallyResultSalt: TALLY_RESULTS.salt, + voteOptionTreeDepth, + spentVoiceCreditsHash: TOTAL_SPENT_VOICE_CREDITS.commitment, + perVOSpentVoiceCreditsHash: PER_VO_SPENT_VOICE_CREDITS.commitment, + }; + + // eslint-disable-next-line no-await-in-loop + await tally.claim(params).then((tx) => tx.wait()); + } + }); + + it("should not allow to claim funds if there are no any votes", async () => { + await expect(tally.claim(emptyClaimParams).then((tx) => tx.wait())).to.be.revertedWithCustomError( + tally, + "NoProjectHasMoreThanOneVote", + ); + }); + + it("should not allow to claim funds if there are not enough funds", async () => { + const total = await payoutToken.balanceOf(tally); + + const params = { + index: 0n, + voiceCreditsPerOption: 0n, + tallyResult: 1n, + totalVotesSquares: 1n, + totalSpent: total + 1n, + tallyResultProof: [], + tallyResultSalt: 0n, + voteOptionTreeDepth: 2, + spentVoiceCreditsHash: 0n, + perVOSpentVoiceCreditsHash: 0n, + }; + + await expect(tally.claim({ ...params }).then((tx) => tx.wait())).to.be.revertedWithCustomError( + tally, + "InvalidBudget", + ); + }); + + it("should withdraw extra after cooldown properly", async () => { + const [contractBalance, initialOwnerBalance, totalAmount] = await Promise.all([ + payoutToken.balanceOf(tally), + payoutToken.balanceOf(owner), + tally.totalAmount(), + ]); + + await tally.withdrawExtra([owner, user], [totalAmount, 0]).then((tx) => tx.wait()); + + const [balance, ownerBalance, totalExtraFunds] = await Promise.all([ + payoutToken.balanceOf(tally), + payoutToken.balanceOf(owner), + tally.totalAmount(), + ]); + + expect(balance).to.equal(0n); + expect(totalExtraFunds).to.equal(0n); + expect(initialOwnerBalance + totalAmount).to.equal(ownerBalance); + expect(contractBalance).to.equal(totalAmount); + }); + + it("should not withdraw extra if there is no enough funds", async () => { + await expect(tally.withdrawExtra([owner], [maxContribution])).to.be.revertedWithCustomError( + tally, + "InvalidWithdrawal", + ); + }); + + it("should not deposit after voting period is over", async () => { + await expect(tally.deposit(1n)).to.be.revertedWithCustomError(tally, "VotingPeriodOver"); + }); +}); diff --git a/packages/contracts/tests/constants.ts b/packages/contracts/tests/constants.ts index 821c0bd2..aa0ee36b 100644 --- a/packages/contracts/tests/constants.ts +++ b/packages/contracts/tests/constants.ts @@ -1,15 +1,32 @@ -import { TreeDepths, STATE_TREE_ARITY, MESSAGE_TREE_ARITY } from "maci-core"; - -export const duration = 2_000; +import { TreeDepths, MESSAGE_TREE_ARITY } from "maci-core"; +import { G1Point, G2Point } from "maci-crypto"; +import { VerifyingKey } from "maci-domainobjs"; export const STATE_TREE_DEPTH = 10; export const MESSAGE_TREE_DEPTH = 2; export const MESSAGE_TREE_SUBDEPTH = 1; -export const messageBatchSize = MESSAGE_TREE_ARITY ** MESSAGE_TREE_SUBDEPTH; +export const MESSAGE_BATCH_SIZE = MESSAGE_TREE_ARITY ** MESSAGE_TREE_SUBDEPTH; export const NOTHING_UP_MY_SLEEVE = 8370432830353022751713833565135785980866757267633941821328460903436894336785n; export const DEFAULT_SR_QUEUE_OPS = 4; -export const initialVoiceCreditBalance = 100; +export const DURATION = 2_000; +export const INITIAL_VOICE_CREDIT_BALANCE = 1000; + +export const TEST_PROCESS_VK = new VerifyingKey( + new G1Point(BigInt(0), BigInt(1)), + new G2Point([BigInt(2), BigInt(3)], [BigInt(4), BigInt(5)]), + new G2Point([BigInt(6), BigInt(7)], [BigInt(8), BigInt(9)]), + new G2Point([BigInt(10), BigInt(11)], [BigInt(12), BigInt(13)]), + [new G1Point(BigInt(14), BigInt(15)), new G1Point(BigInt(16), BigInt(17))], +); + +export const TEST_TALLY_VK = new VerifyingKey( + new G1Point(BigInt(0), BigInt(1)), + new G2Point([BigInt(2), BigInt(3)], [BigInt(4), BigInt(5)]), + new G2Point([BigInt(6), BigInt(7)], [BigInt(8), BigInt(9)]), + new G2Point([BigInt(10), BigInt(11)], [BigInt(12), BigInt(13)]), + [new G1Point(BigInt(14), BigInt(15)), new G1Point(BigInt(16), BigInt(17))], +); export const treeDepths: TreeDepths = { intStateTreeDepth: 1, @@ -18,4 +35,20 @@ export const treeDepths: TreeDepths = { voteOptionTreeDepth: 2, }; -export const tallyBatchSize = STATE_TREE_ARITY ** treeDepths.intStateTreeDepth; +export const TALLY_RESULTS = { + tally: ["12", "107", "159", "67", "26", "35", "53", "17", "114", "158", "65", "60", "0"], + salt: "0xed6956a31ec7a8d7c71e4d9bb90f70281e45e7977644268af3cc061c384feae", + commitment: "0x1f6ebdc3299a1c2eef28cfa14822c4a1e0a607a4044f6aa4434b1b80f1b79ef7", +}; + +export const TOTAL_SPENT_VOICE_CREDITS = { + spent: "12391", + salt: "0x269f6c1a1a34bc13d7369fb93323590504f37edf41d952d5f9e8cf814381c475", + commitment: "0x1f5c444c5dbc821ad8eb76bcfd8966468eca73c08b98cf1504d312751d61b908", +}; + +export const PER_VO_SPENT_VOICE_CREDITS = { + tally: ["86", "1459", "3039", "587", "136", "491", "627", "79", "1644", "2916", "721", "606", "0"], + salt: "0x1f31c9ed6fb5f2beb54c32edbc922a87d467c31d0178c8427fbb7d978048ade7", + commitment: "0xdf601b181267173f055d90e5fb286637634317c29aa57478800077a0f9bc839", +}; diff --git a/packages/contracts/tests/utils.ts b/packages/contracts/tests/utils.ts index de1ecbc3..097edac3 100644 --- a/packages/contracts/tests/utils.ts +++ b/packages/contracts/tests/utils.ts @@ -3,6 +3,7 @@ import { deployConstantInitialVoiceCreditProxy, deployFreeForAllSignUpGatekeeper, deployMaci, + Deployment, deployMockVerifier, deployPoseidonContracts, deployVkRegistry, @@ -13,8 +14,10 @@ import { } from "maci-contracts"; import type { ContractFactory, Signer, JsonRpcProvider } from "ethers"; +import type { TreeDepths } from "maci-core"; +import type { PubKey } from "maci-domainobjs"; -import { type MACI } from "../typechain-types"; +import { Poll, PollFactory as PollFactoryContract, Poll__factory as PollFactory, type MACI } from "../typechain-types"; /** * An interface that represents argument for deployment test contracts @@ -94,7 +97,7 @@ export const deployTestContracts = async ({ "contracts/maci/MACI.sol:MACI", "contracts/maci/PollFactory.sol:PollFactory", "MessageProcessorFactory", - "TallyFactory", + "contracts/maci/TallyFactory.sol:TallyFactory", ].map((factory) => ethers.getContractFactory(factory, { libraries: { @@ -126,6 +129,68 @@ export const deployTestContracts = async ({ }; }; +/** + * An interface that represents arguments for test poll deployment + */ +export interface IDeployTestPollArgs { + signer: Signer; + emptyBallotRoot: bigint | number; + treeDepths: TreeDepths; + duration: bigint | number; + coordinatorPubKey: PubKey; +} + +/** + * Deploy a test poll using poll factory. + * + * @param signer - the signer to use + * @param emptyBallotRoot - the empty ballot root + * @param treeDepths - the tree depths + * @param duration - the poll duration + * @param coordinatorPubKey - the coordinator public key + * @returns the deployed poll contract + */ +export const deployTestPoll = async ({ + signer, + emptyBallotRoot, + treeDepths, + duration, + coordinatorPubKey, +}: IDeployTestPollArgs): Promise => { + const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = + await deployPoseidonContracts(signer); + + const contractFactory = await ethers.getContractFactory("contracts/maci/PollFactory.sol:PollFactory", { + signer, + libraries: { + PoseidonT3: PoseidonT3Contract, + PoseidonT4: PoseidonT4Contract, + PoseidonT5: PoseidonT5Contract, + PoseidonT6: PoseidonT6Contract, + }, + }); + + const deployment = Deployment.getInstance(); + const pollFactory = await deployment.deployContractWithLinkedLibraries({ + contractFactory, + signer, + }); + + const pollAddress = await pollFactory.deploy.staticCall( + duration, + treeDepths, + coordinatorPubKey.asContractParam(), + await signer.getAddress(), + emptyBallotRoot, + ); + + await pollFactory + .deploy("100", treeDepths, coordinatorPubKey.asContractParam(), await signer.getAddress(), emptyBallotRoot) + .then((tx) => tx.wait()); + + return PollFactory.connect(pollAddress, signer); +}; + /** * Utility to travel in time when using a local blockchain * @param seconds - the number of seconds to travel in time diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b1fac88..d64b08eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: maci-core: specifier: ^2.2.0 version: 2.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-crypto: + specifier: ^2.2.0 + version: 2.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) maci-domainobjs: specifier: ^2.2.0 version: 2.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -9458,9 +9461,6 @@ packages: maci-core@2.2.0: resolution: {integrity: sha512-jHS40/uGJZMYvslfDls3LUPXK8gAijVrc8L8o51SJQX44iocgR3aWpWycD8df9rBCGBxScZPbtn04CmtFT0lhQ==} - maci-crypto@2.0.0: - resolution: {integrity: sha512-bkgOoDA1ABG49MXDzzsQPsFVEijAkLk8ocJKGyeNQS7YpNhC3YEVVz/SE4g0td+N4xJhD3PbXsyHeaTM3ApIjw==} - maci-crypto@2.2.0: resolution: {integrity: sha512-kSbWfuAdDWOdtQsEyofvgDIdAE//+iRjFdYjluDpvXnk7//x4t+/U4VEQJlE0kJ3TbCVjmsAaGNcbkmwmU977Q==} @@ -18531,7 +18531,7 @@ snapshots: '@types/concat-stream@1.6.1': dependencies: - '@types/node': 20.14.14 + '@types/node': 22.2.0 '@types/connect-history-api-fallback@1.5.4': dependencies: @@ -18618,7 +18618,7 @@ snapshots: '@types/form-data@0.0.33': dependencies: - '@types/node': 20.14.14 + '@types/node': 22.2.0 '@types/formidable@3.4.5': dependencies: @@ -20070,7 +20070,7 @@ snapshots: axe-core@4.10.0: {} - axios-debug-log@1.0.0(axios@1.7.3(debug@4.3.6)): + axios-debug-log@1.0.0(axios@1.7.3): dependencies: '@types/debug': 4.1.12 axios: 1.7.3(debug@4.3.6) @@ -26316,16 +26316,6 @@ snapshots: - bufferutil - utf-8-validate - maci-crypto@2.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): - dependencies: - '@zk-kit/baby-jubjub': 1.0.1 - '@zk-kit/eddsa-poseidon': 1.0.2 - '@zk-kit/poseidon-cipher': 0.3.1 - ethers: 6.13.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - maci-crypto@2.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@zk-kit/baby-jubjub': 1.0.1 @@ -26338,7 +26328,7 @@ snapshots: maci-domainobjs@2.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: - maci-crypto: 2.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-crypto: 2.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -28837,7 +28827,7 @@ snapshots: resolve@1.22.8: dependencies: - is-core-module: 2.15.0 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -29307,7 +29297,7 @@ snapshots: '@aduh95/viz.js': 3.7.0 '@solidity-parser/parser': 0.16.2 axios: 1.7.3(debug@4.3.6) - axios-debug-log: 1.0.0(axios@1.7.3(debug@4.3.6)) + axios-debug-log: 1.0.0(axios@1.7.3) cli-color: 2.0.4 commander: 11.1.0 convert-svg-to-png: 0.6.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)