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"