diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d5b6628 --- /dev/null +++ b/.env.example @@ -0,0 +1,52 @@ +# NETWORK AND DEPLOYMENT WALLET +DEPLOYMENT_PRIVATE_KEY="..." +ALCHEMY_API_KEY="..." +ETHERSCAN_API_KEY="..." +NETWORK="sepolia" + +# With false, the script will deploy mock tokens +DEPLOY_AS_PRODUCTION=true +MULTISIG_MEMBERS_JSON_FILE_NAME="/script/multisig-members.json" + +# MULTISIG PARAMETERS +MIN_APPROVALS="5" # How many multisig approvals are required +MULTISIG_PROPOSAL_EXPIRATION_PERIOD="864000" # How long until a pending proposal expires (10 days) + +# GAUGE VOTER PARAMETERS +# The token to be used for the escrow +TOKEN1_ADDRESS="0xdc518215FCbeB2b641073F4387895E64d65D51fB" +VE_TOKEN1_NAME="Voting Escrow Token 1" +VE_TOKEN1_SYMBOL="veTK1" + +# Additional tokens +TOKEN2_ADDRESS="0x0000000000000000000000000000000000000000" # Ignored if 0x0 +VE_TOKEN2_NAME="Voting Escrow Token 2" +VE_TOKEN2_SYMBOL="veTK2" + +# 1 ether = 100%, represents the fee taken on withdrawals +FEE_PERCENT_WEI="0" + +# Min seconds after depositing before voting is possible +WARMUP_PERIOD="259200" # 3 days + +# Min seconds after queuing an exit before withdrawing is possible +COOLDOWN_PERIOD="259200" # 3 days + +# Min seconds a user must have locked in escrow before they can queue an exit +MIN_LOCK_DURATION="4838400" # 8 weeks + +# Prevent voting until manually activated by the multisig +VOTING_PAUSED=true + +# PLUGIN REPO PARAMETERS (per-network) +# SEPOLIA +MULTISIG_PLUGIN_REPO_ADDRESS="0x9e7956C8758470dE159481e5DD0d08F8B59217A2" +MULTISIG_PLUGIN_RELEASE="1" +MULTISIG_PLUGIN_BUILD="2" +SIMPLE_GAUGE_VOTER_REPO_ENS_SUBDOMAIN="my-simple-gauge-voter-0" + +# OSx IMPLEMENTATIONS ADDRESSES (network dependent, see active_contracts.json on lib/osx) +# SEPOLIA +DAO_FACTORY="0x7a62da7B56fB3bfCdF70E900787010Bc4c9Ca42e" +PLUGIN_SETUP_PROCESSOR="0xC24188a73dc09aA7C721f96Ad8857B469C01dC9f" +PLUGIN_REPO_FACTORY="0x07f49c49Ce2A99CF7C28F66673d406386BDD8Ff4" diff --git a/.gitmodules b/.gitmodules index 72ef667..8b58daf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ branch = v4.9.2 [submodule "lib/solmate"] path = lib/solmate url = https://github.com/transmissions11/solmate +[submodule "lib/ens-contracts"] + path = lib/ens-contracts + url = https://github.com/ensdomains/ens-contracts diff --git a/README.md b/README.md index 1e007cf..7762dd9 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,43 @@ The main workflow in the Mode Governance build is as follows: ## Curve design To build a flexible approach to curve design, we reviewed implementations such as seen in Curve and Aerodrome and attempted to generalise to higher order polynomials [Details on the curve design research can be found here](https://github.com/jordaniza/ve-explainer/blob/main/README.md) + +## Deployment + +To deploy the DAO, ensure that [Foundry](https://getfoundry.sh/) is installed on your computer. + +1. Edit `script/multisig-members.json` with the list of addresses to set as signers +2. Run `forge build && forge test` +3. Copy `.env.example` into `.env` and define the parameters + +```sh +# Load the env vars +source .env +``` + +```sh +# Set the right RPC URL +RPC_URL="https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}" +``` + +```sh +# Run the deployment script + +# If using Etherscan +forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify + +# If using BlockScout +forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify --verifier blockscout --verifier-url "https://sepolia.explorer.mode.network/api\?" +``` + +If you get the error Failed to get EIP-1559 fees, add `--legacy` to the command: + +```sh +forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify --legacy +``` + +If some contracts fail to verify on Etherscan, retry with this command: + +```sh +forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --verify --legacy --private-key "$DEPLOYMENT_PRIVATE_KEY" --resume +``` diff --git a/foundry.toml b/foundry.toml index 3a70d53..7e652f8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,6 +6,7 @@ bytecode_hash = "none" cbor_metadata = false fuzz = { runs = 256 } gas_reports = ["*"] +fs_permissions = [{ access = "read", path = "./script" }] libs = ["lib"] optimizer = true optimizer_runs = 10_000 diff --git a/lib/ens-contracts b/lib/ens-contracts new file mode 160000 index 0000000..a6139f0 --- /dev/null +++ b/lib/ens-contracts @@ -0,0 +1 @@ +Subproject commit a6139f0381ce6bcc584cbce36933c5ea95837212 diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..1a91f0f --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Script, console} from "forge-std/Script.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {GaugesDaoFactory, DeploymentParameters, Deployment, TokenParameters} from "../src/factory/GaugesDaoFactory.sol"; +import {MultisigSetup as MultisigPluginSetup} from "@aragon/osx/plugins/governance/multisig/MultisigSetup.sol"; +import {VotingEscrow, Clock, Lock, QuadraticIncreasingEscrow, ExitQueue, SimpleGaugeVoter, SimpleGaugeVoterSetup, ISimpleGaugeVoterSetupParams} from "src/voting/SimpleGaugeVoterSetup.sol"; +import {PluginRepo} from "@aragon/osx/framework/plugin/repo/PluginRepo.sol"; +import {PluginRepoFactory} from "@aragon/osx/framework/plugin/repo/PluginRepoFactory.sol"; +import {PluginSetupProcessor} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol"; +import {MockERC20} from "@mocks/MockERC20.sol"; + +contract Deploy is Script { + SimpleGaugeVoterSetup simpleGaugeVoterSetup; + + /// @dev Thrown when attempting to deploy a multisig with no members + error EmptyMultisig(); + + modifier broadcast() { + uint256 privKey = vm.envUint("DEPLOYMENT_PRIVATE_KEY"); + vm.startBroadcast(privKey); + console.log("Deploying from:", vm.addr(privKey)); + + _; + + vm.stopBroadcast(); + } + + /// @notice Runs the deployment flow, records the given parameters and artifacts, and it becomes read only + function run() public broadcast { + // Prepare all parameters + bool isProduction = vm.envBool("DEPLOY_AS_PRODUCTION"); + DeploymentParameters memory parameters = getDeploymentParameters(isProduction); + + // Create the DAO + GaugesDaoFactory factory = new GaugesDaoFactory(parameters); + factory.deployOnce(); + + // Done + printDeploymentSummary(factory); + } + + function getDeploymentParameters( + bool isProduction + ) internal returns (DeploymentParameters memory parameters) { + address[] memory multisigMembers = readMultisigMembers(); + TokenParameters[] memory tokenParameters = getTokenParameters(isProduction); + + // NOTE: Multisig is already deployed, using the existing Aragon's repo + // NOTE: Deploying the plugin setup from the current script to avoid code size constraints + + SimpleGaugeVoterSetup gaugeVoterPluginSetup = deploySimpleGaugeVoterPluginSetup(); + + parameters = DeploymentParameters({ + // Multisig settings + minApprovals: uint8(vm.envUint("MIN_APPROVALS")), + multisigMembers: multisigMembers, + // Gauge Voter + tokenParameters: tokenParameters, + feePercent: vm.envUint("FEE_PERCENT_WEI"), + warmupPeriod: uint64(vm.envUint("WARMUP_PERIOD")), + cooldownPeriod: uint64(vm.envUint("COOLDOWN_PERIOD")), + minLockDuration: uint64(vm.envUint("MIN_LOCK_DURATION")), + votingPaused: vm.envBool("VOTING_PAUSED"), + // Standard multisig repo + multisigPluginRepo: PluginRepo(vm.envAddress("MULTISIG_PLUGIN_REPO_ADDRESS")), + multisigPluginRelease: uint8(vm.envUint("MULTISIG_PLUGIN_RELEASE")), + multisigPluginBuild: uint16(vm.envUint("MULTISIG_PLUGIN_BUILD")), + // Voter plugin setup and ENS + voterPluginSetup: gaugeVoterPluginSetup, + voterEnsSubdomain: vm.envString("SIMPLE_GAUGE_VOTER_REPO_ENS_SUBDOMAIN"), + // OSx addresses + osxDaoFactory: vm.envAddress("DAO_FACTORY"), + pluginSetupProcessor: PluginSetupProcessor(vm.envAddress("PLUGIN_SETUP_PROCESSOR")), + pluginRepoFactory: PluginRepoFactory(vm.envAddress("PLUGIN_REPO_FACTORY")) + }); + } + + function readMultisigMembers() internal view returns (address[] memory result) { + // JSON list of members + string memory membersFilePath = vm.envString("MULTISIG_MEMBERS_JSON_FILE_NAME"); + string memory path = string.concat(vm.projectRoot(), membersFilePath); + string memory strJson = vm.readFile(path); + result = vm.parseJsonAddressArray(strJson, "$.members"); + + if (result.length == 0) revert EmptyMultisig(); + } + + function deploySimpleGaugeVoterPluginSetup() internal returns (SimpleGaugeVoterSetup result) { + result = new SimpleGaugeVoterSetup( + address(new SimpleGaugeVoter()), + address(new QuadraticIncreasingEscrow()), + address(new ExitQueue()), + address(new VotingEscrow()), + address(new Clock()), + address(new Lock()) + ); + } + + function getTokenParameters( + bool isProduction + ) internal returns (TokenParameters[] memory tokenParameters) { + if (isProduction) { + // USE TOKEN(s) + console.log("Using production parameters"); + + bool hasTwoTokens = vm.envAddress("TOKEN2_ADDRESS") != address(0); + tokenParameters = new TokenParameters[](hasTwoTokens ? 2 : 1); + + tokenParameters[0] = TokenParameters({ + token: vm.envAddress("TOKEN1_ADDRESS"), + veTokenName: vm.envString("VE_TOKEN1_NAME"), + veTokenSymbol: vm.envString("VE_TOKEN1_SYMBOL") + }); + + if (hasTwoTokens) { + tokenParameters[1] = TokenParameters({ + token: vm.envAddress("TOKEN2_ADDRESS"), + veTokenName: vm.envString("VE_TOKEN2_NAME"), + veTokenSymbol: vm.envString("VE_TOKEN2_SYMBOL") + }); + } + } else { + // MINT TEST TOKEN + console.log("Using testing parameters (minting 2 dev tokens)"); + + address[] memory multisigMembers = readMultisigMembers(); + tokenParameters = new TokenParameters[](2); + tokenParameters[0] = TokenParameters({ + token: createTestToken(multisigMembers), + veTokenName: "VE Token 1", + veTokenSymbol: "veTK1" + }); + tokenParameters[1] = TokenParameters({ + token: createTestToken(multisigMembers), + veTokenName: "VE Token 2", + veTokenSymbol: "veTK2" + }); + } + } + + function createTestToken(address[] memory holders) internal returns (address) { + console.log(""); + MockERC20 newToken = new MockERC20(); + + for (uint i = 0; i < holders.length; ) { + newToken.mint(holders[i], 50 ether); + console.log("Minting 50 eth for", holders[i]); + + unchecked { + i++; + } + } + + return address(newToken); + } + + function printDeploymentSummary(GaugesDaoFactory factory) internal view { + DeploymentParameters memory deploymentParameters = factory.getDeploymentParameters(); + Deployment memory deployment = factory.getDeployment(); + + console.log(""); + console.log("Chain ID:", block.chainid); + console.log("Factory:", address(factory)); + console.log(""); + console.log("DAO:", address(deployment.dao)); + console.log(""); + + console.log("Plugins"); + console.log("- Multisig plugin:", address(deployment.multisigPlugin)); + console.log(""); + + for (uint i = 0; i < deployment.gaugeVoterPluginSets.length; ) { + console.log("- Using token:", address(deploymentParameters.tokenParameters[i].token)); + console.log( + " Gauge voter plugin:", + address(deployment.gaugeVoterPluginSets[i].plugin) + ); + console.log(" Curve:", address(deployment.gaugeVoterPluginSets[i].curve)); + console.log(" Exit Queue:", address(deployment.gaugeVoterPluginSets[i].exitQueue)); + console.log( + " Voting Escrow:", + address(deployment.gaugeVoterPluginSets[i].votingEscrow) + ); + console.log(" Clock:", address(deployment.gaugeVoterPluginSets[i].clock)); + console.log(" NFT Lock:", address(deployment.gaugeVoterPluginSets[i].nftLock)); + console.log(""); + + unchecked { + i++; + } + } + + console.log("Plugin repositories"); + console.log( + "- Multisig plugin repository (existing):", + address(deploymentParameters.multisigPluginRepo) + ); + console.log("- Gauge voter plugin repository:", address(deployment.gaugeVoterPluginRepo)); + } +} diff --git a/script/multisig-members.json b/script/multisig-members.json new file mode 100644 index 0000000..3cc928f --- /dev/null +++ b/script/multisig-members.json @@ -0,0 +1,4 @@ +{ + "members": [ + ] +} \ No newline at end of file diff --git a/src/escrow/increasing/Lock.sol b/src/escrow/increasing/Lock.sol new file mode 100644 index 0000000..621b183 --- /dev/null +++ b/src/escrow/increasing/Lock.sol @@ -0,0 +1,121 @@ +/// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ILock} from "@escrow-interfaces/ILock.sol"; +import {ERC721EnumerableUpgradeable as ERC721Enumerable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {DaoAuthorizableUpgradeable as DaoAuthorizable} from "@aragon/osx/core/plugin/dao-authorizable/DaoAuthorizableUpgradeable.sol"; +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; + +/// @title NFT representation of an escrow locking mechanism +contract Lock is ILock, ERC721Enumerable, UUPSUpgradeable, DaoAuthorizable { + /// @dev enables transfers without whitelisting + address public constant WHITELIST_ANY_ADDRESS = + address(uint160(uint256(keccak256("WHITELIST_ANY_ADDRESS")))); + + /// @notice role to upgrade this contract + bytes32 public constant LOCK_ADMIN_ROLE = keccak256("LOCK_ADMIN"); + + /// @notice Address of the escrow contract that holds underyling assets + address public escrow; + + /// @notice Whitelisted contracts that are allowed to transfer + mapping(address => bool) public whitelisted; + + /*////////////////////////////////////////////////////////////// + Modifiers + //////////////////////////////////////////////////////////////*/ + + modifier onlyEscrow() { + if (msg.sender != escrow) revert OnlyEscrow(); + _; + } + + /*////////////////////////////////////////////////////////////// + ERC165 + //////////////////////////////////////////////////////////////*/ + + function supportsInterface( + bytes4 _interfaceId + ) public view override(ERC721Enumerable) returns (bool) { + return super.supportsInterface(_interfaceId); + } + + /*////////////////////////////////////////////////////////////// + Initializer + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + function initialize( + address _escrow, + string memory _name, + string memory _symbol, + address _dao + ) external initializer { + __ERC721_init(_name, _symbol); + __DaoAuthorizableUpgradeable_init(IDAO(_dao)); + escrow = _escrow; + + // allow sending nfts to the escrow + whitelisted[escrow] = true; + emit WhitelistSet(address(this), true); + } + + /*////////////////////////////////////////////////////////////// + Transfers + //////////////////////////////////////////////////////////////*/ + + /// @notice Transfers disabled by default, only whitelisted addresses can receive transfers + function setWhitelisted(address _account, bool _isWhitelisted) external auth(LOCK_ADMIN_ROLE) { + whitelisted[_account] = _isWhitelisted; + emit WhitelistSet(_account, _isWhitelisted); + } + + /// @notice Enable transfers to any address without whitelisting + function enableTransfers() external auth(LOCK_ADMIN_ROLE) { + whitelisted[WHITELIST_ANY_ADDRESS] = true; + emit WhitelistSet(WHITELIST_ANY_ADDRESS, true); + } + + /// @dev Override the transfer to check if the recipient is whitelisted + /// This avoids needing to check for mint/burn but is less idomatic than beforeTokenTransfer + function _transfer(address _from, address _to, uint256 _tokenId) internal override { + if (whitelisted[WHITELIST_ANY_ADDRESS] || whitelisted[_to]) { + super._transfer(_from, _to, _tokenId); + } else revert NotWhitelisted(); + } + + /*////////////////////////////////////////////////////////////// + NFT Functions + //////////////////////////////////////////////////////////////*/ + + function isApprovedOrOwner(address _spender, uint256 _tokenId) external view returns (bool) { + return _isApprovedOrOwner(_spender, _tokenId); + } + + /// @notice Minting and burning functions that can only be called by the escrow contract + function mint(address _to, uint256 _tokenId) external onlyEscrow { + _mint(_to, _tokenId); + } + + /// @notice Minting and burning functions that can only be called by the escrow contract + function burn(uint256 _tokenId) external onlyEscrow { + _burn(_tokenId); + } + + /*////////////////////////////////////////////////////////////// + UUPS Upgrade + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the address of the implementation contract in the [proxy storage slot](https://eips.ethereum.org/EIPS/eip-1967) slot the [UUPS proxy](https://eips.ethereum.org/EIPS/eip-1822) is pointing to. + /// @return The address of the implementation contract. + function implementation() public view returns (address) { + return _getImplementation(); + } + + /// @notice Internal method authorizing the upgrade of the contract via the [upgradeability mechanism for UUPS proxies](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable) (see [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822)). + function _authorizeUpgrade(address) internal virtual override auth(LOCK_ADMIN_ROLE) {} +} diff --git a/src/escrow/increasing/VotingEscrowIncreasing.sol b/src/escrow/increasing/VotingEscrowIncreasing.sol index 96924cc..59369e8 100644 --- a/src/escrow/increasing/VotingEscrowIncreasing.sol +++ b/src/escrow/increasing/VotingEscrowIncreasing.sol @@ -4,16 +4,15 @@ pragma solidity ^0.8.17; // token interfaces import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {IERC20MetadataUpgradeable as IERC20Metadata} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; -import {ERC721Upgradeable as ERC721} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; -import {ERC721EnumerableUpgradeable as ERC721Enumerable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import {IERC721EnumerableMintableBurnable as IERC721EMB} from "./interfaces/IERC721EMB.sol"; // veGovernance import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; import {ISimpleGaugeVoter} from "@voting/ISimpleGaugeVoter.sol"; -import {IClockUser, IClock} from "@clock/IClock.sol"; +import {IClock} from "@clock/IClock.sol"; import {IEscrowCurveIncreasing as IEscrowCurve} from "./interfaces/IEscrowCurveIncreasing.sol"; import {IExitQueue} from "./interfaces/IExitQueue.sol"; -import {IVotingEscrowIncreasing as IVotingEscrow, ILockedBalanceIncreasing, IVotingEscrowCore, IDynamicVoter} from "./interfaces/IVotingEscrowIncreasing.sol"; +import {IVotingEscrowIncreasing as IVotingEscrow} from "./interfaces/IVotingEscrowIncreasing.sol"; // libraries import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; @@ -27,15 +26,12 @@ import {DaoAuthorizableUpgradeable as DaoAuthorizable} from "@aragon/osx/core/pl contract VotingEscrow is IVotingEscrow, - IClockUser, ReentrancyGuard, Pausable, DaoAuthorizable, - ERC721Enumerable, UUPSUpgradeable { using SafeERC20 for IERC20; - using SafeCast for uint256; /// @notice Role required to manage the Escrow curve, this typically will be the DAO bytes32 public constant ESCROW_ADMIN_ROLE = keccak256("ESCROW_ADMIN"); @@ -46,10 +42,6 @@ contract VotingEscrow is /// @notice Role required to withdraw underlying tokens from the contract bytes32 public constant SWEEPER_ROLE = keccak256("SWEEPER"); - /// @dev enables transfers without whitelisting - address public constant WHITELIST_ANY_ADDRESS = - address(uint160(uint256(keccak256("WHITELIST_ANY_ADDRESS")))); - /*////////////////////////////////////////////////////////////// NFT Data //////////////////////////////////////////////////////////////*/ @@ -60,6 +52,9 @@ contract VotingEscrow is /// @notice Total supply of underlying tokens deposited in the contract uint256 public totalLocked; + /// @dev tracks the locked balance of each NFT + mapping(uint256 => LockedBalance) private _locked; + /*////////////////////////////////////////////////////////////// Helper Contracts //////////////////////////////////////////////////////////////*/ @@ -80,25 +75,8 @@ contract VotingEscrow is /// @notice Address of the clock contract that manages epoch and voting periods address public clock; - /*////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /// @notice Whitelisted contracts that are allowed to transfer - mapping(address => bool) public whitelisted; - - /// @dev tracks the locked balance of each NFT - mapping(uint256 => LockedBalance) internal _locked; - - /*////////////////////////////////////////////////////////////// - ERC165 - //////////////////////////////////////////////////////////////*/ - - function supportsInterface( - bytes4 _interfaceId - ) public view override(ERC721Enumerable) returns (bool) { - return super.supportsInterface(_interfaceId); - } + /// @notice Address of the NFT contract that is the lock + address public lockNFT; /*////////////////////////////////////////////////////////////// Initialization @@ -108,45 +86,20 @@ contract VotingEscrow is _disableInitializers(); } - function initialize( - address _token, - address _dao, - string memory _name, - string memory _symbol, - address _clock - ) external initializer { + function initialize(address _token, address _dao, address _clock) external initializer { __DaoAuthorizableUpgradeable_init(IDAO(_dao)); __ReentrancyGuard_init(); __Pausable_init(); - __ERC721_init(_name, _symbol); if (IERC20Metadata(_token).decimals() != 18) revert MustBe18Decimals(); token = _token; clock = _clock; - - // allow sending tokens to this contract - whitelisted[address(this)] = true; - emit WhitelistSet(address(this), true); } /*////////////////////////////////////////////////////////////// Admin Setters //////////////////////////////////////////////////////////////*/ - /// @notice Transfers disabled by default, only whitelisted addresses can receive transfers - function setWhitelisted( - address _account, - bool _isWhitelisted - ) external auth(ESCROW_ADMIN_ROLE) { - whitelisted[_account] = _isWhitelisted; - emit WhitelistSet(_account, _isWhitelisted); - } - - function enableTransfers() external auth(ESCROW_ADMIN_ROLE) { - whitelisted[WHITELIST_ANY_ADDRESS] = true; - emit WhitelistSet(WHITELIST_ANY_ADDRESS, true); - } - /// @notice Sets the curve contract that calculates the voting power function setCurve(address _curve) external auth(ESCROW_ADMIN_ROLE) { curve = _curve; @@ -167,6 +120,10 @@ contract VotingEscrow is clock = _clock; } + function setLockNFT(address _nft) external auth(ESCROW_ADMIN_ROLE) { + lockNFT = _nft; + } + function pause() external auth(PAUSER_ROLE) { _pause(); } @@ -180,17 +137,18 @@ contract VotingEscrow is //////////////////////////////////////////////////////////////*/ function isApprovedOrOwner(address _spender, uint256 _tokenId) external view returns (bool) { - return _isApprovedOrOwner(_spender, _tokenId); + return IERC721EMB(lockNFT).isApprovedOrOwner(_spender, _tokenId); } /// @notice Fetch all NFTs owned by an address by leveraging the ERC721Enumerable interface /// @param _owner Address to query /// @return tokenIds Array of token IDs owned by the address function ownedTokens(address _owner) public view returns (uint256[] memory tokenIds) { - uint256 balance = balanceOf(_owner); + IERC721EMB enumerable = IERC721EMB(lockNFT); + uint256 balance = enumerable.balanceOf(_owner); uint256[] memory tokens = new uint256[](balance); for (uint256 i = 0; i < balance; i++) { - tokens[i] = tokenOfOwnerByIndex(_owner, i); + tokens[i] = enumerable.tokenOfOwnerByIndex(_owner, i); } return tokens; } @@ -244,18 +202,6 @@ contract VotingEscrow is return ISimpleGaugeVoter(voter).isVoting(_tokenId); } - /*////////////////////////////////////////////////////////////// - ERC721 LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @dev Override the transfer to check if the recipient is whitelisted - /// This avoids needing to check for mint/burn but is less idomatic than beforeTokenTransfer - function _transfer(address _from, address _to, uint256 _tokenId) internal override { - if (whitelisted[WHITELIST_ANY_ADDRESS] || whitelisted[_to]) { - super._transfer(_from, _to, _tokenId); - } else revert NotWhitelisted(); - } - /*////////////////////////////////////////////////////////////// ESCROW LOGIC //////////////////////////////////////////////////////////////*/ @@ -283,7 +229,7 @@ contract VotingEscrow is // increment the total locked supply and get the new tokenId totalLocked += _value; - uint256 newTokenId = totalSupply() + 1; + uint256 newTokenId = IERC721EMB(lockNFT).totalSupply() + 1; // write the lock and checkpoint the voting power LockedBalance memory lock = LockedBalance(_value, startTime); @@ -296,7 +242,7 @@ contract VotingEscrow is IERC20(token).safeTransferFrom(_msgSender(), address(this), _value); // mint the NFT to complete the deposit - _mint(_to, newTokenId); + IERC721EMB(lockNFT).mint(_to, newTokenId); emit Deposit(_to, newTokenId, startTime, _value, totalLocked); return newTokenId; @@ -339,13 +285,13 @@ contract VotingEscrow is function beginWithdrawal(uint256 _tokenId) public nonReentrant whenNotPaused { // can't exit if you have votes pending if (isVoting(_tokenId)) revert CannotExit(); - address owner = _ownerOf(_tokenId); + address owner = IERC721EMB(lockNFT).ownerOf(_tokenId); // we can remove the user's voting power as it's no longer locked _checkpointClear(_tokenId); - // transfer NFT to the queue and queue the exit - _transfer(_msgSender(), address(this), _tokenId); + // transfer NFT to this and queue the exit + IERC721EMB(lockNFT).transferFrom(_msgSender(), address(this), _tokenId); IExitQueue(queue).queueExit(_tokenId, owner); } @@ -374,7 +320,7 @@ contract VotingEscrow is totalLocked -= value; // Burn the NFT and transfer the tokens to the user - _burn(_tokenId); + IERC721EMB(lockNFT).burn(_tokenId); IERC20(token).safeTransfer(sender, value - fee); emit Withdraw(sender, _tokenId, value - fee, block.timestamp, totalLocked); diff --git a/src/escrow/increasing/interfaces/IERC721EMB.sol b/src/escrow/increasing/interfaces/IERC721EMB.sol new file mode 100644 index 0000000..8f0ba2e --- /dev/null +++ b/src/escrow/increasing/interfaces/IERC721EMB.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; + +interface IERC721EnumerableMintableBurnable is IERC721Enumerable { + function mint(address to, uint256 tokenId) external; + + function burn(uint256 tokenId) external; + + function isApprovedOrOwner(address spender, uint256 tokenId) external view returns (bool); +} diff --git a/src/escrow/increasing/interfaces/ILock.sol b/src/escrow/increasing/interfaces/ILock.sol new file mode 100644 index 0000000..695f821 --- /dev/null +++ b/src/escrow/increasing/interfaces/ILock.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/*/////////////////////////////////////////////////////////////// + WHITELIST +//////////////////////////////////////////////////////////////*/ +interface IWhitelistEvents { + event WhitelistSet(address indexed account, bool status); +} + +interface IWhitelistErrors { + error NotWhitelisted(); +} + +interface IWhitelist is IWhitelistEvents, IWhitelistErrors { + /// @notice Set whitelist status for an address + function setWhitelisted(address addr, bool isWhitelisted) external; + + /// @notice Check if an address is whitelisted + function whitelisted(address addr) external view returns (bool); +} + +interface ILock is IWhitelist { + error OnlyEscrow(); + + /// @notice Address of the escrow contract that holds underyling assets + function escrow() external view returns (address); +} diff --git a/src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol b/src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol index b2a0678..d2ee260 100644 --- a/src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol +++ b/src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol @@ -1,11 +1,6 @@ /// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IERC165, IERC721, IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; -import {IERC6372} from "@openzeppelin/contracts/interfaces/IERC6372.sol"; -import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import {IVotes} from "./IVotes.sol"; - /*/////////////////////////////////////////////////////////////// CORE FUNCTIONALITY //////////////////////////////////////////////////////////////*/ @@ -54,6 +49,9 @@ interface IVotingEscrowCore is /// @notice Address of the underying ERC20 token. function token() external view returns (address); + /// @notice Address of the lock receipt NFT. + function lockNFT() external view returns (address); + /// @notice Total underlying tokens deposited in the contract function totalLocked() external view returns (uint256); @@ -78,25 +76,6 @@ interface IVotingEscrowCore is function isApprovedOrOwner(address spender, uint256 tokenId) external view returns (bool); } -/*/////////////////////////////////////////////////////////////// - WHITELIST ESCROW -//////////////////////////////////////////////////////////////*/ -interface IWhitelistEvents { - event WhitelistSet(address indexed account, bool status); -} - -interface IWhitelistErrors { - error NotWhitelisted(); -} - -interface IWhitelist is IWhitelistEvents, IWhitelistErrors { - /// @notice Set whitelist status for an address - function setWhitelisted(address addr, bool isWhitelisted) external; - - /// @notice Check if an address is whitelisted - function whitelisted(address addr) external view returns (bool); -} - /*/////////////////////////////////////////////////////////////// WITHDRAWAL QUEUE //////////////////////////////////////////////////////////////*/ @@ -191,13 +170,7 @@ interface IDynamicVoter is IDynamicVoterErrors { INCREASED ESCROW //////////////////////////////////////////////////////////////*/ -interface IVotingEscrowIncreasing is - IVotingEscrowCore, - IDynamicVoter, - IWithdrawalQueue, - IWhitelist, - ISweeper -{ +interface IVotingEscrowIncreasing is IVotingEscrowCore, IDynamicVoter, IWithdrawalQueue, ISweeper { } @@ -205,11 +178,9 @@ interface IVotingEscrowIncreasing is interface IVotingEscrowEventsStorageErrorsEvents is IVotingEscrowCoreErrors, IVotingEscrowCoreEvents, - IWhitelistEvents, IWithdrawalQueueErrors, IWithdrawalQueueEvents, ILockedBalanceIncreasing, - IWhitelistErrors, ISweeperEvents, ISweeperErrors { diff --git a/src/factory/GaugesDaoFactory.sol b/src/factory/GaugesDaoFactory.sol new file mode 100644 index 0000000..f86df67 --- /dev/null +++ b/src/factory/GaugesDaoFactory.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {DAOFactory} from "@aragon/osx/framework/dao/DAOFactory.sol"; +import {IEscrowCurveUserStorage} from "@escrow-interfaces/IEscrowCurveIncreasing.sol"; +import {IWithdrawalQueueErrors} from "src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol"; +import {IGaugeVote} from "src/voting/ISimpleGaugeVoter.sol"; +import {VotingEscrow, Clock, Lock, QuadraticIncreasingEscrow, ExitQueue, SimpleGaugeVoter, SimpleGaugeVoterSetup, ISimpleGaugeVoterSetupParams} from "src/voting/SimpleGaugeVoterSetup.sol"; +import {PluginSetupProcessor} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol"; +import {hashHelpers, PluginSetupRef} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessorHelpers.sol"; +import {PluginRepoFactory} from "@aragon/osx/framework/plugin/repo/PluginRepoFactory.sol"; +import {PluginRepo} from "@aragon/osx/framework/plugin/repo/PluginRepo.sol"; +import {IPluginSetup} from "@aragon/osx/framework/plugin/setup/IPluginSetup.sol"; +import {Multisig} from "@aragon/osx/plugins/governance/multisig/Multisig.sol"; +import {MultisigSetup as MultisigPluginSetup} from "@aragon/osx/plugins/governance/multisig/MultisigSetup.sol"; +import {createERC1967Proxy} from "@aragon/osx/utils/Proxy.sol"; +import {PermissionLib} from "@aragon/osx/core/permission/PermissionLib.sol"; + +/// @notice The struct containing all the parameters to deploy the DAO +/// @param minApprovals The amount of approvals required for the multisig to be able to execute a proposal on the DAO +/// @param multisigMembers The list of addresses to be defined as the initial multisig signers +/// @param tokenParameters A list with the tokens and metadata for which a plugin and a VE should be deployed +/// @param feePercent The fee taken on withdrawals (1 ether = 100%) +/// @param warmupPeriod Delay in seconds after depositing before voting becomes possible +/// @param cooldownPeriod Delay seconds after queuing an exit before withdrawing becomes possible +/// @param minLockDuration Min seconds a user must have locked in escrow before they can queue an exit +/// @param votingPaused Prevent voting until manually activated by the multisig +/// @param multisigPluginRepo Address of Aragon's multisig plugin repository on the given network +/// @param multisigPluginRelease The release of the multisig plugin to target +/// @param multisigPluginBuild The build of the multisig plugin to target +/// @param voterPluginSetup The address of the Gauges Voter plugin setup contract to create a repository with +/// @param voterEnsSubdomain The ENS subdomain under which the plugin reposiroty will be created +/// @param osxDaoFactory The address of the OSx DAO factory contract, used to retrieve the DAO implementation address +/// @param pluginSetupProcessor The address of the OSx PluginSetupProcessor contract on the target chain +/// @param pluginRepoFactory The address of the OSx PluginRepoFactory contract on the target chain +struct DeploymentParameters { + // Multisig settings + uint16 minApprovals; + address[] multisigMembers; + // Gauge Voter + TokenParameters[] tokenParameters; + uint256 feePercent; + uint64 warmupPeriod; + uint64 cooldownPeriod; + uint64 minLockDuration; + bool votingPaused; + // Voter plugin setup and ENS + PluginRepo multisigPluginRepo; + uint8 multisigPluginRelease; + uint16 multisigPluginBuild; + SimpleGaugeVoterSetup voterPluginSetup; + string voterEnsSubdomain; + // OSx addresses + address osxDaoFactory; + PluginSetupProcessor pluginSetupProcessor; + PluginRepoFactory pluginRepoFactory; +} + +struct TokenParameters { + address token; + string veTokenName; + string veTokenSymbol; +} + +/// @notice Struct containing the plugin and all of its helpers +struct GaugePluginSet { + SimpleGaugeVoter plugin; + QuadraticIncreasingEscrow curve; + ExitQueue exitQueue; + VotingEscrow votingEscrow; + Clock clock; + Lock nftLock; +} + +/// @notice Contains the artifacts that resulted from running a deployment +struct Deployment { + DAO dao; + // Plugins + Multisig multisigPlugin; + GaugePluginSet[] gaugeVoterPluginSets; + // Plugin repo's + PluginRepo gaugeVoterPluginRepo; +} + +/// @notice A singleton contract designed to run the deployment once and become a read-only store of the contracts deployed +contract GaugesDaoFactory { + /// @notice Thrown when attempting to call deployOnce() when the DAO is already deployed. + error AlreadyDeployed(); + + DeploymentParameters parameters; + Deployment deployment; + + /// @notice Initializes the factory and performs the full deployment. Values become read-only after that. + /// @param _parameters The parameters of the one-time deployment. + constructor(DeploymentParameters memory _parameters) { + parameters.minApprovals = _parameters.minApprovals; + parameters.multisigMembers = _parameters.multisigMembers; + + for (uint i = 0; i < _parameters.tokenParameters.length; ) { + parameters.tokenParameters.push(_parameters.tokenParameters[i]); + + unchecked { + i++; + } + } + + parameters.feePercent = _parameters.feePercent; + parameters.warmupPeriod = _parameters.warmupPeriod; + parameters.cooldownPeriod = _parameters.cooldownPeriod; + parameters.minLockDuration = _parameters.minLockDuration; + parameters.votingPaused = _parameters.votingPaused; + parameters.multisigPluginRepo = _parameters.multisigPluginRepo; + parameters.multisigPluginRelease = _parameters.multisigPluginRelease; + parameters.multisigPluginBuild = _parameters.multisigPluginBuild; + parameters.voterPluginSetup = _parameters.voterPluginSetup; + parameters.voterEnsSubdomain = _parameters.voterEnsSubdomain; + parameters.osxDaoFactory = _parameters.osxDaoFactory; + parameters.pluginSetupProcessor = _parameters.pluginSetupProcessor; + parameters.pluginRepoFactory = _parameters.pluginRepoFactory; + } + + /// @notice Run the deployment and store the artifacts in a read-only store that can be retrieved via `getDeployment()` and `getDeploymentParameters()` + function deployOnce() public { + if (address(deployment.dao) != address(0)) revert AlreadyDeployed(); + + // Deploy the DAO (this contract is the interim owner) + DAO dao = prepareDao(); + deployment.dao = dao; + + // Deploy and install the plugins + + grantApplyInstallationPermissions(dao); + + // MULTISIG + { + IPluginSetup.PreparedSetupData memory preparedMultisigSetupData; + + PluginRepo.Tag memory repoTag = PluginRepo.Tag( + parameters.multisigPluginRelease, + parameters.multisigPluginBuild + ); + + (deployment.multisigPlugin, preparedMultisigSetupData) = prepareMultisig(dao, repoTag); + + applyPluginInstallation( + dao, + address(deployment.multisigPlugin), + parameters.multisigPluginRepo, + repoTag, + preparedMultisigSetupData + ); + } + + // GAUGE VOTER(s) + { + IPluginSetup.PreparedSetupData memory preparedVoterSetupData; + + PluginRepo.Tag memory repoTag = PluginRepo.Tag(1, 1); + GaugePluginSet memory pluginSet; + + PluginRepo pluginRepo = prepareSimpleGaugeVoterPluginRepo(dao); + + for (uint i = 0; i < parameters.tokenParameters.length; ) { + ( + pluginSet, + deployment.gaugeVoterPluginRepo, + preparedVoterSetupData + ) = prepareSimpleGaugeVoterPlugin( + dao, + parameters.tokenParameters[i], + pluginRepo, + repoTag + ); + + deployment.gaugeVoterPluginSets.push(pluginSet); + + applyPluginInstallation( + dao, + address(pluginSet.plugin), + deployment.gaugeVoterPluginRepo, + repoTag, + preparedVoterSetupData + ); + + activateSimpleGaugeVoterInstallation(dao, pluginSet); + + unchecked { + i++; + } + } + } + + // Clean up + revokeApplyInstallationPermissions(dao); + + // Remove this contract as owner + revokeOwnerPermission(deployment.dao); + } + + function prepareDao() internal returns (DAO dao) { + address daoBase = DAOFactory(parameters.osxDaoFactory).daoBase(); + + dao = DAO( + payable( + createERC1967Proxy( + address(daoBase), + abi.encodeCall( + DAO.initialize, + ( + "", // Metadata URI + address(this), // initialOwner + address(0x0), // Trusted forwarder + "" // DAO URI + ) + ) + ) + ) + ); + + // Grant DAO all the needed permissions on itself + PermissionLib.SingleTargetPermission[] + memory items = new PermissionLib.SingleTargetPermission[](3); + items[0] = PermissionLib.SingleTargetPermission( + PermissionLib.Operation.Grant, + address(dao), + dao.ROOT_PERMISSION_ID() + ); + items[1] = PermissionLib.SingleTargetPermission( + PermissionLib.Operation.Grant, + address(dao), + dao.UPGRADE_DAO_PERMISSION_ID() + ); + items[2] = PermissionLib.SingleTargetPermission( + PermissionLib.Operation.Grant, + address(dao), + dao.REGISTER_STANDARD_CALLBACK_PERMISSION_ID() + ); + + dao.applySingleTargetPermissions(address(dao), items); + } + + function prepareMultisig( + DAO dao, + PluginRepo.Tag memory repoTag + ) internal returns (Multisig, IPluginSetup.PreparedSetupData memory) { + bytes memory settingsData = abi.encode( + parameters.multisigMembers, + Multisig.MultisigSettings( + true, // onlyListed + parameters.minApprovals + ) + ); + + (address plugin, IPluginSetup.PreparedSetupData memory preparedSetupData) = parameters + .pluginSetupProcessor + .prepareInstallation( + address(dao), + PluginSetupProcessor.PrepareInstallationParams( + PluginSetupRef(repoTag, parameters.multisigPluginRepo), + settingsData + ) + ); + + return (Multisig(plugin), preparedSetupData); + } + + function prepareSimpleGaugeVoterPluginRepo(DAO dao) internal returns (PluginRepo pluginRepo) { + // Publish repo + pluginRepo = PluginRepoFactory(parameters.pluginRepoFactory) + .createPluginRepoWithFirstVersion( + parameters.voterEnsSubdomain, + address(parameters.voterPluginSetup), + address(dao), + " ", + " " + ); + } + + function prepareSimpleGaugeVoterPlugin( + DAO dao, + TokenParameters memory tokenParameters, + PluginRepo pluginRepo, + PluginRepo.Tag memory repoTag + ) internal returns (GaugePluginSet memory, PluginRepo, IPluginSetup.PreparedSetupData memory) { + // Plugin settings + bytes memory settingsData = parameters.voterPluginSetup.encodeSetupData( + ISimpleGaugeVoterSetupParams({ + isPaused: parameters.votingPaused, + token: tokenParameters.token, + veTokenName: tokenParameters.veTokenName, + veTokenSymbol: tokenParameters.veTokenSymbol, + feePercent: parameters.feePercent, + warmup: parameters.warmupPeriod, + cooldown: parameters.cooldownPeriod, + minLock: parameters.minLockDuration + }) + ); + + (address plugin, IPluginSetup.PreparedSetupData memory preparedSetupData) = parameters + .pluginSetupProcessor + .prepareInstallation( + address(dao), + PluginSetupProcessor.PrepareInstallationParams( + PluginSetupRef(repoTag, pluginRepo), + settingsData + ) + ); + + address[] memory helpers = preparedSetupData.helpers; + GaugePluginSet memory pluginSet = GaugePluginSet({ + plugin: SimpleGaugeVoter(plugin), + curve: QuadraticIncreasingEscrow(helpers[0]), + exitQueue: ExitQueue(helpers[1]), + votingEscrow: VotingEscrow(helpers[2]), + clock: Clock(helpers[3]), + nftLock: Lock(helpers[4]) + }); + + return (pluginSet, pluginRepo, preparedSetupData); + } + + function applyPluginInstallation( + DAO dao, + address plugin, + PluginRepo pluginRepo, + PluginRepo.Tag memory pluginRepoTag, + IPluginSetup.PreparedSetupData memory preparedSetupData + ) internal { + parameters.pluginSetupProcessor.applyInstallation( + address(dao), + PluginSetupProcessor.ApplyInstallationParams( + PluginSetupRef(pluginRepoTag, pluginRepo), + plugin, + preparedSetupData.permissions, + hashHelpers(preparedSetupData.helpers) + ) + ); + } + + function activateSimpleGaugeVoterInstallation( + DAO dao, + GaugePluginSet memory pluginSet + ) internal { + dao.grant( + address(pluginSet.votingEscrow), + address(this), + pluginSet.votingEscrow.ESCROW_ADMIN_ROLE() + ); + + pluginSet.votingEscrow.setCurve(address(pluginSet.curve)); + pluginSet.votingEscrow.setQueue(address(pluginSet.exitQueue)); + pluginSet.votingEscrow.setVoter(address(pluginSet.plugin)); + pluginSet.votingEscrow.setLockNFT(address(pluginSet.nftLock)); + + dao.revoke( + address(pluginSet.votingEscrow), + address(this), + pluginSet.votingEscrow.ESCROW_ADMIN_ROLE() + ); + } + + function grantApplyInstallationPermissions(DAO dao) internal { + // The PSP can manage permissions on the new DAO + dao.grant(address(dao), address(parameters.pluginSetupProcessor), dao.ROOT_PERMISSION_ID()); + + // This factory can call applyInstallation() on the PSP + dao.grant( + address(parameters.pluginSetupProcessor), + address(this), + parameters.pluginSetupProcessor.APPLY_INSTALLATION_PERMISSION_ID() + ); + } + + function revokeApplyInstallationPermissions(DAO dao) internal { + // Revoking the permission for the factory to call applyInstallation() on the PSP + dao.revoke( + address(parameters.pluginSetupProcessor), + address(this), + parameters.pluginSetupProcessor.APPLY_INSTALLATION_PERMISSION_ID() + ); + + // Revoke the PSP permission to manage permissions on the new DAO + dao.revoke( + address(dao), + address(parameters.pluginSetupProcessor), + dao.ROOT_PERMISSION_ID() + ); + } + + function revokeOwnerPermission(DAO dao) internal { + dao.revoke(address(dao), address(this), dao.ROOT_PERMISSION_ID()); + } + + // Getters + + function getDeploymentParameters() public view returns (DeploymentParameters memory) { + return parameters; + } + + function getDeployment() public view returns (Deployment memory) { + return deployment; + } +} diff --git a/src/voting/SimpleGaugeVoterSetup.sol b/src/voting/SimpleGaugeVoterSetup.sol index 9cc6270..39b630c 100644 --- a/src/voting/SimpleGaugeVoterSetup.sol +++ b/src/voting/SimpleGaugeVoterSetup.sol @@ -13,11 +13,13 @@ import {ProxyLib} from "@libs/ProxyLib.sol"; import {PermissionLib} from "@aragon/osx/core/permission/PermissionLib.sol"; import {PluginSetup} from "@aragon/osx/framework/plugin/setup/PluginSetup.sol"; +// these should be interfaces import {SimpleGaugeVoter} from "@voting/SimpleGaugeVoter.sol"; import {VotingEscrow} from "@escrow/VotingEscrowIncreasing.sol"; import {ExitQueue} from "@escrow/ExitQueue.sol"; import {QuadraticIncreasingEscrow} from "@escrow/QuadraticIncreasingEscrow.sol"; import {Clock} from "@clock/Clock.sol"; +import {Lock} from "@escrow/Lock.sol"; /// @param isPaused Whether the voter contract is deployed in a paused state /// @param veTokenName The name of the voting escrow token @@ -28,9 +30,10 @@ import {Clock} from "@clock/Clock.sol"; struct ISimpleGaugeVoterSetupParams { // voter bool isPaused; - // escrow + // escrow - NFT string veTokenName; string veTokenSymbol; + // escrow - main address token; // queue uint256 cooldown; @@ -68,19 +71,24 @@ contract SimpleGaugeVoterSetup is PluginSetup { /// @dev implementation of the clock address clockBase; + /// @dev implementation of the escrow NFT + address nftBase; + /// @notice Deploys the setup by binding the implementation contracts required during installation. constructor( address _voterBase, address _curveBase, address _queueBase, address _escrowBase, - address _clockBase + address _clockBase, + address _nftBase ) PluginSetup() { voterBase = _voterBase; curveBase = _curveBase; queueBase = _queueBase; escrowBase = _escrowBase; clockBase = _clockBase; + nftBase = _nftBase; } function implementation() external view returns (address) { @@ -108,10 +116,7 @@ contract SimpleGaugeVoterSetup is PluginSetup { // deploy the escrow locker VotingEscrow escrow = VotingEscrow( escrowBase.deployUUPSProxy( - abi.encodeCall( - VotingEscrow.initialize, - (params.token, _dao, params.veTokenName, params.veTokenSymbol, clock) - ) + abi.encodeCall(VotingEscrow.initialize, (params.token, _dao, clock)) ) ); @@ -142,6 +147,14 @@ contract SimpleGaugeVoterSetup is PluginSetup { ) ); + // deploy the escrow NFT + address nftLock = nftBase.deployUUPSProxy( + abi.encodeCall( + Lock.initialize, + (address(escrow), params.veTokenName, params.veTokenSymbol, _dao) + ) + ); + // encode our setup data with permissions and helpers PermissionLib.MultiTargetPermission[] memory permissions = getPermissions( _dao, @@ -150,15 +163,17 @@ contract SimpleGaugeVoterSetup is PluginSetup { exitQueue, address(escrow), clock, + nftLock, PermissionLib.Operation.Grant ); - address[] memory helpers = new address[](4); + address[] memory helpers = new address[](5); helpers[0] = curve; helpers[1] = exitQueue; helpers[2] = address(escrow); helpers[3] = clock; + helpers[4] = address(nftLock); preparedSetupData.helpers = helpers; preparedSetupData.permissions = permissions; @@ -170,7 +185,7 @@ contract SimpleGaugeVoterSetup is PluginSetup { SetupPayload calldata _payload ) external view returns (PermissionLib.MultiTargetPermission[] memory permissions) { // check the helpers length - if (_payload.currentHelpers.length != 4) { + if (_payload.currentHelpers.length != 5) { revert WrongHelpersArrayLength(_payload.currentHelpers.length); } @@ -178,6 +193,7 @@ contract SimpleGaugeVoterSetup is PluginSetup { address queue = _payload.currentHelpers[1]; address escrow = _payload.currentHelpers[2]; address clock = _payload.currentHelpers[3]; + address nftLock = _payload.currentHelpers[4]; permissions = getPermissions( _dao, @@ -186,6 +202,7 @@ contract SimpleGaugeVoterSetup is PluginSetup { queue, escrow, clock, + nftLock, PermissionLib.Operation.Revoke ); } @@ -201,10 +218,11 @@ contract SimpleGaugeVoterSetup is PluginSetup { address _queue, address _escrow, address _clock, + address _nft, PermissionLib.Operation _grantOrRevoke ) public view returns (PermissionLib.MultiTargetPermission[] memory) { PermissionLib.MultiTargetPermission[] - memory permissions = new PermissionLib.MultiTargetPermission[](6); + memory permissions = new PermissionLib.MultiTargetPermission[](7); permissions[0] = PermissionLib.MultiTargetPermission({ permissionId: SimpleGaugeVoter(_plugin).GAUGE_ADMIN_ROLE(), @@ -254,6 +272,14 @@ contract SimpleGaugeVoterSetup is PluginSetup { condition: PermissionLib.NO_CONDITION }); + permissions[6] = PermissionLib.MultiTargetPermission({ + permissionId: Lock(_nft).LOCK_ADMIN_ROLE(), + where: _nft, + who: _dao, + operation: _grantOrRevoke, + condition: PermissionLib.NO_CONDITION + }); + return permissions; } diff --git a/test/e2e.t.sol b/test/e2e.t.sol index e296079..19aa123 100644 --- a/test/e2e.t.sol +++ b/test/e2e.t.sol @@ -18,7 +18,7 @@ import {Clock} from "@clock/Clock.sol"; import {IEscrowCurveUserStorage} from "@escrow-interfaces/IEscrowCurveIncreasing.sol"; import {IWithdrawalQueueErrors} from "src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol"; import {IGaugeVote} from "src/voting/ISimpleGaugeVoter.sol"; -import {VotingEscrow, QuadraticIncreasingEscrow, ExitQueue, SimpleGaugeVoter, SimpleGaugeVoterSetup, ISimpleGaugeVoterSetupParams} from "src/voting/SimpleGaugeVoterSetup.sol"; +import {VotingEscrow, Lock, QuadraticIncreasingEscrow, ExitQueue, SimpleGaugeVoter, SimpleGaugeVoterSetup, ISimpleGaugeVoterSetupParams} from "src/voting/SimpleGaugeVoterSetup.sol"; /** * This is going to be a simple E2E test that will build the contracts on Aragon and run a deposit / withdraw flow. @@ -47,6 +47,7 @@ contract TestE2E is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveUserSt MockERC20 token; VotingEscrow ve; + Lock nftLock; QuadraticIncreasingEscrow curve; SimpleGaugeVoter voter; ExitQueue queue; @@ -117,6 +118,9 @@ contract TestE2E is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveUserSt // reset votes to clear voter.reset(tokenId); + // approve the withdrawal + nftLock.approve(address(ve), tokenId); + // can't queue because the min lock is not reached uint256 minLock = queue.timeToMinLock(tokenId); uint256 expectedTime = 20 weeks - depositTime; @@ -137,8 +141,8 @@ contract TestE2E is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveUserSt ve.beginWithdrawal(tokenId); // enter the queue - assertEq(ve.balanceOf(user), 0, "User should have no tokens"); - assertEq(ve.balanceOf(address(ve)), 1, "VE should have the NFT"); + assertEq(nftLock.balanceOf(user), 0, "User should have no tokens"); + assertEq(nftLock.balanceOf(address(ve)), 1, "VE should have the NFT"); // readjust the cached time atm = block.timestamp; @@ -161,8 +165,8 @@ contract TestE2E is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveUserSt vm.warp(atm + 1 weeks); ve.withdraw(tokenId); - assertEq(ve.balanceOf(user), 0, "User not should have the token"); - assertEq(ve.balanceOf(address(ve)), 0, "VE should not have the NFT"); + assertEq(nftLock.balanceOf(user), 0, "User not should have the token"); + assertEq(nftLock.balanceOf(address(ve)), 0, "VE should not have the NFT"); assertEq(token.balanceOf(user), DEPOSIT, "User should have the tokens"); } vm.stopPrank(); @@ -266,8 +270,8 @@ contract TestE2E is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveUserSt // check the user owns the nft assertEq(tokenId, 1, "Token ID should be 1"); - assertEq(ve.balanceOf(user), 1, "User should have 1 token"); - assertEq(ve.ownerOf(tokenId), user, "User should own the token"); + assertEq(nftLock.balanceOf(user), 1, "User should have 1 token"); + assertEq(nftLock.ownerOf(tokenId), user, "User should own the token"); assertEq(token.balanceOf(address(ve)), DEPOSIT, "VE should have the tokens"); assertEq(token.balanceOf(user), 0, "User should have no tokens"); } @@ -352,7 +356,8 @@ contract TestE2E is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveUserSt address(new QuadraticIncreasingEscrow()), address(new ExitQueue()), address(new VotingEscrow()), - address(new Clock()) + address(new Clock()), + address(new Lock()) ); // push to the PSP @@ -381,6 +386,7 @@ contract TestE2E is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveUserSt queue = ExitQueue(helpers[1]); ve = VotingEscrow(helpers[2]); clock = Clock(helpers[3]); + nftLock = Lock(helpers[4]); // set the permissions for (uint i = 0; i < preparedSetupData.permissions.length; i++) { @@ -389,7 +395,7 @@ contract TestE2E is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveUserSt } function _actions() internal view returns (IDAO.Action[] memory) { - IDAO.Action[] memory actions = new IDAO.Action[](4); + IDAO.Action[] memory actions = new IDAO.Action[](5); // action 0: apply the ve installation actions[0] = IDAO.Action({ @@ -422,6 +428,12 @@ contract TestE2E is Test, IWithdrawalQueueErrors, IGaugeVote, IEscrowCurveUserSt data: abi.encodeWithSelector(ve.setVoter.selector, address(voter)) }); + actions[4] = IDAO.Action({ + to: address(ve), + value: 0, + data: abi.encodeWithSelector(ve.setLockNFT.selector, address(nftLock)) + }); + return wrapGrantRevokeRoot(DAO(payable(address(dao))), address(psp), actions); } diff --git a/test/escrow/curve/QuadraticCurveBase.t.sol b/test/escrow/curve/QuadraticCurveBase.t.sol index dda1211..6f8a1d8 100644 --- a/test/escrow/curve/QuadraticCurveBase.t.sol +++ b/test/escrow/curve/QuadraticCurveBase.t.sol @@ -9,6 +9,7 @@ import {DAO, createTestDAO} from "@mocks/MockDAO.sol"; import {QuadraticIncreasingEscrow, IVotingEscrow, IEscrowCurve} from "src/escrow/increasing/QuadraticIncreasingEscrow.sol"; import {Clock} from "@clock/Clock.sol"; import {IVotingEscrowIncreasing, ILockedBalanceIncreasing} from "src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol"; + import {ProxyLib} from "@libs/ProxyLib.sol"; contract MockEscrow { diff --git a/test/escrow/curve/QuadraticCurveLogic.t.sol b/test/escrow/curve/QuadraticCurveLogic.t.sol index 844cb6e..e4d6fd1 100644 --- a/test/escrow/curve/QuadraticCurveLogic.t.sol +++ b/test/escrow/curve/QuadraticCurveLogic.t.sol @@ -4,20 +4,12 @@ import {console2 as console} from "forge-std/console2.sol"; import {QuadraticIncreasingEscrow, IVotingEscrow, IEscrowCurve} from "src/escrow/increasing/QuadraticIncreasingEscrow.sol"; import {IVotingEscrowIncreasing, ILockedBalanceIncreasing} from "src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol"; + import {QuadraticCurveBase, MockEscrow} from "./QuadraticCurveBase.t.sol"; contract TestQuadraticIncreasingCurve is QuadraticCurveBase { address attacker = address(0x1); - // check that our constants are initialized correctly - // check the escrow is set - function testEscrowInitializesCorrectly() public { - // MockEscrow escrow = new MockEscrow(); - // QuadraticIncreasingEscrow curve_ = new QuadraticIncreasingEscrow(); - // curve_.initialize(address(escrow), address(dao)); - // assertEq(address(curve_.escrow()), address(escrow)); - } - function testUUPSUpgrade() public { address newImpl = address(new QuadraticIncreasingEscrow()); curve.upgradeTo(newImpl); @@ -28,29 +20,4 @@ contract TestQuadraticIncreasingCurve is QuadraticCurveBase { vm.expectRevert(err); curve.upgradeTo(newImpl); } - - // validate multiple checkpoint situation - // validate the bias bounding works - // warmup: TODO - how do we ensure the warmup doesn't add to an epoch that snaps - // in the future - // warmup: variable warmup perid (create a setter) - // warmup: empty warmup period returns fase - // supplyAt reverts - // same block checkpointing overwrite user point history - // updating checkpoint with a lower balance - // updating checkpoint with a higher balance - // updating with the same balance - // only the escrow can call checkpoint - // point index with large number of points - // - if userepoch 0 return 0 - // - if latest user epoch before ts, return the latest user epoch - // - implicit zero balance - // understand at what boundary the curve starts to break down by doing a very small and very large - // deposit - // test the bound bias caps at the boundary - // test that the cooldown correcty calculates - // test a checkpoint correctly saves the user point - // test that the cooldown is respected for the NFT balance - // test the fetched NFT balance from a point in timeFirst - // TODO: check aero tests for other ideas } diff --git a/test/escrow/curve/QuadraticCurveMath.t.sol b/test/escrow/curve/QuadraticCurveMath.t.sol index 094697a..449309c 100644 --- a/test/escrow/curve/QuadraticCurveMath.t.sol +++ b/test/escrow/curve/QuadraticCurveMath.t.sol @@ -4,6 +4,7 @@ import {console2 as console} from "forge-std/console2.sol"; import {QuadraticIncreasingEscrow, IVotingEscrow, IEscrowCurve} from "src/escrow/increasing/QuadraticIncreasingEscrow.sol"; import {IVotingEscrowIncreasing, ILockedBalanceIncreasing} from "src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol"; + import {QuadraticCurveBase} from "./QuadraticCurveBase.t.sol"; contract TestQuadraticIncreasingCurve is QuadraticCurveBase { diff --git a/test/escrow/escrow/EscrowAdmin.t.sol b/test/escrow/escrow/EscrowAdmin.t.sol index eec6301..bb5a56e 100644 --- a/test/escrow/escrow/EscrowAdmin.t.sol +++ b/test/escrow/escrow/EscrowAdmin.t.sol @@ -8,6 +8,7 @@ import {Multisig, MultisigSetup} from "@aragon/multisig/MultisigSetup.sol"; import {ProxyLib} from "@libs/ProxyLib.sol"; +import {Lock} from "@escrow/Lock.sol"; import {VotingEscrow} from "@escrow/VotingEscrowIncreasing.sol"; import {QuadraticIncreasingEscrow} from "@escrow/QuadraticIncreasingEscrow.sol"; import {ExitQueue} from "@escrow/ExitQueue.sol"; @@ -100,25 +101,27 @@ contract TestEscrowAdmin is EscrowBase { address addr = address(1); vm.expectEmit(true, false, false, true); emit WhitelistSet(addr, true); - escrow.setWhitelisted(addr, true); - assertTrue(escrow.whitelisted(addr)); - escrow.setWhitelisted(addr, false); - assertFalse(escrow.whitelisted(addr)); + nftLock.setWhitelisted(addr, true); + assertTrue(nftLock.whitelisted(addr)); - escrow.enableTransfers(); + nftLock.setWhitelisted(addr, false); + assertFalse(nftLock.whitelisted(addr)); + + nftLock.enableTransfers(); assertTrue( - escrow.whitelisted(address(uint160(uint256(keccak256("WHITELIST_ANY_ADDRESS"))))) + nftLock.whitelisted(address(uint160(uint256(keccak256("WHITELIST_ANY_ADDRESS"))))) ); - bytes memory err = _authErr(attacker, address(escrow), escrow.ESCROW_ADMIN_ROLE()); + bytes memory err = _authErr(attacker, address(nftLock), nftLock.LOCK_ADMIN_ROLE()); + vm.startPrank(attacker); { vm.expectRevert(err); - escrow.setWhitelisted(addr, true); + nftLock.setWhitelisted(addr, true); vm.expectRevert(err); - escrow.enableTransfers(); + nftLock.enableTransfers(); } vm.stopPrank(); } @@ -131,4 +134,16 @@ contract TestEscrowAdmin is EscrowBase { vm.expectRevert(); escrow.totalVotingPower(); } + + // test upgrading the lock + function testUpgradeLock() public { + address newImpl = address(new Lock()); + nftLock.upgradeTo(newImpl); + assertEq(nftLock.implementation(), newImpl); + + bytes memory err = _authErr(attacker, address(nftLock), nftLock.LOCK_ADMIN_ROLE()); + vm.prank(attacker); + vm.expectRevert(err); + nftLock.upgradeTo(newImpl); + } } diff --git a/test/escrow/escrow/EscrowBase.sol b/test/escrow/escrow/EscrowBase.sol index e603e43..6b27f53 100644 --- a/test/escrow/escrow/EscrowBase.sol +++ b/test/escrow/escrow/EscrowBase.sol @@ -18,13 +18,20 @@ import "@helpers/OSxHelpers.sol"; import {ProxyLib} from "@libs/ProxyLib.sol"; import {IVotingEscrowEventsStorageErrorsEvents} from "@escrow-interfaces/IVotingEscrowIncreasing.sol"; +import {IWhitelistErrors, IWhitelistEvents} from "@escrow-interfaces/ILock.sol"; +import {Lock} from "@escrow/Lock.sol"; import {VotingEscrow} from "@escrow/VotingEscrowIncreasing.sol"; import {QuadraticIncreasingEscrow} from "@escrow/QuadraticIncreasingEscrow.sol"; import {ExitQueue} from "@escrow/ExitQueue.sol"; import {SimpleGaugeVoter, SimpleGaugeVoterSetup} from "src/voting/SimpleGaugeVoterSetup.sol"; import {Clock} from "@clock/Clock.sol"; -contract EscrowBase is Test, IVotingEscrowEventsStorageErrorsEvents { +contract EscrowBase is + Test, + IVotingEscrowEventsStorageErrorsEvents, + IWhitelistErrors, + IWhitelistEvents +{ using ProxyLib for address; string name = "Voting Escrow"; string symbol = "VE"; @@ -33,6 +40,7 @@ contract EscrowBase is Test, IVotingEscrowEventsStorageErrorsEvents { MockDAOFactory daoFactory; MockERC20 token; + Lock nftLock; VotingEscrow escrow; QuadraticIncreasingEscrow curve; SimpleGaugeVoter voter; @@ -44,6 +52,8 @@ contract EscrowBase is Test, IVotingEscrowEventsStorageErrorsEvents { MultisigSetup multisigSetup; address deployer = address(this); + error OnlyEscrow(); + function setUp() public virtual { // _deployOSX(); _deployDAO(); @@ -52,8 +62,9 @@ contract EscrowBase is Test, IVotingEscrowEventsStorageErrorsEvents { token = new MockERC20(); clock = _deployClock(address(dao)); - escrow = _deployEscrow(address(token), address(dao), name, symbol, address(clock)); + escrow = _deployEscrow(address(token), address(dao), address(clock)); curve = _deployCurve(address(escrow), address(dao), 3 days, address(clock)); + nftLock = _deployLock(address(escrow), name, symbol, address(dao)); // to be added as proxies voter = _deployVoter(address(dao), address(escrow), false, address(clock)); @@ -92,10 +103,17 @@ contract EscrowBase is Test, IVotingEscrowEventsStorageErrorsEvents { _permissionId: curve.CURVE_ADMIN_ROLE() }); + dao.grant({ + _who: address(this), + _where: address(nftLock), + _permissionId: nftLock.LOCK_ADMIN_ROLE() + }); + // link them escrow.setCurve(address(curve)); escrow.setVoter(address(voter)); escrow.setQueue(address(queue)); + escrow.setLockNFT(address(nftLock)); } function _authErr( @@ -116,19 +134,32 @@ contract EscrowBase is Test, IVotingEscrowEventsStorageErrorsEvents { function _deployEscrow( address _token, address _dao, - string memory _name, - string memory _symbol, address _clock ) public returns (VotingEscrow) { VotingEscrow impl = new VotingEscrow(); - bytes memory initCalldata = abi.encodeCall( - VotingEscrow.initialize, - (_token, _dao, _name, _symbol, _clock) - ); + bytes memory initCalldata = abi.encodeCall(VotingEscrow.initialize, (_token, _dao, _clock)); return VotingEscrow(address(impl).deployUUPSProxy(initCalldata)); } + function _deployLock( + address _escrow, + string memory _name, + string memory _symbol, + address _dao + ) public returns (Lock) { + Lock impl = new Lock(); + + bytes memory initCalldata = abi.encodeWithSelector( + Lock.initialize.selector, + _escrow, + _name, + _symbol, + _dao + ); + return Lock(address(impl).deployUUPSProxy(initCalldata)); + } + function _deployCurve( address _escrow, address _dao, diff --git a/test/escrow/escrow/EscrowCreateLock.t.sol b/test/escrow/escrow/EscrowCreateLock.t.sol index 4408fc6..bb75c3b 100644 --- a/test/escrow/escrow/EscrowCreateLock.t.sol +++ b/test/escrow/escrow/EscrowCreateLock.t.sol @@ -95,9 +95,9 @@ contract TestCreateLock is EscrowBase, IEscrowCurveUserStorage { // check the user has the nft: { assertEq(tokenId, tokenId, "wrong token id"); - assertEq(escrow.ownerOf(tokenId), _depositor); - assertEq(escrow.balanceOf(_depositor), tokenId); - assertEq(escrow.totalSupply(), tokenId); + assertEq(nftLock.ownerOf(tokenId), _depositor); + assertEq(nftLock.balanceOf(_depositor), tokenId); + assertEq(nftLock.totalSupply(), tokenId); } // check the contract has tokens @@ -175,8 +175,8 @@ contract TestCreateLock is EscrowBase, IEscrowCurveUserStorage { // check the user has the nft: { assertEq(tokenId, expectedTokenId, "token id unexpected"); - assertEq(escrow.ownerOf(tokenId), user.addr, "owner should be the user"); - assertEq(escrow.balanceOf(user.addr), 1, "user should only have 1 token"); + assertEq(nftLock.ownerOf(tokenId), user.addr, "owner should be the user"); + assertEq(nftLock.balanceOf(user.addr), 1, "user should only have 1 token"); } // check the user has the right lock @@ -191,7 +191,7 @@ contract TestCreateLock is EscrowBase, IEscrowCurveUserStorage { { assertEq(escrow.totalLocked(), total); assertEq(token.balanceOf(address(escrow)), total); - assertEq(escrow.totalSupply(), 2); + assertEq(nftLock.totalSupply(), 2); } } @@ -270,7 +270,6 @@ contract TestCreateLock is EscrowBase, IEscrowCurveUserStorage { vm.assume(_who != address(0)); vm.assume(_value > 0); vm.warp(1); - escrow.setWhitelisted(_who, true); token.mint(address(this), _value); token.approve(address(escrow), _value); @@ -279,9 +278,6 @@ contract TestCreateLock is EscrowBase, IEscrowCurveUserStorage { emit Deposit(_who, 1, 1 weeks, _value, _value); escrow.createLockFor(_value, _who); } - // Creating a lock: - // Creating a lock for someone: - // - Test we can make a lock for someone else } contract Mock {} diff --git a/test/escrow/escrow/EScrowTransfers.t.sol b/test/escrow/escrow/EscrowTransfers.t.sol similarity index 67% rename from test/escrow/escrow/EScrowTransfers.t.sol rename to test/escrow/escrow/EscrowTransfers.t.sol index f176b30..0389da4 100644 --- a/test/escrow/escrow/EScrowTransfers.t.sol +++ b/test/escrow/escrow/EscrowTransfers.t.sol @@ -26,28 +26,26 @@ contract TestEscrowTransfers is EscrowBase, IEscrowCurveUserStorage { token.approve(address(escrow), deposit); tokenId = escrow.createLock(deposit); - assertEq(escrow.balanceOf(address(this)), 1); + assertEq(nftLock.balanceOf(address(this)), 1); } function testCannotTransferByDefault() public { vm.expectRevert(NotWhitelisted.selector); - escrow.transferFrom(address(this), address(123), tokenId); + nftLock.transferFrom(address(this), address(123), tokenId); vm.expectRevert(NotWhitelisted.selector); - escrow.safeTransferFrom(address(this), address(123), tokenId); + nftLock.safeTransferFrom(address(this), address(123), tokenId); } function testCanTransferIfWhitelisted() public { - escrow.setWhitelisted(address(123), true); + nftLock.setWhitelisted(address(123), true); - assertEq(escrow.balanceOf(address(123)), 0); - assertEq(escrow.balanceOf(address(this)), 1); + assertEq(nftLock.balanceOf(address(123)), 0); + assertEq(nftLock.balanceOf(address(this)), 1); - escrow.transferFrom(address(this), address(123), tokenId); + nftLock.transferFrom(address(this), address(123), tokenId); - assertEq(escrow.balanceOf(address(123)), 1); - assertEq(escrow.balanceOf(address(this)), 0); - - // todo - reset the voting power + assertEq(nftLock.balanceOf(address(123)), 1); + assertEq(nftLock.balanceOf(address(this)), 0); } } diff --git a/test/escrow/escrow/EscrowWithdraw.t.sol b/test/escrow/escrow/EscrowWithdraw.t.sol index d82c35a..6b6ccb3 100644 --- a/test/escrow/escrow/EscrowWithdraw.t.sol +++ b/test/escrow/escrow/EscrowWithdraw.t.sol @@ -61,7 +61,7 @@ contract TestWithdraw is EscrowBase, IEscrowCurveUserStorage, IGaugeVote { // enter a withdrawal vm.startPrank(_who); { - escrow.approve(address(escrow), tokenId); + nftLock.approve(address(escrow), tokenId); escrow.resetVotesAndBeginWithdrawal(tokenId); } vm.stopPrank(); @@ -73,7 +73,7 @@ contract TestWithdraw is EscrowBase, IEscrowCurveUserStorage, IGaugeVote { // can't force approve vm.expectRevert("ERC721: approve caller is not token owner or approved for all"); vm.prank(_who); - escrow.approve(_who, tokenId); + nftLock.approve(_who, tokenId); // must wait till end of queue vm.warp(3 weeks - 1); @@ -106,8 +106,8 @@ contract TestWithdraw is EscrowBase, IEscrowCurveUserStorage, IGaugeVote { } // nft is burned - assertEq(escrow.balanceOf(_who), 0); - assertEq(escrow.balanceOf(address(escrow)), 0); + assertEq(nftLock.balanceOf(_who), 0); + assertEq(nftLock.balanceOf(address(escrow)), 0); assertEq(escrow.totalLocked(), 0); assertEq(escrow.votingPowerForAccount(_who), 0); @@ -140,14 +140,14 @@ contract TestWithdraw is EscrowBase, IEscrowCurveUserStorage, IGaugeVote { // enter a withdrawal vm.startPrank(_who); { - escrow.approve(address(escrow), tokenId); + nftLock.approve(address(escrow), tokenId); escrow.resetVotesAndBeginWithdrawal(tokenId); } vm.stopPrank(); // should now have the nft in the escrow - assertEq(escrow.balanceOf(_who), 0); - assertEq(escrow.balanceOf(address(escrow)), 1); + assertEq(nftLock.balanceOf(_who), 0); + assertEq(nftLock.balanceOf(address(escrow)), 1); // voting power should still be there as the cp is still active assertGt(escrow.votingPower(tokenId), 0); @@ -185,7 +185,7 @@ contract TestWithdraw is EscrowBase, IEscrowCurveUserStorage, IGaugeVote { // make a vote voter.vote(tokenId, votes); - escrow.approve(address(escrow), tokenId); + nftLock.approve(address(escrow), tokenId); escrow.resetVotesAndBeginWithdrawal(tokenId); } vm.stopPrank(); @@ -210,8 +210,8 @@ contract TestWithdraw is EscrowBase, IEscrowCurveUserStorage, IGaugeVote { // asserts assertEq(token.balanceOf(address(queue)), fee); assertEq(token.balanceOf(_who), _dep - fee); - assertEq(escrow.balanceOf(_who), 0); - assertEq(escrow.balanceOf(address(escrow)), 0); + assertEq(nftLock.balanceOf(_who), 0); + assertEq(nftLock.balanceOf(address(escrow)), 0); assertEq(escrow.totalLocked(), 0); } } diff --git a/test/escrow/escrow/Lock.t.sol b/test/escrow/escrow/Lock.t.sol new file mode 100644 index 0000000..c28e355 --- /dev/null +++ b/test/escrow/escrow/Lock.t.sol @@ -0,0 +1,72 @@ +pragma solidity ^0.8.17; + +import {EscrowBase} from "./EscrowBase.sol"; + +import {console2 as console} from "forge-std/console2.sol"; +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {Multisig, MultisigSetup} from "@aragon/multisig/MultisigSetup.sol"; + +import {ProxyLib} from "@libs/ProxyLib.sol"; + +import {IEscrowCurveUserStorage} from "@escrow-interfaces/IEscrowCurveIncreasing.sol"; +import {VotingEscrow} from "@escrow/VotingEscrowIncreasing.sol"; +import {Lock} from "@escrow/Lock.sol"; + +import {SimpleGaugeVoter, SimpleGaugeVoterSetup} from "src/voting/SimpleGaugeVoterSetup.sol"; +import {IGaugeVote} from "src/voting/ISimpleGaugeVoter.sol"; + +contract TestLockMintBurn is EscrowBase, IEscrowCurveUserStorage, IGaugeVote { + function testDeploy( + string memory _name, + string memory _symbol, + address _dao, + address _escrow + ) public { + Lock _nftLock = _deployLock(_escrow, _name, _symbol, _dao); + + assertEq(_nftLock.name(), _name); + assertEq(_nftLock.symbol(), _symbol); + assertEq(_nftLock.escrow(), _escrow); + assertEq(address(_nftLock.dao()), _dao); + } + + function testFuzz_OnlyEscrowCanMint(address _notEscrow) public { + vm.assume(_notEscrow != address(escrow)); + + vm.expectRevert(OnlyEscrow.selector); + + vm.prank(_notEscrow); + nftLock.mint(address(123), 1); + + assertEq(nftLock.balanceOf(address(123)), 0); + assertEq(nftLock.totalSupply(), 0); + + vm.prank(address(escrow)); + nftLock.mint(address(123), 1); + + assertEq(nftLock.balanceOf(address(123)), 1); + assertEq(nftLock.totalSupply(), 1); + } + + function testFuzz_OnlyEscrowCanBurn(address _notEscrow) public { + vm.assume(_notEscrow != address(escrow)); + + vm.prank(address(escrow)); + nftLock.mint(address(123), 1); + + vm.expectRevert(OnlyEscrow.selector); + + vm.prank(_notEscrow); + nftLock.burn(1); + + assertEq(nftLock.balanceOf(address(123)), 1); + assertEq(nftLock.totalSupply(), 1); + + vm.prank(address(escrow)); + nftLock.burn(1); + + assertEq(nftLock.balanceOf(address(123)), 0); + assertEq(nftLock.totalSupply(), 0); + } +} diff --git a/test/escrow/queue/ExitQueue.t.sol b/test/escrow/queue/ExitQueue.t.sol index 7b51fae..39bc726 100644 --- a/test/escrow/queue/ExitQueue.t.sol +++ b/test/escrow/queue/ExitQueue.t.sol @@ -101,7 +101,7 @@ contract TestExitQueue is ExitQueueBase { // test only the escrow can call stateful functions function testFuzz_onlyEscrowCanCall(address _notEscrow) public { - vm.assume(_notEscrow != address(this)); + vm.assume(_notEscrow != address(escrow)); bytes memory err = abi.encodeWithSelector(OnlyEscrow.selector); vm.startPrank(_notEscrow); @@ -154,6 +154,12 @@ contract TestExitQueue is ExitQueueBase { uint expectedExitDate; uint remainingSecondsBeforeNextCP = 1 weeks - (block.timestamp % 1 weeks); + + // TODO: is this 100% what we want if the cooldown is 0? + if (remainingSecondsBeforeNextCP == 1 weeks) { + remainingSecondsBeforeNextCP = 0; + } + if (queue.cooldown() < remainingSecondsBeforeNextCP) { expectedExitDate = block.timestamp + remainingSecondsBeforeNextCP; } else { diff --git a/test/integration/GaugesDaoFactory.sol b/test/integration/GaugesDaoFactory.sol new file mode 100644 index 0000000..b2833fa --- /dev/null +++ b/test/integration/GaugesDaoFactory.sol @@ -0,0 +1,1296 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {GaugesDaoFactory, Deployment, DeploymentParameters, TokenParameters} from "../../src/factory/GaugesDaoFactory.sol"; +import {MockPluginSetupProcessor} from "@mocks/osx/MockPSP.sol"; +import {MockPluginSetupProcessorMulti} from "@mocks/osx/MockPSPMulti.sol"; +import {MockPluginRepoRegistry} from "@mocks/osx/MockPluginRepoRegistry.sol"; +import {MockDAOFactory} from "@mocks/osx/MockDAOFactory.sol"; +import {PluginSetupProcessor} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol"; +import {PluginRepoFactory} from "@aragon/osx/framework/plugin/repo/PluginRepoFactory.sol"; +import {PluginRepoRegistry} from "@aragon/osx/framework/plugin/repo/PluginRepoRegistry.sol"; +import {PluginRepo} from "@aragon/osx/framework/plugin/repo/PluginRepo.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; +import {MultisigSetup as MultisigPluginSetup} from "@aragon/osx/plugins/governance/multisig/MultisigSetup.sol"; +import {SimpleGaugeVoterSetup, VotingEscrow, Clock, Lock, QuadraticIncreasingEscrow, ExitQueue, SimpleGaugeVoter} from "../../src/voting/SimpleGaugeVoterSetup.sol"; + +contract GaugesDaoFactoryTest is Test { + function test_ShouldStoreTheSettings_1() public { + address[] memory multisigMembers = new address[](13); + for (uint256 i = 0; i < 13; i++) { + multisigMembers[i] = address(uint160(i + 5)); + } + + SimpleGaugeVoterSetup gaugeVoterPluginSetup = new SimpleGaugeVoterSetup( + address(new SimpleGaugeVoter()), + address(new QuadraticIncreasingEscrow()), + address(new ExitQueue()), + address(new VotingEscrow()), + address(new Clock()), + address(new Lock()) + ); + + MockPluginRepoRegistry pRepoRegistry = new MockPluginRepoRegistry(); + PluginRepoFactory pRefoFactory = new PluginRepoFactory( + PluginRepoRegistry(address(pRepoRegistry)) + ); + MockPluginSetupProcessorMulti psp = new MockPluginSetupProcessorMulti(new address[](0)); + MockDAOFactory daoFactory = new MockDAOFactory(MockPluginSetupProcessor(address(psp))); + + TokenParameters[] memory tokenParameters = new TokenParameters[](2); + tokenParameters[0] = TokenParameters({ + token: address(111), + veTokenName: "Name 1", + veTokenSymbol: "TK1" + }); + tokenParameters[1] = TokenParameters({ + token: address(222), + veTokenName: "Name 2", + veTokenSymbol: "TK2" + }); + + DeploymentParameters memory creationParams = DeploymentParameters({ + // Multisig settings + minApprovals: 2, + multisigMembers: multisigMembers, + // Gauge Voter + tokenParameters: tokenParameters, + feePercent: 0.5 ether, + warmupPeriod: 1234, + cooldownPeriod: 2345, + minLockDuration: 3456, + votingPaused: false, + // Standard multisig repo + multisigPluginRepo: PluginRepo(address(5555)), + multisigPluginRelease: 1, + multisigPluginBuild: 2, + // Voter plugin setup and ENS + voterPluginSetup: gaugeVoterPluginSetup, + voterEnsSubdomain: "gauge-ens-subdomain", + // OSx addresses + osxDaoFactory: address(daoFactory), + pluginSetupProcessor: PluginSetupProcessor(address(psp)), + pluginRepoFactory: pRefoFactory + }); + + GaugesDaoFactory factory = new GaugesDaoFactory(creationParams); + + // Check + DeploymentParameters memory actualParams = factory.getDeploymentParameters(); + assertEq(actualParams.minApprovals, creationParams.minApprovals, "Incorrect minApprovals"); + assertEq( + actualParams.multisigMembers.length, + creationParams.multisigMembers.length, + "Incorrect multisigMembers.length" + ); + for (uint256 i = 0; i < 13; i++) { + assertEq(multisigMembers[i], address(uint160(i + 5)), "Incorrect member address"); + } + + assertEq( + actualParams.tokenParameters.length, + creationParams.tokenParameters.length, + "Incorrect tokenParameters.length" + ); + assertEq( + actualParams.tokenParameters[0].token, + creationParams.tokenParameters[0].token, + "Incorrect tokenParameters[0].token" + ); + assertEq( + actualParams.tokenParameters[0].veTokenName, + creationParams.tokenParameters[0].veTokenName, + "Incorrect tokenParameters[0].veTokenName" + ); + assertEq( + actualParams.tokenParameters[0].veTokenSymbol, + creationParams.tokenParameters[0].veTokenSymbol, + "Incorrect tokenParameters[0].veTokenSymbol" + ); + assertEq( + actualParams.tokenParameters[1].token, + creationParams.tokenParameters[1].token, + "Incorrect tokenParameters[1].token" + ); + assertEq( + actualParams.tokenParameters[1].veTokenName, + creationParams.tokenParameters[1].veTokenName, + "Incorrect tokenParameters[1].veTokenName" + ); + assertEq( + actualParams.tokenParameters[1].veTokenSymbol, + creationParams.tokenParameters[1].veTokenSymbol, + "Incorrect tokenParameters[1].veTokenSymbol" + ); + + assertEq(actualParams.feePercent, creationParams.feePercent, "Incorrect feePercent"); + assertEq(actualParams.warmupPeriod, creationParams.warmupPeriod, "Incorrect warmupPeriod"); + assertEq( + actualParams.cooldownPeriod, + creationParams.cooldownPeriod, + "Incorrect cooldownPeriod" + ); + assertEq( + actualParams.minLockDuration, + creationParams.minLockDuration, + "Incorrect minLockDuration" + ); + assertEq(actualParams.votingPaused, creationParams.votingPaused, "Incorrect votingPaused"); + + assertEq( + address(actualParams.multisigPluginRepo), + address(creationParams.multisigPluginRepo), + "Incorrect multisigPluginRepo" + ); + assertEq( + actualParams.multisigPluginRelease, + creationParams.multisigPluginRelease, + "Incorrect multisigPluginRelease" + ); + assertEq( + actualParams.multisigPluginBuild, + creationParams.multisigPluginBuild, + "Incorrect multisigPluginBuild" + ); + assertEq( + address(actualParams.voterPluginSetup), + address(creationParams.voterPluginSetup), + "Incorrect voterPluginSetup" + ); + assertEq( + actualParams.voterEnsSubdomain, + creationParams.voterEnsSubdomain, + "Incorrect voterEnsSubdomain" + ); + + assertEq( + address(actualParams.osxDaoFactory), + address(creationParams.osxDaoFactory), + "Incorrect osxDaoFactory" + ); + assertEq( + address(actualParams.pluginSetupProcessor), + address(creationParams.pluginSetupProcessor), + "Incorrect pluginSetupProcessor" + ); + assertEq( + address(actualParams.pluginRepoFactory), + address(creationParams.pluginRepoFactory), + "Incorrect pluginRepoFactory" + ); + } + + function test_ShouldStoreTheSettings_2() public { + address[] memory multisigMembers = new address[](13); + for (uint256 i = 0; i < 13; i++) { + multisigMembers[i] = address(uint160(i + 10)); + } + + SimpleGaugeVoterSetup gaugeVoterPluginSetup = new SimpleGaugeVoterSetup( + address(new SimpleGaugeVoter()), + address(new QuadraticIncreasingEscrow()), + address(new ExitQueue()), + address(new VotingEscrow()), + address(new Clock()), + address(new Lock()) + ); + + MockPluginRepoRegistry pRepoRegistry = new MockPluginRepoRegistry(); + PluginRepoFactory pRefoFactory = new PluginRepoFactory( + PluginRepoRegistry(address(pRepoRegistry)) + ); + MockPluginSetupProcessorMulti psp = new MockPluginSetupProcessorMulti(new address[](0)); + MockDAOFactory daoFactory = new MockDAOFactory(MockPluginSetupProcessor(address(psp))); + + TokenParameters[] memory tokenParameters = new TokenParameters[](2); + tokenParameters[0] = TokenParameters({ + token: address(333), + veTokenName: "Name 3", + veTokenSymbol: "TK3" + }); + tokenParameters[1] = TokenParameters({ + token: address(444), + veTokenName: "Name 4", + veTokenSymbol: "TK4" + }); + + DeploymentParameters memory creationParams = DeploymentParameters({ + // Multisig settings + minApprovals: 3, + multisigMembers: multisigMembers, + // Gauge Voter + tokenParameters: tokenParameters, + feePercent: 0.1 ether, + warmupPeriod: 7654, + cooldownPeriod: 6543, + minLockDuration: 5432, + votingPaused: true, + // Standard multisig repo + multisigPluginRepo: PluginRepo(address(3333)), + multisigPluginRelease: 2, + multisigPluginBuild: 10, + // Voter plugin setup and ENS + voterPluginSetup: gaugeVoterPluginSetup, + voterEnsSubdomain: "gauge-ens-subdomain-bis", + // OSx addresses + osxDaoFactory: address(daoFactory), + pluginSetupProcessor: PluginSetupProcessor(address(psp)), + pluginRepoFactory: pRefoFactory + }); + + GaugesDaoFactory factory = new GaugesDaoFactory(creationParams); + + // Check + DeploymentParameters memory actualParams = factory.getDeploymentParameters(); + assertEq(actualParams.minApprovals, creationParams.minApprovals, "Incorrect minApprovals"); + assertEq( + actualParams.multisigMembers.length, + creationParams.multisigMembers.length, + "Incorrect multisigMembers.length" + ); + for (uint256 i = 0; i < 13; i++) { + assertEq(multisigMembers[i], address(uint160(i + 10)), "Incorrect member address"); + } + + assertEq( + actualParams.tokenParameters.length, + creationParams.tokenParameters.length, + "Incorrect tokenParameters.length" + ); + assertEq( + actualParams.tokenParameters[0].token, + creationParams.tokenParameters[0].token, + "Incorrect tokenParameters[0].token" + ); + assertEq( + actualParams.tokenParameters[0].veTokenName, + creationParams.tokenParameters[0].veTokenName, + "Incorrect tokenParameters[0].veTokenName" + ); + assertEq( + actualParams.tokenParameters[0].veTokenSymbol, + creationParams.tokenParameters[0].veTokenSymbol, + "Incorrect tokenParameters[0].veTokenSymbol" + ); + assertEq( + actualParams.tokenParameters[1].token, + creationParams.tokenParameters[1].token, + "Incorrect tokenParameters[1].token" + ); + assertEq( + actualParams.tokenParameters[1].veTokenName, + creationParams.tokenParameters[1].veTokenName, + "Incorrect tokenParameters[1].veTokenName" + ); + assertEq( + actualParams.tokenParameters[1].veTokenSymbol, + creationParams.tokenParameters[1].veTokenSymbol, + "Incorrect tokenParameters[1].veTokenSymbol" + ); + + assertEq(actualParams.feePercent, creationParams.feePercent, "Incorrect feePercent"); + assertEq(actualParams.warmupPeriod, creationParams.warmupPeriod, "Incorrect warmupPeriod"); + assertEq( + actualParams.cooldownPeriod, + creationParams.cooldownPeriod, + "Incorrect cooldownPeriod" + ); + assertEq( + actualParams.minLockDuration, + creationParams.minLockDuration, + "Incorrect minLockDuration" + ); + assertEq(actualParams.votingPaused, creationParams.votingPaused, "Incorrect votingPaused"); + + assertEq( + address(actualParams.multisigPluginRepo), + address(creationParams.multisigPluginRepo), + "Incorrect multisigPluginRepo" + ); + assertEq( + actualParams.multisigPluginRelease, + creationParams.multisigPluginRelease, + "Incorrect multisigPluginRelease" + ); + assertEq( + actualParams.multisigPluginBuild, + creationParams.multisigPluginBuild, + "Incorrect multisigPluginBuild" + ); + assertEq( + address(actualParams.voterPluginSetup), + address(creationParams.voterPluginSetup), + "Incorrect voterPluginSetup" + ); + assertEq( + actualParams.voterEnsSubdomain, + creationParams.voterEnsSubdomain, + "Incorrect voterEnsSubdomain" + ); + + assertEq( + address(actualParams.osxDaoFactory), + address(creationParams.osxDaoFactory), + "Incorrect osxDaoFactory" + ); + assertEq( + address(actualParams.pluginSetupProcessor), + address(creationParams.pluginSetupProcessor), + "Incorrect pluginSetupProcessor" + ); + assertEq( + address(actualParams.pluginRepoFactory), + address(creationParams.pluginRepoFactory), + "Incorrect pluginRepoFactory" + ); + } + + function test_StandardDeployment_1() public { + address[] memory multisigMembers = new address[](13); + for (uint256 i = 0; i < 13; i++) { + multisigMembers[i] = address(uint160(i + 5)); + } + + PluginRepoFactory pRefoFactory = new PluginRepoFactory( + PluginRepoRegistry(address(new MockPluginRepoRegistry())) + ); + + // Publish repo + MultisigPluginSetup multisigPluginSetup = new MultisigPluginSetup(); + PluginRepo multisigPluginRepo = PluginRepoFactory(pRefoFactory) + .createPluginRepoWithFirstVersion( + "multisig-subdomain", + address(multisigPluginSetup), + address(this), + " ", + " " + ); + + SimpleGaugeVoterSetup gaugeVoterPluginSetup = new SimpleGaugeVoterSetup( + address(new SimpleGaugeVoter()), + address(new QuadraticIncreasingEscrow()), + address(new ExitQueue()), + address(new VotingEscrow()), + address(new Clock()), + address(new Lock()) + ); + + TokenParameters[] memory tokenParameters = new TokenParameters[](2); + tokenParameters[0] = TokenParameters({ + token: address(deployMockERC20("T1", "T1", 18)), + veTokenName: "Name 1", + veTokenSymbol: "TK1" + }); + tokenParameters[1] = TokenParameters({ + token: address(deployMockERC20("T2", "T2", 18)), + veTokenName: "Name 2", + veTokenSymbol: "TK2" + }); + + // PSP with voter plugin setup and multisig + MockPluginSetupProcessorMulti psp; + { + address[] memory pluginSetups = new address[](3); + pluginSetups[0] = address(gaugeVoterPluginSetup); // Token 1 + pluginSetups[1] = address(gaugeVoterPluginSetup); // Token 2 + pluginSetups[2] = address(multisigPluginSetup); + + psp = new MockPluginSetupProcessorMulti(pluginSetups); + } + MockDAOFactory daoFactory = new MockDAOFactory(MockPluginSetupProcessor(address(psp))); + + DeploymentParameters memory creationParams = DeploymentParameters({ + // Multisig settings + minApprovals: 2, + multisigMembers: multisigMembers, + // Gauge Voter + tokenParameters: tokenParameters, + feePercent: 0.5 ether, + warmupPeriod: 1234, + cooldownPeriod: 2345, + minLockDuration: 3456, + votingPaused: false, + // Standard multisig repo + multisigPluginRepo: multisigPluginRepo, + multisigPluginRelease: 1, + multisigPluginBuild: 2, + // Voter plugin setup and ENS + voterPluginSetup: gaugeVoterPluginSetup, + voterEnsSubdomain: "gauge-ens-subdomain", + // OSx addresses + osxDaoFactory: address(daoFactory), + pluginSetupProcessor: PluginSetupProcessor(address(psp)), + pluginRepoFactory: pRefoFactory + }); + + GaugesDaoFactory factory = new GaugesDaoFactory(creationParams); + + factory.deployOnce(); + Deployment memory deployment = factory.getDeployment(); + + vm.roll(block.number + 1); // mint one block + + // DAO checks + + assertNotEq(address(deployment.dao), address(0), "Empty DAO field"); + assertEq(deployment.dao.daoURI(), "", "DAO URI should be empty"); + assertEq( + address(deployment.dao.signatureValidator()), + address(0), + "signatureValidator should be empty" + ); + assertEq( + address(deployment.dao.getTrustedForwarder()), + address(0), + "trustedForwarder should be empty" + ); + assertEq( + deployment.dao.hasPermission( + address(deployment.dao), + address(deployment.dao), + deployment.dao.ROOT_PERMISSION_ID(), + bytes("") + ), + true, + "The DAO should be ROOT on itself" + ); + assertEq( + deployment.dao.hasPermission( + address(deployment.dao), + address(deployment.dao), + deployment.dao.UPGRADE_DAO_PERMISSION_ID(), + bytes("") + ), + true, + "The DAO should have UPGRADE_DAO_PERMISSION on itself" + ); + assertEq( + deployment.dao.hasPermission( + address(deployment.dao), + address(deployment.dao), + deployment.dao.REGISTER_STANDARD_CALLBACK_PERMISSION_ID(), + bytes("") + ), + true, + "The DAO should have REGISTER_STANDARD_CALLBACK_PERMISSION_ID on itself" + ); + + // Multisig plugin + + assertNotEq(address(deployment.multisigPlugin), address(0), "Empty multisig field"); + assertEq( + deployment.multisigPlugin.lastMultisigSettingsChange(), + block.number - 1, + "Invalid lastMultisigSettingsChange" + ); + assertEq(deployment.multisigPlugin.proposalCount(), 0, "Invalid proposal count"); + assertEq(deployment.multisigPlugin.addresslistLength(), 13, "Invalid addresslistLength"); + for (uint256 i = 0; i < 13; i++) { + assertEq( + deployment.multisigPlugin.isMember(multisigMembers[i]), + true, + "Should be a member" + ); + } + for (uint256 i = 14; i < 50; i++) { + assertEq( + deployment.multisigPlugin.isMember(address(uint160(i + 5))), + false, + "Should not be a member" + ); + } + { + (bool onlyListed, uint16 minApprovals) = deployment.multisigPlugin.multisigSettings(); + + assertEq(onlyListed, true, "Invalid onlyListed"); + assertEq(minApprovals, 2, "Invalid minApprovals"); + } + + // Gauge voter plugin + + assertEq( + deployment.gaugeVoterPluginSets.length, + 2, + "Incorrect gaugeVoterPluginSets length" + ); + // 0 + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].plugin), + address(0), + "Empty plugin address" + ); + assertEq(deployment.gaugeVoterPluginSets[0].plugin.paused(), false, "Should not be paused"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].curve), + address(0), + "Empty curve address" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].curve.warmupPeriod(), + 1234, + "Incorrect warmupPeriod" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].exitQueue), + address(0), + "Empty exitQueue address" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].exitQueue.feePercent(), + 0.5 ether, + "Incorrect feePercent" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].exitQueue.cooldown(), + 2345, + "Incorrect cooldown" + ); + assertEq(deployment.gaugeVoterPluginSets[0].exitQueue.minLock(), 3456, "Incorrect minLock"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].votingEscrow), + address(0), + "Empty votingEscrow address" + ); + + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.token(), + tokenParameters[0].token, + "Incorrect token contract" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.voter(), + address(deployment.gaugeVoterPluginSets[0].plugin), + "Incorrect voter" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.curve(), + address(deployment.gaugeVoterPluginSets[0].curve), + "Incorrect curve" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.queue(), + address(deployment.gaugeVoterPluginSets[0].exitQueue), + "Incorrect queue" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.clock(), + address(deployment.gaugeVoterPluginSets[0].clock), + "Incorrect clock" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.lockNFT(), + address(deployment.gaugeVoterPluginSets[0].nftLock), + "Incorrect lockNFT" + ); + + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].clock), + address(0), + "Empty clock address" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].nftLock), + address(0), + "Empty nftLock address" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].nftLock.name(), + tokenParameters[0].veTokenName, + "Incorrect veTokenName" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].nftLock.symbol(), + tokenParameters[0].veTokenSymbol, + "Incorrect veTokenSymbol" + ); + // 1 + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].plugin), + address(0), + "Empty plugin address" + ); + assertEq(deployment.gaugeVoterPluginSets[1].plugin.paused(), false, "Should not be paused"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].curve), + address(0), + "Empty curve address" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].curve.warmupPeriod(), + 1234, + "Incorrect warmupPeriod" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].exitQueue), + address(0), + "Empty exitQueue address" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].exitQueue.feePercent(), + 0.5 ether, + "Incorrect feePercent" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].exitQueue.cooldown(), + 2345, + "Incorrect cooldown" + ); + assertEq(deployment.gaugeVoterPluginSets[1].exitQueue.minLock(), 3456, "Incorrect minLock"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].votingEscrow), + address(0), + "Empty votingEscrow address" + ); + + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.token(), + tokenParameters[1].token, + "Incorrect token contract" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.voter(), + address(deployment.gaugeVoterPluginSets[1].plugin), + "Incorrect voter" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.curve(), + address(deployment.gaugeVoterPluginSets[1].curve), + "Incorrect curve" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.queue(), + address(deployment.gaugeVoterPluginSets[1].exitQueue), + "Incorrect queue" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.clock(), + address(deployment.gaugeVoterPluginSets[1].clock), + "Incorrect clock" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.lockNFT(), + address(deployment.gaugeVoterPluginSets[1].nftLock), + "Incorrect lockNFT" + ); + + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].clock), + address(0), + "Empty clock address" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].nftLock), + address(0), + "Empty nftLock address" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].nftLock.name(), + tokenParameters[1].veTokenName, + "Incorrect veTokenName" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].nftLock.symbol(), + tokenParameters[1].veTokenSymbol, + "Incorrect veTokenSymbol" + ); + + // PLUGIN REPO's + + PluginRepo.Version memory version; + + // Multisig code + version = multisigPluginRepo.getLatestVersion(1); + assertEq( + address(multisigPluginSetup.implementation()), + address(deployment.multisigPlugin.implementation()), + "Invalid multisigPluginSetup" + ); + + // Gauge voter plugin + assertNotEq( + address(deployment.gaugeVoterPluginRepo), + address(0), + "Empty gaugeVoterPluginRepo field" + ); + assertEq(deployment.gaugeVoterPluginRepo.latestRelease(), 1, "Invalid latestRelease"); + assertEq(deployment.gaugeVoterPluginRepo.buildCount(1), 1, "Invalid buildCount"); + version = deployment.gaugeVoterPluginRepo.getLatestVersion(1); + assertEq( + address(version.pluginSetup), + address(gaugeVoterPluginSetup), + "Invalid gaugeVoterPluginSetup" + ); + } + + function test_StandardDeployment_2() public { + address[] memory multisigMembers = new address[](13); + for (uint256 i = 0; i < 13; i++) { + multisigMembers[i] = address(uint160(i + 10)); + } + + PluginRepoFactory pRefoFactory = new PluginRepoFactory( + PluginRepoRegistry(address(new MockPluginRepoRegistry())) + ); + + // Publish repo + MultisigPluginSetup multisigPluginSetup = new MultisigPluginSetup(); + PluginRepo multisigPluginRepo = PluginRepoFactory(pRefoFactory) + .createPluginRepoWithFirstVersion( + "multisig-2-subdomain", + address(multisigPluginSetup), + address(this), + " ", + " " + ); + + SimpleGaugeVoterSetup gaugeVoterPluginSetup = new SimpleGaugeVoterSetup( + address(new SimpleGaugeVoter()), + address(new QuadraticIncreasingEscrow()), + address(new ExitQueue()), + address(new VotingEscrow()), + address(new Clock()), + address(new Lock()) + ); + + TokenParameters[] memory tokenParameters = new TokenParameters[](3); + tokenParameters[0] = TokenParameters({ + token: address(deployMockERC20("T3", "T3", 18)), + veTokenName: "Name 3", + veTokenSymbol: "TK3" + }); + tokenParameters[1] = TokenParameters({ + token: address(deployMockERC20("T4", "T4", 18)), + veTokenName: "Name 4", + veTokenSymbol: "TK4" + }); + tokenParameters[2] = TokenParameters({ + token: address(deployMockERC20("T5", "T5", 18)), + veTokenName: "Name 5", + veTokenSymbol: "TK5" + }); + + // PSP with voter plugin setup and multisig + MockPluginSetupProcessorMulti psp; + { + address[] memory pluginSetups = new address[](4); + pluginSetups[0] = address(gaugeVoterPluginSetup); // Token 1 + pluginSetups[1] = address(gaugeVoterPluginSetup); // Token 2 + pluginSetups[2] = address(gaugeVoterPluginSetup); // Token 3 + pluginSetups[3] = address(multisigPluginSetup); + + psp = new MockPluginSetupProcessorMulti(pluginSetups); + } + MockDAOFactory daoFactory = new MockDAOFactory(MockPluginSetupProcessor(address(psp))); + + DeploymentParameters memory creationParams = DeploymentParameters({ + // Multisig settings + minApprovals: 5, + multisigMembers: multisigMembers, + // Gauge Voter + tokenParameters: tokenParameters, + feePercent: 0.2 ether, + warmupPeriod: 5678, + cooldownPeriod: 6789, + minLockDuration: 7890, + votingPaused: true, + // Standard multisig repo + multisigPluginRepo: multisigPluginRepo, + multisigPluginRelease: 1, + multisigPluginBuild: 2, + // Voter plugin setup and ENS + voterPluginSetup: gaugeVoterPluginSetup, + voterEnsSubdomain: "gauge-ens-subdomain", + // OSx addresses + osxDaoFactory: address(daoFactory), + pluginSetupProcessor: PluginSetupProcessor(address(psp)), + pluginRepoFactory: pRefoFactory + }); + + GaugesDaoFactory factory = new GaugesDaoFactory(creationParams); + + factory.deployOnce(); + Deployment memory deployment = factory.getDeployment(); + + vm.roll(block.number + 1); // mint one block + + // DAO checks + + assertNotEq(address(deployment.dao), address(0), "Empty DAO field"); + assertEq(deployment.dao.daoURI(), "", "DAO URI should be empty"); + assertEq( + address(deployment.dao.signatureValidator()), + address(0), + "signatureValidator should be empty" + ); + assertEq( + address(deployment.dao.getTrustedForwarder()), + address(0), + "trustedForwarder should be empty" + ); + assertEq( + deployment.dao.hasPermission( + address(deployment.dao), + address(deployment.dao), + deployment.dao.ROOT_PERMISSION_ID(), + bytes("") + ), + true, + "The DAO should be ROOT on itself" + ); + assertEq( + deployment.dao.hasPermission( + address(deployment.dao), + address(deployment.dao), + deployment.dao.UPGRADE_DAO_PERMISSION_ID(), + bytes("") + ), + true, + "The DAO should have UPGRADE_DAO_PERMISSION on itself" + ); + assertEq( + deployment.dao.hasPermission( + address(deployment.dao), + address(deployment.dao), + deployment.dao.REGISTER_STANDARD_CALLBACK_PERMISSION_ID(), + bytes("") + ), + true, + "The DAO should have REGISTER_STANDARD_CALLBACK_PERMISSION_ID on itself" + ); + + // Multisig plugin + + assertNotEq(address(deployment.multisigPlugin), address(0), "Empty multisig field"); + assertEq( + deployment.multisigPlugin.lastMultisigSettingsChange(), + block.number - 1, + "Invalid lastMultisigSettingsChange" + ); + assertEq(deployment.multisigPlugin.proposalCount(), 0, "Invalid proposal count"); + assertEq(deployment.multisigPlugin.addresslistLength(), 13, "Invalid addresslistLength"); + for (uint256 i = 0; i < 13; i++) { + assertEq( + deployment.multisigPlugin.isMember(multisigMembers[i]), + true, + "Should be a member" + ); + } + for (uint256 i = 14; i < 50; i++) { + assertEq( + deployment.multisigPlugin.isMember(address(uint160(i + 10))), + false, + "Should not be a member" + ); + } + { + (bool onlyListed, uint16 minApprovals) = deployment.multisigPlugin.multisigSettings(); + + assertEq(onlyListed, true, "Invalid onlyListed"); + assertEq(minApprovals, 5, "Invalid minApprovals"); + } + + // Gauge voter plugin + + assertEq( + deployment.gaugeVoterPluginSets.length, + 3, + "Incorrect gaugeVoterPluginSets length" + ); + // 0 + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].plugin), + address(0), + "Empty plugin address" + ); + assertEq(deployment.gaugeVoterPluginSets[0].plugin.paused(), true, "Should be paused"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].curve), + address(0), + "Empty curve address" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].curve.warmupPeriod(), + 5678, + "Incorrect warmupPeriod" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].exitQueue), + address(0), + "Empty exitQueue address" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].exitQueue.feePercent(), + 0.2 ether, + "Incorrect feePercent" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].exitQueue.cooldown(), + 6789, + "Incorrect cooldown" + ); + assertEq(deployment.gaugeVoterPluginSets[0].exitQueue.minLock(), 7890, "Incorrect minLock"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].votingEscrow), + address(0), + "Empty votingEscrow address" + ); + + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.token(), + tokenParameters[0].token, + "Incorrect token contract" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.voter(), + address(deployment.gaugeVoterPluginSets[0].plugin), + "Incorrect voter" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.curve(), + address(deployment.gaugeVoterPluginSets[0].curve), + "Incorrect curve" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.queue(), + address(deployment.gaugeVoterPluginSets[0].exitQueue), + "Incorrect queue" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.clock(), + address(deployment.gaugeVoterPluginSets[0].clock), + "Incorrect clock" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].votingEscrow.lockNFT(), + address(deployment.gaugeVoterPluginSets[0].nftLock), + "Incorrect lockNFT" + ); + + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].clock), + address(0), + "Empty clock address" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[0].nftLock), + address(0), + "Empty nftLock address" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].nftLock.name(), + tokenParameters[0].veTokenName, + "Incorrect veTokenName" + ); + assertEq( + deployment.gaugeVoterPluginSets[0].nftLock.symbol(), + tokenParameters[0].veTokenSymbol, + "Incorrect veTokenSymbol" + ); + // 1 + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].plugin), + address(0), + "Empty plugin address" + ); + assertEq(deployment.gaugeVoterPluginSets[1].plugin.paused(), true, "Should be paused"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].curve), + address(0), + "Empty curve address" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].curve.warmupPeriod(), + 5678, + "Incorrect warmupPeriod" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].exitQueue), + address(0), + "Empty exitQueue address" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].exitQueue.feePercent(), + 0.2 ether, + "Incorrect feePercent" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].exitQueue.cooldown(), + 6789, + "Incorrect cooldown" + ); + assertEq(deployment.gaugeVoterPluginSets[1].exitQueue.minLock(), 7890, "Incorrect minLock"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].votingEscrow), + address(0), + "Empty votingEscrow address" + ); + + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.token(), + tokenParameters[1].token, + "Incorrect token contract" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.voter(), + address(deployment.gaugeVoterPluginSets[1].plugin), + "Incorrect voter" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.curve(), + address(deployment.gaugeVoterPluginSets[1].curve), + "Incorrect curve" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.queue(), + address(deployment.gaugeVoterPluginSets[1].exitQueue), + "Incorrect queue" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.clock(), + address(deployment.gaugeVoterPluginSets[1].clock), + "Incorrect clock" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].votingEscrow.lockNFT(), + address(deployment.gaugeVoterPluginSets[1].nftLock), + "Incorrect lockNFT" + ); + + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].clock), + address(0), + "Empty clock address" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[1].nftLock), + address(0), + "Empty nftLock address" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].nftLock.name(), + tokenParameters[1].veTokenName, + "Incorrect veTokenName" + ); + assertEq( + deployment.gaugeVoterPluginSets[1].nftLock.symbol(), + tokenParameters[1].veTokenSymbol, + "Incorrect veTokenSymbol" + ); + // 2 + assertNotEq( + address(deployment.gaugeVoterPluginSets[2].plugin), + address(0), + "Empty plugin address" + ); + assertEq(deployment.gaugeVoterPluginSets[2].plugin.paused(), true, "Should be paused"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[2].curve), + address(0), + "Empty curve address" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].curve.warmupPeriod(), + 5678, + "Incorrect warmupPeriod" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[2].exitQueue), + address(0), + "Empty exitQueue address" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].exitQueue.feePercent(), + 0.2 ether, + "Incorrect feePercent" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].exitQueue.cooldown(), + 6789, + "Incorrect cooldown" + ); + assertEq(deployment.gaugeVoterPluginSets[2].exitQueue.minLock(), 7890, "Incorrect minLock"); + assertNotEq( + address(deployment.gaugeVoterPluginSets[2].votingEscrow), + address(0), + "Empty votingEscrow address" + ); + + assertEq( + deployment.gaugeVoterPluginSets[2].votingEscrow.token(), + tokenParameters[2].token, + "Incorrect token contract" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].votingEscrow.voter(), + address(deployment.gaugeVoterPluginSets[2].plugin), + "Incorrect voter" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].votingEscrow.curve(), + address(deployment.gaugeVoterPluginSets[2].curve), + "Incorrect curve" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].votingEscrow.queue(), + address(deployment.gaugeVoterPluginSets[2].exitQueue), + "Incorrect queue" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].votingEscrow.clock(), + address(deployment.gaugeVoterPluginSets[2].clock), + "Incorrect clock" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].votingEscrow.lockNFT(), + address(deployment.gaugeVoterPluginSets[2].nftLock), + "Incorrect lockNFT" + ); + + assertNotEq( + address(deployment.gaugeVoterPluginSets[2].clock), + address(0), + "Empty clock address" + ); + assertNotEq( + address(deployment.gaugeVoterPluginSets[2].nftLock), + address(0), + "Empty nftLock address" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].nftLock.name(), + tokenParameters[2].veTokenName, + "Incorrect veTokenName" + ); + assertEq( + deployment.gaugeVoterPluginSets[2].nftLock.symbol(), + tokenParameters[2].veTokenSymbol, + "Incorrect veTokenSymbol" + ); + + // PLUGIN REPO's + + PluginRepo.Version memory version; + + // Multisig code + version = multisigPluginRepo.getLatestVersion(1); + assertEq( + address(multisigPluginSetup.implementation()), + address(deployment.multisigPlugin.implementation()), + "Invalid multisigPluginSetup" + ); + + // Gauge voter plugin + assertNotEq( + address(deployment.gaugeVoterPluginRepo), + address(0), + "Empty gaugeVoterPluginRepo field" + ); + assertEq(deployment.gaugeVoterPluginRepo.latestRelease(), 1, "Invalid latestRelease"); + assertEq(deployment.gaugeVoterPluginRepo.buildCount(1), 1, "Invalid buildCount"); + version = deployment.gaugeVoterPluginRepo.getLatestVersion(1); + assertEq( + address(version.pluginSetup), + address(gaugeVoterPluginSetup), + "Invalid gaugeVoterPluginSetup" + ); + } + + function test_MultipleDeploysDoNothing() public { + address[] memory multisigMembers = new address[](13); + for (uint256 i = 0; i < 13; i++) { + multisigMembers[i] = address(uint160(i + 10)); + } + + PluginRepoFactory pRefoFactory = new PluginRepoFactory( + PluginRepoRegistry(address(new MockPluginRepoRegistry())) + ); + + // Publish repo + MultisigPluginSetup multisigPluginSetup = new MultisigPluginSetup(); + PluginRepo multisigPluginRepo = PluginRepoFactory(pRefoFactory) + .createPluginRepoWithFirstVersion( + "multisig-2-subdomain", + address(multisigPluginSetup), + address(this), + " ", + " " + ); + + SimpleGaugeVoterSetup gaugeVoterPluginSetup = new SimpleGaugeVoterSetup( + address(new SimpleGaugeVoter()), + address(new QuadraticIncreasingEscrow()), + address(new ExitQueue()), + address(new VotingEscrow()), + address(new Clock()), + address(new Lock()) + ); + + TokenParameters[] memory tokenParameters = new TokenParameters[](3); + tokenParameters[0] = TokenParameters({ + token: address(deployMockERC20("T3", "T3", 18)), + veTokenName: "Name 3", + veTokenSymbol: "TK3" + }); + tokenParameters[1] = TokenParameters({ + token: address(deployMockERC20("T4", "T4", 18)), + veTokenName: "Name 4", + veTokenSymbol: "TK4" + }); + tokenParameters[2] = TokenParameters({ + token: address(deployMockERC20("T5", "T5", 18)), + veTokenName: "Name 5", + veTokenSymbol: "TK5" + }); + + // PSP with voter plugin setup and multisig + MockPluginSetupProcessorMulti psp; + { + address[] memory pluginSetups = new address[](4); + pluginSetups[0] = address(gaugeVoterPluginSetup); // Token 1 + pluginSetups[1] = address(gaugeVoterPluginSetup); // Token 2 + pluginSetups[2] = address(gaugeVoterPluginSetup); // Token 3 + pluginSetups[3] = address(multisigPluginSetup); + + psp = new MockPluginSetupProcessorMulti(pluginSetups); + } + MockDAOFactory daoFactory = new MockDAOFactory(MockPluginSetupProcessor(address(psp))); + + DeploymentParameters memory creationParams = DeploymentParameters({ + // Multisig settings + minApprovals: 5, + multisigMembers: multisigMembers, + // Gauge Voter + tokenParameters: tokenParameters, + feePercent: 0.5 ether, + warmupPeriod: 1234, + cooldownPeriod: 2345, + minLockDuration: 3456, + votingPaused: false, + // Standard multisig repo + multisigPluginRepo: multisigPluginRepo, + multisigPluginRelease: 1, + multisigPluginBuild: 2, + // Voter plugin setup and ENS + voterPluginSetup: gaugeVoterPluginSetup, + voterEnsSubdomain: "gauge-ens-subdomain", + // OSx addresses + osxDaoFactory: address(daoFactory), + pluginSetupProcessor: PluginSetupProcessor(address(psp)), + pluginRepoFactory: pRefoFactory + }); + + GaugesDaoFactory factory = new GaugesDaoFactory(creationParams); + + // ok + factory.deployOnce(); + + vm.expectRevert(abi.encodeWithSelector(GaugesDaoFactory.AlreadyDeployed.selector)); + factory.deployOnce(); + + vm.expectRevert(abi.encodeWithSelector(GaugesDaoFactory.AlreadyDeployed.selector)); + factory.deployOnce(); + } +} diff --git a/test/mocks/osx/MockPSPMulti.sol b/test/mocks/osx/MockPSPMulti.sol new file mode 100644 index 0000000..fab1480 --- /dev/null +++ b/test/mocks/osx/MockPSPMulti.sol @@ -0,0 +1,744 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.17; + +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import {DAO, IDAO} from "@aragon/osx/core/dao/DAO.sol"; +import {PermissionLib} from "@aragon/osx/core/permission/PermissionLib.sol"; +import {PluginUUPSUpgradeable} from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol"; +import {IPlugin} from "@aragon/osx/core/plugin/IPlugin.sol"; +import {PluginRepo} from "@aragon/osx/framework/plugin/repo/PluginRepo.sol"; +import {IPluginSetup} from "@aragon/osx/framework/plugin/setup/IPluginSetup.sol"; +import {PluginSetup} from "@aragon/osx/framework/plugin/setup/PluginSetup.sol"; +import {PluginSetupRef, hashHelpers, hashPermissions, _getPreparedSetupId, _getAppliedSetupId, _getPluginInstallationId, PreparationType} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessorHelpers.sol"; + +/// @title PluginSetupProcessor +/// @author Aragon Association - 2022-2023 +/// @notice This contract processes the preparation and application of plugin setups (installation, update, uninstallation) on behalf of a requesting DAO. +/// @dev This contract is temporarily granted the `ROOT_PERMISSION_ID` permission on the applying DAO and therefore is highly security critical. +contract MockPluginSetupProcessorMulti { + using ERC165Checker for address; + + /// @notice The ID of the permission required to call the `applyInstallation` function. + bytes32 public constant APPLY_INSTALLATION_PERMISSION_ID = + keccak256("APPLY_INSTALLATION_PERMISSION"); + + /// @notice The ID of the permission required to call the `applyUpdate` function. + bytes32 public constant APPLY_UPDATE_PERMISSION_ID = keccak256("APPLY_UPDATE_PERMISSION"); + + /// @notice The ID of the permission required to call the `applyUninstallation` function. + bytes32 public constant APPLY_UNINSTALLATION_PERMISSION_ID = + keccak256("APPLY_UNINSTALLATION_PERMISSION"); + + /// @notice The hash obtained from the bytes-encoded empty array to be used for UI updates being required to submit an empty permission array. + /// @dev The hash is computed via `keccak256(abi.encode([]))`. + bytes32 private constant EMPTY_ARRAY_HASH = + 0x569e75fc77c1a856f6daaf9e69d8a9566ca34aa47f9133711ce065a571af0cfd; + + /// @notice The hash obtained from the bytes-encoded zero value. + /// @dev The hash is computed via `keccak256(abi.encode(0))`. + bytes32 private constant ZERO_BYTES_HASH = + 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563; + + /// @notice A struct containing information related to plugin setups that have been applied. + /// @param blockNumber The block number at which the `applyInstallation`, `applyUpdate` or `applyUninstallation` was executed. + /// @param currentAppliedSetupId The current setup id that plugin holds. Needed to confirm that `prepareUpdate` or `prepareUninstallation` happens for the plugin's current/valid dependencies. + /// @param preparedSetupIdToBlockNumber The mapping between prepared setup IDs and block numbers at which `prepareInstallation`, `prepareUpdate` or `prepareUninstallation` was executed. + struct PluginState { + uint256 blockNumber; + bytes32 currentAppliedSetupId; + mapping(bytes32 => uint256) preparedSetupIdToBlockNumber; + } + + /// @notice A mapping between the plugin installation ID (obtained from the DAO and plugin address) and the plugin state information. + /// @dev This variable is public on purpose to allow future versions to access and migrate the storage. + mapping(bytes32 => PluginState) public states; + + /// @notice The struct containing the parameters for the `prepareInstallation` function. + /// @param pluginSetupRef The reference to the plugin setup to be used for the installation. + /// @param data The bytes-encoded data containing the input parameters for the installation preparation as specified in the corresponding ABI on the version's metadata. + struct PrepareInstallationParams { + PluginSetupRef pluginSetupRef; + bytes data; + } + + /// @notice The struct containing the parameters for the `applyInstallation` function. + /// @param pluginSetupRef The reference to the plugin setup used for the installation. + /// @param plugin The address of the plugin contract to be installed. + /// @param permissions The array of multi-targeted permission operations to be applied by the `PluginSetupProcessor` to the DAO. + /// @param helpersHash The hash of helpers that were deployed in `prepareInstallation`. This helps to derive the setup ID. + struct ApplyInstallationParams { + PluginSetupRef pluginSetupRef; + address plugin; + PermissionLib.MultiTargetPermission[] permissions; + bytes32 helpersHash; + } + + /// @notice The struct containing the parameters for the `prepareUpdate` function. + /// @param currentVersionTag The tag of the current plugin version to update from. + /// @param newVersionTag The tag of the new plugin version to update to. + /// @param pluginSetupRepo The plugin setup repository address on which the plugin exists. + /// @param setupPayload The payload containing the plugin and helper contract addresses deployed in a preparation step as well as optional data to be consumed by the plugin setup. + /// This includes the bytes-encoded data containing the input parameters for the update preparation as specified in the corresponding ABI on the version's metadata. + struct PrepareUpdateParams { + PluginRepo.Tag currentVersionTag; + PluginRepo.Tag newVersionTag; + PluginRepo pluginSetupRepo; + IPluginSetup.SetupPayload setupPayload; + } + + /// @notice The struct containing the parameters for the `applyUpdate` function. + /// @param plugin The address of the plugin contract to be updated. + /// @param pluginSetupRef The reference to the plugin setup used for the update. + /// @param initData The encoded data (function selector and arguments) to be provided to `upgradeToAndCall`. + /// @param permissions The array of multi-targeted permission operations to be applied by the `PluginSetupProcessor` to the DAO. + /// @param helpersHash The hash of helpers that were deployed in `prepareUpdate`. This helps to derive the setup ID. + struct ApplyUpdateParams { + address plugin; + PluginSetupRef pluginSetupRef; + bytes initData; + PermissionLib.MultiTargetPermission[] permissions; + bytes32 helpersHash; + } + + /// @notice The struct containing the parameters for the `prepareUninstallation` function. + /// @param pluginSetupRef The reference to the plugin setup to be used for the uninstallation. + /// @param setupPayload The payload containing the plugin and helper contract addresses deployed in a preparation step as well as optional data to be consumed by the plugin setup. + /// This includes the bytes-encoded data containing the input parameters for the uninstallation preparation as specified in the corresponding ABI on the version's metadata. + struct PrepareUninstallationParams { + PluginSetupRef pluginSetupRef; + IPluginSetup.SetupPayload setupPayload; + } + + /// @notice The struct containing the parameters for the `applyInstallation` function. + /// @param plugin The address of the plugin contract to be uninstalled. + /// @param pluginSetupRef The reference to the plugin setup used for the uninstallation. + /// @param permissions The array of multi-targeted permission operations to be applied by the `PluginSetupProcess. + struct ApplyUninstallationParams { + address plugin; + PluginSetupRef pluginSetupRef; + PermissionLib.MultiTargetPermission[] permissions; + } + + /// @notice The plugin repo registry listing the `PluginRepo` contracts versioning the `PluginSetup` contracts. + // PluginRepoRegistry public repoRegistry; + + /// @notice Thrown if a setup is unauthorized and cannot be applied because of a missing permission of the associated DAO. + /// @param dao The address of the DAO to which the plugin belongs. + /// @param caller The address (EOA or contract) that requested the application of a setup on the associated DAO. + /// @param permissionId The permission identifier. + /// @dev This is thrown if the `APPLY_INSTALLATION_PERMISSION_ID`, `APPLY_UPDATE_PERMISSION_ID`, or APPLY_UNINSTALLATION_PERMISSION_ID is missing. + error SetupApplicationUnauthorized(address dao, address caller, bytes32 permissionId); + + /// @notice Thrown if a plugin is not upgradeable. + /// @param plugin The address of the plugin contract. + error PluginNonupgradeable(address plugin); + + /// @notice Thrown if the upgrade of an `UUPSUpgradeable` proxy contract (see [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822)) failed. + /// @param proxy The address of the proxy. + /// @param implementation The address of the implementation contract. + /// @param initData The initialization data to be passed to the upgradeable plugin contract via `upgradeToAndCall`. + error PluginProxyUpgradeFailed(address proxy, address implementation, bytes initData); + + /// @notice Thrown if a contract does not support the `IPlugin` interface. + /// @param plugin The address of the contract. + error IPluginNotSupported(address plugin); + + /// @notice Thrown if a plugin repository does not exist on the plugin repo registry. + error PluginRepoNonexistent(); + + /// @notice Thrown if a plugin setup was already prepared indicated by the prepared setup ID. + /// @param preparedSetupId The prepared setup ID. + error SetupAlreadyPrepared(bytes32 preparedSetupId); + + /// @notice Thrown if a prepared setup ID is not eligible to be applied. This can happen if another setup has been already applied or if the setup wasn't prepared in the first place. + /// @param preparedSetupId The prepared setup ID. + error SetupNotApplicable(bytes32 preparedSetupId); + + /// @notice Thrown if the update version is invalid. + /// @param currentVersionTag The tag of the current version to update from. + /// @param newVersionTag The tag of the new version to update to. + error InvalidUpdateVersion(PluginRepo.Tag currentVersionTag, PluginRepo.Tag newVersionTag); + + /// @notice Thrown if plugin is already installed and one tries to prepare or apply install on it. + error PluginAlreadyInstalled(); + + /// @notice Thrown if the applied setup ID resulting from the supplied setup payload does not match with the current applied setup ID. + /// @param currentAppliedSetupId The current applied setup ID with which the data in the supplied payload must match. + /// @param appliedSetupId The applied setup ID obtained from the data in the supplied setup payload. + error InvalidAppliedSetupId(bytes32 currentAppliedSetupId, bytes32 appliedSetupId); + + /// @notice Emitted with a prepared plugin installation to store data relevant for the application step. + /// @param sender The sender that prepared the plugin installation. + /// @param dao The address of the DAO to which the plugin belongs. + /// @param preparedSetupId The prepared setup ID obtained from the supplied data. + /// @param pluginSetupRepo The repository storing the `PluginSetup` contracts of all versions of a plugin. + /// @param versionTag The version tag of the plugin setup of the prepared installation. + /// @param data The bytes-encoded data containing the input parameters for the preparation as specified in the corresponding ABI on the version's metadata. + /// @param plugin The address of the plugin contract. + /// @param preparedSetupData The deployed plugin's relevant data which consists of helpers and permissions. + event InstallationPrepared( + address indexed sender, + address indexed dao, + bytes32 preparedSetupId, + PluginRepo indexed pluginSetupRepo, + PluginRepo.Tag versionTag, + bytes data, + address plugin, + IPluginSetup.PreparedSetupData preparedSetupData + ); + + /// @notice Emitted after a plugin installation was applied. + /// @param dao The address of the DAO to which the plugin belongs. + /// @param plugin The address of the plugin contract. + /// @param preparedSetupId The prepared setup ID. + /// @param appliedSetupId The applied setup ID. + event InstallationApplied( + address indexed dao, + address indexed plugin, + bytes32 preparedSetupId, + bytes32 appliedSetupId + ); + + /// @notice Emitted with a prepared plugin update to store data relevant for the application step. + /// @param sender The sender that prepared the plugin update. + /// @param dao The address of the DAO to which the plugin belongs. + /// @param preparedSetupId The prepared setup ID. + /// @param pluginSetupRepo The repository storing the `PluginSetup` contracts of all versions of a plugin. + /// @param versionTag The version tag of the plugin setup of the prepared update. + /// @param setupPayload The payload containing the plugin and helper contract addresses deployed in a preparation step as well as optional data to be consumed by the plugin setup. + /// @param preparedSetupData The deployed plugin's relevant data which consists of helpers and permissions. + /// @param initData The initialization data to be passed to the upgradeable plugin contract. + event UpdatePrepared( + address indexed sender, + address indexed dao, + bytes32 preparedSetupId, + PluginRepo indexed pluginSetupRepo, + PluginRepo.Tag versionTag, + IPluginSetup.SetupPayload setupPayload, + IPluginSetup.PreparedSetupData preparedSetupData, + bytes initData + ); + + /// @notice Emitted after a plugin update was applied. + /// @param dao The address of the DAO to which the plugin belongs. + /// @param plugin The address of the plugin contract. + /// @param preparedSetupId The prepared setup ID. + /// @param appliedSetupId The applied setup ID. + event UpdateApplied( + address indexed dao, + address indexed plugin, + bytes32 preparedSetupId, + bytes32 appliedSetupId + ); + + /// @notice Emitted with a prepared plugin uninstallation to store data relevant for the application step. + /// @param sender The sender that prepared the plugin uninstallation. + /// @param dao The address of the DAO to which the plugin belongs. + /// @param preparedSetupId The prepared setup ID. + /// @param pluginSetupRepo The repository storing the `PluginSetup` contracts of all versions of a plugin. + /// @param versionTag The version tag of the plugin to used for install preparation. + /// @param setupPayload The payload containing the plugin and helper contract addresses deployed in a preparation step as well as optional data to be consumed by the plugin setup. + /// @param permissions The list of multi-targeted permission operations to be applied to the installing DAO. + event UninstallationPrepared( + address indexed sender, + address indexed dao, + bytes32 preparedSetupId, + PluginRepo indexed pluginSetupRepo, + PluginRepo.Tag versionTag, + IPluginSetup.SetupPayload setupPayload, + PermissionLib.MultiTargetPermission[] permissions + ); + + /// @notice Emitted after a plugin installation was applied. + /// @param dao The address of the DAO to which the plugin belongs. + /// @param plugin The address of the plugin contract. + /// @param preparedSetupId The prepared setup ID. + event UninstallationApplied( + address indexed dao, + address indexed plugin, + bytes32 preparedSetupId + ); + + /// @notice A modifier to check if a caller has the permission to apply a prepared setup. + /// @param _dao The address of the DAO. + /// @param _permissionId The permission identifier. + modifier canApply(address _dao, bytes32 _permissionId) { + _canApply(_dao, _permissionId); + _; + } + + /// @notice Constructs the plugin setup processor by setting the associated plugin repo registry. + // / @param _repoRegistry The plugin repo registry contract. + constructor(address[] memory _setups) { + for (uint256 i = 0; i < _setups.length; i++) { + queueSetup(_setups[i]); + } + // repoRegistry = _repoRegistry; + } + + address[] setups; + + function queueSetup(address _setup) public { + setups.push(_setup); + } + + function popSetup() internal returns (address) { + require(setups.length > 0, "No setups queued"); + address _setup = setups[setups.length - 1]; + setups.pop(); + return _setup; + } + + /// @notice Prepares the installation of a plugin. + /// @param _dao The address of the installing DAO. + /// @param _params The struct containing the parameters for the `prepareInstallation` function. + /// @return plugin The prepared plugin contract address. + /// @return preparedSetupData The data struct containing the array of helper contracts and permissions that the setup has prepared. + function prepareInstallation( + address _dao, + PrepareInstallationParams calldata _params + ) external returns (address plugin, IPluginSetup.PreparedSetupData memory preparedSetupData) { + // PluginRepo pluginSetupRepo = _params.pluginSetupRef.pluginSetupRepo; + + // // Check that the plugin repository exists on the plugin repo registry. + // if (!repoRegistry.entries(address(pluginSetupRepo))) { + // revert PluginRepoNonexistent(); + // } + + // // reverts if not found + // PluginRepo.Version memory version = pluginSetupRepo.getVersion( + // _params.pluginSetupRef.versionTag + // ); + + // Prepare the installation + address setup = popSetup(); + (plugin, preparedSetupData) = PluginSetup(setup).prepareInstallation(_dao, _params.data); + + // bytes32 pluginInstallationId = _getPluginInstallationId(_dao, plugin); + + // bytes32 preparedSetupId = _getPreparedSetupId( + // _params.pluginSetupRef, + // hashPermissions(preparedSetupData.permissions), + // hashHelpers(preparedSetupData.helpers), + // bytes(""), + // PreparationType.Installation + // ); + + // PluginState storage pluginState = states[pluginInstallationId]; + + // // Check if this plugin is already installed. + // if (pluginState.currentAppliedSetupId != bytes32(0)) { + // revert PluginAlreadyInstalled(); + // } + + // // Check if this setup has already been prepared before and is pending. + // if (pluginState.blockNumber < pluginState.preparedSetupIdToBlockNumber[preparedSetupId]) { + // revert SetupAlreadyPrepared({preparedSetupId: preparedSetupId}); + // } + + // pluginState.preparedSetupIdToBlockNumber[preparedSetupId] = block.number; + + // emit InstallationPrepared({ + // sender: msg.sender, + // dao: _dao, + // preparedSetupId: preparedSetupId, + // pluginSetupRepo: pluginSetupRepo, + // versionTag: _params.pluginSetupRef.versionTag, + // data: _params.data, + // plugin: plugin, + // preparedSetupData: preparedSetupData + // }); + + return (plugin, preparedSetupData); + } + + /// @notice Applies the permissions of a prepared installation to a DAO. + /// @param _dao The address of the installing DAO. + /// @param _params The struct containing the parameters for the `applyInstallation` function. + function applyInstallation( + address _dao, + ApplyInstallationParams calldata _params + ) external canApply(_dao, APPLY_INSTALLATION_PERMISSION_ID) { + // bytes32 pluginInstallationId = _getPluginInstallationId(_dao, _params.plugin); + + // PluginState storage pluginState = states[pluginInstallationId]; + + // bytes32 preparedSetupId = _getPreparedSetupId( + // _params.pluginSetupRef, + // hashPermissions(_params.permissions), + // _params.helpersHash, + // bytes(""), + // PreparationType.Installation + // ); + + // // Check if this plugin is already installed. + // if (pluginState.currentAppliedSetupId != bytes32(0)) { + // revert PluginAlreadyInstalled(); + // } + + // validatePreparedSetupId(pluginInstallationId, preparedSetupId); + + // bytes32 appliedSetupId = _getAppliedSetupId(_params.pluginSetupRef, _params.helpersHash); + + // pluginState.currentAppliedSetupId = appliedSetupId; + // pluginState.blockNumber = block.number; + + // Process the permissions, which requires the `ROOT_PERMISSION_ID` from the installing DAO. + if (_params.permissions.length > 0) { + DAO(payable(_dao)).applyMultiTargetPermissions(_params.permissions); + } + + // emit InstallationApplied({ + // dao: _dao, + // plugin: _params.plugin, + // preparedSetupId: preparedSetupId, + // appliedSetupId: appliedSetupId + // }); + } + + /// @notice Prepares the update of an UUPS upgradeable plugin. + /// @param _dao The address of the DAO For which preparation of update happens. + /// @param _params The struct containing the parameters for the `prepareUpdate` function. + /// @return initData The initialization data to be passed to upgradeable contracts when the update is applied + /// @return preparedSetupData The data struct containing the array of helper contracts and permissions that the setup has prepared. + /// @dev The list of `_params.setupPayload.currentHelpers` has to be specified in the same order as they were returned from previous setups preparation steps (the latest `prepareInstallation` or `prepareUpdate` step that has happend) on which the update is prepared for. + function prepareUpdate( + address _dao, + PrepareUpdateParams calldata _params + ) + external + returns (bytes memory initData, IPluginSetup.PreparedSetupData memory preparedSetupData) + { + if ( + _params.currentVersionTag.release != _params.newVersionTag.release || + _params.currentVersionTag.build >= _params.newVersionTag.build + ) { + revert InvalidUpdateVersion({ + currentVersionTag: _params.currentVersionTag, + newVersionTag: _params.newVersionTag + }); + } + + bytes32 pluginInstallationId = _getPluginInstallationId(_dao, _params.setupPayload.plugin); + + PluginState storage pluginState = states[pluginInstallationId]; + + bytes32 currentHelpersHash = hashHelpers(_params.setupPayload.currentHelpers); + + bytes32 appliedSetupId = _getAppliedSetupId( + PluginSetupRef(_params.currentVersionTag, _params.pluginSetupRepo), + currentHelpersHash + ); + + // The following check implicitly confirms that plugin is currently installed. + // Otherwise, `currentAppliedSetupId` would not be set. + if (pluginState.currentAppliedSetupId != appliedSetupId) { + revert InvalidAppliedSetupId({ + currentAppliedSetupId: pluginState.currentAppliedSetupId, + appliedSetupId: appliedSetupId + }); + } + + PluginRepo.Version memory currentVersion = _params.pluginSetupRepo.getVersion( + _params.currentVersionTag + ); + + PluginRepo.Version memory newVersion = _params.pluginSetupRepo.getVersion( + _params.newVersionTag + ); + + bytes32 preparedSetupId; + + // If the current and new plugin setup are identical, this is an UI update. + // In this case, the permission hash is set to the empty array hash and the `prepareUpdate` call is skipped to avoid side effects. + if (currentVersion.pluginSetup == newVersion.pluginSetup) { + preparedSetupId = _getPreparedSetupId( + PluginSetupRef(_params.newVersionTag, _params.pluginSetupRepo), + EMPTY_ARRAY_HASH, + currentHelpersHash, + bytes(""), + PreparationType.Update + ); + + // Because UI updates do not change the plugin functionality, the array of helpers + // associated with this plugin version `preparedSetupData.helpers` and being returned must + // equal `_params.setupPayload.currentHelpers` returned by the previous setup step (installation or update ) + // that this update is transitioning from. + preparedSetupData.helpers = _params.setupPayload.currentHelpers; + } else { + // Check that plugin is `PluginUUPSUpgradable`. + if (!_params.setupPayload.plugin.supportsInterface(type(IPlugin).interfaceId)) { + revert IPluginNotSupported({plugin: _params.setupPayload.plugin}); + } + if (IPlugin(_params.setupPayload.plugin).pluginType() != IPlugin.PluginType.UUPS) { + revert PluginNonupgradeable({plugin: _params.setupPayload.plugin}); + } + + // Prepare the update. + (initData, preparedSetupData) = PluginSetup(newVersion.pluginSetup).prepareUpdate( + _dao, + _params.currentVersionTag.build, + _params.setupPayload + ); + + preparedSetupId = _getPreparedSetupId( + PluginSetupRef(_params.newVersionTag, _params.pluginSetupRepo), + hashPermissions(preparedSetupData.permissions), + hashHelpers(preparedSetupData.helpers), + initData, + PreparationType.Update + ); + } + + // Check if this setup has already been prepared before and is pending. + if (pluginState.blockNumber < pluginState.preparedSetupIdToBlockNumber[preparedSetupId]) { + revert SetupAlreadyPrepared({preparedSetupId: preparedSetupId}); + } + + pluginState.preparedSetupIdToBlockNumber[preparedSetupId] = block.number; + + // Avoid stack too deep. + emitPrepareUpdateEvent(_dao, preparedSetupId, _params, preparedSetupData, initData); + + return (initData, preparedSetupData); + } + + /// @notice Applies the permissions of a prepared update of an UUPS upgradeable proxy contract to a DAO. + /// @param _dao The address of the updating DAO. + /// @param _params The struct containing the parameters for the `applyInstallation` function. + function applyUpdate( + address _dao, + ApplyUpdateParams calldata _params + ) external canApply(_dao, APPLY_UPDATE_PERMISSION_ID) { + bytes32 pluginInstallationId = _getPluginInstallationId(_dao, _params.plugin); + + PluginState storage pluginState = states[pluginInstallationId]; + + bytes32 preparedSetupId = _getPreparedSetupId( + _params.pluginSetupRef, + hashPermissions(_params.permissions), + _params.helpersHash, + _params.initData, + PreparationType.Update + ); + + validatePreparedSetupId(pluginInstallationId, preparedSetupId); + + bytes32 appliedSetupId = _getAppliedSetupId(_params.pluginSetupRef, _params.helpersHash); + + pluginState.blockNumber = block.number; + pluginState.currentAppliedSetupId = appliedSetupId; + + PluginRepo.Version memory version = _params.pluginSetupRef.pluginSetupRepo.getVersion( + _params.pluginSetupRef.versionTag + ); + + address currentImplementation = PluginUUPSUpgradeable(_params.plugin).implementation(); + address newImplementation = PluginSetup(version.pluginSetup).implementation(); + + if (currentImplementation != newImplementation) { + _upgradeProxy(_params.plugin, newImplementation, _params.initData); + } + + // Process the permissions, which requires the `ROOT_PERMISSION_ID` from the updating DAO. + if (_params.permissions.length > 0) { + DAO(payable(_dao)).applyMultiTargetPermissions(_params.permissions); + } + + emit UpdateApplied({ + dao: _dao, + plugin: _params.plugin, + preparedSetupId: preparedSetupId, + appliedSetupId: appliedSetupId + }); + } + + /// @notice Prepares the uninstallation of a plugin. + /// @param _dao The address of the uninstalling DAO. + /// @param _params The struct containing the parameters for the `prepareUninstallation` function. + /// @return permissions The list of multi-targeted permission operations to be applied to the uninstalling DAO. + /// @dev The list of `_params.setupPayload.currentHelpers` has to be specified in the same order as they were returned from previous setups preparation steps (the latest `prepareInstallation` or `prepareUpdate` step that has happend) on which the uninstallation was prepared for. + function prepareUninstallation( + address _dao, + PrepareUninstallationParams calldata _params + ) external returns (PermissionLib.MultiTargetPermission[] memory permissions) { + // bytes32 pluginInstallationId = _getPluginInstallationId(_dao, _params.setupPayload.plugin); + + // PluginState storage pluginState = states[pluginInstallationId]; + + // bytes32 appliedSetupId = _getAppliedSetupId( + // _params.pluginSetupRef, + // hashHelpers(_params.setupPayload.currentHelpers) + // ); + + // if (pluginState.currentAppliedSetupId != appliedSetupId) { + // revert InvalidAppliedSetupId({ + // currentAppliedSetupId: pluginState.currentAppliedSetupId, + // appliedSetupId: appliedSetupId + // }); + // } + + // PluginRepo.Version memory version = _params.pluginSetupRef.pluginSetupRepo.getVersion( + // _params.pluginSetupRef.versionTag + // ); + + permissions = PluginSetup(popSetup()).prepareUninstallation(_dao, _params.setupPayload); + + // bytes32 preparedSetupId = _getPreparedSetupId( + // _params.pluginSetupRef, + // hashPermissions(permissions), + // ZERO_BYTES_HASH, + // bytes(""), + // PreparationType.Uninstallation + // ); + + // // Check if this setup has already been prepared before and is pending. + // if (pluginState.blockNumber < pluginState.preparedSetupIdToBlockNumber[preparedSetupId]) { + // revert SetupAlreadyPrepared({preparedSetupId: preparedSetupId}); + // } + + // pluginState.preparedSetupIdToBlockNumber[preparedSetupId] = block.number; + + // emit UninstallationPrepared({ + // sender: msg.sender, + // dao: _dao, + // preparedSetupId: preparedSetupId, + // pluginSetupRepo: _params.pluginSetupRef.pluginSetupRepo, + // versionTag: _params.pluginSetupRef.versionTag, + // setupPayload: _params.setupPayload, + // permissions: permissions + // }); + } + + /// @notice Applies the permissions of a prepared uninstallation to a DAO. + /// @param _dao The address of the DAO. + /// @param _dao The address of the uninstalling DAO. + /// @param _params The struct containing the parameters for the `applyUninstallation` function. + /// @dev The list of `_params.setupPayload.currentHelpers` has to be specified in the same order as they were returned from previous setups preparation steps (the latest `prepareInstallation` or `prepareUpdate` step that has happend) on which the uninstallation was prepared for. + function applyUninstallation( + address _dao, + ApplyUninstallationParams calldata _params + ) external canApply(_dao, APPLY_UNINSTALLATION_PERMISSION_ID) { + // bytes32 pluginInstallationId = _getPluginInstallationId(_dao, _params.plugin); + + // PluginState storage pluginState = states[pluginInstallationId]; + + // bytes32 preparedSetupId = _getPreparedSetupId( + // _params.pluginSetupRef, + // hashPermissions(_params.permissions), + // ZERO_BYTES_HASH, + // bytes(""), + // PreparationType.Uninstallation + // ); + + // validatePreparedSetupId(pluginInstallationId, preparedSetupId); + + // // Since the plugin is uninstalled, only the current block number must be updated. + // pluginState.blockNumber = block.number; + // pluginState.currentAppliedSetupId = bytes32(0); + + // Process the permissions, which requires the `ROOT_PERMISSION_ID` from the uninstalling DAO. + if (_params.permissions.length > 0) { + DAO(payable(_dao)).applyMultiTargetPermissions(_params.permissions); + } + + // emit UninstallationApplied({ + // dao: _dao, + // plugin: _params.plugin, + // preparedSetupId: preparedSetupId + // }); + } + + /// @notice Validates that a setup ID can be applied for `applyInstallation`, `applyUpdate`, or `applyUninstallation`. + /// @param pluginInstallationId The plugin installation ID obtained from the hash of `abi.encode(daoAddress, pluginAddress)`. + /// @param preparedSetupId The prepared setup ID to be validated. + /// @dev If the block number stored in `states[pluginInstallationId].blockNumber` exceeds the one stored in `pluginState.preparedSetupIdToBlockNumber[preparedSetupId]`, the prepared setup with `preparedSetupId` is outdated and not applicable anymore. + function validatePreparedSetupId( + bytes32 pluginInstallationId, + bytes32 preparedSetupId + ) public view { + PluginState storage pluginState = states[pluginInstallationId]; + if (pluginState.blockNumber >= pluginState.preparedSetupIdToBlockNumber[preparedSetupId]) { + revert SetupNotApplicable({preparedSetupId: preparedSetupId}); + } + } + + /// @notice Upgrades a UUPS upgradeable proxy contract (see [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822)). + /// @param _proxy The address of the proxy. + /// @param _implementation The address of the implementation contract. + /// @param _initData The initialization data to be passed to the upgradeable plugin contract via `upgradeToAndCall`. + function _upgradeProxy( + address _proxy, + address _implementation, + bytes memory _initData + ) private { + if (_initData.length > 0) { + try + PluginUUPSUpgradeable(_proxy).upgradeToAndCall(_implementation, _initData) + {} catch Error(string memory reason) { + revert(reason); + } catch (bytes memory) /*lowLevelData*/ { + revert PluginProxyUpgradeFailed({ + proxy: _proxy, + implementation: _implementation, + initData: _initData + }); + } + } else { + try PluginUUPSUpgradeable(_proxy).upgradeTo(_implementation) {} catch Error( + string memory reason + ) { + revert(reason); + } catch (bytes memory) /*lowLevelData*/ { + revert PluginProxyUpgradeFailed({ + proxy: _proxy, + implementation: _implementation, + initData: _initData + }); + } + } + } + + /// @notice Checks if a caller can apply a setup. The caller can be either the DAO to which the plugin setup is applied to or another account to which the DAO has granted the respective permission. + /// @param _dao The address of the applying DAO. + /// @param _permissionId The permission ID. + function _canApply(address _dao, bytes32 _permissionId) private view { + if ( + msg.sender != _dao && + !DAO(payable(_dao)).hasPermission(address(this), msg.sender, _permissionId, bytes("")) + ) { + revert SetupApplicationUnauthorized({ + dao: _dao, + caller: msg.sender, + permissionId: _permissionId + }); + } + } + + /// @notice A helper to emit the `UpdatePrepared` event from the supplied, structured data. + /// @param _dao The address of the updating DAO. + /// @param _preparedSetupId The prepared setup ID. + /// @param _params The struct containing the parameters for the `prepareUpdate` function. + /// @param _preparedSetupData The deployed plugin's relevant data which consists of helpers and permissions. + /// @param _initData The initialization data to be passed to upgradeable contracts when the update is applied + /// @dev This functions exists to avoid stack-too-deep errors. + function emitPrepareUpdateEvent( + address _dao, + bytes32 _preparedSetupId, + PrepareUpdateParams calldata _params, + IPluginSetup.PreparedSetupData memory _preparedSetupData, + bytes memory _initData + ) private { + emit UpdatePrepared({ + sender: msg.sender, + dao: _dao, + preparedSetupId: _preparedSetupId, + pluginSetupRepo: _params.pluginSetupRepo, + versionTag: _params.newVersionTag, + setupPayload: _params.setupPayload, + preparedSetupData: _preparedSetupData, + initData: _initData + }); + } +} diff --git a/test/mocks/osx/MockPluginRepoRegistry.sol b/test/mocks/osx/MockPluginRepoRegistry.sol new file mode 100644 index 0000000..5fa8a89 --- /dev/null +++ b/test/mocks/osx/MockPluginRepoRegistry.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity 0.8.17; + +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {InterfaceBasedRegistry} from "@aragon/osx/test/utils/InterfaceBasedRegistryMock.sol"; +import {IPluginRepo} from "@aragon/osx/framework/plugin/repo/IPluginRepo.sol"; + +/// @title MockPluginRepoRegistry +/// @author Aragon Association - 2022-2023 +/// @notice This contract maintains an address-based registry of plugin repositories in the Aragon App DAO framework. +contract MockPluginRepoRegistry is InterfaceBasedRegistry { + /// @notice The ID of the permission required to call the `register` function. + // bytes32 public constant REGISTER_PLUGIN_REPO_PERMISSION_ID = keccak256("REGISTER_PLUGIN_REPO_PERMISSION"); + + /// @notice Emitted if a new plugin repository is registered. + /// @param subdomain The subdomain of the plugin repository. + /// @param pluginRepo The address of the plugin repository. + event PluginRepoRegistered(string subdomain, address pluginRepo); + + // /// @notice Thrown if the plugin subdomain doesn't match the regex `[0-9a-z\-]` + // error InvalidPluginSubdomain(string subdomain); + + // /// @notice Thrown if the plugin repository subdomain is empty. + // error EmptyPluginRepoSubdomain(); + + /// @dev Used to disallow initializing the implementation contract by an attacker for extra safety. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract by setting calling the `InterfaceBasedRegistry` base class initialize method. + /// @param _dao The address of the managing DAO. + function initialize(IDAO _dao) external initializer { + bytes4 pluginRepoInterfaceId = type(IPluginRepo).interfaceId; + __InterfaceBasedRegistry_init(_dao, pluginRepoInterfaceId); + + // subdomainRegistrar = _subdomainRegistrar; + } + + /// @notice Registers a plugin repository with a subdomain and address. + /// @param subdomain The subdomain of the PluginRepo. + /// @param pluginRepo The address of the PluginRepo contract. + function registerPluginRepo( + string calldata subdomain, + address pluginRepo + ) external // auth(REGISTER_PLUGIN_REPO_PERMISSION_ID) + { + // if (!(bytes(subdomain).length > 0)) { + // revert EmptyPluginRepoSubdomain(); + // } + // if (!isSubdomainValid(subdomain)) { + // revert InvalidPluginSubdomain({subdomain: subdomain}); + // } + // bytes32 labelhash = keccak256(bytes(subdomain)); + // subdomainRegistrar.registerSubnode(labelhash, pluginRepo); + // _register(pluginRepo); + // emit PluginRepoRegistered(subdomain, pluginRepo); + } + + /// @notice This empty reserved space is put in place to allow future versions to add new variables without shifting down storage in the inheritance chain (see [OpenZeppelin's guide about storage gaps](https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps)). + uint256[50] private __gap; +} diff --git a/test/voting/GaugeTime.t.sol b/test/voting/GaugeTime.t.sol index 1213047..7962d88 100644 --- a/test/voting/GaugeTime.t.sol +++ b/test/voting/GaugeTime.t.sol @@ -36,52 +36,63 @@ contract TestGaugeTime is GaugeVotingBase { uint start = block.timestamp; // start + assertEq(clock.elapsedInEpoch(), 0); assertEq(voter.epochId(), i); assertEq(voter.epochStart(), block.timestamp); assertEq(voter.epochVoteStart(), block.timestamp + 1 hours); assertEq(voter.epochVoteEnd(), block.timestamp + 1 weeks - 1 hours); assertEq(voter.votingActive(), false); assertEq(nextDeposit(), block.timestamp); + assertEq(clock.epochStartsIn(), 0); // +1hr: voting starts vm.warp(start + 1 hours); + assertEq(clock.elapsedInEpoch(), 1 hours); assertEq(voter.epochId(), i); assertEq(voter.epochStart(), block.timestamp + 2 weeks - 1 hours); assertEq(voter.epochVoteStart(), block.timestamp); assertEq(voter.epochVoteEnd(), block.timestamp + 1 weeks - 2 hours); assertEq(voter.votingActive(), true); assertEq(nextDeposit(), block.timestamp + 1 weeks - 1 hours); + assertEq(clock.epochStartsIn(), 2 weeks - 1 hours); // +1 week - 1 hours: voting ends vm.warp(start + 1 weeks - 1 hours); + assertEq(clock.elapsedInEpoch(), 1 weeks - 1 hours); assertEq(voter.epochId(), i); assertEq(voter.epochStart(), block.timestamp + 1 weeks + 1 hours); assertEq(voter.epochVoteStart(), block.timestamp + 1 weeks + 2 hours); assertEq(voter.epochVoteEnd(), block.timestamp); assertEq(voter.votingActive(), false); assertEq(nextDeposit(), block.timestamp + 1 hours); + assertEq(clock.epochStartsIn(), 1 weeks + 1 hours); // +1 week: next deposit opens vm.warp(start + 1 weeks); + + assertEq(clock.elapsedInEpoch(), 1 weeks); assertEq(voter.epochId(), i); assertEq(voter.epochStart(), block.timestamp + 1 weeks); assertEq(voter.epochVoteStart(), block.timestamp + 1 weeks + 1 hours); assertEq(voter.epochVoteEnd(), block.timestamp); assertEq(voter.votingActive(), false); assertEq(nextDeposit(), block.timestamp); + assertEq(clock.epochStartsIn(), 1 weeks); // whole of next week calculates correctly vm.warp(start + 1 weeks + 3 days); + assertEq(clock.elapsedInEpoch(), 1 weeks + 3 days); assertEq(voter.epochId(), i); assertEq(voter.epochStart(), block.timestamp + 4 days); assertEq(voter.epochVoteStart(), block.timestamp + 4 days + 1 hours); assertEq(voter.epochVoteEnd(), block.timestamp); assertEq(voter.votingActive(), false); assertEq(nextDeposit(), block.timestamp + 4 days); + assertEq(clock.epochStartsIn(), 4 days); // +1 week + 2 hours: next epoch starts vm.warp(start + 2 weeks); diff --git a/test/voting/GaugeVote.t.sol b/test/voting/GaugeVote.t.sol index 96ff83a..e3fdacc 100644 --- a/test/voting/GaugeVote.t.sol +++ b/test/voting/GaugeVote.t.sol @@ -377,7 +377,7 @@ contract TestGaugeVote is GaugeVotingBase { uint256[] memory tokens = escrow.ownedTokens(owner); vm.prank(owner); - escrow.setApprovalForAll(address(voter), true); + nftLock.setApprovalForAll(address(voter), true); uint vp0 = escrow.votingPower(tokens[0]); uint vp1 = escrow.votingPower(tokens[1]); diff --git a/test/voting/GaugeVotingBase.sol b/test/voting/GaugeVotingBase.sol index 17ef57f..e6f4e1a 100644 --- a/test/voting/GaugeVotingBase.sol +++ b/test/voting/GaugeVotingBase.sol @@ -21,7 +21,7 @@ import {ISimpleGaugeVoterStorageEventsErrors} from "src/voting/ISimpleGaugeVoter import {IEscrowCurveUserStorage} from "@escrow-interfaces/IEscrowCurveIncreasing.sol"; import {IWithdrawalQueueErrors} from "src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol"; import {IGaugeVote} from "src/voting/ISimpleGaugeVoter.sol"; -import {VotingEscrow, QuadraticIncreasingEscrow, ExitQueue, SimpleGaugeVoter, SimpleGaugeVoterSetup, ISimpleGaugeVoterSetupParams} from "src/voting/SimpleGaugeVoterSetup.sol"; +import {VotingEscrow, Lock, QuadraticIncreasingEscrow, ExitQueue, SimpleGaugeVoter, SimpleGaugeVoterSetup, ISimpleGaugeVoterSetupParams} from "src/voting/SimpleGaugeVoterSetup.sol"; contract GaugeVotingBase is Test, @@ -41,6 +41,7 @@ contract GaugeVotingBase is MockDAOFactory daoFactory; MockERC20 token; + Lock nftLock; VotingEscrow escrow; QuadraticIncreasingEscrow curve; SimpleGaugeVoter voter; @@ -113,7 +114,8 @@ contract GaugeVotingBase is address(new QuadraticIncreasingEscrow()), address(new ExitQueue()), address(new VotingEscrow()), - address(new Clock()) + address(new Clock()), + address(new Lock()) ); // push to the PSP @@ -142,6 +144,7 @@ contract GaugeVotingBase is queue = ExitQueue(helpers[1]); escrow = VotingEscrow(helpers[2]); clock = Clock(helpers[3]); + nftLock = Lock(helpers[4]); // set the permissions for (uint i = 0; i < preparedSetupData.permissions.length; i++) { @@ -150,7 +153,7 @@ contract GaugeVotingBase is } function _actions() internal view returns (IDAO.Action[] memory) { - IDAO.Action[] memory actions = new IDAO.Action[](8); + IDAO.Action[] memory actions = new IDAO.Action[](9); // action 0: apply the ve installation actions[0] = IDAO.Action({ @@ -183,8 +186,15 @@ contract GaugeVotingBase is data: abi.encodeWithSelector(escrow.setVoter.selector, address(voter)) }); - // for testing, give this contract the admin roles on all the periphery contracts + // action 5: set the nft lock actions[4] = IDAO.Action({ + to: address(escrow), + value: 0, + data: abi.encodeWithSelector(escrow.setLockNFT.selector, address(nftLock)) + }); + + // for testing, give this contract the admin roles on all the periphery contracts + actions[5] = IDAO.Action({ to: address(dao), value: 0, data: abi.encodeCall( @@ -193,7 +203,7 @@ contract GaugeVotingBase is ) }); - actions[5] = IDAO.Action({ + actions[6] = IDAO.Action({ to: address(dao), value: 0, data: abi.encodeCall( @@ -202,7 +212,7 @@ contract GaugeVotingBase is ) }); - actions[6] = IDAO.Action({ + actions[7] = IDAO.Action({ to: address(dao), value: 0, data: abi.encodeCall( @@ -211,7 +221,7 @@ contract GaugeVotingBase is ) }); - actions[7] = IDAO.Action({ + actions[8] = IDAO.Action({ to: address(dao), value: 0, data: abi.encodeCall( diff --git a/test/voting/Setup.t.sol b/test/voting/Setup.t.sol index dec0881..96dfad7 100644 --- a/test/voting/Setup.t.sol +++ b/test/voting/Setup.t.sol @@ -27,11 +27,12 @@ contract VoterSetupTest is GaugeVotingBase { error WrongHelpersArrayLength(uint256 length); function testUninstall() public { - address[] memory currentHelpers = new address[](4); + address[] memory currentHelpers = new address[](5); currentHelpers[0] = address(curve); currentHelpers[1] = address(queue); currentHelpers[2] = address(escrow); currentHelpers[3] = address(clock); + currentHelpers[4] = address(nftLock); IPluginSetup.SetupPayload memory payload = IPluginSetup.SetupPayload({ plugin: address(voter), @@ -93,6 +94,15 @@ contract VoterSetupTest is GaugeVotingBase { _data: "" }) ); + + assertFalse( + dao.hasPermission({ + _who: address(nftLock), + _where: address(dao), + _permissionId: nftLock.LOCK_ADMIN_ROLE(), + _data: "" + }) + ); } function testCantPassIncorrectHelpers() public { @@ -161,6 +171,13 @@ contract VoterSetupTest is GaugeVotingBase { // coverage autism function testConstructor() public { - new SimpleGaugeVoterSetup(address(0), address(0), address(0), address(0), address(0)); + new SimpleGaugeVoterSetup( + address(0), + address(0), + address(0), + address(0), + address(0), + address(0) + ); } }