├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .husky ├── .pre-commit.sh └── pre-commit ├── .solhintignore ├── .solhintrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── addresses ├── 1.json ├── 11155111.json ├── 31337.json ├── Addresses.sol ├── AddressesDuplicated.json ├── AddressesDuplicatedDifferentName.json └── IAddresses.sol ├── docs ├── README.md ├── SUMMARY.md ├── actions │ └── print-calldata.md ├── assets │ └── diagram.svg ├── guides │ ├── customizing-proposal.md │ ├── governor-bravo-proposal.md │ ├── introduction.md │ ├── multisig-proposal.md │ ├── oz-governor-proposal.md │ └── timelock-proposal.md ├── mainnet-examples │ ├── ArbitrumTimelock.md │ ├── CompoundGovernorBravo.md │ ├── ENSOzGovernor.md │ └── OptimismMultisig.md ├── overview │ ├── architecture │ │ ├── README.md │ │ ├── addresses.md │ │ └── proposal-functions.md │ └── use-cases.md └── testing │ └── integration-tests.md ├── foundry.toml ├── mocks ├── MockAuction.sol ├── MockBravoProposal.sol ├── MockDuplicatedActionProposal.sol ├── MockGovernorAlpha.sol ├── MockMultisigProposal.sol ├── MockOZGovernorProposal.sol ├── MockSavingContract.sol ├── MockTimelockProposal.sol ├── MockToken.sol ├── MockTokenWrapper.sol ├── MockUpgrade.sol └── MockVotingContract.sol ├── package-lock.json ├── package.json ├── remappings.txt ├── run-proposal.sh ├── src ├── interface │ ├── ICompoundConfigurator.sol │ ├── IGovernor.sol │ ├── IGovernorBravo.sol │ ├── IProxy.sol │ ├── IProxyAdmin.sol │ ├── ITimelockController.sol │ └── IVotes.sol └── proposals │ ├── CrossChainProposal.sol │ ├── GovernorBravoProposal.sol │ ├── IProposal.sol │ ├── MultisigProposal.sol │ ├── OZGovernorProposal.sol │ ├── Proposal.sol │ └── TimelockProposal.sol ├── test ├── Addresses.t.sol ├── BravoProposal.t.sol ├── DuplicatedAction.t.sol ├── GovernorOZProposal.t.sol ├── MultisigProposal.t.sol ├── MultisigProposalCalldataTest.t.sol ├── TimelockProposal.t.sol ├── multisigProposals │ ├── MultisigProposal_01.sol │ ├── MultisigProposal_02.sol │ ├── MultisigProposal_03.sol │ ├── MultisigProposal_04.sol │ └── MultisigProposal_05.sol └── utils │ ├── duplicate-addresses-different-name │ └── 31337.json │ └── duplicate-addresses │ └── 31337.json └── utils ├── Address.sol └── Constants.sol /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | env: 4 | FOUNDRY_PROFILE: "ci" 5 | 6 | on: 7 | workflow_dispatch: 8 | pull_request: 9 | push: 10 | branches: 11 | - "main" 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: "Check out the repo" 18 | uses: actions/checkout@v3 19 | with: 20 | submodules: recursive 21 | - name: "Install Node.js" 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: lts/* 25 | 26 | - name: "Install the Node.js dependencies" 27 | run: npm install 28 | 29 | - name: Run linter and check for errors 30 | id: lint 31 | run: | 32 | LINT_OUTCOME=$(npm run lint 2>&1 || true) # Prevent the step from failing immediately 33 | echo "$LINT_OUTCOME" 34 | echo "LINT_OUTCOME<> $GITHUB_ENV 35 | echo "$LINT_OUTCOME" >> $GITHUB_ENV 36 | echo "EOF" >> $GITHUB_ENV 37 | if echo "$LINT_OUTCOME" | grep -q " error "; then 38 | echo "## Lint result" >> $GITHUB_STEP_SUMMARY 39 | echo "❌ Failed due to errors" >> $GITHUB_STEP_SUMMARY 40 | exit 1 41 | else 42 | echo "## Lint result" >> $GITHUB_STEP_SUMMARY 43 | echo "✅ Passed or warnings found" >> $GITHUB_STEP_SUMMARY 44 | fi 45 | 46 | integration-test: 47 | runs-on: "ubuntu-latest" 48 | steps: 49 | - name: "Check out the repo" 50 | uses: "actions/checkout@v3" 51 | with: 52 | submodules: "recursive" 53 | 54 | - name: "Install Foundry" 55 | uses: "foundry-rs/foundry-toolchain@v1" 56 | 57 | - name: "Show the Foundry config" 58 | run: "forge config" 59 | 60 | - name: 61 | "Generate a fuzz seed that changes weekly to avoid burning through RPC 62 | allowance" 63 | run: > 64 | echo "FOUNDRY_FUZZ_SEED=$( 65 | echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) 66 | )" >> $GITHUB_ENV 67 | 68 | - name: "Build the contracts and print their size" 69 | run: "forge build --sizes" 70 | 71 | - name: "Proposal Simulator Integration Tests" 72 | run: "forge test --mc IntegrationTest" 73 | 74 | - name: "Add test summary" 75 | run: | 76 | echo "## Proposal simulator tests result" >> $GITHUB_STEP_SUMMARY 77 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 78 | 79 | addresses-test: 80 | runs-on: "ubuntu-latest" 81 | steps: 82 | - name: "Check out the repo" 83 | uses: "actions/checkout@v3" 84 | with: 85 | submodules: "recursive" 86 | 87 | - name: "Install Foundry" 88 | uses: "foundry-rs/foundry-toolchain@v1" 89 | 90 | - name: "Show the Foundry config" 91 | run: "forge config" 92 | 93 | - name: 94 | "Generate a fuzz seed that changes weekly to avoid burning through RPC 95 | allowance" 96 | run: > 97 | echo "FOUNDRY_FUZZ_SEED=$( 98 | echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) 99 | )" >> $GITHUB_ENV 100 | 101 | - name: "Build the contracts and print their size" 102 | run: "forge build --sizes" 103 | 104 | - name: "Run the tests" 105 | run: "forge test --mc TestAddresses" 106 | 107 | - name: "Add test summary" 108 | run: | 109 | echo "## Proposal simulator tests result" >> $GITHUB_STEP_SUMMARY 110 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | broadcast/ 2 | 3 | artifacts/ 4 | 5 | cache/ 6 | 7 | out/ 8 | 9 | node_modules/ 10 | 11 | .env 12 | 13 | lcov.info 14 | 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /.husky/.pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Temporary file to hold list of staged .sol files 4 | STAGED_SOL_FILES=$(mktemp) 5 | # Temporary file to hold list of all staged files for formatting 6 | STAGED_FILES=$(mktemp) 7 | 8 | # List staged .sol files ignoring deleted files 9 | 10 | # List only renamed .sol files 11 | git diff --cached --name-status -- '*.sol' | grep -v '^D' | grep -E '^R' | cut -f3 > "$STAGED_SOL_FILES" 12 | # Append .sol files ignoring renamed and deleted files 13 | git diff --cached --name-status -- '*.sol' | grep -v '^D' | grep -E '^[^R]' | cut -f2 >> "$STAGED_SOL_FILES" 14 | 15 | # List all staged files ignoring deleted files 16 | 17 | # List only renamed files 18 | git diff --cached --name-status | grep -v '^D' | grep -E '^R' | cut -f3 > "$STAGED_FILES" 19 | # Append all staged files ignoring renamed and deleted files 20 | git diff --cached --name-status | grep -v '^D' | grep -E '^[^R]' | cut -f2 >> "$STAGED_FILES" 21 | 22 | # Run Solhint on staged .sol files, if any 23 | if [ -s "$STAGED_SOL_FILES" ]; then 24 | # If there are staged .sol files, run Solhint on them 25 | SOLHINT_OUTPUT=$(cat "$STAGED_SOL_FILES" | xargs npx solhint --config ./.solhintrc) 26 | SOLHINT_EXIT_CODE=$? 27 | 28 | if [ $SOLHINT_EXIT_CODE -ne 0 ]; then 29 | echo "Solhint errors detected:" 30 | echo "$SOLHINT_OUTPUT" 31 | rm "$STAGED_SOL_FILES" "$STAGED_FILES" 32 | exit $SOLHINT_EXIT_CODE 33 | else 34 | # Re-add the .sol files to include any automatic fixes by Solhint 35 | cat "$STAGED_SOL_FILES" | xargs git add 36 | fi 37 | fi 38 | 39 | # Run forge fmt and check for errors on staged files 40 | if [ -s "$STAGED_FILES" ]; then 41 | # Note: Run forge fmt to automatically fix formatting 42 | FMT_OUTPUT=$(cat "$STAGED_FILES" | xargs forge fmt) 43 | FMT_EXIT_CODE=$? 44 | 45 | if [ $FMT_EXIT_CODE -ne 0 ]; then 46 | echo "Forge fmt formatting errors detected:" 47 | echo "$FMT_OUTPUT" 48 | rm "$STAGED_SOL_FILES" "$STAGED_FILES" 49 | exit $FMT_EXIT_CODE 50 | else 51 | # Re-add the files to include any formatting changes by forge fmt 52 | cat "$STAGED_FILES" | xargs git add 53 | fi 54 | fi 55 | 56 | # Clean up 57 | rm "$STAGED_SOL_FILES" "$STAGED_FILES" 58 | 59 | # If we reach this point, either there were no issues or only warnings. 60 | # Warnings are allowed, so we exit successfully. 61 | exit 0 62 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | ./.husky/.pre-commit.sh 5 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | out/ 3 | -------------------------------------------------------------------------------- /.solhintrc: -------------------------------------------------------------------------------- 1 | {"extends": "solhint:recommended", 2 | "rules": { 3 | "compiler-version": ["error", ">=0.8.0"], 4 | "func-name-mixedcase": "off", 5 | "func-visibility": ["error", { "ignoreConstructors": true }], 6 | "max-line-length": "off", 7 | "named-parameters-mapping": "warn", 8 | "no-console": "off", 9 | "not-rely-on-time": "off", 10 | "one-contract-per-file": "off", 11 | "no-inline-assembly": "off", 12 | "no-empty-blocks": "off", 13 | "var-name-mixedcase": "off", 14 | "no-global-import": "off", 15 | "reason-string": "off", 16 | "immutable-vars-naming": "off", 17 | "avoid-low-level-calls": "off", 18 | "contract-name-camelcase": "off", 19 | "no-unused-import": "error", 20 | "explicit-types": "off", 21 | "const-name-snakecase": "off", 22 | "gas-custom-errors": "off", 23 | "quotes": "off" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | There are many ways to contribute to Forge Proposal Simulator. 4 | 5 | ## Opening an issue 6 | 7 | You can [open an issue] to suggest a feature or report a minor bug. 8 | 9 | Before opening an issue, be sure to search through the existing open and closed issues, and consider posting a comment in one of those instead. 10 | 11 | When requesting a new feature, include as many details as you can, especially around the use cases that motivate it. Features are prioritized according to the impact they may have on the ecosystem, so we appreciate information showing that the impact could be high. 12 | 13 | [open an issue]: https://github.com/solidity-labs-io/forge-proposal-simulator/issues/new 14 | 15 | ## Submitting a pull request 16 | 17 | If you would like to contribute code or documentation you may do so by forking the repository and submitting a pull request. 18 | 19 | Any non-trivial code contribution must be first discussed with the maintainers in an issue (see [Opening an issue](#opening-an-issue)). Only very minor changes are accepted without prior discussion. 20 | 21 | Run linter, forge fmt and tests to make sure your pull request is good before submitting it. 22 | 23 | If you're looking for a good place to start, look for issues labelled ["good first issue"](https://github.com/solidity-labs-io/forge-proposal-simulator/labels/good%20first%20issue)! 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Solidity Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The Forge Proposal Simulator (FPS) offers a framework for creating secure governance proposals and deployment scripts, enhancing safety, and ensuring protocol health throughout the proposal lifecycle. The major benefits of using this tool are standardization of proposals, safe calldata generation, and preventing deployment and governance action bugs. 4 | 5 | For guidance on tool usage, please read the [documentation](https://solidity-labs.gitbook.io/forge-proposal-simulator/). 6 | 7 | ## Usage 8 | 9 | ### Proposal Simulation 10 | 11 | #### Step 1: Install 12 | 13 | Add `forge-proposal-simulator` to your project using Forge: 14 | 15 | ```sh 16 | forge install https://github.com/solidity-labs-io/forge-proposal-simulator.git 17 | ``` 18 | 19 | #### Step 2: Set Remappings 20 | 21 | Update your remappings.txt to include: 22 | 23 | ```sh 24 | echo @forge-proposal-simulator=lib/forge-proposal-simulator/ >> remappings.txt 25 | ``` 26 | 27 | #### Step 3: Create Addresses File 28 | 29 | Create a JSON file following the instructions provided in 30 | [Addresses.md](docs/overview/architecture/addresses.md). We recommend keeping the 31 | addresses file in a separate folder, for example `./addresses/addresses.json`. 32 | Once the file is created, be sure to allow read access to `addresses.json` inside of `foundry.toml`. 33 | 34 | ```toml 35 | [profile.default] 36 | ... 37 | fs_permissions = [{ access = "read", path = "./addresses/addresses.json"}] 38 | ``` 39 | 40 | #### Step 4: Create a Proposal 41 | 42 | Choose a model that fits your needs: 43 | 44 | - [Multisig Proposal](docs/guides/multisig-proposal.md) 45 | - [Timelock Proposal](docs/guides/timelock-proposal.md) 46 | - [Governor Bravo Proposal](docs/guides/governor-bravo-proposal.md) 47 | - [OZ Governor proposal](docs/guides/oz-governor-proposal.md) 48 | 49 | #### Step 5: Implement Scripts and Tests 50 | 51 | Create scripts and/or tests. Check [Guides](docs/guides/multisig-proposal.md) and [Integration Tests](docs/testing/integration-tests.md). 52 | 53 | ## Contribute 54 | 55 | There are many ways you can participate and help build the next version of FPS. Check out the [contribution guide](CONTRIBUTING.md)! 56 | 57 | ## License 58 | 59 | Forge Proposal Simulator is made available under the MIT License, which disclaims all warranties in relation to the project and which limits the liability of those that contribute and maintain the project. As set out further in the Terms, you acknowledge that you are solely responsible for any use of Forge Proposal Simulator contracts and you assume all risks associated with any such use. The authors make no warranties about the safety, suitability, reliability, timeliness, and accuracy of the software. 60 | 61 | Further license details can be found in [LICENSE](LICENSE). 62 | -------------------------------------------------------------------------------- /addresses/1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 4 | "name": "DEPLOYER_EOA", 5 | "isContract": false 6 | }, 7 | { 8 | "addr": "0xc0da02939e1441f497fd74f78ce7decb17b66529", 9 | "isContract": true, 10 | "name": "COMPOUND_GOVERNOR_BRAVO" 11 | }, 12 | { 13 | "addr": "0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3", 14 | "isContract": true, 15 | "name": "COMPOUND_CONFIGURATOR" 16 | }, 17 | { 18 | "addr": "0xf603265f91f58F1EfA4fAd57694Fb3B77b25fC18", 19 | "isContract": false, 20 | "name": "COMPOUND_PROPOSER" 21 | }, 22 | { 23 | "addr": "0xa17581a9e3356d9a858b789d68b4d866e593ae94", 24 | "isContract": true, 25 | "name": "COMPOUND_COMET" 26 | }, 27 | { 28 | "addr": "0xc00e94cb662c3520282e6f5717214004a7f26888", 29 | "isContract": true, 30 | "name": "COMP_TOKEN" 31 | }, 32 | { 33 | "addr": "0x6d903f6003cca6255D85CcA4D3B5E5146dC33925", 34 | "isContract": true, 35 | "name": "COMPOUND_TIMELOCK_BRAVO" 36 | }, 37 | { 38 | "addr": "0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A", 39 | "name": "OPTIMISM_MULTISIG", 40 | "isContract": true 41 | }, 42 | { 43 | "addr": "0x543bA4AADBAb8f9025686Bd03993043599c6fB04", 44 | "name": "OPTIMISM_PROXY_ADMIN", 45 | "isContract": true 46 | }, 47 | { 48 | "addr": "0x5a7749f83b81b301cab5f48eb8516b986daef23d", 49 | "name": "OPTIMISM_L1_NFT_BRIDGE_PROXY", 50 | "isContract": true 51 | }, 52 | { 53 | "addr": "0xAE2AF01232a6c4a4d3012C5eC5b1b35059caF10d", 54 | "name": "OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION", 55 | "isContract": true 56 | }, 57 | { 58 | "addr": "0xE6841D92B0C345144506576eC13ECf5103aC7f49", 59 | "name": "ARBITRUM_L1_TIMELOCK", 60 | "isContract": true 61 | }, 62 | { 63 | "addr": "0x3ffFbAdAF827559da092217e474760E2b2c3CeDd", 64 | "name": "ARBITRUM_L1_UPGRADE_EXECUTOR", 65 | "isContract": true 66 | }, 67 | { 68 | "addr": "0x9aD46fac0Cf7f790E5be05A0F15223935A0c0aDa", 69 | "name": "ARBITRUM_L1_PROXY_ADMIN", 70 | "isContract": true 71 | }, 72 | { 73 | "addr": "0xd92023e9d9911199a6711321d1277285e6d4e2db", 74 | "name": "ARBITRUM_L1_WETH_GATEWAY_PROXY", 75 | "isContract": true 76 | }, 77 | { 78 | "addr": "0x8315177aB297bA92A06054cE80a67Ed4DBd7ed3a", 79 | "name": "ARBITRUM_BRIDGE", 80 | "isContract": true 81 | }, 82 | { 83 | "addr": "0x323a76393544d5ecca80cd6ef2a560c6a395b7e3", 84 | "name": "ENS_GOVERNOR", 85 | "isContract": true 86 | }, 87 | { 88 | "addr": "0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7", 89 | "name": "ENS_TIMELOCK", 90 | "isContract": true 91 | }, 92 | { 93 | "addr": "0xaB528d626EC275E3faD363fF1393A41F581c5897", 94 | "name": "ENS_ROOT", 95 | "isContract": true 96 | } 97 | ] 98 | -------------------------------------------------------------------------------- /addresses/11155111.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 4 | "name": "DEPLOYER_EOA", 5 | "isContract": false 6 | }, 7 | { 8 | "addr": "0x37039662a2364Df8206691c1D1b9ff8d22389290", 9 | "isContract": true, 10 | "name": "PROTOCOL_TIMELOCK_BRAVO" 11 | }, 12 | { 13 | "addr": "0x201CA53059484131D3d8058ec8D9655117E05dC2", 14 | "isContract": true, 15 | "name": "PROTOCOL_GOVERNANCE_TOKEN" 16 | }, 17 | { 18 | "addr": "0xAE96E645c7c7628BA9507B1e91A5fA3Cb1EB5DA1", 19 | "isContract": true, 20 | "name": "PROTOCOL_TIMELOCK" 21 | }, 22 | { 23 | "addr": "0x7D26D5978cD4617C6c7a02C75a42927eF52f405C", 24 | "isContract": true, 25 | "name": "TIMELOCK_VAULT" 26 | }, 27 | { 28 | "addr": "0x861bB12491F453F48fE1A05e9C034588de638262", 29 | "isContract": true, 30 | "name": "TIMELOCK_TOKEN" 31 | }, 32 | { 33 | "addr": "0x0CB10d561C2a273AFBB81E535738BA3095E9dCF2", 34 | "isContract": true, 35 | "name": "BRAVO_VAULT" 36 | }, 37 | { 38 | "addr": "0x76126BDFC0893d9A27336b93824778432cA60808", 39 | "isContract": true, 40 | "name": "BRAVO_VAULT_TOKEN" 41 | } 42 | ] -------------------------------------------------------------------------------- /addresses/31337.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 4 | "name": "DEPLOYER_EOA", 5 | "isContract": false 6 | }, 7 | { 8 | "addr": "0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A", 9 | "name": "PROTOCOL_MULTISIG", 10 | "isContract": false 11 | } 12 | ] -------------------------------------------------------------------------------- /addresses/AddressesDuplicated.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 4 | "name": "DEPLOYER_EOA", 5 | "chainId": 31337, 6 | "isContract": false 7 | }, 8 | { 9 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 10 | "name": "DEPLOYER_EOA", 11 | "chainId": 31337, 12 | "isContract": false 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /addresses/AddressesDuplicatedDifferentName.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 4 | "name": "DEPLOYER_EOA", 5 | "chainId": 31337, 6 | "isContract": false 7 | }, 8 | { 9 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 10 | "name": "DEPLOYER_EOA2", 11 | "chainId": 31337, 12 | "isContract": false 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /addresses/IAddresses.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | /// @notice This is a contract that stores addresses for different networks. 5 | /// It allows a project to have a single source of truth to get all the addresses 6 | /// for a given network. 7 | interface IAddresses { 8 | /// @notice get an address for the current chainId 9 | /// @param name the name of the address 10 | function getAddress(string memory name) external view returns (address); 11 | 12 | /// @notice get an address for a specific chainId 13 | /// @param name the name of the address 14 | function getAddress( 15 | string memory name, 16 | uint256 chainId 17 | ) external view returns (address); 18 | 19 | /// @notice add an address for the current chainId 20 | /// @param name the name of the address 21 | /// @param addr the address to add 22 | /// @param isContract whether the address is a contract 23 | function addAddress( 24 | string memory name, 25 | address addr, 26 | bool isContract 27 | ) external; 28 | 29 | /// @notice add an address for a specific chainId 30 | /// @param name the name of the address 31 | /// @param addr the address to add 32 | /// @param chainId the chain id 33 | /// @param isContract whether the address is a contract 34 | function addAddress( 35 | string memory name, 36 | address addr, 37 | uint256 chainId, 38 | bool isContract 39 | ) external; 40 | 41 | /// @notice change an address for the current chainId 42 | /// @param name the name of the address 43 | /// @param addr the address to change 44 | /// @param isContract whether the address is a contract 45 | function changeAddress( 46 | string memory name, 47 | address addr, 48 | bool isContract 49 | ) external; 50 | 51 | /// @notice change an address for a specific chainId 52 | /// @param name the name of the address 53 | /// @param addr the address to change 54 | /// @param chainId the chain id 55 | /// @param isContract whether the address is a contract 56 | function changeAddress( 57 | string memory name, 58 | address addr, 59 | uint256 chainId, 60 | bool isContract 61 | ) external; 62 | 63 | /// @notice remove recorded addresses 64 | function resetRecordingAddresses() external; 65 | 66 | /// @notice remove changed addresses 67 | function resetChangedAddresses() external; 68 | 69 | /// @notice get recorded addresses from a proposal's deployment 70 | function getRecordedAddresses() 71 | external 72 | view 73 | returns ( 74 | string[] memory names, 75 | uint256[] memory chainIds, 76 | address[] memory addresses 77 | ); 78 | 79 | /// @notice get changed addresses from a proposal 80 | function getChangedAddresses() 81 | external 82 | view 83 | returns ( 84 | string[] memory names, 85 | uint256[] memory chainIds, 86 | address[] memory oldAddresses, 87 | address[] memory newAddresses 88 | ); 89 | 90 | /// @notice check if an address is a contract 91 | /// @param name the name of the address 92 | function isAddressContract(string memory name) external view returns (bool); 93 | 94 | /// @notice check if an address is set 95 | /// @param name the name of the address 96 | function isAddressSet(string memory name) external view returns (bool); 97 | 98 | /// @notice check if an address is set for a specific chain id 99 | /// @param name the name of the address 100 | /// @param chainId the chain id 101 | function isAddressSet( 102 | string memory name, 103 | uint256 chainId 104 | ) external view returns (bool); 105 | } 106 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Forge Proposal Simulator 2 | 3 | ## Overview 4 | 5 | The Forge Proposal Simulator (FPS) offers a framework for creating secure governance proposals and deployment scripts, enhancing safety, and ensuring protocol health throughout the proposal lifecycle. The major benefits of using this tool are standardization of proposals, programmatic calldata generation, prevention of deployment parameterization and governance action bugs. 6 | 7 | 1. **Standardized Governance**: This standardization simplifies the review process and enables thorough testing of proposed changes against the current state of the protocol. It ensures the protocol's stability post-implementation. With FPS, every protocol modification undergoes rigorous checks through an integrated test suite, confirming the protocol's integrity from proposal creation to execution. 8 | 9 | 2. **Safe Calldata Generation**: Enhance proposal security by generating and verifying calldata programmatically out of the box. Using FPS, developers can easily test their proposals within a forked environment before posting them on-chain. This introduces an extra layer of security for governance. Technical signers can quickly access proposal calldata through a standard method in the proposal contract. This allows for easy retrieval of calldata, which can then be checked against the data proposed in the governance contracts. 10 | 11 | 3. **Preventing Governance Bugs**: Mistakes happen often in governance. This tool offers testing of contracts in their post governance proposal state, helping to mitigate risks. 12 | 13 | 4. **Preventing Deployment Script Bugs**: Developers can now easily test their deployment scripts with integration tests, making it simple to leverage this tool's capabilities to help eliminate an entire category of bugs. 14 | 15 | ## Quick links 16 | 17 | {% content-ref url="./overview/use-cases.md" %} 18 | [Use cases](./overview/use-cases.md) 19 | {% endcontent-ref %} 20 | 21 | {% content-ref url="./overview/architecture" %} 22 | [Architecture](./overview/architecture) 23 | {% endcontent-ref %} 24 | 25 | ## Get Started 26 | 27 | {% content-ref url="./guides/introduction.md" %} 28 | [Introduction](./guides/introduction.md) 29 | {% endcontent-ref %} 30 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | - [Forge Proposal Simulator](README.md) 4 | 5 | ## Overview 6 | 7 | - [Use cases](overview/use-cases.md) 8 | - [Architecture](overview/architecture/README.md) 9 | - [Addresses](overview/architecture/addresses.md) 10 | - [Proposal Functions](overview/architecture/proposal-functions.md) 11 | 12 | ## Guides 13 | 14 | - [Introduction](guides/introduction.md) 15 | - [Multisig Proposal](guides/multisig-proposal.md) 16 | - [Timelock Proposal](guides/timelock-proposal.md) 17 | - [Governor Bravo Proposal](guides/governor-bravo-proposal.md) 18 | - [OZ Governor Proposal](guides/oz-governor-proposal.md) 19 | - [Customizing A Proposal](guides/customizing-proposal.md) 20 | - Mainnet examples 21 | - [Arbitrum Timelock](mainnet-examples/ArbitrumTimelock.md) 22 | - [Compound Governor Bravo](mainnet-examples/CompoundGovernorBravo.md) 23 | - [Optimism Multisig](mainnet-examples/OptimismMultisig.md) 24 | - [ENS OZ Governor](mainnet-examples/ENSOzGovernor.md) 25 | 26 | ## Testing 27 | 28 | - [Integration Tests](testing/integration-tests.md) 29 | 30 | ## Github Actions 31 | 32 | - [Printing Calldata on Pull Requests](actions/print-calldata.md) 33 | -------------------------------------------------------------------------------- /docs/actions/print-calldata.md: -------------------------------------------------------------------------------- 1 | # Print Calldata on Pull Requests 2 | 3 | ## Overview 4 | 5 | The following guide explains how to create a GitHub Action that prints the proposal output on PRs. The output includes calldata, newly deployed addresses, changed addresses, and proposal actions. Once the action is executed, the output will be printed in a comment on the PR. It's worth noting that if a PR touches multiple proposals, the comment will only contain the output of the proposal with the greatest number. 6 | 7 | ## Creating the Workflow 8 | 9 | To create the workflow, follow these steps: 10 | 11 | 1. Copy the code from [fps-example-repo](https://github.com/solidity-labs-io/fps-example-repo/blob/main/.github/workflows/run-latest-proposal.yml) and paste it into a new file named `run-proposal.yml` in the `.github/workflows` folder. 12 | 2. Change the `PROPOSALS_FOLDER` to the folder where the proposals are located. 13 | 3. Add the Forge Proposal Simulator path before the `run-proposal.sh` script: `lib/forge-proposal-simulator/run-proposal.sh`. 14 | 4. In case the proposal names are different from `*Proposal_1.sol`, copy the [script](https://github.com/solidity-labs-io/fps-example-repo/blob/main/run-proposal.sh) into your repository and customize it as needed. Update the path in step 3 to `./run-proposal.sh`. 15 | 5. Check the Github repository settings and make sure Read and Write Permissions are enabled in the Workflow Permissions section. 16 | 17 | Whenever a Pull Request that involves a proposal is created, the action will automatically execute and display the output of the proposal in a comment on the PR. This enables the developer to locally run the proposal and validate whether the output corresponds with the one shown on the PR. 18 | 19 | ## Example implementation 20 | 21 | The above workflow has been implemented in [fps-example-repo](https://github.com/solidity-labs-io/fps-example-repo/.github/workflows/run-latest-proposal.yml). 22 | -------------------------------------------------------------------------------- /docs/guides/introduction.md: -------------------------------------------------------------------------------- 1 | # Guides 2 | 3 | FPS is designed to be loosely coupled, making it easy to integrate into any governance model. Each of these governance models have their unique specifications. To accommodate the unique requirements of different governance systems, FPS introduces governance-specific contracts. Each contract is designed to align with their respective governance model. There are mainnet examples added as well for each governance model. 4 | 5 | ## Getting Set Up 6 | 7 | ### Step 1: Add Dependency 8 | 9 | Add `forge-proposal-simulator` to your project using Forge: 10 | 11 | ```sh 12 | forge install https://github.com/solidity-labs-io/forge-proposal-simulator.git 13 | ``` 14 | 15 | ### Step 2: Remapping 16 | 17 | Update your `remappings.txt` to include: 18 | 19 | ```sh 20 | echo @forge-proposal-simulator=lib/forge-proposal-simulator/ >> remappings.txt 21 | ``` 22 | 23 | ### Step 3: Addresses File 24 | 25 | Create a JSON file following the standard on [Addresses](../overview/architecture/addresses.md). It is recommended to keep the JSON file in a separate folder, for example, `./addresses/31337.json`. The name of the JSON file should be the same as the network id. If there are multiple networks, addresses should be added in the JSON files corresponding to their network. Be sure to allow read access for this file in `foundry.toml`. 26 | 27 | ```toml 28 | [profile.default] 29 | ... 30 | fs_permissions = [{ access = "read", path = "./addresses/31337.json"}] 31 | ``` 32 | 33 | ### Step 4: Setting Up Your Deployer Address 34 | 35 | The deployer address is the one used to broadcast the transactions deploying the proposal contracts. Ensure your deployer address has enough funds from the faucet to cover deployment costs on the testnet. Security is prioritized when it comes to private key management. To avoid storing the private key as an environment variable foundry's cast tool is used. Ensure cast address is the same as deployer address. 36 | 37 | If there are no wallets in the `~/.foundry/keystores/` folder, create one by executing: 38 | 39 | ```sh 40 | cast wallet import ${wallet_name} --interactive 41 | ``` 42 | 43 | ## Executing Proposals 44 | 45 | Before proceeding with the guides, make sure to have a cleaned JSON file with read permissions set to `foundry.toml` in your preferred location. Each guide includes a proposal simulation section that provides detailed explanations of the proposal execution steps. 46 | 47 | There are two methods for executing proposals: 48 | 49 | 1. **Using `forge test`**: Detailed information on this method can be found in the [integration-tests.md](../testing/integration-tests.md) section. 50 | 2. **Using `forge script`**: All the guides employs this method. 51 | 52 | Ensure that the ${wallet_name} and ${wallet_address} accurately match the wallet details saved in `~/.foundry/keystores/` at the time of proposal simulation through `forge script`. It's essential to verify that ${wallet_address} is correctly listed as the deployer address in the JSON file. Failure to align these details will result in script execution failure. If a password was provide to the wallet, the script will prompt for the password before broadcasting the proposal. 53 | 54 | ## Validated Governance Models 55 | 56 | This framework has been validated through successful integration with leading governance models. FPS has been tested and confirmed to be compatible with governance models specified below. Guides below explain how FPS can be used to simulate governance proposals. 57 | 58 | 1. [Gnosis Safe Multisig](./multisig-proposal.md) 59 | 2. [Openzeppelin Timelock Controller](./timelock-proposal.md) 60 | 3. [Governor Bravo](./governor-bravo-proposal.md) 61 | 4. [OZ Governor](./oz-governor-proposal.md) 62 | 63 | Each guide drafts a proposal to perform the following steps: 64 | 65 | 1. deploy new instances of `Vault` and `Token` 66 | 2. mint tokens to governance contract 67 | 3. transfer ownerships of `Vault` and `Token` to respective governance contract 68 | 4. whitelist `Token` on `Vault` 69 | 5. approve and deposit all tokens into `Vault` 70 | 71 | Above `Token` and `Vault` contracts can be found in the fps-example-repo [mocks folder](https://github.com/solidity-labs-io/fps-example-repo/tree/main/src/mocks/vault). Clone the fps-example-repo repo before proceeding with the respective guides. It is important to understand that these contracts are intended solely for demonstration and are not for production use due to their lack of validation, testing, and audits. Their sole purpose is to illustrate the deployment process and the setup of protocol parameters within proposals within the forge proposal simulator. 72 | 73 | ## Customized Governance Models 74 | 75 | The framework can be customized to meet unique protocol requirements for simulating the proposal flow. An [example](./customizing-proposal.md) has been provided using Arbitrum Proposal flow to demonstrate FPS flexibility. 76 | 77 | ## Mainnet examples 78 | 79 | Mainnet examples highlight how each of the FPS governance models can be easily implemented for existing real world projects for proposal simulation. 80 | 81 | 1. [Arbitrum Timelock](../mainnet-examples/ArbitrumTimelock.md) 82 | 2. [Compound Governor Bravo](../mainnet-examples/CompoundGovernorBravo.md) 83 | 3. [ENS OZ Governor](../mainnet-examples/ENSOzGovernor.md) 84 | 4. [Optimism Multisig](../mainnet-examples/OptimismMultisig.md) 85 | -------------------------------------------------------------------------------- /docs/mainnet-examples/ArbitrumTimelock.md: -------------------------------------------------------------------------------- 1 | # Arbitrum Timelock Proposal 2 | 3 | ## Overview 4 | 5 | This example showcases how FPS can be utilized for simulating proposals for the Arbitrum timelock on mainnet. Specifically, it upgrades the WETH gateway on L1. The proposal involves deploying a new implementation contract `ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION` and a governance action contract `ARBITRUM_GAC_UPGRADE_WETH_GATEWAY`. Then, the timelock employs `upgradeExecutor` to upgrade the WETH gateway. The proposer for the L1 timelock should always be the Arbitrum bridge. 6 | 7 | The relevant contract can be found in the [mocks folder](../../mocks/MockTimelockProposal.sol). 8 | 9 | Let's review each of the overridden functions: 10 | 11 | - `name()`: Defines the name of the proposal. 12 | 13 | ```solidity 14 | function name() public pure override returns (string memory) { 15 | return "ARBITRUM_L1_TIMELOCK_MOCK"; 16 | } 17 | ``` 18 | 19 | - `description()`: Provides a detailed description of the proposal. 20 | 21 | ```solidity 22 | function description() public pure override returns (string memory) { 23 | return "Mock proposal for upgrading the WETH gateway"; 24 | } 25 | ``` 26 | 27 | - `deploy()`: This function demonstrates the deployment of a new MockUpgrade, which will be used as the new implementation for the WETH Gateway Proxy and a new GAC contract for the upgrade. 28 | 29 | ```solidity 30 | function deploy() public override { 31 | // Deploy new WETH gateway implementation if not already deployed 32 | if ( 33 | !addresses.isAddressSet("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION") 34 | ) { 35 | // In a real case, this function would be responsible for 36 | // deploying a new implementation contract instead of using a mock 37 | address l1NFTBridgeImplementation = address(new MockUpgrade()); 38 | 39 | addresses.addAddress( 40 | "ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION", 41 | l1NFTBridgeImplementation, 42 | true 43 | ); 44 | } 45 | 46 | // Deploy new GAC contract for gateway upgrade if not already deployed 47 | if (!addresses.isAddressSet("ARBITRUM_GAC_UPGRADE_WETH_GATEWAY")) { 48 | address gac = address(new GovernanceActionUpgradeWethGateway()); 49 | addresses.addAddress( 50 | "ARBITRUM_GAC_UPGRADE_WETH_GATEWAY", 51 | gac, 52 | true 53 | ); 54 | } 55 | } 56 | ``` 57 | 58 | - `preBuildMock()`: Post-deployment mock actions, such as setting a new `outBox` for `Arbitrum bridge` using `vm.store` foundry cheatcode. 59 | 60 | ```solidity 61 | function preBuildMock() public override { 62 | // Deploy new mockOutBox address 63 | address mockOutbox = address(new MockOutbox()); 64 | 65 | // This is a workaround to replace the mainnet outBox with the newly deployed one for testing purposes only 66 | vm.store( 67 | addresses.getAddress("ARBITRUM_BRIDGE"), 68 | bytes32(uint256(5)), 69 | bytes32(uint256(uint160(mockOutbox))) 70 | ); 71 | } 72 | ``` 73 | 74 | - `build()`: Add actions to the proposal contract. In this example, `ARBITRUM_L1_WETH_GATEWAY_PROXY` is upgraded to the new implementation. The actions should be written in solidity code and in the order they should be executed. Any calls (except to the Addresses object) will be recorded and stored as actions to execute in the run function. The `caller` address is passed into `buildModifier`; it will call the actions in `build`. The caller is the Arbitrum timelock in this example. The `buildModifier` is a necessary modifier for the `build` function and will not work without it. For further reading, see the [build function](../overview/architecture/proposal-functions.md#build-function). 75 | 76 | ```solidity 77 | function build() public override buildModifier(address(timelock)) { 78 | /// STATICCALL -- not recorded for the run stage 79 | 80 | // Get upgrade executor address 81 | IUpgradeExecutor upgradeExecutor = IUpgradeExecutor( 82 | addresses.getAddress("ARBITRUM_L1_UPGRADE_EXECUTOR") 83 | ); 84 | 85 | /// CALLS -- mutative and recorded 86 | 87 | // Upgrade WETH gateway using GAC contract to the newly deployed implementation 88 | upgradeExecutor.execute( 89 | addresses.getAddress("ARBITRUM_GAC_UPGRADE_WETH_GATEWAY"), 90 | abi.encodeWithSelector( 91 | GovernanceActionUpgradeWethGateway.upgradeWethGateway.selector, 92 | addresses.getAddress("ARBITRUM_L1_PROXY_ADMIN"), 93 | addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_PROXY"), 94 | addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION") 95 | ) 96 | ); 97 | } 98 | ``` 99 | 100 | - `run()`: Sets up the environment for running the proposal, and executes all proposal actions. This sets `addresses`, `primaryForkId`, and `timelock` and calls `super.run()` to run the entire proposal. In this example, `primaryForkId` is set to `mainnet` and the fork for running the proposal is selected. Next, the `addresses` object is set by reading the JSON file. The timelock contract to test is set using `setTimelock`. This will be used to check onchain calldata and simulate the proposal. For further reading, see the [run function](../overview/architecture/proposal-functions.md#run-function). 101 | 102 | ```solidity 103 | function run() public override { 104 | // Create and select the mainnet fork for proposal execution 105 | primaryForkId = vm.createFork("mainnet"); 106 | vm.selectFork(primaryForkId); 107 | 108 | uint256[] memory chainIds = new uint256[](1); 109 | chainIds[0] = 1; 110 | // Set the addresses object by reading addresses from the json file 111 | addresses = new Addresses( 112 | vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds 113 | ); 114 | 115 | // Set the timelock. This address is used for proposal simulation and checking on-chain proposal state 116 | setTimelock(addresses.getAddress("ARBITRUM_L1_TIMELOCK")); 117 | 118 | // Call the run function of the parent contract 'Proposal.sol' 119 | super.run(); 120 | } 121 | ``` 122 | 123 | - `simulate()`: Executes the proposal actions outlined in the `build()` step. This function performs a call to `_simulateActions` from the inherited `TimelockProposal` contract. Internally, `_simulateActions()` simulates a call to Timelock [scheduleBatch](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/governance/TimelockController.sol#L291) and [executeBatch](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/governance/TimelockController.sol#L385) with the calldata generated from the actions set up in the build step. 124 | 125 | ```solidity 126 | function simulate() public override { 127 | // Proposer must be the Arbitrum bridge 128 | address proposer = addresses.getAddress("ARBITRUM_BRIDGE"); 129 | 130 | // Executor can be anyone 131 | address executor = address(1); 132 | 133 | // Simulate the actions in the `build` function 134 | _simulateActions(proposer, executor); 135 | } 136 | ``` 137 | 138 | - `validate()`: Validates that the implementation is upgraded correctly. 139 | 140 | ```solidity 141 | function validate() public override { 142 | // Get proxy address 143 | IProxy proxy = IProxy( 144 | addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_PROXY") 145 | ); 146 | 147 | // Ensure implementation is upgraded to the newly deployed implementation 148 | require( 149 | proxy.implementation() == 150 | addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION"), 151 | "Proxy implementation not set" 152 | ); 153 | } 154 | ``` 155 | 156 | ## Running the Proposal 157 | 158 | ```sh 159 | forge script mocks/MockTimelockProposal.sol:MockTimelockProposal --fork-url mainnet 160 | ``` 161 | 162 | All required addresses should be in the JSON file, including `DEPLOYER_EOA` address, which will deploy the new contracts. If these do not align, the script execution will fail. 163 | 164 | The script will output the following: 165 | 166 | ```sh 167 | == Logs == 168 | 169 | 170 | --------- Addresses added --------- 171 | { 172 | "addr": "0x714CB817EfD08fEe91558b07A924a87C3587F3C1", 173 | "isContract": true, 174 | "name": "ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION" 175 | }, 176 | { 177 | "addr": "0x56a0dFA59fD02284D1b39327CfE92251051Da6bb", 178 | "isContract": true, 179 | "name": "ARBITRUM_GAC_UPGRADE_WETH_GATEWAY" 180 | } 181 | 182 | ---------------- Proposal Description ---------------- 183 | Mock proposal that upgrades the weth gateway 184 | 185 | ------------------ Proposal Actions ------------------ 186 | 1). calling ARBITRUM_L1_UPGRADE_EXECUTOR @0x3ffFbAdAF827559da092217e474760E2b2c3CeDd with 0 eth and 0x1cff79cd00000000000000000000000056a0dfa59fd02284d1b39327cfe92251051da6bb0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006409b461c10000000000000000000000009ad46fac0cf7f790e5be05a0f15223935a0c0ada000000000000000000000000d92023e9d9911199a6711321d1277285e6d4e2db000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c100000000000000000000000000000000000000000000000000000000 data. 187 | target: ARBITRUM_L1_UPGRADE_EXECUTOR @0x3ffFbAdAF827559da092217e474760E2b2c3CeDd 188 | payload 189 | 0x1cff79cd00000000000000000000000056a0dfa59fd02284d1b39327cfe92251051da6bb0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006409b461c10000000000000000000000009ad46fac0cf7f790e5be05a0f15223935a0c0ada000000000000000000000000d92023e9d9911199a6711321d1277285e6d4e2db000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c100000000000000000000000000000000000000000000000000000000 190 | 191 | 192 | 193 | ----------------- Proposal Changes --------------- 194 | 195 | 196 | ARBITRUM_L1_UPGRADE_EXECUTOR @0x3ffFbAdAF827559da092217e474760E2b2c3CeDd: 197 | 198 | State Changes: 199 | Slot: 0x0000000000000000000000000000000000000000000000000000000000000097 200 | - 0x0000000000000000000000000000000000000000000000000000000000000001 201 | + 0x0000000000000000000000000000000000000000000000000000000000000002 202 | Slot: 0x0000000000000000000000000000000000000000000000000000000000000097 203 | - 0x0000000000000000000000000000000000000000000000000000000000000002 204 | + 0x0000000000000000000000000000000000000000000000000000000000000001 205 | 206 | 207 | ARBITRUM_L1_WETH_GATEWAY_PROXY @0xd92023E9d9911199a6711321D1277285e6d4e2db: 208 | 209 | State Changes: 210 | Slot: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 211 | - 0x0000000000000000000000004b8e9b3f253e68837bf719997b1eeb9e8f1960e2 212 | + 0x000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c1 213 | 214 | 215 | ------------------ Schedule Calldata ------------------ 216 | 0x8f2a0bb000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000050deb3e0ef55ff1976003bef5ca1a251beebbeb0d17ef15e6340ea825bbfe8e8000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000010000000000000000000000003fffbadaf827559da092217e474760e2b2c3cedd000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e41cff79cd00000000000000000000000056a0dfa59fd02284d1b39327cfe92251051da6bb0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006409b461c10000000000000000000000009ad46fac0cf7f790e5be05a0f15223935a0c0ada000000000000000000000000d92023e9d9911199a6711321d1277285e6d4e2db000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 217 | 218 | 219 | ------------------ Execute Calldata ------------------ 220 | 0xe38335e500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000050deb3e0ef55ff1976003bef5ca1a251beebbeb0d17ef15e6340ea825bbfe8e800000000000000000000000000000000000000000000000000000000000000010000000000000000000000003fffbadaf827559da092217e474760e2b2c3cedd000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e41cff79cd00000000000000000000000056a0dfa59fd02284d1b39327cfe92251051da6bb0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006409b461c10000000000000000000000009ad46fac0cf7f790e5be05a0f15223935a0c0ada000000000000000000000000d92023e9d9911199a6711321d1277285e6d4e2db000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 221 | ``` 222 | 223 | It is crucial to note that two new addresses have been added to the `Addresses.sol` storage. These addresses are not included in the JSON files when proposal is run without the `DO_UPDATE_ADDRESS_JSON` flag set to true. 224 | -------------------------------------------------------------------------------- /docs/mainnet-examples/CompoundGovernorBravo.md: -------------------------------------------------------------------------------- 1 | # Compound Governor Bravo Proposal 2 | 3 | ## Overview 4 | 5 | This serves as a mainnet example of FPS, where FPS is utilized to propose adjustments for the Compound Governor Bravo. No deployments are included in this example. Specifically, this example sets Comet's borrow and supply kink to 0.75 \* 1e18 through the Compound Configurator. 6 | 7 | The contract outlined below is located in the [mocks folder](../../mocks/MockBravoProposal.sol). 8 | 9 | Let's examine each of the functions that are overridden: 10 | 11 | - `name()`: Specifies the name of the proposal. 12 | 13 | ```solidity 14 | function name() public pure override returns (string memory) { 15 | return "ADJUST_WETH_IR_CURVE"; 16 | } 17 | ``` 18 | 19 | - `description()`: Offers a detailed description of the proposal. 20 | 21 | ```solidity 22 | function description() public pure override returns (string memory) { 23 | return 24 | "Mock proposal to adjust the IR Curve for Compound v3 WETH on Mainnet"; 25 | } 26 | ``` 27 | 28 | - `build()`: Add actions to the proposal contract. In this instance, borrow and supply kink are set through the configurator by Bravo's timelock. The actions should be written in Solidity code and in the order they are intended to be executed. Any calls (except to the Addresses object) will be recorded and stored as actions to execute in the run function. The `caller` address is passed into `buildModifier`, which will call actions in `build`. In this example, the caller is Governor's timelock. The `buildModifier` is a necessary modifier for the `build` function and will not function without it. For further reading, see the [build function](../overview/architecture/proposal-functions.md#build-function). 29 | 30 | ```solidity 31 | function build() 32 | public 33 | override 34 | buildModifier(addresses.getAddress("COMPOUND_TIMELOCK_BRAVO")) 35 | { 36 | /// STATICCALL -- not recorded for the run stage 37 | 38 | // get configurator address 39 | ICompoundConfigurator configurator = ICompoundConfigurator( 40 | addresses.getAddress("COMPOUND_CONFIGURATOR") 41 | ); 42 | 43 | // get comet address 44 | address comet = addresses.getAddress("COMPOUND_COMET"); 45 | 46 | /// CALLS -- mutative and recorded 47 | 48 | // set borrow kink to 0.75 * 1e18 49 | configurator.setBorrowKink(comet, kink); 50 | 51 | // set supply kink to 0.75 * 1e18 52 | configurator.setSupplyKink(comet, kink); 53 | } 54 | ``` 55 | 56 | - `run()`: Sets up the environment for running the proposal. This sets `addresses`, `primaryForkId`, and `governor`, and then calls `super.run()` to run the entire proposal. In this example, `primaryForkId` is set to `mainnet`, selecting the fork for running the proposal. Next, the `addresses` object is set by reading the JSON file. The Governor Bravo contract to test is set using `setGovernor`. This will be used to check onchain calldata and simulate the proposal. For further reading, see the [run function](../overview/architecture/proposal-functions.md#run-function). 57 | 58 | ```solidity 59 | function run() public override { 60 | // Create and select the mainnet fork for proposal execution. 61 | primaryForkId = vm.createFork("mainnet"); 62 | vm.selectFork(primaryForkId); 63 | 64 | uint256[] memory chainIds = new uint256[](1); 65 | chainIds[0] = 1; 66 | // Set the addresses object by reading addresses from the JSON file. 67 | setAddresses( 68 | new Addresses( 69 | vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds 70 | ) 71 | ); 72 | 73 | // Set Governor Bravo. This address is used for proposal simulation and checking the on-chain proposal state. 74 | setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 75 | 76 | // Call the run function of the parent contract 'Proposal.sol'. 77 | super.run(); 78 | } 79 | ``` 80 | 81 | - `validate()`: Validates that the supply and borrow kink are set correctly. 82 | 83 | ```solidity 84 | function validate() public view override { 85 | // get configurator address 86 | ICompoundConfigurator configurator = ICompoundConfigurator( 87 | addresses.getAddress("COMPOUND_CONFIGURATOR") 88 | ); 89 | 90 | // get comet address 91 | address comet = addresses.getAddress("COMPOUND_COMET"); 92 | 93 | // get comet configuration 94 | ICompoundConfigurator.Configuration memory config = configurator 95 | .getConfiguration(comet); 96 | 97 | // ensure supply kink is set to 0.75 * 1e18 98 | assertEq(config.supplyKink, kink); 99 | 100 | // ensure borrow kink is set to 0.75 * 1e18 101 | assertEq(config.borrowKink, kink); 102 | } 103 | ``` 104 | 105 | ## Running the Proposal 106 | 107 | ```sh 108 | forge script mocks/MockBravoProposal.sol --fork-url mainnet 109 | ``` 110 | 111 | All required addresses should be in the JSON file, including the `DEPLOYER_EOA` address, which will deploy the new contracts. If these do not align, the script execution will fail. 112 | 113 | The script will output the following: 114 | 115 | ```sh 116 | == Logs == 117 | 118 | ---------------- Proposal Description ---------------- 119 | Mock proposal that adjust IR Curve for Compound v3 WETH on Mainnet 120 | 121 | ------------------ Proposal Actions ------------------ 122 | 1). calling COMPOUND_CONFIGURATOR @0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3 with 0 eth and 0x5bfb8373000000000000000000000000a17581a9e3356d9a858b789d68b4d866e593ae940000000000000000000000000000000000000000000000000a688906bd8b0000 data. 123 | target: COMPOUND_CONFIGURATOR @0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3 124 | payload 125 | 0x5bfb8373000000000000000000000000a17581a9e3356d9a858b789d68b4d866e593ae940000000000000000000000000000000000000000000000000a688906bd8b0000 126 | 127 | 128 | 2). calling COMPOUND_CONFIGURATOR @0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3 with 0 eth and 0x058e4155000000000000000000000000a17581a9e3356d9a858b789d68b4d866e593ae940000000000000000000000000000000000000000000000000a688906bd8b0000 data. 129 | target: COMPOUND_CONFIGURATOR @0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3 130 | payload 131 | 0x058e4155000000000000000000000000a17581a9e3356d9a858b789d68b4d866e593ae940000000000000000000000000000000000000000000000000a688906bd8b0000 132 | 133 | 134 | 135 | ----------------- Proposal Changes --------------- 136 | 137 | 138 | COMPOUND_CONFIGURATOR @0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3: 139 | 140 | State Changes: 141 | Slot: 0x81786960a4c38938c01fafa8d0783bc04e719c248eafc32d7335695ed80183a0 142 | - 0x0bcbce7f1b15000000000000000000000de0b6b3a76400000041b9a6e8584000 143 | + 0x0a688906bd8b000000000000000000000de0b6b3a76400000041b9a6e8584000 144 | Slot: 0x81786960a4c38938c01fafa8d0783bc04e719c248eafc32d7335695ed801839f 145 | - 0x000000000bcbce7f1b150000e2c1f54aff6b38fd9df7a69f22cb5fd3ba09f030 146 | + 0x000000000a688906bd8b0000e2c1f54aff6b38fd9df7a69f22cb5fd3ba09f030 147 | 148 | 149 | ------------------ Proposal Calldata ------------------ 150 | 0xda95691a00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000316f9708bb98af7da9c68c1c3b5e79039cd336e3000000000000000000000000316f9708bb98af7da9c68c1c3b5e79039cd336e3000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000445bfb8373000000000000000000000000a17581a9e3356d9a858b789d68b4d866e593ae940000000000000000000000000000000000000000000000000a688906bd8b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044058e4155000000000000000000000000a17581a9e3356d9a858b789d68b4d866e593ae940000000000000000000000000000000000000000000000000a688906bd8b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000424d6f636b2070726f706f73616c20746861742061646a75737420495220437572766520666f7220436f6d706f756e642076332057455448206f6e204d61696e6e6574000000000000000000000000000000000000000000000000000000000000 151 | ``` 152 | -------------------------------------------------------------------------------- /docs/mainnet-examples/ENSOzGovernor.md: -------------------------------------------------------------------------------- 1 | # ENS OZ Governor Proposal 2 | 3 | ## Overview 4 | 5 | This example on mainnet demonstrates FPS being utilized to simulate proposals for the ENS Governor on mainnet. The proposal involves setting up a new DNSSEC on the ENS root. It entails deploying a new DNSSEC contract named `UPGRADE_DNSSEC_SUPPORT`. Subsequently, the timelock sets the newly deployed DNSSEC contract as the controller for the ENS Root. 6 | 7 | The contract for this proposal is located in the [mocks folder](../../mocks/MockOZGovernorProposal.sol). 8 | 9 | Let's review each of the overridden functions: 10 | 11 | - `name()`: Specifies the name of the proposal. 12 | 13 | ```solidity 14 | function name() public pure override returns (string memory) { 15 | return "UPGRADE_DNSSEC_SUPPORT"; 16 | } 17 | ``` 18 | 19 | - `description()`: Provides a detailed description of the proposal. 20 | 21 | ```solidity 22 | function description() public pure override returns (string memory) { 23 | return 24 | "Call setController on the Root contract at root.ens.eth, passing in the address of the new DNS registrar"; 25 | } 26 | ``` 27 | 28 | - `deploy()`: Deploys any necessary contracts. This example demonstrates the deployment of a new `dnsSec` contract (only a mock for this proposal). Once the contracts are deployed, they are added to the `Addresses` contract by calling `addAddress()`. 29 | 30 | ```solidity 31 | function deploy() public override { 32 | // Deploy a mock upgrade contract to set controller if not already deployed 33 | if (!addresses.isAddressSet("ENS_DNSSEC")) { 34 | // In a real case, this function would be responsible for 35 | // deploying the DNSSEC contract instead of using a mock 36 | address dnsSec = address(new MockUpgrade()); 37 | 38 | addresses.addAddress("ENS_DNSSEC", dnsSec, true); 39 | } 40 | } 41 | ``` 42 | 43 | Since these changes do not persist from runs themselves, after the contracts are deployed, the user must update the Addresses.json file with the newly deployed contract addresses. 44 | 45 | - `build()`: Add actions to the proposal contract. In this example, the newly deployed `dnsSec` contract is set as the controller for the root contract. Any calls (except to the Addresses object) will be recorded and stored as actions to execute in the run function. The `caller` address that will call actions is passed into `buildModifier`. In this example, it is the OZ Governor's timelock. The `buildModifier` is a necessary modifier for the `build` function and will not function without it. For further reading, see the [build function](../overview/architecture/proposal-functions.md#build-function). 46 | 47 | ```solidity 48 | function build() 49 | public 50 | override 51 | buildModifier(addresses.getAddress("ENS_TIMELOCK")) 52 | { 53 | /// STATICCALL -- non-mutative and hence not recorded for the run stage 54 | 55 | // Get ENS root address 56 | IControllable control = IControllable(addresses.getAddress("ENS_ROOT")); 57 | 58 | // Get deployed dnsSec address 59 | address dnsSec = addresses.getAddress("ENS_DNSSEC"); 60 | 61 | /// CALLS -- mutative and recorded 62 | 63 | // Set controller to newly deployed dnsSec contract 64 | control.setController(dnsSec, true); 65 | } 66 | ``` 67 | 68 | - `run()`: Sets up the environment for running the proposal, and executes all proposal actions. This sets `addresses`, `primaryForkId`, and `governor`, and then calls `super.run()` to run the entire proposal. In this example, `primaryForkId` is set to `mainnet`, selecting the fork for running the proposal. Next, the `addresses` object is set by reading the `addresses.json` file. The OZ Governor contract to test is set using `setGovernor`. This will be used to check onchain calldata and simulate the proposal. For further reading, see the [run function](../overview/architecture/proposal-functions.md#run-function). 69 | 70 | ```solidity 71 | function run() public override { 72 | // Create and select the mainnet fork for proposal execution. 73 | setPrimaryForkId(vm.createFork("mainnet")); 74 | vm.selectFork(primaryForkId); 75 | 76 | uint256[] memory chainIds = new uint256[](1); 77 | chainIds[0] = 1; 78 | // Set the addresses object by reading addresses from the JSON file. 79 | setAddresses( 80 | new Addresses( 81 | vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds 82 | ) 83 | ); 84 | 85 | // Set Governor Bravo. This address is used for proposal simulation and checking the on-chain proposal state. 86 | setGovernor(addresses.getAddress("ENS_GOVERNOR")); 87 | 88 | // Call the run function of the parent contract 'Proposal.sol'. 89 | super.run(); 90 | } 91 | ``` 92 | 93 | - `validate()`: This final step validates the system in its post-execution state. It ensures that the dnsSec contract is set as the controller for the root contract. 94 | 95 | ```solidity 96 | function validate() public view override { 97 | // Get ENS root address 98 | IControllable control = IControllable(addresses.getAddress("ENS_ROOT")); 99 | 100 | // Get deployed dnsSec address 101 | address dnsSec = addresses.getAddress("ENS_DNSSEC"); 102 | 103 | // Ensure dnsSec is set as the controller for the ENS root contract 104 | assertEq(control.controllers(dnsSec), true); 105 | } 106 | ``` 107 | 108 | ## Running the Proposal 109 | 110 | ```sh 111 | forge script mocks/MockOZGovernorProposal.sol:MockOZGovernorProposal --fork-url mainnet 112 | ``` 113 | 114 | All required addresses should be in the Addresses.json file, including the `DEPLOYER_EOA` address, which will deploy the new contracts. If these do not align, the script execution will fail. 115 | 116 | The script will output the following: 117 | 118 | ```sh 119 | == Logs == 120 | 121 | 122 | --------- Addresses added --------- 123 | { 124 | "addr": "0x714CB817EfD08fEe91558b07A924a87C3587F3C1", 125 | "isContract": true, 126 | "name": "ENS_DNSSEC" 127 | } 128 | 129 | ---------------- Proposal Description ---------------- 130 | Call setController on the Root contract at root.ens.eth, passing in the address of the new DNS registrar 131 | 132 | ------------------ Proposal Actions ------------------ 133 | 1). calling ENS_ROOT @0xaB528d626EC275E3faD363fF1393A41F581c5897 with 0 eth and 0xe0dba60f000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c10000000000000000000000000000000000000000000000000000000000000001 data. 134 | target: ENS_ROOT @0xaB528d626EC275E3faD363fF1393A41F581c5897 135 | payload 136 | 0xe0dba60f000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c10000000000000000000000000000000000000000000000000000000000000001 137 | 138 | 139 | 140 | ----------------- Proposal Changes --------------- 141 | 142 | 143 | ENS_ROOT @0xaB528d626EC275E3faD363fF1393A41F581c5897: 144 | 145 | State Changes: 146 | Slot: 0x683b779d654146db8352b5203c98de0bc792fdb59471541d9a885b4f9933a736 147 | - 0x0000000000000000000000000000000000000000000000000000000000000000 148 | + 0x0000000000000000000000000000000000000000000000000000000000000001 149 | 150 | 151 | ------------------ Proposal Calldata ------------------ 152 | 0x7d5e81e2000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000ab528d626ec275e3fad363ff1393a41f581c589700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000044e0dba60f000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006843616c6c20736574436f6e74726f6c6c6572206f6e2074686520526f6f7420636f6e747261637420617420726f6f742e656e732e6574682c2070617373696e6720696e207468652061646472657373206f6620746865206e657720444e5320726567697374726172000000000000000000000000000000000000000000000000 153 | ``` 154 | -------------------------------------------------------------------------------- /docs/mainnet-examples/OptimismMultisig.md: -------------------------------------------------------------------------------- 1 | # Optimism Multisig Proposal 2 | 3 | ## Overview 4 | 5 | This is an example where FPS is used to make proposals for the Optimism Multisig on mainnet. This example upgrades the L1 NFT Bridge contract. The Optimism Multisig calls `upgrade` on the proxy contract to upgrade the implementation to a new `MockUpgrade`. 6 | 7 | The following contract is present in the [mocks folder](../../mocks/MockMultisigProposal.sol). 8 | 9 | Let's go through each of the functions that are overridden: 10 | 11 | - `name()`: Defines the name of your proposal. 12 | 13 | ```solidity 14 | function name() public pure override returns (string memory) { 15 | return "OPTIMISM_MULTISIG_MOCK"; 16 | } 17 | ``` 18 | 19 | - `description()`: Provides a detailed description of your proposal. 20 | 21 | ```solidity 22 | function description() public pure override returns (string memory) { 23 | return "Mock proposal that upgrades the L1 NFT Bridge"; 24 | } 25 | ``` 26 | 27 | - `deploy()`: This example demonstrates the deployment of the new MockUpgrade, which will be used as the new implementation for the proxy. 28 | 29 | ```solidity 30 | function deploy() public override { 31 | if (!addresses.isAddressSet("OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION")) { 32 | address l1NFTBridgeImplementation = address(new MockUpgrade()); 33 | 34 | addresses.addAddress( 35 | "OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION", 36 | l1NFTBridgeImplementation, 37 | true 38 | ); 39 | } 40 | } 41 | ``` 42 | 43 | Since these changes do not persist from runs themselves, after the contracts are deployed, the user must update the Addresses.json file with the newly deployed contract addresses. 44 | 45 | - `build()`: Add actions to the proposal contract. In this example, the L1 NFT Bridge is upgraded to a new implementation. The actions should be written in solidity code and in the order they should be executed. Any calls (except to the Addresses object) will be recorded and stored as actions to execute in the run function. The `caller` address is passed into `buildModifier` that will call actions in `build`. The caller is the Optimism Multisig for this example. The `buildModifier` is a necessary modifier for the `build` function and will not work without it. For further reading, see the [build function](../overview/architecture/proposal-functions.md#build-function). 46 | 47 | ```solidity 48 | function build() 49 | public 50 | override 51 | buildModifier(addresses.getAddress("OPTIMISM_MULTISIG")) 52 | { 53 | /// STATICCALL -- not recorded for the run stage 54 | IProxyAdmin proxy = IProxyAdmin( 55 | addresses.getAddress("OPTIMISM_PROXY_ADMIN") 56 | ); 57 | 58 | /// CALLS -- mutative and recorded 59 | proxy.upgrade( 60 | addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_PROXY"), 61 | addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION") 62 | ); 63 | } 64 | ``` 65 | 66 | - `run()`: Sets up the environment for running the proposal, and executes all proposal actions. This sets `addresses`, `primaryForkId`, and calls `super.run()` to run the entire proposal. In this example, `primaryForkId` is set to `mainnet` and selecting the fork for running the proposal. Next, the `addresses` object is set by reading from the JSON file. For further reading, see the [run function](../overview/architecture/proposal-functions.md#run-function). 67 | 68 | ```solidity 69 | function run() public override { 70 | // Create and select mainnet fork for proposal execution. 71 | primaryForkId = vm.createFork("mainnet"); 72 | vm.selectFork(primaryForkId); 73 | 74 | uint256[] memory chainIds = new uint256[](1); 75 | chainIds[0] = 1; 76 | // Set the addresses object by reading addresses from the JSON file. 77 | setAddresses( 78 | new Addresses( 79 | vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds 80 | ) 81 | ); 82 | 83 | // Call the run function of parent contract 'Proposal.sol'. 84 | super.run(); 85 | } 86 | ``` 87 | 88 | - `simulate()`: Executes the proposal actions outlined in the `build()` step. This function performs a call to `_simulateActions()` from the inherited `MultisigProposal` contract. Internally, `_simulateActions()` simulates a call to the [Multicall3](https://www.multicall3.com/) contract with the calldata generated from the actions set up in the build step. 89 | 90 | ```solidity 91 | function simulate() public override { 92 | // get multisig address 93 | address multisig = addresses.getAddress("OPTIMISM_MULTISIG"); 94 | 95 | // simulate all actions in 'build' functions through multisig 96 | _simulateActions(multisig); 97 | } 98 | ``` 99 | 100 | - `validate()`: Validates that the implementation is upgraded correctly. 101 | 102 | ```solidity 103 | function validate() public override { 104 | // get proxy address 105 | IProxy proxy = IProxy( 106 | addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_PROXY") 107 | ); 108 | 109 | // implementation() caller must be the owner 110 | vm.startPrank(addresses.getAddress("OPTIMISM_PROXY_ADMIN")); 111 | 112 | // ensure implementation is upgraded 113 | require( 114 | proxy.implementation() == 115 | addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION"), 116 | "Proxy implementation not set" 117 | ); 118 | vm.stopPrank(); 119 | } 120 | ``` 121 | 122 | ## Running the Proposal 123 | 124 | ```sh 125 | forge script mocks/MockMultisigProposal.sol --fork-url mainnet 126 | ``` 127 | 128 | All required addresses should be in the JSON file, including the `DEPLOYER_EOA` address, which will deploy the new contracts. If these do not align, the script execution will fail. 129 | 130 | The script will output the following: 131 | 132 | ```sh 133 | == Logs == 134 | 135 | 136 | --------- Addresses added --------- 137 | { 138 | "addr": "0x714CB817EfD08fEe91558b07A924a87C3587F3C1", 139 | "isContract": true, 140 | "name": "OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION" 141 | } 142 | 143 | ---------------- Proposal Description ---------------- 144 | Mock proposal that upgrade the L1 NFT Bridge 145 | 146 | ------------------ Proposal Actions ------------------ 147 | 1). calling OPTIMISM_PROXY_ADMIN @0x543bA4AADBAb8f9025686Bd03993043599c6fB04 with 0 eth and 0x99a88ec40000000000000000000000005a7749f83b81b301cab5f48eb8516b986daef23d000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c1 data. 148 | target: OPTIMISM_PROXY_ADMIN @0x543bA4AADBAb8f9025686Bd03993043599c6fB04 149 | payload 150 | 0x99a88ec40000000000000000000000005a7749f83b81b301cab5f48eb8516b986daef23d000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c1 151 | 152 | 153 | 154 | ----------------- Proposal Changes --------------- 155 | 156 | 157 | OPTIMISM_L1_NFT_BRIDGE_PROXY @0x5a7749f83b81B301cAb5f48EB8516B986DAef23D: 158 | 159 | State Changes: 160 | Slot: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 161 | - 0x000000000000000000000000ae2af01232a6c4a4d3012c5ec5b1b35059caf10d 162 | + 0x000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c1 163 | 164 | 165 | ------------------ Proposal Calldata ------------------ 166 | 0x174dea71000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000543ba4aadbab8f9025686bd03993043599c6fb04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004499a88ec40000000000000000000000005a7749f83b81b301cab5f48eb8516b986daef23d000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c100000000000000000000000000000000000000000000000000000000 167 | ``` 168 | -------------------------------------------------------------------------------- /docs/overview/architecture/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | The Forge Proposal Simulator (FPS) offers a versatile solution for protocols with trusted actors to create and validate governance proposals. 4 | --- 5 | 6 | # Architecture 7 | 8 | FPS design architecture 9 | 10 | The diagram illustrates the architecture of the Forge Proposal Simulator. It is composed of various components that interact with each other to simulate, execute, and test governance proposals. 11 | 12 | ## Proposal Generic Contract 13 | 14 | At its core, the FPS features a [Proposal.sol](../../../src/proposals/Proposal.sol) contract that defines [functions](proposal-functions.md) that can be overridden to adapt to specific governance architectures. The `run` function serves as the entry point to execute a proposal using the `forge script`. 15 | 16 | ## Governance Specific Contracts 17 | 18 | FPS supports different Governance types (e.g., Timelock, Multisig, Governor Bravo, OZ Governor) through proposal contract types inheriting from [Proposal.sol](../../../src/proposals/Proposal.sol), customizing their functions to unique governance requirements. New proposal types can be included to support different governance contracts. 19 | 20 | ## Proposal Specific Contract 21 | 22 | Protocols using FPS must create their own Proposal Specific Contracts, conforming to FPS standards. These contracts override functions relevant to the particular proposal, such as `deploy()` and `preBuildMock()` for proposals involving new contract deployments. For more details, refer to [proposal functions](proposal-functions.md). 23 | 24 | {% content-ref url="proposal-functions.md" %} 25 | [proposal-functions.md](proposal-functions.md) 26 | {% endcontent-ref %} 27 | 28 | {% content-ref url="addresses.md" %} 29 | [addresses.md](addresses.md) 30 | {% endcontent-ref %} 31 | -------------------------------------------------------------------------------- /docs/overview/architecture/addresses.md: -------------------------------------------------------------------------------- 1 | # Addresses 2 | 3 | ## Overview 4 | 5 | The Addresses contract plays an important role in managing and storing the addresses of deployed contracts and protocol EOAs. This functionality is essential for facilitating access to these contracts within proposal contracts and ensuring accurate record-keeping post-execution. Additionally, this contract contains important safety checks such as checking bytecode and providing error messages when non-existent addresses are queried. 6 | 7 | ## Structure 8 | 9 | Deployed contract addresses are registered along with their respective names. This data is stored in an array within a JSON file, the JSON file is named with the chain id representing the network to which the addresses belong. If there are deployments on multiple networks, files correspoding to each network are created. JSON files adhere to the following example format: 10 | 11 | ```json 12 | [ 13 | { 14 | "addr": "0x3dd46846eed8D147841AE162C8425c08BD8E1b41", 15 | "name": "DEV_MULTISIG", 16 | "isContract": true 17 | }, 18 | { 19 | "addr": "0x7da82C7AB4771ff031b66538D2fB9b0B047f6CF9", 20 | "name": "TEAM_MULTISIG", 21 | "isContract": true 22 | }, 23 | { 24 | "addr": "0x1a9C8182C09F50C8318d769245beA52c32BE35BC", 25 | "name": "PROTOCOL_TIMELOCK", 26 | "isContract": true 27 | }, 28 | { 29 | "addr": "0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f", 30 | "name": "DEPLOYER_EOA", 31 | "isContract": true 32 | } 33 | ] 34 | ``` 35 | 36 | Here is an example folder tree structure of multiple JSON files corresponding to different networks. 37 | ``` 38 | addresses/ 39 | 1.json 40 | 31337.json 41 | 11155111.json 42 | ``` 43 | 44 | FPS allows contracts with identical names as long as they are deployed on different networks. However, duplicates on the same network are not permitted. The `Addresses.sol` contract enforces this rule by reverting during construction if such a duplicate is detected. It also checks that the same address is not set under two different names on the same network. 45 | 46 | ## Functions 47 | 48 | ### Adding 49 | 50 | Addresses can be added to the object during a proposal or test by calling the `addAddress` function with the name to be saved in storage, the address of the contract to be stored with that name and whether the address is a contract. Calling this function without a chain id will save the contract and name to the current chain id. 51 | 52 | ```solidity 53 | addresses.addAddress("CONTRACT_NAME", contractAddress, isContract); 54 | ``` 55 | 56 | If the address needs to be added to a chain id that is not the current chain id, that address can still be added by calling the same function with an additional chain id parameter. 57 | 58 | ```solidity 59 | addresses.addAddress("CONTRACT_NAME", contractAddress, chainId, isContract); 60 | ``` 61 | 62 | FPS has the following type checks implemented for the function `addAddress`: 63 | 64 | - Address must be unique for a given name and chain id. 65 | - Address must be non-zero. 66 | - Chain id must be non-zero. 67 | - Address must be a contract in the specified chain if `isContract` is set to `true`. 68 | - Address must not be a contract in the specified chain if `isContract` is set to `false`. 69 | 70 | Addresses can be added before the proposal runs by modifying the Addresses JSON file. After a successful deployment, the `getRecordedAddresses` function will return all of the newly deployed addresses and their respective names and chain id's. 71 | 72 | ### Updating 73 | 74 | If an address is already stored, and the name stays the same, but the address changes during a proposal or test, the `changeAddress` function can be called with the new address for the name. 75 | 76 | ```solidity 77 | addresses.changeAddress("CONTRACT_NAME", contractAddress, isContract); 78 | ``` 79 | 80 | If the address needs to be updated on a chain id that is not the current chain id, that address can still be updated by calling the same function with an additional chain id parameter. 81 | 82 | ```solidity 83 | addresses.changeAddress("CONTRACT_NAME", contractAddress, chainId); 84 | ``` 85 | 86 | FPS has the following type checks implemented for the function `changeAddress`: 87 | 88 | - Address must be unique for a given name and chain id. 89 | - Address must be non-zero. 90 | - Chain id must be non-zero. 91 | - Address must be a contract in the specified chain if `isContract` is set to `true`. 92 | - Address must not be a contract in the specified chain if `isContract` is set to `false`. 93 | - Address must be different from the existing address. 94 | - An address for the specified name must already exist. 95 | 96 | After a proposal that changes the address, the `getChangedAddresses` function should be called. This will return all of the old addresses, new addresses, and their respective names and chain id's. 97 | 98 | ### Removing 99 | 100 | An address can be removed from storage by removing its entry from the Addresses JSON file. This way, when the Address contract is constructed, the name and address will not be saved to storage. Addresses should not be removed during a governance proposal or test. 101 | 102 | ### Retrieving 103 | 104 | Addresses can be retrieved by calling the `getAddress` function with the name of the contract. 105 | 106 | ```solidity 107 | addresses.getAddress("CONTRACT_NAME"); 108 | ``` 109 | 110 | If the address needs to be retrieved from a chain id that is not the current chain id, that address can still be retrieved by calling the same function with an additional chain id parameter. 111 | 112 | ```solidity 113 | addresses.getAddress("CONTRACT_NAME", chainId); 114 | ``` 115 | 116 | ### Retrieving Recorded Addresses 117 | 118 | Addresses added during the proposals executions can be retrieved by calling the `getRecordedAddresses` function. 119 | 120 | ```solidity 121 | addresses.getRecordedAddresses(); 122 | ``` 123 | 124 | ### Retrieving Changed Addresses 125 | 126 | Addresses changed during the proposals executions can be retrieved by calling the `getChangedAddresses` function. 127 | 128 | ```solidity 129 | addresses.getChangedAddresses(); 130 | ``` 131 | 132 | ### Print Added and Changed Addressses 133 | 134 | Addresses that are changed or newly added during the proposal's execution can be retrieved by calling the `printJSONChanges` method. It prints the changes in JSON format, making it easy for users to add them to corresponding JSON files. 135 | 136 | ```solidity 137 | addresses.printJSONChanges(); 138 | ``` 139 | 140 | ### Address exists 141 | 142 | The `isAddressSet` function checks if an address exists in the Addresses contract storage. 143 | 144 | ```solidity 145 | addresses.isAddressSet("CONTRACT_NAME"); 146 | ``` 147 | 148 | ```solidity 149 | addresses.isAddressSet("CONTRACT_NAME", chainId); 150 | ``` 151 | 152 | ### Address is a contract 153 | 154 | The `isAddressContract` function determines whether an address on the execution chain represents a contract. This is useful for distinguishing between contract and non-contract addresses, helping to avoid runtime errors when attempting to interact with non-existent contracts or contracts not deployed on the current chain. 155 | 156 | ```solidity 157 | addresses.isAddressContract("CONTRACT_NAME"); 158 | ``` 159 | 160 | ### Update addresses file 161 | 162 | The `updateJson` function updates the JSON files with the newly added and changed addresses. JSON files are updated corresponding to the network where new addresses are added or changed. This is helpful as a user doesn't need to update the file manually after the proposal run. 163 | 164 | ```solidity 165 | addresses.updateJson(); 166 | ``` 167 | 168 | ## Usage 169 | 170 | When writing a proposal, set the `addresses` object using the `setAddresses` method. Ensure the correct path for `Addresses.json` file is passed inside the constructor while creating the `addresses` object. Use the `addresses` object to add, update, retrieve, and remove addresses. 171 | 172 | ```solidity 173 | pragma solidity ^0.8.0; 174 | 175 | import { MultisigProposal } from "@forge-proposal-simulator/proposals/MultisigProposal.sol"; 176 | 177 | import { Addresses } from "@forge-proposal-simulator/addresses/Addresses.sol"; 178 | import { MyContract } from "@path/to/MyContract.sol"; 179 | 180 | contract PROPOSAL_01 is MultisigProposal { 181 | string private constant ADDRESSES_PATH = "./addresses/Addresses.json"; 182 | 183 | function deploy() public override { 184 | if (!addresses.isAddressSet("CONTRACT_NAME")) { 185 | /// Deploy a new contract 186 | MyContract myContract = new MyContract(); 187 | 188 | /// Interact with the Addresses object, adding the new contract address 189 | addresses.addAddress("CONTRACT_NAME", address(myContract), true); 190 | } 191 | } 192 | 193 | function run() { 194 | // Set addresses object for the proposal 195 | setAddresses(new Addresses(ADDRESSES_PATH)); 196 | 197 | super.run(); 198 | } 199 | } 200 | ``` 201 | -------------------------------------------------------------------------------- /docs/overview/use-cases.md: -------------------------------------------------------------------------------- 1 | # Use cases 2 | 3 | The framework is compatible with OpenZeppelin Timelock, Compound Governor Bravo, OZ Governor, and GnosisSafe Multisig wallet contracts. We encourage the submission of pull requests to accommodate different governance models. 4 | 5 | ### OpenZeppelin Timelock Controller 6 | 7 | The [Timelock Proposal](../guides/timelock-proposal.md) facilitates the creation of the scheduling and execution of calldata. It also allows developers to test the calldata by simulating the entire proposal lifecycle, from submission to execution, using foundry cheat codes to bypass the delay period. 8 | 9 | ### Gnosis Safe Multisig 10 | 11 | The [Multisig Proposal](../guides/multisig-proposal.md) generates and simulates the Multicall calldata. This allows developers to check protocol health after calldata execution by using Foundry cheat codes to simulate actions from the actual Multisig address. Calldata generated from this module can be used directly in Gnosis Safe's UI. 12 | 13 | ### Compound Governor Bravo 14 | 15 | The [Governor Bravo Proposal](../guides/governor-bravo-proposal.md) facilitates the creation of the governor `propose` calldata. It also allows developers to test the calldata by simulating the entire proposal lifecycle, from proposing, voting, queuing, and finally executing. 16 | 17 | ### OZ Governor 18 | 19 | Similar to Compound Governor Bravo, [OZ Governor Proposal](../guides/oz-governor-proposal.md) simulates the entire proposal lifecycle for the governor with a timelock controller extension. 20 | -------------------------------------------------------------------------------- /docs/testing/integration-tests.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | FPS enables the simulation of proposals within integration tests. This capability is essential for verifying the functionality of your proposals and ensuring they don't break existing features. Additionally, it allows testing of the entire proposal lifecycle, including governance proposals and deployment scripts. This guide illustrates writing integration tests with the Multisig example from our [Multisig Proposal Guide](../guides/multisig-proposal.md). These integration tests have already been implemented in the fps-example [repo](https://github.com/solidity-labs-io/fps-example-repo/tree/main/test/multisig). 4 | 5 | ## Setting Up PostProposalCheck.sol 6 | 7 | The first step is to create a `PostProposalCheck.sol` contract, which serves as a base for your integration test contracts. This contract is responsible for deploying proposal contracts, executing them, and updating the addresses object. This allows integration tests to run against the newly updated state after all changes from the governance proposal go into effect. 8 | 9 | ```solidity 10 | pragma solidity ^0.8.0; 11 | 12 | import "@forge-std/Test.sol"; 13 | 14 | import { MultisigProposal } from "@forge-proposal-simulator/src/proposals/MultisigProposal.sol"; 15 | import { Addresses } from "@forge-proposal-simulator/addresses/Addresses.sol"; 16 | 17 | // @notice this is a helper contract to execute proposals before running integration tests. 18 | // @dev should be inherited by integration test contracts. 19 | contract MultisigPostProposalCheck is Test { 20 | Addresses public addresses; 21 | 22 | function setUp() public virtual { 23 | string[] memory inputs = new string[](2); 24 | inputs[0] = "./get-latest-proposal.sh"; 25 | inputs[1] = "MultisigProposal"; 26 | 27 | string memory output = string(vm.ffi(inputs)); 28 | 29 | MultisigProposal multisigProposal = MultisigProposal( 30 | deployCode(output) 31 | ); 32 | vm.makePersistent(address(multisigProposal)); 33 | 34 | // Execute proposals 35 | multisigProposal.run(); 36 | 37 | addresses = multisigProposal.addresses(); 38 | } 39 | } 40 | ``` 41 | 42 | ## Creating a script that returns the latest proposal based on the type of proposal. 43 | 44 | ```bash 45 | #!/bin/bash 46 | BASE_DIR="out" 47 | 48 | PROPOSAL_TYPE="$1" 49 | 50 | # Find proposal directories and get the latest one 51 | LATEST_PROPOSAL_DIR=$(ls -1v ${BASE_DIR}/ | grep "^$PROPOSAL_TYPE" | tail -n 1) 52 | 53 | LATEST_FILE="${LATEST_PROPOSAL_DIR%.sol}" 54 | 55 | # Print the path to the latest proposal artifact json file 56 | echo "${BASE_DIR}/${LATEST_PROPOSAL_DIR}/${LATEST_FILE}.json" 57 | ``` 58 | 59 | ## Creating Integration Test Contracts 60 | 61 | Next, the creation of the `MultisigProposalIntegrationTest` contract is required, which will inherit from `MultisigPostProposalCheck`. Tests should be added to this contract. Utilize the addresses object within this contract to access the addresses of the contracts that have been deployed by the proposals. 62 | 63 | ```solidity 64 | pragma solidity ^0.8.0; 65 | 66 | import { Vault } from "src/mocks/Vault.sol"; 67 | import { Token } from "src/mocks/Token.sol"; 68 | import { MultisigPostProposalCheck } from "./MultisigPostProposalCheck.sol"; 69 | 70 | // @dev This test contract inherits MultisigPostProposalCheck, granting it 71 | // the ability to interact with state modifications effected by proposals 72 | // and to work with newly deployed contracts, if applicable. 73 | contract MultisigProposalIntegrationTest is MultisigPostProposalCheck { 74 | // Tests adding a token to the whitelist in the Vault contract 75 | function test_addTokenToWhitelist() public { 76 | // Retrieves the Vault instance using its address from the Addresses contract 77 | Vault multisigVault = Vault(addresses.getAddress("MULTISIG_VAULT")); 78 | // Retrieves the address of the multisig wallet 79 | address multisig = addresses.getAddress("DEV_MULTISIG"); 80 | // Creates a new instance of Token 81 | Token token = new Token(); 82 | 83 | // Sets the next caller of the function to be the multisig address 84 | vm.prank(multisig); 85 | 86 | // Whitelists the newly created token in the Vault 87 | multisigVault.whitelistToken(address(token), true); 88 | 89 | // Asserts that the token is successfully whitelisted 90 | assertTrue( 91 | multisigVault.tokenWhitelist(address(token)), 92 | "Token should be whitelisted" 93 | ); 94 | } 95 | 96 | // Tests deposit functionality in the Vault contract 97 | function test_depositToVault() public { 98 | // Retrieves the Vault instance using its address from the Addresses contract 99 | Vault multisigVault = Vault(addresses.getAddress("MULTISIG_VAULT")); 100 | // Retrieves the address of the multisig wallet 101 | address multisig = addresses.getAddress("DEV_MULTISIG"); 102 | // Retrieves the address of the token to be deposited 103 | address token = addresses.getAddress("MULTISIG_TOKEN"); 104 | 105 | (uint256 prevDeposits, ) = multisigVault.deposits( 106 | address(token), 107 | multisig 108 | ); 109 | 110 | uint256 depositAmount = 100; 111 | 112 | // Starts a prank session with the multisig address as the caller 113 | vm.startPrank(multisig); 114 | // Mints 100 tokens to the multisig contract's address 115 | Token(token).mint(multisig, depositAmount); 116 | // Approves the Vault to spend depositAmount tokens 117 | Token(token).approve(address(multisigVault), depositAmount); 118 | // Deposits depositAmount tokens into the Vault 119 | multisigVault.deposit(address(token), depositAmount); 120 | 121 | // Retrieves the deposit amount of the token in the Vault for the multisig address 122 | (uint256 amount, ) = multisigVault.deposits(address(token), multisig); 123 | // Asserts that the deposit amount is equal to previous deposit + depositAmount 124 | assertTrue( 125 | amount == prevDeposits + depositAmount, 126 | "Token should be deposited" 127 | ); 128 | } 129 | } 130 | ``` 131 | 132 | ## Running Integration Tests 133 | 134 | Executing the integration tests triggers the `setUp()` function before each test, ensuring the 135 | tests are always executed on a fresh state after the proposals execution. 136 | 137 | ```bash 138 | forge test --mc MultisigProposalIntegrationTest -vvv --ffi 139 | ``` 140 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | libs = ['lib'] 3 | test = 'test' 4 | auto_detect_solc = true 5 | # Require read access for 'addresses' and 'out' folder on root repo for bytecode verification 6 | fs_permissions = [{ access = "read-write", path = "./"}] 7 | optimizer = true 8 | optimizer_runs = 200 9 | 10 | [rpc_endpoints] 11 | localhost = "http://127.0.0.1:8545" 12 | sepolia = "https://sepolia.drpc.org" 13 | mainnet = "https://mainnet.gateway.tenderly.co" 14 | 15 | [fmt] 16 | line_length = 80 17 | 18 | #[etherscan] 19 | #sepolia = { key = "${ETHERSCAN_API_KEY}" } 20 | 21 | -------------------------------------------------------------------------------- /mocks/MockAuction.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /// @notice This is a mock contract for testing purposes only, it SHOULD NOT be used in production. 4 | contract MockAuction { 5 | address public highestBidder; 6 | uint public highestBid; 7 | address public owner; 8 | 9 | constructor() { 10 | owner = msg.sender; 11 | } 12 | 13 | function bid() public payable { 14 | require(msg.value > highestBid, "Bid not high enough"); 15 | address previousBidder = highestBidder; 16 | 17 | highestBidder = msg.sender; 18 | highestBid = msg.value; 19 | 20 | if (previousBidder != address(0)) { 21 | (bool success, ) = payable(previousBidder).call{value: msg.value}(""); 22 | assert(success); 23 | } 24 | } 25 | 26 | function endAuction() public { 27 | require(msg.sender == owner, "Only owner can end auction"); 28 | payable(owner).transfer(highestBid); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mocks/MockBravoProposal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {GovernorBravoProposal} from "@proposals/GovernorBravoProposal.sol"; 5 | 6 | import {ICompoundConfigurator} from "@interface/ICompoundConfigurator.sol"; 7 | 8 | import {Addresses} from "@addresses/Addresses.sol"; 9 | 10 | contract MockBravoProposal is GovernorBravoProposal { 11 | // @notice new kink value 12 | uint64 public kink = 0.75 * 1e18; 13 | 14 | function name() public pure override returns (string memory) { 15 | return "ADJUST_WETH_IR_CURVE"; 16 | } 17 | 18 | function description() public pure override returns (string memory) { 19 | return 20 | "Mock proposal that adjust IR Curve for Compound v3 WETH on Mainnet"; 21 | } 22 | 23 | function run() public override { 24 | setPrimaryForkId(vm.createSelectFork("mainnet")); 25 | 26 | uint256[] memory chainIds = new uint256[](1); 27 | chainIds[0] = 1; 28 | 29 | setAddresses( 30 | new Addresses( 31 | vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds 32 | ) 33 | ); 34 | 35 | setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 36 | 37 | super.run(); 38 | } 39 | 40 | function build() 41 | public 42 | override 43 | buildModifier(addresses.getAddress("COMPOUND_TIMELOCK_BRAVO")) 44 | { 45 | /// STATICCALL -- not recorded for the run stage 46 | ICompoundConfigurator configurator = 47 | ICompoundConfigurator(addresses.getAddress("COMPOUND_CONFIGURATOR")); 48 | address comet = addresses.getAddress("COMPOUND_COMET"); 49 | 50 | /// CALLS -- mutative and recorded 51 | configurator.setBorrowKink(comet, kink); 52 | configurator.setSupplyKink(comet, kink); 53 | } 54 | 55 | function validate() public view override { 56 | ICompoundConfigurator configurator = 57 | ICompoundConfigurator(addresses.getAddress("COMPOUND_CONFIGURATOR")); 58 | address comet = addresses.getAddress("COMPOUND_COMET"); 59 | 60 | ICompoundConfigurator.Configuration memory config = 61 | configurator.getConfiguration(comet); 62 | assertEq(config.supplyKink, kink); 63 | assertEq(config.borrowKink, kink); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mocks/MockDuplicatedActionProposal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {GovernorBravoProposal} from "@proposals/GovernorBravoProposal.sol"; 5 | 6 | import {ICompoundConfigurator} from "@interface/ICompoundConfigurator.sol"; 7 | 8 | import {Addresses} from "@addresses/Addresses.sol"; 9 | 10 | contract MockDuplicatedActionProposal is GovernorBravoProposal { 11 | // @notice new kink value 12 | uint64 public kink = 0.75 * 1e18; 13 | 14 | function name() public pure override returns (string memory) { 15 | return "ADJUST_WETH_IR_CURVE"; 16 | } 17 | 18 | function description() public pure override returns (string memory) { 19 | return 20 | "Mock proposal that adjust IR Curve for Compound v3 WETH on Mainnet"; 21 | } 22 | 23 | function run() public override { 24 | uint256[] memory chainIds = new uint256[](1); 25 | chainIds[0] = 1; 26 | addresses = new Addresses( 27 | vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds 28 | ); 29 | 30 | setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 31 | 32 | super.run(); 33 | } 34 | 35 | function build() 36 | public 37 | override 38 | buildModifier(addresses.getAddress("COMPOUND_TIMELOCK_BRAVO")) 39 | { 40 | /// STATICCALL -- not recorded for the run stage 41 | 42 | ICompoundConfigurator configurator = 43 | ICompoundConfigurator(addresses.getAddress("COMPOUND_CONFIGURATOR")); 44 | address comet = addresses.getAddress("COMPOUND_COMET"); 45 | 46 | /// CALLS -- mutative and recorded 47 | configurator.setSupplyKink(comet, kink); 48 | configurator.setSupplyKink(comet, kink); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mocks/MockGovernorAlpha.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract MockGovernorAlpha { 5 | function proposalCount() public pure returns (uint256) { 6 | return 1; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mocks/MockMultisigProposal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Addresses} from "@addresses/Addresses.sol"; 5 | 6 | import {MultisigProposal} from "@proposals/MultisigProposal.sol"; 7 | 8 | import {IProxy} from "@interface/IProxy.sol"; 9 | import {IProxyAdmin} from "@interface/IProxyAdmin.sol"; 10 | 11 | import {MockUpgrade} from "@mocks/MockUpgrade.sol"; 12 | 13 | contract MockMultisigProposal is MultisigProposal { 14 | function name() public pure override returns (string memory) { 15 | return "OPTMISM_MULTISIG_MOCK"; 16 | } 17 | 18 | function description() public pure override returns (string memory) { 19 | return "Mock proposal that upgrade the L1 NFT Bridge"; 20 | } 21 | 22 | function run() public override { 23 | setPrimaryForkId(vm.createSelectFork("mainnet")); 24 | 25 | uint256[] memory chainIds = new uint256[](1); 26 | chainIds[0] = 1; 27 | 28 | addresses = new Addresses( 29 | vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds 30 | ); 31 | 32 | super.run(); 33 | } 34 | 35 | function deploy() public override { 36 | if (!addresses.isAddressSet("OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION")) { 37 | address mockUpgrade = address(new MockUpgrade()); 38 | 39 | addresses.addAddress( 40 | "OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION", mockUpgrade, true 41 | ); 42 | } 43 | } 44 | 45 | function build() 46 | public 47 | override 48 | buildModifier(addresses.getAddress("OPTIMISM_MULTISIG")) 49 | { 50 | IProxyAdmin proxy = 51 | IProxyAdmin(addresses.getAddress("OPTIMISM_PROXY_ADMIN")); 52 | 53 | proxy.upgrade( 54 | addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_PROXY"), 55 | addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION") 56 | ); 57 | } 58 | 59 | function simulate() public override { 60 | address multisig = addresses.getAddress("OPTIMISM_MULTISIG"); 61 | 62 | _simulateActions(multisig); 63 | } 64 | 65 | function validate() public override { 66 | IProxy proxy = 67 | IProxy(addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_PROXY")); 68 | 69 | // implementation() caller must be the owner 70 | vm.startPrank(addresses.getAddress("OPTIMISM_PROXY_ADMIN")); 71 | require( 72 | proxy.implementation() 73 | == addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION"), 74 | "Proxy implementation not set" 75 | ); 76 | vm.stopPrank(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mocks/MockOZGovernorProposal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {OZGovernorProposal} from "@proposals/OZGovernorProposal.sol"; 5 | 6 | import {Addresses} from "@addresses/Addresses.sol"; 7 | 8 | import {MockUpgrade} from "@mocks/MockUpgrade.sol"; 9 | 10 | interface IControllable { 11 | function setController(address controller, bool enabled) external; 12 | 13 | function controllers(address) external view returns (bool); 14 | } 15 | 16 | // @notice This is a mock proposal that uses ENS to demostrate OZ Governor proposal type. 17 | // Inspired on https://www.tally.xyz/gov/ens/proposal/4208408830555077285685632645423534041634535116286721240943655761928631543220 18 | contract MockOZGovernorProposal is OZGovernorProposal { 19 | function name() public pure override returns (string memory) { 20 | return "UPGRADE_DNSSEC_SUPPORT"; 21 | } 22 | 23 | function description() public pure override returns (string memory) { 24 | return 25 | "Call setController on the Root contract at root.ens.eth, passing in the address of the new DNS registrar"; 26 | } 27 | 28 | function run() public override { 29 | setPrimaryForkId(vm.createSelectFork("mainnet")); 30 | 31 | uint256[] memory chainIds = new uint256[](1); 32 | chainIds[0] = 1; 33 | 34 | setAddresses( 35 | new Addresses( 36 | vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds 37 | ) 38 | ); 39 | 40 | setGovernor(addresses.getAddress("ENS_GOVERNOR")); 41 | 42 | super.run(); 43 | } 44 | 45 | function deploy() public override { 46 | if (!addresses.isAddressSet("ENS_DNSSEC")) { 47 | // In a real case, this function would be responsable for 48 | // deployig the DNSSEC contract instead of using a mock 49 | address dnsSec = address(new MockUpgrade()); 50 | 51 | addresses.addAddress("ENS_DNSSEC", dnsSec, true); 52 | } 53 | } 54 | 55 | function build() 56 | public 57 | override 58 | buildModifier(addresses.getAddress("ENS_TIMELOCK")) 59 | { 60 | /// STATICCALL -- not recorded for the run stage 61 | IControllable control = IControllable(addresses.getAddress("ENS_ROOT")); 62 | address dnsSec = addresses.getAddress("ENS_DNSSEC"); 63 | 64 | /// CALLS -- mutative and recorded 65 | control.setController(dnsSec, true); 66 | } 67 | 68 | function validate() public view override { 69 | IControllable control = IControllable(addresses.getAddress("ENS_ROOT")); 70 | address dnsSec = addresses.getAddress("ENS_DNSSEC"); 71 | 72 | assertTrue(control.controllers(dnsSec)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mocks/MockSavingContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /// @notice This is a mock contract for testing purposes only, it SHOULD NOT be used in production. 4 | contract MockSavingContract { 5 | struct Deposit { 6 | uint256 amount; 7 | uint256 unlockTime; 8 | } 9 | 10 | mapping(address user => Deposit[] deposits) public deposits; 11 | 12 | function deposit(uint256 lockTime) public payable { 13 | require(msg.value > 0, "Must send some Ether"); 14 | deposits[msg.sender].push( 15 | Deposit({amount: msg.value, unlockTime: block.timestamp + lockTime}) 16 | ); 17 | } 18 | 19 | function withdraw(uint256 depositIndex) public { 20 | Deposit memory userDeposit = deposits[msg.sender][depositIndex]; 21 | require( 22 | block.timestamp >= userDeposit.unlockTime, 23 | "Deposit is still locked" 24 | ); 25 | require(userDeposit.amount > 0, "No funds to withdraw"); 26 | 27 | uint256 amount = userDeposit.amount; 28 | deposits[msg.sender][depositIndex].amount = 0; 29 | payable(msg.sender).transfer(amount); 30 | } 31 | 32 | function getDeposit( 33 | address user, 34 | uint256 depositIndex 35 | ) public view returns (uint256 amount, uint256 unlockTime) { 36 | return ( 37 | deposits[user][depositIndex].amount, 38 | deposits[user][depositIndex].unlockTime 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mocks/MockTimelockProposal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Addresses} from "@addresses/Addresses.sol"; 5 | 6 | import {TimelockProposal} from "@proposals/TimelockProposal.sol"; 7 | 8 | import {IProxy} from "@interface/IProxy.sol"; 9 | import {IProxyAdmin} from "@interface/IProxyAdmin.sol"; 10 | 11 | import {MockUpgrade} from "@mocks/MockUpgrade.sol"; 12 | 13 | interface IUpgradeExecutor { 14 | function execute(address upgrader, bytes memory upgradeCalldata) 15 | external 16 | payable; 17 | } 18 | 19 | // Arbitrum upgrades must be done through a delegate call to a GAC deployed contract 20 | contract GovernanceActionUpgradeWethGateway { 21 | function upgradeWethGateway( 22 | address proxyAdmin, 23 | address wethGatewayProxy, 24 | address wethGatewayImpl 25 | ) public { 26 | IProxyAdmin proxy = IProxyAdmin(proxyAdmin); 27 | proxy.upgrade(wethGatewayProxy, wethGatewayImpl); 28 | } 29 | } 30 | 31 | // Mock arbitrum outbox to return L2 timelock on l2ToL1Sender call 32 | // otherwise L1 timelock reverts on onlyCounterpartTimelock modifier 33 | contract MockOutbox { 34 | function l2ToL1Sender() external pure returns (address) { 35 | return 0x34d45e99f7D8c45ed05B5cA72D54bbD1fb3F98f0; 36 | } 37 | } 38 | 39 | contract MockTimelockProposal is TimelockProposal { 40 | function name() public pure override returns (string memory) { 41 | return "ARBITRUM_L1_TIMELOCK_MOCK"; 42 | } 43 | 44 | function description() public pure override returns (string memory) { 45 | return "Mock proposal that upgrades the weth gateway"; 46 | } 47 | 48 | function run() public override { 49 | setPrimaryForkId(vm.createSelectFork("mainnet")); 50 | 51 | uint256[] memory chainIds = new uint256[](1); 52 | chainIds[0] = 1; 53 | 54 | addresses = new Addresses( 55 | vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds 56 | ); 57 | 58 | setTimelock(addresses.getAddress("ARBITRUM_L1_TIMELOCK")); 59 | 60 | super.run(); 61 | } 62 | 63 | function deploy() public override { 64 | if (!addresses.isAddressSet("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION")) 65 | { 66 | address mockUpgrade = address(new MockUpgrade()); 67 | 68 | addresses.addAddress( 69 | "ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION", mockUpgrade, true 70 | ); 71 | } 72 | 73 | if (!addresses.isAddressSet("ARBITRUM_GAC_UPGRADE_WETH_GATEWAY")) { 74 | address gac = address(new GovernanceActionUpgradeWethGateway()); 75 | addresses.addAddress("ARBITRUM_GAC_UPGRADE_WETH_GATEWAY", gac, true); 76 | } 77 | } 78 | 79 | function preBuildMock() public override { 80 | address mockOutbox = address(new MockOutbox()); 81 | 82 | vm.store( 83 | addresses.getAddress("ARBITRUM_BRIDGE"), 84 | bytes32(uint256(5)), 85 | bytes32(uint256(uint160(mockOutbox))) 86 | ); 87 | } 88 | 89 | function build() public override buildModifier(address(timelock)) { 90 | IUpgradeExecutor upgradeExecutor = IUpgradeExecutor( 91 | addresses.getAddress("ARBITRUM_L1_UPGRADE_EXECUTOR") 92 | ); 93 | 94 | upgradeExecutor.execute( 95 | addresses.getAddress("ARBITRUM_GAC_UPGRADE_WETH_GATEWAY"), 96 | abi.encodeWithSelector( 97 | GovernanceActionUpgradeWethGateway.upgradeWethGateway.selector, 98 | addresses.getAddress("ARBITRUM_L1_PROXY_ADMIN"), 99 | addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_PROXY"), 100 | addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION") 101 | ) 102 | ); 103 | } 104 | 105 | function simulate() public override { 106 | // Proposer must be arbitrum bridge 107 | address proposer = addresses.getAddress("ARBITRUM_BRIDGE"); 108 | 109 | // Executor can be anyone 110 | address executor = address(1); 111 | 112 | _simulateActions(proposer, executor); 113 | } 114 | 115 | function validate() public override { 116 | IProxy proxy = 117 | IProxy(addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_PROXY")); 118 | 119 | // implementation() caller must be the owner 120 | vm.startPrank(addresses.getAddress("ARBITRUM_L1_PROXY_ADMIN")); 121 | require( 122 | proxy.implementation() 123 | == addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION"), 124 | "Proxy implementation not set" 125 | ); 126 | vm.stopPrank(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /mocks/MockToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/mocks/MockERC20.sol"; 5 | 6 | /// @notice This is a mock contract for testing purposes only, it SHOULD NOT be used in production. 7 | contract MockToken is MockERC20 { 8 | constructor(string memory name, string memory symbol) { 9 | initialize(name, symbol, 18); 10 | } 11 | 12 | function mint(address to, uint256 amount) external { 13 | _mint(to, amount); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mocks/MockTokenWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {MockToken} from "mocks/MockToken.sol"; 5 | 6 | /// @notice This is a mock contract for testing purposes only, it SHOULD NOT be used in production. 7 | contract MockTokenWrapper { 8 | address internal _tokenAddress; 9 | 10 | constructor(address tokenAddress) { 11 | _tokenAddress = tokenAddress; 12 | } 13 | 14 | function mint() external payable { 15 | MockToken(_tokenAddress).transfer(msg.sender, msg.value); 16 | } 17 | 18 | function redeemTokens(uint256 tokenAmount) external { 19 | require( 20 | MockToken(_tokenAddress).balanceOf(msg.sender) >= tokenAmount, 21 | "Insufficient token balance" 22 | ); 23 | 24 | require( 25 | address(this).balance >= tokenAmount, 26 | "Insufficient ETH balance in contract" 27 | ); 28 | 29 | MockToken(_tokenAddress).transferFrom( 30 | msg.sender, 31 | address(this), 32 | tokenAmount 33 | ); 34 | payable(msg.sender).transfer(tokenAmount); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mocks/MockUpgrade.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | contract MockUpgrade {} 4 | -------------------------------------------------------------------------------- /mocks/MockVotingContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /// @notice This is a mock contract for testing purposes only, it SHOULD NOT be used in production. 4 | contract MockVotingContract { 5 | mapping(string candidate => uint256 votes) public votesReceived; 6 | string[] public candidateList; 7 | 8 | constructor(string[] memory candidateNames) { 9 | candidateList = candidateNames; 10 | } 11 | 12 | function vote(string memory candidate) public { 13 | votesReceived[candidate] += 1; 14 | } 15 | 16 | function totalVotesFor(string memory candidate) public view returns (uint) { 17 | return votesReceived[candidate]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "standard-repo", 3 | "description": "Foundry-based library for proposals simulations", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "husky": "9.0.10", 7 | "solhint": "4.5.2" 8 | }, 9 | "keywords": [ 10 | "blockchain", 11 | "ethereum", 12 | "forge", 13 | "foundry", 14 | "smart-contracts", 15 | "solidity" 16 | ], 17 | "scripts": { 18 | "prepare": "husky", 19 | "clean": "rm -rf cache out", 20 | "build": "forge build", 21 | "lint": "solhint --config ./.solhintrc --ignore-path .solhintignore '**/*.sol'", 22 | "lint:write": "solhint --config ./.solhintrc --fix '**/*.sol'", 23 | "fmt": "forge fmt --check", 24 | "fmt:write": "forge fmt", 25 | "test": "forge test", 26 | "test:coverage": "forge coverage", 27 | "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @forge-std/=lib/forge-std/src/ 2 | @test=test/ 3 | @proposals/=src/proposals/ 4 | @interface/=src/interface/ 5 | @addresses/=addresses/ 6 | @utils/=utils/ 7 | @examples/=examples/ 8 | @script=script/ 9 | @mocks=mocks/ 10 | -------------------------------------------------------------------------------- /run-proposal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is used on the CI to print proposal output for the highest numbered .sol file in the PR. 3 | 4 | # PR_CHANGED_FILES is a list of files changed in the PR, set by the CI 5 | CHANGED_FILES=$PR_CHANGED_FILES 6 | FOLDER=$PROPOSALS_FOLDER 7 | 8 | if [[ ! -z "$CHANGED_FILES" ]]; then 9 | IFS=' ' read -r -a files_array <<< "$CHANGED_FILES" 10 | 11 | # Initialize an empty array to hold numbers and corresponding file names 12 | max_number=-1 13 | selected_file="" 14 | 15 | for file in "${files_array[@]}"; do 16 | if [[ $file == "$FOLDER"/*.sol ]]; then 17 | 18 | # Extract the number following 'Proposal_' before '.sol' 19 | number=$(echo $file | sed -E 's/.*Proposal_([0-9]+)\.sol/\1/') 20 | 21 | # Check if a number was actually found; if not, skip this file 22 | if [[ -z "$number" ]]; then 23 | continue 24 | fi 25 | 26 | # Check if this number is the highest found so far 27 | if [[ "$number" -gt "$max_number" ]]; then 28 | max_number=$number 29 | selected_file=$file 30 | fi 31 | fi 32 | done 33 | 34 | # If file was found 35 | if [[ ! -z "$selected_file" ]]; then 36 | echo "Processing $selected_file..." 37 | output=$(forge script "$selected_file" 2>&1) 38 | # Removal of ANSI Escape Codes 39 | clean_output=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g') 40 | 41 | echo "Output for $selected_file:" 42 | echo "$clean_output" 43 | 44 | # Extracting the relevant part of the output 45 | selected_output=$(echo "$clean_output" | awk ' 46 | /------------------ Proposal Actions ------------------/, /\n\nProposal Description:/ { 47 | if (/\n\nProposal Description:/) exit; # Exit before printing the line with "Proposal Description:" 48 | print; 49 | } 50 | ') 51 | 52 | json_output="" 53 | # Write to JSON if selected_output otherwise write a failure message 54 | if [ ! -z "$selected_output" ]; then 55 | json_output=$(jq -n --arg file "$selected_file" --arg output "$selected_output" '{file: $file, output: $output}') 56 | else 57 | json_output=$(jq -n --arg file "$selected_file" --arg output "Proposal $selected_file failed. Check CI logs" '{file: $file, output: $output}') 58 | fi 59 | 60 | echo "Writing JSON to output.json..." 61 | # Create output.json 62 | touch output.json 63 | # Write JSON to output.json 64 | echo "$json_output" > output.json 65 | fi 66 | fi 67 | -------------------------------------------------------------------------------- /src/interface/ICompoundConfigurator.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | interface ICompoundConfigurator { 4 | struct Configuration { 5 | address governor; 6 | address pauseGuardian; 7 | address baseToken; 8 | address baseTokenPriceFeed; 9 | address extensionDelegate; 10 | uint64 supplyKink; 11 | uint64 supplyPerYearInterestRateSlopeLow; 12 | uint64 supplyPerYearInterestRateSlopeHigh; 13 | uint64 supplyPerYearInterestRateBase; 14 | uint64 borrowKink; 15 | uint64 borrowPerYearInterestRateSlopeLow; 16 | uint64 borrowPerYearInterestRateSlopeHigh; 17 | uint64 borrowPerYearInterestRateBase; 18 | uint64 storeFrontPriceFactor; 19 | uint64 trackingIndexScale; 20 | uint64 baseTrackingSupplySpeed; 21 | uint64 baseTrackingBorrowSpeed; 22 | uint104 baseMinForRewards; 23 | uint104 baseBorrowMin; 24 | uint104 targetReserves; 25 | AssetConfig[] assetConfigs; 26 | } 27 | 28 | struct AssetConfig { 29 | address asset; 30 | address priceFeed; 31 | uint8 decimals; 32 | uint64 borrowCollateralFactor; 33 | uint64 liquidateCollateralFactor; 34 | uint64 liquidationFactor; 35 | uint128 supplyCap; 36 | } 37 | 38 | function getConfiguration(address cometProxy) 39 | external 40 | view 41 | returns (Configuration memory); 42 | 43 | function setBorrowKink(address cometProxy, uint64 newBorrowKink) external; 44 | 45 | function setSupplyKink(address cometProxy, uint64 newSupplyKink) external; 46 | } 47 | -------------------------------------------------------------------------------- /src/interface/IGovernor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.0; 3 | 4 | /// @notice OZ Governor interface 5 | interface IGovernor { 6 | /// @notice Possible states that a proposal may be in 7 | enum ProposalState { 8 | Pending, 9 | Active, 10 | Canceled, 11 | Defeated, 12 | Succeeded, 13 | Queued, 14 | Expired, 15 | Executed 16 | } 17 | 18 | /** 19 | * @notice module:voting 20 | * @dev A description of the possible `support` values for {castVote} and the way these votes are counted, meant to 21 | * be consumed by UIs to show correct vote options and interpret the results. The string is a URL-encoded sequence of 22 | * key-value pairs that each describe one aspect, for example `support=bravo&quorum=for,abstain`. 23 | * 24 | * There are 2 standard keys: `support` and `quorum`. 25 | * 26 | * - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. 27 | * - `quorum=bravo` means that only For votes are counted towards quorum. 28 | * - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. 29 | * 30 | * If a counting module makes use of encoded `params`, it should include this under a `params` key with a unique 31 | * name that describes the behavior. For example: 32 | * 33 | * - `params=fractional` might refer to a scheme where votes are divided fractionally between for/against/abstain. 34 | * - `params=erc721` might refer to a scheme where specific NFTs are delegated to vote. 35 | * 36 | * NOTE: The string can be decoded by the standard 37 | * https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] 38 | * JavaScript class. 39 | */ 40 | // solhint-disable-next-line func-name-mixedcase 41 | function COUNTING_MODE() external view returns (string memory); 42 | 43 | /** 44 | * @notice module:core 45 | * @dev Hashing function used to (re)build the proposal id from the proposal details.. 46 | */ 47 | function hashProposal( 48 | address[] memory targets, 49 | uint256[] memory values, 50 | bytes[] memory calldatas, 51 | bytes32 descriptionHash 52 | ) external pure returns (uint256); 53 | 54 | /** 55 | * @notice module:core 56 | * @dev Current state of a proposal, following Compound's convention 57 | */ 58 | function state(uint256 proposalId) external view returns (ProposalState); 59 | 60 | /** 61 | * @notice module:core 62 | * @dev The number of votes required in order for a voter to become a proposer. 63 | */ 64 | function proposalThreshold() external view returns (uint256); 65 | 66 | /** 67 | * @notice module:core 68 | * @dev Timepoint used to retrieve user's votes and quorum. If using block number (as per Compound's Comp), the 69 | * snapshot is performed at the end of this block. Hence, voting for this proposal starts at the beginning of the 70 | * following block. 71 | */ 72 | function proposalSnapshot(uint256 proposalId) 73 | external 74 | view 75 | returns (uint256); 76 | 77 | /** 78 | * @notice module:core 79 | * @dev Timepoint at which votes close. If using block number, votes close at the end of this block, so it is 80 | * possible to cast a vote during this block. 81 | */ 82 | function proposalDeadline(uint256 proposalId) 83 | external 84 | view 85 | returns (uint256); 86 | 87 | /** 88 | * @notice module:core 89 | * @dev The account that created a proposal. 90 | */ 91 | function proposalProposer(uint256 proposalId) 92 | external 93 | view 94 | returns (address); 95 | 96 | /** 97 | * @notice module:core 98 | * @dev The time when a queued proposal becomes executable ("ETA"). Unlike {proposalSnapshot} and 99 | * {proposalDeadline}, this doesn't use the governor clock, and instead relies on the executor's clock which may be 100 | * different. In most cases this will be a timestamp. 101 | */ 102 | function proposalEta(uint256 proposalId) external view returns (uint256); 103 | 104 | /** 105 | * @notice module:core 106 | * @dev Whether a proposal needs to be queued before execution. 107 | */ 108 | function proposalNeedsQueuing(uint256 proposalId) 109 | external 110 | view 111 | returns (bool); 112 | 113 | /** 114 | * @notice module:user-config 115 | * @dev Delay, between the proposal is created and the vote starts. The unit this duration is expressed in depends 116 | * on the clock (see ERC-6372) this contract uses. 117 | * 118 | * This can be increased to leave time for users to buy voting power, or delegate it, before the voting of a 119 | * proposal starts. 120 | * 121 | * NOTE: While this interface returns a uint256, timepoints are stored as uint48 following the ERC-6372 clock type. 122 | * Consequently this value must fit in a uint48 (when added to the current clock). See {IERC6372-clock}. 123 | */ 124 | function votingDelay() external view returns (uint256); 125 | 126 | /** 127 | * @notice module:user-config 128 | * @dev Delay between the vote start and vote end. The unit this duration is expressed in depends on the clock 129 | * (see ERC-6372) this contract uses. 130 | * 131 | * NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting 132 | * duration compared to the voting delay. 133 | * 134 | * NOTE: This value is stored when the proposal is submitted so that possible changes to the value do not affect 135 | * proposals that have already been submitted. The type used to save it is a uint32. Consequently, while this 136 | * interface returns a uint256, the value it returns should fit in a uint32. 137 | */ 138 | function votingPeriod() external view returns (uint256); 139 | 140 | /** 141 | * @notice module:user-config 142 | * @dev Minimum number of cast voted required for a proposal to be successful. 143 | * 144 | * NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This allows to scale the 145 | * quorum depending on values such as the totalSupply of a token at this timepoint (see {ERC20Votes}). 146 | */ 147 | function quorum(uint256 timepoint) external view returns (uint256); 148 | 149 | /** 150 | * @notice module:reputation 151 | * @dev Voting power of an `account` at a specific `timepoint`. 152 | * 153 | * Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or 154 | * multiple), {ERC20Votes} tokens. 155 | */ 156 | function getVotes(address account, uint256 timepoint) 157 | external 158 | view 159 | returns (uint256); 160 | 161 | /** 162 | * @notice module:reputation 163 | * @dev Voting power of an `account` at a specific `timepoint` given additional encoded parameters. 164 | */ 165 | function getVotesWithParams( 166 | address account, 167 | uint256 timepoint, 168 | bytes memory params 169 | ) external view returns (uint256); 170 | 171 | /** 172 | * @notice module:voting 173 | * @dev Returns whether `account` has cast a vote on `proposalId`. 174 | */ 175 | function hasVoted(uint256 proposalId, address account) 176 | external 177 | view 178 | returns (bool); 179 | 180 | /** 181 | * @dev Create a new proposal. Vote start after a delay specified by {IGovernor-votingDelay} and lasts for a 182 | * duration specified by {IGovernor-votingPeriod}. 183 | * 184 | * Emits a {ProposalCreated} event. 185 | * 186 | * NOTE: The state of the Governor and `targets` may change between the proposal creation and its execution. 187 | * This may be the result of third party actions on the targeted contracts, or other governor proposals. 188 | * For example, the balance of this contract could be updated or its access control permissions may be modified, 189 | * possibly compromising the proposal's ability to execute successfully (e.g. the governor doesn't have enough 190 | * value to cover a proposal with multiple transfers). 191 | */ 192 | function propose( 193 | address[] memory targets, 194 | uint256[] memory values, 195 | bytes[] memory calldatas, 196 | string memory description 197 | ) external returns (uint256 proposalId); 198 | 199 | /** 200 | * @dev Queue a proposal. Some governors require this step to be performed before execution can happen. If queuing 201 | * is not necessary, this function may revert. 202 | * Queuing a proposal requires the quorum to be reached, the vote to be successful, and the deadline to be reached. 203 | * 204 | * Emits a {ProposalQueued} event. 205 | */ 206 | function queue( 207 | address[] memory targets, 208 | uint256[] memory values, 209 | bytes[] memory calldatas, 210 | bytes32 descriptionHash 211 | ) external returns (uint256 proposalId); 212 | 213 | /** 214 | * @dev Execute a successful proposal. This requires the quorum to be reached, the vote to be successful, and the 215 | * deadline to be reached. Depending on the governor it might also be required that the proposal was queued and 216 | * that some delay passed. 217 | * 218 | * Emits a {ProposalExecuted} event. 219 | * 220 | * NOTE: Some modules can modify the requirements for execution, for example by adding an additional timelock. 221 | */ 222 | function execute( 223 | address[] memory targets, 224 | uint256[] memory values, 225 | bytes[] memory calldatas, 226 | bytes32 descriptionHash 227 | ) external payable returns (uint256 proposalId); 228 | 229 | /** 230 | * @dev Cancel a proposal. A proposal is cancellable by the proposer, but only while it is Pending state, i.e. 231 | * before the vote starts. 232 | * 233 | * Emits a {ProposalCanceled} event. 234 | */ 235 | function cancel( 236 | address[] memory targets, 237 | uint256[] memory values, 238 | bytes[] memory calldatas, 239 | bytes32 descriptionHash 240 | ) external returns (uint256 proposalId); 241 | 242 | /** 243 | * @dev Cast a vote 244 | * 245 | * Emits a {VoteCast} event. 246 | */ 247 | function castVote(uint256 proposalId, uint8 support) 248 | external 249 | returns (uint256 balance); 250 | 251 | /** 252 | * @dev Cast a vote with a reason 253 | * 254 | * Emits a {VoteCast} event. 255 | */ 256 | function castVoteWithReason( 257 | uint256 proposalId, 258 | uint8 support, 259 | string calldata reason 260 | ) external returns (uint256 balance); 261 | 262 | /** 263 | * @dev Cast a vote with a reason and additional encoded parameters 264 | * 265 | * Emits a {VoteCast} or {VoteCastWithParams} event depending on the length of params. 266 | */ 267 | function castVoteWithReasonAndParams( 268 | uint256 proposalId, 269 | uint8 support, 270 | string calldata reason, 271 | bytes memory params 272 | ) external returns (uint256 balance); 273 | 274 | /** 275 | * @dev Cast a vote using the voter's signature, including ERC-1271 signature support. 276 | * 277 | * Emits a {VoteCast} event. 278 | */ 279 | function castVoteBySig( 280 | uint256 proposalId, 281 | uint8 support, 282 | address voter, 283 | bytes memory signature 284 | ) external returns (uint256 balance); 285 | 286 | /** 287 | * @dev Cast a vote with a reason and additional encoded parameters using the voter's signature, 288 | * including ERC-1271 signature support. 289 | * 290 | * Emits a {VoteCast} or {VoteCastWithParams} event depending on the length of params. 291 | */ 292 | function castVoteWithReasonAndParamsBySig( 293 | uint256 proposalId, 294 | uint8 support, 295 | address voter, 296 | string calldata reason, 297 | bytes memory params, 298 | bytes memory signature 299 | ) external returns (uint256 balance); 300 | } 301 | 302 | interface IGovernorTimelockControl { 303 | /** 304 | * @dev Public accessor to check the address of the timelock 305 | */ 306 | function timelock() external view returns (address); 307 | } 308 | 309 | interface IGovernorVotes { 310 | function token() external view returns (address); 311 | } 312 | -------------------------------------------------------------------------------- /src/interface/IGovernorBravo.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | pragma solidity ^0.8.0; 3 | 4 | interface IGovernorBravo { 5 | /// @notice Possible states that a proposal may be in 6 | enum ProposalState { 7 | Pending, 8 | Active, 9 | Canceled, 10 | Defeated, 11 | Succeeded, 12 | Queued, 13 | Expired, 14 | Executed 15 | } 16 | 17 | function votingDelay() external view returns (uint256); 18 | 19 | function votingPeriod() external view returns (uint256); 20 | 21 | function proposalThreshold() external view returns (uint256); 22 | 23 | function proposalCount() external view returns (uint256); 24 | 25 | function quorumVotes() external view returns (uint256); 26 | 27 | function timelock() external view returns (ITimelockBravo); 28 | 29 | function getActions(uint256 proposalId) 30 | external 31 | view 32 | returns ( 33 | address[] memory targets, 34 | uint256[] memory values, 35 | string[] memory signatures, 36 | bytes[] memory calldatas 37 | ); 38 | 39 | function castVote(uint256 proposalId, uint8 support) external; 40 | 41 | function queue(uint256 proposalId) external; 42 | 43 | function execute(uint256 proposalId) external payable; 44 | 45 | function state(uint256 proposalId) external view returns (ProposalState); 46 | 47 | function comp() external view returns (IERC20VotesComp); 48 | } 49 | 50 | interface ITimelockBravo { 51 | function delay() external view returns (uint256); 52 | function GRACE_PERIOD() external view returns (uint256); 53 | function acceptAdmin() external; 54 | function queuedTransactions(bytes32 hash) external view returns (bool); 55 | function queueTransaction( 56 | address target, 57 | uint256 value, 58 | string calldata signature, 59 | bytes calldata data, 60 | uint256 eta 61 | ) external returns (bytes32); 62 | function cancelTransaction( 63 | address target, 64 | uint256 value, 65 | string calldata signature, 66 | bytes calldata data, 67 | uint256 eta 68 | ) external; 69 | function executeTransaction( 70 | address target, 71 | uint256 value, 72 | string calldata signature, 73 | bytes calldata data, 74 | uint256 eta 75 | ) external payable returns (bytes memory); 76 | } 77 | 78 | interface IERC20VotesComp { 79 | function getPriorVotes(address account, uint256 blockNumber) 80 | external 81 | view 82 | returns (uint96); 83 | 84 | function delegate(address delegatee) external; 85 | } 86 | -------------------------------------------------------------------------------- /src/interface/IProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IProxy { 5 | function admin() external view returns (address); 6 | 7 | function implementation() external view returns (address); 8 | } 9 | -------------------------------------------------------------------------------- /src/interface/IProxyAdmin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IProxyAdmin { 5 | function upgrade(address proxy, address implementation) external; 6 | } 7 | -------------------------------------------------------------------------------- /src/interface/ITimelockController.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /// openzeppelin timelock controller interface 4 | interface ITimelockController { 5 | function isOperation(bytes32) external view returns (bool); 6 | function isOperationPending(bytes32) external view returns (bool); 7 | function isOperationDone(bytes32) external view returns (bool); 8 | function getMinDelay() external view returns (uint256); 9 | function getTimestamp(bytes32) external view returns (uint256); 10 | function execute( 11 | address target, 12 | uint256 value, 13 | bytes calldata payload, 14 | bytes32 predecessor, 15 | bytes32 salt 16 | ) external payable; 17 | function executeBatch( 18 | address[] calldata targets, 19 | uint256[] calldata values, 20 | bytes[] calldata payloads, 21 | bytes32 predecessor, 22 | bytes32 salt 23 | ) external; 24 | 25 | function cancel(bytes32 id) external; 26 | 27 | function hashOperationBatch( 28 | address[] calldata targets, 29 | uint256[] calldata values, 30 | bytes[] calldata payloads, 31 | bytes32 predecessor, 32 | bytes32 salt 33 | ) external pure returns (bytes32); 34 | 35 | function hashOperation( 36 | address target, 37 | uint256 value, 38 | bytes calldata data, 39 | bytes32 predecessor, 40 | bytes32 salt 41 | ) external pure returns (bytes32); 42 | 43 | function scheduleBatch( 44 | address[] calldata targets, 45 | uint256[] calldata values, 46 | bytes[] calldata payloads, 47 | bytes32 predecessor, 48 | bytes32 salt, 49 | uint256 delay 50 | ) external; 51 | 52 | function schedule( 53 | address target, 54 | uint256 value, 55 | bytes calldata data, 56 | bytes32 predecessor, 57 | bytes32 salt, 58 | uint256 delay 59 | ) external; 60 | } 61 | -------------------------------------------------------------------------------- /src/interface/IVotes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v5.0.0) (governance/utils/IVotes.sol) 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Common interface for {ERC20Votes}, {ERC721Votes}, and other {Votes}-enabled contracts. 7 | */ 8 | interface IVotes { 9 | /** 10 | * @dev The signature used has expired. 11 | */ 12 | error VotesExpiredSignature(uint256 expiry); 13 | 14 | /** 15 | * @dev Emitted when an account changes their delegate. 16 | */ 17 | event DelegateChanged( 18 | address indexed delegator, 19 | address indexed fromDelegate, 20 | address indexed toDelegate 21 | ); 22 | 23 | /** 24 | * @dev Emitted when a token transfer or delegate change results in changes to a delegate's number of voting units. 25 | */ 26 | event DelegateVotesChanged( 27 | address indexed delegate, uint256 previousVotes, uint256 newVotes 28 | ); 29 | 30 | /** 31 | * @dev Returns the current amount of votes that `account` has. 32 | */ 33 | function getVotes(address account) external view returns (uint256); 34 | 35 | /** 36 | * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is 37 | * configured to use block numbers, this will return the value at the end of the corresponding block. 38 | */ 39 | function getPastVotes(address account, uint256 timepoint) 40 | external 41 | view 42 | returns (uint256); 43 | 44 | /** 45 | * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is 46 | * configured to use block numbers, this will return the value at the end of the corresponding block. 47 | * 48 | * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. 49 | * Votes that have not been delegated are still part of total supply, even though they would not participate in a 50 | * vote. 51 | */ 52 | function getPastTotalSupply(uint256 timepoint) 53 | external 54 | view 55 | returns (uint256); 56 | 57 | /** 58 | * @dev Returns the delegate that `account` has chosen. 59 | */ 60 | function delegates(address account) external view returns (address); 61 | 62 | /** 63 | * @dev Delegates votes from the sender to `delegatee`. 64 | */ 65 | function delegate(address delegatee) external; 66 | 67 | /** 68 | * @dev Delegates votes from signer to `delegatee`. 69 | */ 70 | function delegateBySig( 71 | address delegatee, 72 | uint256 nonce, 73 | uint256 expiry, 74 | uint8 v, 75 | bytes32 r, 76 | bytes32 s 77 | ) external; 78 | } 79 | -------------------------------------------------------------------------------- /src/proposals/CrossChainProposal.sol: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2023 Lunar Enterprise Ventures, Ltd. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | */ 15 | 16 | pragma solidity ^0.8.0; 17 | 18 | import {MultisigProposal} from "@proposals/MultisigProposal.sol"; 19 | import "@forge-std/Test.sol"; 20 | 21 | /// @notice Cross Chain Proposal is a type of proposal to execute and simulate 22 | /// cross chain calls within the context of a proposal. 23 | /// Reuse Multisig Proposal contract for readability and to avoid code duplication. 24 | abstract contract CrossChainProposal is MultisigProposal { 25 | /// @notice nonce for wormhole 26 | uint32 public nonce; 27 | 28 | /// instant finality on moonbeam https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consiste#consistency-levels 29 | uint16 public consistencyLevel = 200; 30 | 31 | /// @notice set the nonce for the cross chain proposal 32 | function _setNonce(uint32 _nonce) internal { 33 | nonce = _nonce; 34 | } 35 | 36 | function getTargetsPayloadsValues() 37 | public 38 | view 39 | returns (address[] memory, uint256[] memory, bytes[] memory) 40 | { 41 | /// target cannot be address 0 as that call will fail 42 | /// value can be 0 43 | /// arguments can be 0 as long as eth is sent 44 | 45 | uint256 proposalLength = actions.length; 46 | 47 | address[] memory targets = new address[](proposalLength); 48 | uint256[] memory values = new uint256[](proposalLength); 49 | bytes[] memory payloads = new bytes[](proposalLength); 50 | 51 | for (uint256 i = 0; i < proposalLength; i++) { 52 | require( 53 | actions[i].target != address(0), "Invalid target for governance" 54 | ); 55 | 56 | /// if there are no args and no eth, the action is not valid 57 | require( 58 | (actions[i].arguments.length == 0 && actions[i].value > 0) 59 | || actions[i].arguments.length > 0, 60 | "Invalid arguments for governance" 61 | ); 62 | 63 | targets[i] = actions[i].target; 64 | values[i] = actions[i].value; 65 | payloads[i] = actions[i].arguments; 66 | } 67 | 68 | return (targets, values, payloads); 69 | } 70 | 71 | function getTimelockCalldata(address timelock) 72 | public 73 | view 74 | returns (bytes memory) 75 | { 76 | ( 77 | address[] memory targets, 78 | uint256[] memory values, 79 | bytes[] memory payloads 80 | ) = getTargetsPayloadsValues(); 81 | 82 | return abi.encodeWithSignature( 83 | "publishMessage(uint32,bytes,uint8)", 84 | nonce, 85 | abi.encode(timelock, targets, values, payloads), 86 | consistencyLevel 87 | ); 88 | } 89 | 90 | function getArtemisGovernorCalldata(address timelock, address wormholeCore) 91 | public 92 | view 93 | returns (bytes memory) 94 | { 95 | bytes memory timelockCalldata = getTimelockCalldata(timelock); 96 | 97 | address[] memory targets = new address[](1); 98 | targets[0] = wormholeCore; 99 | 100 | uint256[] memory values = new uint256[](1); 101 | values[0] = 0; 102 | 103 | bytes[] memory payloads = new bytes[](1); 104 | payloads[0] = timelockCalldata; 105 | 106 | string[] memory signatures = new string[](1); 107 | signatures[0] = ""; 108 | 109 | bytes memory artemisPayload = abi.encodeWithSignature( 110 | "propose(address[],uint256[],string[],bytes[],string)", 111 | targets, 112 | values, 113 | signatures, 114 | payloads, 115 | description() 116 | ); 117 | 118 | return artemisPayload; 119 | } 120 | 121 | function printActions(address timelock, address wormholeCore) public { 122 | bytes memory timelockCalldata = getTimelockCalldata(timelock); 123 | 124 | console.log("timelock governance calldata"); 125 | emit log_bytes(timelockCalldata); 126 | 127 | bytes memory wormholePublishCalldata = abi.encodeWithSignature( 128 | "publishMessage(uint32,bytes,uint8)", 129 | nonce, 130 | timelockCalldata, 131 | consistencyLevel 132 | ); 133 | 134 | console.log("wormhole publish governance calldata"); 135 | emit log_bytes(wormholePublishCalldata); 136 | 137 | bytes memory artemisPayload = 138 | getArtemisGovernorCalldata(timelock, wormholeCore); 139 | 140 | console.log("artemis governor queue governance calldata"); 141 | emit log_bytes(artemisPayload); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/proposals/GovernorBravoProposal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@forge-std/console.sol"; 5 | 6 | import { 7 | IGovernorBravo, 8 | ITimelockBravo, 9 | IERC20VotesComp 10 | } from "@interface/IGovernorBravo.sol"; 11 | 12 | import {Address} from "@utils/Address.sol"; 13 | 14 | import {Proposal} from "./Proposal.sol"; 15 | 16 | abstract contract GovernorBravoProposal is Proposal { 17 | using Address for address; 18 | 19 | /// @notice Governor Bravo contract 20 | /// @dev must be set by the inheriting contract 21 | IGovernorBravo public governor; 22 | 23 | /// @notice set the Governor Bravo contract 24 | function setGovernor(address _governor) public { 25 | governor = IGovernorBravo(_governor); 26 | } 27 | 28 | /// @notice Getter function for `GovernorBravoDelegate.propose()` calldata 29 | function getCalldata() 30 | public 31 | view 32 | virtual 33 | override 34 | returns (bytes memory data) 35 | { 36 | ( 37 | address[] memory targets, 38 | uint256[] memory values, 39 | bytes[] memory calldatas 40 | ) = getProposalActions(); 41 | string[] memory signatures = new string[](targets.length); 42 | 43 | data = abi.encodeWithSignature( 44 | "propose(address[],uint256[],string[],bytes[],string)", 45 | targets, 46 | values, 47 | signatures, 48 | calldatas, 49 | description() 50 | ); 51 | } 52 | 53 | /// @notice Check if there are any on-chain proposals that match the 54 | /// proposal calldata 55 | function getProposalId() 56 | public 57 | view 58 | override 59 | returns (uint256 proposalId) 60 | { 61 | uint256 proposalCount = governor.proposalCount(); 62 | 63 | while (proposalCount > 0) { 64 | ( 65 | address[] memory targets, 66 | uint256[] memory values, 67 | string[] memory signatures, 68 | bytes[] memory calldatas 69 | ) = governor.getActions(proposalCount); 70 | 71 | bytes memory onchainCalldata = abi.encodeWithSignature( 72 | "propose(address[],uint256[],string[],bytes[],string)", 73 | targets, 74 | values, 75 | signatures, 76 | calldatas, 77 | description() 78 | ); 79 | 80 | bytes memory proposalCalldata = getCalldata(); 81 | 82 | if (keccak256(proposalCalldata) == keccak256(onchainCalldata)) { 83 | if (DEBUG) { 84 | console.log( 85 | "Proposal calldata matches on-chain calldata with proposalId: ", 86 | proposalCount 87 | ); 88 | } 89 | return proposalCount; 90 | } 91 | 92 | proposalCount--; 93 | } 94 | return 0; 95 | } 96 | 97 | /// @notice Simulate governance proposal 98 | function simulate() public override { 99 | address proposerAddress = address(1); 100 | IERC20VotesComp governanceToken = governor.comp(); 101 | { 102 | // Ensure proposer has meets minimum proposal threshold and quorum votes to pass the proposal 103 | uint256 quorumVotes = governor.quorumVotes(); 104 | uint256 proposalThreshold = governor.proposalThreshold(); 105 | uint256 votingPower = quorumVotes > proposalThreshold 106 | ? quorumVotes 107 | : proposalThreshold; 108 | deal(address(governanceToken), proposerAddress, votingPower); 109 | // Delegate proposer's votes to itself 110 | vm.prank(proposerAddress); 111 | IERC20VotesComp(governanceToken).delegate(proposerAddress); 112 | vm.roll(block.number + 1); 113 | } 114 | 115 | bytes memory proposeCalldata = getCalldata(); 116 | 117 | // Register the proposal 118 | vm.prank(proposerAddress); 119 | bytes memory data = address(governor).functionCall(proposeCalldata); 120 | uint256 proposalId = abi.decode(data, (uint256)); 121 | 122 | // Check proposal is in Pending state 123 | require( 124 | governor.state(proposalId) == IGovernorBravo.ProposalState.Pending 125 | ); 126 | 127 | // Roll to Active state (voting period) 128 | vm.roll(block.number + governor.votingDelay() + 1); 129 | require( 130 | governor.state(proposalId) == IGovernorBravo.ProposalState.Active 131 | ); 132 | 133 | // Vote YES 134 | vm.prank(proposerAddress); 135 | governor.castVote(proposalId, 1); 136 | 137 | // Roll to allow proposal state transitions 138 | vm.roll(block.number + governor.votingPeriod()); 139 | require( 140 | governor.state(proposalId) == IGovernorBravo.ProposalState.Succeeded 141 | ); 142 | 143 | // Queue the proposal 144 | governor.queue(proposalId); 145 | require( 146 | governor.state(proposalId) == IGovernorBravo.ProposalState.Queued 147 | ); 148 | 149 | // Warp to allow proposal execution on timelock 150 | ITimelockBravo timelock = ITimelockBravo(governor.timelock()); 151 | vm.warp(block.timestamp + timelock.delay()); 152 | 153 | // Execute the proposal 154 | governor.execute(proposalId); 155 | require( 156 | governor.state(proposalId) == IGovernorBravo.ProposalState.Executed 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/proposals/IProposal.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {Addresses} from "@addresses/Addresses.sol"; 4 | 5 | interface IProposal { 6 | /// @notice proposal name, e.g. "BIP15". 7 | /// @dev override this to set the proposal name. 8 | function name() external view returns (string memory); 9 | 10 | /// @notice proposal description. 11 | /// @dev override this to set the proposal description. 12 | function description() external view returns (string memory); 13 | 14 | /// @notice function to be used by forge script. 15 | /// @dev use flags to determine which actions to take 16 | /// this function shoudn't be overriden. 17 | function run() external; 18 | 19 | /// @notice return proposal actions. 20 | /// @dev this function shoudn't be overriden. 21 | function getProposalActions() 22 | external 23 | returns ( 24 | address[] memory targets, 25 | uint256[] memory values, 26 | bytes[] memory arguments 27 | ); 28 | 29 | /// @notice return proposal calldata 30 | function getCalldata() external returns (bytes memory data); 31 | 32 | /// @notice check and return proposal id if there are any on-chain proposal that matches the 33 | /// proposal calldata 34 | function getProposalId() external returns (uint256); 35 | 36 | /// @notice return Addresses object 37 | function addresses() external view returns (Addresses); 38 | 39 | /// @notice deploy any contracts needed for the proposal. 40 | /// @dev contracts calls here are broadcast if the broadcast flag is set. 41 | function deploy() external; 42 | 43 | /// @notice helper function to mock on-chain data before build 44 | /// e.g. pranking, etching, etc. 45 | function preBuildMock() external; 46 | 47 | /// @notice build the proposal actions 48 | /// @dev contract calls must be perfomed in plain solidity. 49 | /// overriden requires using buildModifier modifier to leverage 50 | /// foundry snapshot and state diff recording to populate the actions array. 51 | function build() external; 52 | 53 | /// @notice actually simulates the proposal. 54 | /// e.g. schedule and execute on Timelock Controller, 55 | /// proposes, votes and execute on Governor Bravo, etc. 56 | function simulate() external; 57 | 58 | /// @notice execute post-proposal checks. 59 | /// e.g. read state variables of the deployed contracts to make 60 | /// sure they are deployed and initialized correctly, or read 61 | /// states that are expected to have changed during the simulate step. 62 | function validate() external; 63 | 64 | /// @notice print proposal description, actions and calldata 65 | function print() external; 66 | 67 | /// @notice set the Addresses contract 68 | function setAddresses(Addresses _addresses) external; 69 | 70 | /// @notice set the primary fork id 71 | function setPrimaryForkId(uint256 _forkId) external; 72 | } 73 | -------------------------------------------------------------------------------- /src/proposals/MultisigProposal.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@forge-std/console.sol"; 4 | 5 | import {Address} from "@utils/Address.sol"; 6 | import {Proposal} from "./Proposal.sol"; 7 | import {Constants} from "@utils/Constants.sol"; 8 | 9 | abstract contract MultisigProposal is Proposal { 10 | using Address for address; 11 | 12 | bytes32 public constant MULTISIG_BYTECODE_HASH = bytes32( 13 | 0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000 14 | ); 15 | 16 | struct Call3Value { 17 | address target; 18 | bool allowFailure; 19 | uint256 value; 20 | bytes callData; 21 | } 22 | 23 | /// @notice return calldata, log if debug is set to true 24 | function getCalldata() public view override returns (bytes memory data) { 25 | /// get proposal actions 26 | ( 27 | address[] memory targets, 28 | uint256[] memory values, 29 | bytes[] memory arguments 30 | ) = getProposalActions(); 31 | 32 | /// create calls array with targets and arguments 33 | Call3Value[] memory calls = new Call3Value[](targets.length); 34 | 35 | for (uint256 i; i < calls.length; i++) { 36 | require(targets[i] != address(0), "Invalid target for multisig"); 37 | calls[i] = Call3Value({ 38 | target: targets[i], 39 | allowFailure: false, 40 | value: values[i], 41 | callData: arguments[i] 42 | }); 43 | } 44 | 45 | /// generate calldata 46 | data = abi.encodeWithSignature( 47 | "aggregate3Value((address,bool,uint256,bytes)[])", calls 48 | ); 49 | } 50 | 51 | /// @notice Check if there are any on-chain proposal that matches the 52 | /// proposal calldata 53 | function getProposalId() public pure override returns (uint256) { 54 | revert("Not implemented"); 55 | } 56 | 57 | function _simulateActions(address multisig) internal { 58 | vm.startPrank(multisig); 59 | 60 | /// this is a hack because multisig execTransaction requires owners signatures 61 | /// so we cannot simulate it exactly as it will be executed on mainnet 62 | vm.etch(multisig, Constants.MULTICALL_BYTECODE); 63 | 64 | bytes memory data = getCalldata(); 65 | 66 | multisig.functionCall(data); 67 | 68 | /// revert contract code to original safe bytecode 69 | vm.etch(multisig, Constants.SAFE_BYTECODE); 70 | 71 | vm.stopPrank(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/proposals/OZGovernorProposal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@forge-std/console.sol"; 5 | 6 | import { 7 | IGovernor, 8 | IGovernorTimelockControl, 9 | IGovernorVotes 10 | } from "@interface/IGovernor.sol"; 11 | import {IVotes} from "@interface/IVotes.sol"; 12 | import {ITimelockController} from "@interface/ITimelockController.sol"; 13 | 14 | // TODO move utils to inside src 15 | import {Address} from "@utils/Address.sol"; 16 | 17 | import {Proposal} from "./Proposal.sol"; 18 | 19 | abstract contract OZGovernorProposal is Proposal { 20 | using Address for address; 21 | 22 | /// @notice Governor contract 23 | /// @dev must be set by the inheriting contract 24 | IGovernor public governor; 25 | 26 | /// @notice set the Governor contract 27 | function setGovernor(address _governor) public { 28 | governor = IGovernor(_governor); 29 | } 30 | 31 | /// @notice Getter function for `IGovernor.propose()` calldata 32 | function getCalldata() 33 | public 34 | virtual 35 | override 36 | returns (bytes memory data) 37 | { 38 | ( 39 | address[] memory targets, 40 | uint256[] memory values, 41 | bytes[] memory calldatas 42 | ) = getProposalActions(); 43 | 44 | data = abi.encodeWithSignature( 45 | "propose(address[],uint256[],bytes[],string)", 46 | targets, 47 | values, 48 | calldatas, 49 | description() 50 | ); 51 | } 52 | 53 | /// @notice Check if there are any on-chain proposals that match the 54 | /// proposal calldata 55 | function getProposalId() 56 | public 57 | view 58 | override 59 | returns (uint256 proposalId) 60 | { 61 | ( 62 | address[] memory targets, 63 | uint256[] memory values, 64 | bytes[] memory calldatas 65 | ) = getProposalActions(); 66 | 67 | proposalId = governor.hashProposal( 68 | targets, 69 | values, 70 | calldatas, 71 | keccak256(abi.encodePacked(description())) 72 | ); 73 | 74 | // proposal exist if state call doesn't revert 75 | try governor.state(proposalId) { 76 | return proposalId; 77 | } catch { 78 | return 0; 79 | } 80 | } 81 | 82 | /// @notice Simulate governance proposal 83 | function simulate() public virtual override { 84 | address proposerAddress = address(1); 85 | IVotes governanceToken = 86 | IVotes(IGovernorVotes(address(governor)).token()); 87 | { 88 | // Ensure proposer has meets minimum proposal threshold and quorum votes to pass the proposal 89 | uint256 quorumVotes = governor.quorum(block.number - 1); 90 | uint256 proposalThreshold = governor.proposalThreshold(); 91 | uint256 votingPower = quorumVotes > proposalThreshold 92 | ? quorumVotes 93 | : proposalThreshold; 94 | deal(address(governanceToken), proposerAddress, votingPower); 95 | vm.roll(block.number - 1); 96 | // Delegate proposer's votes to itself 97 | vm.prank(proposerAddress); 98 | IVotes(governanceToken).delegate(proposerAddress); 99 | vm.roll(block.number + 2); 100 | } 101 | 102 | bytes memory proposeCalldata = getCalldata(); 103 | 104 | // Register the proposal 105 | vm.prank(proposerAddress); 106 | bytes memory data = address(governor).functionCall(proposeCalldata); 107 | 108 | uint256 returnedProposalId = abi.decode(data, (uint256)); 109 | 110 | ( 111 | address[] memory targets, 112 | uint256[] memory values, 113 | bytes[] memory calldatas 114 | ) = getProposalActions(); 115 | 116 | // Check that the proposal was registered correctly 117 | uint256 proposalId = governor.hashProposal( 118 | targets, 119 | values, 120 | calldatas, 121 | keccak256(abi.encodePacked(description())) 122 | ); 123 | 124 | require(returnedProposalId == proposalId, "Proposal id mismatch"); 125 | 126 | // Check proposal is in Pending state 127 | require(governor.state(proposalId) == IGovernor.ProposalState.Pending); 128 | 129 | // Roll to Active state (voting period) 130 | vm.roll(block.number + governor.votingDelay() + 1); 131 | 132 | require(governor.state(proposalId) == IGovernor.ProposalState.Active); 133 | 134 | // Vote YES 135 | vm.prank(proposerAddress); 136 | governor.castVote(proposalId, 1); 137 | 138 | // Roll to allow proposal state transitions 139 | vm.roll(block.number + governor.votingPeriod()); 140 | 141 | require(governor.state(proposalId) == IGovernor.ProposalState.Succeeded); 142 | 143 | vm.warp(block.timestamp + governor.proposalEta(proposalId) + 1); 144 | 145 | // Queue the proposal 146 | governor.queue( 147 | targets, 148 | values, 149 | calldatas, 150 | keccak256(abi.encodePacked(description())) 151 | ); 152 | 153 | require(governor.state(proposalId) == IGovernor.ProposalState.Queued); 154 | 155 | // Warp to allow proposal execution on timelock 156 | ITimelockController timelock = ITimelockController( 157 | IGovernorTimelockControl(address(governor)).timelock() 158 | ); 159 | vm.warp(block.timestamp + timelock.getMinDelay()); 160 | 161 | // Execute the proposal 162 | governor.execute( 163 | targets, 164 | values, 165 | calldatas, 166 | keccak256(abi.encodePacked(description())) 167 | ); 168 | 169 | require(governor.state(proposalId) == IGovernor.ProposalState.Executed); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/proposals/TimelockProposal.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {console} from "@forge-std/console.sol"; 4 | 5 | import {ITimelockController} from "@interface/ITimelockController.sol"; 6 | 7 | import {Address} from "@utils/Address.sol"; 8 | import {Proposal} from "@proposals/Proposal.sol"; 9 | 10 | abstract contract TimelockProposal is Proposal { 11 | using Address for address; 12 | 13 | /// @notice the predecessor timelock id - default is 0 but inherited 14 | bytes32 public predecessor = bytes32(0); 15 | 16 | /// @notice the timelock controller 17 | /// @dev must be set by the inheriting contract 18 | ITimelockController public timelock; 19 | 20 | /// @notice set the timelock controller 21 | function setTimelock(address _timelock) public { 22 | timelock = ITimelockController(_timelock); 23 | } 24 | 25 | /// @notice get schedule calldata 26 | function getCalldata() 27 | public 28 | view 29 | override 30 | returns (bytes memory scheduleCalldata) 31 | { 32 | bytes32 salt = keccak256(abi.encode(description())); 33 | 34 | ( 35 | address[] memory targets, 36 | uint256[] memory values, 37 | bytes[] memory payloads 38 | ) = getProposalActions(); 39 | 40 | uint256 delay = timelock.getMinDelay(); 41 | 42 | scheduleCalldata = abi.encodeWithSignature( 43 | "scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256)", 44 | targets, 45 | values, 46 | payloads, 47 | predecessor, 48 | salt, 49 | delay 50 | ); 51 | } 52 | 53 | /// @notice get execute calldata 54 | function getExecuteCalldata() 55 | public 56 | view 57 | returns (bytes memory executeCalldata) 58 | { 59 | bytes32 salt = keccak256(abi.encode(description())); 60 | 61 | ( 62 | address[] memory targets, 63 | uint256[] memory values, 64 | bytes[] memory payloads 65 | ) = getProposalActions(); 66 | 67 | executeCalldata = abi.encodeWithSignature( 68 | "executeBatch(address[],uint256[],bytes[],bytes32,bytes32)", 69 | targets, 70 | values, 71 | payloads, 72 | predecessor, 73 | salt 74 | ); 75 | } 76 | 77 | /// @notice Check and return proposal hash if there are any on-chain proposal that matches the 78 | /// proposal calldata 79 | function getProposalId() 80 | public 81 | view 82 | override 83 | returns (uint256 proposalId) 84 | { 85 | ( 86 | address[] memory targets, 87 | uint256[] memory values, 88 | bytes[] memory payloads 89 | ) = getProposalActions(); 90 | 91 | bytes32 salt = keccak256(abi.encode(description())); 92 | 93 | bytes32 hash = timelock.hashOperationBatch( 94 | targets, values, payloads, predecessor, salt 95 | ); 96 | 97 | if (DEBUG) { 98 | console.log( 99 | "Proposal calldata matches on-chain calldata with proposal hash: " 100 | ); 101 | console.logBytes32(hash); 102 | } 103 | 104 | if (timelock.isOperation(hash) || timelock.isOperationPending(hash)) { 105 | return uint256(hash); 106 | } else { 107 | return 0; 108 | } 109 | } 110 | 111 | /// @notice simulate timelock proposal 112 | /// @param proposerAddress account to propose the proposal to the timelock 113 | /// @param executorAddress account to execute the proposal on the timelock 114 | function _simulateActions(address proposerAddress, address executorAddress) 115 | internal 116 | { 117 | bytes32 salt = keccak256(abi.encode(description())); 118 | 119 | if (DEBUG) { 120 | console.log("salt:"); 121 | console.logBytes32(salt); 122 | } 123 | 124 | bytes memory scheduleCalldata = getCalldata(); 125 | bytes memory executeCalldata = getExecuteCalldata(); 126 | 127 | ( 128 | address[] memory targets, 129 | uint256[] memory values, 130 | bytes[] memory payloads 131 | ) = getProposalActions(); 132 | 133 | bytes32 proposalId = timelock.hashOperationBatch( 134 | targets, values, payloads, predecessor, salt 135 | ); 136 | 137 | if ( 138 | !timelock.isOperationPending(proposalId) 139 | && !timelock.isOperation(proposalId) 140 | ) { 141 | vm.prank(proposerAddress); 142 | 143 | // Perform the low-level call 144 | bytes memory returndata = 145 | address(timelock).functionCall(scheduleCalldata); 146 | 147 | if (DEBUG && returndata.length > 0) { 148 | console.log("schedule calldata return data:"); 149 | console.logBytes(returndata); 150 | } 151 | } else if (DEBUG) { 152 | console.log("proposal already scheduled for id"); 153 | console.logBytes32(proposalId); 154 | } 155 | 156 | uint256 delay = timelock.getMinDelay(); 157 | vm.warp(block.timestamp + delay); 158 | 159 | if (!timelock.isOperationDone(proposalId)) { 160 | vm.prank(executorAddress); 161 | 162 | // Perform the low-level call 163 | bytes memory returndata = 164 | address(timelock).functionCall(executeCalldata); 165 | 166 | if (DEBUG && returndata.length > 0) { 167 | console.log("returndata"); 168 | console.logBytes(returndata); 169 | } 170 | } else if (DEBUG) { 171 | console.log("proposal already executed"); 172 | } 173 | } 174 | 175 | /// @notice print schedule and execute calldata 176 | function _printProposalCalldata() internal view override { 177 | console.log( 178 | "\n\n------------------ Schedule Calldata ------------------" 179 | ); 180 | console.logBytes(getCalldata()); 181 | 182 | console.log( 183 | "\n\n------------------ Execute Calldata -------------------" 184 | ); 185 | console.logBytes(getExecuteCalldata()); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /test/BravoProposal.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "@forge-std/Test.sol"; 5 | 6 | import {Addresses} from "@addresses/Addresses.sol"; 7 | import {GovernorBravoProposal} from "@proposals/GovernorBravoProposal.sol"; 8 | import {MockBravoProposal} from "@mocks/MockBravoProposal.sol"; 9 | 10 | contract BravoProposalIntegrationTest is Test { 11 | Addresses public addresses; 12 | GovernorBravoProposal public proposal; 13 | 14 | function setUp() public { 15 | uint256[] memory chainIds = new uint256[](1); 16 | chainIds[0] = 1; 17 | 18 | // Instantiate the Addresses contract 19 | addresses = new Addresses("./addresses", chainIds); 20 | vm.makePersistent(address(addresses)); 21 | 22 | // Instantiate the BravoProposal contract 23 | proposal = GovernorBravoProposal(new MockBravoProposal()); 24 | 25 | proposal.setPrimaryForkId(vm.createSelectFork("mainnet")); 26 | 27 | // Set the addresses contract 28 | proposal.setAddresses(addresses); 29 | 30 | // Set the bravo address 31 | proposal.setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 32 | } 33 | 34 | function test_setUp() public view { 35 | assertEq( 36 | proposal.name(), 37 | string("ADJUST_WETH_IR_CURVE"), 38 | "Wrong proposal name" 39 | ); 40 | assertEq( 41 | proposal.description(), 42 | string( 43 | "Mock proposal that adjust IR Curve for Compound v3 WETH on Mainnet" 44 | ), 45 | "Wrong proposal description" 46 | ); 47 | assertEq( 48 | address(proposal.governor()), 49 | addresses.getAddress("COMPOUND_GOVERNOR_BRAVO"), 50 | "Wrong governor address" 51 | ); 52 | } 53 | 54 | function test_build() public { 55 | vm.expectRevert("No actions found"); 56 | proposal.getProposalActions(); 57 | 58 | proposal.build(); 59 | 60 | ( 61 | address[] memory targets, 62 | uint256[] memory values, 63 | bytes[] memory calldatas 64 | ) = proposal.getProposalActions(); 65 | 66 | address target = addresses.getAddress("COMPOUND_CONFIGURATOR"); 67 | assertEq(targets.length, 2, "Wrong targets length"); 68 | assertEq(targets[0], target, "Wrong target at index 0"); 69 | assertEq(targets[1], target, "Wrong target at index 1"); 70 | 71 | uint256 expectedValue = 0; 72 | assertEq(values.length, 2, "Wrong values length"); 73 | assertEq(values[0], expectedValue, "Wrong value at index 0"); 74 | assertEq(values[1], expectedValue, "Wrong value at index 1"); 75 | 76 | uint64 kink = 750000000000000000; 77 | assertEq(calldatas.length, 2); 78 | assertEq( 79 | calldatas[0], 80 | abi.encodeWithSignature( 81 | "setBorrowKink(address,uint64)", 82 | addresses.getAddress("COMPOUND_COMET"), 83 | kink 84 | ), 85 | "Wrong calldata at index 0" 86 | ); 87 | 88 | assertEq( 89 | calldatas[1], 90 | abi.encodeWithSignature( 91 | "setSupplyKink(address,uint64)", 92 | addresses.getAddress("COMPOUND_COMET"), 93 | kink 94 | ), 95 | "Wrong calldata at index 1" 96 | ); 97 | } 98 | 99 | function test_simulate() public { 100 | test_build(); 101 | 102 | proposal.simulate(); 103 | 104 | // check that proposal exists 105 | assertTrue(proposal.getProposalId() > 0); 106 | 107 | proposal.validate(); 108 | } 109 | 110 | function test_getCalldata() public { 111 | test_build(); 112 | 113 | ( 114 | address[] memory targets, 115 | uint256[] memory values, 116 | bytes[] memory calldatas 117 | ) = proposal.getProposalActions(); 118 | 119 | string[] memory signatures = new string[](targets.length); 120 | 121 | bytes memory expectedData = abi.encodeWithSignature( 122 | "propose(address[],uint256[],string[],bytes[],string)", 123 | targets, 124 | values, 125 | signatures, 126 | calldatas, 127 | proposal.description() 128 | ); 129 | 130 | bytes memory data = proposal.getCalldata(); 131 | 132 | assertEq(data, expectedData, "Wrong propose calldata"); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /test/DuplicatedAction.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "@forge-std/Test.sol"; 5 | 6 | import {Addresses} from "@addresses/Addresses.sol"; 7 | import {GovernorBravoProposal} from "@proposals/GovernorBravoProposal.sol"; 8 | import {MockDuplicatedActionProposal} from 9 | "@mocks/MockDuplicatedActionProposal.sol"; 10 | 11 | contract DuplicatedActionProposalIntegrationTest is Test { 12 | Addresses public addresses; 13 | GovernorBravoProposal public proposal; 14 | 15 | function setUp() public { 16 | uint256[] memory chainIds = new uint256[](1); 17 | chainIds[0] = 1; 18 | 19 | // Instantiate the Addresses contract 20 | addresses = new Addresses("./addresses", chainIds); 21 | vm.makePersistent(address(addresses)); 22 | 23 | // Instantiate the BravoProposal contract 24 | proposal = GovernorBravoProposal(new MockDuplicatedActionProposal()); 25 | 26 | proposal.setPrimaryForkId(vm.createSelectFork("mainnet")); 27 | 28 | // Set the addresses contract 29 | proposal.setAddresses(addresses); 30 | 31 | // Set the bravo address 32 | proposal.setGovernor(addresses.getAddress("COMPOUND_GOVERNOR_BRAVO")); 33 | } 34 | 35 | function test_build() public { 36 | vm.expectRevert("No actions found"); 37 | proposal.getProposalActions(); 38 | 39 | vm.expectRevert("Duplicated action found"); 40 | proposal.build(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/GovernorOZProposal.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "@forge-std/Test.sol"; 5 | 6 | import {Addresses} from "@addresses/Addresses.sol"; 7 | import {OZGovernorProposal} from "@proposals/OZGovernorProposal.sol"; 8 | import {MockOZGovernorProposal} from "@mocks/MockOZGovernorProposal.sol"; 9 | 10 | contract OZGovernorProposalIntegrationTest is Test { 11 | Addresses public addresses; 12 | OZGovernorProposal public proposal; 13 | 14 | function setUp() public { 15 | uint256[] memory chainIds = new uint256[](1); 16 | chainIds[0] = 1; 17 | 18 | // Instantiate the Addresses contract 19 | addresses = new Addresses("./addresses", chainIds); 20 | vm.makePersistent(address(addresses)); 21 | 22 | // Instantiate the OZ Proposal contract 23 | proposal = OZGovernorProposal(new MockOZGovernorProposal()); 24 | 25 | // Select the primary fork 26 | // ENS Governor is not cross chain so there is only a fork and should be mainnet 27 | proposal.setPrimaryForkId(vm.createSelectFork("mainnet")); 28 | 29 | // Set the addresses contract 30 | proposal.setAddresses(addresses); 31 | 32 | // Set the bravo address 33 | proposal.setGovernor(addresses.getAddress("ENS_GOVERNOR")); 34 | } 35 | 36 | function test_setUp() public view { 37 | assertEq( 38 | proposal.name(), 39 | string("UPGRADE_DNSSEC_SUPPORT"), 40 | "Wrong proposal name" 41 | ); 42 | assertEq( 43 | proposal.description(), 44 | string( 45 | "Call setController on the Root contract at root.ens.eth, passing in the address of the new DNS registrar" 46 | ), 47 | "Wrong proposal description" 48 | ); 49 | assertEq( 50 | address(proposal.governor()), 51 | addresses.getAddress("ENS_GOVERNOR"), 52 | "Wrong governor address" 53 | ); 54 | } 55 | 56 | function test_deploy() public { 57 | vm.startPrank(addresses.getAddress("DEPLOYER_EOA")); 58 | proposal.deploy(); 59 | vm.stopPrank(); 60 | 61 | assertTrue(addresses.isAddressSet("ENS_DNSSEC")); 62 | } 63 | 64 | function test_build() public { 65 | test_deploy(); 66 | 67 | vm.expectRevert("No actions found"); 68 | proposal.getProposalActions(); 69 | 70 | proposal.build(); 71 | 72 | ( 73 | address[] memory targets, 74 | uint256[] memory values, 75 | bytes[] memory calldatas 76 | ) = proposal.getProposalActions(); 77 | 78 | address target = addresses.getAddress("ENS_ROOT"); 79 | assertEq(targets.length, 1, "Wrong targets length"); 80 | assertEq(targets[0], target, "Wrong target at index 0"); 81 | assertEq(targets[0], target, "Wrong target at index 1"); 82 | 83 | uint256 expectedValue = 0; 84 | assertEq(values.length, 1, "Wrong values length"); 85 | assertEq(values[0], expectedValue, "Wrong value at index 0"); 86 | assertEq(values[0], expectedValue, "Wrong value at index 1"); 87 | 88 | assertEq(calldatas.length, 1); 89 | assertEq( 90 | calldatas[0], 91 | abi.encodeWithSignature( 92 | "setController(address,bool)", 93 | addresses.getAddress("ENS_DNSSEC"), 94 | true 95 | ), 96 | "Wrong calldata at index 0" 97 | ); 98 | } 99 | 100 | function test_simulate() public { 101 | test_build(); 102 | 103 | proposal.simulate(); 104 | 105 | // check that proposal exists 106 | assertTrue(proposal.getProposalId() > 0); 107 | 108 | proposal.validate(); 109 | } 110 | 111 | function test_getCalldata() public { 112 | test_build(); 113 | 114 | ( 115 | address[] memory targets, 116 | uint256[] memory values, 117 | bytes[] memory calldatas 118 | ) = proposal.getProposalActions(); 119 | 120 | bytes memory expectedData = abi.encodeWithSignature( 121 | "propose(address[],uint256[],bytes[],string)", 122 | targets, 123 | values, 124 | calldatas, 125 | proposal.description() 126 | ); 127 | 128 | bytes memory data = proposal.getCalldata(); 129 | 130 | assertEq(data, expectedData, "Wrong propose calldata"); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/MultisigProposal.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "@forge-std/Test.sol"; 5 | 6 | import {Addresses} from "@addresses/Addresses.sol"; 7 | import {MultisigProposal} from "@proposals/MultisigProposal.sol"; 8 | import {MockMultisigProposal} from "@mocks/MockMultisigProposal.sol"; 9 | 10 | contract MultisigProposalIntegrationTest is Test { 11 | Addresses public addresses; 12 | MultisigProposal public proposal; 13 | 14 | struct Call3Value { 15 | address target; 16 | bool allowFailure; 17 | uint256 value; 18 | bytes callData; 19 | } 20 | 21 | function setUp() public { 22 | uint256[] memory chainIds = new uint256[](1); 23 | chainIds[0] = 1; 24 | 25 | // Instantiate the Addresses contract 26 | addresses = new Addresses("./addresses", chainIds); 27 | vm.makePersistent(address(addresses)); 28 | 29 | // Instantiate the MultisigProposal contract 30 | proposal = MultisigProposal(new MockMultisigProposal()); 31 | 32 | proposal.setPrimaryForkId(vm.createSelectFork("mainnet")); 33 | 34 | // Set the addresses contract 35 | proposal.setAddresses(addresses); 36 | } 37 | 38 | function test_setUp() public view { 39 | assertEq( 40 | proposal.name(), 41 | string("OPTMISM_MULTISIG_MOCK"), 42 | "Wrong proposal name" 43 | ); 44 | assertEq( 45 | proposal.description(), 46 | string("Mock proposal that upgrade the L1 NFT Bridge"), 47 | "Wrong proposal description" 48 | ); 49 | } 50 | 51 | function test_deploy() public { 52 | vm.startPrank(addresses.getAddress("DEPLOYER_EOA")); 53 | proposal.deploy(); 54 | vm.stopPrank(); 55 | 56 | assertTrue( 57 | addresses.isAddressSet("OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION") 58 | ); 59 | } 60 | 61 | function test_build() public { 62 | test_deploy(); 63 | 64 | vm.expectRevert("No actions found"); 65 | proposal.getProposalActions(); 66 | 67 | proposal.build(); 68 | 69 | ( 70 | address[] memory targets, 71 | uint256[] memory values, 72 | bytes[] memory calldatas 73 | ) = proposal.getProposalActions(); 74 | 75 | // check that the proposal targets are correct 76 | assertEq(targets.length, 1, "Wrong targets length"); 77 | assertEq( 78 | targets[0], 79 | addresses.getAddress("OPTIMISM_PROXY_ADMIN"), 80 | "Wrong target at index 0" 81 | ); 82 | 83 | // check that the proposal values are correct 84 | assertEq(values.length, 1, "Wrong values length"); 85 | assertEq(values[0], 0, "Wrong value at index 0"); 86 | 87 | // check that the proposal calldatas are correct 88 | assertEq(calldatas.length, 1); 89 | assertEq( 90 | calldatas[0], 91 | abi.encodeWithSignature( 92 | "upgrade(address,address)", 93 | addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_PROXY"), 94 | addresses.getAddress("OPTIMISM_L1_NFT_BRIDGE_IMPLEMENTATION") 95 | ), 96 | "Wrong calldata at index 0" 97 | ); 98 | } 99 | 100 | function test_simulate() public { 101 | test_build(); 102 | 103 | proposal.simulate(); 104 | 105 | proposal.validate(); 106 | } 107 | 108 | function test_getCalldata() public { 109 | test_build(); 110 | 111 | ( 112 | address[] memory targets, 113 | uint256[] memory values, 114 | bytes[] memory calldatas 115 | ) = proposal.getProposalActions(); 116 | 117 | Call3Value[] memory calls = new Call3Value[](targets.length); 118 | 119 | for (uint256 i; i < calls.length; i++) { 120 | calls[i] = Call3Value({ 121 | target: targets[i], 122 | allowFailure: false, 123 | value: values[i], 124 | callData: calldatas[i] 125 | }); 126 | } 127 | 128 | bytes memory expectedData = abi.encodeWithSignature( 129 | "aggregate3Value((address,bool,uint256,bytes)[])", calls 130 | ); 131 | 132 | bytes memory data = proposal.getCalldata(); 133 | 134 | assertEq(data, expectedData, "Wrong aggregate calldata"); 135 | } 136 | 137 | function test_getProposalId() public { 138 | vm.expectRevert("Not implemented"); 139 | proposal.getProposalId(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/MultisigProposalCalldataTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "@forge-std/Test.sol"; 5 | 6 | import {Addresses} from "@addresses/Addresses.sol"; 7 | import {MultisigProposal} from "@proposals/MultisigProposal.sol"; 8 | import {MultisigProposal_01} from 9 | "@test/multisigProposals/MultisigProposal_01.sol"; 10 | import {MultisigProposal_02} from 11 | "@test/multisigProposals/MultisigProposal_02.sol"; 12 | import {MultisigProposal_03} from 13 | "@test/multisigProposals/MultisigProposal_03.sol"; 14 | import {MultisigProposal_04} from 15 | "@test/multisigProposals/MultisigProposal_04.sol"; 16 | import {MultisigProposal_05} from 17 | "@test/multisigProposals/MultisigProposal_05.sol"; 18 | 19 | contract MultisigProposalCalldataTest is Test { 20 | Addresses public addresses; 21 | address[] public proposals; 22 | 23 | function setUp() public { 24 | // Instantiate the Addresses contract 25 | string memory addressesFolderPath = "./addresses"; 26 | uint256[] memory chainIds = new uint256[](1); 27 | chainIds[0] = 31337; 28 | addresses = new Addresses(addressesFolderPath, chainIds); 29 | 30 | // Instantiate the MultisigProposal contracts 31 | address proposal = address(new MultisigProposal_01()); 32 | proposals.push(proposal); 33 | 34 | proposal = address(new MultisigProposal_02()); 35 | proposals.push(proposal); 36 | 37 | proposal = address(new MultisigProposal_03()); 38 | proposals.push(proposal); 39 | 40 | proposal = address(new MultisigProposal_04()); 41 | proposals.push(proposal); 42 | 43 | proposal = address(new MultisigProposal_05()); 44 | proposals.push(proposal); 45 | 46 | for (uint256 i; i < proposals.length; ++i) { 47 | MultisigProposal(proposals[i]).setAddresses(addresses); 48 | MultisigProposal(proposals[i]).run(); 49 | } 50 | } 51 | 52 | function test_targets() public view { 53 | for (uint256 i; i < proposals.length; ++i) { 54 | (address[] memory targets,,) = 55 | MultisigProposal(proposals[i]).getProposalActions(); 56 | 57 | (address[] memory expectedTargets,,) = getProposalDetail(i); 58 | 59 | // check that the proposal targets are correct 60 | assertEq( 61 | targets.length, expectedTargets.length, "Wrong targets length" 62 | ); 63 | 64 | for (uint256 j; j < targets.length; ++j) { 65 | assertEq(targets[j], expectedTargets[j], "Incorrect target"); 66 | } 67 | } 68 | } 69 | 70 | function test_calldata() public view { 71 | for (uint256 i; i < proposals.length; ++i) { 72 | (,, bytes[] memory calldatas) = 73 | MultisigProposal(proposals[i]).getProposalActions(); 74 | 75 | (,, bytes[] memory expectedCalldatas) = getProposalDetail(i); 76 | 77 | // check that the proposal calldatas are correct 78 | assertEq( 79 | calldatas.length, 80 | expectedCalldatas.length, 81 | "Wrong calldatas length" 82 | ); 83 | 84 | for (uint256 j; j < calldatas.length; ++j) { 85 | assertEq( 86 | calldatas[j], expectedCalldatas[j], "Incorrect calldata" 87 | ); 88 | } 89 | } 90 | } 91 | 92 | function test_value() public view { 93 | for (uint256 i; i < proposals.length; ++i) { 94 | (, uint256[] memory values,) = 95 | MultisigProposal(proposals[i]).getProposalActions(); 96 | 97 | (, uint256[] memory expectedValues,) = getProposalDetail(i); 98 | 99 | // check that the proposal values are correct 100 | assertEq( 101 | values.length, expectedValues.length, "Wrong values length" 102 | ); 103 | 104 | for (uint256 j; j < values.length; ++j) { 105 | assertEq(values[j], expectedValues[j], "Incorrect value"); 106 | } 107 | } 108 | } 109 | 110 | function getProposalDetail(uint256 proposalIndex) 111 | public 112 | view 113 | returns ( 114 | address[] memory targets, 115 | uint256[] memory values, 116 | bytes[] memory calldatas 117 | ) 118 | { 119 | if (proposalIndex == 0) { 120 | return getFirstProposalDetail(); 121 | } else if (proposalIndex == 1) { 122 | return getSecondProposalDetail(); 123 | } else if (proposalIndex == 2) { 124 | return getThirdProposalDetail(); 125 | } else if (proposalIndex == 3) { 126 | return getFourthProposalDetail(); 127 | } else { 128 | return getFifthProposalDetail(); 129 | } 130 | } 131 | 132 | function getFirstProposalDetail() 133 | internal 134 | view 135 | returns ( 136 | address[] memory targets, 137 | uint256[] memory values, 138 | bytes[] memory calldatas 139 | ) 140 | { 141 | targets = new address[](5); 142 | values = new uint256[](5); 143 | calldatas = new bytes[](5); 144 | 145 | address mockToken = addresses.getAddress("TOKEN"); 146 | 147 | targets[0] = mockToken; 148 | calldatas[0] = abi.encodeWithSignature( 149 | "approve(address,uint256)", 150 | addresses.getAddress("DEPLOYER_EOA"), 151 | 200 152 | ); 153 | values[0] = 0; 154 | 155 | targets[1] = mockToken; 156 | calldatas[1] = abi.encodeWithSignature( 157 | "transfer(address,uint256)", 158 | addresses.getAddress("DEPLOYER_EOA"), 159 | 500 160 | ); 161 | values[1] = 0; 162 | 163 | targets[2] = mockToken; 164 | calldatas[2] = abi.encodeWithSignature( 165 | "transfer(address,uint256)", 166 | addresses.getAddress("DEPLOYER_EOA"), 167 | 100 168 | ); 169 | values[2] = 0; 170 | 171 | targets[3] = mockToken; 172 | calldatas[3] = abi.encodeWithSignature( 173 | "approve(address,uint256)", 174 | addresses.getAddress("PROTOCOL_MULTISIG"), 175 | 200 176 | ); 177 | values[3] = 0; 178 | 179 | targets[4] = mockToken; 180 | calldatas[4] = abi.encodeWithSignature( 181 | "transferFrom(address,address,uint256)", 182 | addresses.getAddress("PROTOCOL_MULTISIG"), 183 | addresses.getAddress("DEPLOYER_EOA"), 184 | 200 185 | ); 186 | values[4] = 0; 187 | } 188 | 189 | function getSecondProposalDetail() 190 | internal 191 | view 192 | returns ( 193 | address[] memory targets, 194 | uint256[] memory values, 195 | bytes[] memory calldatas 196 | ) 197 | { 198 | targets = new address[](8); 199 | values = new uint256[](8); 200 | calldatas = new bytes[](8); 201 | address tokenWrapper = addresses.getAddress("TOKEN_WRAPPER"); 202 | 203 | targets[0] = addresses.getAddress("TOKEN"); 204 | calldatas[0] = abi.encodeWithSignature( 205 | "approve(address,uint256)", address(tokenWrapper), 60 ether 206 | ); 207 | values[0] = 0; 208 | 209 | targets[1] = tokenWrapper; 210 | calldatas[1] = abi.encodeWithSignature("mint()"); 211 | values[1] = 10 ether; 212 | 213 | targets[2] = tokenWrapper; 214 | calldatas[2] = 215 | abi.encodeWithSignature("redeemTokens(uint256)", 10 ether); 216 | values[2] = 0; 217 | 218 | targets[3] = tokenWrapper; 219 | calldatas[3] = abi.encodeWithSignature("mint()"); 220 | values[3] = 20 ether; 221 | 222 | targets[4] = tokenWrapper; 223 | calldatas[4] = abi.encodeWithSignature("mint()"); 224 | values[4] = 30 ether; 225 | 226 | targets[5] = tokenWrapper; 227 | calldatas[5] = 228 | abi.encodeWithSignature("redeemTokens(uint256)", 50 ether); 229 | values[5] = 0; 230 | 231 | targets[6] = tokenWrapper; 232 | calldatas[6] = abi.encodeWithSignature("mint()"); 233 | values[6] = 40 ether; 234 | 235 | targets[7] = tokenWrapper; 236 | calldatas[7] = abi.encodeWithSignature("mint()"); 237 | values[7] = 50 ether; 238 | } 239 | 240 | function getThirdProposalDetail() 241 | internal 242 | view 243 | returns ( 244 | address[] memory targets, 245 | uint256[] memory values, 246 | bytes[] memory calldatas 247 | ) 248 | { 249 | targets = new address[](7); 250 | values = new uint256[](7); 251 | calldatas = new bytes[](7); 252 | 253 | address votingContract = addresses.getAddress("VOTING_CONTRACT"); 254 | 255 | targets[0] = votingContract; 256 | calldatas[0] = abi.encodeWithSignature("vote(string)", "candidate0"); 257 | values[0] = 0; 258 | 259 | targets[1] = votingContract; 260 | calldatas[1] = abi.encodeWithSignature("vote(string)", "candidate2"); 261 | values[1] = 0; 262 | 263 | targets[2] = votingContract; 264 | calldatas[2] = abi.encodeWithSignature("vote(string)", "candidate4"); 265 | values[2] = 0; 266 | 267 | targets[3] = votingContract; 268 | calldatas[3] = abi.encodeWithSignature("vote(string)", "candidate7"); 269 | values[3] = 0; 270 | 271 | targets[4] = votingContract; 272 | calldatas[4] = abi.encodeWithSignature("vote(string)", "candidate9"); 273 | values[4] = 0; 274 | 275 | targets[5] = votingContract; 276 | calldatas[5] = abi.encodeWithSignature("vote(string)", "candidate5"); 277 | values[5] = 0; 278 | 279 | targets[6] = votingContract; 280 | calldatas[6] = abi.encodeWithSignature("vote(string)", "candidate8"); 281 | values[6] = 0; 282 | } 283 | 284 | function getFourthProposalDetail() 285 | internal 286 | view 287 | returns ( 288 | address[] memory targets, 289 | uint256[] memory values, 290 | bytes[] memory calldatas 291 | ) 292 | { 293 | targets = new address[](5); 294 | values = new uint256[](5); 295 | calldatas = new bytes[](5); 296 | 297 | address auctionContract = addresses.getAddress("AUCTION_CONTRACT"); 298 | 299 | targets[0] = auctionContract; 300 | calldatas[0] = abi.encodeWithSignature("bid()"); 301 | values[0] = 10 ether; 302 | 303 | targets[1] = auctionContract; 304 | calldatas[1] = abi.encodeWithSignature("bid()"); 305 | values[1] = 40 ether; 306 | 307 | targets[2] = auctionContract; 308 | calldatas[2] = abi.encodeWithSignature("bid()"); 309 | values[2] = 50 ether; 310 | 311 | targets[3] = auctionContract; 312 | calldatas[3] = abi.encodeWithSignature("bid()"); 313 | values[3] = 90 ether; 314 | 315 | targets[4] = auctionContract; 316 | calldatas[4] = abi.encodeWithSignature("bid()"); 317 | values[4] = 100 ether; 318 | } 319 | 320 | function getFifthProposalDetail() 321 | internal 322 | view 323 | returns ( 324 | address[] memory targets, 325 | uint256[] memory values, 326 | bytes[] memory calldatas 327 | ) 328 | { 329 | targets = new address[](7); 330 | values = new uint256[](7); 331 | calldatas = new bytes[](7); 332 | 333 | address savingContract = addresses.getAddress("SAVING_CONTRACT"); 334 | 335 | targets[0] = savingContract; 336 | calldatas[0] = abi.encodeWithSignature("deposit(uint256)", 0); 337 | values[0] = 20 ether; 338 | 339 | targets[1] = savingContract; 340 | calldatas[1] = abi.encodeWithSignature("deposit(uint256)", 20 days); 341 | values[1] = 40 ether; 342 | 343 | targets[2] = savingContract; 344 | calldatas[2] = abi.encodeWithSignature("withdraw(uint256)", 0); 345 | values[2] = 0; 346 | 347 | targets[3] = savingContract; 348 | calldatas[3] = abi.encodeWithSignature("deposit(uint256)", 0); 349 | values[3] = 60 ether; 350 | 351 | targets[4] = savingContract; 352 | calldatas[4] = abi.encodeWithSignature("deposit(uint256)", 60 days); 353 | values[4] = 80 ether; 354 | 355 | targets[5] = savingContract; 356 | calldatas[5] = abi.encodeWithSignature("deposit(uint256)", 90 days); 357 | values[5] = 100 ether; 358 | 359 | targets[6] = savingContract; 360 | calldatas[6] = abi.encodeWithSignature("withdraw(uint256)", 2); 361 | values[6] = 0; 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /test/TimelockProposal.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "@forge-std/Test.sol"; 5 | 6 | import {Addresses} from "@addresses/Addresses.sol"; 7 | import {TimelockProposal} from "@proposals/TimelockProposal.sol"; 8 | import {MockTimelockProposal} from "@mocks/MockTimelockProposal.sol"; 9 | import {ITimelockController} from "@interface/ITimelockController.sol"; 10 | 11 | contract TimelockProposalIntegrationTest is Test { 12 | Addresses public addresses; 13 | TimelockProposal public proposal; 14 | 15 | function setUp() public { 16 | uint256[] memory chainIds = new uint256[](1); 17 | chainIds[0] = 1; 18 | 19 | // Instantiate the Addresses contract 20 | addresses = new Addresses("./addresses", chainIds); 21 | vm.makePersistent(address(addresses)); 22 | 23 | // Instantiate the TimelockProposal contract 24 | proposal = TimelockProposal(new MockTimelockProposal()); 25 | 26 | proposal.setPrimaryForkId(vm.createSelectFork("mainnet")); 27 | 28 | // Set the addresses contract 29 | proposal.setAddresses(addresses); 30 | 31 | // Set the timelock address 32 | proposal.setTimelock(addresses.getAddress("ARBITRUM_L1_TIMELOCK")); 33 | } 34 | 35 | function test_setUp() public view { 36 | assertEq( 37 | proposal.name(), 38 | string("ARBITRUM_L1_TIMELOCK_MOCK"), 39 | "Wrong proposal name" 40 | ); 41 | assertEq( 42 | proposal.description(), 43 | string("Mock proposal that upgrades the weth gateway"), 44 | "Wrong proposal description" 45 | ); 46 | } 47 | 48 | function test_deploy() public { 49 | vm.startPrank(addresses.getAddress("DEPLOYER_EOA")); 50 | proposal.deploy(); 51 | vm.stopPrank(); 52 | 53 | // calls after deploy mock to mock arbitrum outbox contract 54 | proposal.preBuildMock(); 55 | 56 | assertTrue( 57 | addresses.isAddressSet("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION") 58 | ); 59 | assertTrue(addresses.isAddressSet("ARBITRUM_GAC_UPGRADE_WETH_GATEWAY")); 60 | } 61 | 62 | function test_build() public { 63 | test_deploy(); 64 | 65 | vm.expectRevert("No actions found"); 66 | proposal.getProposalActions(); 67 | 68 | proposal.build(); 69 | 70 | ( 71 | address[] memory targets, 72 | uint256[] memory values, 73 | bytes[] memory calldatas 74 | ) = proposal.getProposalActions(); 75 | 76 | // check that the proposal targets are correct 77 | assertEq(targets.length, 1, "Wrong targets length"); 78 | assertEq( 79 | targets[0], 80 | addresses.getAddress("ARBITRUM_L1_UPGRADE_EXECUTOR"), 81 | "Wrong target at index 0" 82 | ); 83 | 84 | // check that the proposal values are correct 85 | assertEq(values.length, 1, "Wrong values length"); 86 | assertEq(values[0], 0, "Wrong value at index 0"); 87 | 88 | bytes memory innerCalldata = abi.encodeWithSignature( 89 | "upgradeWethGateway(address,address,address)", 90 | addresses.getAddress("ARBITRUM_L1_PROXY_ADMIN"), 91 | addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_PROXY"), 92 | addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION") 93 | ); 94 | // check that the proposal calldatas are correct 95 | assertEq(calldatas.length, 1); 96 | assertEq( 97 | calldatas[0], 98 | abi.encodeWithSignature( 99 | "execute(address,bytes)", 100 | addresses.getAddress("ARBITRUM_GAC_UPGRADE_WETH_GATEWAY"), 101 | innerCalldata 102 | ), 103 | "Wrong calldata at index 0" 104 | ); 105 | } 106 | 107 | function test_simulate() public { 108 | test_build(); 109 | 110 | proposal.simulate(); 111 | 112 | proposal.validate(); 113 | } 114 | 115 | function test_getCalldata() public { 116 | test_build(); 117 | 118 | ( 119 | address[] memory targets, 120 | uint256[] memory values, 121 | bytes[] memory calldatas 122 | ) = proposal.getProposalActions(); 123 | 124 | bytes32 salt = keccak256(abi.encode(proposal.description())); 125 | uint256 delay = ITimelockController( 126 | payable(addresses.getAddress("ARBITRUM_L1_TIMELOCK")) 127 | ).getMinDelay(); 128 | 129 | bytes memory expectedData = abi.encodeWithSignature( 130 | "scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256)", 131 | targets, 132 | values, 133 | calldatas, 134 | bytes32(0), 135 | salt, 136 | delay 137 | ); 138 | 139 | bytes memory data = proposal.getCalldata(); 140 | 141 | assertEq(data, expectedData, "Wrong scheduleBatch calldata"); 142 | } 143 | 144 | function test_getExecuteCalldata() public { 145 | test_build(); 146 | 147 | ( 148 | address[] memory targets, 149 | uint256[] memory values, 150 | bytes[] memory calldatas 151 | ) = proposal.getProposalActions(); 152 | 153 | bytes32 salt = keccak256(abi.encode(proposal.description())); 154 | 155 | bytes memory expectedData = abi.encodeWithSignature( 156 | "executeBatch(address[],uint256[],bytes[],bytes32,bytes32)", 157 | targets, 158 | values, 159 | calldatas, 160 | bytes32(0), 161 | salt 162 | ); 163 | 164 | bytes memory data = proposal.getExecuteCalldata(); 165 | 166 | assertEq(data, expectedData, "Wrong executeBatch calldata"); 167 | } 168 | 169 | function test_getProposalId() public { 170 | test_simulate(); 171 | 172 | ( 173 | address[] memory targets, 174 | uint256[] memory values, 175 | bytes[] memory calldatas 176 | ) = proposal.getProposalActions(); 177 | 178 | bytes32 salt = keccak256(abi.encode(proposal.description())); 179 | 180 | bytes32 hash = ITimelockController( 181 | payable(addresses.getAddress("ARBITRUM_L1_TIMELOCK")) 182 | ).hashOperationBatch(targets, values, calldatas, bytes32(0), salt); 183 | 184 | assertEq(proposal.getProposalId(), uint256(hash)); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /test/multisigProposals/MultisigProposal_01.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {MockToken} from "mocks/MockToken.sol"; 5 | 6 | import {MultisigProposal} from "@proposals/MultisigProposal.sol"; 7 | 8 | contract MultisigProposal_01 is MultisigProposal { 9 | function name() public pure override returns (string memory) { 10 | return "MOCK_MULTISIG_PROPOSAL_01"; 11 | } 12 | 13 | function description() public pure override returns (string memory) { 14 | return "Mock multisig proposal 01"; 15 | } 16 | 17 | function run() public override { 18 | super.run(); 19 | } 20 | 21 | function deploy() public override { 22 | if (!addresses.isAddressSet("TOKEN")) { 23 | MockToken mockERC20 = new MockToken("MOCK_TOKEN", "MTOKEN"); 24 | 25 | mockERC20.mint( 26 | addresses.getAddress("PROTOCOL_MULTISIG"), 1000 ether 27 | ); 28 | 29 | addresses.addAddress("TOKEN", address(mockERC20), true); 30 | } 31 | } 32 | 33 | function build() 34 | public 35 | override 36 | buildModifier(addresses.getAddress("PROTOCOL_MULTISIG")) 37 | { 38 | MockToken mockToken = MockToken(addresses.getAddress("TOKEN")); 39 | address deployer = addresses.getAddress("DEPLOYER_EOA"); 40 | 41 | // Actions 42 | mockToken.approve(deployer, 200); 43 | mockToken.transfer(deployer, 500); 44 | mockToken.transfer(deployer, 100); 45 | mockToken.approve(addresses.getAddress("PROTOCOL_MULTISIG"), 200); 46 | mockToken.transferFrom( 47 | addresses.getAddress("PROTOCOL_MULTISIG"), deployer, 200 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/multisigProposals/MultisigProposal_02.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {MockToken} from "mocks/MockToken.sol"; 4 | 5 | import {MockTokenWrapper} from "mocks/MockTokenWrapper.sol"; 6 | 7 | import {MultisigProposal} from "@proposals/MultisigProposal.sol"; 8 | 9 | contract MultisigProposal_02 is MultisigProposal { 10 | function name() public pure override returns (string memory) { 11 | return "MOCK_MULTISIG_PROPOSAL_02"; 12 | } 13 | 14 | function description() public pure override returns (string memory) { 15 | return "Mock multisig proposal 02"; 16 | } 17 | 18 | function run() public override { 19 | super.run(); 20 | } 21 | 22 | function deploy() public override { 23 | address multisig = addresses.getAddress("PROTOCOL_MULTISIG"); 24 | 25 | // mint 100 eth to multisig contract 26 | vm.deal(multisig, 100 ether); 27 | 28 | MockToken token = MockToken(addresses.getAddress("TOKEN")); 29 | 30 | MockTokenWrapper tokenWrapper = 31 | new MockTokenWrapper(addresses.getAddress("TOKEN")); 32 | 33 | token.mint(addresses.getAddress("DEPLOYER_EOA"), 1000 ether); 34 | 35 | // transfer 100 tokens to token wrapper contract 36 | token.transfer(address(tokenWrapper), 100 ether); 37 | 38 | // add TOKEN_WRAPPER address 39 | addresses.addAddress("TOKEN_WRAPPER", address(tokenWrapper), true); 40 | } 41 | 42 | function build() 43 | public 44 | override 45 | buildModifier(addresses.getAddress("PROTOCOL_MULTISIG")) 46 | { 47 | MockTokenWrapper tokenWrapper = 48 | MockTokenWrapper(addresses.getAddress("TOKEN_WRAPPER")); 49 | 50 | // actions 51 | MockToken(addresses.getAddress("TOKEN")).approve( 52 | address(tokenWrapper), 60 ether 53 | ); 54 | tokenWrapper.mint{value: 10 ether}(); 55 | tokenWrapper.redeemTokens(10 ether); 56 | tokenWrapper.mint{value: 20 ether}(); 57 | tokenWrapper.mint{value: 30 ether}(); 58 | tokenWrapper.redeemTokens(50 ether); 59 | tokenWrapper.mint{value: 40 ether}(); 60 | tokenWrapper.mint{value: 50 ether}(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/multisigProposals/MultisigProposal_03.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "forge-std/mocks/MockERC20.sol"; 4 | 5 | import {MockVotingContract} from "mocks/MockVotingContract.sol"; 6 | 7 | import {MultisigProposal} from "@proposals/MultisigProposal.sol"; 8 | 9 | contract MultisigProposal_03 is MultisigProposal { 10 | function name() public pure override returns (string memory) { 11 | return "MOCK_MULTISIG_PROPOSAL_03"; 12 | } 13 | 14 | function description() public pure override returns (string memory) { 15 | return "Mock multisig proposal 03"; 16 | } 17 | 18 | function run() public override { 19 | super.run(); 20 | } 21 | 22 | function deploy() public override { 23 | string[] memory candidates = new string[](10); 24 | candidates[0] = "candidate0"; 25 | candidates[1] = "candidate1"; 26 | candidates[2] = "candidate2"; 27 | candidates[3] = "candidate3"; 28 | candidates[4] = "candidate4"; 29 | candidates[5] = "candidate5"; 30 | candidates[6] = "candidate6"; 31 | candidates[7] = "candidate7"; 32 | candidates[8] = "candidate8"; 33 | candidates[9] = "candidate9"; 34 | MockVotingContract votingContract = new MockVotingContract(candidates); 35 | 36 | // add Voting contract address 37 | addresses.addAddress("VOTING_CONTRACT", address(votingContract), true); 38 | } 39 | 40 | function build() 41 | public 42 | override 43 | buildModifier(addresses.getAddress("PROTOCOL_MULTISIG")) 44 | { 45 | MockVotingContract votingContract = 46 | MockVotingContract(addresses.getAddress("VOTING_CONTRACT")); 47 | 48 | // actions 49 | votingContract.vote("candidate0"); 50 | votingContract.vote("candidate2"); 51 | votingContract.vote("candidate4"); 52 | votingContract.vote("candidate7"); 53 | votingContract.vote("candidate9"); 54 | votingContract.vote("candidate5"); 55 | votingContract.vote("candidate8"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/multisigProposals/MultisigProposal_04.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "forge-std/mocks/MockERC20.sol"; 4 | 5 | import {MockAuction} from "mocks/MockAuction.sol"; 6 | 7 | import {MultisigProposal} from "@proposals/MultisigProposal.sol"; 8 | 9 | contract MultisigProposal_04 is MultisigProposal { 10 | function name() public pure override returns (string memory) { 11 | return "MOCK_MULTISIG_PROPOSAL_04"; 12 | } 13 | 14 | function description() public pure override returns (string memory) { 15 | return "Mock multisig proposal 04"; 16 | } 17 | 18 | function run() public override { 19 | super.run(); 20 | } 21 | 22 | function deploy() public override { 23 | MockAuction auctionContract = new MockAuction(); 24 | 25 | // mint 100 eth to multisig contract 26 | vm.deal(addresses.getAddress("PROTOCOL_MULTISIG"), 1000 ether); 27 | 28 | // add Voting contract address 29 | addresses.addAddress("AUCTION_CONTRACT", address(auctionContract), true); 30 | } 31 | 32 | function build() 33 | public 34 | override 35 | buildModifier(addresses.getAddress("PROTOCOL_MULTISIG")) 36 | { 37 | MockAuction auctionContract = 38 | MockAuction(addresses.getAddress("AUCTION_CONTRACT")); 39 | 40 | // actions 41 | auctionContract.bid{value: 10 ether}(); 42 | auctionContract.bid{value: 40 ether}(); 43 | auctionContract.bid{value: 50 ether}(); 44 | auctionContract.bid{value: 90 ether}(); 45 | auctionContract.bid{value: 100 ether}(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/multisigProposals/MultisigProposal_05.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "forge-std/mocks/MockERC20.sol"; 4 | 5 | import {MockSavingContract} from "mocks/MockSavingContract.sol"; 6 | 7 | import {MultisigProposal} from "@proposals/MultisigProposal.sol"; 8 | 9 | contract MultisigProposal_05 is MultisigProposal { 10 | function name() public pure override returns (string memory) { 11 | return "MOCK_MULTISIG_PROPOSAL_05"; 12 | } 13 | 14 | function description() public pure override returns (string memory) { 15 | return "Mock multisig proposal 05"; 16 | } 17 | 18 | function run() public override { 19 | super.run(); 20 | } 21 | 22 | function deploy() public override { 23 | MockSavingContract savingContract = new MockSavingContract(); 24 | 25 | // mint 100 eth to multisig contract 26 | vm.deal(addresses.getAddress("PROTOCOL_MULTISIG"), 1000 ether); 27 | 28 | // add Voting contract address 29 | addresses.addAddress("SAVING_CONTRACT", address(savingContract), true); 30 | } 31 | 32 | function build() 33 | public 34 | override 35 | buildModifier(addresses.getAddress("PROTOCOL_MULTISIG")) 36 | { 37 | MockSavingContract savingContract = 38 | MockSavingContract(addresses.getAddress("SAVING_CONTRACT")); 39 | 40 | // actions 41 | savingContract.deposit{value: 20 ether}(0); 42 | savingContract.deposit{value: 40 ether}(20 days); 43 | savingContract.withdraw(0); 44 | savingContract.deposit{value: 60 ether}(0); 45 | savingContract.deposit{value: 80 ether}(60 days); 46 | savingContract.deposit{value: 100 ether}(90 days); 47 | savingContract.withdraw(2); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/utils/duplicate-addresses-different-name/31337.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 4 | "name": "DEPLOYER_EOA", 5 | "isContract": false 6 | }, 7 | { 8 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 9 | "name": "DEPLOYER_EOA2", 10 | "isContract": false 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /test/utils/duplicate-addresses/31337.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 4 | "name": "DEPLOYER_EOA", 5 | "isContract": false 6 | }, 7 | { 8 | "addr": "0x9679E26bf0C470521DE83Ad77BB1bf1e7312f739", 9 | "name": "DEPLOYER_EOA", 10 | "isContract": false 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /utils/Address.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Collection of functions related to the address type 7 | */ 8 | library Address { 9 | /** 10 | * @dev The ETH balance of the account is not enough to perform the operation. 11 | */ 12 | error AddressInsufficientBalance(address account); 13 | 14 | /** 15 | * @dev There's no code at `target` (it is not a contract). 16 | */ 17 | error AddressEmptyCode(address target); 18 | 19 | /** 20 | * @dev A call to an address target failed. The target may have reverted. 21 | */ 22 | error FailedInnerCall(); 23 | 24 | /** 25 | * @dev Replacement for Solidity's `transfer`: sends `amount` wei to 26 | * `recipient`, forwarding all available gas and reverting on errors. 27 | * 28 | * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost 29 | * of certain opcodes, possibly making contracts go over the 2300 gas limit 30 | * imposed by `transfer`, making them unable to receive funds via 31 | * `transfer`. {sendValue} removes this limitation. 32 | * 33 | * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. 34 | * 35 | * IMPORTANT: because control is transferred to `recipient`, care must be 36 | * taken to not create reentrancy vulnerabilities. Consider using 37 | * {ReentrancyGuard} or the 38 | * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. 39 | */ 40 | function sendValue(address payable recipient, uint256 amount) internal { 41 | if (address(this).balance < amount) { 42 | revert AddressInsufficientBalance(address(this)); 43 | } 44 | 45 | (bool success, ) = recipient.call{value: amount}(""); 46 | if (!success) { 47 | revert FailedInnerCall(); 48 | } 49 | } 50 | 51 | /** 52 | * @dev Performs a Solidity function call using a low level `call`. A 53 | * plain `call` is an unsafe replacement for a function call: use this 54 | * function instead. 55 | * 56 | * If `target` reverts with a revert reason or custom error, it is bubbled 57 | * up by this function (like regular Solidity function calls). However, if 58 | * the call reverted with no returned reason, this function reverts with a 59 | * {FailedInnerCall} error. 60 | * 61 | * Returns the raw returned data. To convert to the expected return value, 62 | * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. 63 | * 64 | * Requirements: 65 | * 66 | * - `target` must be a contract. 67 | * - calling `target` with `data` must not revert. 68 | */ 69 | function functionCall( 70 | address target, 71 | bytes memory data 72 | ) internal returns (bytes memory) { 73 | return functionCallWithValue(target, data, 0); 74 | } 75 | 76 | /** 77 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], 78 | * but also transferring `value` wei to `target`. 79 | * 80 | * Requirements: 81 | * 82 | * - the calling contract must have an ETH balance of at least `value`. 83 | * - the called Solidity function must be `payable`. 84 | */ 85 | function functionCallWithValue( 86 | address target, 87 | bytes memory data, 88 | uint256 value 89 | ) internal returns (bytes memory) { 90 | if (address(this).balance < value) { 91 | revert AddressInsufficientBalance(address(this)); 92 | } 93 | (bool success, bytes memory returndata) = target.call{value: value}( 94 | data 95 | ); 96 | return verifyCallResultFromTarget(target, success, returndata); 97 | } 98 | 99 | /** 100 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], 101 | * but performing a static call. 102 | */ 103 | function functionStaticCall( 104 | address target, 105 | bytes memory data 106 | ) internal view returns (bytes memory) { 107 | (bool success, bytes memory returndata) = target.staticcall(data); 108 | return verifyCallResultFromTarget(target, success, returndata); 109 | } 110 | 111 | /** 112 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], 113 | * but performing a delegate call. 114 | */ 115 | function functionDelegateCall( 116 | address target, 117 | bytes memory data 118 | ) internal returns (bytes memory) { 119 | (bool success, bytes memory returndata) = target.delegatecall(data); 120 | return verifyCallResultFromTarget(target, success, returndata); 121 | } 122 | 123 | /** 124 | * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target 125 | * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an 126 | * unsuccessful call. 127 | */ 128 | function verifyCallResultFromTarget( 129 | address target, 130 | bool success, 131 | bytes memory returndata 132 | ) internal view returns (bytes memory) { 133 | if (!success) { 134 | _revert(returndata); 135 | } else { 136 | // only check if target is a contract if the call was successful and the return data is empty 137 | // otherwise we already know that it was a contract 138 | if (returndata.length == 0 && target.code.length == 0) { 139 | revert AddressEmptyCode(target); 140 | } 141 | return returndata; 142 | } 143 | } 144 | 145 | /** 146 | * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the 147 | * revert reason or with a default {FailedInnerCall} error. 148 | */ 149 | function verifyCallResult( 150 | bool success, 151 | bytes memory returndata 152 | ) internal pure returns (bytes memory) { 153 | if (!success) { 154 | _revert(returndata); 155 | } else { 156 | return returndata; 157 | } 158 | } 159 | 160 | /** 161 | * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}. 162 | */ 163 | function _revert(bytes memory returndata) private pure { 164 | // Look for revert reason and bubble it up if present 165 | if (returndata.length > 0) { 166 | // The easiest way to bubble the revert reason is using memory via assembly 167 | /// @solidity memory-safe-assembly 168 | assembly { 169 | let returndata_size := mload(returndata) 170 | revert(add(32, returndata), returndata_size) 171 | } 172 | } else { 173 | revert FailedInnerCall(); 174 | } 175 | } 176 | 177 | function getContractHash(address a) internal view returns (bytes32 hash) { 178 | assembly { 179 | hash := extcodehash(a) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /utils/Constants.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | library Constants { 4 | bytes public constant MULTICALL_BYTECODE = 5 | hex"6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c0033"; 6 | 7 | bytes public constant SAFE_BYTECODE = 8 | hex"608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea2646970667358221220d1429297349653a4918076d650332de1a1068c5f3e07c5c82360c277770b955264736f6c63430007060033"; 9 | } 10 | --------------------------------------------------------------------------------