diff --git a/.github/workflows/testnet.yml b/.github/workflows/testnet.yml index 7baeac4..089a352 100644 --- a/.github/workflows/testnet.yml +++ b/.github/workflows/testnet.yml @@ -62,5 +62,5 @@ jobs: run: | export DEPLOYED_LIBRARY=$(bin/library.py Constant.sol.json) echo "Using library ${DEPLOYED_LIBRARY}" - forge script ./script/DeployCommunityBuilder.sol --sig 'deploy()' --libraries ${DEPLOYED_LIBRARY} --slow --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_API_KEY} --verify || true - forge script ./script/DeployCollective.sol --sig 'deploy()' --libraries ${DEPLOYED_LIBRARY} --slow --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_API_KEY} --verify || true + BUILDER_ADDRESS=0x4ba7E0dc43180Cb10EF53FF9Da923E02f459Ec9F forge script ./script/DeployCommunityBuilder.sol --sig 'upgrade()' --libraries ${DEPLOYED_LIBRARY} --slow --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY} + BUILDER_ADDRESS=0xe1a13ea37F2BFE35B612799fcf9eBD43efA00B87 forge script ./script/DeployCollective.sol --sig 'upgrade()' --libraries ${DEPLOYED_LIBRARY} --slow --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY} diff --git a/.gitmodules b/.gitmodules index 2f92bf1..181496f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,8 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std - branch = v1.5.3 \ No newline at end of file + branch = v1.5.3 +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts + branch = v4.8.3 \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 88403d1..645f8d0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -136,7 +136,7 @@ "options": { "cwd": "${workspaceFolder}", "env": { - "CONSTANT_LIB_ADDRESS": "0x5a7D758870C57d8A4E1AB309079705E77D279D0a" + "CONSTANT_LIB_ADDRESS": "0xb72FB606aF07D03f9D3075F72f7705F1148A5450" } }, "dependsOn": "test", @@ -152,7 +152,7 @@ "options": { "cwd": "${workspaceFolder}", "env": { - "CONSTANT_LIB_ADDRESS": "0x5a7D758870C57d8A4E1AB309079705E77D279D0a", + "CONSTANT_LIB_ADDRESS": "0xb72FB606aF07D03f9D3075F72f7705F1148A5450", "BUILDER_ADDRESS": "0x2b3FbC7371Bc6DE55D314Bd0D74E08A3E2c9dbE9" } }, @@ -169,7 +169,7 @@ "options": { "cwd": "${workspaceFolder}", "env": { - "CONSTANT_LIB_ADDRESS": "0x5a7D758870C57d8A4E1AB309079705E77D279D0a", + "CONSTANT_LIB_ADDRESS": "0xb72FB606aF07D03f9D3075F72f7705F1148A5450", "CLASS_PROXY": "0x0Ee183DEA88Be769fD1CAb473fDaCdd16791163A", "TARGET_PROTOTYPE": "0xe7879fdb66b6107709502D2ad362fa43BC278DbE" } @@ -187,7 +187,7 @@ "options": { "cwd": "${workspaceFolder}", "env": { - "CONSTANT_LIB_ADDRESS": "0x5a7D758870C57d8A4E1AB309079705E77D279D0a" + "CONSTANT_LIB_ADDRESS": "0xb72FB606aF07D03f9D3075F72f7705F1148A5450" } }, "dependsOn": "test", @@ -203,7 +203,7 @@ "options": { "cwd": "${workspaceFolder}", "env": { - "CONSTANT_LIB_ADDRESS": "0x5a7D758870C57d8A4E1AB309079705E77D279D0a", + "CONSTANT_LIB_ADDRESS": "0xb72FB606aF07D03f9D3075F72f7705F1148A5450", "BUILDER_ADDRESS": "0xA117420676c6Ff31b7126cBE8fdC752Eb9A1602c" } }, @@ -220,10 +220,10 @@ "options": { "cwd": "${workspaceFolder}", "env": { - "CONSTANT_LIB_ADDRESS": "0x5a7D758870C57d8A4E1AB309079705E77D279D0a", - "GOVERNANCE_ADDRESS": "0x0089699e6391011991f6ffe2e36bb3d745a2ba65", - "STORAGE_ADDRESS": "0xe0bfa34b0beb30853489c67c8172f4c64eb8c43b", - "META_ADDRESS": "0x46e0f484a22a2e091b524b7495b45b2583f69846" + "CONSTANT_LIB_ADDRESS": "0xb72FB606aF07D03f9D3075F72f7705F1148A5450", + "GOVERNANCE_ADDRESS": "0x6b5b3b287a80742d728511f51638315cf1987977", + "STORAGE_ADDRESS": "0xc28ea107f64746ac90349c6c09c193d966f1ca91", + "META_ADDRESS": "0xd75fa6c5c3fdfd495d851c43e5331e16a058902a" } }, "dependsOn": "test", @@ -239,7 +239,7 @@ "options": { "cwd": "${workspaceFolder}", "env": { - "CONSTANT_LIB_ADDRESS": "0x5a7D758870C57d8A4E1AB309079705E77D279D0a", + "CONSTANT_LIB_ADDRESS": "0xb72FB606aF07D03f9D3075F72f7705F1148A5450", "BUILDER_ADDRESS": "", "GOVERNANCE_ADDRESS": "0xa2f50e55ac910ba030b3e4cab92da8de5b38ef2d", "STORAGE_ADDRESS": "0xe0c76c1621738b870eac5a37447fdadc8c077c21", diff --git a/LICENSE b/LICENSE index ec0d90f..c79bad3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2022, collective +Copyright (c) 2022-2023, collective All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 01aeda6..2427316 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ Collective Governance has been designed from the ground up to be very easy to us | Contract | Ethereum Address | Version | | ----------------- | ------------------------------------------ | ------- | | Constant | 0xD5DA9B812806E080948476A801d2004f3305E63F | 0.9.8 | -| CommunityBuilder | 0x4ba7E0dc43180Cb10EF53FF9Da923E02f459Ec9F | 0.9.8 | -| GovernanceBuilder | 0xe1a13ea37F2BFE35B612799fcf9eBD43efA00B87 | 0.9.8 | +| CommunityBuilder | 0x4ba7E0dc43180Cb10EF53FF9Da923E02f459Ec9F | LATEST | +| GovernanceBuilder | 0xe1a13ea37F2BFE35B612799fcf9eBD43efA00B87 | LATEST | #### Görli TestNet @@ -51,13 +51,13 @@ Collective Governance has been designed from the ground up to be very easy to us ### VS Code - Using the Remote module in VSCode simply reopen the project in it's container. +Using the Remote module in VSCode simply reopen the project in it's container. `Reopen in Container` ### Foundry - This project is using [Foundry](https://github.com/foundry-rs/foundry). Development is enabled with the [Foundry Development](https://github.com/collectivexyz/foundry/pkgs/container/foundry) container +This project is using [Foundry](https://github.com/foundry-rs/foundry). Development is enabled with the [Foundry Development](https://github.com/collectivexyz/foundry) container ### JavaScript API diff --git a/abi/CommunityBuilder.json b/abi/CommunityBuilder.json index 76216a5..c6be5e7 100644 --- a/abi/CommunityBuilder.json +++ b/abi/CommunityBuilder.json @@ -308,7 +308,7 @@ { "indexed": false, "internalType": "address", - "name": "projectClassFactory", + "name": "tokenClassFactory", "type": "address" } ], @@ -378,7 +378,7 @@ { "indexed": false, "internalType": "address", - "name": "projectClassFactory", + "name": "tokenClassFactory", "type": "address" }, { @@ -443,6 +443,30 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "project", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenThreshold", + "type": "uint256" + } + ], + "name": "asClosedErc20Community", + "outputs": [ + { + "internalType": "contract CommunityBuilder", + "name": "", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -467,6 +491,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "project", + "type": "address" + } + ], + "name": "asErc20Community", + "outputs": [ + { + "internalType": "contract CommunityBuilder", + "name": "", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -536,6 +579,11 @@ "internalType": "address", "name": "_project", "type": "address" + }, + { + "internalType": "address", + "name": "_token", + "type": "address" } ], "name": "initialize", @@ -633,6 +681,11 @@ "name": "_project", "type": "address" }, + { + "internalType": "address", + "name": "_token", + "type": "address" + }, { "internalType": "uint8", "name": "_version", diff --git a/contracts/Constant.sol b/contracts/Constant.sol index 77028b9..b447f89 100644 --- a/contracts/Constant.sol +++ b/contracts/Constant.sol @@ -96,7 +96,7 @@ library Constant { uint256 public constant MAXIMUM_REBATE_BASE_FEE = 200 gwei; /// software versions - uint32 public constant CURRENT_VERSION = 3; + uint32 public constant CURRENT_VERSION = 4; /// @notice Compute the length of any string in solidity /// @dev This method is expensive and is used only for validating diff --git a/contracts/ProposalBuilder.sol b/contracts/ProposalBuilder.sol index e69fa50..e272e1d 100644 --- a/contracts/ProposalBuilder.sol +++ b/contracts/ProposalBuilder.sol @@ -61,6 +61,12 @@ import { Transaction, TransactionCollection } from "../contracts/collection/Tran import { Storage } from "../contracts/storage/Storage.sol"; import { MetaStorage } from "../contracts/storage/MetaStorage.sol"; +/** + * @notice ProposalBuilder is designed to help building up on-chain proposals. One of the features + * of the ProposalBuilder, like the other builders in this project is it remembers the previous settings + * for the build. This makes it easy and cost effective to create multiple proposals because only + * changed information needs to be updated on each build cycle. + */ contract ProposalBuilder is VersionedContract, ERC165, OwnableInitializable, UUPSUpgradeable, Initializable { string public constant NAME = "proposal builder"; diff --git a/contracts/ProposalBuilderProxy.sol b/contracts/ProposalBuilderProxy.sol index dca5e34..517403c 100644 --- a/contracts/ProposalBuilderProxy.sol +++ b/contracts/ProposalBuilderProxy.sol @@ -48,6 +48,9 @@ import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy import { Constant } from "./Constant.sol"; import { ProposalBuilder } from "./ProposalBuilder.sol"; +/** + * @notice This contract is intended to act as an upgradeable Proxy for the ProposalBuilder + */ contract ProposalBuilderProxy is ERC1967Proxy { constructor( address _implementation, diff --git a/contracts/community/CommunityBuilder.sol b/contracts/community/CommunityBuilder.sol index 6e8c9b3..1e0c090 100644 --- a/contracts/community/CommunityBuilder.sol +++ b/contracts/community/CommunityBuilder.sol @@ -53,14 +53,17 @@ import { Versioned } from "../../contracts/access/Versioned.sol"; import { VersionedContract } from "../../contracts/access/VersionedContract.sol"; import { AddressCollection } from "../../contracts/collection/AddressSet.sol"; import { WeightedCommunityClass } from "../../contracts/community/CommunityClass.sol"; -import { WeightedClassFactory, ProjectClassFactory } from "../../contracts/community/CommunityFactory.sol"; +import { WeightedClassFactory, ProjectClassFactory, TokenClassFactory } from "../../contracts/community/CommunityFactory.sol"; import { CommunityClassVoterPool } from "../../contracts/community/CommunityClassVoterPool.sol"; import { CommunityClassOpenVote } from "../../contracts/community/CommunityClassOpenVote.sol"; import { CommunityClassERC721 } from "../../contracts/community/CommunityClassERC721.sol"; import { CommunityClassClosedERC721 } from "../../contracts/community/CommunityClassClosedERC721.sol"; +import { CommunityClassERC20 } from "../../contracts/community/CommunityClassERC20.sol"; +import { CommunityClassClosedERC20 } from "../../contracts/community/CommunityClassClosedERC20.sol"; + import { OwnableInitializable } from "../../contracts/access/OwnableInitializable.sol"; -/// @title Community Creator +/// @title CommunityBuilder /// @notice This builder is for creating a community class for use with the Collective /// Governance contract contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UUPSUpgradeable, Initializable { @@ -77,8 +80,8 @@ contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UU error VoterPoolRequired(); event UpgradeAuthorized(address sender, address owner); - event Initialized(address weightedClassFactory, address projectClassFactory); - event Upgraded(address weightedClassFactory, address projectClassFactory, uint8 version); + event Initialized(address weightedClassFactory, address tokenClassFactory); + event Upgraded(address weightedClassFactory, address tokenClassFactory, uint8 version); event CommunityClassInitialized(address sender); event CommunityClassType(CommunityType communityType); @@ -99,7 +102,9 @@ contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UU OPEN, POOL, ERC721, - ERC721_CLOSED + ERC721_CLOSED, + ERC20, + ERC20_CLOSED } struct CommunityProperties { @@ -124,20 +129,29 @@ contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UU ProjectClassFactory private _projectFactory; + TokenClassFactory private _tokenFactory; + constructor() { _disableInitializers(); } - function initialize(address _weighted, address _project) public initializer { + function initialize(address _weighted, address _project, address _token) public initializer { ownerInitialize(msg.sender); _weightedFactory = WeightedClassFactory(_weighted); _projectFactory = ProjectClassFactory(_project); + _tokenFactory = TokenClassFactory(_token); emit Initialized(_weighted, _project); } - function upgrade(address _weighted, address _project, uint8 _version) public onlyOwner reinitializer(_version) { + function upgrade( + address _weighted, + address _project, + address _token, + uint8 _version + ) public onlyOwner reinitializer(_version) { _weightedFactory = WeightedClassFactory(_weighted); _projectFactory = ProjectClassFactory(_project); + _tokenFactory = TokenClassFactory(_token); emit Upgraded(_weighted, _project, _version); } @@ -159,7 +173,7 @@ contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UU * @return CommunityBuilder - this contract */ function aCommunity() external returns (CommunityBuilder) { - reset(); + clear(msg.sender); emit CommunityClassInitialized(msg.sender); return this; } @@ -207,6 +221,8 @@ contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UU /** * build Closed ERC-721 community * + * @dev community is closed to external proposals + * * @param project the token contract address * @param tokenThreshold the number of tokens required to propose * @@ -221,6 +237,40 @@ contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UU return this; } + /** + * build ERC-20 community + * + * @param project the token contract address + * + * @return CommunityBuilder - this contract + */ + function asErc20Community(address project) external requireNone returns (CommunityBuilder) { + CommunityProperties storage _properties = _buildMap[msg.sender]; + _properties.communityType = CommunityType.ERC20; + _properties.projectToken = project; + emit CommunityClassType(CommunityType.ERC20); + return this; + } + + /** + * build Closed ERC-20 community + * + * @dev community is closed to external proposals + * + * @param project the token contract address + * @param tokenThreshold the number of tokens required to propose + * + * @return CommunityBuilder - this contract + */ + function asClosedErc20Community(address project, uint256 tokenThreshold) external requireNone returns (CommunityBuilder) { + CommunityProperties storage _properties = _buildMap[msg.sender]; + _properties.communityType = CommunityType.ERC20_CLOSED; + _properties.projectToken = project; + _properties.tokenThreshold = tokenThreshold; + emit CommunityClassType(CommunityType.ERC20_CLOSED); + return this; + } + /** * append a voter for a pool community * @@ -387,6 +437,22 @@ contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UU _properties.maximumBaseFeeRebate, _properties.communitySupervisor ); + } else if (_properties.communityType == CommunityType.ERC20_CLOSED) { + if (_properties.projectToken == address(0x0)) revert ProjectTokenRequired(_properties.projectToken); + if (_properties.tokenThreshold == 0) revert TokenThresholdRequired(_properties.tokenThreshold); + _proxy = _tokenFactory.createClosedErc20( + _properties.projectToken, + _properties.tokenThreshold, + _properties.weight, + _properties.minimumProjectQuorum, + _properties.minimumVoteDelay, + _properties.maximumVoteDelay, + _properties.minimumVoteDuration, + _properties.maximumVoteDuration, + _properties.maximumGasUsedRebate, + _properties.maximumBaseFeeRebate, + _properties.communitySupervisor + ); } else if (_properties.communityType == CommunityType.ERC721) { if (_properties.projectToken == address(0x0)) revert ProjectTokenRequired(_properties.projectToken); _proxy = _projectFactory.createErc721( @@ -401,6 +467,20 @@ contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UU _properties.maximumBaseFeeRebate, _properties.communitySupervisor ); + } else if (_properties.communityType == CommunityType.ERC20) { + if (_properties.projectToken == address(0x0)) revert ProjectTokenRequired(_properties.projectToken); + _proxy = _tokenFactory.createErc20( + _properties.projectToken, + _properties.weight, + _properties.minimumProjectQuorum, + _properties.minimumVoteDelay, + _properties.maximumVoteDelay, + _properties.minimumVoteDuration, + _properties.maximumVoteDuration, + _properties.maximumGasUsedRebate, + _properties.maximumBaseFeeRebate, + _properties.communitySupervisor + ); } else if (_properties.communityType == CommunityType.OPEN) { _proxy = _weightedFactory.createOpenVote( _properties.weight, @@ -454,8 +534,14 @@ contract CommunityBuilder is VersionedContract, ERC165, OwnableInitializable, UU super.supportsInterface(interfaceId); } - function reset() public { - CommunityProperties storage _properties = _buildMap[msg.sender]; + /// @notice remove storage used by builder + function reset() external { + clear(msg.sender); + delete _buildMap[msg.sender]; + } + + function clear(address sender) internal { + CommunityProperties storage _properties = _buildMap[sender]; _properties.weight = DEFAULT_WEIGHT; _properties.communityType = CommunityType.NONE; _properties.minimumProjectQuorum = 0; diff --git a/contracts/community/CommunityBuilderProxy.sol b/contracts/community/CommunityBuilderProxy.sol index bfaac48..ad7afcb 100644 --- a/contracts/community/CommunityBuilderProxy.sol +++ b/contracts/community/CommunityBuilderProxy.sol @@ -47,7 +47,7 @@ import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy import { Constant } from "../Constant.sol"; import { CommunityBuilder } from "../community/CommunityBuilder.sol"; -import { WeightedClassFactory, ProjectClassFactory } from "../../contracts/community/CommunityFactory.sol"; +import { WeightedClassFactory, ProjectClassFactory, TokenClassFactory } from "../../contracts/community/CommunityFactory.sol"; /** * @notice Proxy for CommunityBuilder @@ -56,24 +56,26 @@ contract CommunityBuilderProxy is ERC1967Proxy { constructor( address _implementation, address _weightedFactory, - address _projectFactory + address _projectFactory, + address _tokenFactory ) ERC1967Proxy( _implementation, - abi.encodeWithSelector(CommunityBuilder.initialize.selector, _weightedFactory, _projectFactory) + abi.encodeWithSelector(CommunityBuilder.initialize.selector, _weightedFactory, _projectFactory, _tokenFactory) ) // solhint-disable-next-line no-empty-blocks { } - function upgrade(address _implementation, address _weightedFactory, address _projectFactory) external { + function upgrade(address _implementation, address _weightedFactory, address _projectFactory, address _tokenFactory) external { _upgradeToAndCallUUPS( _implementation, abi.encodeWithSelector( CommunityBuilder.upgrade.selector, _weightedFactory, _projectFactory, + _tokenFactory, Constant.CURRENT_VERSION ), true @@ -85,12 +87,14 @@ contract CommunityBuilderProxy is ERC1967Proxy { function createCommunityBuilder() returns (CommunityBuilder) { WeightedClassFactory _weightedFactory = new WeightedClassFactory(); ProjectClassFactory _projectFactory = new ProjectClassFactory(); + TokenClassFactory _tokenFactory = new TokenClassFactory(); CommunityBuilder _builder = new CommunityBuilder(); CommunityBuilderProxy _proxy = new CommunityBuilderProxy( address(_builder), address(_weightedFactory), - address(_projectFactory) + address(_projectFactory), + address(_tokenFactory) ); address _proxyAddress = address(_proxy); return CommunityBuilder(_proxyAddress); diff --git a/contracts/community/CommunityClassClosedERC20.sol b/contracts/community/CommunityClassClosedERC20.sol new file mode 100644 index 0000000..67cad07 --- /dev/null +++ b/contracts/community/CommunityClassClosedERC20.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * 88 88 88 + * 88 88 ,d "" + * 88 88 88 + * ,adPPYba, ,adPPYba, 88 88 ,adPPYba, ,adPPYba, MM88MMM 88 8b d8 ,adPPYba, + * a8" "" a8" "8a 88 88 a8P_____88 a8" "" 88 88 `8b d8' a8P_____88 + * 8b 8b d8 88 88 8PP""""""" 8b 88 88 `8b d8' 8PP""""""" + * "8a, ,aa "8a, ,a8" 88 88 "8b, ,aa "8a, ,aa 88, 88 `8b,d8' "8b, ,aa + * `"Ybbd8"' `"YbbdP"' 88 88 `"Ybbd8"' `"Ybbd8"' "Y888 88 "8" `"Ybbd8"' + * + */ +/* + * BSD 3-Clause License + * + * Copyright (c) 2023, collective + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +pragma solidity ^0.8.15; + +import { AddressCollection } from "../../contracts/collection/AddressSet.sol"; +import { CommunityClassERC20 } from "../../contracts/community/CommunityClassERC20.sol"; + +/// @title Closed ERC20 VoterClass +/// @notice similar to CommunityClassERC20 however proposals are only allowed for wallet with +/// a positive balance +contract CommunityClassClosedERC20 is CommunityClassERC20 { + error RequiredParameterIsZero(); + + // number of tokens required to propose + uint256 public _tokenRequirement; + + /// @param _contract Address of the token contract + /// @param _requirement The token requirement + /// @param _voteWeight The integral weight to apply to each token held by the wallet + /// @param _minimumQuorum the least possible quorum for any vote + /// @param _minimumDelay the least possible vote delay + /// @param _maximumDelay the least possible vote delay + /// @param _minimumDuration the least possible voting duration + /// @param _maximumDuration the least possible voting duration + /// @param _gasUsedRebate The maximum rebate for gas used + /// @param _baseFeeRebate The maximum base fee rebate + /// @param _supervisorList the list of supervisors for this project + function initialize( + address _contract, + uint256 _requirement, + uint256 _voteWeight, + uint256 _minimumQuorum, + uint256 _minimumDelay, + uint256 _maximumDelay, + uint256 _minimumDuration, + uint256 _maximumDuration, + uint256 _gasUsedRebate, + uint256 _baseFeeRebate, + AddressCollection _supervisorList + ) public requireNonZero(_requirement) { + initialize( + _contract, + _voteWeight, + _minimumQuorum, + _minimumDelay, + _maximumDelay, + _minimumDuration, + _maximumDuration, + _gasUsedRebate, + _baseFeeRebate, + _supervisorList + ); + _tokenRequirement = _requirement; + } + + modifier requireNonZero(uint256 _requirement) { + if (_requirement < 1) revert RequiredParameterIsZero(); + _; + } + + /// @notice determine if adding a proposal is approved for this voter + /// @return bool true if this address is approved + function canPropose(address _wallet) external view virtual override(CommunityClassERC20) onlyFinal returns (bool) { + uint256 balance = votesAvailable(_wallet); + return balance >= _tokenRequirement; + } +} diff --git a/contracts/community/CommunityClassClosedERC721.sol b/contracts/community/CommunityClassClosedERC721.sol index faa8a54..96f2126 100644 --- a/contracts/community/CommunityClassClosedERC721.sol +++ b/contracts/community/CommunityClassClosedERC721.sol @@ -49,7 +49,8 @@ import { AddressCollection } from "../../contracts/collection/AddressSet.sol"; import { CommunityClassERC721 } from "../../contracts/community/CommunityClassERC721.sol"; /// @title Closed ERC721 VoterClass -/// @notice similar to CommunityClassERC721 however proposals are only allowed for voters +/// @notice similar to CommunityClassERC721 however proposals are only allowed for wallet +/// with a positive balance contract CommunityClassClosedERC721 is CommunityClassERC721 { error RequiredParameterIsZero(); diff --git a/contracts/community/CommunityClassERC20.sol b/contracts/community/CommunityClassERC20.sol new file mode 100644 index 0000000..f1f27e8 --- /dev/null +++ b/contracts/community/CommunityClassERC20.sol @@ -0,0 +1,183 @@ +// SPDhX-License-Identifier: BSD-3-Clause +/* + * 88 88 88 + * 88 88 ,d "" + * 88 88 88 + * ,adPPYba, ,adPPYba, 88 88 ,adPPYba, ,adPPYba, MM88MMM 88 8b d8 ,adPPYba, + * a8" "" a8" "8a 88 88 a8P_____88 a8" "" 88 88 `8b d8' a8P_____88 + * 8b 8b d8 88 88 8PP""""""" 8b 88 88 `8b d8' 8PP""""""" + * "8a, ,aa "8a, ,a8" 88 88 "8b, ,aa "8a, ,aa 88, 88 `8b,d8' "8b, ,aa + * `"Ybbd8"' `"YbbdP"' 88 88 `"Ybbd8"' `"Ybbd8"' "Y888 88 "8" `"Ybbd8"' + * + */ +/* + * BSD 3-Clause License + * + * Copyright (c) 2022, collective + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +pragma solidity ^0.8.15; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { AlwaysFinal } from "../../contracts/access/AlwaysFinal.sol"; +import { AddressCollection } from "../../contracts/collection/AddressSet.sol"; +import { ProjectCommunityClass } from "../../contracts/community/CommunityClass.sol"; +import { ScheduledCommunityClass } from "../../contracts/community/ScheduledCommunityClass.sol"; + +/// @title ERC20 Implementation of CommunityClass +/// @notice This contract implements a voter pool based on ownership of an ERC-20 token. +/// A class member is considered a voter if they have signing access to a wallet that +/// has a positive balance +contract CommunityClassERC20 is ScheduledCommunityClass, ProjectCommunityClass, AlwaysFinal { + error BalanceMismatch(address wallet, uint256 expected, uint256 provided); + + string public constant NAME = "CommunityClassERC20"; + + address internal _contractAddress; + + /// @param _contract Address of the token contract + /// @param _voteWeight The integral weight to apply to each token held by the wallet + /// @param _minimumQuorum the least possible quorum for any vote + /// @param _minimumDelay the least possible vote delay + /// @param _maximumDelay the least possible vote delay + /// @param _minimumDuration the least possible voting duration + /// @param _maximumDuration the least possible voting duration + /// @param _gasUsedRebate The maximum rebate for gas used + /// @param _baseFeeRebate The maximum base fee rebate + /// @param _supervisorList the list of supervisors for this project + function initialize( + address _contract, + uint256 _voteWeight, + uint256 _minimumQuorum, + uint256 _minimumDelay, + uint256 _maximumDelay, + uint256 _minimumDuration, + uint256 _maximumDuration, + uint256 _gasUsedRebate, + uint256 _baseFeeRebate, + AddressCollection _supervisorList + ) public virtual { + initialize( + _voteWeight, + _minimumQuorum, + _minimumDelay, + _maximumDelay, + _minimumDuration, + _maximumDuration, + _gasUsedRebate, + _baseFeeRebate, + _supervisorList + ); + _contractAddress = _contract; + } + + /// @param _voteWeight The integral weight to apply to each token held by the wallet + /// @param _minimumQuorum the least possible quorum for any vote + /// @param _minimumDelay the least possible vote delay + /// @param _maximumDelay the least possible vote delay + /// @param _minimumDuration the least possible voting duration + /// @param _maximumDuration the least possible voting duration + /// @param _gasUsedRebate The maximum rebate for gas used + /// @param _baseFeeRebate The maximum base fee rebate + /// @param _supervisorList the list of supervisors for this project + function initialize( + uint256 _voteWeight, + uint256 _minimumQuorum, + uint256 _minimumDelay, + uint256 _maximumDelay, + uint256 _minimumDuration, + uint256 _maximumDuration, + uint256 _gasUsedRebate, + uint256 _baseFeeRebate, + AddressCollection _supervisorList + ) public virtual { + initialize( + _voteWeight, + _minimumQuorum, + _minimumDelay, + _maximumDelay, + _minimumDuration, + _maximumDuration, + _gasUsedRebate, + _baseFeeRebate, + _supervisorList, + msg.sender + ); + } + + modifier requireValidShare(address _wallet, uint256 _shareId) { + if (_shareId == 0 || _shareId != uint160(_wallet)) revert UnknownToken(_shareId); + _; + } + + /// @notice determine if wallet holds at least one token from the ERC-721 contract + /// @return bool true if wallet can sign for votes on this class + function isVoter(address _wallet) public view onlyFinal returns (bool) { + return IERC20(_contractAddress).balanceOf(_wallet) > 0; + } + + /// @notice determine if adding a proposal is approved for this voter + /// @return bool true if this address is approved + function canPropose(address) external view virtual onlyFinal returns (bool) { + return true; + } + + /// @notice tabulate the number of votes available for the specified wallet + /// @param _wallet The wallet to test for ownership + function votesAvailable(address _wallet) public view onlyFinal returns (uint256) { + return IERC20(_contractAddress).balanceOf(_wallet); + } + + /// @notice discover the number of tokens available to vote + /// @return uint256[] array in memory of number of votes available + function discover(address _wallet) external view onlyFinal returns (uint256[] memory) { + uint256 tokenBalance = votesAvailable(_wallet); + if (tokenBalance == 0) revert NotVoter(_wallet); + uint256[] memory shareList = new uint256[](1); + shareList[0] = uint160(_wallet); + return shareList; + } + + /// @notice confirm tokenId is associated with wallet for voting + /// @dev does not require IERC721Enumerable, tokenId ownership is checked directly using ERC-721 + /// @param _wallet the wallet holding the tokens + /// @param _shareId the walletId + /// @return uint256 The number of weighted votes confirmed + function confirm(address _wallet, uint256 _shareId) external view requireValidShare(_wallet, _shareId) onlyFinal returns (uint256) { + uint256 voteCount = this.votesAvailable(_wallet); + if (voteCount == 0) revert NotVoter(_wallet); + return weight() * voteCount; + } + + /// @notice return the name of this implementation + /// @return string memory representation of name + function name() external pure virtual returns (string memory) { + return NAME; + } +} diff --git a/contracts/community/CommunityClassERC721.sol b/contracts/community/CommunityClassERC721.sol index 2d9f6f2..7b869a6 100644 --- a/contracts/community/CommunityClassERC721.sol +++ b/contracts/community/CommunityClassERC721.sol @@ -181,6 +181,8 @@ contract CommunityClassERC721 is ScheduledCommunityClass, ProjectCommunityClass, /// @notice confirm tokenId is associated with wallet for voting /// @dev does not require IERC721Enumerable, tokenId ownership is checked directly using ERC-721 + /// @param _wallet the wallet holding the token + /// @param _tokenId the id of the token /// @return uint256 The number of weighted votes confirmed function confirm(address _wallet, uint256 _tokenId) external view onlyFinal requireValidToken(_tokenId) returns (uint256) { uint256 voteCount = this.votesAvailable(_wallet, _tokenId); diff --git a/contracts/community/CommunityFactory.sol b/contracts/community/CommunityFactory.sol index 47ace31..d2777cd 100644 --- a/contracts/community/CommunityFactory.sol +++ b/contracts/community/CommunityFactory.sol @@ -52,8 +52,13 @@ import { CommunityClassOpenVote } from "../../contracts/community/CommunityClass import { CommunityClassVoterPool } from "../../contracts/community/CommunityClassVoterPool.sol"; import { CommunityClassERC721 } from "../../contracts/community/CommunityClassERC721.sol"; import { CommunityClassClosedERC721 } from "../../contracts/community/CommunityClassClosedERC721.sol"; +import { CommunityClassERC20 } from "../../contracts/community/CommunityClassERC20.sol"; +import { CommunityClassClosedERC20 } from "../../contracts/community/CommunityClassClosedERC20.sol"; import { WeightedCommunityClassProxy, ProjectCommunityClassProxy, ClosedProjectCommunityClassProxy } from "../../contracts/community/CommunityClassProxy.sol"; +/** + * @notice Upgrade open voting community by proxy + */ // solhint-disable-next-line func-visibility function upgradeOpenVote( address payable proxyAddress, @@ -83,6 +88,9 @@ function upgradeOpenVote( ); } +/** + * @notice Upgrade pool voting community by proxy + */ // solhint-disable-next-line func-visibility function upgradeVoterPool( address payable proxyAddress, @@ -112,6 +120,9 @@ function upgradeVoterPool( ); } +/** + * @notice Upgrade ERC-721 voting community by proxy + */ // solhint-disable-next-line func-visibility function upgradeErc721( address payable proxyAddress, @@ -141,6 +152,9 @@ function upgradeErc721( ); } +/** + * @notice Upgrade closed ERC-721 voting community by proxy + */ // solhint-disable-next-line func-visibility function upgradeClosedErc721( address payable proxyAddress, @@ -170,6 +184,70 @@ function upgradeClosedErc721( ); } +/** + * @notice Upgrade ERC-20 voting community by proxy + */ +// solhint-disable-next-line func-visibility +function upgradeErc20( + address payable proxyAddress, + uint256 weight, + uint256 minimumProjectQuorum, + uint256 minimumVoteDelay, + uint256 maximumVoteDelay, + uint256 minimumVoteDuration, + uint256 maximumVoteDuration, + uint256 _gasUsedRebate, + uint256 _baseFeeRebate, + AddressCollection _supervisorList +) { + CommunityClass _class = new CommunityClassERC20(); + ProjectCommunityClassProxy _proxy = ProjectCommunityClassProxy(proxyAddress); + _proxy.upgrade( + address(_class), + weight, + minimumProjectQuorum, + minimumVoteDelay, + maximumVoteDelay, + minimumVoteDuration, + maximumVoteDuration, + _gasUsedRebate, + _baseFeeRebate, + _supervisorList + ); +} + +/** + * @notice Upgrade closed ERC-20 voting community by proxy + */ +// solhint-disable-next-line func-visibility +function upgradeClosedErc20( + address payable proxyAddress, + uint256 weight, + uint256 minimumProjectQuorum, + uint256 minimumVoteDelay, + uint256 maximumVoteDelay, + uint256 minimumVoteDuration, + uint256 maximumVoteDuration, + uint256 _gasUsedRebate, + uint256 _baseFeeRebate, + AddressCollection _supervisorList +) { + CommunityClass _class = new CommunityClassClosedERC20(); + ProjectCommunityClassProxy _proxy = ProjectCommunityClassProxy(proxyAddress); + _proxy.upgrade( + address(_class), + weight, + minimumProjectQuorum, + minimumVoteDelay, + maximumVoteDelay, + minimumVoteDuration, + maximumVoteDuration, + _gasUsedRebate, + _baseFeeRebate, + _supervisorList + ); +} + /** * @title Weighted Class Factory * @notice small factory intended to reduce construction size impact for weighted community classes @@ -346,3 +424,96 @@ contract ProjectClassFactory { return _proxyClass; } } + +/** + * @title Token Class Factory + * @notice small factory intended to reduce construction size for project community classes + */ +contract TokenClassFactory { + /// @notice create a new community class representing an ERC-20 token based community + /// @param projectToken the token underlier for the community + /// @param weight the weight of a single voting share + /// @param minimumProjectQuorum the least possible quorum for any vote + /// @param minimumVoteDelay the least possible vote delay + /// @param maximumVoteDelay the least possible vote delay + /// @param minimumVoteDuration the least possible voting duration + /// @param maximumVoteDuration the least possible voting duration + /// @param _gasUsedRebate The maximum rebate for gas used + /// @param _baseFeeRebate The maximum base fee rebate + /// @param _supervisorList the list of supervisors for this project + function createErc20( + address projectToken, + uint256 weight, + uint256 minimumProjectQuorum, + uint256 minimumVoteDelay, + uint256 maximumVoteDelay, + uint256 minimumVoteDuration, + uint256 maximumVoteDuration, + uint256 _gasUsedRebate, + uint256 _baseFeeRebate, + AddressCollection _supervisorList + ) external returns (ProjectCommunityClass) { + CommunityClass _class = new CommunityClassERC20(); + ERC1967Proxy _proxy = new ProjectCommunityClassProxy( + address(_class), + projectToken, + weight, + minimumProjectQuorum, + minimumVoteDelay, + maximumVoteDelay, + minimumVoteDuration, + maximumVoteDuration, + _gasUsedRebate, + _baseFeeRebate, + _supervisorList + ); + CommunityClassERC20 _proxyClass = CommunityClassERC20(address(_proxy)); + _proxyClass.transferOwnership(msg.sender); + return _proxyClass; + } + + /// @notice create a new community class representing a closed ERC-20 token based community + /// @param projectToken the token underlier for the community + /// @param tokenThreshold the number of tokens required to propose a vote + /// @param weight the weight of a single voting share + /// @param minimumProjectQuorum the least possible quorum for any vote + /// @param minimumVoteDelay the least possible vote delay + /// @param maximumVoteDelay the least possible vote delay + /// @param minimumVoteDuration the least possible voting duration + /// @param maximumVoteDuration the least possible voting duration + /// @param _gasUsedRebate The maximum rebate for gas used + /// @param _baseFeeRebate The maximum base fee rebate + /// @param _supervisorList the list of supervisors for this project + function createClosedErc20( + address projectToken, + uint256 tokenThreshold, + uint256 weight, + uint256 minimumProjectQuorum, + uint256 minimumVoteDelay, + uint256 maximumVoteDelay, + uint256 minimumVoteDuration, + uint256 maximumVoteDuration, + uint256 _gasUsedRebate, + uint256 _baseFeeRebate, + AddressCollection _supervisorList + ) external returns (ProjectCommunityClass) { + CommunityClass _class = new CommunityClassClosedERC20(); + ERC1967Proxy _proxy = new ClosedProjectCommunityClassProxy( + address(_class), + projectToken, + tokenThreshold, + weight, + minimumProjectQuorum, + minimumVoteDelay, + maximumVoteDelay, + minimumVoteDuration, + maximumVoteDuration, + _gasUsedRebate, + _baseFeeRebate, + _supervisorList + ); + CommunityClassClosedERC20 _proxyClass = CommunityClassClosedERC20(address(_proxy)); + _proxyClass.transferOwnership(msg.sender); + return _proxyClass; + } +} diff --git a/contracts/governance/GovernanceBuilder.sol b/contracts/governance/GovernanceBuilder.sol index 09b8ed5..13e3cf0 100644 --- a/contracts/governance/GovernanceBuilder.sol +++ b/contracts/governance/GovernanceBuilder.sol @@ -274,9 +274,8 @@ contract GovernanceBuilder is VersionedContract, ERC165, OwnableInitializable, U return _governanceContractRegistered[_contract]; } - /// @notice clear and reset resources associated with sender build requests - function reset() external { - // overwrite to truncate data lifetime + /// @notice remove storage used by builder + function reset() public { clear(msg.sender); delete _buildMap[msg.sender]; } diff --git a/contracts/storage/MappedMetaStorage.sol b/contracts/storage/MappedMetaStorage.sol index 0611e6f..c063ef3 100644 --- a/contracts/storage/MappedMetaStorage.sol +++ b/contracts/storage/MappedMetaStorage.sol @@ -53,6 +53,9 @@ import { MetaStorage } from "../../contracts/storage/MetaStorage.sol"; import { Versioned } from "../../contracts/access/Versioned.sol"; import { VersionedContract } from "../../contracts/access/VersionedContract.sol"; +/** + * @notice MappedMetaStoraged Eternal storage for Metadata in Collective Goveranance + */ contract MappedMetaStorage is MetaStorage, VersionedContract, ERC165, Ownable { string public constant NAME = "meta storage"; diff --git a/contracts/storage/MetaStorageFactory.sol b/contracts/storage/MetaStorageFactory.sol index 9421369..28e0b27 100644 --- a/contracts/storage/MetaStorageFactory.sol +++ b/contracts/storage/MetaStorageFactory.sol @@ -55,7 +55,7 @@ import { VersionedContract } from "../../contracts/access/VersionedContract.sol" import { OwnableInitializable } from "../../contracts/access/OwnableInitializable.sol"; /** - * @title CollectiveStorage creational contract + * @title MetaStorage creational contract */ contract MetaStorageFactory is VersionedContract, OwnableInitializable, UUPSUpgradeable, Initializable, ERC165 { event UpgradeAuthorized(address sender, address owner); diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..0a25c19 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 0a25c1940ca220686588c4af3ec526f725fe2582 diff --git a/package.json b/package.json index f1d8ff0..0773bb9 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "prettier-plugin-solidity": "1.1.3" }, "dependencies": { - "@openzeppelin/contracts": "4.8.3", "nvm": "0.0.4" }, "keywords": [], diff --git a/remappings.txt b/remappings.txt index 217c567..81da917 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,3 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ -@openzeppelin/=node_modules/@openzeppelin +@openzeppelin/=lib/openzeppelin-contracts/ diff --git a/script/DeployCollective.sol b/script/DeployCollective.sol index 73b4df2..74cffa7 100644 --- a/script/DeployCollective.sol +++ b/script/DeployCollective.sol @@ -46,7 +46,7 @@ pragma solidity ^0.8.15; import { Script } from "forge-std/Script.sol"; -import { WeightedClassFactory, ProjectClassFactory } from "../contracts/community/CommunityFactory.sol"; +import { WeightedClassFactory, TokenClassFactory } from "../contracts/community/CommunityFactory.sol"; import { CommunityBuilder } from "../contracts/community/CommunityBuilder.sol"; import { CommunityBuilderProxy } from "../contracts/community/CommunityBuilderProxy.sol"; import { StorageFactory } from "../contracts/storage/StorageFactory.sol"; diff --git a/script/DeployCommunityBuilder.sol b/script/DeployCommunityBuilder.sol index 64c4bf4..37b8d41 100644 --- a/script/DeployCommunityBuilder.sol +++ b/script/DeployCommunityBuilder.sol @@ -46,7 +46,7 @@ pragma solidity ^0.8.15; import { Script } from "forge-std/Script.sol"; -import { WeightedClassFactory, ProjectClassFactory } from "../contracts/community/CommunityFactory.sol"; +import { WeightedClassFactory, ProjectClassFactory, TokenClassFactory } from "../contracts/community/CommunityFactory.sol"; import { CommunityBuilder } from "../contracts/community/CommunityBuilder.sol"; import { CommunityBuilderProxy } from "../contracts/community/CommunityBuilderProxy.sol"; @@ -64,12 +64,14 @@ contract DeployCommunityBuilder is Script { vm.startBroadcast(); WeightedClassFactory _weightedFactory = new WeightedClassFactory(); ProjectClassFactory _projectFactory = new ProjectClassFactory(); + TokenClassFactory _tokenFactory = new TokenClassFactory(); CommunityBuilder _builder = new CommunityBuilder(); CommunityBuilderProxy _proxy = new CommunityBuilderProxy( address(_builder), address(_weightedFactory), - address(_projectFactory) + address(_projectFactory), + address(_tokenFactory) ); emit CommunityBuilderDeployed(address(_proxy)); vm.stopBroadcast(); @@ -84,10 +86,11 @@ contract DeployCommunityBuilder is Script { vm.startBroadcast(); WeightedClassFactory _weightedFactory = new WeightedClassFactory(); ProjectClassFactory _projectFactory = new ProjectClassFactory(); + TokenClassFactory _tokenFactory = new TokenClassFactory(); CommunityBuilder _builder = new CommunityBuilder(); CommunityBuilderProxy _pbuilder = CommunityBuilderProxy(_proxy); - _pbuilder.upgrade(address(_builder), address(_weightedFactory), address(_projectFactory)); + _pbuilder.upgrade(address(_builder), address(_weightedFactory), address(_projectFactory), address(_tokenFactory)); emit CommunityBuilderUpgraded(address(_proxy)); vm.stopBroadcast(); } diff --git a/script/DeployProposalBuilder.sol b/script/DeployProposalBuilder.sol index 5ef8668..6e6a2e3 100644 --- a/script/DeployProposalBuilder.sol +++ b/script/DeployProposalBuilder.sol @@ -46,6 +46,8 @@ pragma solidity ^0.8.15; import { Script } from "forge-std/Script.sol"; +import { OwnableInitializable } from "../contracts/access/OwnableInitializable.sol"; + import { ProposalBuilder } from "../contracts/ProposalBuilder.sol"; import { ProposalBuilderProxy } from "../contracts/ProposalBuilderProxy.sol"; @@ -67,6 +69,9 @@ contract DeployProposalBuilder is Script { ProposalBuilder _builder = new ProposalBuilder(); ProposalBuilderProxy _proxy = new ProposalBuilderProxy(address(_builder), _governance, _storage, _meta); emit ProposalBuilderDeployed(address(_proxy)); + + OwnableInitializable _ownable = OwnableInitializable(_meta); + _ownable.transferOwnership(address(_proxy)); vm.stopBroadcast(); } diff --git a/site/index.rst b/site/index.rst index 297659b..627f035 100644 --- a/site/index.rst +++ b/site/index.rst @@ -40,8 +40,8 @@ _____________ Contract Ethereum Address Version Description ===================== ========================================== =========== =========================== `Constant`_ 0xD5DA9B812806E080948476A801d2004f3305E63F 0.9.8 Constant library -`CommunityBuilder`_ 0x4ba7E0dc43180Cb10EF53FF9Da923E02f459Ec9F 0.9.8 CommunityBuilder Factory -`GovernanceBuilder`_ 0xe1a13ea37F2BFE35B612799fcf9eBD43efA00B87 0.9.8 Governance Contract Builder +`CommunityBuilder`_ 0x4ba7E0dc43180Cb10EF53FF9Da923E02f459Ec9F LATEST CommunityBuilder Factory +`GovernanceBuilder`_ 0xe1a13ea37F2BFE35B612799fcf9eBD43efA00B87 LATEST Governance Contract Builder ===================== ========================================== =========== =========================== diff --git a/test/community/CommunityBuilder.t.sol b/test/community/CommunityBuilder.t.sol index b67679c..3fb4b8b 100644 --- a/test/community/CommunityBuilder.t.sol +++ b/test/community/CommunityBuilder.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.15; import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC20PresetFixedSupply } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import { Test } from "forge-std/Test.sol"; @@ -12,9 +14,8 @@ import { OwnableInitializable } from "../../contracts/access/OwnableInitializabl import { AddressCollection } from "../../contracts/collection/AddressSet.sol"; import { CommunityBuilder } from "../../contracts/community/CommunityBuilder.sol"; import { createCommunityBuilder, CommunityBuilderProxy } from "../../contracts/community/CommunityBuilderProxy.sol"; -import { VoterClass } from "../../contracts/community/VoterClass.sol"; import { CommunityClass, WeightedCommunityClass } from "../../contracts/community/CommunityClass.sol"; -import { WeightedClassFactory, ProjectClassFactory } from "../../contracts/community/CommunityFactory.sol"; +import { WeightedClassFactory, ProjectClassFactory, TokenClassFactory } from "../../contracts/community/CommunityFactory.sol"; import { MockERC721 } from "../mock/MockERC721.sol"; @@ -192,7 +193,7 @@ contract CommunityBuilderTest is Test { .withQuorum(1) .withCommunitySupervisor(_SUPERVISOR) .build(); - VoterClass _class = VoterClass(_classAddress); + CommunityClass _class = CommunityClass(_classAddress); assertTrue(_class.isVoter(address(0x1))); } @@ -205,8 +206,8 @@ contract CommunityBuilderTest is Test { .withQuorum(1) .withCommunitySupervisor(_SUPERVISOR) .build(); - VoterClass _class = VoterClass(_classAddress); - assertTrue(_class.supportsInterface(type(VoterClass).interfaceId)); + CommunityClass _class = CommunityClass(_classAddress); + assertTrue(_class.supportsInterface(type(CommunityClass).interfaceId)); assertTrue(_class.supportsInterface(type(CommunityClass).interfaceId)); assertTrue(_class.isVoter(address(0x1))); } @@ -289,10 +290,40 @@ contract CommunityBuilderTest is Test { function testUpgradeRequiresOwner() public { WeightedClassFactory _wFactory = new WeightedClassFactory(); ProjectClassFactory _pFactory = new ProjectClassFactory(); + TokenClassFactory _tFactory = new TokenClassFactory(); CommunityBuilder _cBuilder = new CommunityBuilder(); CommunityBuilderProxy _proxy = CommunityBuilderProxy(payable(address(_builder))); vm.expectRevert(abi.encodeWithSelector(OwnableInitializable.NotOwner.selector, _OTHER)); vm.prank(_OTHER, _OTHER); - _proxy.upgrade(address(_cBuilder), address(_wFactory), address(_pFactory)); + _proxy.upgrade(address(_cBuilder), address(_wFactory), address(_pFactory), address(_tFactory)); + } + + function testErc20Community() public { + uint256 tokenCount = 75; + IERC20 _token = new ERC20PresetFixedSupply("TestToken", "TT20", tokenCount, address(0x1234)); + address _classAddress = _builder + .aCommunity() + .asErc20Community(address(_token)) + .withQuorum(1) + .withCommunitySupervisor(_SUPERVISOR) + .build(); + CommunityClass _class = CommunityClass(_classAddress); + assertTrue(_class.isVoter(address(0x1234))); + } + + function testClosedErc20Community() public { + uint256 tokenCount = 75; + IERC20 _token = new ERC20PresetFixedSupply("TestToken", "TT20", tokenCount, address(0x1234)); + address _classAddress = _builder + .aCommunity() + .asClosedErc20Community(address(_token), 25) + .withQuorum(1) + .withCommunitySupervisor(_SUPERVISOR) + .build(); + CommunityClass _class = CommunityClass(_classAddress); + assertTrue(_class.isVoter(address(0x1234))); + assertFalse(_class.isVoter(address(0x1))); + assertTrue(_class.canPropose(address(0x1234))); + assertFalse(_class.canPropose(address(0x1))); } } diff --git a/test/community/CommunityBuilderProxy.t.sol b/test/community/CommunityBuilderProxy.t.sol index 8f52a24..6f70eba 100644 --- a/test/community/CommunityBuilderProxy.t.sol +++ b/test/community/CommunityBuilderProxy.t.sol @@ -5,7 +5,7 @@ import { Test } from "forge-std/Test.sol"; import { CommunityBuilder } from "../../contracts/community/CommunityBuilder.sol"; import { createCommunityBuilder, CommunityBuilderProxy } from "../../contracts/community/CommunityBuilderProxy.sol"; -import { WeightedClassFactory, ProjectClassFactory } from "../../contracts/community/CommunityFactory.sol"; +import { WeightedClassFactory, ProjectClassFactory, TokenClassFactory } from "../../contracts/community/CommunityFactory.sol"; contract CommunityBuilderProxyTest is Test { CommunityBuilder private _builder; @@ -21,10 +21,11 @@ contract CommunityBuilderProxyTest is Test { function testProxyUpgrade() public { WeightedClassFactory _weightedFactory = new WeightedClassFactory(); ProjectClassFactory _projectFactory = new ProjectClassFactory(); + TokenClassFactory _tokenFactory = new TokenClassFactory(); address payable _paddr = payable(address(_builder)); CommunityBuilderProxy _proxy = CommunityBuilderProxy(_paddr); CommunityBuilder _cbuilder = new CBuilder2(); - _proxy.upgrade(address(_cbuilder), address(_weightedFactory), address(_projectFactory)); + _proxy.upgrade(address(_cbuilder), address(_weightedFactory), address(_projectFactory), address(_tokenFactory)); assertEq(_builder.name(), "test upgrade"); } } diff --git a/test/community/CommunityClassClosedERC20.t.sol b/test/community/CommunityClassClosedERC20.t.sol new file mode 100644 index 0000000..eeee454 --- /dev/null +++ b/test/community/CommunityClassClosedERC20.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.15; + +import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC20PresetFixedSupply } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; + +import { Test } from "forge-std/Test.sol"; + +import { Mutable } from "../../contracts/access/Mutable.sol"; +import { VoterClass } from "../../contracts/community/VoterClass.sol"; +import { WeightedCommunityClass } from "../../contracts/community/CommunityClass.sol"; +import { CommunityBuilder } from "../../contracts/community/CommunityBuilder.sol"; +import { createCommunityBuilder } from "../../contracts/community/CommunityBuilderProxy.sol"; +import { CommunityClassClosedERC20 } from "../../contracts/community/CommunityClassClosedERC20.sol"; + +import { Versioned } from "../../contracts/access/Versioned.sol"; + +contract CommunityClassClosedERC20Test is Test { + address private constant _OWNER = address(0xffeeeeff); + address private constant _NOTOWNER = address(0x55); + address private constant _PARTOWNER = address(0x56); + address private constant _SUPERVISOR = address(0x1234); + uint256 private constant _NTOKEN = 10000; + + ERC20PresetFixedSupply private _tokenContract; + WeightedCommunityClass private _class; + CommunityBuilder private _builder; + + function setUp() public { + _tokenContract = new ERC20PresetFixedSupply("TestToken", "TT20", _NTOKEN, _OWNER); + _builder = createCommunityBuilder(); + address _classAddress = _builder + .aCommunity() + .asClosedErc20Community(address(_tokenContract), _NTOKEN / 2) + .withQuorum(1) + .withCommunitySupervisor(_SUPERVISOR) + .build(); + _class = WeightedCommunityClass(_classAddress); + } + + function testOpenToMemberPropose() public { + assertTrue(_class.canPropose(_OWNER)); + } + + function testClosedToPartOwner() public { + vm.prank(_OWNER); + _tokenContract.transfer(_PARTOWNER, _NTOKEN / 2 - 1); + assertFalse(_class.canPropose(_PARTOWNER)); + } + + function testClosedToPropose() public { + assertFalse(_class.canPropose(_NOTOWNER)); + } + + function testFinal() public { + assertTrue(_class.isFinal()); + } + + function testName() public { + assertEq("CommunityClassERC20", _class.name()); + } +} diff --git a/test/community/CommunityClassERC20.t.sol b/test/community/CommunityClassERC20.t.sol new file mode 100644 index 0000000..7385c88 --- /dev/null +++ b/test/community/CommunityClassERC20.t.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.15; + +import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC20PresetFixedSupply } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; + +import { Test } from "forge-std/Test.sol"; + +import { Mutable } from "../../contracts/access/Mutable.sol"; +import { VoterClass } from "../../contracts/community/VoterClass.sol"; +import { WeightedCommunityClass } from "../../contracts/community/CommunityClass.sol"; +import { CommunityBuilder } from "../../contracts/community/CommunityBuilder.sol"; +import { createCommunityBuilder } from "../../contracts/community/CommunityBuilderProxy.sol"; +import { CommunityClassERC20 } from "../../contracts/community/CommunityClassERC20.sol"; + +import { Versioned } from "../../contracts/access/Versioned.sol"; + +contract CommunityClassERC20Test is Test { + address private constant _OWNER = address(0xffeeeeff); + address private constant _NOTOWNER = address(0x55); + address private constant _SUPERVISOR = address(0x1234); + uint256 private constant _NTOKEN = 10000; + + IERC20 private _tokenContract; + WeightedCommunityClass private _class; + CommunityBuilder private _builder; + + function setUp() public { + _tokenContract = new ERC20PresetFixedSupply("TestToken", "TT20", _NTOKEN, _OWNER); + _builder = createCommunityBuilder(); + address _classAddress = _builder + .aCommunity() + .asErc20Community(address(_tokenContract)) + .withQuorum(1) + .withCommunitySupervisor(_SUPERVISOR) + .build(); + _class = WeightedCommunityClass(_classAddress); + } + + function testDiscoveryNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(VoterClass.NotVoter.selector, _NOTOWNER)); + _class.discover(_NOTOWNER); + } + + function testDiscoveryOwner() public { + uint256[] memory tokens = _class.discover(_OWNER); + assertEq(tokens.length, 1); + assertEq(tokens[0], uint160(_OWNER)); + } + + function testConfirmOwner() public { + uint256 _count = _class.confirm(_OWNER, uint160(_OWNER)); + assertEq(_count, _NTOKEN); + } + + function testConfirmNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(VoterClass.NotVoter.selector, _NOTOWNER)); + _class.confirm(_NOTOWNER, uint160(_NOTOWNER)); + } + + function testOpenToPropose() public { + assertTrue(_class.canPropose(_NOTOWNER)); + } + + function testWeight() public { + assertEq(1, _class.weight()); + } + + function testFinal() public { + assertTrue(_class.isFinal()); + } + + function testSupportsInterface() public { + IERC165 _erc165 = IERC165(address(_class)); + assertTrue(_erc165.supportsInterface(type(VoterClass).interfaceId)); + assertTrue(_erc165.supportsInterface(type(Mutable).interfaceId)); + assertTrue(_erc165.supportsInterface(type(IERC165).interfaceId)); + } + + function testSupportsInterfaceVersioned() public { + bytes4 ifId = type(Versioned).interfaceId; + assertTrue(_class.supportsInterface(ifId)); + } + + function testName() public { + assertEq("CommunityClassERC20", _class.name()); + } +} diff --git a/test/community/CommunityFactory.t.sol b/test/community/CommunityFactory.t.sol index 20a6cd5..48efb7d 100644 --- a/test/community/CommunityFactory.t.sol +++ b/test/community/CommunityFactory.t.sol @@ -1,17 +1,22 @@ // SPDX-License-Identifier: BSD-3-Clause pragma solidity ^0.8.15; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC20PresetFixedSupply } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; + + import { Test } from "forge-std/Test.sol"; import { Constant } from "../../contracts/Constant.sol"; import { Mutable } from "../../contracts/access/Mutable.sol"; import { AddressCollection, AddressSet } from "../../contracts/collection/AddressSet.sol"; -import { upgradeOpenVote, upgradeVoterPool, upgradeErc721, upgradeClosedErc721, WeightedClassFactory, ProjectClassFactory } from "../../contracts/community/CommunityFactory.sol"; +import { upgradeOpenVote, upgradeVoterPool, upgradeErc721, upgradeClosedErc721, upgradeErc20, upgradeClosedErc20, WeightedClassFactory, ProjectClassFactory, TokenClassFactory } from "../../contracts/community/CommunityFactory.sol"; import { WeightedCommunityClass, ProjectCommunityClass } from "../../contracts/community/CommunityClass.sol"; import { MockERC721 } from "../../test/mock/MockERC721.sol"; contract WeightedCommunityFactoryTest is Test { + address private constant _OWNER = address(0x1234); address private constant _OTHER = address(0x10); WeightedClassFactory private _weightedFactory; AddressCollection private _supervisorSet; @@ -19,7 +24,7 @@ contract WeightedCommunityFactoryTest is Test { function setUp() public { _weightedFactory = new WeightedClassFactory(); _supervisorSet = new AddressSet(); - _supervisorSet.add(address(0x1234)); + _supervisorSet.add(_OWNER); } function testOpenVote() public { @@ -35,7 +40,7 @@ contract WeightedCommunityFactoryTest is Test { _supervisorSet ); assertEq(_class.weight(), 19); - assertTrue(_class.communitySupervisorSet().contains(address(0x1234))); + assertTrue(_class.communitySupervisorSet().contains(_OWNER)); } function testOpenVoteUpgrade() public { @@ -50,7 +55,7 @@ contract WeightedCommunityFactoryTest is Test { Constant.MAXIMUM_REBATE_BASE_FEE, _supervisorSet ); - _supervisorSet.erase(address(0x1234)); + _supervisorSet.erase(_OWNER); _supervisorSet.add(address(0x1235)); upgradeOpenVote( payable(address(_class)), @@ -65,7 +70,7 @@ contract WeightedCommunityFactoryTest is Test { _supervisorSet ); assertEq(_class.weight(), 20); - assertFalse(_class.communitySupervisorSet().contains(address(0x1234))); + assertFalse(_class.communitySupervisorSet().contains(_OWNER)); assertTrue(_class.communitySupervisorSet().contains(address(0x1235))); } @@ -110,7 +115,7 @@ contract WeightedCommunityFactoryTest is Test { assertEq(_class.weight(), 10); Mutable _mutable = Mutable(address(_class)); assertFalse(_mutable.isFinal()); - assertTrue(_class.communitySupervisorSet().contains(address(0x1234))); + assertTrue(_class.communitySupervisorSet().contains(_OWNER)); } function testPoolComunityUpgrade() public { @@ -125,7 +130,7 @@ contract WeightedCommunityFactoryTest is Test { Constant.MAXIMUM_REBATE_BASE_FEE, _supervisorSet ); - _supervisorSet.erase(address(0x1234)); + _supervisorSet.erase(_OWNER); _supervisorSet.add(address(0x1235)); upgradeVoterPool( payable(address(_class)), @@ -142,19 +147,20 @@ contract WeightedCommunityFactoryTest is Test { assertEq(_class.weight(), 11); Mutable _mutable = Mutable(address(_class)); assertFalse(_mutable.isFinal()); - assertFalse(_class.communitySupervisorSet().contains(address(0x1234))); + assertFalse(_class.communitySupervisorSet().contains(_OWNER)); assertTrue(_class.communitySupervisorSet().contains(address(0x1235))); } } contract ProjectFactoryTest is Test { + address private constant _OWNER = address(0x1234); ProjectClassFactory private _projectFactory; AddressSet private _supervisorSet; function setUp() public { _projectFactory = new ProjectClassFactory(); _supervisorSet = new AddressSet(); - _supervisorSet.add(address(0x1234)); + _supervisorSet.add(_OWNER); } function testErc721() public { @@ -175,7 +181,7 @@ contract ProjectFactoryTest is Test { assertEq(_class.weight(), 2); assertTrue(_class.canPropose(address(0x1))); assertTrue(_class.canPropose(address(0x2))); - assertTrue(_class.communitySupervisorSet().contains(address(0x1234))); + assertTrue(_class.communitySupervisorSet().contains(_OWNER)); } function testUpgradeErc721() public { @@ -193,7 +199,7 @@ contract ProjectFactoryTest is Test { Constant.MAXIMUM_REBATE_BASE_FEE, _supervisorSet ); - _supervisorSet.erase(address(0x1234)); + _supervisorSet.erase(_OWNER); _supervisorSet.add(address(0x1235)); upgradeErc721( payable(address(_class)), @@ -210,7 +216,7 @@ contract ProjectFactoryTest is Test { assertEq(_class.weight(), 3); assertTrue(_class.canPropose(address(0x1))); assertTrue(_class.canPropose(address(0x2))); - assertFalse(_class.communitySupervisorSet().contains(address(0x1234))); + assertFalse(_class.communitySupervisorSet().contains(_OWNER)); assertTrue(_class.communitySupervisorSet().contains(address(0x1235))); } @@ -233,7 +239,7 @@ contract ProjectFactoryTest is Test { assertEq(_class.weight(), 2); assertTrue(_class.canPropose(address(0x1))); assertFalse(_class.canPropose(address(0x2))); - assertTrue(_class.communitySupervisorSet().contains(address(0x1234))); + assertTrue(_class.communitySupervisorSet().contains(_OWNER)); } function testUpgradeClosedErc721() public { @@ -252,7 +258,7 @@ contract ProjectFactoryTest is Test { Constant.MAXIMUM_REBATE_BASE_FEE, _supervisorSet ); - _supervisorSet.erase(address(0x1234)); + _supervisorSet.erase(_OWNER); _supervisorSet.add(address(0x1235)); upgradeClosedErc721( payable(address(_class)), @@ -269,7 +275,127 @@ contract ProjectFactoryTest is Test { assertEq(_class.weight(), 7); assertTrue(_class.canPropose(address(0x1))); assertFalse(_class.canPropose(address(0x2))); - assertFalse(_class.communitySupervisorSet().contains(address(0x1234))); + assertFalse(_class.communitySupervisorSet().contains(_OWNER)); assertTrue(_class.communitySupervisorSet().contains(address(0x1235))); } } + +contract TokenFactoryTest is Test { + address private constant _OWNER = address(0x1234); + uint256 private constant _NTOKEN = 1000; + + TokenClassFactory private _tokenFactory; + AddressSet private _supervisorSet; + IERC20 private _tokenContract; + + function setUp() public { + _tokenContract = new ERC20PresetFixedSupply("TestToken", "TT20", _NTOKEN, _OWNER); + _tokenFactory = new TokenClassFactory(); + _supervisorSet = new AddressSet(); + _supervisorSet.add(_OWNER); + } + + function testErc20() public { + ProjectCommunityClass _class = _tokenFactory.createErc20( + address(_tokenContract), + 2, + Constant.MINIMUM_PROJECT_QUORUM, + Constant.MINIMUM_VOTE_DELAY, + Constant.MAXIMUM_VOTE_DELAY, + Constant.MINIMUM_VOTE_DURATION, + Constant.MAXIMUM_VOTE_DURATION, + Constant.MAXIMUM_REBATE_GAS_USED, + Constant.MAXIMUM_REBATE_BASE_FEE, + _supervisorSet + ); + assertEq(_class.weight(), 2); + assertTrue(_class.canPropose(_OWNER)); + assertTrue(_class.communitySupervisorSet().contains(_OWNER)); + } + + function testClosedErc20() public { + ProjectCommunityClass _class = _tokenFactory.createClosedErc20( + address(_tokenContract), + 25, + 2, + Constant.MINIMUM_PROJECT_QUORUM, + Constant.MINIMUM_VOTE_DELAY, + Constant.MAXIMUM_VOTE_DELAY, + Constant.MINIMUM_VOTE_DURATION, + Constant.MAXIMUM_VOTE_DURATION, + Constant.MAXIMUM_REBATE_GAS_USED, + Constant.MAXIMUM_REBATE_BASE_FEE, + _supervisorSet + ); + assertEq(_class.weight(), 2); + assertTrue(_class.canPropose(_OWNER)); + assertTrue(_class.communitySupervisorSet().contains(_OWNER)); + } + + function testErc20Upgrade() public { + ProjectCommunityClass _class = _tokenFactory.createErc20( + address(_tokenContract), + 2, + Constant.MINIMUM_PROJECT_QUORUM, + Constant.MINIMUM_VOTE_DELAY, + Constant.MAXIMUM_VOTE_DELAY, + Constant.MINIMUM_VOTE_DURATION, + Constant.MAXIMUM_VOTE_DURATION, + Constant.MAXIMUM_REBATE_GAS_USED, + Constant.MAXIMUM_REBATE_BASE_FEE, + _supervisorSet + ); + + upgradeErc20( + payable(address(_class)), + 3, + Constant.MINIMUM_PROJECT_QUORUM + 1, + Constant.MINIMUM_VOTE_DELAY, + Constant.MAXIMUM_VOTE_DELAY, + Constant.MINIMUM_VOTE_DURATION, + Constant.MAXIMUM_VOTE_DURATION, + Constant.MAXIMUM_REBATE_GAS_USED, + Constant.MAXIMUM_REBATE_BASE_FEE, + _supervisorSet + ); + + assertEq(_class.weight(), 3); + assertTrue(_class.canPropose(_OWNER)); + assertEq(Constant.MINIMUM_PROJECT_QUORUM + 1, _class.minimumProjectQuorum()); + assertTrue(_class.communitySupervisorSet().contains(_OWNER)); + } + + function testClosedErc20Upgrade() public { + ProjectCommunityClass _class = _tokenFactory.createClosedErc20( + address(_tokenContract), + 25, + 2, + Constant.MINIMUM_PROJECT_QUORUM, + Constant.MINIMUM_VOTE_DELAY, + Constant.MAXIMUM_VOTE_DELAY, + Constant.MINIMUM_VOTE_DURATION, + Constant.MAXIMUM_VOTE_DURATION, + Constant.MAXIMUM_REBATE_GAS_USED, + Constant.MAXIMUM_REBATE_BASE_FEE, + _supervisorSet + ); + + upgradeClosedErc20( + payable(address(_class)), + 3, + Constant.MINIMUM_PROJECT_QUORUM + 1, + Constant.MINIMUM_VOTE_DELAY, + Constant.MAXIMUM_VOTE_DELAY, + Constant.MINIMUM_VOTE_DURATION, + Constant.MAXIMUM_VOTE_DURATION, + Constant.MAXIMUM_REBATE_GAS_USED, + Constant.MAXIMUM_REBATE_BASE_FEE, + _supervisorSet + ); + + assertEq(_class.weight(), 3); + assertEq(Constant.MINIMUM_PROJECT_QUORUM + 1, _class.minimumProjectQuorum()); + assertTrue(_class.canPropose(_OWNER)); + assertTrue(_class.communitySupervisorSet().contains(_OWNER)); + } +} diff --git a/test/governance/CollectiveGovernance.t.sol b/test/governance/CollectiveGovernance.t.sol index c75c4f3..92dc031 100644 --- a/test/governance/CollectiveGovernance.t.sol +++ b/test/governance/CollectiveGovernance.t.sol @@ -6,6 +6,7 @@ import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import { IERC721 } from "@openzeppelin/contracts/interfaces/IERC721.sol"; import { IERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; +import { ERC20PresetMinterPauser } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; import { Test } from "forge-std/Test.sol"; @@ -1632,6 +1633,50 @@ contract CollectiveGovernanceTest is Test { assertTrue(flag.isSet()); assertTrue(_storage.isExecuted(proposalId)); } + + function testErc20Vote() public { + ERC20PresetMinterPauser _token = new ERC20PresetMinterPauser("Test20", "ERT20"); + (_governanceAddress, _storageAddress, ) = buildERC20(address(_token)); + governance = CollectiveGovernance(_governanceAddress); + _storage = Storage(_storageAddress); + version = governance.version(); + _token.mint(_OWNER, 100); + vm.prank(_OWNER, _OWNER); + proposalId = governance.propose(); + assertEq(proposalId, PROPOSAL_ID); + vm.startPrank(_SUPERVISOR, _SUPERVISOR); + governance.configure(proposalId, 100); + governance.startVote(proposalId); + vm.stopPrank(); + _token.mint(_VOTER1, 100); + vm.prank(_VOTER1, _VOTER1); + governance.voteFor(proposalId); + assertEq(_storage.forVotes(proposalId), 100); + } + + function testErc20TokenDistributionDoesNotEnableRevote() public { + ERC20PresetMinterPauser _token = new ERC20PresetMinterPauser("Test20", "ERT20"); + (_governanceAddress, _storageAddress, ) = buildERC20(address(_token)); + governance = CollectiveGovernance(_governanceAddress); + _storage = Storage(_storageAddress); + version = governance.version(); + _token.mint(_OWNER, 100); + vm.prank(_OWNER, _OWNER); + proposalId = governance.propose(); + assertEq(proposalId, PROPOSAL_ID); + vm.startPrank(_SUPERVISOR, _SUPERVISOR); + governance.configure(proposalId, 100); + governance.startVote(proposalId); + vm.stopPrank(); + _token.mint(_VOTER1, 100); + vm.prank(_VOTER1, _VOTER1); + governance.voteFor(proposalId); + _token.mint(_VOTER1, 100); + vm.expectRevert(abi.encodeWithSelector(Storage.TokenVoted.selector, proposalId, _VOTER1, uint160(_VOTER1))); + vm.prank(_VOTER1, _VOTER1); + governance.voteFor(proposalId); + } + function mintTokens() private returns (IERC721) { MockERC721 merc721 = new MockERC721(); @@ -1661,6 +1706,19 @@ contract CollectiveGovernanceTest is Test { return _builder.aGovernance().withCommunityClass(_class).build(); } + function buildERC20(address projectAddress) private returns (address payable, address, address) { + CommunityBuilder _communityBuilder = createCommunityBuilder(); + address _communityLocation = _communityBuilder + .aCommunity() + .asClosedErc20Community(projectAddress, 100) + .withQuorum(99) + .withCommunitySupervisor(_SUPERVISOR) + .build(); + CommunityClass _class = CommunityClass(_communityLocation); + return _builder.aGovernance().withCommunityClass(_class).build(); + } + + function buildERC721( address projectAddress, uint256 minimumProjectQuorum, diff --git a/test/mock/MockERC721Enum.t.sol b/test/mock/MockERC721Enum.t.sol index aead050..8de8d22 100644 --- a/test/mock/MockERC721Enum.t.sol +++ b/test/mock/MockERC721Enum.t.sol @@ -38,7 +38,7 @@ contract MockERC721EnumTest is Test { assertEq(erc721.tokenOfOwnerByIndex(_nextowner, 0), _tokenId3); } - function testFailOwnerInvalidIndex(uint256 index) public { + function testFailOwnerInvalidIndex(uint256 index) public view { vm.assume(index > 2); erc721.tokenOfOwnerByIndex(_owner, index); } @@ -50,7 +50,7 @@ contract MockERC721EnumTest is Test { assertEq(erc721.tokenByIndex(3), _tokenId4); } - function testFailTokenGlobalInvalidIndex(uint256 index) public { + function testFailTokenGlobalInvalidIndex(uint256 index) public view { vm.assume(index > 3); erc721.tokenByIndex(index); } diff --git a/yarn.lock b/yarn.lock index 05f42a1..4c7fe9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,11 +23,6 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@openzeppelin/contracts@4.8.3": - version "4.8.3" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.3.tgz#cbef3146bfc570849405f59cba18235da95a252a" - integrity sha512-bQHV8R9Me8IaJoJ2vPG4rXcL7seB7YVuskr4f+f5RyOStSZetwzkWtoqDMl5erkBJy0lDRUnIR2WIkPiC0GJlg== - "@solidity-parser/parser@^0.14.5": version "0.14.5" resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.5.tgz#87bc3cc7b068e08195c219c91cd8ddff5ef1a804"