From 35ee4d96e3f3550cb0123ffcf4965c1b5dc6d3d5 Mon Sep 17 00:00:00 2001 From: joYyHack Date: Wed, 24 Jul 2024 03:31:17 +0300 Subject: [PATCH 1/8] Initial high-level (unfinished )version of zkmultisig --- contracts/ZKMultisig.sol | 342 ++++++++++++++++++++ contracts/ZKMultisigFactory.sol | 95 ++++++ contracts/interfaces/IZKMultisig.sol | 95 ++++++ contracts/interfaces/IZKMultisigFactory.sol | 34 ++ contracts/mock/tokens/ERC20Mock.sol | 28 -- package-lock.json | 34 +- package.json | 3 +- 7 files changed, 590 insertions(+), 41 deletions(-) create mode 100644 contracts/ZKMultisig.sol create mode 100644 contracts/ZKMultisigFactory.sol create mode 100644 contracts/interfaces/IZKMultisig.sol create mode 100644 contracts/interfaces/IZKMultisigFactory.sol delete mode 100644 contracts/mock/tokens/ERC20Mock.sol diff --git a/contracts/ZKMultisig.sol b/contracts/ZKMultisig.sol new file mode 100644 index 0000000..19fa4d8 --- /dev/null +++ b/contracts/ZKMultisig.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {IZKMultisig} from "./interfaces/IZKMultisig.sol"; + +import {SparseMerkleTree} from "@solarity/solidity-lib/libs/data-structures/SparseMerkleTree.sol"; +import {PRECISION, PERCENTAGE_100} from "@solarity/solidity-lib/utils/Globals.sol"; +import {Paginator} from "@solarity/solidity-lib/libs/arrays/Paginator.sol"; + +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {PoseidonUnit1L} from "@iden3/contracts/lib/Poseidon.sol"; + +contract ZKMultisig is UUPSUpgradeable, IZKMultisig { + using SparseMerkleTree for SparseMerkleTree.Bytes32SMT; + using EnumerableSet for EnumerableSet.Bytes32Set; + using EnumerableSet for EnumerableSet.UintSet; + using Paginator for EnumerableSet.UintSet; + using Math for uint256; + + enum ParticipantsAction { + ADD, + REMOVE + } + + uint256 public constant TREE_SIZE = 20; + + SparseMerkleTree.Bytes32SMT internal _bytes32Tree; + EnumerableSet.Bytes32Set internal _participants; + EnumerableSet.UintSet internal _proposalIds; + + uint256 private _quorumPercentage; + + mapping(uint256 => ProposalInfoView) private _proposals; + mapping(uint256 => uint256) private _blinders; + + event Initialized(uint256 participantsAmount, uint256 quorumPercentage); + event RootUpdated(bytes32 indexed root); + event QuorumPercentageUpdated(uint256 indexed newQuorumPercentage); + + modifier onlyThis() { + require(msg.sender == address(this), "ZKMultisig: Not authorized call"); + _; + } + + modifier withRootUpdate() { + _; + _notifyRoot(); + } + + modifier withQuorumUpdate() { + _; + _notifyQourumPercentage(); + } + + constructor() { + _disableInitializers(); + } + + function initialize( + uint256[] memory participants_, + uint256 quorumPercentage_ + ) external initializer { + __ZKMultisig_init(participants_, quorumPercentage_); + } + + function __ZKMultisig_init( + uint256[] memory participants_, + uint256 quorumPercentage_ + ) internal { + _updateQourumPercentage(quorumPercentage_); + _bytes32Tree.initialize(uint32(TREE_SIZE)); + _addParticipants(participants_); + } + + function addParticipants( + uint256[] calldata participantsToAdd_ + ) external onlyThis withRootUpdate { + _addParticipants(participantsToAdd_); + } + + function removeParticipants( + uint256[] calldata participantsToRemove_ + ) external onlyThis withRootUpdate { + _removeParticipants(participantsToRemove_); + } + + function updateQuorumPercentage( + uint256 newQuorumPercentage_ + ) external onlyThis withQuorumUpdate { + _updateQourumPercentage(newQuorumPercentage_); + } + + function create( + ProposalContent calldata content_, + uint256 duration_, + uint256 salt_, + ZKParams calldata proofData_ + ) external returns (uint256) { + uint256 proposalId_ = _computeProposalId(content_, salt_); + + require( + _proposals[proposalId_].status == ProposalStatus.NONE, + "ZKMultisig: Proposal already exists" + ); + + require(duration_ > 0, "ZKMultisig: Invalid duration"); + + uint256 votesCount_ = 1; // 1 vote from creator + uint256 requiredQuorum_ = ((_participants.length() * _quorumPercentage) / PERCENTAGE_100) + .max(1); + + _proposals[proposalId_] = ProposalInfoView({ + content: content_, + proposalEndTime: block.timestamp + duration_, + status: votesCount_ >= requiredQuorum_ + ? ProposalStatus.ACCEPTED + : ProposalStatus.VOTING, + votesCount: votesCount_, + requiredQuorum: requiredQuorum_ + }); + + _proposalIds.add(proposalId_); + // assign proposalId to blinder + _blinders[proofData_.inputs[0]] = proposalId_; + + emit ProposalCreated(proposalId_, content_); + + return proposalId_; + } + + function vote(uint256 proposalId_, ZKParams calldata proofData_) external { + ProposalInfoView storage _proposal = _proposals[proposalId_]; + uint256 blinder_ = proofData_.inputs[0]; + + require( + _proposal.status == ProposalStatus.VOTING, + "ZKMultisig: Proposal is not in voting state" + ); + + require(block.timestamp < _proposal.proposalEndTime, "ZKMultisig: Proposal expired"); + + require(!_isBlinderVoted(proposalId_, blinder_), "ZKMultisig: Already voted"); + + _blinders[blinder_] = proposalId_; + + _proposal.votesCount += 1; + + if (_proposal.votesCount >= _proposal.requiredQuorum) { + _proposal.status = ProposalStatus.ACCEPTED; + } + + emit ProposalVoted(proposalId_, blinder_); + } + + function execute(uint256 proposalId_) external { + ProposalInfoView storage _proposal = _proposals[proposalId_]; + + require( + _proposal.status == ProposalStatus.ACCEPTED, + "ZKMultisig: Proposal is not accepted" + ); + + (bool success, ) = _proposal.content.target.call{value: _proposal.content.value}( + _proposal.content.data + ); + + require(success, "ZKMultisig: Proposal execution failed"); + + _proposal.status = ProposalStatus.EXECUTED; + + emit ProposalExecuted(proposalId_); + } + + function getParticipantsSMTRoot() external view returns (bytes32) { + return _bytes32Tree.getRoot(); + } + + function getParticipantsSMTProof( + bytes32 publicKeyHash_ + ) external view override returns (SparseMerkleTree.Proof memory) { + return _bytes32Tree.getProof(publicKeyHash_); + } + + function getParticipantsCount() external view returns (uint256) { + return _participants.length(); + } + + function getParticipants() external view returns (bytes32[] memory) { + return _participants.values(); + } + + function getProposalsCount() external view returns (uint256) { + return _proposalIds.length(); + } + + function getProposalsIds( + uint256 offset, + uint256 limit + ) external view override returns (uint256[] memory) { + return _proposalIds.part(offset, limit); + } + + function getQuorumPercentage() external view returns (uint256) { + return _quorumPercentage; + } + + function getProposalInfo(uint256 proposalId_) external view returns (ProposalInfoView memory) { + return _proposals[proposalId_]; + } + + function getProposalStatus(uint256 proposalId_) external view returns (ProposalStatus) { + return _proposals[proposalId_].status; + } + + // double check doc, bc there uint248(keccak256(abi.encode(block.chainid, address(this), proposalId_))) is used + function getProposalChallenge(uint256 proposalId_) external view returns (uint256) { + return + uint256( + PoseidonUnit1L.poseidon( + [uint256(keccak256(abi.encode(block.chainid, address(this), proposalId_)))] + ) + ); + } + + function computeProposalId( + ProposalContent calldata content_, + uint256 salt_ + ) external pure returns (uint256) { + return _computeProposalId(content_, salt_); + } + + function isBlinderVoted( + uint256 proposalId_, + uint256 blinderToCheck_ + ) external view returns (bool) { + return _isBlinderVoted(proposalId_, blinderToCheck_); + } + + function _authorizeUpgrade(address newImplementation_) internal override onlyThis {} + + function _addParticipants(uint256[] memory participantsToAdd_) internal { + require( + _participants.length() + participantsToAdd_.length <= 2 ** TREE_SIZE, + "ZKMultisig: Too many participants" + ); + _processParticipants(participantsToAdd_, ParticipantsAction.ADD); + + // require(participantsToAdd.length > 0, "ZKMultisig: No participants to add"); + // for (uint256 i = 0; i < participantsToAdd.length; i++) { + // uint256 participant_ = participantsToAdd[i]; + // bytes32 participantKey_ = keccak256(abi.encodePacked(participant_)); + // if (_uintTree.getProof(participantKey_).existence) { + // continue; + // // or revert? + // } + // _uintTree.add(participantKey_, participant_); + // } + } + + function _removeParticipants(uint256[] memory participantsToRemove_) internal { + require( + _participants.length() > participantsToRemove_.length, + "ZKMultisig: Cannot remove all participants" + ); + _processParticipants(participantsToRemove_, ParticipantsAction.REMOVE); + + // require(participantsToRemove.length > 0, "ZKMultisig: No participants to remove"); + // for (uint256 i = 0; i < participantsToRemove.length; i++) { + // uint256 participant_ = participantsToRemove[i]; + // bytes32 participantKey_ = keccak256(abi.encodePacked(participant_)); + // if (_uintTree.getProof(participantKey_).existence) { + // _uintTree.remove(participantKey_); + // // should revert if false? + // } + // } + } + + function _updateQourumPercentage(uint256 newQuorumPercentage_) internal { + require( + newQuorumPercentage_ > 0 && + newQuorumPercentage_ <= 100 && + newQuorumPercentage_ != _quorumPercentage, + "ZKMultisig: Invalid quorum percentage" + ); + + _quorumPercentage = newQuorumPercentage_; + } + + function _computeProposalId( + ProposalContent calldata content_, + uint256 salt_ + ) internal pure returns (uint256) { + return + uint256(keccak256(abi.encode(content_.target, content_.value, content_.data, salt_))); + } + + function _isBlinderVoted( + uint256 proposalId_, + uint256 blinderToCheck_ + ) internal view returns (bool) { + return _blinders[blinderToCheck_] == proposalId_; + } + + function _notifyRoot() internal { + emit RootUpdated(_bytes32Tree.getRoot()); + } + + function _notifyQourumPercentage() internal { + emit QuorumPercentageUpdated(_quorumPercentage); + } + + function _processParticipants( + uint256[] memory participants_, + ParticipantsAction action_ + ) private { + require(participants_.length > 0, "Multisig: No participants to process"); + + for (uint256 i = 0; i < participants_.length; i++) { + bytes32 participant_ = bytes32(participants_[i]); + bytes32 participantKey_ = keccak256(abi.encodePacked(participant_)); + + bool nodeExists = _bytes32Tree.getProof(participantKey_).existence; + + // revert in false case? + if (!nodeExists && action_ == ParticipantsAction.ADD) { + _bytes32Tree.add(participantKey_, participant_); + _participants.add(participant_); + } + + // revert in false case? + if (nodeExists && action_ == ParticipantsAction.REMOVE) { + _bytes32Tree.remove(participantKey_); + _participants.remove(participant_); + } + } + } +} diff --git a/contracts/ZKMultisigFactory.sol b/contracts/ZKMultisigFactory.sol new file mode 100644 index 0000000..620ff0a --- /dev/null +++ b/contracts/ZKMultisigFactory.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {IZKMultisigFactory} from "./interfaces/IZKMultisigFactory.sol"; +import {IZKMultisig} from "./interfaces/IZKMultisig.sol"; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import {Paginator} from "@solarity/solidity-lib/libs/arrays/Paginator.sol"; + +contract ZKMultisigFactory is EIP712, IZKMultisigFactory { + using EnumerableSet for EnumerableSet.AddressSet; + using Paginator for EnumerableSet.AddressSet; + + EnumerableSet.AddressSet private _zkMultisigs; + + bytes32 private constant KDF_MESSAGE_TYPEHASH = keccak256("KDF(address zkMultisigAddress)"); + + string private constant EIP712_NAME = "ZKMultisigFactory"; + string private constant EIP712_VERSION = "1"; + + address private immutable _zkMulsigImplementation; + + constructor(address zkMultisigImplementation_) EIP712(EIP712_NAME, EIP712_VERSION) { + _zkMulsigImplementation = zkMultisigImplementation_; + } + + function createMultisig( + uint256[] calldata participants_, + uint256 quorumPercentage_, + uint256 salt_ + ) external returns (address) { + address zkMultisigAddress_ = address( + new ERC1967Proxy{salt: keccak256(abi.encode(msg.sender, salt_))}( + _zkMulsigImplementation, + "" + ) + ); + + IZKMultisig(zkMultisigAddress_).initialize(participants_, quorumPercentage_); + + _zkMultisigs.add(zkMultisigAddress_); + + emit ZKMutlisigCreated(zkMultisigAddress_, participants_, quorumPercentage_); + + return zkMultisigAddress_; + } + + function computeZKMultisigAddress( + address deployer_, + uint256 salt_ + ) external view returns (address) { + return + Create2.computeAddress( + keccak256(abi.encode(deployer_, salt_)), + keccak256( + abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode(_zkMulsigImplementation) + ) + ) + ); + } + + function getZKMultisigsCount() external view returns (uint256) { + return _zkMultisigs.length(); + } + + function getZKMultisigs( + uint256 offset_, + uint256 limit_ + ) external view returns (address[] memory) { + return _zkMultisigs.part(offset_, limit_); + } + + function isZKMultisig(address multisigAddress_) external view returns (bool) { + return _zkMultisigs.contains(multisigAddress_); + } + + function getDefaultKDFMSGToSign() external view returns (bytes32) { + return _hashTypedDataV4(getKDFMSGHash(address(0))); + } + + function getKDFMSGToSign(address zkMutlisigAddress_) public view returns (bytes32) { + return _hashTypedDataV4(getKDFMSGHash(zkMutlisigAddress_)); + } + + function getKDFMSGHash(address zkMutlisigAddress_) private pure returns (bytes32) { + return keccak256(abi.encode(KDF_MESSAGE_TYPEHASH, zkMutlisigAddress_)); + } +} diff --git a/contracts/interfaces/IZKMultisig.sol b/contracts/interfaces/IZKMultisig.sol new file mode 100644 index 0000000..2e6d7bc --- /dev/null +++ b/contracts/interfaces/IZKMultisig.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {SparseMerkleTree} from "@solarity/solidity-lib/libs/data-structures/SparseMerkleTree.sol"; + +interface IZKMultisig { + enum ProposalStatus { + NONE, + VOTING, + ACCEPTED, + EXPIRED, + EXECUTED + } + + struct ZKParams { + uint256[2] a; + uint256[2][2] b; + uint256[2] c; + uint256[] inputs; // 0 -> blinder, 1 -> challenge, 2 -> SMT root + } + + struct ProposalContent { + address target; + uint256 value; + bytes data; + } + + struct ProposalInfoView { + ProposalContent content; + uint256 proposalEndTime; + ProposalStatus status; + uint256 votesCount; + uint256 requiredQuorum; + } + + event ProposalCreated(uint256 indexed proposalId, ProposalContent content); + + event ProposalVoted(uint256 indexed proposalId, uint256 voterBlinder); + + event ProposalExecuted(uint256 indexed proposalId); + + function initialize(uint256[] memory participants_, uint256 quorumPercentage_) external; + + function addParticipants(uint256[] calldata participantsToAdd) external; + + function removeParticipants(uint256[] calldata participantsToRemove) external; + + function updateQuorumPercentage(uint256 newQuorumPercentage) external; + + function create( + ProposalContent calldata content, + uint256 duration, + uint256 salt, + ZKParams calldata proofData + ) external returns (uint256); + + function vote(uint256 proposalId, ZKParams calldata proofData) external; + + function execute(uint256 proposalId) external; + + function getParticipantsSMTRoot() external view returns (bytes32); + + function getParticipantsSMTProof( + bytes32 publicKeyHash + ) external view returns (SparseMerkleTree.Proof memory); + + function getParticipantsCount() external view returns (uint256); + + function getParticipants() external view returns (bytes32[] memory); + + function getProposalsCount() external view returns (uint256); + + function getProposalsIds( + uint256 offset, + uint256 limit + ) external view returns (uint256[] memory); + + function getQuorumPercentage() external view returns (uint256); + + function getProposalInfo(uint256 proposalId) external view returns (ProposalInfoView memory); + + function getProposalStatus(uint256 proposalId) external view returns (ProposalStatus); + + function getProposalChallenge(uint256 proposalId) external view returns (uint256); + + function computeProposalId( + ProposalContent calldata content, + uint256 salt + ) external view returns (uint256); + + function isBlinderVoted( + uint256 proposalId, + uint256 blinderToCheck + ) external view returns (bool); +} diff --git a/contracts/interfaces/IZKMultisigFactory.sol b/contracts/interfaces/IZKMultisigFactory.sol new file mode 100644 index 0000000..b5c9d3e --- /dev/null +++ b/contracts/interfaces/IZKMultisigFactory.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +interface IZKMultisigFactory { + event ZKMutlisigCreated( + address indexed zkMultisigAddress, + uint256[] initialParticipants, + uint256 initialQuorumPercentage + ); + + function createMultisig( + uint256[] calldata participants_, + uint256 quorumPercentage_, + uint256 salt_ + ) external returns (address); + + function getZKMultisigsCount() external view returns (uint256); + + function getZKMultisigs( + uint256 offset_, + uint256 limit_ + ) external view returns (address[] memory); + + function computeZKMultisigAddress( + address deployer, + uint256 salt + ) external view returns (address); + + function getKDFMSGToSign(address zkMutlisigAddress_) external view returns (bytes32); + + function getDefaultKDFMSGToSign() external view returns (bytes32); + + function isZKMultisig(address multisigAddress_) external view returns (bool); +} diff --git a/contracts/mock/tokens/ERC20Mock.sol b/contracts/mock/tokens/ERC20Mock.sol deleted file mode 100644 index e0482a7..0000000 --- a/contracts/mock/tokens/ERC20Mock.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract ERC20Mock is ERC20 { - uint8 internal _decimals; - - constructor( - string memory name_, - string memory symbol_, - uint8 decimalPlaces_ - ) ERC20(name_, symbol_) { - _decimals = decimalPlaces_; - } - - function decimals() public view override returns (uint8) { - return _decimals; - } - - function mint(address to_, uint256 amount_) public { - _mint(to_, amount_); - } - - function burn(address to_, uint256 amount_) public { - _burn(to_, amount_); - } -} diff --git a/package-lock.json b/package-lock.json index f204e27..e99de26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,10 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@iden3/contracts": "2.1.2", "@openzeppelin/contracts": "5.0.2", "@openzeppelin/contracts-upgradeable": "5.0.2", - "@solarity/solidity-lib": "2.7.2", + "@solarity/solidity-lib": "2.7.11", "dotenv": "16.4.5", "hardhat": "2.20.1", "typechain": "8.3.2" @@ -998,6 +999,15 @@ "node": ">=14" } }, + "node_modules/@iden3/contracts": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@iden3/contracts/-/contracts-2.1.2.tgz", + "integrity": "sha512-0I+Cmbqn0nNEV4eSg8BqGZSbveY8WokposmwmmKE12gR/IKZAH4MsUHmz8uOTaS2q4bclT3iMEPWu3+vYGg77g==", + "dependencies": { + "@openzeppelin/contracts": "^5.0.2", + "@openzeppelin/contracts-upgradeable": "^5.0.2" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2345,12 +2355,12 @@ } }, "node_modules/@solarity/solidity-lib": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@solarity/solidity-lib/-/solidity-lib-2.7.2.tgz", - "integrity": "sha512-P08xkimAcR5dG2ncqA9OEjmwNwQtJeH036wmkdpp9TDESo2KGPwGwAlcy7RpfTellE8RFDuAvOBj7sedFCkeOg==", + "version": "2.7.11", + "resolved": "https://registry.npmjs.org/@solarity/solidity-lib/-/solidity-lib-2.7.11.tgz", + "integrity": "sha512-iQCh/Rx+ha9TX9+x9OrRUnn0cNkR9m3Dg8aTFnE5WrKBr0rAU/RbnowSMcBl6VsSH3iAo81AOf9Z4p8ZOvhdaQ==", "dependencies": { - "@openzeppelin/contracts": "4.9.5", - "@openzeppelin/contracts-upgradeable": "4.9.5", + "@openzeppelin/contracts": "4.9.6", + "@openzeppelin/contracts-upgradeable": "4.9.6", "@uniswap/v2-core": "1.0.1", "@uniswap/v2-periphery": "1.1.0-beta.0", "@uniswap/v3-core": "1.0.1", @@ -2358,14 +2368,14 @@ } }, "node_modules/@solarity/solidity-lib/node_modules/@openzeppelin/contracts": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.5.tgz", - "integrity": "sha512-ZK+W5mVhRppff9BE6YdR8CC52C8zAvsVAiWhEtQ5+oNxFE6h1WdeWo+FJSF8KKvtxxVYZ7MTP/5KoVpAU3aSWg==" + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.6.tgz", + "integrity": "sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA==" }, "node_modules/@solarity/solidity-lib/node_modules/@openzeppelin/contracts-upgradeable": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.5.tgz", - "integrity": "sha512-f7L1//4sLlflAN7fVzJLoRedrf5Na3Oal5PZfIq55NFcVZ90EpV1q5xOvL4lFvg3MNICSDr2hH0JUBxwlxcoPg==" + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.6.tgz", + "integrity": "sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA==" }, "node_modules/@solidity-parser/parser": { "version": "0.18.0", diff --git a/package.json b/package.json index 1b69cc6..8d82c2c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "dependencies": { "@openzeppelin/contracts": "5.0.2", "@openzeppelin/contracts-upgradeable": "5.0.2", - "@solarity/solidity-lib": "2.7.2", + "@solarity/solidity-lib": "2.7.11", + "@iden3/contracts": "2.1.2", "dotenv": "16.4.5", "hardhat": "2.20.1", "typechain": "8.3.2" From fb19c435f99a6e55574d49532842140421445c29 Mon Sep 17 00:00:00 2001 From: joYyHack Date: Thu, 25 Jul 2024 02:08:48 +0300 Subject: [PATCH 2/8] Adjustments + zk proof verify --- contracts/ZKMultisig.sol | 316 ++++++++++++++------------- contracts/ZKMultisigFactory.sol | 21 +- contracts/interfaces/IZKMultisig.sol | 8 +- contracts/libs/Poseidon.sol | 6 + package-lock.json | 14 +- package.json | 3 +- 6 files changed, 198 insertions(+), 170 deletions(-) create mode 100644 contracts/libs/Poseidon.sol diff --git a/contracts/ZKMultisig.sol b/contracts/ZKMultisig.sol index 19fa4d8..a822940 100644 --- a/contracts/ZKMultisig.sol +++ b/contracts/ZKMultisig.sol @@ -1,97 +1,83 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -import {IZKMultisig} from "./interfaces/IZKMultisig.sol"; - -import {SparseMerkleTree} from "@solarity/solidity-lib/libs/data-structures/SparseMerkleTree.sol"; -import {PRECISION, PERCENTAGE_100} from "@solarity/solidity-lib/utils/Globals.sol"; -import {Paginator} from "@solarity/solidity-lib/libs/arrays/Paginator.sol"; - import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {PoseidonUnit1L} from "@iden3/contracts/lib/Poseidon.sol"; +import {SparseMerkleTree} from "@solarity/solidity-lib/libs/data-structures/SparseMerkleTree.sol"; +import {PRECISION, PERCENTAGE_100} from "@solarity/solidity-lib/utils/Globals.sol"; +import {Paginator} from "@solarity/solidity-lib/libs/arrays/Paginator.sol"; +import {VerifierHelper} from "@solarity/solidity-lib/libs/zkp/snarkjs/VerifierHelper.sol"; + +import {IZKMultisig} from "./interfaces/IZKMultisig.sol"; +import {PoseidonUnit1L} from "./libs/Poseidon.sol"; contract ZKMultisig is UUPSUpgradeable, IZKMultisig { using SparseMerkleTree for SparseMerkleTree.Bytes32SMT; using EnumerableSet for EnumerableSet.Bytes32Set; using EnumerableSet for EnumerableSet.UintSet; using Paginator for EnumerableSet.UintSet; + using VerifierHelper for address; + using Address for address; using Math for uint256; - enum ParticipantsAction { - ADD, - REMOVE + struct ProposalData { + ProposalContent content; + uint256 proposalEndTime; + EnumerableSet.UintSet blinders; + uint256 requiredQuorum; + bool executed; } - uint256 public constant TREE_SIZE = 20; + uint256 public constant PARTICIPANTS_TREE_DEPTH = 20; + uint256 public constant MIN_QUORUM_SIZE = 1; + + address public _participantVerifier; - SparseMerkleTree.Bytes32SMT internal _bytes32Tree; + SparseMerkleTree.Bytes32SMT internal _participantsSMTTree; EnumerableSet.Bytes32Set internal _participants; EnumerableSet.UintSet internal _proposalIds; uint256 private _quorumPercentage; - mapping(uint256 => ProposalInfoView) private _proposals; - mapping(uint256 => uint256) private _blinders; - - event Initialized(uint256 participantsAmount, uint256 quorumPercentage); - event RootUpdated(bytes32 indexed root); - event QuorumPercentageUpdated(uint256 indexed newQuorumPercentage); + mapping(uint256 => ProposalData) private _proposals; modifier onlyThis() { require(msg.sender == address(this), "ZKMultisig: Not authorized call"); _; } - modifier withRootUpdate() { - _; - _notifyRoot(); - } - - modifier withQuorumUpdate() { - _; - _notifyQourumPercentage(); - } - constructor() { _disableInitializers(); } function initialize( uint256[] memory participants_, - uint256 quorumPercentage_ + uint256 quorumPercentage_, + address participantVerifier_ ) external initializer { - __ZKMultisig_init(participants_, quorumPercentage_); - } + require(participantVerifier_ != address(0), "ZKMultisig: Invalid verifier address"); - function __ZKMultisig_init( - uint256[] memory participants_, - uint256 quorumPercentage_ - ) internal { _updateQourumPercentage(quorumPercentage_); - _bytes32Tree.initialize(uint32(TREE_SIZE)); + _participantsSMTTree.initialize(uint32(PARTICIPANTS_TREE_DEPTH)); _addParticipants(participants_); + + _participantVerifier = participantVerifier_; } - function addParticipants( - uint256[] calldata participantsToAdd_ - ) external onlyThis withRootUpdate { + function addParticipants(uint256[] calldata participantsToAdd_) external onlyThis { _addParticipants(participantsToAdd_); } - function removeParticipants( - uint256[] calldata participantsToRemove_ - ) external onlyThis withRootUpdate { + function removeParticipants(uint256[] calldata participantsToRemove_) external onlyThis { _removeParticipants(participantsToRemove_); } - function updateQuorumPercentage( - uint256 newQuorumPercentage_ - ) external onlyThis withQuorumUpdate { + function updateQuorumPercentage(uint256 newQuorumPercentage_) external onlyThis { _updateQourumPercentage(newQuorumPercentage_); } @@ -101,32 +87,37 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { uint256 salt_, ZKParams calldata proofData_ ) external returns (uint256) { + // validate inputs + require(duration_ > 0, "ZKMultisig: Invalid duration"); + require(content_.target != address(0), "ZKMultisig: Invalid target"); + uint256 proposalId_ = _computeProposalId(content_, salt_); + // validate proposal state require( - _proposals[proposalId_].status == ProposalStatus.NONE, + !_proposalIds.contains(proposalId_) && + _getProposalStatus(proposalId_) == ProposalStatus.NONE, "ZKMultisig: Proposal already exists" ); - require(duration_ > 0, "ZKMultisig: Invalid duration"); + // validate zk params + _validateZKParams(proposalId_, proofData_); - uint256 votesCount_ = 1; // 1 vote from creator - uint256 requiredQuorum_ = ((_participants.length() * _quorumPercentage) / PERCENTAGE_100) - .max(1); + ProposalData storage _proposal = _proposals[proposalId_]; + _proposalIds.add(proposalId_); - _proposals[proposalId_] = ProposalInfoView({ - content: content_, - proposalEndTime: block.timestamp + duration_, - status: votesCount_ >= requiredQuorum_ - ? ProposalStatus.ACCEPTED - : ProposalStatus.VOTING, - votesCount: votesCount_, - requiredQuorum: requiredQuorum_ - }); + _proposal.content = content_; + _proposal.proposalEndTime = block.timestamp + duration_; + _proposal.requiredQuorum = ((_participants.length() * _quorumPercentage) / PERCENTAGE_100) + .max(MIN_QUORUM_SIZE); - _proposalIds.add(proposalId_); - // assign proposalId to blinder - _blinders[proofData_.inputs[0]] = proposalId_; + require( + _getProposalStatus(proposalId_) == ProposalStatus.VOTING, + "ZKMultisig: Incorrect proposal voting state after creation" + ); + + // vote on behalf of the creator + _vote(proposalId_, proofData_.inputs[0]); emit ProposalCreated(proposalId_, content_); @@ -134,56 +125,46 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { } function vote(uint256 proposalId_, ZKParams calldata proofData_) external { - ProposalInfoView storage _proposal = _proposals[proposalId_]; - uint256 blinder_ = proofData_.inputs[0]; - require( - _proposal.status == ProposalStatus.VOTING, + _getProposalStatus(proposalId_) == ProposalStatus.VOTING, "ZKMultisig: Proposal is not in voting state" ); - require(block.timestamp < _proposal.proposalEndTime, "ZKMultisig: Proposal expired"); - - require(!_isBlinderVoted(proposalId_, blinder_), "ZKMultisig: Already voted"); + _validateZKParams(proposalId_, proofData_); - _blinders[blinder_] = proposalId_; - - _proposal.votesCount += 1; - - if (_proposal.votesCount >= _proposal.requiredQuorum) { - _proposal.status = ProposalStatus.ACCEPTED; - } + _vote(proposalId_, proofData_.inputs[0]); - emit ProposalVoted(proposalId_, blinder_); + emit ProposalVoted(proposalId_, proofData_.inputs[0]); } - function execute(uint256 proposalId_) external { - ProposalInfoView storage _proposal = _proposals[proposalId_]; - + function execute(uint256 proposalId_) external payable { require( - _proposal.status == ProposalStatus.ACCEPTED, + _getProposalStatus(proposalId_) == ProposalStatus.ACCEPTED, "ZKMultisig: Proposal is not accepted" ); - (bool success, ) = _proposal.content.target.call{value: _proposal.content.value}( - _proposal.content.data - ); + ProposalData storage _proposal = _proposals[proposalId_]; + + require(msg.value == _proposal.content.value, "ZKMultisig: Invalid value"); - require(success, "ZKMultisig: Proposal execution failed"); + _proposal.content.target.functionCallWithValue( + _proposal.content.data, + _proposal.content.value + ); - _proposal.status = ProposalStatus.EXECUTED; + _proposal.executed = true; emit ProposalExecuted(proposalId_); } function getParticipantsSMTRoot() external view returns (bytes32) { - return _bytes32Tree.getRoot(); + return _participantsSMTTree.getRoot(); } function getParticipantsSMTProof( bytes32 publicKeyHash_ ) external view override returns (SparseMerkleTree.Proof memory) { - return _bytes32Tree.getProof(publicKeyHash_); + return _participantsSMTTree.getProof(publicKeyHash_); } function getParticipantsCount() external view returns (uint256) { @@ -210,21 +191,24 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { } function getProposalInfo(uint256 proposalId_) external view returns (ProposalInfoView memory) { - return _proposals[proposalId_]; + ProposalData storage _proposal = _proposals[proposalId_]; + + return + ProposalInfoView({ + content: _proposal.content, + proposalEndTime: _proposal.proposalEndTime, + status: _getProposalStatus(proposalId_), + votesCount: _proposal.blinders.length(), + requiredQuorum: _proposal.requiredQuorum + }); } function getProposalStatus(uint256 proposalId_) external view returns (ProposalStatus) { - return _proposals[proposalId_].status; + return _getProposalStatus(proposalId_); } - // double check doc, bc there uint248(keccak256(abi.encode(block.chainid, address(this), proposalId_))) is used function getProposalChallenge(uint256 proposalId_) external view returns (uint256) { - return - uint256( - PoseidonUnit1L.poseidon( - [uint256(keccak256(abi.encode(block.chainid, address(this), proposalId_)))] - ) - ); + return _getProposalChallenge(proposalId_); } function computeProposalId( @@ -245,45 +229,23 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { function _addParticipants(uint256[] memory participantsToAdd_) internal { require( - _participants.length() + participantsToAdd_.length <= 2 ** TREE_SIZE, + _participants.length() + participantsToAdd_.length <= 2 ** PARTICIPANTS_TREE_DEPTH, "ZKMultisig: Too many participants" ); - _processParticipants(participantsToAdd_, ParticipantsAction.ADD); - // require(participantsToAdd.length > 0, "ZKMultisig: No participants to add"); - // for (uint256 i = 0; i < participantsToAdd.length; i++) { - // uint256 participant_ = participantsToAdd[i]; - // bytes32 participantKey_ = keccak256(abi.encodePacked(participant_)); - // if (_uintTree.getProof(participantKey_).existence) { - // continue; - // // or revert? - // } - // _uintTree.add(participantKey_, participant_); - // } + _processParticipants(participantsToAdd_, true); } function _removeParticipants(uint256[] memory participantsToRemove_) internal { - require( - _participants.length() > participantsToRemove_.length, - "ZKMultisig: Cannot remove all participants" - ); - _processParticipants(participantsToRemove_, ParticipantsAction.REMOVE); + _processParticipants(participantsToRemove_, false); - // require(participantsToRemove.length > 0, "ZKMultisig: No participants to remove"); - // for (uint256 i = 0; i < participantsToRemove.length; i++) { - // uint256 participant_ = participantsToRemove[i]; - // bytes32 participantKey_ = keccak256(abi.encodePacked(participant_)); - // if (_uintTree.getProof(participantKey_).existence) { - // _uintTree.remove(participantKey_); - // // should revert if false? - // } - // } + require(_participants.length() > 0, "ZKMultisig: Cannot remove all participants"); } function _updateQourumPercentage(uint256 newQuorumPercentage_) internal { require( newQuorumPercentage_ > 0 && - newQuorumPercentage_ <= 100 && + newQuorumPercentage_ <= PERCENTAGE_100 && newQuorumPercentage_ != _quorumPercentage, "ZKMultisig: Invalid quorum percentage" ); @@ -291,6 +253,75 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { _quorumPercentage = newQuorumPercentage_; } + // internal vote skipping validation + function _vote(uint256 proposalId_, uint256 blinder_) internal { + ProposalData storage _proposal = _proposals[proposalId_]; + _proposal.blinders.add(blinder_); + } + + function _validateZKParams(uint256 proposalId_, ZKParams calldata proofData_) internal view { + require(proofData_.inputs.length == 3, "ZKMultisig: Invalid proof data"); + + require( + !_isBlinderVoted(proposalId_, proofData_.inputs[0]), + "ZKMultisig: Blinder already voted" + ); + + require( + proofData_.inputs[1] == _getProposalChallenge(proposalId_), + "ZKMultisig: Invalid challenge" + ); + + require( + proofData_.inputs[2] == uint256(_participantsSMTTree.getRoot()), + "ZKMultisig: Invalid SMT root" + ); + + require( + _participantVerifier.verifyProof( + proofData_.inputs, + VerifierHelper.ProofPoints({a: proofData_.a, b: proofData_.b, c: proofData_.c}) + ), + "ZKMultisig: Invalid proof" + ); + } + + function _getProposalStatus(uint256 proposalId_) internal view returns (ProposalStatus) { + ProposalData storage _proposal = _proposals[proposalId_]; + + // Check if the proposal exists by verifying the end time + if (_proposal.proposalEndTime == 0) { + return ProposalStatus.NONE; + } + + // Check if the proposal has been executed + if (_proposal.executed) { + return ProposalStatus.EXECUTED; + } + + // Check if the proposal has met the quorum requirement + if (_proposal.blinders.length() >= _proposal.requiredQuorum) { + return ProposalStatus.ACCEPTED; + } + + // Check if the proposal is still within the voting period + if (_proposal.proposalEndTime > block.timestamp) { + return ProposalStatus.VOTING; + } + + // If the proposal has not met the quorum and the voting period has expired + return ProposalStatus.EXPIRED; + } + + function _getProposalChallenge(uint256 proposalId_) internal view returns (uint256) { + return + uint256( + PoseidonUnit1L.poseidon( + [uint256(keccak256(abi.encode(block.chainid, address(this), proposalId_)))] + ) + ); + } + function _computeProposalId( ProposalContent calldata content_, uint256 salt_ @@ -303,39 +334,26 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { uint256 proposalId_, uint256 blinderToCheck_ ) internal view returns (bool) { - return _blinders[blinderToCheck_] == proposalId_; + return _proposals[proposalId_].blinders.contains(blinderToCheck_); } - function _notifyRoot() internal { - emit RootUpdated(_bytes32Tree.getRoot()); - } - - function _notifyQourumPercentage() internal { - emit QuorumPercentageUpdated(_quorumPercentage); - } - - function _processParticipants( - uint256[] memory participants_, - ParticipantsAction action_ - ) private { + function _processParticipants(uint256[] memory participants_, bool isAdding_) private { require(participants_.length > 0, "Multisig: No participants to process"); for (uint256 i = 0; i < participants_.length; i++) { bytes32 participant_ = bytes32(participants_[i]); bytes32 participantKey_ = keccak256(abi.encodePacked(participant_)); - bool nodeExists = _bytes32Tree.getProof(participantKey_).existence; - - // revert in false case? - if (!nodeExists && action_ == ParticipantsAction.ADD) { - _bytes32Tree.add(participantKey_, participant_); - _participants.add(participant_); - } - - // revert in false case? - if (nodeExists && action_ == ParticipantsAction.REMOVE) { - _bytes32Tree.remove(participantKey_); - _participants.remove(participant_); + if (isAdding_) { + if (!_participants.contains(participant_)) { + _participantsSMTTree.add(participantKey_, participant_); + _participants.add(participant_); + } + } else { + if (_participants.contains(participant_)) { + _participantsSMTTree.remove(participantKey_); + _participants.remove(participant_); + } } } } diff --git a/contracts/ZKMultisigFactory.sol b/contracts/ZKMultisigFactory.sol index 620ff0a..02e8104 100644 --- a/contracts/ZKMultisigFactory.sol +++ b/contracts/ZKMultisigFactory.sol @@ -20,12 +20,19 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { bytes32 private constant KDF_MESSAGE_TYPEHASH = keccak256("KDF(address zkMultisigAddress)"); - string private constant EIP712_NAME = "ZKMultisigFactory"; - string private constant EIP712_VERSION = "1"; - + address private _participantVerifier; address private immutable _zkMulsigImplementation; - constructor(address zkMultisigImplementation_) EIP712(EIP712_NAME, EIP712_VERSION) { + constructor( + address zkMultisigImplementation_, + address participantVerifier_ + ) EIP712("ZKMultisigFactory", "1") { + require( + zkMultisigImplementation_ != address(0) && participantVerifier_ != address(0), + "ZKMultisigFactory: Invalid implementation or verifier address" + ); + + _participantVerifier = participantVerifier_; _zkMulsigImplementation = zkMultisigImplementation_; } @@ -41,7 +48,11 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { ) ); - IZKMultisig(zkMultisigAddress_).initialize(participants_, quorumPercentage_); + IZKMultisig(zkMultisigAddress_).initialize( + participants_, + quorumPercentage_, + _participantVerifier + ); _zkMultisigs.add(zkMultisigAddress_); diff --git a/contracts/interfaces/IZKMultisig.sol b/contracts/interfaces/IZKMultisig.sol index 2e6d7bc..c8b8acb 100644 --- a/contracts/interfaces/IZKMultisig.sol +++ b/contracts/interfaces/IZKMultisig.sol @@ -39,7 +39,11 @@ interface IZKMultisig { event ProposalExecuted(uint256 indexed proposalId); - function initialize(uint256[] memory participants_, uint256 quorumPercentage_) external; + function initialize( + uint256[] memory participants_, + uint256 quorumPercentage_, + address participantVerifier_ + ) external; function addParticipants(uint256[] calldata participantsToAdd) external; @@ -56,7 +60,7 @@ interface IZKMultisig { function vote(uint256 proposalId, ZKParams calldata proofData) external; - function execute(uint256 proposalId) external; + function execute(uint256 proposalId) external payable; function getParticipantsSMTRoot() external view returns (bytes32); diff --git a/contracts/libs/Poseidon.sol b/contracts/libs/Poseidon.sol new file mode 100644 index 0000000..9892599 --- /dev/null +++ b/contracts/libs/Poseidon.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +library PoseidonUnit1L { + function poseidon(uint256[1] calldata) public pure returns (uint256) {} +} diff --git a/package-lock.json b/package-lock.json index e99de26..34cd070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { - "name": "hardhat-template", + "name": "zk-multisig", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "hardhat-template", + "name": "zk-multisig", "version": "1.0.0", "license": "MIT", "dependencies": { - "@iden3/contracts": "2.1.2", "@openzeppelin/contracts": "5.0.2", "@openzeppelin/contracts-upgradeable": "5.0.2", "@solarity/solidity-lib": "2.7.11", @@ -999,15 +998,6 @@ "node": ">=14" } }, - "node_modules/@iden3/contracts": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@iden3/contracts/-/contracts-2.1.2.tgz", - "integrity": "sha512-0I+Cmbqn0nNEV4eSg8BqGZSbveY8WokposmwmmKE12gR/IKZAH4MsUHmz8uOTaS2q4bclT3iMEPWu3+vYGg77g==", - "dependencies": { - "@openzeppelin/contracts": "^5.0.2", - "@openzeppelin/contracts-upgradeable": "^5.0.2" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", diff --git a/package.json b/package.json index 8d82c2c..0284577 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "hardhat-template", + "name": "zk-multisig", "version": "1.0.0", "license": "MIT", "author": "Distributed Lab", @@ -33,7 +33,6 @@ "@openzeppelin/contracts": "5.0.2", "@openzeppelin/contracts-upgradeable": "5.0.2", "@solarity/solidity-lib": "2.7.11", - "@iden3/contracts": "2.1.2", "dotenv": "16.4.5", "hardhat": "2.20.1", "typechain": "8.3.2" From af63687f570672e598f1a4e4b04f103eabec193e Mon Sep 17 00:00:00 2001 From: joYyHack Date: Thu, 25 Jul 2024 23:22:49 +0300 Subject: [PATCH 3/8] Adjustments --- contracts/.gitkeep | 0 contracts/ZKMultisig.sol | 190 ++++++++++++--------------- contracts/ZKMultisigFactory.sol | 31 ++--- contracts/interfaces/IZKMultisig.sol | 10 +- 4 files changed, 103 insertions(+), 128 deletions(-) delete mode 100644 contracts/.gitkeep diff --git a/contracts/.gitkeep b/contracts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/contracts/ZKMultisig.sol b/contracts/ZKMultisig.sol index a822940..80c2796 100644 --- a/contracts/ZKMultisig.sol +++ b/contracts/ZKMultisig.sol @@ -17,29 +17,20 @@ import {IZKMultisig} from "./interfaces/IZKMultisig.sol"; import {PoseidonUnit1L} from "./libs/Poseidon.sol"; contract ZKMultisig is UUPSUpgradeable, IZKMultisig { - using SparseMerkleTree for SparseMerkleTree.Bytes32SMT; - using EnumerableSet for EnumerableSet.Bytes32Set; - using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for *; using Paginator for EnumerableSet.UintSet; + using SparseMerkleTree for SparseMerkleTree.UintSMT; using VerifierHelper for address; using Address for address; using Math for uint256; - struct ProposalData { - ProposalContent content; - uint256 proposalEndTime; - EnumerableSet.UintSet blinders; - uint256 requiredQuorum; - bool executed; - } - uint256 public constant PARTICIPANTS_TREE_DEPTH = 20; uint256 public constant MIN_QUORUM_SIZE = 1; - address public _participantVerifier; + address public participantVerifier; - SparseMerkleTree.Bytes32SMT internal _participantsSMTTree; - EnumerableSet.Bytes32Set internal _participants; + SparseMerkleTree.UintSMT internal _participantsSMT; + EnumerableSet.UintSet internal _participants; EnumerableSet.UintSet internal _proposalIds; uint256 private _quorumPercentage; @@ -60,13 +51,11 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { uint256 quorumPercentage_, address participantVerifier_ ) external initializer { - require(participantVerifier_ != address(0), "ZKMultisig: Invalid verifier address"); + _participantsSMT.initialize(uint32(PARTICIPANTS_TREE_DEPTH)); + _updateParticipantVerifier(participantVerifier_); _updateQourumPercentage(quorumPercentage_); - _participantsSMTTree.initialize(uint32(PARTICIPANTS_TREE_DEPTH)); _addParticipants(participants_); - - _participantVerifier = participantVerifier_; } function addParticipants(uint256[] calldata participantsToAdd_) external onlyThis { @@ -81,6 +70,10 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { _updateQourumPercentage(newQuorumPercentage_); } + function updateParticipantVerifier(address participantVerifier_) external onlyThis { + _updateParticipantVerifier(participantVerifier_); + } + function create( ProposalContent calldata content_, uint256 duration_, @@ -91,12 +84,11 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { require(duration_ > 0, "ZKMultisig: Invalid duration"); require(content_.target != address(0), "ZKMultisig: Invalid target"); - uint256 proposalId_ = _computeProposalId(content_, salt_); + uint256 proposalId_ = computeProposalId(content_, salt_); // validate proposal state require( - !_proposalIds.contains(proposalId_) && - _getProposalStatus(proposalId_) == ProposalStatus.NONE, + getProposalStatus(proposalId_) == ProposalStatus.NONE, "ZKMultisig: Proposal already exists" ); @@ -108,16 +100,14 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { _proposal.content = content_; _proposal.proposalEndTime = block.timestamp + duration_; - _proposal.requiredQuorum = ((_participants.length() * _quorumPercentage) / PERCENTAGE_100) - .max(MIN_QUORUM_SIZE); require( - _getProposalStatus(proposalId_) == ProposalStatus.VOTING, + getProposalStatus(proposalId_) == ProposalStatus.VOTING, "ZKMultisig: Incorrect proposal voting state after creation" ); // vote on behalf of the creator - _vote(proposalId_, proofData_.inputs[0]); + _proposal.blinders.add(proofData_.inputs[0]); emit ProposalCreated(proposalId_, content_); @@ -126,20 +116,21 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { function vote(uint256 proposalId_, ZKParams calldata proofData_) external { require( - _getProposalStatus(proposalId_) == ProposalStatus.VOTING, + getProposalStatus(proposalId_) == ProposalStatus.VOTING, "ZKMultisig: Proposal is not in voting state" ); _validateZKParams(proposalId_, proofData_); - _vote(proposalId_, proofData_.inputs[0]); + ProposalData storage _proposal = _proposals[proposalId_]; + _proposal.blinders.add(proofData_.inputs[0]); emit ProposalVoted(proposalId_, proofData_.inputs[0]); } function execute(uint256 proposalId_) external payable { require( - _getProposalStatus(proposalId_) == ProposalStatus.ACCEPTED, + getProposalStatus(proposalId_) == ProposalStatus.ACCEPTED, "ZKMultisig: Proposal is not accepted" ); @@ -158,20 +149,20 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { } function getParticipantsSMTRoot() external view returns (bytes32) { - return _participantsSMTTree.getRoot(); + return _participantsSMT.getRoot(); } function getParticipantsSMTProof( bytes32 publicKeyHash_ ) external view override returns (SparseMerkleTree.Proof memory) { - return _participantsSMTTree.getProof(publicKeyHash_); + return _participantsSMT.getProof(publicKeyHash_); } function getParticipantsCount() external view returns (uint256) { return _participants.length(); } - function getParticipants() external view returns (bytes32[] memory) { + function getParticipants() external view returns (uint256[] memory) { return _participants.values(); } @@ -197,32 +188,67 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { ProposalInfoView({ content: _proposal.content, proposalEndTime: _proposal.proposalEndTime, - status: _getProposalStatus(proposalId_), + status: getProposalStatus(proposalId_), votesCount: _proposal.blinders.length(), - requiredQuorum: _proposal.requiredQuorum + requiredQuorum: getRequiredQuorum() }); } - function getProposalStatus(uint256 proposalId_) external view returns (ProposalStatus) { - return _getProposalStatus(proposalId_); - } - - function getProposalChallenge(uint256 proposalId_) external view returns (uint256) { - return _getProposalChallenge(proposalId_); - } - function computeProposalId( ProposalContent calldata content_, uint256 salt_ - ) external pure returns (uint256) { - return _computeProposalId(content_, salt_); + ) public pure returns (uint256) { + return + uint256(keccak256(abi.encode(content_.target, content_.value, content_.data, salt_))); + } + + function getProposalChallenge(uint256 proposalId_) public view returns (uint256) { + return + uint256( + PoseidonUnit1L.poseidon( + [uint256(keccak256(abi.encode(block.chainid, address(this), proposalId_)))] + ) + ); } function isBlinderVoted( uint256 proposalId_, uint256 blinderToCheck_ - ) external view returns (bool) { - return _isBlinderVoted(proposalId_, blinderToCheck_); + ) public view returns (bool) { + return _proposals[proposalId_].blinders.contains(blinderToCheck_); + } + + function getProposalStatus(uint256 proposalId_) public view returns (ProposalStatus) { + ProposalData storage _proposal = _proposals[proposalId_]; + + // Check if the proposal exists by verifying the end time + if (_proposal.proposalEndTime == 0) { + return ProposalStatus.NONE; + } + + // Check if the proposal has been executed + if (_proposal.executed) { + return ProposalStatus.EXECUTED; + } + + // Check if the proposal has met the quorum requirement + if (_proposal.blinders.length() >= getRequiredQuorum()) { + return ProposalStatus.ACCEPTED; + } + + // Check if the proposal is still within the voting period + if (_proposal.proposalEndTime > block.timestamp) { + return ProposalStatus.VOTING; + } + + // If the proposal has not met the quorum and the voting period has expired + return ProposalStatus.EXPIRED; + } + + // return the required quorum amount (not percentage) for a given number of participants + function getRequiredQuorum() public view returns (uint256) { + return + ((_participants.length() * _quorumPercentage) / PERCENTAGE_100).max(MIN_QUORUM_SIZE); } function _authorizeUpgrade(address newImplementation_) internal override onlyThis {} @@ -253,32 +279,32 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { _quorumPercentage = newQuorumPercentage_; } - // internal vote skipping validation - function _vote(uint256 proposalId_, uint256 blinder_) internal { - ProposalData storage _proposal = _proposals[proposalId_]; - _proposal.blinders.add(blinder_); + function _updateParticipantVerifier(address participantVerifier_) internal { + require(participantVerifier_ != address(0), "ZKMultisig: Invalid verifier address"); + + participantVerifier = participantVerifier_; } function _validateZKParams(uint256 proposalId_, ZKParams calldata proofData_) internal view { require(proofData_.inputs.length == 3, "ZKMultisig: Invalid proof data"); require( - !_isBlinderVoted(proposalId_, proofData_.inputs[0]), + !isBlinderVoted(proposalId_, proofData_.inputs[0]), "ZKMultisig: Blinder already voted" ); require( - proofData_.inputs[1] == _getProposalChallenge(proposalId_), + proofData_.inputs[1] == getProposalChallenge(proposalId_), "ZKMultisig: Invalid challenge" ); require( - proofData_.inputs[2] == uint256(_participantsSMTTree.getRoot()), + proofData_.inputs[2] == uint256(_participantsSMT.getRoot()), "ZKMultisig: Invalid SMT root" ); require( - _participantVerifier.verifyProof( + participantVerifier.verifyProof( proofData_.inputs, VerifierHelper.ProofPoints({a: proofData_.a, b: proofData_.b, c: proofData_.c}) ), @@ -286,72 +312,20 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { ); } - function _getProposalStatus(uint256 proposalId_) internal view returns (ProposalStatus) { - ProposalData storage _proposal = _proposals[proposalId_]; - - // Check if the proposal exists by verifying the end time - if (_proposal.proposalEndTime == 0) { - return ProposalStatus.NONE; - } - - // Check if the proposal has been executed - if (_proposal.executed) { - return ProposalStatus.EXECUTED; - } - - // Check if the proposal has met the quorum requirement - if (_proposal.blinders.length() >= _proposal.requiredQuorum) { - return ProposalStatus.ACCEPTED; - } - - // Check if the proposal is still within the voting period - if (_proposal.proposalEndTime > block.timestamp) { - return ProposalStatus.VOTING; - } - - // If the proposal has not met the quorum and the voting period has expired - return ProposalStatus.EXPIRED; - } - - function _getProposalChallenge(uint256 proposalId_) internal view returns (uint256) { - return - uint256( - PoseidonUnit1L.poseidon( - [uint256(keccak256(abi.encode(block.chainid, address(this), proposalId_)))] - ) - ); - } - - function _computeProposalId( - ProposalContent calldata content_, - uint256 salt_ - ) internal pure returns (uint256) { - return - uint256(keccak256(abi.encode(content_.target, content_.value, content_.data, salt_))); - } - - function _isBlinderVoted( - uint256 proposalId_, - uint256 blinderToCheck_ - ) internal view returns (bool) { - return _proposals[proposalId_].blinders.contains(blinderToCheck_); - } - function _processParticipants(uint256[] memory participants_, bool isAdding_) private { require(participants_.length > 0, "Multisig: No participants to process"); for (uint256 i = 0; i < participants_.length; i++) { - bytes32 participant_ = bytes32(participants_[i]); - bytes32 participantKey_ = keccak256(abi.encodePacked(participant_)); + uint256 participant_ = participants_[i]; if (isAdding_) { if (!_participants.contains(participant_)) { - _participantsSMTTree.add(participantKey_, participant_); + _participantsSMT.add(bytes32(participant_), participant_); _participants.add(participant_); } } else { if (_participants.contains(participant_)) { - _participantsSMTTree.remove(participantKey_); + _participantsSMT.remove(bytes32(participant_)); _participants.remove(participant_); } } diff --git a/contracts/ZKMultisigFactory.sol b/contracts/ZKMultisigFactory.sol index 02e8104..e33820d 100644 --- a/contracts/ZKMultisigFactory.sol +++ b/contracts/ZKMultisigFactory.sol @@ -1,27 +1,26 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -import {IZKMultisigFactory} from "./interfaces/IZKMultisigFactory.sol"; -import {IZKMultisig} from "./interfaces/IZKMultisig.sol"; - import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; - import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {Paginator} from "@solarity/solidity-lib/libs/arrays/Paginator.sol"; +import {IZKMultisigFactory} from "./interfaces/IZKMultisigFactory.sol"; +import {IZKMultisig} from "./interfaces/IZKMultisig.sol"; + contract ZKMultisigFactory is EIP712, IZKMultisigFactory { using EnumerableSet for EnumerableSet.AddressSet; using Paginator for EnumerableSet.AddressSet; - EnumerableSet.AddressSet private _zkMultisigs; - bytes32 private constant KDF_MESSAGE_TYPEHASH = keccak256("KDF(address zkMultisigAddress)"); - address private _participantVerifier; - address private immutable _zkMulsigImplementation; + EnumerableSet.AddressSet private _zkMultisigs; + + address public immutable PARTICIPANT_VERIFIER; + address public immutable ZK_MULTISIG_IMPL; constructor( address zkMultisigImplementation_, @@ -32,8 +31,8 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { "ZKMultisigFactory: Invalid implementation or verifier address" ); - _participantVerifier = participantVerifier_; - _zkMulsigImplementation = zkMultisigImplementation_; + PARTICIPANT_VERIFIER = participantVerifier_; + ZK_MULTISIG_IMPL = zkMultisigImplementation_; } function createMultisig( @@ -42,16 +41,13 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { uint256 salt_ ) external returns (address) { address zkMultisigAddress_ = address( - new ERC1967Proxy{salt: keccak256(abi.encode(msg.sender, salt_))}( - _zkMulsigImplementation, - "" - ) + new ERC1967Proxy{salt: keccak256(abi.encode(msg.sender, salt_))}(ZK_MULTISIG_IMPL, "") ); IZKMultisig(zkMultisigAddress_).initialize( participants_, quorumPercentage_, - _participantVerifier + PARTICIPANT_VERIFIER ); _zkMultisigs.add(zkMultisigAddress_); @@ -69,10 +65,7 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { Create2.computeAddress( keccak256(abi.encode(deployer_, salt_)), keccak256( - abi.encodePacked( - type(ERC1967Proxy).creationCode, - abi.encode(_zkMulsigImplementation) - ) + abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(ZK_MULTISIG_IMPL)) ) ); } diff --git a/contracts/interfaces/IZKMultisig.sol b/contracts/interfaces/IZKMultisig.sol index c8b8acb..3834332 100644 --- a/contracts/interfaces/IZKMultisig.sol +++ b/contracts/interfaces/IZKMultisig.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {SparseMerkleTree} from "@solarity/solidity-lib/libs/data-structures/SparseMerkleTree.sol"; interface IZKMultisig { @@ -25,6 +26,13 @@ interface IZKMultisig { bytes data; } + struct ProposalData { + ProposalContent content; + uint256 proposalEndTime; + EnumerableSet.UintSet blinders; + bool executed; + } + struct ProposalInfoView { ProposalContent content; uint256 proposalEndTime; @@ -70,7 +78,7 @@ interface IZKMultisig { function getParticipantsCount() external view returns (uint256); - function getParticipants() external view returns (bytes32[] memory); + function getParticipants() external view returns (uint256[] memory); function getProposalsCount() external view returns (uint256); From 8d58691e076964bcff8584436026ecd2f03da440 Mon Sep 17 00:00:00 2001 From: joYyHack Date: Sun, 28 Jul 2024 09:01:42 +0200 Subject: [PATCH 4/8] Rename --- contracts/ZKMultisig.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/ZKMultisig.sol b/contracts/ZKMultisig.sol index 80c2796..684ebd9 100644 --- a/contracts/ZKMultisig.sol +++ b/contracts/ZKMultisig.sol @@ -54,7 +54,7 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { _participantsSMT.initialize(uint32(PARTICIPANTS_TREE_DEPTH)); _updateParticipantVerifier(participantVerifier_); - _updateQourumPercentage(quorumPercentage_); + _updateQuorumPercentage(quorumPercentage_); _addParticipants(participants_); } @@ -67,7 +67,7 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { } function updateQuorumPercentage(uint256 newQuorumPercentage_) external onlyThis { - _updateQourumPercentage(newQuorumPercentage_); + _updateQuorumPercentage(newQuorumPercentage_); } function updateParticipantVerifier(address participantVerifier_) external onlyThis { @@ -268,7 +268,7 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { require(_participants.length() > 0, "ZKMultisig: Cannot remove all participants"); } - function _updateQourumPercentage(uint256 newQuorumPercentage_) internal { + function _updateQuorumPercentage(uint256 newQuorumPercentage_) internal { require( newQuorumPercentage_ > 0 && newQuorumPercentage_ <= PERCENTAGE_100 && From 4c798ac95384d3e1c1ce4e61324784bc9cf69c14 Mon Sep 17 00:00:00 2001 From: joYyHack Date: Tue, 30 Jul 2024 15:41:38 +0200 Subject: [PATCH 5/8] ZKMultisigFactory tests --- contracts/ZKMultisigFactory.sol | 13 +- contracts/interfaces/IZKMultisigFactory.sol | 2 +- contracts/mock/VerifierMock.sol | 24 +++ package-lock.json | 153 ++++++++++++++++++++ package.json | 4 +- test/ZKMultisigFactory.tests.ts | 150 +++++++++++++++++++ test/helpers/index.ts | 3 + test/helpers/poseidon-deploy.ts | 69 +++++++++ test/helpers/poseidon-hash.ts | 44 ++++++ test/mock/tokens/ERC20Mock.test.ts | 56 ------- 10 files changed, 455 insertions(+), 63 deletions(-) create mode 100644 contracts/mock/VerifierMock.sol create mode 100644 test/ZKMultisigFactory.tests.ts create mode 100644 test/helpers/index.ts create mode 100644 test/helpers/poseidon-deploy.ts create mode 100644 test/helpers/poseidon-hash.ts delete mode 100644 test/mock/tokens/ERC20Mock.test.ts diff --git a/contracts/ZKMultisigFactory.sol b/contracts/ZKMultisigFactory.sol index e33820d..e30bcc9 100644 --- a/contracts/ZKMultisigFactory.sol +++ b/contracts/ZKMultisigFactory.sol @@ -15,13 +15,13 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { using EnumerableSet for EnumerableSet.AddressSet; using Paginator for EnumerableSet.AddressSet; - bytes32 private constant KDF_MESSAGE_TYPEHASH = keccak256("KDF(address zkMultisigAddress)"); - - EnumerableSet.AddressSet private _zkMultisigs; + bytes32 public constant KDF_MESSAGE_TYPEHASH = keccak256("KDF(address zkMultisigAddress)"); address public immutable PARTICIPANT_VERIFIER; address public immutable ZK_MULTISIG_IMPL; + EnumerableSet.AddressSet private _zkMultisigs; + constructor( address zkMultisigImplementation_, address participantVerifier_ @@ -52,7 +52,7 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { _zkMultisigs.add(zkMultisigAddress_); - emit ZKMutlisigCreated(zkMultisigAddress_, participants_, quorumPercentage_); + emit ZKMultisigCreated(zkMultisigAddress_, participants_, quorumPercentage_); return zkMultisigAddress_; } @@ -65,7 +65,10 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { Create2.computeAddress( keccak256(abi.encode(deployer_, salt_)), keccak256( - abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(ZK_MULTISIG_IMPL)) + abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode(ZK_MULTISIG_IMPL, "") + ) ) ); } diff --git a/contracts/interfaces/IZKMultisigFactory.sol b/contracts/interfaces/IZKMultisigFactory.sol index b5c9d3e..18dea8d 100644 --- a/contracts/interfaces/IZKMultisigFactory.sol +++ b/contracts/interfaces/IZKMultisigFactory.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.4; interface IZKMultisigFactory { - event ZKMutlisigCreated( + event ZKMultisigCreated( address indexed zkMultisigAddress, uint256[] initialParticipants, uint256 initialQuorumPercentage diff --git a/contracts/mock/VerifierMock.sol b/contracts/mock/VerifierMock.sol new file mode 100644 index 0000000..2613090 --- /dev/null +++ b/contracts/mock/VerifierMock.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +contract PositiveVerifierMock { + function verifyProof( + uint256[2] calldata, + uint256[2][2] calldata, + uint256[2] calldata, + uint256[24] calldata + ) public pure returns (bool) { + return true; + } +} + +contract NegativeVerifierMock { + function verifyProof( + uint256[2] calldata, + uint256[2][2] calldata, + uint256[2] calldata, + uint256[24] calldata + ) public pure returns (bool) { + return false; + } +} diff --git a/package-lock.json b/package-lock.json index 34cd070..5587c20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "typechain": "8.3.2" }, "devDependencies": { + "@iden3/js-crypto": "^1.1.0", "@metamask/eth-sig-util": "^7.0.1", "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-ethers": "^3.0.5", @@ -31,6 +32,7 @@ "@types/node": "^18.16.0", "bignumber.js": "^9.1.2", "chai": "^4.4.1", + "circomlibjs": "^0.1.7", "ethers": "^6.11.1", "hardhat-contract-sizer": "^2.10.0", "hardhat-gas-reporter": "^2.0.2", @@ -998,6 +1000,12 @@ "node": ">=14" } }, + "node_modules/@iden3/js-crypto": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@iden3/js-crypto/-/js-crypto-1.1.0.tgz", + "integrity": "sha512-MbL7OpOxBoCybAPoorxrp+fwjDVESyDe6giIWxErjEIJy0Q2n1DU4VmKh4vDoCyhJx/RdVgT8Dkb59lKwISqsw==", + "dev": true + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3903,6 +3911,12 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4023,6 +4037,47 @@ "readable-stream": "^3.4.0" } }, + "node_modules/blake-hash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/blake-hash/-/blake-hash-2.0.0.tgz", + "integrity": "sha512-Igj8YowDu1PRkRsxZA7NVkdFNxH5rKv5cpLxQ0CVXSIA77pVYwCPRQJ2sMew/oneUpfuYRyjG6r8SmmmnbZb1w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/blake-hash/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true + }, + "node_modules/blake2b": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/blake2b/-/blake2b-2.1.4.tgz", + "integrity": "sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==", + "dev": true, + "dependencies": { + "blake2b-wasm": "^2.4.0", + "nanoassert": "^2.0.0" + } + }, + "node_modules/blake2b-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz", + "integrity": "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==", + "dev": true, + "dependencies": { + "b4a": "^1.0.1", + "nanoassert": "^2.0.0" + } + }, "node_modules/blakejs": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", @@ -4605,6 +4660,66 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/circomlibjs": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/circomlibjs/-/circomlibjs-0.1.7.tgz", + "integrity": "sha512-GRAUoAlKAsiiTa+PA725G9RmEmJJRc8tRFxw/zKktUxlQISGznT4hH4ESvW8FNTsrGg/nNd06sGP/Wlx0LUHVg==", + "dev": true, + "dependencies": { + "blake-hash": "^2.0.0", + "blake2b": "^2.1.3", + "ethers": "^5.5.1", + "ffjavascript": "^0.2.45" + } + }, + "node_modules/circomlibjs/node_modules/ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + }, "node_modules/class-is": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz", @@ -6329,6 +6444,17 @@ "reusify": "^1.0.4" } }, + "node_modules/ffjavascript": { + "version": "0.2.63", + "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.2.63.tgz", + "integrity": "sha512-dBgdsfGks58b66JnUZeZpGxdMIDQ4QsD3VYlRJyFVrKQHb2kJy4R2gufx5oetrTxXPT+aEjg0dOvOLg1N0on4A==", + "dev": true, + "dependencies": { + "wasmbuilder": "0.0.16", + "wasmcurves": "0.2.2", + "web-worker": "1.2.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -9180,6 +9306,12 @@ "integrity": "sha512-9MqxMH/BSJC7dnLsEMPyfN5Dvoo49IsPFYMcHw3Bcfc2kN0lpHRBSzlMSVx4HGyJ7s9B31CyBTVehWJoQ8Ctew==", "dev": true }, + "node_modules/nanoassert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", + "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -13137,6 +13269,21 @@ } } }, + "node_modules/wasmbuilder": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/wasmbuilder/-/wasmbuilder-0.0.16.tgz", + "integrity": "sha512-Qx3lEFqaVvp1cEYW7Bfi+ebRJrOiwz2Ieu7ZG2l7YyeSJIok/reEQCQCuicj/Y32ITIJuGIM9xZQppGx5LrQdA==", + "dev": true + }, + "node_modules/wasmcurves": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/wasmcurves/-/wasmcurves-0.2.2.tgz", + "integrity": "sha512-JRY908NkmKjFl4ytnTu5ED6AwPD+8VJ9oc94kdq7h5bIwbj0L4TDJ69mG+2aLs2SoCmGfqIesMWTEJjtYsoQXQ==", + "dev": true, + "dependencies": { + "wasmbuilder": "0.0.16" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -13146,6 +13293,12 @@ "defaults": "^1.0.3" } }, + "node_modules/web-worker": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==", + "dev": true + }, "node_modules/web3": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.4.tgz", diff --git a/package.json b/package.json index 0284577..bae5500 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "solidity-coverage": "^0.8.11", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.4.3" + "typescript": "^5.4.3", + "circomlibjs": "^0.1.7", + "@iden3/js-crypto": "^1.1.0" } } diff --git a/test/ZKMultisigFactory.tests.ts b/test/ZKMultisigFactory.tests.ts new file mode 100644 index 0000000..5270bd2 --- /dev/null +++ b/test/ZKMultisigFactory.tests.ts @@ -0,0 +1,150 @@ +import { PRECISION, ZERO_ADDR } from "@/scripts/utils/constants"; +import { Reverter } from "@/test/helpers/reverter"; +import { + ERC1967Proxy__factory, + NegativeVerifierMock, + PositiveVerifierMock, + ZKMultisig, + ZKMultisigFactory, +} from "@ethers-v6"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { expect } from "chai"; +import { randomBytes } from "crypto"; +import { AbiCoder, solidityPacked as encodePacked, keccak256, TypedDataDomain } from "ethers"; +import { ethers } from "hardhat"; +import { getPoseidon } from "./helpers"; + +describe("ZKMultisig Factory", () => { + const reverter = new Reverter(); + + let alice: SignerWithAddress; + + let participantVerifier: PositiveVerifierMock | NegativeVerifierMock; + let zkMultisig: ZKMultisig; + let zkMultisigFactory: ZKMultisigFactory; + + const encode = (types: ReadonlyArray, values: ReadonlyArray): string => { + return AbiCoder.defaultAbiCoder().encode(types, values); + }; + + const randomNumber = () => BigInt("0x" + randomBytes(32).toString("hex")); + + before(async () => { + [alice] = await ethers.getSigners(); + + var verifier__factory = await ethers.getContractFactory("PositiveVerifierMock"); + participantVerifier = await verifier__factory.deploy(); + + await participantVerifier.waitForDeployment(); + + var zkMultisig__factory = await ethers.getContractFactory("ZKMultisig", { + libraries: { + PoseidonUnit1L: await (await getPoseidon(1)).getAddress(), + }, + }); + zkMultisig = await zkMultisig__factory.deploy(); + + await zkMultisig.waitForDeployment(); + + var zkMultisigFactory__factory = await ethers.getContractFactory("ZKMultisigFactory"); + zkMultisigFactory = await zkMultisigFactory__factory.deploy( + await zkMultisig.getAddress(), + await participantVerifier.getAddress(), + ); + + await zkMultisigFactory.waitForDeployment(); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe("initial", () => { + it("should set parameters correctly", async () => { + expect(await zkMultisigFactory.ZK_MULTISIG_IMPL()).to.eq(await zkMultisig.getAddress()); + expect(await zkMultisigFactory.PARTICIPANT_VERIFIER()).to.eq(await participantVerifier.getAddress()); + }); + + it("should have correct initial state", async () => { + expect(await zkMultisigFactory.getZKMultisigsCount()).to.be.eq(0); + expect(await zkMultisigFactory.getZKMultisigs(0, 1)).to.be.deep.eq([]); + }); + + it("should revert if contructor parameters are incorrect", async () => { + const factory = await ethers.getContractFactory("ZKMultisigFactory"); + const err = "ZKMultisigFactory: Invalid implementation or verifier address"; + + // deploy multisig factory with zero address + await expect(factory.deploy(ZERO_ADDR, await participantVerifier.getAddress())).to.be.revertedWith(err); + await expect(factory.deploy(await zkMultisig.getAddress(), ZERO_ADDR)).to.be.revertedWith(err); + }); + }); + + describe("KDF message", () => { + it("should return correct KDF messages", async () => { + const domain = { + name: "ZKMultisigFactory", + version: "1", + chainId: (await ethers.provider.getNetwork()).chainId, + verifyingContract: await zkMultisigFactory.getAddress(), + } as TypedDataDomain; + + const types = { KDF: [{ name: "zkMultisigAddress", type: "address" }] }; + + let values = { zkMultisigAddress: await zkMultisig.getAddress() }; + const msgHash = ethers.TypedDataEncoder.hash(domain, types, values); + + values = { zkMultisigAddress: ZERO_ADDR }; + const defaultMsgHash = ethers.TypedDataEncoder.hash(domain, types, values); + + expect(await zkMultisigFactory.getKDFMSGToSign(await zkMultisig.getAddress())).to.be.eq(msgHash); + expect(await zkMultisigFactory.getDefaultKDFMSGToSign()).to.be.eq(defaultMsgHash); + }); + }); + + describe("zkMultisig factory", () => { + it("should correctly calculate address of create2", async () => { + const salt = randomNumber(); + + const multisigAddress = await zkMultisigFactory.computeZKMultisigAddress(alice.address, salt); + + const calculatedAddress = ethers.getCreate2Address( + await zkMultisigFactory.getAddress(), + keccak256(encode(["address", "uint256"], [alice.address, salt])), + keccak256( + encodePacked( + ["bytes", "bytes"], + [ERC1967Proxy__factory.bytecode, encode(["address", "bytes"], [await zkMultisig.getAddress(), "0x"])], + ), + ), + ); + + expect(multisigAddress).to.be.eq(calculatedAddress); + }); + + it("should create zkMultisig contract", async () => { + const salt = randomNumber(); + const multisigAddress = await zkMultisigFactory.computeZKMultisigAddress(alice.address, salt); + + expect(await zkMultisigFactory.isZKMultisig(multisigAddress)).to.be.eq(false); + expect(await zkMultisigFactory.getZKMultisigsCount()).to.be.eq(0); + expect(await zkMultisigFactory.getZKMultisigs(0, 1)).to.be.deep.eq([]); + + // add participants + let participants: bigint[] = []; + for (let i = 0; i < 5; i++) { + participants.push(randomNumber()); + } + + const quorum = BigInt(80) * PRECISION; + + const tx = zkMultisigFactory.connect(alice).createMultisig(participants, quorum, salt); + + await expect(tx).to.emit(zkMultisigFactory, "ZKMultisigCreated").withArgs(multisigAddress, participants, quorum); + + expect(await zkMultisigFactory.isZKMultisig(multisigAddress)).to.be.eq(true); + expect(await zkMultisigFactory.getZKMultisigsCount()).to.be.eq(1); + expect(await zkMultisigFactory.getZKMultisigs(0, 1)).to.be.deep.eq([multisigAddress]); + }); + }); +}); diff --git a/test/helpers/index.ts b/test/helpers/index.ts new file mode 100644 index 0000000..36e8c4e --- /dev/null +++ b/test/helpers/index.ts @@ -0,0 +1,3 @@ +export * from "./reverter"; +export * from "./poseidon-hash"; +export * from "./poseidon-deploy"; diff --git a/test/helpers/poseidon-deploy.ts b/test/helpers/poseidon-deploy.ts new file mode 100644 index 0000000..16023d8 --- /dev/null +++ b/test/helpers/poseidon-deploy.ts @@ -0,0 +1,69 @@ +import { ethers } from "hardhat"; + +const { poseidonContract } = require("circomlibjs"); + +export async function deployPoseidons(deployer: any, poseidonSizeParams: number[], isLog: boolean = true) { + poseidonSizeParams.forEach((size) => { + if (![1, 2, 3, 4, 5, 6].includes(size)) { + throw new Error(`Poseidon should be integer in a range 1..6. Poseidon size provided: ${size}`); + } + }); + + const deployPoseidon = async (params: number, isLog: boolean) => { + const abi = poseidonContract.generateABI(params); + const code = poseidonContract.createCode(params); + + const PoseidonElements = new ethers.ContractFactory(abi, code, deployer); + const poseidonElements = await PoseidonElements.deploy(); + + if (isLog) { + console.log(`Poseidon${params}Elements deployed to:`, await poseidonElements.getAddress()); + } + + return poseidonElements; + }; + + const result = []; + + for (const size of poseidonSizeParams) { + result.push(await deployPoseidon(size, isLog)); + } + + return result; +} + +export async function deployPoseidonFacade() { + const poseidonContracts = await deployPoseidons( + (await ethers.getSigners())[0], + new Array(6).fill(6).map((_, i) => i + 1), + false, + ); + + const SpongePoseidonFactory = await ethers.getContractFactory("SpongePoseidon", { + libraries: { + PoseidonUnit6L: await poseidonContracts[5].getAddress(), + }, + }); + + const spongePoseidon = await SpongePoseidonFactory.deploy(); + + const PoseidonFacadeFactory = await ethers.getContractFactory("PoseidonFacade", { + libraries: { + PoseidonUnit1L: await poseidonContracts[0].getAddress(), + PoseidonUnit2L: await poseidonContracts[1].getAddress(), + PoseidonUnit3L: await poseidonContracts[2].getAddress(), + PoseidonUnit4L: await poseidonContracts[3].getAddress(), + PoseidonUnit5L: await poseidonContracts[4].getAddress(), + PoseidonUnit6L: await poseidonContracts[5].getAddress(), + SpongePoseidon: await spongePoseidon.getAddress(), + }, + }); + + const poseidonFacade = await PoseidonFacadeFactory.deploy(); + + return { + poseidonContracts, + spongePoseidon, + poseidonFacade, + }; +} diff --git a/test/helpers/poseidon-hash.ts b/test/helpers/poseidon-hash.ts new file mode 100644 index 0000000..12deff4 --- /dev/null +++ b/test/helpers/poseidon-hash.ts @@ -0,0 +1,44 @@ +import { BaseContract } from "ethers"; +import { ethers } from "hardhat"; + +// @ts-ignore +import { poseidonContract } from "circomlibjs"; + +import { Poseidon } from "@iden3/js-crypto"; + +export async function getPoseidon(num: number): Promise { + if (num < 1 || num > 6) { + throw new Error("Poseidon Hash: Invalid number"); + } + + const [deployer] = await ethers.getSigners(); + const PoseidonHasher = new ethers.ContractFactory( + poseidonContract.generateABI(num), + poseidonContract.createCode(num), + deployer, + ); + const poseidonHasher = await PoseidonHasher.deploy(); + await poseidonHasher.waitForDeployment(); + + return poseidonHasher; +} + +export function poseidonHash(data: string): string { + data = ethers.hexlify(data); + + const chunks = splitHexIntoChunks(data.replace("0x", ""), 64); + const inputs = chunks.map((v) => BigInt(v)); + + return ethers.toBeHex(Poseidon.hash(inputs), 32); +} + +function splitHexIntoChunks(hexString: string, chunkSize = 64) { + const regex = new RegExp(`.{1,${chunkSize}}`, "g"); + const chunks = hexString.match(regex); + + if (!chunks) { + throw new Error("Invalid hex string"); + } + + return chunks.map((chunk) => "0x" + chunk); +} diff --git a/test/mock/tokens/ERC20Mock.test.ts b/test/mock/tokens/ERC20Mock.test.ts deleted file mode 100644 index e58c53c..0000000 --- a/test/mock/tokens/ERC20Mock.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ethers } from "hardhat"; -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { expect } from "chai"; -import { Reverter } from "@/test/helpers/reverter"; -import { wei } from "@/scripts/utils/utils"; -import { ERC20Mock } from "@ethers-v6"; - -describe("ERC20Mock", () => { - const reverter = new Reverter(); - - let OWNER: SignerWithAddress; - let SECOND: SignerWithAddress; - - let erc20: ERC20Mock; - - before(async () => { - [OWNER, SECOND] = await ethers.getSigners(); - - const ERC20Mock = await ethers.getContractFactory("ERC20Mock"); - erc20 = await ERC20Mock.deploy("Mock", "Mock", 18); - - await reverter.snapshot(); - }); - - afterEach(reverter.revert); - - describe("#constructor", () => { - it("should set parameters correctly", async () => { - expect(await erc20.name()).to.eq("Mock"); - expect(await erc20.symbol()).to.eq("Mock"); - expect(await erc20.decimals()).to.eq(18); - }); - }); - - describe("#mint", () => { - it("should mint correctly", async () => { - expect(await erc20.balanceOf(SECOND.address)).to.eq(0); - - const tx = erc20.mint(SECOND.address, wei(1000)); - - await expect(tx).to.changeTokenBalance(erc20, SECOND, wei(1000)); - }); - }); - - describe("#burn", () => { - it("should burn correctly", async () => { - expect(await erc20.balanceOf(SECOND.address)).to.eq(0); - - await erc20.mint(SECOND.address, wei(1000)); - - const tx = erc20.burn(SECOND.address, wei("0.5")); - - await expect(tx).to.changeTokenBalance(erc20, SECOND, wei("-0.5")); - }); - }); -}); From 726bf7eedfef2310bd587609db36b2fc587f04d5 Mon Sep 17 00:00:00 2001 From: joYyHack Date: Tue, 30 Jul 2024 15:54:35 +0200 Subject: [PATCH 6/8] Slight optimizations --- contracts/ZKMultisig.sol | 43 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/contracts/ZKMultisig.sol b/contracts/ZKMultisig.sol index 684ebd9..f7002af 100644 --- a/contracts/ZKMultisig.sol +++ b/contracts/ZKMultisig.sol @@ -38,7 +38,7 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { mapping(uint256 => ProposalData) private _proposals; modifier onlyThis() { - require(msg.sender == address(this), "ZKMultisig: Not authorized call"); + _validateMsgSender(); _; } @@ -92,22 +92,16 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { "ZKMultisig: Proposal already exists" ); - // validate zk params - _validateZKParams(proposalId_, proofData_); - + // create proposal ProposalData storage _proposal = _proposals[proposalId_]; _proposalIds.add(proposalId_); _proposal.content = content_; _proposal.proposalEndTime = block.timestamp + duration_; - require( - getProposalStatus(proposalId_) == ProposalStatus.VOTING, - "ZKMultisig: Incorrect proposal voting state after creation" - ); - // vote on behalf of the creator - _proposal.blinders.add(proofData_.inputs[0]); + // zk validation is processed inside _vote + _vote(proposalId_, proofData_); emit ProposalCreated(proposalId_, content_); @@ -115,17 +109,7 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { } function vote(uint256 proposalId_, ZKParams calldata proofData_) external { - require( - getProposalStatus(proposalId_) == ProposalStatus.VOTING, - "ZKMultisig: Proposal is not in voting state" - ); - - _validateZKParams(proposalId_, proofData_); - - ProposalData storage _proposal = _proposals[proposalId_]; - _proposal.blinders.add(proofData_.inputs[0]); - - emit ProposalVoted(proposalId_, proofData_.inputs[0]); + _vote(proposalId_, proofData_); } function execute(uint256 proposalId_) external payable { @@ -285,6 +269,19 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { participantVerifier = participantVerifier_; } + function _vote(uint256 proposalId_, ZKParams calldata proofData_) internal { + require( + getProposalStatus(proposalId_) == ProposalStatus.VOTING, + "ZKMultisig: Proposal is not in voting state" + ); + + _validateZKParams(proposalId_, proofData_); + + _proposals[proposalId_].blinders.add(proofData_.inputs[0]); + + emit ProposalVoted(proposalId_, proofData_.inputs[0]); + } + function _validateZKParams(uint256 proposalId_, ZKParams calldata proofData_) internal view { require(proofData_.inputs.length == 3, "ZKMultisig: Invalid proof data"); @@ -331,4 +328,8 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { } } } + + function _validateMsgSender() private view { + require(msg.sender == address(this), "ZKMultisig: Not authorized call"); + } } From 5142f62134a4e96e6b1692ba260369144061db66 Mon Sep 17 00:00:00 2001 From: joYyHack Date: Tue, 20 Aug 2024 13:14:16 +0300 Subject: [PATCH 7/8] Multisig tests + slight adjustments --- contracts/ZKMultisig.sol | 17 +- contracts/ZKMultisigFactory.sol | 6 +- contracts/mock/VerifierMock.sol | 4 +- test/ZKMultisig.tests.ts | 389 ++++++++++++++++++++++++++++++++ 4 files changed, 411 insertions(+), 5 deletions(-) create mode 100644 test/ZKMultisig.tests.ts diff --git a/contracts/ZKMultisig.sol b/contracts/ZKMultisig.sol index f7002af..0977867 100644 --- a/contracts/ZKMultisig.sol +++ b/contracts/ZKMultisig.sol @@ -190,7 +190,17 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { return uint256( PoseidonUnit1L.poseidon( - [uint256(keccak256(abi.encode(block.chainid, address(this), proposalId_)))] + [ + uint256( + uint248( + uint256( + keccak256( + abi.encode(block.chainid, address(this), proposalId_) + ) + ) + ) + ) + ] ) ); } @@ -265,6 +275,11 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { function _updateParticipantVerifier(address participantVerifier_) internal { require(participantVerifier_ != address(0), "ZKMultisig: Invalid verifier address"); + require( + participantVerifier_ != participantVerifier, + "ZKMultisig: The same verifier address" + ); + require(participantVerifier_.code.length > 0, "ZKMultisig: Not a contract"); participantVerifier = participantVerifier_; } diff --git a/contracts/ZKMultisigFactory.sol b/contracts/ZKMultisigFactory.sol index e30bcc9..a95116d 100644 --- a/contracts/ZKMultisigFactory.sol +++ b/contracts/ZKMultisigFactory.sol @@ -27,7 +27,10 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { address participantVerifier_ ) EIP712("ZKMultisigFactory", "1") { require( - zkMultisigImplementation_ != address(0) && participantVerifier_ != address(0), + zkMultisigImplementation_ != address(0) && + zkMultisigImplementation_.code.length > 0 && + participantVerifier_ != address(0) && + participantVerifier_.code.length > 0, "ZKMultisigFactory: Invalid implementation or verifier address" ); @@ -43,7 +46,6 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { address zkMultisigAddress_ = address( new ERC1967Proxy{salt: keccak256(abi.encode(msg.sender, salt_))}(ZK_MULTISIG_IMPL, "") ); - IZKMultisig(zkMultisigAddress_).initialize( participants_, quorumPercentage_, diff --git a/contracts/mock/VerifierMock.sol b/contracts/mock/VerifierMock.sol index 2613090..ec0a8f1 100644 --- a/contracts/mock/VerifierMock.sol +++ b/contracts/mock/VerifierMock.sol @@ -6,7 +6,7 @@ contract PositiveVerifierMock { uint256[2] calldata, uint256[2][2] calldata, uint256[2] calldata, - uint256[24] calldata + uint256[3] calldata ) public pure returns (bool) { return true; } @@ -17,7 +17,7 @@ contract NegativeVerifierMock { uint256[2] calldata, uint256[2][2] calldata, uint256[2] calldata, - uint256[24] calldata + uint256[3] calldata ) public pure returns (bool) { return false; } diff --git a/test/ZKMultisig.tests.ts b/test/ZKMultisig.tests.ts new file mode 100644 index 0000000..a425967 --- /dev/null +++ b/test/ZKMultisig.tests.ts @@ -0,0 +1,389 @@ +import { PRECISION, ZERO_ADDR } from "@/scripts/utils/constants"; +import { Reverter } from "@/test/helpers/reverter"; +import { + IZKMultisig, + NegativeVerifierMock, + PositiveVerifierMock, + ZKMultisig, + ZKMultisig__factory, + ZKMultisigFactory, +} from "@ethers-v6"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { expect } from "chai"; +import { randomBytes } from "crypto"; +import { AbiCoder, BytesLike } from "ethers"; +import { ethers } from "hardhat"; +import { getPoseidon } from "./helpers"; + +type ZKParams = IZKMultisig.ZKParamsStruct; +type ProposalContent = IZKMultisig.ProposalContentStruct; + +enum ProposalStatus { + NONE, + VOTING, + ACCEPTED, + EXPIRED, + EXECUTED, +} + +describe("ZKMultisig", () => { + const reverter = new Reverter(); + const randomNumber = () => BigInt("0x" + randomBytes(32).toString("hex")); + + const MIN_QUORUM = BigInt(80) * PRECISION; + const DAY_IN_SECONDS = 60 * 60 * 24; + + let alice: SignerWithAddress; + + let positiveParticipantVerifier: PositiveVerifierMock; + let negativeParticipantVerifier: NegativeVerifierMock; + + let zkMultisig: ZKMultisig; + let zkMultisigFactory: ZKMultisigFactory; + + let initialParticipants: bigint[]; + + let zkParams: ZKParams; + + let proposalContent: ProposalContent; + + const encode = (types: ReadonlyArray, values: ReadonlyArray): string => { + return AbiCoder.defaultAbiCoder().encode(types, values); + }; + + const generateParticipants = (length: number) => { + const participants: bigint[] = []; + for (let i = 0; i < length; i++) { + participants.push(randomNumber()); + } + + return participants; + }; + + before(async () => { + [alice] = await ethers.getSigners(); + + const positiveVerifier__factory = await ethers.getContractFactory("PositiveVerifierMock"); + positiveParticipantVerifier = await positiveVerifier__factory.deploy(); + + await positiveParticipantVerifier.waitForDeployment(); + + const negativeVerifier__factory = await ethers.getContractFactory("NegativeVerifierMock"); + negativeParticipantVerifier = await negativeVerifier__factory.deploy(); + + await negativeParticipantVerifier.waitForDeployment(); + + const zkMultisig__factory = await ethers.getContractFactory("ZKMultisig", { + libraries: { + PoseidonUnit1L: await (await getPoseidon(1)).getAddress(), + }, + }); + const zkMultisigImpl = await zkMultisig__factory.deploy(); + + await zkMultisigImpl.waitForDeployment(); + + var zkMultisigFactory__factory = await ethers.getContractFactory("ZKMultisigFactory"); + zkMultisigFactory = await zkMultisigFactory__factory.deploy( + await zkMultisigImpl.getAddress(), + await positiveParticipantVerifier.getAddress(), + ); + + await zkMultisigFactory.waitForDeployment(); + + const salt = randomNumber(); + initialParticipants = generateParticipants(5); + + // create multisig + await zkMultisigFactory.connect(alice).createMultisig(initialParticipants, MIN_QUORUM, salt); + // get deployed proxy + const address = await zkMultisigFactory.computeZKMultisigAddress(alice.address, salt); + // attach proxy address to zkMultisig + zkMultisig = zkMultisigImpl.attach(address) as ZKMultisig; + + // default proposal content + proposalContent = { + target: await zkMultisig.getAddress(), + value: 0, + data: "0x", + }; + + // default zk params + zkParams = { + a: [randomNumber(), randomNumber()], + b: [ + [randomNumber(), randomNumber()], + [randomNumber(), randomNumber()], + ], + c: [randomNumber(), randomNumber()], + inputs: [randomNumber(), randomNumber(), randomNumber()], + }; + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe("initial", async () => { + it("should have correct initial state", async () => { + expect(await zkMultisig.getParticipantsSMTRoot()).to.be.ok; + + expect(await zkMultisig.getParticipantsCount()).to.be.eq(initialParticipants.length); + expect(await zkMultisig.getParticipants()).to.be.deep.eq(initialParticipants); + + expect((await zkMultisig.getParticipantsSMTProof(ethers.toBeHex(initialParticipants[0]))).existence).to.be.true; + expect((await zkMultisig.getParticipantsSMTProof(ethers.toBeHex(randomNumber()))).existence).to.be.false; + + expect(await zkMultisig.getProposalsCount()).to.be.eq(0); + expect(await zkMultisig.getProposalsIds(0, 10)).to.be.deep.eq([]); + + expect(await zkMultisig.getQuorumPercentage()).to.be.eq(MIN_QUORUM); + }); + + it("should not allow to initialize twice", async () => { + await expect( + zkMultisig.initialize(initialParticipants, MIN_QUORUM, positiveParticipantVerifier), + ).to.be.revertedWithCustomError({ interface: ZKMultisig__factory.createInterface() }, "InvalidInitialization"); + }); + + it("should not allow to call proposals functions directly", async () => { + await expect(zkMultisig.addParticipants(generateParticipants(3))).to.be.revertedWith( + "ZKMultisig: Not authorized call", + ); + + await expect(zkMultisig.removeParticipants(generateParticipants(3))).to.be.revertedWith( + "ZKMultisig: Not authorized call", + ); + + await expect(zkMultisig.updateQuorumPercentage(MIN_QUORUM)).to.be.revertedWith("ZKMultisig: Not authorized call"); + + await expect(zkMultisig.updateParticipantVerifier(ZERO_ADDR)).to.be.revertedWith( + "ZKMultisig: Not authorized call", + ); + }); + }); + + describe("proposal flow", async () => { + const createProposal = async (data: BytesLike): Promise<{ proposalId: bigint; zkParams: ZKParams }> => { + proposalContent.data = data; + const salt = randomNumber(); + + // blinder + zkParams.inputs[0] = randomNumber(); + // challange + const proposalId = await zkMultisig.computeProposalId(proposalContent, salt); + zkParams.inputs[1] = await zkMultisig.getProposalChallenge(proposalId); + // root + zkParams.inputs[2] = await zkMultisig.getParticipantsSMTRoot(); + + const tx = await zkMultisig.create(proposalContent, DAY_IN_SECONDS, salt, zkParams); + + expect(tx).to.emit(zkMultisigFactory, "ZKMultisigCreated").withArgs(proposalId, proposalContent); + expect(tx).to.emit(zkMultisig, "ProposalCreated").withArgs(proposalId, proposalContent); + expect(tx).to.emit(zkMultisig, "ProposalVoted").withArgs(proposalId, zkParams.inputs[0]); + + expect(await zkMultisig.getProposalsCount()).to.be.eq(1); + expect(await zkMultisig.getProposalsIds(0, 10)).to.be.deep.eq([proposalId]); + + expect(await zkMultisig.getProposalStatus(proposalId)).to.be.eq(BigInt(ProposalStatus.VOTING)); + + return { proposalId, zkParams }; + }; + + const vote = async (proposalId: bigint, zkParams: ZKParams) => { + const tx = await zkMultisig.vote(proposalId, zkParams); + + expect(tx).to.emit(zkMultisig, "ProposalVoted").withArgs(proposalId, zkParams.inputs[0]); + expect(await zkMultisig.isBlinderVoted(proposalId, zkParams.inputs[0])).to.be.true; + expect(await zkMultisig.getProposalStatus(proposalId)).to.be.oneOf([ + BigInt(ProposalStatus.VOTING), + BigInt(ProposalStatus.ACCEPTED), + ]); + }; + + const execute = async (proposalId: bigint) => { + const tx = await zkMultisig.execute(proposalId); + + expect(tx).to.emit(zkMultisig, "ProposalExecuted").withArgs(proposalId); + expect(await zkMultisig.getProposalStatus(proposalId)).to.be.eq(BigInt(ProposalStatus.EXECUTED)); + }; + + describe("add particpants", async () => { + it("create", async () => { + const newParticipants = generateParticipants(2); + const data = ZKMultisig__factory.createInterface().encodeFunctionData("addParticipants(uint256[])", [ + newParticipants, + ]); + + await createProposal(data); + }); + + it("vote", async () => { + const newParticipants = generateParticipants(2); + const data = ZKMultisig__factory.createInterface().encodeFunctionData("addParticipants(uint256[])", [ + newParticipants, + ]); + + const { proposalId, zkParams } = await createProposal(data); + + //update blinder as af + zkParams.inputs[0] = randomNumber(); + await vote(proposalId, zkParams); + }); + + it("execute", async () => { + const newParticipants = generateParticipants(2); + const data = ZKMultisig__factory.createInterface().encodeFunctionData("addParticipants(uint256[])", [ + newParticipants, + ]); + + const { proposalId, zkParams } = await createProposal(data); + + while ((await zkMultisig.getProposalStatus(proposalId)) != BigInt(ProposalStatus.ACCEPTED)) { + //update blinder as af + zkParams.inputs[0] = randomNumber(); + await vote(proposalId, zkParams); + } + + await execute(proposalId); + + expect(await zkMultisig.getParticipantsCount()).to.be.eq(initialParticipants.length + newParticipants.length); + expect(await zkMultisig.getParticipants()).to.be.deep.eq([...initialParticipants, ...newParticipants]); + + expect((await zkMultisig.getParticipantsSMTProof(ethers.toBeHex(newParticipants[0]))).existence).to.be.true; + expect((await zkMultisig.getParticipantsSMTProof(ethers.toBeHex(newParticipants[1]))).existence).to.be.true; + }); + }); + + describe("remove particpants", async () => { + it("create", async () => { + const participantsToDelete = (await zkMultisig.getParticipants()).slice(0, 2); + const data = ZKMultisig__factory.createInterface().encodeFunctionData("removeParticipants(uint256[])", [ + participantsToDelete, + ]); + + await createProposal(data); + }); + + it("vote", async () => { + const participantsToDelete = (await zkMultisig.getParticipants()).slice(0, 2); + const data = ZKMultisig__factory.createInterface().encodeFunctionData("removeParticipants(uint256[])", [ + participantsToDelete, + ]); + + const { proposalId, zkParams } = await createProposal(data); + + //update blinder as af + zkParams.inputs[0] = randomNumber(); + await vote(proposalId, zkParams); + }); + + it("execute", async () => { + const participantsToDelete = (await zkMultisig.getParticipants()).slice(0, 2); + const data = ZKMultisig__factory.createInterface().encodeFunctionData("removeParticipants(uint256[])", [ + participantsToDelete, + ]); + + const { proposalId, zkParams } = await createProposal(data); + + while ((await zkMultisig.getProposalStatus(proposalId)) != BigInt(ProposalStatus.ACCEPTED)) { + //update blinder as af + zkParams.inputs[0] = randomNumber(); + await vote(proposalId, zkParams); + } + + await execute(proposalId); + + expect(await zkMultisig.getParticipantsCount()).to.be.eq( + initialParticipants.length - participantsToDelete.length, + ); + + expect((await zkMultisig.getParticipantsSMTProof(ethers.toBeHex(initialParticipants[0]))).existence).to.be + .false; + expect((await zkMultisig.getParticipantsSMTProof(ethers.toBeHex(initialParticipants[4]))).existence).to.be.true; + }); + }); + + describe("update quorum percentage", async () => { + it("create", async () => { + const newQuorum = MIN_QUORUM + BigInt(10) * PRECISION; + const data = ZKMultisig__factory.createInterface().encodeFunctionData("updateQuorumPercentage(uint256)", [ + newQuorum, + ]); + + await createProposal(data); + }); + + it("vote", async () => { + const newQuorum = MIN_QUORUM + BigInt(10) * PRECISION; + const data = ZKMultisig__factory.createInterface().encodeFunctionData("updateQuorumPercentage(uint256)", [ + newQuorum, + ]); + + const { proposalId, zkParams } = await createProposal(data); + + //update blinder as af + zkParams.inputs[0] = randomNumber(); + await vote(proposalId, zkParams); + }); + + it("execute", async () => { + const newQuorum = MIN_QUORUM + BigInt(10) * PRECISION; + const data = ZKMultisig__factory.createInterface().encodeFunctionData("updateQuorumPercentage(uint256)", [ + newQuorum, + ]); + + const { proposalId, zkParams } = await createProposal(data); + + while ((await zkMultisig.getProposalStatus(proposalId)) != BigInt(ProposalStatus.ACCEPTED)) { + //update blinder as af + zkParams.inputs[0] = randomNumber(); + await vote(proposalId, zkParams); + } + + await execute(proposalId); + + expect(await zkMultisig.getQuorumPercentage()).to.be.eq(newQuorum); + }); + }); + + describe("update participant verifier", async () => { + it("create", async () => { + const data = ZKMultisig__factory.createInterface().encodeFunctionData("updateParticipantVerifier(address)", [ + await negativeParticipantVerifier.getAddress(), + ]); + + await createProposal(data); + }); + + it("vote", async () => { + const data = ZKMultisig__factory.createInterface().encodeFunctionData("updateParticipantVerifier(address)", [ + await negativeParticipantVerifier.getAddress(), + ]); + + const { proposalId, zkParams } = await createProposal(data); + + //update blinder as af + zkParams.inputs[0] = randomNumber(); + await vote(proposalId, zkParams); + }); + + it("execute", async () => { + const data = ZKMultisig__factory.createInterface().encodeFunctionData("updateParticipantVerifier(address)", [ + await negativeParticipantVerifier.getAddress(), + ]); + + const { proposalId, zkParams } = await createProposal(data); + + while ((await zkMultisig.getProposalStatus(proposalId)) != BigInt(ProposalStatus.ACCEPTED)) { + //update blinder as af + zkParams.inputs[0] = randomNumber(); + await vote(proposalId, zkParams); + } + + await execute(proposalId); + + expect(await zkMultisig.participantVerifier()).to.be.eq(await negativeParticipantVerifier.getAddress()); + }); + }); + }); +}); From 83933b1ebb5beaa104f284c23f9048feaf226554 Mon Sep 17 00:00:00 2001 From: joYyHack Date: Mon, 26 Aug 2024 09:57:38 +0300 Subject: [PATCH 8/8] Minor adjustments --- README.md | 69 ++++++--------------------------- contracts/ZKMultisig.sol | 20 ++++------ contracts/ZKMultisigFactory.sol | 5 +-- test/ZKMultisig.tests.ts | 2 +- test/ZKMultisigFactory.tests.ts | 6 +-- 5 files changed, 25 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 547eda4..7281f47 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,18 @@ -# Hardhat template +# Basic Implementation of ZK Multisig Smart Contracts -Template hardhat repository for ad-hoc smart contracts development. +This project consists of a basic implementation of ZK (Zero-Knowledge) multisig smart contracts. -### How to use +The contracts are divided into two parts: -The template works out of the box. To clean up the repo, you may need to delete the mock contracts, tests and migration files. +- **ZK Multisig Factory** - Manages and deploys multisig contracts. +- **ZK Multisig** - The implementation of the multisig contract itself. -#### Compilation +For more details, refer to the documentation: [Link to Documentation] -To compile the contracts, use the next script: +## Steps to Build the Project -```bash -npm run compile -``` - -#### Test - -To run the tests, execute the following command: - -```bash -npm run test -``` - -Or to see the coverage, run: - -```bash -npm run coverage -``` - -#### Local deployment - -To deploy the contracts locally, run the following commands (in the different terminals): - -```bash -npm run private-network -npm run deploy-localhost -``` - -#### Bindings - -The command to generate the bindings is as follows: - -```bash -npm run generate-types -``` - -> See the full list of available commands in the `package.json` file. - -### Integrated plugins - -- Hardhat official `ethers` + `ethers-v6` -- [`Typechain`](https://www.npmjs.com/package/@typechain/hardhat) -- [`hardhat-migrate`](https://www.npmjs.com/package/@solarity/hardhat-migrate), [`hardhat-markup`](https://www.npmjs.com/package/@solarity/hardhat-markup), [`hardhat-gobind`](https://www.npmjs.com/package/@solarity/hardhat-gobind) -- [`hardhat-contract-sizer`](https://www.npmjs.com/package/hardhat-contract-sizer) -- [`hardhat-gas-reporter`](https://www.npmjs.com/package/hardhat-gas-reporter) -- [`solidity-coverage`](https://www.npmjs.com/package/solidity-coverage) - -### Other niceties - -- The template comes with presetup `prettier` and `solhint` that lint the project via `husky` before compilation hook. -- The `.env.example` file is provided to check what is required as ENVs -- Preinstalled `@openzeppelin/contracts` and `@solarity/solidity-lib` +1. Compile the contracts: + ```bash + npm run compile + npm run test + ``` diff --git a/contracts/ZKMultisig.sol b/contracts/ZKMultisig.sol index 0977867..e1cc0ab 100644 --- a/contracts/ZKMultisig.sol +++ b/contracts/ZKMultisig.sol @@ -188,20 +188,16 @@ contract ZKMultisig is UUPSUpgradeable, IZKMultisig { function getProposalChallenge(uint256 proposalId_) public view returns (uint256) { return - uint256( - PoseidonUnit1L.poseidon( - [ - uint256( - uint248( - uint256( - keccak256( - abi.encode(block.chainid, address(this), proposalId_) - ) - ) + PoseidonUnit1L.poseidon( + [ + uint256( + uint248( + uint256( + keccak256(abi.encode(block.chainid, address(this), proposalId_)) ) ) - ] - ) + ) + ] ); } diff --git a/contracts/ZKMultisigFactory.sol b/contracts/ZKMultisigFactory.sol index a95116d..5d1baf3 100644 --- a/contracts/ZKMultisigFactory.sol +++ b/contracts/ZKMultisigFactory.sol @@ -27,10 +27,7 @@ contract ZKMultisigFactory is EIP712, IZKMultisigFactory { address participantVerifier_ ) EIP712("ZKMultisigFactory", "1") { require( - zkMultisigImplementation_ != address(0) && - zkMultisigImplementation_.code.length > 0 && - participantVerifier_ != address(0) && - participantVerifier_.code.length > 0, + zkMultisigImplementation_.code.length > 0 && participantVerifier_.code.length > 0, "ZKMultisigFactory: Invalid implementation or verifier address" ); diff --git a/test/ZKMultisig.tests.ts b/test/ZKMultisig.tests.ts index a425967..0910689 100644 --- a/test/ZKMultisig.tests.ts +++ b/test/ZKMultisig.tests.ts @@ -82,7 +82,7 @@ describe("ZKMultisig", () => { await zkMultisigImpl.waitForDeployment(); - var zkMultisigFactory__factory = await ethers.getContractFactory("ZKMultisigFactory"); + const zkMultisigFactory__factory = await ethers.getContractFactory("ZKMultisigFactory"); zkMultisigFactory = await zkMultisigFactory__factory.deploy( await zkMultisigImpl.getAddress(), await positiveParticipantVerifier.getAddress(), diff --git a/test/ZKMultisigFactory.tests.ts b/test/ZKMultisigFactory.tests.ts index 5270bd2..faf875d 100644 --- a/test/ZKMultisigFactory.tests.ts +++ b/test/ZKMultisigFactory.tests.ts @@ -32,12 +32,12 @@ describe("ZKMultisig Factory", () => { before(async () => { [alice] = await ethers.getSigners(); - var verifier__factory = await ethers.getContractFactory("PositiveVerifierMock"); + const verifier__factory = await ethers.getContractFactory("PositiveVerifierMock"); participantVerifier = await verifier__factory.deploy(); await participantVerifier.waitForDeployment(); - var zkMultisig__factory = await ethers.getContractFactory("ZKMultisig", { + const zkMultisig__factory = await ethers.getContractFactory("ZKMultisig", { libraries: { PoseidonUnit1L: await (await getPoseidon(1)).getAddress(), }, @@ -46,7 +46,7 @@ describe("ZKMultisig Factory", () => { await zkMultisig.waitForDeployment(); - var zkMultisigFactory__factory = await ethers.getContractFactory("ZKMultisigFactory"); + const zkMultisigFactory__factory = await ethers.getContractFactory("ZKMultisigFactory"); zkMultisigFactory = await zkMultisigFactory__factory.deploy( await zkMultisig.getAddress(), await participantVerifier.getAddress(),