From b7b470838e5d167fc7a9f5aa7c36f2cbe906a650 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Fri, 16 Dec 2022 11:21:26 -0800 Subject: [PATCH 1/9] feat: add `PartyList` contract and tests --- contracts/party/IPartyFactory.sol | 27 ++++++- contracts/party/PartyFactory.sol | 30 +++++++- contracts/party/PartyList.sol | 73 +++++++++++++++++++ deploy/deploy.sol | 16 +++- sol-tests/crowdfund/MockPartyFactory.sol | 3 +- sol-tests/party/PartyFactory.t.sol | 26 ++++++- sol-tests/party/PartyGovernance.t.sol | 3 +- sol-tests/party/PartyGovernanceNFT.t.sol | 3 +- sol-tests/party/PartyList.t.sol | 62 ++++++++++++++++ .../ListOnZoraProposalIntegration.t.sol | 3 +- sol-tests/utils/PartyGovernanceHelpers.t.sol | 3 +- 11 files changed, 235 insertions(+), 14 deletions(-) create mode 100644 contracts/party/PartyList.sol create mode 100644 sol-tests/party/PartyList.t.sol diff --git a/contracts/party/IPartyFactory.sol b/contracts/party/IPartyFactory.sol index 9a099d217..2591bc4d0 100644 --- a/contracts/party/IPartyFactory.sol +++ b/contracts/party/IPartyFactory.sol @@ -17,8 +17,9 @@ interface IPartyFactory { ); /// @notice Deploy a new party instance. Afterwards, governance NFTs can be minted - /// for party members using the `mint()` function from the newly - /// created party. + /// for party members by the authority (usually the crowdfund + /// instance, if created from a successful crowdfund) using the + /// `mint()` function. /// @param authority The address that can call `mint()`. /// @param opts Options used to initialize the party. These are fixed /// and cannot be changed later. @@ -35,6 +36,28 @@ interface IPartyFactory { uint256[] memory preciousTokenIds ) external returns (Party party); + /// @notice Deploy a new party instance from a list of members and their + /// voting powers. Afterwards, governance NFTs can be minted for + /// party members through the `PartyList` contract using the `mint()` function. + /// @param opts Options used to initialize the party. These are fixed + /// and cannot be changed later. + /// @param preciousTokens The tokens that are considered precious by the + /// party.These are protected assets and are subject + /// to extra restrictions in proposals vs other + /// assets. + /// @param preciousTokenIds The IDs associated with each token in `preciousTokens`. + /// @param listMerkleRoot Merkle root of list of initial members and + /// voting power for each member. Each leaf in the + /// list should be encoded as: + /// `abi.encodePacked(address member, uint96 votingPower)`. + /// @return party The newly created `Party` instance. + function createPartyFromList( + Party.PartyOptions memory opts, + IERC721[] memory preciousTokens, + uint256[] memory preciousTokenIds, + bytes32 listMerkleRoot + ) external returns (Party party); + /// @notice The `Globals` contract storing global configuration values. This contract /// is immutable and it’s address will never change. function GLOBALS() external view returns (IGlobals); diff --git a/contracts/party/PartyFactory.sol b/contracts/party/PartyFactory.sol index 3477d332f..e3a52e392 100644 --- a/contracts/party/PartyFactory.sol +++ b/contracts/party/PartyFactory.sol @@ -9,20 +9,21 @@ import "../renderers/RendererStorage.sol"; import "./Party.sol"; import "./IPartyFactory.sol"; +import "./PartyList.sol"; /// @notice Factory used to deploy new proxified `Party` instances. contract PartyFactory is IPartyFactory { error InvalidAuthorityError(address authority); - /// @inheritdoc IPartyFactory IGlobals public immutable GLOBALS; + PartyList public immutable PARTY_LIST; // Set the `Globals` contract. - constructor(IGlobals globals) { + constructor(IGlobals globals, PartyList partyList) { GLOBALS = globals; + PARTY_LIST = partyList; } - /// @inheritdoc IPartyFactory function createParty( address authority, Party.PartyOptions memory opts, @@ -33,6 +34,29 @@ contract PartyFactory is IPartyFactory { if (authority == address(0)) { revert InvalidAuthorityError(authority); } + // Create the party. + return _createParty(authority, opts, preciousTokens, preciousTokenIds); + } + + function createPartyFromList( + Party.PartyOptions memory opts, + IERC721[] memory preciousTokens, + uint256[] memory preciousTokenIds, + bytes32 listMerkleRoot + ) public returns (Party party) { + // Create the party. + party = _createParty(address(PARTY_LIST), opts, preciousTokens, preciousTokenIds); + // Create the list used to determine the initial list of members and voting + // power for each member. + PARTY_LIST.createList(party, listMerkleRoot); + } + + function _createParty( + address authority, + Party.PartyOptions memory opts, + IERC721[] memory preciousTokens, + uint256[] memory preciousTokenIds + ) private returns (Party party) { // Deploy a new proxified `Party` instance. Party.PartyInitData memory initData = Party.PartyInitData({ options: opts, diff --git a/contracts/party/PartyList.sol b/contracts/party/PartyList.sol new file mode 100644 index 000000000..1c6103c36 --- /dev/null +++ b/contracts/party/PartyList.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import "solmate/utils/MerkleProofLib.sol"; + +import "./Party.sol"; +import "./IPartyFactory.sol"; + +contract PartyList { + error ListAlreadyExistsError(Party party, bytes32 merkleRoot); + error UnauthorizedError(); + error InvalidProofError(); + + mapping(Party => bytes32) public listMerkleRoots; + + IGlobals private immutable _GLOBALS; + + // Set the `Globals` contract. + constructor(IGlobals globals) { + _GLOBALS = globals; + } + + function createList(Party party, bytes32 merkleRoot) external { + if (msg.sender != _GLOBALS.getAddress(LibGlobals.GLOBAL_PARTY_FACTORY)) + revert UnauthorizedError(); + + bytes32 root = listMerkleRoots[party]; + if (root != bytes32(0)) revert ListAlreadyExistsError(party, root); + + listMerkleRoots[party] = merkleRoot; + } + + function mint( + Party party, + address member, + uint96 votingPower, + address delegate, + bytes32[] calldata proof + ) public returns (uint256 tokenId) { + if (!isAllowed(party, member, votingPower, proof)) revert InvalidProofError(); + return party.mint(member, votingPower, delegate); + } + + function batchMint( + Party party, + address[] calldata members, + uint96[] calldata votingPowers, + address[] calldata delegates, + bytes32[][] calldata proofs + ) external returns (uint256[] memory tokenIds) { + tokenIds = new uint256[](members.length); + for (uint256 i; i < members.length; ++i) { + tokenIds[0] = mint(party, members[i], votingPowers[i], delegates[i], proofs[i]); + } + } + + function isAllowed( + Party party, + address member, + uint96 votingPower, + bytes32[] calldata proof + ) public view returns (bool) { + bytes32 leaf; + assembly { + // leaf = keccak256(abi.encodePacked(member, votingPower)) + mstore(0, shl(96, member)) + mstore(20, shl(160, votingPower)) + leaf := keccak256(0, 32) + } + + return MerkleProofLib.verify(proof, listMerkleRoots[party], leaf); + } +} diff --git a/deploy/deploy.sol b/deploy/deploy.sol index be44d0f54..80fc4a1be 100644 --- a/deploy/deploy.sol +++ b/deploy/deploy.sol @@ -14,6 +14,7 @@ import "../contracts/gatekeepers/IGateKeeper.sol"; import "../contracts/globals/Globals.sol"; import "../contracts/globals/LibGlobals.sol"; import "../contracts/party/Party.sol"; +import "../contracts/party/PartyList.sol"; import "../contracts/party/PartyFactory.sol"; import "../contracts/renderers/CrowdfundNFTRenderer.sol"; import "../contracts/renderers/PartyNFTRenderer.sol"; @@ -49,6 +50,7 @@ abstract contract Deploy { CollectionBuyCrowdfund public collectionBuyCrowdfundImpl; CrowdfundFactory public crowdfundFactory; Party public partyImpl; + PartyList public partyList; PartyFactory public partyFactory; IOpenseaExchange public seaport; ProposalExecutionEngine public proposalEngineImpl; @@ -121,13 +123,22 @@ abstract contract Deploy { _trackDeployerGasAfter(); console.log(" Deployed - Party implementation", address(partyImpl)); + // DEPLOY_PARTY_LIST + console.log(""); + console.log("### PartyList"); + console.log(" Deploying - PartyList"); + _trackDeployerGasBefore(); + partyList = new PartyList(globals); + _trackDeployerGasAfter(); + console.log(" Deployed - PartyList", address(partyList)); + // DEPLOY_PARTY_FACTORY console.log(""); console.log("### PartyFactory"); console.log(" Deploying - PartyFactory"); _switchDeployer(DeployerRole.PartyFactory); _trackDeployerGasBefore(); - partyFactory = new PartyFactory(globals); + partyFactory = new PartyFactory(globals, partyList); _trackDeployerGasAfter(); console.log(" Deployed - PartyFactory", address(partyFactory)); _switchDeployer(DeployerRole.Default); @@ -521,7 +532,7 @@ contract DeployScript is Script, Deploy { Deploy.deploy(deployConstants); vm.stopBroadcast(); - AddressMapping[] memory addressMapping = new AddressMapping[](20); + AddressMapping[] memory addressMapping = new AddressMapping[](21); addressMapping[0] = AddressMapping("globals", address(globals)); addressMapping[1] = AddressMapping("tokenDistributor", address(tokenDistributor)); addressMapping[2] = AddressMapping("seaportExchange", address(seaport)); @@ -557,6 +568,7 @@ contract DeployScript is Script, Deploy { "pixeldroidConsoleFont", address(pixeldroidConsoleFont) ); + addressMapping[20] = AddressMapping("partyList", address(partyList)); console.log(""); console.log("### Deployed addresses"); diff --git a/sol-tests/crowdfund/MockPartyFactory.sol b/sol-tests/crowdfund/MockPartyFactory.sol index b11357a15..07fa44d34 100644 --- a/sol-tests/crowdfund/MockPartyFactory.sol +++ b/sol-tests/crowdfund/MockPartyFactory.sol @@ -1,10 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8; -import "../../contracts/party/IPartyFactory.sol"; import "./MockParty.sol"; -contract MockPartyFactory is IPartyFactory { +contract MockPartyFactory { MockParty public mockParty = new MockParty(); event MockPartyFactoryCreateParty( diff --git a/sol-tests/party/PartyFactory.t.sol b/sol-tests/party/PartyFactory.t.sol index 27adb3064..295e840fd 100644 --- a/sol-tests/party/PartyFactory.t.sol +++ b/sol-tests/party/PartyFactory.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8; import "forge-std/Test.sol"; import "../../contracts/party/PartyFactory.sol"; +import "../../contracts/party/PartyList.sol"; import "../../contracts/globals/Globals.sol"; import "../TestUtils.sol"; import "../../contracts/proposals/ProposalExecutionEngine.sol"; @@ -11,7 +12,8 @@ import "../../contracts/proposals/ProposalExecutionEngine.sol"; contract PartyFactoryTest is Test, TestUtils { Globals globals = new Globals(address(this)); Party partyImpl = new Party(globals); - PartyFactory factory = new PartyFactory(globals); + PartyList partyList = new PartyList(globals); + PartyFactory factory = new PartyFactory(globals, partyList); ProposalExecutionEngine eng; Party.PartyOptions defaultPartyOptions; @@ -35,6 +37,7 @@ contract PartyFactoryTest is Test, TestUtils { globals.setAddress(LibGlobals.GLOBAL_PARTY_IMPL, address(partyImpl)); globals.setAddress(LibGlobals.GLOBAL_PROPOSAL_ENGINE_IMPL, address(eng)); + globals.setAddress(LibGlobals.GLOBAL_PARTY_FACTORY, address(factory)); } function _createPreciouses( @@ -117,4 +120,25 @@ contract PartyFactoryTest is Test, TestUtils { ); factory.createParty(authority, opts, preciousTokens, preciousTokenIds); } + + function testCreatePartyFromList() external { + (IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds) = _createPreciouses(3); + address member = _randomAddress(); + address delegate = _randomAddress(); + uint96 votingPower = 0.1e18; + bytes32 listMerkleRoot = keccak256(abi.encodePacked(member, votingPower)); + Party party = factory.createPartyFromList( + defaultPartyOptions, + preciousTokens, + preciousTokenIds, + listMerkleRoot + ); + assertEq(party.mintAuthority(), address(partyList)); + assertEq(partyList.listMerkleRoots(party), listMerkleRoot); + + uint256 tokenId = partyList.mint(party, member, votingPower, delegate, new bytes32[](0)); + assertEq(party.balanceOf(member), 1); + assertEq(party.delegationsByVoter(member), delegate); + assertEq(party.votingPowerByTokenId(tokenId), votingPower); + } } diff --git a/sol-tests/party/PartyGovernance.t.sol b/sol-tests/party/PartyGovernance.t.sol index 55d9f7432..a9a66e594 100644 --- a/sol-tests/party/PartyGovernance.t.sol +++ b/sol-tests/party/PartyGovernance.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8; import "forge-std/Test.sol"; import "../../contracts/party/PartyFactory.sol"; +import "../../contracts/party/PartyList.sol"; import "../../contracts/party/Party.sol"; import "../../contracts/globals/Globals.sol"; import "../proposals/DummySimpleProposalEngineImpl.sol"; @@ -33,7 +34,7 @@ contract PartyGovernanceTest is Test, TestUtils { eng = new DummySimpleProposalEngineImpl(); globalsAdmin.setProposalEng(address(eng)); - partyFactory = new PartyFactory(globals); + partyFactory = new PartyFactory(globals, PartyList(address(0))); john = new PartyParticipant(); danny = new PartyParticipant(); diff --git a/sol-tests/party/PartyGovernanceNFT.t.sol b/sol-tests/party/PartyGovernanceNFT.t.sol index d28b07b59..6a107c232 100644 --- a/sol-tests/party/PartyGovernanceNFT.t.sol +++ b/sol-tests/party/PartyGovernanceNFT.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "forge-std/console2.sol"; import "../../contracts/party/PartyFactory.sol"; +import "../../contracts/party/PartyList.sol"; import "../../contracts/party/Party.sol"; import "../../contracts/globals/Globals.sol"; import "../../contracts/renderers/PartyNFTRenderer.sol"; @@ -42,7 +43,7 @@ contract PartyGovernanceNFTTest is Test, TestUtils { eng = new DummySimpleProposalEngineImpl(); globalsAdmin.setProposalEng(address(eng)); - partyFactory = new PartyFactory(globals); + partyFactory = new PartyFactory(globals, PartyList(address(0))); john = new PartyParticipant(); partyAdmin = new PartyAdmin(partyFactory); diff --git a/sol-tests/party/PartyList.t.sol b/sol-tests/party/PartyList.t.sol new file mode 100644 index 000000000..f7045b6a6 --- /dev/null +++ b/sol-tests/party/PartyList.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8; + +import "forge-std/Test.sol"; + +import "../../contracts/party/PartyList.sol"; +import "../../contracts/globals/Globals.sol"; +import "../crowdfund/MockParty.sol"; +import "../TestUtils.sol"; + +contract PartyListTest is Test, TestUtils { + Globals globals = new Globals(address(this)); + PartyList partyList = new PartyList(globals); + Party party = Party(payable(address(new MockParty()))); + + constructor() { + // Set party factory to this address so that we have authority to create list + globals.setAddress(LibGlobals.GLOBAL_PARTY_FACTORY, address(this)); + } + + function test_createList_works() public { + bytes32 merkleRoot = keccak256(abi.encodePacked(_randomUint256())); + partyList.createList(party, merkleRoot); + assertEq(partyList.listMerkleRoots(party), merkleRoot); + } + + function test_createList_onlyPartyFactory() public { + bytes32 merkleRoot = keccak256(abi.encodePacked(_randomUint256())); + vm.prank(_randomAddress()); + vm.expectRevert(PartyList.UnauthorizedError.selector); + partyList.createList(party, merkleRoot); + } + + function test_createList_onlyOncePerParty() public { + bytes32 merkleRoot = keccak256(abi.encodePacked(_randomUint256())); + partyList.createList(party, merkleRoot); + vm.expectRevert( + abi.encodeWithSelector(PartyList.ListAlreadyExistsError.selector, party, merkleRoot) + ); + partyList.createList(party, merkleRoot); + } + + function test_mint_works() public { + address member = _randomAddress(); + uint96 votingPower = 0.1e18; + address delegate = _randomAddress(); + bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower)); + partyList.createList(party, merkleRoot); + uint256 tokenId = partyList.mint(party, member, votingPower, delegate, new bytes32[](0)); + assertEq(party.ownerOf(tokenId), member); + } + + function test_mint_invalidProof() public { + address member = _randomAddress(); + uint96 votingPower = 0.1e18; + address delegate = _randomAddress(); + bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower)); + partyList.createList(party, merkleRoot); + vm.expectRevert(abi.encodeWithSelector(PartyList.InvalidProofError.selector)); + partyList.mint(party, member, votingPower, delegate, new bytes32[](1)); + } +} diff --git a/sol-tests/proposals/ListOnZoraProposalIntegration.t.sol b/sol-tests/proposals/ListOnZoraProposalIntegration.t.sol index 60f73ec05..c6503dd9f 100644 --- a/sol-tests/proposals/ListOnZoraProposalIntegration.t.sol +++ b/sol-tests/proposals/ListOnZoraProposalIntegration.t.sol @@ -6,6 +6,7 @@ import "forge-std/Test.sol"; import "../../contracts/globals/Globals.sol"; import "../../contracts/globals/LibGlobals.sol"; import "../../contracts/proposals/ProposalExecutionEngine.sol"; +import "../../contracts/party/PartyList.sol"; import "../../contracts/distribution/TokenDistributor.sol"; import "../TestUtils.sol"; @@ -51,7 +52,7 @@ contract ListOnZoraProposalIntegrationTest is Test, TestUtils, ZoraTestUtils { ); globalsAdmin.setProposalEng(address(pe)); - partyFactory = new PartyFactory(globals); + partyFactory = new PartyFactory(globals, PartyList(address(0))); } function testForked_simpleZora() public onlyForked { diff --git a/sol-tests/utils/PartyGovernanceHelpers.t.sol b/sol-tests/utils/PartyGovernanceHelpers.t.sol index 3995170d0..2d40c3ce8 100644 --- a/sol-tests/utils/PartyGovernanceHelpers.t.sol +++ b/sol-tests/utils/PartyGovernanceHelpers.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "forge-std/console2.sol"; import "../../contracts/party/PartyFactory.sol"; +import "../../contracts/party/PartyList.sol"; import "../../contracts/party/Party.sol"; import "../../contracts/globals/Globals.sol"; import "../../contracts/renderers/PartyNFTRenderer.sol"; @@ -41,7 +42,7 @@ contract PartyGovernanceHelpersTest is Test, TestUtils { nftRenderer = new PartyNFTRenderer(globals, RendererStorage(address(0)), IFont(address(0))); globalsAdmin.setGovernanceNftRendererAddress(address(nftRenderer)); - partyFactory = new PartyFactory(globals); + partyFactory = new PartyFactory(globals, PartyList(address(0))); john = new PartyParticipant(); steve = new PartyParticipant(); From cec99efdc4b4753d2beee6f0ebf9343bf42615c0 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Mon, 19 Dec 2022 09:25:38 -0800 Subject: [PATCH 2/9] test: fix tests --- sol-tests/crowdfund/AuctionCrowdfund.t.sol | 2 +- sol-tests/crowdfund/MockPartyFactory.sol | 1 + sol-tests/proposals/ZoraTestUtils.sol | 2 +- tests/integration/system.ts | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sol-tests/crowdfund/AuctionCrowdfund.t.sol b/sol-tests/crowdfund/AuctionCrowdfund.t.sol index 6f5639e20..3653a636c 100644 --- a/sol-tests/crowdfund/AuctionCrowdfund.t.sol +++ b/sol-tests/crowdfund/AuctionCrowdfund.t.sol @@ -445,7 +445,7 @@ contract AuctionCrowdfundTest is Test, TestUtils { // Contribute and delegate. address payable contributor = _randomAddress(); _contribute(cf, contributor, 1e18); - uint256 bid = market.getMinimumBid(auctionId); + market.getMinimumBid(auctionId); // Expire the CF. skip(defaultDuration); _expectEmit0(); diff --git a/sol-tests/crowdfund/MockPartyFactory.sol b/sol-tests/crowdfund/MockPartyFactory.sol index 07fa44d34..06152faa7 100644 --- a/sol-tests/crowdfund/MockPartyFactory.sol +++ b/sol-tests/crowdfund/MockPartyFactory.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8; +import "contracts/party/Party.sol"; import "./MockParty.sol"; contract MockPartyFactory { diff --git a/sol-tests/proposals/ZoraTestUtils.sol b/sol-tests/proposals/ZoraTestUtils.sol index fb945a160..15bc8bda3 100644 --- a/sol-tests/proposals/ZoraTestUtils.sol +++ b/sol-tests/proposals/ZoraTestUtils.sol @@ -17,7 +17,7 @@ contract ZoraTestUtils is Test { _ZORA.createBid{ value: bidPrice }(auctionId, bidPrice); } - function _getNextZoraAuctionId() internal returns (uint256 auctionId) { + function _getNextZoraAuctionId() internal view returns (uint256 auctionId) { return uint256(vm.load(address(_ZORA), bytes32(uint256(5)))); } } diff --git a/tests/integration/system.ts b/tests/integration/system.ts index 08bd471e3..b853c7967 100644 --- a/tests/integration/system.ts +++ b/tests/integration/system.ts @@ -232,6 +232,7 @@ export class System { const partyFactory = await deployContract(worker, artifacts.PartyFactory as any, [ globals.address, + NULL_ADDRESS, ]); await (await globals.setAddress(GlobalKeys.PartyFactory, partyFactory.address)).wait(); From e0755ea60449379d67f47dc43c3bbddc4274bd72 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Tue, 20 Dec 2022 19:24:26 -0800 Subject: [PATCH 3/9] feat: update contracts and tests for `PartyFactory` and `PartyList` --- contracts/party/IPartyFactory.sol | 45 ++++++++------ contracts/party/PartyFactory.sol | 26 +++++--- contracts/party/PartyList.sol | 99 +++++++++++++++++++++++++++--- sol-tests/party/PartyFactory.t.sol | 66 ++++++++++++-------- sol-tests/party/PartyList.t.sol | 53 ++++++++++++---- 5 files changed, 218 insertions(+), 71 deletions(-) diff --git a/contracts/party/IPartyFactory.sol b/contracts/party/IPartyFactory.sol index 2591bc4d0..e5e3664d1 100644 --- a/contracts/party/IPartyFactory.sol +++ b/contracts/party/IPartyFactory.sol @@ -16,6 +16,32 @@ interface IPartyFactory { address creator ); + struct PartyFromListInitOpts { + // Options used to initialize the party. These are fixed and cannot be + // changed later. + Party.PartyOptions opts; + // The tokens to transfer to the party. + IERC721[] tokens; + // The IDs associated with each token in `tokens`. + uint256[] tokenIds; + // The tokens that are considered precious by the party. These are + // protected assets and are subject to extra restrictions in + // proposals vs other assets. + IERC721[] preciousTokens; + // The IDs associated with each token in `preciousTokens`. + uint256[] preciousTokenIds; + // The address of the party creator to mint card for. + address creator; + // The voting power of the party creator. + uint96 creatorVotingPower; + // The address to delegate creator's voting power to. + address creatorDelegate; + // Merkle root of list of initial members and voting power for each member. + // Each leaf in the list should be encoded as: + // `abi.encodePacked(address member, uint96 votingPower)` + bytes32 listMerkleRoot; + } + /// @notice Deploy a new party instance. Afterwards, governance NFTs can be minted /// for party members by the authority (usually the crowdfund /// instance, if created from a successful crowdfund) using the @@ -39,24 +65,7 @@ interface IPartyFactory { /// @notice Deploy a new party instance from a list of members and their /// voting powers. Afterwards, governance NFTs can be minted for /// party members through the `PartyList` contract using the `mint()` function. - /// @param opts Options used to initialize the party. These are fixed - /// and cannot be changed later. - /// @param preciousTokens The tokens that are considered precious by the - /// party.These are protected assets and are subject - /// to extra restrictions in proposals vs other - /// assets. - /// @param preciousTokenIds The IDs associated with each token in `preciousTokens`. - /// @param listMerkleRoot Merkle root of list of initial members and - /// voting power for each member. Each leaf in the - /// list should be encoded as: - /// `abi.encodePacked(address member, uint96 votingPower)`. - /// @return party The newly created `Party` instance. - function createPartyFromList( - Party.PartyOptions memory opts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds, - bytes32 listMerkleRoot - ) external returns (Party party); + function createPartyFromList(PartyFromListInitOpts memory initOpts) external returns (Party party); /// @notice The `Globals` contract storing global configuration values. This contract /// is immutable and it’s address will never change. diff --git a/contracts/party/PartyFactory.sol b/contracts/party/PartyFactory.sol index e3a52e392..f4c6c8057 100644 --- a/contracts/party/PartyFactory.sol +++ b/contracts/party/PartyFactory.sol @@ -39,16 +39,28 @@ contract PartyFactory is IPartyFactory { } function createPartyFromList( - Party.PartyOptions memory opts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds, - bytes32 listMerkleRoot + PartyFromListInitOpts memory initOpts ) public returns (Party party) { // Create the party. - party = _createParty(address(PARTY_LIST), opts, preciousTokens, preciousTokenIds); + party = _createParty( + address(PARTY_LIST), + initOpts.opts, + initOpts.preciousTokens, + initOpts.preciousTokenIds + ); // Create the list used to determine the initial list of members and voting - // power for each member. - PARTY_LIST.createList(party, listMerkleRoot); + // power for each member and mint the party creator their card. + PARTY_LIST.createList( + party, + initOpts.listMerkleRoot, + initOpts.creator, + initOpts.creatorVotingPower, + initOpts.creatorDelegate + ); + // Transfer the tokens to the party. + for (uint256 i; i < initOpts.tokens.length; ++i) { + initOpts.tokens[i].transferFrom(msg.sender, address(party), initOpts.tokenIds[i]); + } } function _createParty( diff --git a/contracts/party/PartyList.sol b/contracts/party/PartyList.sol index 1c6103c36..cc88d077e 100644 --- a/contracts/party/PartyList.sol +++ b/contracts/party/PartyList.sol @@ -8,10 +8,14 @@ import "./IPartyFactory.sol"; contract PartyList { error ListAlreadyExistsError(Party party, bytes32 merkleRoot); + error InvalidProofError(bytes32[] proof); + error AlreadyMintedError(bytes32 leaf); error UnauthorizedError(); - error InvalidProofError(); + /// @notice party address => merkle root mapping(Party => bytes32) public listMerkleRoots; + /// @notice party address => leaf => minted + mapping(Party => mapping(bytes32 => bool)) public minted; IGlobals private immutable _GLOBALS; @@ -20,54 +24,131 @@ contract PartyList { _GLOBALS = globals; } - function createList(Party party, bytes32 merkleRoot) external { + modifier onlyPartyFactory() { if (msg.sender != _GLOBALS.getAddress(LibGlobals.GLOBAL_PARTY_FACTORY)) revert UnauthorizedError(); + _; + } + /** + * @notice Creates a new list for a party and mints the creator their card. + * @dev Only the `PartyFactory` address is authorized to create lists. + * @param party The party to create the list for. + * @param merkleRoot The root of the Merkle tree for the list. + * @param creator The address of the creator of the list. + * @param creatorVotingPower The voting power of the creator. + * @param creatorDelegate The address of the delegate of the creator. + */ + function createList( + Party party, + bytes32 merkleRoot, + address creator, + uint96 creatorVotingPower, + address creatorDelegate + ) external onlyPartyFactory { bytes32 root = listMerkleRoots[party]; if (root != bytes32(0)) revert ListAlreadyExistsError(party, root); listMerkleRoots[party] = merkleRoot; - } + if (creator != address(0) && creatorVotingPower > 0) { + party.mint(creator, creatorVotingPower, creatorDelegate); + } + } + /** + * @notice Mints a party card for a member from the party's list. + * @param party The party from which the token is being minted + * @param member The address of the party member for whom the token is being minted + * @param votingPower The voting power of the token + * @param nonce A number used to prevent double-minting + * @param delegate The address to delegate voting power to + * @param proof A set of data used to verify the validity of the minting + * @return tokenId The ID of the newly minted token + */ function mint( Party party, address member, uint96 votingPower, + uint256 nonce, address delegate, bytes32[] calldata proof ) public returns (uint256 tokenId) { - if (!isAllowed(party, member, votingPower, proof)) revert InvalidProofError(); + (bool allowed, bytes32 leaf) = _verify(party, member, votingPower, nonce, proof); + + if (!allowed) revert InvalidProofError(proof); + if (minted[party][leaf]) revert AlreadyMintedError(leaf); + + minted[party][leaf] = true; + return party.mint(member, votingPower, delegate); } + /** + * @notice Mints party cards for members from the party's list. + * @param party The party from which the token is being minted + * @param members The address of party members for whom the tokens are being minted + * @param votingPowers The voting powers of the tokens + * @param nonces A numbers used to prevent double-minting + * @param delegates The addresses to delegate voting power to + * @param proofs A set of data used to verify the validity of the minting + * @return tokenIds The IDs of the newly minted tokens + */ function batchMint( Party party, address[] calldata members, uint96[] calldata votingPowers, + uint256[] calldata nonces, address[] calldata delegates, bytes32[][] calldata proofs ) external returns (uint256[] memory tokenIds) { tokenIds = new uint256[](members.length); for (uint256 i; i < members.length; ++i) { - tokenIds[0] = mint(party, members[i], votingPowers[i], delegates[i], proofs[i]); + tokenIds[0] = mint( + party, + members[i], + votingPowers[i], + nonces[i], + delegates[i], + proofs[i] + ); } } + /** + * @notice Checks if a given member is allowed to mint from a party's list. + * @param party The party for which the `member` is being checked. + * @param member The address of the member to check. + * @param votingPower The voting power of the `member` in the `party`. + * @param nonce A nonce associated with the mint used to prevent double-minting. + * @param proof A set of data used to verify the validity of the minting. + * @return allowed A boolean indicating if the `member` is allowed to mint. + */ function isAllowed( Party party, address member, uint96 votingPower, + uint256 nonce, bytes32[] calldata proof ) public view returns (bool) { - bytes32 leaf; + (bool allowed, bytes32 leaf) = _verify(party, member, votingPower, nonce, proof); + return allowed && !minted[party][leaf]; + } + + function _verify( + Party party, + address member, + uint96 votingPower, + uint256 nonce, + bytes32[] calldata proof + ) private view returns (bool allowed, bytes32 leaf) { assembly { - // leaf = keccak256(abi.encodePacked(member, votingPower)) + // leaf = keccak256(abi.encodePacked(member, votingPower, nonce)) mstore(0, shl(96, member)) mstore(20, shl(160, votingPower)) - leaf := keccak256(0, 32) + mstore(32, nonce) + leaf := keccak256(0, 64) } - return MerkleProofLib.verify(proof, listMerkleRoots[party], leaf); + allowed = MerkleProofLib.verify(proof, listMerkleRoots[party], leaf); } } diff --git a/sol-tests/party/PartyFactory.t.sol b/sol-tests/party/PartyFactory.t.sol index 295e840fd..7810470f5 100644 --- a/sol-tests/party/PartyFactory.t.sol +++ b/sol-tests/party/PartyFactory.t.sol @@ -8,6 +8,7 @@ import "../../contracts/party/PartyList.sol"; import "../../contracts/globals/Globals.sol"; import "../TestUtils.sol"; import "../../contracts/proposals/ProposalExecutionEngine.sol"; +import "../DummyERC721.sol"; contract PartyFactoryTest is Test, TestUtils { Globals globals = new Globals(address(this)); @@ -16,6 +17,9 @@ contract PartyFactoryTest is Test, TestUtils { PartyFactory factory = new PartyFactory(globals, partyList); ProposalExecutionEngine eng; Party.PartyOptions defaultPartyOptions; + IERC721[] preciousTokens; + uint256[] preciousTokenIds; + bytes32 preciousListHash; constructor() { defaultPartyOptions.name = "PARTY"; @@ -38,26 +42,23 @@ contract PartyFactoryTest is Test, TestUtils { globals.setAddress(LibGlobals.GLOBAL_PARTY_IMPL, address(partyImpl)); globals.setAddress(LibGlobals.GLOBAL_PROPOSAL_ENGINE_IMPL, address(eng)); globals.setAddress(LibGlobals.GLOBAL_PARTY_FACTORY, address(factory)); - } - function _createPreciouses( - uint256 count - ) private view returns (IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds) { - preciousTokens = new IERC721[](count); - preciousTokenIds = new uint256[](count); - for (uint256 i; i < count; ++i) { - preciousTokens[i] = IERC721(_randomAddress()); - preciousTokenIds[i] = _randomUint256(); + preciousTokens = new IERC721[](3); + preciousTokenIds = new uint256[](3); + for (uint256 i; i < 3; ++i) { + DummyERC721 t = new DummyERC721(); + preciousTokens[i] = IERC721(address(t)); + preciousTokenIds[i] = t.mint(address(this)); } } function _hashPreciousList( - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds + IERC721[] memory _preciousTokens, + uint256[] memory _preciousTokenIds ) internal pure returns (bytes32 h) { assembly { - mstore(0x00, keccak256(add(preciousTokens, 0x20), mul(mload(preciousTokens), 0x20))) - mstore(0x20, keccak256(add(preciousTokenIds, 0x20), mul(mload(preciousTokenIds), 0x20))) + mstore(0x00, keccak256(add(_preciousTokens, 0x20), mul(mload(_preciousTokens), 0x20))) + mstore(0x20, keccak256(add(_preciousTokenIds, 0x20), mul(mload(_preciousTokenIds), 0x20))) h := keccak256(0x00, 0x40) } } @@ -71,7 +72,6 @@ contract PartyFactoryTest is Test, TestUtils { vm.assume(randomBps <= 1e4); address authority = _randomAddress(); - (IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds) = _createPreciouses(3); Party.PartyOptions memory opts = Party.PartyOptions({ governance: PartyGovernance.GovernanceOpts({ hosts: _toAddressArray(_randomAddress()), @@ -106,7 +106,6 @@ contract PartyFactoryTest is Test, TestUtils { vm.assume(passThresholdBps > 1e4 || feeBps > 1e4); address authority = _randomAddress(); - (IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds) = _createPreciouses(3); Party.PartyOptions memory opts = defaultPartyOptions; opts.governance.feeBps = feeBps; @@ -122,23 +121,38 @@ contract PartyFactoryTest is Test, TestUtils { } function testCreatePartyFromList() external { - (IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds) = _createPreciouses(3); address member = _randomAddress(); address delegate = _randomAddress(); uint96 votingPower = 0.1e18; - bytes32 listMerkleRoot = keccak256(abi.encodePacked(member, votingPower)); - Party party = factory.createPartyFromList( - defaultPartyOptions, - preciousTokens, - preciousTokenIds, - listMerkleRoot - ); + uint256 nonce = _randomUint256(); + + for (uint256 i; i < 3; ++i) { + preciousTokens[i].approve(address(factory), preciousTokenIds[i]); + } + + IPartyFactory.PartyFromListInitOpts memory initOpts = IPartyFactory.PartyFromListInitOpts({ + opts: defaultPartyOptions, + tokens: preciousTokens, + tokenIds: preciousTokenIds, + preciousTokens: preciousTokens, + preciousTokenIds: preciousTokenIds, + creator: _randomAddress(), + creatorVotingPower: 0.3e18, + creatorDelegate: _randomAddress(), + listMerkleRoot: keccak256(abi.encodePacked(member, votingPower, nonce)) + }); + + Party party = factory.createPartyFromList(initOpts); + assertEq(party.mintAuthority(), address(partyList)); - assertEq(partyList.listMerkleRoots(party), listMerkleRoot); + assertEq(partyList.listMerkleRoots(party), initOpts.listMerkleRoot); + assertEq(party.balanceOf(initOpts.creator), 1); + assertEq(party.delegationsByVoter(initOpts.creator), initOpts.creatorDelegate); + assertEq(party.votingPowerByTokenId(1), initOpts.creatorVotingPower); - uint256 tokenId = partyList.mint(party, member, votingPower, delegate, new bytes32[](0)); + partyList.mint(party, member, votingPower, nonce, delegate, new bytes32[](0)); assertEq(party.balanceOf(member), 1); assertEq(party.delegationsByVoter(member), delegate); - assertEq(party.votingPowerByTokenId(tokenId), votingPower); + assertEq(party.votingPowerByTokenId(2), votingPower); } } diff --git a/sol-tests/party/PartyList.t.sol b/sol-tests/party/PartyList.t.sol index f7045b6a6..e499c5afe 100644 --- a/sol-tests/party/PartyList.t.sol +++ b/sol-tests/party/PartyList.t.sol @@ -19,44 +19,75 @@ contract PartyListTest is Test, TestUtils { } function test_createList_works() public { + address creator = _randomAddress(); + uint96 creatorVotingPower = 0.3e18; + address creatorDelegate = _randomAddress(); bytes32 merkleRoot = keccak256(abi.encodePacked(_randomUint256())); - partyList.createList(party, merkleRoot); + partyList.createList(party, merkleRoot, creator, creatorVotingPower, creatorDelegate); assertEq(partyList.listMerkleRoots(party), merkleRoot); } function test_createList_onlyPartyFactory() public { + address creator = _randomAddress(); + uint96 creatorVotingPower = 0.3e18; + address creatorDelegate = _randomAddress(); bytes32 merkleRoot = keccak256(abi.encodePacked(_randomUint256())); vm.prank(_randomAddress()); vm.expectRevert(PartyList.UnauthorizedError.selector); - partyList.createList(party, merkleRoot); + partyList.createList(party, merkleRoot, creator, creatorVotingPower, creatorDelegate); } function test_createList_onlyOncePerParty() public { + address creator = _randomAddress(); + uint96 creatorVotingPower = 0.3e18; + address creatorDelegate = _randomAddress(); bytes32 merkleRoot = keccak256(abi.encodePacked(_randomUint256())); - partyList.createList(party, merkleRoot); + partyList.createList(party, merkleRoot, creator, creatorVotingPower, creatorDelegate); vm.expectRevert( abi.encodeWithSelector(PartyList.ListAlreadyExistsError.selector, party, merkleRoot) ); - partyList.createList(party, merkleRoot); + partyList.createList(party, merkleRoot, creator, creatorVotingPower, creatorDelegate); } function test_mint_works() public { address member = _randomAddress(); uint96 votingPower = 0.1e18; + uint256 nonce = _randomUint256(); address delegate = _randomAddress(); - bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower)); - partyList.createList(party, merkleRoot); - uint256 tokenId = partyList.mint(party, member, votingPower, delegate, new bytes32[](0)); + bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); + partyList.createList(party, merkleRoot, address(0), 0, address(0)); + uint256 tokenId = partyList.mint( + party, + member, + votingPower, + nonce, + delegate, + new bytes32[](0) + ); assertEq(party.ownerOf(tokenId), member); } + function test_mint_cannotMintTwice() public { + address member = _randomAddress(); + uint96 votingPower = 0.1e18; + uint256 nonce = _randomUint256(); + address delegate = _randomAddress(); + bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); + partyList.createList(party, merkleRoot, address(0), 0, address(0)); + partyList.mint(party, member, votingPower, nonce, delegate, new bytes32[](0)); + vm.expectRevert(abi.encodeWithSelector(PartyList.AlreadyMintedError.selector, merkleRoot)); + partyList.mint(party, member, votingPower, nonce, delegate, new bytes32[](0)); + } + function test_mint_invalidProof() public { address member = _randomAddress(); uint96 votingPower = 0.1e18; + uint256 nonce = _randomUint256(); address delegate = _randomAddress(); - bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower)); - partyList.createList(party, merkleRoot); - vm.expectRevert(abi.encodeWithSelector(PartyList.InvalidProofError.selector)); - partyList.mint(party, member, votingPower, delegate, new bytes32[](1)); + bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); + bytes32[] memory proof = new bytes32[](1); + partyList.createList(party, merkleRoot, address(0), 0, address(0)); + vm.expectRevert(abi.encodeWithSelector(PartyList.InvalidProofError.selector, proof)); + partyList.mint(party, member, votingPower, nonce, delegate, proof); } } From 7a3781cc99d2ae49f2f2a706e8bfceecb64780e6 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Wed, 21 Dec 2022 09:46:35 -0800 Subject: [PATCH 4/9] feat: enable `batchMint` to continue on failed mints and update tests --- contracts/party/PartyList.sol | 46 ++++++++++++++++++------------ sol-tests/party/PartyFactory.t.sol | 6 +++- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/contracts/party/PartyList.sol b/contracts/party/PartyList.sol index cc88d077e..bf20037f6 100644 --- a/contracts/party/PartyList.sol +++ b/contracts/party/PartyList.sol @@ -5,8 +5,13 @@ import "solmate/utils/MerkleProofLib.sol"; import "./Party.sol"; import "./IPartyFactory.sol"; +import "../utils/LibRawResult.sol"; contract PartyList { + using LibRawResult for bytes; + + event ListCreated(Party party, bytes32 merkleRoot); + error ListAlreadyExistsError(Party party, bytes32 merkleRoot); error InvalidProofError(bytes32[] proof); error AlreadyMintedError(bytes32 leaf); @@ -54,7 +59,10 @@ contract PartyList { if (creator != address(0) && creatorVotingPower > 0) { party.mint(creator, creatorVotingPower, creatorDelegate); } + + emit ListCreated(party, merkleRoot); } + /** * @notice Mints a party card for a member from the party's list. * @param party The party from which the token is being minted @@ -91,7 +99,6 @@ contract PartyList { * @param nonces A numbers used to prevent double-minting * @param delegates The addresses to delegate voting power to * @param proofs A set of data used to verify the validity of the minting - * @return tokenIds The IDs of the newly minted tokens */ function batchMint( Party party, @@ -99,30 +106,31 @@ contract PartyList { uint96[] calldata votingPowers, uint256[] calldata nonces, address[] calldata delegates, - bytes32[][] calldata proofs - ) external returns (uint256[] memory tokenIds) { - tokenIds = new uint256[](members.length); + bytes32[][] calldata proofs, + bool revertOnFailure + ) external { for (uint256 i; i < members.length; ++i) { - tokenIds[0] = mint( - party, - members[i], - votingPowers[i], - nonces[i], - delegates[i], - proofs[i] + (bool s, bytes memory r) = address(this).delegatecall( + abi.encodeCall( + this.mint, + (party, members[i], votingPowers[i], nonces[i], delegates[i], proofs[i]) + ) ); + if (revertOnFailure && !s) { + r.rawRevert(); + } } } /** - * @notice Checks if a given member is allowed to mint from a party's list. - * @param party The party for which the `member` is being checked. - * @param member The address of the member to check. - * @param votingPower The voting power of the `member` in the `party`. - * @param nonce A nonce associated with the mint used to prevent double-minting. - * @param proof A set of data used to verify the validity of the minting. - * @return allowed A boolean indicating if the `member` is allowed to mint. - */ + * @notice Checks if a given member is allowed to mint from a party's list. + * @param party The party for which the `member` is being checked. + * @param member The address of the member to check. + * @param votingPower The voting power of the `member` in the `party`. + * @param nonce A nonce associated with the mint used to prevent double-minting. + * @param proof A set of data used to verify the validity of the minting. + * @return allowed A boolean indicating if the `member` is allowed to mint. + */ function isAllowed( Party party, address member, diff --git a/sol-tests/party/PartyFactory.t.sol b/sol-tests/party/PartyFactory.t.sol index 7810470f5..2cf7079ff 100644 --- a/sol-tests/party/PartyFactory.t.sol +++ b/sol-tests/party/PartyFactory.t.sol @@ -126,7 +126,7 @@ contract PartyFactoryTest is Test, TestUtils { uint96 votingPower = 0.1e18; uint256 nonce = _randomUint256(); - for (uint256 i; i < 3; ++i) { + for (uint256 i; i < preciousTokens.length; ++i) { preciousTokens[i].approve(address(factory), preciousTokenIds[i]); } @@ -154,5 +154,9 @@ contract PartyFactoryTest is Test, TestUtils { assertEq(party.balanceOf(member), 1); assertEq(party.delegationsByVoter(member), delegate); assertEq(party.votingPowerByTokenId(2), votingPower); + + for (uint256 i; i < preciousTokens.length; ++i) { + assertEq(preciousTokens[i].ownerOf(preciousTokenIds[i]), address(party)); + } } } From 27e530523b97893267e7ec34e8a12aed6915f531 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Wed, 21 Dec 2022 10:10:04 -0800 Subject: [PATCH 5/9] refactor: update `PartyList` mint args to use struct to avoid stack errors --- contracts/party/PartyList.sol | 82 ++++++++++++++---------------- sol-tests/party/PartyFactory.t.sol | 16 +++++- sol-tests/party/PartyList.t.sol | 47 +++++++++++++---- 3 files changed, 90 insertions(+), 55 deletions(-) diff --git a/contracts/party/PartyList.sol b/contracts/party/PartyList.sol index bf20037f6..91abdd37c 100644 --- a/contracts/party/PartyList.sol +++ b/contracts/party/PartyList.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.17; -import "solmate/utils/MerkleProofLib.sol"; +import "openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "./Party.sol"; import "./IPartyFactory.sol"; @@ -10,6 +10,21 @@ import "../utils/LibRawResult.sol"; contract PartyList { using LibRawResult for bytes; + struct MintArgs { + // The party from which the token is being minted + Party party; + // The address of the party member for whom the token is being minted + address member; + // The voting power of the token + uint96 votingPower; + // A number used to prevent double-minting + uint256 nonce; + // The address to delegate voting power to + address delegate; + // A set of data used to verify the validity of the minting + bytes32[] proof; + } + event ListCreated(Party party, bytes32 merkleRoot); error ListAlreadyExistsError(Party party, bytes32 merkleRoot); @@ -65,56 +80,35 @@ contract PartyList { /** * @notice Mints a party card for a member from the party's list. - * @param party The party from which the token is being minted - * @param member The address of the party member for whom the token is being minted - * @param votingPower The voting power of the token - * @param nonce A number used to prevent double-minting - * @param delegate The address to delegate voting power to - * @param proof A set of data used to verify the validity of the minting - * @return tokenId The ID of the newly minted token + * @param args The arguments for minting a party card. + * @return tokenId The ID of the newly minted party card. */ - function mint( - Party party, - address member, - uint96 votingPower, - uint256 nonce, - address delegate, - bytes32[] calldata proof - ) public returns (uint256 tokenId) { - (bool allowed, bytes32 leaf) = _verify(party, member, votingPower, nonce, proof); + function mint(MintArgs memory args) external returns (uint256 tokenId) { + (bool allowed, bytes32 leaf) = _verify( + args.party, + args.member, + args.votingPower, + args.nonce, + args.proof + ); - if (!allowed) revert InvalidProofError(proof); - if (minted[party][leaf]) revert AlreadyMintedError(leaf); + if (!allowed) revert InvalidProofError(args.proof); + if (minted[args.party][leaf]) revert AlreadyMintedError(leaf); - minted[party][leaf] = true; + minted[args.party][leaf] = true; - return party.mint(member, votingPower, delegate); + return args.party.mint(args.member, args.votingPower, args.delegate); } /** * @notice Mints party cards for members from the party's list. - * @param party The party from which the token is being minted - * @param members The address of party members for whom the tokens are being minted - * @param votingPowers The voting powers of the tokens - * @param nonces A numbers used to prevent double-minting - * @param delegates The addresses to delegate voting power to - * @param proofs A set of data used to verify the validity of the minting + * @param args The arguments for minting party cards. + * @param revertOnFailure A boolean indicating if the function should revert if a mint fails. */ - function batchMint( - Party party, - address[] calldata members, - uint96[] calldata votingPowers, - uint256[] calldata nonces, - address[] calldata delegates, - bytes32[][] calldata proofs, - bool revertOnFailure - ) external { - for (uint256 i; i < members.length; ++i) { + function batchMint(MintArgs[] memory args, bool revertOnFailure) external { + for (uint256 i; i < args.length; ++i) { (bool s, bytes memory r) = address(this).delegatecall( - abi.encodeCall( - this.mint, - (party, members[i], votingPowers[i], nonces[i], delegates[i], proofs[i]) - ) + abi.encodeCall(this.mint, (args[i])) ); if (revertOnFailure && !s) { r.rawRevert(); @@ -136,7 +130,7 @@ contract PartyList { address member, uint96 votingPower, uint256 nonce, - bytes32[] calldata proof + bytes32[] memory proof ) public view returns (bool) { (bool allowed, bytes32 leaf) = _verify(party, member, votingPower, nonce, proof); return allowed && !minted[party][leaf]; @@ -147,7 +141,7 @@ contract PartyList { address member, uint96 votingPower, uint256 nonce, - bytes32[] calldata proof + bytes32[] memory proof ) private view returns (bool allowed, bytes32 leaf) { assembly { // leaf = keccak256(abi.encodePacked(member, votingPower, nonce)) @@ -157,6 +151,6 @@ contract PartyList { leaf := keccak256(0, 64) } - allowed = MerkleProofLib.verify(proof, listMerkleRoots[party], leaf); + allowed = MerkleProof.verify(proof, listMerkleRoots[party], leaf); } } diff --git a/sol-tests/party/PartyFactory.t.sol b/sol-tests/party/PartyFactory.t.sol index 2cf7079ff..c60508810 100644 --- a/sol-tests/party/PartyFactory.t.sol +++ b/sol-tests/party/PartyFactory.t.sol @@ -58,7 +58,10 @@ contract PartyFactoryTest is Test, TestUtils { ) internal pure returns (bytes32 h) { assembly { mstore(0x00, keccak256(add(_preciousTokens, 0x20), mul(mload(_preciousTokens), 0x20))) - mstore(0x20, keccak256(add(_preciousTokenIds, 0x20), mul(mload(_preciousTokenIds), 0x20))) + mstore( + 0x20, + keccak256(add(_preciousTokenIds, 0x20), mul(mload(_preciousTokenIds), 0x20)) + ) h := keccak256(0x00, 0x40) } } @@ -150,7 +153,16 @@ contract PartyFactoryTest is Test, TestUtils { assertEq(party.delegationsByVoter(initOpts.creator), initOpts.creatorDelegate); assertEq(party.votingPowerByTokenId(1), initOpts.creatorVotingPower); - partyList.mint(party, member, votingPower, nonce, delegate, new bytes32[](0)); + partyList.mint( + PartyList.MintArgs({ + party: party, + member: member, + votingPower: votingPower, + nonce: nonce, + delegate: delegate, + proof: new bytes32[](0) + }) + ); assertEq(party.balanceOf(member), 1); assertEq(party.delegationsByVoter(member), delegate); assertEq(party.votingPowerByTokenId(2), votingPower); diff --git a/sol-tests/party/PartyList.t.sol b/sol-tests/party/PartyList.t.sol index e499c5afe..ce5dc1e99 100644 --- a/sol-tests/party/PartyList.t.sol +++ b/sol-tests/party/PartyList.t.sol @@ -57,12 +57,14 @@ contract PartyListTest is Test, TestUtils { bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); partyList.createList(party, merkleRoot, address(0), 0, address(0)); uint256 tokenId = partyList.mint( - party, - member, - votingPower, - nonce, - delegate, - new bytes32[](0) + PartyList.MintArgs({ + party: party, + member: member, + votingPower: votingPower, + nonce: nonce, + delegate: delegate, + proof: new bytes32[](0) + }) ); assertEq(party.ownerOf(tokenId), member); } @@ -74,9 +76,27 @@ contract PartyListTest is Test, TestUtils { address delegate = _randomAddress(); bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); partyList.createList(party, merkleRoot, address(0), 0, address(0)); - partyList.mint(party, member, votingPower, nonce, delegate, new bytes32[](0)); + partyList.mint( + PartyList.MintArgs({ + party: party, + member: member, + votingPower: votingPower, + nonce: nonce, + delegate: delegate, + proof: new bytes32[](0) + }) + ); vm.expectRevert(abi.encodeWithSelector(PartyList.AlreadyMintedError.selector, merkleRoot)); - partyList.mint(party, member, votingPower, nonce, delegate, new bytes32[](0)); + partyList.mint( + PartyList.MintArgs({ + party: party, + member: member, + votingPower: votingPower, + nonce: nonce, + delegate: delegate, + proof: new bytes32[](0) + }) + ); } function test_mint_invalidProof() public { @@ -88,6 +108,15 @@ contract PartyListTest is Test, TestUtils { bytes32[] memory proof = new bytes32[](1); partyList.createList(party, merkleRoot, address(0), 0, address(0)); vm.expectRevert(abi.encodeWithSelector(PartyList.InvalidProofError.selector, proof)); - partyList.mint(party, member, votingPower, nonce, delegate, proof); + partyList.mint( + PartyList.MintArgs({ + party: party, + member: member, + votingPower: votingPower, + nonce: nonce, + delegate: delegate, + proof: proof + }) + ); } } From 50e27a623dad9d5294df9ea6124d7901ad5ceb56 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Thu, 22 Dec 2022 10:51:53 -0800 Subject: [PATCH 6/9] refactor: use lib to hash precious list --- contracts/party/PartyGovernance.sol | 17 ++++------------- contracts/utils/LibPreciousList.sol | 17 +++++++++++++++++ sol-tests/party/PartyFactory.t.sol | 19 ++++--------------- sol-tests/party/PartyGovernanceUnit.t.sol | 5 ++--- 4 files changed, 27 insertions(+), 31 deletions(-) create mode 100644 contracts/utils/LibPreciousList.sol diff --git a/contracts/party/PartyGovernance.sol b/contracts/party/PartyGovernance.sol index a3a632a69..365d12547 100644 --- a/contracts/party/PartyGovernance.sol +++ b/contracts/party/PartyGovernance.sol @@ -12,6 +12,7 @@ import "../tokens/ERC1155Receiver.sol"; import "../utils/LibERC20Compat.sol"; import "../utils/LibRawResult.sol"; import "../utils/LibSafeCast.sol"; +import "../utils/LibPreciousList.sol"; import "../globals/IGlobals.sol"; import "../globals/LibGlobals.sol"; import "../proposals/IProposalExecutionEngine.sol"; @@ -1051,25 +1052,15 @@ abstract contract PartyGovernance is if (preciousTokens.length != preciousTokenIds.length) { revert MismatchedPreciousListLengths(); } - preciousListHash = _hashPreciousList(preciousTokens, preciousTokenIds); + preciousListHash = LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds); } function _isPreciousListCorrect( IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) private view returns (bool) { - return preciousListHash == _hashPreciousList(preciousTokens, preciousTokenIds); - } - - function _hashPreciousList( - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds - ) internal pure returns (bytes32 h) { - assembly { - mstore(0x00, keccak256(add(preciousTokens, 0x20), mul(mload(preciousTokens), 0x20))) - mstore(0x20, keccak256(add(preciousTokenIds, 0x20), mul(mload(preciousTokenIds), 0x20))) - h := keccak256(0x00, 0x40) - } + return + preciousListHash == LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds); } // Assert that the hash of a proposal matches expectedHash. diff --git a/contracts/utils/LibPreciousList.sol b/contracts/utils/LibPreciousList.sol new file mode 100644 index 000000000..ea4ae14e3 --- /dev/null +++ b/contracts/utils/LibPreciousList.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import "../tokens/IERC721.sol"; + +library LibPreciousList { + function hashPreciousList( + IERC721[] memory preciousTokens, + uint256[] memory preciousTokenIds + ) internal pure returns (bytes32 h) { + assembly { + mstore(0x00, keccak256(add(preciousTokens, 0x20), mul(mload(preciousTokens), 0x20))) + mstore(0x20, keccak256(add(preciousTokenIds, 0x20), mul(mload(preciousTokenIds), 0x20))) + h := keccak256(0x00, 0x40) + } + } +} diff --git a/sol-tests/party/PartyFactory.t.sol b/sol-tests/party/PartyFactory.t.sol index c60508810..e9d33cab3 100644 --- a/sol-tests/party/PartyFactory.t.sol +++ b/sol-tests/party/PartyFactory.t.sol @@ -52,20 +52,6 @@ contract PartyFactoryTest is Test, TestUtils { } } - function _hashPreciousList( - IERC721[] memory _preciousTokens, - uint256[] memory _preciousTokenIds - ) internal pure returns (bytes32 h) { - assembly { - mstore(0x00, keccak256(add(_preciousTokens, 0x20), mul(mload(_preciousTokens), 0x20))) - mstore( - 0x20, - keccak256(add(_preciousTokenIds, 0x20), mul(mload(_preciousTokenIds), 0x20)) - ) - h := keccak256(0x00, 0x40) - } - } - function testCreateParty( string memory randomStr, uint96 randomUint96, @@ -101,7 +87,10 @@ contract PartyFactoryTest is Test, TestUtils { assertEq(party.feeBps(), opts.governance.feeBps); assertEq(party.feeRecipient(), opts.governance.feeRecipient); assertEq(address(party.getProposalExecutionEngine()), address(eng)); - assertEq(party.preciousListHash(), _hashPreciousList(preciousTokens, preciousTokenIds)); + assertEq( + party.preciousListHash(), + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); } function testCreatePartyWithInvalidBps(uint16 passThresholdBps, uint16 feeBps) external { diff --git a/sol-tests/party/PartyGovernanceUnit.t.sol b/sol-tests/party/PartyGovernanceUnit.t.sol index 07e74a5c4..8fda36df8 100644 --- a/sol-tests/party/PartyGovernanceUnit.t.sol +++ b/sol-tests/party/PartyGovernanceUnit.t.sol @@ -204,7 +204,7 @@ contract TestablePartyGovernance is PartyGovernance { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) public pure returns (bytes32 h) { - h = _hashPreciousList(preciousTokens, preciousTokenIds); + h = LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds); } } @@ -2266,14 +2266,13 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); bytes32 expectedHash = keccak256( abi.encode( keccak256(abi.encode(preciousTokens[0], preciousTokens[1])), keccak256(abi.encode(preciousTokenIds[0], preciousTokenIds[1])) ) ); - bytes32 actualHash = gov.hashPreciousList(preciousTokens, preciousTokenIds); + bytes32 actualHash = LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds); assertEq(actualHash, expectedHash); } From 1ba6b30a64d7f10db2de37a76cc95d014d8370ae Mon Sep 17 00:00:00 2001 From: Brian Le Date: Thu, 22 Dec 2022 14:57:48 -0800 Subject: [PATCH 7/9] refactor: combine `PartyOptions` and `PartyInitData` --- contracts/crowdfund/Crowdfund.sol | 23 +- contracts/party/IPartyFactory.sol | 33 +- contracts/party/Party.sol | 36 +-- contracts/party/PartyFactory.sol | 51 +-- contracts/party/PartyGovernance.sol | 19 +- contracts/party/PartyGovernanceNFT.sol | 23 +- contracts/utils/LibPreciousList.sol | 6 + sol-tests/TestUsers.sol | 25 +- sol-tests/crowdfund/AuctionCrowdfund.t.sol | 26 +- sol-tests/crowdfund/BuyCrowdfund.t.sol | 26 +- .../crowdfund/CollectionBuyCrowdfund.t.sol | 31 +- sol-tests/crowdfund/Crowdfund.t.sol | 62 ++-- sol-tests/crowdfund/MockPartyFactory.sol | 22 +- sol-tests/party/Party.t.sol | 8 +- sol-tests/party/PartyFactory.t.sol | 61 ++-- sol-tests/party/PartyGovernanceNFTUnit.sol | 33 +- sol-tests/party/PartyGovernanceUnit.t.sol | 298 +++++++++++++----- tests/integration/system.ts | 7 +- tests/utils.ts | 17 + 19 files changed, 433 insertions(+), 374 deletions(-) diff --git a/contracts/crowdfund/Crowdfund.sol b/contracts/crowdfund/Crowdfund.sol index be5cf6394..519ad2028 100644 --- a/contracts/crowdfund/Crowdfund.sol +++ b/contracts/crowdfund/Crowdfund.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.17; import "../utils/LibAddress.sol"; import "../utils/LibRawResult.sol"; import "../utils/LibSafeCast.sol"; +import "../utils/LibPreciousList.sol"; import "../tokens/ERC721Receiver.sol"; import "../party/Party.sol"; import "../globals/IGlobals.sol"; @@ -96,6 +97,7 @@ abstract contract Crowdfund is Implementation, ERC721Receiver, CrowdfundNFT { error OnlyPartyHostError(); error OnlyContributorError(); error MissingHostsError(); + error MismatchedPreciousListLengths(); error OnlyPartyDaoError(address notDao); error OnlyPartyDaoOrHostError(address notDao); error OnlyWhenEmergencyActionsAllowedError(); @@ -414,12 +416,14 @@ abstract contract Crowdfund is Implementation, ERC721Receiver, CrowdfundNFT { } // Create a party. party = party_ = _getPartyFactory().createParty( - address(this), - Party.PartyOptions({ + Party.PartyOpts({ name: name, symbol: symbol, - // Indicates to the party to use the same customization preset as the crowdfund. + // ID of 0 typically not a valid option, but here it indicates + // to the party to use the same customization preset as the + // crowdfund. customizationPresetId: 0, + preciousListHash: _hashPreciousList(preciousTokens, preciousTokenIds), governance: PartyGovernance.GovernanceOpts({ hosts: governanceOpts.hosts, voteDuration: governanceOpts.voteDuration, @@ -430,8 +434,7 @@ abstract contract Crowdfund is Implementation, ERC721Receiver, CrowdfundNFT { feeRecipient: governanceOpts.feeRecipient }) }), - preciousTokens, - preciousTokenIds + address(this) ); // Transfer the acquired NFTs to the new party. for (uint256 i; i < preciousTokens.length; ++i) { @@ -479,6 +482,16 @@ abstract contract Crowdfund is Implementation, ERC721Receiver, CrowdfundNFT { } } + function _hashPreciousList( + IERC721[] memory preciousTokens, + uint256[] memory preciousTokenIds + ) private pure returns (bytes32 preciousListHash) { + if (preciousTokens.length != preciousTokenIds.length) { + revert MismatchedPreciousListLengths(); + } + preciousListHash = LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds); + } + function _getFinalContribution( address contributor ) internal view returns (uint256 ethUsed, uint256 ethOwed, uint256 votingPower) { diff --git a/contracts/party/IPartyFactory.sol b/contracts/party/IPartyFactory.sol index e5e3664d1..5e078211a 100644 --- a/contracts/party/IPartyFactory.sol +++ b/contracts/party/IPartyFactory.sol @@ -10,26 +10,19 @@ import "./Party.sol"; interface IPartyFactory { event PartyCreated( Party indexed party, - Party.PartyOptions opts, - IERC721[] preciousTokens, - uint256[] preciousTokenIds, + Party.PartyOpts opts, + address mintAuthority, address creator ); - struct PartyFromListInitOpts { + struct PartyFromListOpts { // Options used to initialize the party. These are fixed and cannot be // changed later. - Party.PartyOptions opts; + Party.PartyOpts partyOpts; // The tokens to transfer to the party. IERC721[] tokens; // The IDs associated with each token in `tokens`. uint256[] tokenIds; - // The tokens that are considered precious by the party. These are - // protected assets and are subject to extra restrictions in - // proposals vs other assets. - IERC721[] preciousTokens; - // The IDs associated with each token in `preciousTokens`. - uint256[] preciousTokenIds; // The address of the party creator to mint card for. address creator; // The voting power of the party creator. @@ -46,26 +39,18 @@ interface IPartyFactory { /// for party members by the authority (usually the crowdfund /// instance, if created from a successful crowdfund) using the /// `mint()` function. - /// @param authority The address that can call `mint()`. - /// @param opts Options used to initialize the party. These are fixed - /// and cannot be changed later. - /// @param preciousTokens The tokens that are considered precious by the - /// party.These are protected assets and are subject - /// to extra restrictions in proposals vs other - /// assets. - /// @param preciousTokenIds The IDs associated with each token in `preciousTokens`. + /// @param opts Options used to initialize the party. /// @return party The newly created `Party` instance. function createParty( - address authority, - Party.PartyOptions calldata opts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds + Party.PartyOpts memory opts, + address mintAuthority ) external returns (Party party); /// @notice Deploy a new party instance from a list of members and their /// voting powers. Afterwards, governance NFTs can be minted for /// party members through the `PartyList` contract using the `mint()` function. - function createPartyFromList(PartyFromListInitOpts memory initOpts) external returns (Party party); + /// @param opts Options used to initialize the party from a list. + function createPartyFromList(PartyFromListOpts memory opts) external returns (Party party); /// @notice The `Globals` contract storing global configuration values. This contract /// is immutable and it’s address will never change. diff --git a/contracts/party/Party.sol b/contracts/party/Party.sol index cc9d860b9..0822b3a22 100644 --- a/contracts/party/Party.sol +++ b/contracts/party/Party.sol @@ -9,20 +9,20 @@ import "./PartyGovernance.sol"; /// @notice The governance contract that also custodies the precious NFTs. This /// is also the Governance NFT 721 contract. contract Party is PartyGovernanceNFT { - // Arguments used to initialize the party. - struct PartyOptions { - PartyGovernance.GovernanceOpts governance; + struct PartyOpts { + // The name of the party. string name; + // The symbol of the party. string symbol; + // Preset ID to use for customizing rendering of governance NFTs. uint256 customizationPresetId; - } - - // Arguments used to initialize the `PartyGovernanceNFT`. - struct PartyInitData { - PartyOptions options; - IERC721[] preciousTokens; - uint256[] preciousTokenIds; - address mintAuthority; + // Hash of tokens and token IDs that are considered precious by the + // party. These are protected assets and are subject to extra + // restrictions in proposals vs other assets. This is used to verify + // that the list of precious tokens and token IDs is correct. + bytes32 preciousListHash; + // Options used to initialize the party governance. + PartyGovernance.GovernanceOpts governance; } // Set the `Globals` contract. @@ -30,17 +30,9 @@ contract Party is PartyGovernanceNFT { /// @notice Initializer to be delegatecalled by `Proxy` constructor. Will /// revert if called outside the constructor. - /// @param initData Options used to initialize the party governance. - function initialize(PartyInitData memory initData) external onlyConstructor { - PartyGovernanceNFT._initialize( - initData.options.name, - initData.options.symbol, - initData.options.customizationPresetId, - initData.options.governance, - initData.preciousTokens, - initData.preciousTokenIds, - initData.mintAuthority - ); + /// @param opts Options used to initialize the party governance. + function initialize(PartyOpts memory opts, address mintAuthority) external onlyConstructor { + PartyGovernanceNFT._initialize(opts, mintAuthority); } receive() external payable {} diff --git a/contracts/party/PartyFactory.sol b/contracts/party/PartyFactory.sol index f4c6c8057..e7b7697c6 100644 --- a/contracts/party/PartyFactory.sol +++ b/contracts/party/PartyFactory.sol @@ -25,65 +25,48 @@ contract PartyFactory is IPartyFactory { } function createParty( - address authority, - Party.PartyOptions memory opts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds + Party.PartyOpts memory opts, + address mintAuthority ) external returns (Party party) { // Ensure a valid authority is set to mint governance NFTs. - if (authority == address(0)) { - revert InvalidAuthorityError(authority); + if (mintAuthority == address(0)) { + revert InvalidAuthorityError(mintAuthority); } // Create the party. - return _createParty(authority, opts, preciousTokens, preciousTokenIds); + return _createParty(opts, mintAuthority); } - function createPartyFromList( - PartyFromListInitOpts memory initOpts - ) public returns (Party party) { + function createPartyFromList(PartyFromListOpts memory opts) public returns (Party party) { // Create the party. - party = _createParty( - address(PARTY_LIST), - initOpts.opts, - initOpts.preciousTokens, - initOpts.preciousTokenIds - ); + party = _createParty(opts.partyOpts, address(PARTY_LIST)); // Create the list used to determine the initial list of members and voting // power for each member and mint the party creator their card. PARTY_LIST.createList( party, - initOpts.listMerkleRoot, - initOpts.creator, - initOpts.creatorVotingPower, - initOpts.creatorDelegate + opts.listMerkleRoot, + opts.creator, + opts.creatorVotingPower, + opts.creatorDelegate ); // Transfer the tokens to the party. - for (uint256 i; i < initOpts.tokens.length; ++i) { - initOpts.tokens[i].transferFrom(msg.sender, address(party), initOpts.tokenIds[i]); + for (uint256 i; i < opts.tokens.length; ++i) { + opts.tokens[i].transferFrom(msg.sender, address(party), opts.tokenIds[i]); } } function _createParty( - address authority, - Party.PartyOptions memory opts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds + Party.PartyOpts memory opts, + address mintAuthority ) private returns (Party party) { // Deploy a new proxified `Party` instance. - Party.PartyInitData memory initData = Party.PartyInitData({ - options: opts, - preciousTokens: preciousTokens, - preciousTokenIds: preciousTokenIds, - mintAuthority: authority - }); party = Party( payable( new Proxy( GLOBALS.getImplementation(LibGlobals.GLOBAL_PARTY_IMPL), - abi.encodeCall(Party.initialize, (initData)) + abi.encodeCall(Party.initialize, (opts, mintAuthority)) ) ) ); - emit PartyCreated(party, opts, preciousTokens, preciousTokenIds, msg.sender); + emit PartyCreated(party, opts, mintAuthority, msg.sender); } } diff --git a/contracts/party/PartyGovernance.sol b/contracts/party/PartyGovernance.sol index 365d12547..3b5e01264 100644 --- a/contracts/party/PartyGovernance.sol +++ b/contracts/party/PartyGovernance.sol @@ -166,7 +166,6 @@ abstract contract PartyGovernance is event HostStatusTransferred(address oldHost, address newHost); event EmergencyExecuteDisabled(); - error MismatchedPreciousListLengths(); error BadProposalStatusError(ProposalStatus status); error BadProposalHashError(bytes32 proposalHash, bytes32 actualHash); error ExecutionTimeExceededError(uint40 maxExecutableTime, uint40 timestamp); @@ -285,11 +284,7 @@ abstract contract PartyGovernance is } // Initialize storage for proxy contracts and initialize the proposal execution engine. - function _initialize( - GovernanceOpts memory opts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds - ) internal virtual { + function _initialize(GovernanceOpts memory opts, bytes32 preciousListHash_) internal virtual { // Check BPS are valid. if (opts.feeBps > 1e4) { revert InvalidBpsError(opts.feeBps); @@ -313,7 +308,7 @@ abstract contract PartyGovernance is feeBps = opts.feeBps; feeRecipient = opts.feeRecipient; // Set the precious list. - _setPreciousList(preciousTokens, preciousTokenIds); + preciousListHash = preciousListHash_; // Set the party hosts. for (uint256 i = 0; i < opts.hosts.length; ++i) { isHost[opts.hosts[i]] = true; @@ -1045,16 +1040,6 @@ abstract contract PartyGovernance is return (uint256(voteCount) * 1e4) / uint256(totalVotingPower) >= uint256(passThresholdBps); } - function _setPreciousList( - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds - ) private { - if (preciousTokens.length != preciousTokenIds.length) { - revert MismatchedPreciousListLengths(); - } - preciousListHash = LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds); - } - function _isPreciousListCorrect( IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds diff --git a/contracts/party/PartyGovernanceNFT.sol b/contracts/party/PartyGovernanceNFT.sol index 3924e49c1..0166a4df1 100644 --- a/contracts/party/PartyGovernanceNFT.sol +++ b/contracts/party/PartyGovernanceNFT.sol @@ -7,6 +7,7 @@ import "openzeppelin/contracts/interfaces/IERC2981.sol"; import "../globals/IGlobals.sol"; import "../tokens/IERC721.sol"; import "../vendor/solmate/ERC721.sol"; +import "./Party.sol"; import "./PartyGovernance.sol"; import "../renderers/RendererStorage.sol"; @@ -47,22 +48,14 @@ contract PartyGovernanceNFT is PartyGovernance, ERC721, IERC2981 { } // Initialize storage for proxy contracts. - function _initialize( - string memory name_, - string memory symbol_, - uint256 customizationPresetId, - PartyGovernance.GovernanceOpts memory governanceOpts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds, - address mintAuthority_ - ) internal { - PartyGovernance._initialize(governanceOpts, preciousTokens, preciousTokenIds); - name = name_; - symbol = symbol_; - mintAuthority = mintAuthority_; - if (customizationPresetId != 0) { + function _initialize(Party.PartyOpts memory opts, address authority) internal { + PartyGovernance._initialize(opts.governance, opts.preciousListHash); + name = opts.name; + symbol = opts.symbol; + mintAuthority = authority; + if (opts.customizationPresetId != 0) { RendererStorage(_GLOBALS.getAddress(LibGlobals.GLOBAL_RENDERER_STORAGE)) - .useCustomizationPreset(customizationPresetId); + .useCustomizationPreset(opts.customizationPresetId); } } diff --git a/contracts/utils/LibPreciousList.sol b/contracts/utils/LibPreciousList.sol index ea4ae14e3..1a50f1fda 100644 --- a/contracts/utils/LibPreciousList.sol +++ b/contracts/utils/LibPreciousList.sol @@ -9,6 +9,12 @@ library LibPreciousList { uint256[] memory preciousTokenIds ) internal pure returns (bytes32 h) { assembly { + // keccak256( + // abi.encode( + // keccak256(abi.encode(preciousTokens[0], preciousTokens[1], ...)), + // keccak256(abi.encode(preciousTokenIds[0], preciousTokenIds[1], ...)) + // ) + // ); mstore(0x00, keccak256(add(preciousTokens, 0x20), mul(mload(preciousTokens), 0x20))) mstore(0x20, keccak256(add(preciousTokenIds, 0x20), mul(mload(preciousTokenIds), 0x20))) h := keccak256(0x00, 0x40) diff --git a/sol-tests/TestUsers.sol b/sol-tests/TestUsers.sol index 028b3d863..8ed96482c 100644 --- a/sol-tests/TestUsers.sol +++ b/sol-tests/TestUsers.sol @@ -81,6 +81,12 @@ contract PartyAdmin is Test { hosts[0] = opts.host1; hosts[1] = opts.host2; + IERC721[] memory preciousTokens = new IERC721[](1); + preciousTokens[0] = IERC721(opts.preciousTokenAddress); + + uint256[] memory preciousTokenIds = new uint256[](1); + preciousTokenIds[0] = opts.preciousTokenId; + PartyGovernance.GovernanceOpts memory govOpts = PartyGovernance.GovernanceOpts({ hosts: hosts, voteDuration: 99, @@ -90,24 +96,15 @@ contract PartyAdmin is Test { feeRecipient: opts.feeRecipient, feeBps: opts.feeBps }); - Party.PartyOptions memory po = Party.PartyOptions({ - governance: govOpts, + Party.PartyOpts memory po = Party.PartyOpts({ name: "Dope party", symbol: "DOPE", - customizationPresetId: 0 + customizationPresetId: 0, + preciousListHash: LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds), + governance: govOpts }); - IERC721[] memory preciousTokens = new IERC721[](1); - preciousTokens[0] = IERC721(opts.preciousTokenAddress); - - uint256[] memory preciousTokenIds = new uint256[](1); - preciousTokenIds[0] = opts.preciousTokenId; - Party party = _partyFactory.createParty( - address(this), - po, - preciousTokens, - preciousTokenIds - ); + Party party = _partyFactory.createParty(po, address(this)); return (party, preciousTokens, preciousTokenIds); } diff --git a/sol-tests/crowdfund/AuctionCrowdfund.t.sol b/sol-tests/crowdfund/AuctionCrowdfund.t.sol index 3653a636c..4f25c9c92 100644 --- a/sol-tests/crowdfund/AuctionCrowdfund.t.sol +++ b/sol-tests/crowdfund/AuctionCrowdfund.t.sol @@ -17,13 +17,7 @@ import "./MockParty.sol"; import "./MockMarketWrapper.sol"; contract AuctionCrowdfundTest is Test, TestUtils { - event MockPartyFactoryCreateParty( - address caller, - address authority, - Party.PartyOptions opts, - IERC721[] preciousTokens, - uint256[] preciousTokenIds - ); + event MockPartyFactoryCreateParty(address caller, Party.PartyOpts opts, address authority); event MockMint(address caller, address owner, uint256 amount, address delegate); @@ -126,14 +120,20 @@ contract AuctionCrowdfundTest is Test, TestUtils { ); } - function _createExpectedPartyOptions( + function _createExpectedPartyOpts( + ERC721 preciousToken, + uint256 preciousTokenId, uint256 finalPrice - ) private view returns (Party.PartyOptions memory opts) { + ) private view returns (Party.PartyOpts memory opts) { return - Party.PartyOptions({ + Party.PartyOpts({ name: defaultName, symbol: defaultSymbol, customizationPresetId: 0, + preciousListHash: LibPreciousList.hashPreciousList( + _toERC721Array(preciousToken), + _toUint256Array(preciousTokenId) + ), governance: PartyGovernance.GovernanceOpts({ hosts: defaultGovernanceOpts.hosts, voteDuration: defaultGovernanceOpts.voteDuration, @@ -167,10 +167,8 @@ contract AuctionCrowdfundTest is Test, TestUtils { _expectEmit0(); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(1337), - _toERC721Array(tokenToBuy), - _toUint256Array(tokenId) + _createExpectedPartyOpts(ERC721(address(tokenToBuy)), tokenId, 1337), + address(cf) ); Party party_ = cf.finalize(defaultGovernanceOpts); assertEq(address(party_), address(party)); diff --git a/sol-tests/crowdfund/BuyCrowdfund.t.sol b/sol-tests/crowdfund/BuyCrowdfund.t.sol index 313be9996..76404d815 100644 --- a/sol-tests/crowdfund/BuyCrowdfund.t.sol +++ b/sol-tests/crowdfund/BuyCrowdfund.t.sol @@ -16,13 +16,7 @@ import "./MockPartyFactory.sol"; import "./TestERC721Vault.sol"; contract BuyCrowdfundTest is Test, TestUtils { - event MockPartyFactoryCreateParty( - address caller, - address authority, - Party.PartyOptions opts, - IERC721[] preciousTokens, - uint256[] preciousTokenIds - ); + event MockPartyFactoryCreateParty(address caller, Party.PartyOpts opts, address authority); event MockMint(address caller, address owner, uint256 amount, address delegate); @@ -114,14 +108,20 @@ contract BuyCrowdfundTest is Test, TestUtils { ); } - function _createExpectedPartyOptions( + function _createExpectedPartyOpts( + ERC721 preciousToken, + uint256 preciousTokenId, uint256 finalPrice - ) private view returns (Party.PartyOptions memory opts) { + ) private view returns (Party.PartyOpts memory opts) { return - Party.PartyOptions({ + Party.PartyOpts({ name: defaultName, symbol: defaultSymbol, customizationPresetId: 0, + preciousListHash: LibPreciousList.hashPreciousList( + _toERC721Array(preciousToken), + _toUint256Array(preciousTokenId) + ), governance: PartyGovernance.GovernanceOpts({ hosts: defaultGovernanceOpts.hosts, voteDuration: defaultGovernanceOpts.voteDuration, @@ -148,10 +148,8 @@ contract BuyCrowdfundTest is Test, TestUtils { vm.expectEmit(false, false, false, true); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(0.5e18), - _toERC721Array(erc721Vault.token()), - _toUint256Array(tokenId) + _createExpectedPartyOpts(ERC721(address(erc721Vault.token())), tokenId, 0.5e18), + address(cf) ); Party party_ = cf.buy( payable(address(erc721Vault)), diff --git a/sol-tests/crowdfund/CollectionBuyCrowdfund.t.sol b/sol-tests/crowdfund/CollectionBuyCrowdfund.t.sol index 3a6f6f819..5209f3c02 100644 --- a/sol-tests/crowdfund/CollectionBuyCrowdfund.t.sol +++ b/sol-tests/crowdfund/CollectionBuyCrowdfund.t.sol @@ -16,13 +16,7 @@ import "./MockParty.sol"; import "./TestERC721Vault.sol"; contract CollectionBuyCrowdfundTest is Test, TestUtils { - event MockPartyFactoryCreateParty( - address caller, - address authority, - Party.PartyOptions opts, - IERC721[] preciousTokens, - uint256[] preciousTokenIds - ); + event MockPartyFactoryCreateParty(address caller, Party.PartyOpts opts, address authority); event MockMint(address caller, address owner, uint256 amount, address delegate); @@ -94,15 +88,21 @@ contract CollectionBuyCrowdfundTest is Test, TestUtils { ); } - function _createExpectedPartyOptions( + function _createExpectedPartyOpts( address[] memory hosts, + ERC721 preciousToken, + uint256 preciousTokenId, uint256 finalPrice - ) private view returns (Party.PartyOptions memory opts) { + ) private view returns (Party.PartyOpts memory opts) { return - Party.PartyOptions({ + Party.PartyOpts({ name: defaultName, symbol: defaultSymbol, customizationPresetId: 0, + preciousListHash: LibPreciousList.hashPreciousList( + _toERC721Array(preciousToken), + _toUint256Array(preciousTokenId) + ), governance: PartyGovernance.GovernanceOpts({ hosts: hosts, voteDuration: defaultGovernanceOpts.voteDuration, @@ -133,10 +133,13 @@ contract CollectionBuyCrowdfundTest is Test, TestUtils { vm.expectEmit(false, false, false, true); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(_toAddressArray(host), 0.5e18), - _toERC721Array(erc721Vault.token()), - _toUint256Array(tokenId) + _createExpectedPartyOpts( + _toAddressArray(host), + ERC721(address(erc721Vault.token())), + tokenId, + 0.5e18 + ), + address(cf) ); vm.prank(host); Party party_ = cf.buy( diff --git a/sol-tests/crowdfund/Crowdfund.t.sol b/sol-tests/crowdfund/Crowdfund.t.sol index 083817768..a276ab248 100644 --- a/sol-tests/crowdfund/Crowdfund.t.sol +++ b/sol-tests/crowdfund/Crowdfund.t.sol @@ -31,13 +31,7 @@ contract BadERC721Receiver { } contract CrowdfundTest is Test, TestUtils { - event MockPartyFactoryCreateParty( - address caller, - address authority, - Party.PartyOptions opts, - IERC721[] preciousTokens, - uint256[] preciousTokenIds - ); + event MockPartyFactoryCreateParty(address caller, Party.PartyOpts opts, address authority); event MockMint(address caller, address owner, uint256 amount, address delegate); @@ -169,16 +163,22 @@ contract CrowdfundTest is Test, TestUtils { return _createCrowdfund(initialContribution, address(this), defaultInitialDelegate, 0); } - function _createExpectedPartyOptions( + function _createExpectedPartyOpts( TestableCrowdfund cf, + IERC721[] memory preciousTokens, + uint256[] memory preciousTokenIds, uint256 finalPrice - ) private view returns (Party.PartyOptions memory opts) { + ) private view returns (Party.PartyOpts memory opts) { Crowdfund.FixedGovernanceOpts memory govOpts = cf.getFixedGovernanceOpts(); return - Party.PartyOptions({ + Party.PartyOpts({ name: defaultName, symbol: defaultSymbol, customizationPresetId: 0, + preciousListHash: LibPreciousList.hashPreciousList( + preciousTokens, + preciousTokenIds + ), governance: PartyGovernance.GovernanceOpts({ hosts: govOpts.hosts, voteDuration: govOpts.voteDuration, @@ -259,10 +259,8 @@ contract CrowdfundTest is Test, TestUtils { vm.expectEmit(false, false, false, true); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(cf, 1e18), - erc721Tokens, - erc721TokenIds + _createExpectedPartyOpts(cf, erc721Tokens, erc721TokenIds, 1e18), + address(cf) ); Party party_ = cf.testSetWon(1e18, defaultGovernanceOpts, erc721Tokens, erc721TokenIds); assertEq(address(party_), address(party)); @@ -298,10 +296,8 @@ contract CrowdfundTest is Test, TestUtils { vm.expectEmit(false, false, false, true); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(cf, 1.5e18), - erc721Tokens, - erc721TokenIds + _createExpectedPartyOpts(cf, erc721Tokens, erc721TokenIds, 1.5e18), + address(cf) ); Party party_ = cf.testSetWon(1.5e18, defaultGovernanceOpts, erc721Tokens, erc721TokenIds); assertEq(address(party_), address(party)); @@ -342,10 +338,8 @@ contract CrowdfundTest is Test, TestUtils { vm.expectEmit(false, false, false, true); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(cf, 1.25e18), - erc721Tokens, - erc721TokenIds + _createExpectedPartyOpts(cf, erc721Tokens, erc721TokenIds, 1.25e18), + address(cf) ); cf.testSetWon(1.25e18, defaultGovernanceOpts, erc721Tokens, erc721TokenIds); // contributor1 burns tokens @@ -390,10 +384,8 @@ contract CrowdfundTest is Test, TestUtils { vm.expectEmit(false, false, false, true); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(cf, 1.65e18), - erc721Tokens, - erc721TokenIds + _createExpectedPartyOpts(cf, erc721Tokens, erc721TokenIds, 1.65e18), + address(cf) ); cf.testSetWon(1.65e18, defaultGovernanceOpts, erc721Tokens, erc721TokenIds); // contributor1 burns tokens @@ -428,10 +420,8 @@ contract CrowdfundTest is Test, TestUtils { vm.expectEmit(false, false, false, true); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(cf, 0), - erc721Tokens, - erc721TokenIds + _createExpectedPartyOpts(cf, erc721Tokens, erc721TokenIds, 0), + address(cf) ); Party party_ = cf.testSetWon(0, defaultGovernanceOpts, erc721Tokens, erc721TokenIds); assertEq(address(party_), address(party)); @@ -813,10 +803,8 @@ contract CrowdfundTest is Test, TestUtils { vm.expectEmit(false, false, false, true); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(cf, 1e18), - erc721Tokens, - erc721TokenIds + _createExpectedPartyOpts(cf, erc721Tokens, erc721TokenIds, 1e18), + address(cf) ); Party party_ = cf.testSetWon(1e18, defaultGovernanceOpts, erc721Tokens, erc721TokenIds); assertEq(address(party_), address(party)); @@ -866,10 +854,8 @@ contract CrowdfundTest is Test, TestUtils { vm.expectEmit(false, false, false, true); emit MockPartyFactoryCreateParty( address(cf), - address(cf), - _createExpectedPartyOptions(cf, 1e18), - erc721Tokens, - erc721TokenIds + _createExpectedPartyOpts(cf, erc721Tokens, erc721TokenIds, 1e18), + address(cf) ); Party party_ = cf.testSetWon(1e18, defaultGovernanceOpts, erc721Tokens, erc721TokenIds); assertEq(address(party_), address(party)); diff --git a/sol-tests/crowdfund/MockPartyFactory.sol b/sol-tests/crowdfund/MockPartyFactory.sol index 06152faa7..bf7cda306 100644 --- a/sol-tests/crowdfund/MockPartyFactory.sol +++ b/sol-tests/crowdfund/MockPartyFactory.sol @@ -7,31 +7,17 @@ import "./MockParty.sol"; contract MockPartyFactory { MockParty public mockParty = new MockParty(); - event MockPartyFactoryCreateParty( - address caller, - address authority, - Party.PartyOptions opts, - IERC721[] preciousTokens, - uint256[] preciousTokenIds - ); + event MockPartyFactoryCreateParty(address caller, Party.PartyOpts opts, address authority); function GLOBALS() external pure returns (IGlobals g) {} function abdicate(Party) external {} function createParty( - address authority, - Party.PartyOptions memory opts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds + Party.PartyOpts memory opts, + address authority ) external returns (Party party) { - emit MockPartyFactoryCreateParty( - msg.sender, - authority, - opts, - preciousTokens, - preciousTokenIds - ); + emit MockPartyFactoryCreateParty(msg.sender, opts, authority); return Party(payable(address(mockParty))); } } diff --git a/sol-tests/party/Party.t.sol b/sol-tests/party/Party.t.sol index 69d374bd6..0ca1e7fb7 100644 --- a/sol-tests/party/Party.t.sol +++ b/sol-tests/party/Party.t.sol @@ -20,11 +20,13 @@ contract PartyTest is Test, TestUtils { } function test_cannotReinitialize() external { - Party.PartyInitData memory initData; + Party.PartyOpts memory opts; Party party = Party( - payable(address(new Proxy(partyImpl, abi.encodeCall(Party.initialize, initData)))) + payable( + address(new Proxy(partyImpl, abi.encodeCall(Party.initialize, (opts, address(0))))) + ) ); vm.expectRevert(abi.encodeWithSelector(Implementation.OnlyConstructorError.selector)); - party.initialize(initData); + party.initialize(opts, address(0)); } } diff --git a/sol-tests/party/PartyFactory.t.sol b/sol-tests/party/PartyFactory.t.sol index e9d33cab3..8c44cb500 100644 --- a/sol-tests/party/PartyFactory.t.sol +++ b/sol-tests/party/PartyFactory.t.sol @@ -16,20 +16,19 @@ contract PartyFactoryTest is Test, TestUtils { PartyList partyList = new PartyList(globals); PartyFactory factory = new PartyFactory(globals, partyList); ProposalExecutionEngine eng; - Party.PartyOptions defaultPartyOptions; + Party.PartyOpts defaultPartyOpts; IERC721[] preciousTokens; uint256[] preciousTokenIds; - bytes32 preciousListHash; constructor() { - defaultPartyOptions.name = "PARTY"; - defaultPartyOptions.symbol = "PR-T"; - defaultPartyOptions.governance.hosts.push(_randomAddress()); - defaultPartyOptions.governance.hosts.push(_randomAddress()); - defaultPartyOptions.governance.voteDuration = 1 days; - defaultPartyOptions.governance.executionDelay = 8 hours; - defaultPartyOptions.governance.passThresholdBps = 0.51e4; - defaultPartyOptions.governance.totalVotingPower = 100e18; + defaultPartyOpts.name = "PARTY"; + defaultPartyOpts.symbol = "PR-T"; + defaultPartyOpts.governance.hosts.push(_randomAddress()); + defaultPartyOpts.governance.hosts.push(_randomAddress()); + defaultPartyOpts.governance.voteDuration = 1 days; + defaultPartyOpts.governance.executionDelay = 8 hours; + defaultPartyOpts.governance.passThresholdBps = 0.51e4; + defaultPartyOpts.governance.totalVotingPower = 100e18; eng = new ProposalExecutionEngine( globals, @@ -61,7 +60,15 @@ contract PartyFactoryTest is Test, TestUtils { vm.assume(randomBps <= 1e4); address authority = _randomAddress(); - Party.PartyOptions memory opts = Party.PartyOptions({ + bytes32 preciousListHash = LibPreciousList.hashPreciousList( + preciousTokens, + preciousTokenIds + ); + Party.PartyOpts memory opts = Party.PartyOpts({ + name: randomStr, + symbol: randomStr, + customizationPresetId: 0, + preciousListHash: preciousListHash, governance: PartyGovernance.GovernanceOpts({ hosts: _toAddressArray(_randomAddress()), voteDuration: randomUint40, @@ -70,12 +77,9 @@ contract PartyFactoryTest is Test, TestUtils { totalVotingPower: randomUint96, feeBps: randomBps, feeRecipient: payable(_randomAddress()) - }), - name: randomStr, - symbol: randomStr, - customizationPresetId: 0 + }) }); - Party party = factory.createParty(authority, opts, preciousTokens, preciousTokenIds); + Party party = factory.createParty(opts, authority); assertEq(party.name(), opts.name); assertEq(party.symbol(), opts.symbol); assertEq(party.mintAuthority(), authority); @@ -87,10 +91,7 @@ contract PartyFactoryTest is Test, TestUtils { assertEq(party.feeBps(), opts.governance.feeBps); assertEq(party.feeRecipient(), opts.governance.feeRecipient); assertEq(address(party.getProposalExecutionEngine()), address(eng)); - assertEq( - party.preciousListHash(), - LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) - ); + assertEq(party.preciousListHash(), preciousListHash); } function testCreatePartyWithInvalidBps(uint16 passThresholdBps, uint16 feeBps) external { @@ -99,7 +100,7 @@ contract PartyFactoryTest is Test, TestUtils { address authority = _randomAddress(); - Party.PartyOptions memory opts = defaultPartyOptions; + Party.PartyOpts memory opts = defaultPartyOpts; opts.governance.feeBps = feeBps; opts.governance.passThresholdBps = passThresholdBps; @@ -109,7 +110,7 @@ contract PartyFactoryTest is Test, TestUtils { feeBps > 1e4 ? feeBps : passThresholdBps ) ); - factory.createParty(authority, opts, preciousTokens, preciousTokenIds); + factory.createParty(opts, authority); } function testCreatePartyFromList() external { @@ -122,25 +123,23 @@ contract PartyFactoryTest is Test, TestUtils { preciousTokens[i].approve(address(factory), preciousTokenIds[i]); } - IPartyFactory.PartyFromListInitOpts memory initOpts = IPartyFactory.PartyFromListInitOpts({ - opts: defaultPartyOptions, + IPartyFactory.PartyFromListOpts memory opts = IPartyFactory.PartyFromListOpts({ + partyOpts: defaultPartyOpts, tokens: preciousTokens, tokenIds: preciousTokenIds, - preciousTokens: preciousTokens, - preciousTokenIds: preciousTokenIds, creator: _randomAddress(), creatorVotingPower: 0.3e18, creatorDelegate: _randomAddress(), listMerkleRoot: keccak256(abi.encodePacked(member, votingPower, nonce)) }); - Party party = factory.createPartyFromList(initOpts); + Party party = factory.createPartyFromList(opts); assertEq(party.mintAuthority(), address(partyList)); - assertEq(partyList.listMerkleRoots(party), initOpts.listMerkleRoot); - assertEq(party.balanceOf(initOpts.creator), 1); - assertEq(party.delegationsByVoter(initOpts.creator), initOpts.creatorDelegate); - assertEq(party.votingPowerByTokenId(1), initOpts.creatorVotingPower); + assertEq(partyList.listMerkleRoots(party), opts.listMerkleRoot); + assertEq(party.balanceOf(opts.creator), 1); + assertEq(party.delegationsByVoter(opts.creator), opts.creatorDelegate); + assertEq(party.votingPowerByTokenId(1), opts.creatorVotingPower); partyList.mint( PartyList.MintArgs({ diff --git a/sol-tests/party/PartyGovernanceNFTUnit.sol b/sol-tests/party/PartyGovernanceNFTUnit.sol index a0613ab39..a24d4aa40 100644 --- a/sol-tests/party/PartyGovernanceNFTUnit.sol +++ b/sol-tests/party/PartyGovernanceNFTUnit.sol @@ -13,24 +13,8 @@ import "../TestUtils.sol"; contract TestablePartyGovernanceNFT is PartyGovernanceNFT { constructor() PartyGovernanceNFT(new Globals(msg.sender)) {} - function initialize( - string memory name_, - string memory symbol_, - uint256 customizationPresetId, - PartyGovernance.GovernanceOpts memory governanceOpts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds, - address mintAuthority_ - ) external { - _initialize( - name_, - symbol_, - customizationPresetId, - governanceOpts, - preciousTokens, - preciousTokenIds, - mintAuthority_ - ); + function initialize(Party.PartyOpts memory opts, address authority) external { + _initialize(opts, authority); } function getCurrentVotingPower(address voter) external view returns (uint96 vp) { @@ -49,12 +33,13 @@ contract PartyGovernanceNFTUnitTest is TestUtils { function _initGovernance() private { defaultGovernanceOpts.totalVotingPower = 1e18; nft.initialize( - "TEST", - "TST", - 0, - defaultGovernanceOpts, - new IERC721[](0), - new uint256[](0), + Party.PartyOpts({ + name: "TEST", + symbol: "TST", + customizationPresetId: 0, + preciousListHash: bytes32(0), + governance: defaultGovernanceOpts + }), address(this) ); } diff --git a/sol-tests/party/PartyGovernanceUnit.t.sol b/sol-tests/party/PartyGovernanceUnit.t.sol index 8fda36df8..e9b21a4ab 100644 --- a/sol-tests/party/PartyGovernanceUnit.t.sol +++ b/sol-tests/party/PartyGovernanceUnit.t.sol @@ -148,10 +148,9 @@ contract TestablePartyGovernance is PartyGovernance { constructor( IGlobals globals, GovernanceOpts memory opts, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds + bytes32 preciousListHash ) PartyGovernance(globals) { - _initialize(opts, preciousTokens, preciousTokenIds); + _initialize(opts, preciousListHash); } function rawAdjustVotingPower( @@ -199,13 +198,6 @@ contract TestablePartyGovernance is PartyGovernance { h = getProposalHash(proposal); h = getProposalHash(proposal); } - - function hashPreciousList( - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds - ) public pure returns (bytes32 h) { - h = LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds); - } } contract PartyGovernanceUnitTest is Test, TestUtils { @@ -270,17 +262,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { function _createGovernance( uint96 totalVotingPower, - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds + bytes32 preciousListHash ) private returns (TestablePartyGovernance gov) { defaultGovernanceOpts.totalVotingPower = totalVotingPower; - return - new TestablePartyGovernance( - globals, - defaultGovernanceOpts, - preciousTokens, - preciousTokenIds - ); + return new TestablePartyGovernance(globals, defaultGovernanceOpts, preciousListHash); } function _createProposal( @@ -358,7 +343,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -415,7 +403,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -494,7 +485,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 100/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 100e18, address(0)); @@ -545,7 +539,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter1 = _randomAddress(); address undelegatedVoter2 = _randomAddress(); // undelegatedVoter1 has 75/100 intrinsic VP (delegated to no one/self) @@ -602,7 +599,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 99.99% of total voting supply. uint256 vp = (100e18 * 0.9999e4) / 1e4; @@ -652,7 +652,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 50/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 50e18, address(0)); @@ -696,7 +699,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -730,7 +736,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -768,7 +777,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -811,7 +823,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -850,7 +865,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter = _randomAddress(); // voter has 100% intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(voter, 100e18, address(0)); @@ -889,7 +907,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter = _randomAddress(); // voter has 100% intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(voter, 100e18, address(0)); @@ -926,7 +947,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter = _randomAddress(); // voter has 100% intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(voter, 100e18, address(0)); @@ -970,7 +994,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 50/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 50e18, address(0)); @@ -998,7 +1025,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 50/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 50e18, address(0)); @@ -1021,7 +1051,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 50/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 50e18, address(0)); @@ -1066,7 +1099,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -1112,7 +1148,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -1153,7 +1192,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -1194,7 +1236,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -1237,7 +1282,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { ) = _createPreciousTokens(2); defaultGovernanceOpts.executionDelay = 60; defaultGovernanceOpts.voteDuration = 61; - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 50/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 50e18, address(0)); @@ -1276,7 +1324,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address delegate = _randomAddress(); address delegatedVoter = _randomAddress(); address undelegatedVoter = _randomAddress(); @@ -1325,7 +1376,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address delegate = _randomAddress(); address delegatedVoter = _randomAddress(); address undelegatedVoter = _randomAddress(); @@ -1368,7 +1422,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter1 = _randomAddress(); address undelegatedVoter2 = _randomAddress(); // undelegatedVoter1 has 50/100 intrinsic VP (delegated to no one/self) @@ -1410,7 +1467,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 50/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 50e18, address(0)); @@ -1439,7 +1499,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address delegate = _randomAddress(); address delegatedVoter = _randomAddress(); // delegatedVoter has 50/100 intrinsic VP (delegated to no one/self) @@ -1469,7 +1532,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address delegate = _randomAddress(); address delegatedVoter = _randomAddress(); // delegatedVoter has 50/100 intrinsic VP (delegated to no one/self) @@ -1499,7 +1565,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter1 = _randomAddress(); address undelegatedVoter2 = _randomAddress(); // undelegatedVoter1 has 50/100 intrinsic VP (delegated to no one/self) @@ -1545,7 +1614,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address delegate = _randomAddress(); address undelegatedVoter = _randomAddress(); address delegatedVoter = _randomAddress(); @@ -1620,7 +1692,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); // Give voter 30 intrinsic VP. address voter = _randomAddress(); gov.rawAdjustVotingPower(voter, 30e18, address(0)); @@ -1651,7 +1726,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address delegate1 = _randomAddress(); address delegate2 = _randomAddress(); // Set up circular delegation just to be extra tricky. @@ -1689,7 +1767,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address undelegatedVoter = _randomAddress(); // undelegatedVoter has 51 intrinsic VP gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); @@ -1706,7 +1787,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter = _randomAddress(); // voter has 50 intrinsic VP at snapshot index 0 @@ -1722,7 +1806,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter = _randomAddress(); // voter has 50 intrinsic VP at snapshot index 0 gov.rawAdjustVotingPower(voter, 50e18, address(0)); @@ -1754,7 +1841,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter = _randomAddress(); // voter has 50 intrinsic VP at snapshot index 0 @@ -1770,7 +1860,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter = _randomAddress(); // no snapshots, should return 0 voting power @@ -1786,7 +1879,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter = _randomAddress(); // voter has no voting power snapshots @@ -1824,7 +1920,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address delegate1 = _randomAddress(); address delegate2 = _randomAddress(); address voter = _randomAddress(); @@ -1890,7 +1989,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter1 = _randomAddress(); address voter2 = _randomAddress(); // voter has 50 intrinsic VP, delegated to zero. @@ -1918,7 +2020,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address newHost = _randomAddress(); @@ -1942,7 +2047,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address host = _getRandomDefaultHost(); @@ -1959,7 +2067,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address nonHost = _randomAddress(); address nonHost2 = _randomAddress(); @@ -1975,7 +2086,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address pastMember = _randomAddress(); // Used to have VP. gov.rawAdjustVotingPower(pastMember, 50e18, address(0)); @@ -1992,7 +2106,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); skip(1); address nonMember = _randomAddress(); assertEq(gov.getVotingPowerAt(nonMember, uint40(block.timestamp), 1), 0); @@ -2004,7 +2121,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address pastMember = _randomAddress(); // Uesd to have VP. gov.rawAdjustVotingPower(pastMember, 50e18, address(0)); @@ -2025,7 +2145,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address voter1 = _randomAddress(); address voter2 = _randomAddress(); @@ -2078,8 +2201,7 @@ contract PartyGovernanceUnitTest is Test, TestUtils { ) = _createPreciousTokens(2); TestablePartyGovernance gov = _createGovernance( uint96(totalVotingPower), - preciousTokens, - preciousTokenIds + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) ); address voter1 = _randomAddress(); @@ -2115,7 +2237,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); // Only a member with VP can call distribute(). address member = _randomAddress(); @@ -2145,7 +2270,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); // Only a member with VP can call distribute(). address member = _randomAddress(); @@ -2177,7 +2305,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address member = _randomAddress(); gov.rawAdjustVotingPower(member, 1e18, member); @@ -2197,7 +2328,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address member = _randomAddress(); gov.rawAdjustVotingPower(member, 1, address(0)); @@ -2220,7 +2354,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address owner = _randomAddress(); DummyERC1155 erc1155 = new DummyERC1155(); @@ -2234,7 +2371,10 @@ contract PartyGovernanceUnitTest is Test, TestUtils { IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance( + 100e18, + LibPreciousList.hashPreciousList(preciousTokens, preciousTokenIds) + ); address owner = _randomAddress(); DummyERC721 erc721 = new DummyERC721(); @@ -2244,11 +2384,7 @@ contract PartyGovernanceUnitTest is Test, TestUtils { } function test_getProposalHash_isCorrect() external { - ( - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds - ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance(100e18, bytes32(0)); PartyGovernance.Proposal memory proposal = _createProposal(1); bytes32 expectedHash = keccak256( abi.encode( @@ -2277,11 +2413,7 @@ contract PartyGovernanceUnitTest is Test, TestUtils { } function test_supportsInterface() external { - ( - IERC721[] memory preciousTokens, - uint256[] memory preciousTokenIds - ) = _createPreciousTokens(2); - TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); + TestablePartyGovernance gov = _createGovernance(100e18, bytes32(0)); gov.supportsInterface(0x01ffc9a7); // EIP165 gov.supportsInterface(0x2a55205a); // ERC2981 gov.supportsInterface(0x80ac58cd); // ERC721 diff --git a/tests/integration/system.ts b/tests/integration/system.ts index b853c7967..d7142b1bb 100644 --- a/tests/integration/system.ts +++ b/tests/integration/system.ts @@ -1,6 +1,6 @@ import { Contract, BigNumber, Wallet } from "ethers"; import * as ethers from "ethers"; -import { deployContract, NULL_ADDRESS, NULL_BYTES, NULL_HASH } from "../utils"; +import { deployContract, hashPreciousList, NULL_ADDRESS, NULL_BYTES, NULL_HASH } from "../utils"; import GLOBALS_ARTIFACT from "../../out/Globals.sol/Globals.json"; import PARTY_FACTORY_ARTIFACT from "../../out/PartyFactory.sol/PartyFactory.json"; @@ -284,11 +284,11 @@ export class Party { const partyFactory = opts.sys.partyFactory.connect(opts.worker); const tx = await ( await partyFactory.createParty( - opts.worker.address, { name: opts.name, symbol: opts.symbol, customizationPresetId: 0, + preciousListHash: hashPreciousList(preciousTokens), governance: { hosts: opts.hostAddresses, voteDuration: opts.voteDuration, @@ -299,8 +299,7 @@ export class Party { feeRecipient: opts.feeRecipient || NULL_ADDRESS, }, }, - preciousTokens.map(({ token }) => token.address), - preciousTokens.map(({ tokenId }) => tokenId), + opts.worker.address, ) ).wait(); const partyAddress = tx.events.find((e: any) => e.event === "PartyCreated").args[0]; diff --git a/tests/utils.ts b/tests/utils.ts index 79b1a89cd..d7e683116 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -51,6 +51,23 @@ export async function deployContract( return cf.deploy(...args, ...[overrides ? [overrides] : []]); } +export function hashPreciousList( + preciousTokens: Array<{ token: Contract; tokenId: BigNumber }>, +): string { + let abi = ethers.utils.defaultAbiCoder; + let tokens = preciousTokens.map(({ token }) => token.address); + let tokenIds = preciousTokens.map(({ tokenId }) => tokenId); + return ethers.utils.keccak256( + abi.encode( + ["bytes32", "bytes32"], + [ + ethers.utils.keccak256(abi.encode(Array(tokens.length).fill("address"), tokens)), + ethers.utils.keccak256(abi.encode(Array(tokenIds.length).fill("uint256"), tokenIds)), + ], + ), + ); +} + export function describeFork(name: string, body: (forkProvider: MockProvider) => void) { let it = global.it; if (!ENV.FORK_URL) { From 9fdd1f8ed0c5665aae0ad0bff5f3ac0d1cc684f7 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Thu, 5 Jan 2023 14:18:58 -0800 Subject: [PATCH 8/9] docs: update `listMerkleRoot` leaf encoding --- contracts/party/IPartyFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/party/IPartyFactory.sol b/contracts/party/IPartyFactory.sol index 5e078211a..768546801 100644 --- a/contracts/party/IPartyFactory.sol +++ b/contracts/party/IPartyFactory.sol @@ -31,7 +31,7 @@ interface IPartyFactory { address creatorDelegate; // Merkle root of list of initial members and voting power for each member. // Each leaf in the list should be encoded as: - // `abi.encodePacked(address member, uint96 votingPower)` + // `abi.encodePacked(address member, uint96 votingPower, uint256 nonce)` bytes32 listMerkleRoot; } From 44a2d52eb91d25bb044dc040e1d807b7e95e1dbd Mon Sep 17 00:00:00 2001 From: Brian Le Date: Fri, 10 Feb 2023 16:00:11 -0800 Subject: [PATCH 9/9] feat: restrict delegate when minting on behalf --- contracts/party/PartyList.sol | 49 +++++++++++++++----- sol-tests/crowdfund/MockParty.sol | 2 + sol-tests/party/PartyFactory.t.sol | 9 ++-- sol-tests/party/PartyList.t.sol | 73 +++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 17 deletions(-) diff --git a/contracts/party/PartyList.sol b/contracts/party/PartyList.sol index 91abdd37c..6f9c08042 100644 --- a/contracts/party/PartyList.sol +++ b/contracts/party/PartyList.sol @@ -29,12 +29,20 @@ contract PartyList { error ListAlreadyExistsError(Party party, bytes32 merkleRoot); error InvalidProofError(bytes32[] proof); + error InvalidDelegationError(address delegate); error AlreadyMintedError(bytes32 leaf); error UnauthorizedError(); - /// @notice party address => merkle root - mapping(Party => bytes32) public listMerkleRoots; - /// @notice party address => leaf => minted + struct ListData { + // The root of the Merkle tree for the list + bytes32 merkleRoot; + // The address of the party's creator + address creator; + } + + /// @notice Mapping from a party to data about the party's list. + mapping(Party => ListData) public listData; + /// @notice Whether a leaf from the Merkle tree has been consumed to mint a party card mapping(Party => mapping(bytes32 => bool)) public minted; IGlobals private immutable _GLOBALS; @@ -66,13 +74,18 @@ contract PartyList { uint96 creatorVotingPower, address creatorDelegate ) external onlyPartyFactory { - bytes32 root = listMerkleRoots[party]; + ListData storage list = listData[party]; + bytes32 root = list.merkleRoot; if (root != bytes32(0)) revert ListAlreadyExistsError(party, root); - listMerkleRoots[party] = merkleRoot; + list.merkleRoot = merkleRoot; + + if (creator != address(0)) { + list.creator = creator; - if (creator != address(0) && creatorVotingPower > 0) { - party.mint(creator, creatorVotingPower, creatorDelegate); + if (creatorVotingPower > 0) { + party.mint(creator, creatorVotingPower, creatorDelegate); + } } emit ListCreated(party, merkleRoot); @@ -84,8 +97,17 @@ contract PartyList { * @return tokenId The ID of the newly minted party card. */ function mint(MintArgs memory args) external returns (uint256 tokenId) { + ListData memory list = listData[args.party]; + + // If minting on behalf of another member, only allow delegating to the + // member or party's creator. + if (msg.sender != args.member) { + if (args.delegate != args.member && args.delegate != list.creator) + revert InvalidDelegationError(args.delegate); + } + (bool allowed, bytes32 leaf) = _verify( - args.party, + list.merkleRoot, args.member, args.votingPower, args.nonce, @@ -132,17 +154,20 @@ contract PartyList { uint256 nonce, bytes32[] memory proof ) public view returns (bool) { - (bool allowed, bytes32 leaf) = _verify(party, member, votingPower, nonce, proof); + ListData storage list = listData[party]; + + (bool allowed, bytes32 leaf) = _verify(list.merkleRoot, member, votingPower, nonce, proof); + return allowed && !minted[party][leaf]; } function _verify( - Party party, + bytes32 root, address member, uint96 votingPower, uint256 nonce, bytes32[] memory proof - ) private view returns (bool allowed, bytes32 leaf) { + ) private pure returns (bool allowed, bytes32 leaf) { assembly { // leaf = keccak256(abi.encodePacked(member, votingPower, nonce)) mstore(0, shl(96, member)) @@ -151,6 +176,6 @@ contract PartyList { leaf := keccak256(0, 64) } - allowed = MerkleProof.verify(proof, listMerkleRoots[party], leaf); + allowed = MerkleProof.verify(proof, root, leaf); } } diff --git a/sol-tests/crowdfund/MockParty.sol b/sol-tests/crowdfund/MockParty.sol index 47c697eea..8019ce635 100644 --- a/sol-tests/crowdfund/MockParty.sol +++ b/sol-tests/crowdfund/MockParty.sol @@ -8,6 +8,7 @@ contract MockParty is ERC721("MockParty", "MOCK") { event MockMint(address caller, address owner, uint256 amount, address delegate); uint256 public tokenCount; + mapping(address => address) public delegationsByVoter; function tokenURI(uint256 id) public view override returns (string memory) {} @@ -17,6 +18,7 @@ contract MockParty is ERC721("MockParty", "MOCK") { address delegate ) external returns (uint256 tokenId) { tokenId = ++tokenCount; + delegationsByVoter[owner] = delegate; _safeMint(owner, tokenId); emit MockMint(msg.sender, owner, amount, delegate); } diff --git a/sol-tests/party/PartyFactory.t.sol b/sol-tests/party/PartyFactory.t.sol index 8c44cb500..51f08c1e2 100644 --- a/sol-tests/party/PartyFactory.t.sol +++ b/sol-tests/party/PartyFactory.t.sol @@ -115,7 +115,6 @@ contract PartyFactoryTest is Test, TestUtils { function testCreatePartyFromList() external { address member = _randomAddress(); - address delegate = _randomAddress(); uint96 votingPower = 0.1e18; uint256 nonce = _randomUint256(); @@ -135,8 +134,10 @@ contract PartyFactoryTest is Test, TestUtils { Party party = factory.createPartyFromList(opts); + (bytes32 merkleRoot, address creator) = partyList.listData(party); + assertEq(merkleRoot, opts.listMerkleRoot); + assertEq(creator, opts.creator); assertEq(party.mintAuthority(), address(partyList)); - assertEq(partyList.listMerkleRoots(party), opts.listMerkleRoot); assertEq(party.balanceOf(opts.creator), 1); assertEq(party.delegationsByVoter(opts.creator), opts.creatorDelegate); assertEq(party.votingPowerByTokenId(1), opts.creatorVotingPower); @@ -147,12 +148,12 @@ contract PartyFactoryTest is Test, TestUtils { member: member, votingPower: votingPower, nonce: nonce, - delegate: delegate, + delegate: member, proof: new bytes32[](0) }) ); assertEq(party.balanceOf(member), 1); - assertEq(party.delegationsByVoter(member), delegate); + assertEq(party.delegationsByVoter(member), member); assertEq(party.votingPowerByTokenId(2), votingPower); for (uint256 i; i < preciousTokens.length; ++i) { diff --git a/sol-tests/party/PartyList.t.sol b/sol-tests/party/PartyList.t.sol index ce5dc1e99..4b6047540 100644 --- a/sol-tests/party/PartyList.t.sol +++ b/sol-tests/party/PartyList.t.sol @@ -24,7 +24,9 @@ contract PartyListTest is Test, TestUtils { address creatorDelegate = _randomAddress(); bytes32 merkleRoot = keccak256(abi.encodePacked(_randomUint256())); partyList.createList(party, merkleRoot, creator, creatorVotingPower, creatorDelegate); - assertEq(partyList.listMerkleRoots(party), merkleRoot); + (bytes32 storedMerkleRoot, address storedCreator) = partyList.listData(party); + assertEq(storedMerkleRoot, merkleRoot); + assertEq(storedCreator, creator); } function test_createList_onlyPartyFactory() public { @@ -56,6 +58,7 @@ contract PartyListTest is Test, TestUtils { address delegate = _randomAddress(); bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); partyList.createList(party, merkleRoot, address(0), 0, address(0)); + vm.prank(member); uint256 tokenId = partyList.mint( PartyList.MintArgs({ party: party, @@ -67,6 +70,7 @@ contract PartyListTest is Test, TestUtils { }) ); assertEq(party.ownerOf(tokenId), member); + assertEq(party.delegationsByVoter(member), delegate); } function test_mint_cannotMintTwice() public { @@ -76,6 +80,7 @@ contract PartyListTest is Test, TestUtils { address delegate = _randomAddress(); bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); partyList.createList(party, merkleRoot, address(0), 0, address(0)); + vm.prank(member); partyList.mint( PartyList.MintArgs({ party: party, @@ -87,6 +92,7 @@ contract PartyListTest is Test, TestUtils { }) ); vm.expectRevert(abi.encodeWithSelector(PartyList.AlreadyMintedError.selector, merkleRoot)); + vm.prank(member); partyList.mint( PartyList.MintArgs({ party: party, @@ -108,6 +114,7 @@ contract PartyListTest is Test, TestUtils { bytes32[] memory proof = new bytes32[](1); partyList.createList(party, merkleRoot, address(0), 0, address(0)); vm.expectRevert(abi.encodeWithSelector(PartyList.InvalidProofError.selector, proof)); + vm.prank(member); partyList.mint( PartyList.MintArgs({ party: party, @@ -119,4 +126,68 @@ contract PartyListTest is Test, TestUtils { }) ); } + + function test_mint_onBehalf_canDelegateToMember() public { + address member = _randomAddress(); + uint96 votingPower = 0.1e18; + uint256 nonce = _randomUint256(); + bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); + partyList.createList(party, merkleRoot, address(0), 0, address(0)); + // Mint on behalf of another member and delgate to them + partyList.mint( + PartyList.MintArgs({ + party: party, + member: member, + votingPower: votingPower, + nonce: nonce, + delegate: member, + proof: new bytes32[](0) + }) + ); + assertEq(party.delegationsByVoter(member), member); + } + + function test_mint_onBehalf_canDelegateToCreator() public { + address creator = _randomAddress(); + address member = _randomAddress(); + uint96 votingPower = 0.1e18; + uint256 nonce = _randomUint256(); + bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); + partyList.createList(party, merkleRoot, creator, 0, address(0)); + // Mint on behalf of another member and delgate to them + partyList.mint( + PartyList.MintArgs({ + party: party, + member: member, + votingPower: votingPower, + nonce: nonce, + delegate: creator, + proof: new bytes32[](0) + }) + ); + assertEq(party.delegationsByVoter(member), creator); + } + + function test_mint_onBehalf_cannotDelegateToRandomAddress() public { + address member = _randomAddress(); + address delegate = _randomAddress(); + uint96 votingPower = 0.1e18; + uint256 nonce = _randomUint256(); + bytes32 merkleRoot = keccak256(abi.encodePacked(member, votingPower, nonce)); + partyList.createList(party, merkleRoot, address(0), 0, address(0)); + // Mint on behalf of another member and delgate to random address + vm.expectRevert( + abi.encodeWithSelector(PartyList.InvalidDelegationError.selector, delegate) + ); + partyList.mint( + PartyList.MintArgs({ + party: party, + member: member, + votingPower: votingPower, + nonce: nonce, + delegate: delegate, + proof: new bytes32[](0) + }) + ); + } }