diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a6b3c7..385afd2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,11 @@ -name: CI +name: Tests on: push: + branches: + - 'main' + - 'develop' + - 'deploy/**' pull_request: workflow_dispatch: @@ -9,32 +13,99 @@ env: FOUNDRY_PROFILE: ci jobs: - check: + test-upgrades: strategy: fail-fast: true + name: Test upgrades + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - uses: oven-sh/setup-bun@v2 + + - name: foundry-toolchain + uses: foundry-rs/foundry-toolchain@v1.3.1 - name: Foundry project + - name: Show Forge version + run: forge --version + + - name: Build + run: bun run build-all + + - name: Run tests + run: bun run test-upgrades + test-fork: + strategy: + fail-fast: true + name: Test fork runs-on: ubuntu-latest + env: + NETWORK: "sepolia" + FORK_TEST_MODE: "new-factory" + DEPLOY_AS_PRODUCTION: false + TOKEN_TEST_WHALE: "0x" + FACTORY_ADDRESS: "0x" + MULTISIG_MEMBERS_JSON_FILE_NAME: "/script/multisig-members.json" + MIN_APPROVALS: 1 + MULTISIG_PROPOSAL_EXPIRATION_PERIOD: "864000" + MINT_TEST_TOKENS: true + TOKEN1_ADDRESS: "0x0000000000000000000000000000000000000000" + VE_TOKEN1_NAME: "Voting Escrow Token 1" + VE_TOKEN1_SYMBOL: "veTK1" + TOKEN2_ADDRESS: "0x0000000000000000000000000000000000000000" + VE_TOKEN2_NAME: "Voting Escrow Token 2" + VE_TOKEN2_SYMBOL: "veTK2" + FEE_PERCENT: "0" # 10_000 = 100% + WARMUP_PERIOD: "259200" # 3 days + COOLDOWN_PERIOD: "259200" # 3 days + MIN_LOCK_DURATION: "1" + VOTING_PAUSED: true + MIN_DEPOSIT: "10000000000000000" # 0.01 ETH + MULTISIG_PLUGIN_REPO_ADDRESS: "0x9e7956C8758470dE159481e5DD0d08F8B59217A2" + MULTISIG_PLUGIN_RELEASE: "1" + MULTISIG_PLUGIN_BUILD: "2" + SIMPLE_GAUGE_VOTER_REPO_ENS_SUBDOMAIN: "gauge-voter-${{ vars.GITHUB_RUN_ID }}" + DAO_FACTORY: "0x7a62da7B56fB3bfCdF70E900787010Bc4c9Ca42e" + PLUGIN_SETUP_PROCESSOR: "0xC24188a73dc09aA7C721f96Ad8857B469C01dC9f" + PLUGIN_REPO_FACTORY: "0x07f49c49Ce2A99CF7C28F66673d406386BDD8Ff4" steps: - uses: actions/checkout@v4 with: - submodules: recursive + submodules: true + - uses: oven-sh/setup-bun@v2 + + - name: foundry-toolchain + uses: foundry-rs/foundry-toolchain@v1.3.1 - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + - name: Show Forge version + run: forge --version + + - name: Build + run: bun run build + + - name: Run tests + run: bun run test-fork + test-unit: + strategy: + fail-fast: true + name: Test unit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 with: - version: nightly + submodules: true + - uses: oven-sh/setup-bun@v2 + + - name: foundry-toolchain + uses: foundry-rs/foundry-toolchain@v1.3.1 - name: Show Forge version - run: | - forge --version - - - name: Run Forge build - run: | - forge build --sizes - id: build - - - name: Run Forge tests - run: | - forge test --no-match-path "test/fork/**/*.sol" -vvv - id: test + run: forge --version + + - name: Build + run: bun run build + + - name: Run tests + run: bun run test-unit + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 09d3b94..2816bf1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ # Foundry cache/ out/ +ref_builds/ broadcast/ # JS diff --git a/.gitmodules b/.gitmodules index 8b58daf..93307e0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,9 @@ branch = v4.9.2 [submodule "lib/ens-contracts"] path = lib/ens-contracts url = https://github.com/ensdomains/ens-contracts +[submodule "lib/openzeppelin-foundry-upgrades"] + path = lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "lib/ve-governance/v101"] + path = lib/ve-governance/v101 + url = https://github.com/aragon/ve-governance diff --git a/foundry.toml b/foundry.toml index ee3e61a..3501401 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,6 +18,11 @@ test = "test" verbosity = 3 evm_version = "shanghai" +ffi = true +ast = true +build_info = true +extra_output = ["storageLayout"] + [profile.ci] fuzz = { runs = 10_000 } verbosity = 4 diff --git a/lib/forge-std b/lib/forge-std index 1714bee..3b20d60 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d +Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades new file mode 160000 index 0000000..cbce1e0 --- /dev/null +++ b/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit cbce1e00305e943aa1661d43f41e5ac72c662b07 diff --git a/lib/ve-governance/v101 b/lib/ve-governance/v101 new file mode 160000 index 0000000..76a0ab5 --- /dev/null +++ b/lib/ve-governance/v101 @@ -0,0 +1 @@ +Subproject commit 76a0ab525ac4254123ebc7d018aea9e3edc06caa diff --git a/package.json b/package.json index 613f0ef..03161f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,18 @@ { + "name": "ve-governance", + "version": "1.0.0", "devDependencies": { "prettier": "^3.3.3", "prettier-plugin-solidity": "^1.4.1" + }, + "scripts": { + "build": "forge build", + "build-clean": "forge build --force", + "build-refs": "bash script/build-reference-builds.sh", + "build-all": "bun run build-refs && bun run build-clean", + "test-unit": "forge test --no-match-path \"{test/fork/*.sol,test/upgrades/*.sol}\" -vvv", + "test-fork": "forge test --match-path \"test/fork/**/*.sol\" --rpc-url https://sepolia.drpc.org -vvv", + "test-upgrades": "forge test --build-info --match-path \"test/upgrades/**/*.sol\" -vvv", + "test": "forge test -vvv" } } diff --git a/remappings.txt b/remappings.txt index 5c24c6f..5737b72 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,6 +5,7 @@ utils/=test/utils/ @aragon/osx/=lib/osx/packages/contracts/src/ @aragon/admin/=lib/osx/packages/contracts/src/plugins/governance/admin/ @aragon/multisig/=lib/osx/packages/contracts/src/plugins/governance/multisig/ +@aragon/ve-governance-v101=lib/ve-governance/v101/src/ @interfaces/=src/interfaces/ @mocks/=test/mocks/ @@ -25,5 +26,6 @@ ens-contracts/=lib/ens-contracts/contracts/ openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ openzeppelin-contracts/=lib/openzeppelin-contracts/ erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/ +openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src @solmate/=lib/solmate/src/ \ No newline at end of file diff --git a/script/build-reference-builds.sh b/script/build-reference-builds.sh new file mode 100644 index 0000000..98f6806 --- /dev/null +++ b/script/build-reference-builds.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +rm -rf ref_builds +mkdir -p ref_builds + +for file in lib/ve-governance/*; +do + echo "Processing $file" + filename=$(echo $file | sed 's/\//-/g') + filename=$(echo $filename | sed 's/lib-ve-governance-//g') + mkdir -p ref_builds/build-info-$filename + forge build --force --root=$file + mv $file/out/build-info/*.json ref_builds/build-info-$filename +done diff --git a/src/clock/Clock.sol b/src/clock/Clock.sol index 100bef2..5c793f1 100644 --- a/src/clock/Clock.sol +++ b/src/clock/Clock.sol @@ -29,6 +29,7 @@ contract Clock is IClock, DaoAuthorizable, UUPSUpgradeable { Initialization //////////////////////////////////////////////////////////////*/ + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } diff --git a/test/fork/e2eV2.t.sol b/test/fork/e2eV2.t.sol index 0dcc4ba..178bacc 100644 --- a/test/fork/e2eV2.t.sol +++ b/test/fork/e2eV2.t.sol @@ -987,9 +987,6 @@ contract TestE2EV2 is AragonTest, IWithdrawalQueueErrors, IGaugeVote, IEscrowCur // alice tries to exit vm.startPrank(alice); { - vm.expectRevert(VotingInactive.selector); - escrow.resetVotesAndBeginWithdrawal(1); - vm.expectRevert(CannotExit.selector); escrow.beginWithdrawal(1); } diff --git a/test/integration/GaugesDaoFactory.sol b/test/integration/GaugesDaoFactory.sol index 328ba72..8b5e958 100644 --- a/test/integration/GaugesDaoFactory.sol +++ b/test/integration/GaugesDaoFactory.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; +import {MockERC20} from "@solmate/test/utils/mocks/MockERC20.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"; @@ -382,12 +383,12 @@ contract GaugesDaoFactoryTest is Test { TokenParameters[] memory tokenParameters = new TokenParameters[](2); tokenParameters[0] = TokenParameters({ - token: address(deployMockERC20("T1", "T1", 18)), + token: address(new MockERC20("T1", "T1", 18)), veTokenName: "Name 1", veTokenSymbol: "TK1" }); tokenParameters[1] = TokenParameters({ - token: address(deployMockERC20("T2", "T2", 18)), + token: address(new MockERC20("T2", "T2", 18)), veTokenName: "Name 2", veTokenSymbol: "TK2" }); @@ -767,17 +768,17 @@ contract GaugesDaoFactoryTest is Test { TokenParameters[] memory tokenParameters = new TokenParameters[](3); tokenParameters[0] = TokenParameters({ - token: address(deployMockERC20("T3", "T3", 18)), + token: address(new MockERC20("T3", "T3", 18)), veTokenName: "Name 3", veTokenSymbol: "TK3" }); tokenParameters[1] = TokenParameters({ - token: address(deployMockERC20("T4", "T4", 18)), + token: address(new MockERC20("T4", "T4", 18)), veTokenName: "Name 4", veTokenSymbol: "TK4" }); tokenParameters[2] = TokenParameters({ - token: address(deployMockERC20("T5", "T5", 18)), + token: address(new MockERC20("T5", "T5", 18)), veTokenName: "Name 5", veTokenSymbol: "TK5" }); @@ -1261,17 +1262,17 @@ contract GaugesDaoFactoryTest is Test { TokenParameters[] memory tokenParameters = new TokenParameters[](3); tokenParameters[0] = TokenParameters({ - token: address(deployMockERC20("T3", "T3", 18)), + token: address(new MockERC20("T3", "T3", 18)), veTokenName: "Name 3", veTokenSymbol: "TK3" }); tokenParameters[1] = TokenParameters({ - token: address(deployMockERC20("T4", "T4", 18)), + token: address(new MockERC20("T4", "T4", 18)), veTokenName: "Name 4", veTokenSymbol: "TK4" }); tokenParameters[2] = TokenParameters({ - token: address(deployMockERC20("T5", "T5", 18)), + token: address(new MockERC20("T5", "T5", 18)), veTokenName: "Name 5", veTokenSymbol: "TK5" }); diff --git a/test/upgrades/E2EUpgrades.t.sol b/test/upgrades/E2EUpgrades.t.sol new file mode 100644 index 0000000..fa195d8 --- /dev/null +++ b/test/upgrades/E2EUpgrades.t.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.17; + +import {AragonTest} from "../base/AragonTest.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {DAO, createTestDAO} from "@mocks/MockDAO.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {Multisig, MultisigSetup} from "@aragon/multisig/MultisigSetup.sol"; +import {UUPSUpgradeable as UUPS} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {MockERC20} from "@mocks/MockERC20.sol"; + +import "../helpers/OSxHelpers.sol"; + +import { + GaugesDaoFactory as GaugesDaoFactory_v101, + GaugePluginSet as GaugePluginSet_v101, + Deployment as Deployment_v101 +} from "@aragon/ve-governance-v101/factory/GaugesDaoFactory.sol"; +import { + DeployGauges as DeployGauges_v101, + DeploymentParameters as DeploymentParameters_v101 +} from "../../lib/ve-governance/v101/script/DeployGauges.s.sol"; + +import { + Clock as Clock_v101, + VotingEscrow as VotingEscrow_v101, + Lock as Lock_v101, + QuadraticIncreasingEscrow as QuadraticIncreasingEscrow_v101, + ExitQueue as ExitQueue_v101, + SimpleGaugeVoter as SimpleGaugeVoter_v101, + SimpleGaugeVoterSetup as SimpleGaugeVoterSetup_v101, + ISimpleGaugeVoterSetupParams as ISimpleGaugeVoterSetupParams_v101 +} from '@aragon/ve-governance-v101/voting/SimpleGaugeVoterSetup.sol'; + +import { + Clock as Clock_v110, + VotingEscrow as VotingEscrow_v110, + Lock as Lock_v110, + QuadraticIncreasingEscrow as QuadraticIncreasingEscrow_v110, + ExitQueue as ExitQueue_v110, + SimpleGaugeVoter as SimpleGaugeVoter_v110, + SimpleGaugeVoterSetup as SimpleGaugeVoterSetup_v110, + ISimpleGaugeVoterSetupParams as ISimpleGaugeVoterSetupParams_v110 + } from "src/voting/SimpleGaugeVoterSetup.sol"; + + +import {Upgrades} from "openzeppelin-foundry-upgrades/LegacyUpgrades.sol"; +import {Options} from "openzeppelin-foundry-upgrades/Options.sol"; + +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import {ProxyLib} from "@libs/ProxyLib.sol"; + + +contract E2EUpgrades is AragonTest { + using Address for address; + using Clones for address; + using ERC165Checker for address; + using ProxyLib for address; + + GaugesDaoFactory_v101 factory; + SimpleGaugeVoter_v101 voter; + QuadraticIncreasingEscrow_v101 curve; + ExitQueue_v101 queue; + VotingEscrow_v101 escrow; + Clock_v101 clock; + Lock_v101 lock; + + DAO dao; + + MockERC20 token; + + function setUp() public { + token = new MockERC20(); + dao = createTestDAO(address(this)); + + address voterBase = address(new SimpleGaugeVoter_v101()); + address curveBase = address(new QuadraticIncreasingEscrow_v101()); + address queueBase = address(new ExitQueue_v101()); + address escrowBase = address(new VotingEscrow_v101()); + address clockBase = address(new Clock_v101()); + address lockBase = address(new Lock_v101()); + + // deploy the clock + clock = Clock_v101( + clockBase.deployUUPSProxy(abi.encodeWithSelector(Clock_v101.initialize.selector, dao)) + ); + + DAO(payable(address(dao))).grant({ + _who: address(this), + _where: address(clock), + _permissionId: clock.CLOCK_ADMIN_ROLE() + }); + + // deploy the escrow locker + escrow = VotingEscrow_v101( + escrowBase.deployUUPSProxy( + abi.encodeCall( + VotingEscrow_v101.initialize, + (address(token), address(dao), address(clock), 0x0) + ) + ) + ); + + DAO(payable(address(dao))).grant({ + _who: address(this), + _where: address(escrow), + _permissionId: escrow.ESCROW_ADMIN_ROLE() + }); + + // deploy the voting contract (plugin) + voter = SimpleGaugeVoter_v101( + voterBase.deployUUPSProxy( + abi.encodeCall( + SimpleGaugeVoter_v101.initialize, + (address(dao), address(escrow), true, address(clock)) + ) + ) + ); + + DAO(payable(address(dao))).grant({ + _who: address(this), + _where: address(voter), + _permissionId: voter.GAUGE_ADMIN_ROLE() + }); + + // deploy the curve + curve = QuadraticIncreasingEscrow_v101( + curveBase.deployUUPSProxy( + abi.encodeCall( + QuadraticIncreasingEscrow_v101.initialize, + (address(escrow), address(dao), 0, address(clock)) + ) + ) + ); + + DAO(payable(address(dao))).grant({ + _who: address(this), + _where: address(curve), + _permissionId: curve.CURVE_ADMIN_ROLE() + }); + + // deploy the exit queue + queue = ExitQueue_v101( + queueBase.deployUUPSProxy( + abi.encodeCall( + ExitQueue_v101.initialize, + (address(escrow), 0, address(dao), 0, address(clock), 1) + ) + ) + ); + + DAO(payable(address(dao))).grant({ + _who: address(this), + _where: address(queue), + _permissionId: queue.QUEUE_ADMIN_ROLE() + }); + + // deploy the escrow NFT + lock = Lock_v101( + lockBase.deployUUPSProxy( + abi.encodeCall( + Lock_v101.initialize, + (address(escrow), "Test", "tt", address(dao)) + ) + ) + ); + + DAO(payable(address(dao))).grant({ + _who: address(this), + _where: address(lock), + _permissionId: lock.LOCK_ADMIN_ROLE() + }); + + // increment the block by 1 to ensure we have a new block + // A new multisig requires this after changing settings + + vm.roll(block.number + 1); + } + + function testSimulateUpgrades() public { + Options memory options; + options.referenceBuildInfoDir = "ref_builds/build-info-v101"; + + options.referenceContract = "build-info-v101:Lock"; + Upgrades.upgradeProxy(address(lock), "Lock.sol", "", options); + + options.referenceContract = "build-info-v101:VotingEscrow"; + Upgrades.upgradeProxy(address(escrow), "VotingEscrow.sol", "", options); + + options.referenceContract = "build-info-v101:SimpleGaugeVoter"; + Upgrades.upgradeProxy(address(voter), "SimpleGaugeVoter.sol", "", options); + + options.referenceContract = "build-info-v101:QuadraticIncreasingEscrow"; + Upgrades.upgradeProxy(address(curve), "QuadraticIncreasingEscrow.sol", "", options); + + options.referenceContract = "build-info-v101:ExitQueue"; + Upgrades.upgradeProxy(address(queue), "ExitQueue.sol", "", options); + + options.referenceContract = "build-info-v101:Clock"; + Upgrades.upgradeProxy(address(clock), "Clock.sol", "", options); + } +} diff --git a/test/upgrades/TestUpgrades.t.sol b/test/upgrades/TestUpgrades.t.sol new file mode 100644 index 0000000..7aa7ad3 --- /dev/null +++ b/test/upgrades/TestUpgrades.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.17; + +import {AragonTest} from "../base/AragonTest.sol"; + +import {Upgrades} from "openzeppelin-foundry-upgrades/LegacyUpgrades.sol"; +import {Options} from "openzeppelin-foundry-upgrades/Options.sol"; + +contract TestUpgrades is AragonTest { + function testValidateUpgrades() public { + Options memory options; + options.referenceBuildInfoDir = "ref_builds/build-info-v101"; + + options.referenceContract = "build-info-v101:Lock"; + Upgrades.validateUpgrade("Lock.sol", options); + + options.referenceContract = "build-info-v101:VotingEscrow"; + Upgrades.validateUpgrade("VotingEscrow.sol", options); + + options.referenceContract = "build-info-v101:SimpleGaugeVoter"; + Upgrades.validateUpgrade("SimpleGaugeVoter.sol", options); + + options.referenceContract = "build-info-v101:QuadraticIncreasingEscrow"; + Upgrades.validateUpgrade("QuadraticIncreasingEscrow.sol", options); + + options.referenceContract = "build-info-v101:ExitQueue"; + Upgrades.validateUpgrade("ExitQueue.sol", options); + + options.referenceContract = "build-info-v101:Clock"; + Upgrades.validateUpgrade("Clock.sol", options); + } +}