diff --git a/.github/workflows/artifacts-publish.yml b/.github/workflows/artifacts-publish.yml deleted file mode 100644 index 2fa9355..0000000 --- a/.github/workflows/artifacts-publish.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Publish Artifacts - -on: - pull_request: - types: [closed] - branches: - - release-v* - -env: - FOUNDRY_PROFILE: ci - -jobs: - publish-artifacts-to-npm: - runs-on: ubuntu-latest - permissions: - contents: write - packages: write - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - submodules: recursive - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - registry-url: 'https://registry.npmjs.org/' - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: stable - - - name: Configure Git for Foundry - run: | - git config --global user.name "github-actions" - git config --global user.email "github-actions@github.com" - - - name: Configure NPM for Scoped Package - run: | - cd npm-artifacts - SCOPE=$(jq -r '.name' package.json | cut -d'/' -f1) - echo "$SCOPE:registry=https://registry.npmjs.org/" > ~/.npmrc - echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc - - - name: Get Version from package.json - id: get_version - run: | - cd npm-artifacts - VERSION=$(jq -r '.version' package.json) - TAG_VERSION="v$VERSION" - echo "VERSION=$TAG_VERSION" >> $GITHUB_ENV - - - name: Create Git Tag - run: | - git config --global user.name "github-actions" - git config --global user.email "github-actions@github.com" - git tag $VERSION - git push origin $VERSION - - - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Install dependencies - run: cd npm-artifacts && pnpm install --ignore-scripts - - - name: Build Package - env: - ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} - run: | - cd npm-artifacts - pnpm run prepare-abi - pnpm run build - - - name: Publish to NPM - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - cd npm-artifacts - if [[ "$VERSION" == *"-alpha"* ]]; then - npm publish --tag alpha --access public - else - npm publish --tag latest --access public - fi diff --git a/.github/workflows/check-artifacts.yml b/.github/workflows/check-artifacts.yml new file mode 100644 index 0000000..e175eb9 --- /dev/null +++ b/.github/workflows/check-artifacts.yml @@ -0,0 +1,48 @@ +name: Generated artifacts in sync + +# Validates that auto-generated content committed to the repo is in sync +# with its sources: +# npm-artifacts/src/abi.ts ← extracted ABIs (shipped via @aragon/staged-proposal-processor-plugin-artifacts) +# +# If a PR changes a contract but forgets to regenerate the ABI, CI fails with +# a clear message telling the contributor what to run. + +on: + push: + branches: + - main + - release-v* + pull_request: + paths: + - 'src/**' + - 'foundry.toml' + - 'remappings.txt' + - 'npm-artifacts/prepare-abi.sh' + - 'npm-artifacts/src/abi.ts' + +jobs: + check: + name: Generated artifacts + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + persist-credentials: false + + - uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + - uses: extractions/setup-just@v2 + - run: sudo apt-get update && sudo apt-get install -y jq + + - name: Verify ABI is in sync with contracts + run: | + (cd npm-artifacts && just abi) + if ! git diff --exit-code -- npm-artifacts/src/abi.ts; then + echo "::error::npm-artifacts/src/abi.ts is out of date." + echo "::error::Run 'just abi' from npm-artifacts/ locally and commit the diff." + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5bfef5f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: Release + +# Releases @aragon/staged-proposal-processor-plugin-artifacts. +# +# Flow: +# 1. A PR bumps `npm-artifacts/package.json#version`. +# 2. On merge to main or any release-v* branch, this workflow runs. +# 3. If the version is new (no tag `vX.Y.Z` yet), it builds, tags, pushes the +# tag, and publishes to NPM. Otherwise it exits cleanly. + +on: + push: + branches: + - main + - release-v* + paths: ['npm-artifacts/package.json'] + +permissions: + contents: write # to push the tag + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-tags: true # need existing tags for the version-already-released check + submodules: recursive + persist-credentials: true # needed to push the tag + + - uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + - uses: extractions/setup-just@v2 + - uses: oven-sh/setup-bun@v2 + - run: sudo apt-get update && sudo apt-get install -y jq + + - name: Resolve version + skip flag + id: meta + run: | + set -euo pipefail + version=$(jq -r '.version' npm-artifacts/package.json) + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9]+(\.[0-9]+)?)?$ ]] || { echo "::error::Bad semver in package.json: $version"; exit 1; } + tag="v$version" + + if git rev-parse "$tag" >/dev/null 2>&1; then + echo "Tag $tag already exists — version is unchanged or was previously released. Skipping." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Build + if: steps.meta.outputs.skip != 'true' + run: cd npm-artifacts && just build + + - name: Tag + if: steps.meta.outputs.skip != 'true' + run: | + git tag "${{ steps.meta.outputs.tag }}" + git push origin "${{ steps.meta.outputs.tag }}" + + - name: Publish to NPM + if: steps.meta.outputs.skip != 'true' + env: + NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} + run: cd npm-artifacts && bun publish --access public diff --git a/.gitignore b/.gitignore index 3a8eb31..588d3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ docs-gen/bun.lock .DS_Store src/.DS_Store -npm-artifacts/src/abi.ts npm-artifacts/dist +npm-artifacts/node_modules report/ lcov.info diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e10081..cde9424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v1.2 + +### Changed + +- Recompiled against an amended `osx-commons` `RuledCondition._evalLogic`. The IF_ELSE starting rule now evaluates with `_where`/`_who` in the same order as the surrounding `_evalRule` call. No setup interface change. + +### Added + +- SPP-level regression tests for `SPPRuleCondition.isGranted` covering an asymmetric IF_ELSE predicate (success/failure routing and a swapped-args caller path). +- Unit and fork tests for `prepareUpdate`. +- `script/NewVersion.s.sol` now also prints the management DAO multisig `createProposal` calldata wrapping the `createVersion` action — including the pinned `PROPOSAL_METADATA` URI as the proposal metadata — so a multisig member can submit it directly. +- `script/Deploy.s.sol` now publishes `PlaceholderSetup` builds for builds 1..VERSION_BUILD-1 on a fresh repo before publishing the real `SPPSetup` build, keeping on-chain build numbers aligned across networks. +- `PROPOSAL_METADATA` and `PLACEHOLDER_BUILD_METADATA` constants in `PluginSettings.sol`, and `script/new-version-proposal-metadata.json` as the v1.2 proposal metadata source. + ## v1.1 ### Added diff --git a/README.md b/README.md index e483f6b..f0137e6 100644 --- a/README.md +++ b/README.md @@ -40,18 +40,40 @@ forge build ```shell just test # unit tests just test-fork # fork tests (requires RPC_URL) -just validate-upgrade SPPStorageV1 StagedProposalProcessor # storage layout check +just check-upgrade SPPStorageV1 StagedProposalProcessor # storage layout compatibility check ``` ## Deploy ```shell just deploy # initial deployment (creates plugin repo, publishes v1) -just new-version # deploy new setup + print DAO proposal calldata +just new-version # deploy new setup + print management DAO multisig proposal calldata ``` Set `SPP_ENS_SUBDOMAIN=spp` in `.env` for production deployments. Omitting it generates a unique name (`spp-`), which is useful for testing. +### Publishing a new build + +1. Bump `VERSION_BUILD` in `src/utils/PluginSettings.sol`. +2. Edit `src/build-metadata.json` and `script/new-version-proposal-metadata.json` for this build (and `src/release-metadata.json` if shipping a new release). +3. Pin and update the matching constants in `PluginSettings.sol`: + ```shell + just ipfs-pin src/build-metadata.json # → BUILD_METADATA + just ipfs-pin script/new-version-proposal-metadata.json # → PROPOSAL_METADATA + just ipfs-pin src/release-metadata.json # → RELEASE_METADATA (only on a new release) + ``` +4. Run `just new-version`. The script deploys the new `SPPSetup` and prints two calldata blobs: + - the inner `createVersion` action (`to = SPP_PLUGIN_REPO_ADDRESS`), and + - the outer management DAO multisig `createProposal` call (`to = MANAGEMENT_DAO_MULTISIG_ADDRESS`) including the pinned `PROPOSAL_METADATA` URI — submit it from any listed multisig member to publish the version. + +On a brand-new network, `just deploy` automatically publishes `PlaceholderSetup` builds for any build numbers below `VERSION_BUILD` before publishing the real one, so build numbers stay aligned with networks where prior builds shipped. + +### Upgrading existing installations + +Publishing a new build does not upgrade installed plugins. Each DAO running an older build needs a proposal that calls `psp.applyUpdate(...)`. + +Version 1.2 is published with the same `IMPLEMENTATION` as 1.1 (bytecode is identical), so `applyUpdate` skips the proxy upgrade — no `UPGRADE_PLUGIN_PERMISSION` grant/revoke bracket is required. + ### Deployment Checklist - [ ] I have cloned the official repository on my computer and I have checked out the `main` branch @@ -73,6 +95,7 @@ Set `SPP_ENS_SUBDOMAIN=spp` in `.env` for production deployments. Omitting it ge - [ ] I have created a new burner wallet with `cast wallet new` and used its private key as `DEPLOYER_KEY` - [ ] I am the only person of the ceremony that will operate the deployment wallet - [ ] All the tests run clean (`just test`) +- [ ] `just check-upgrade OldContract NewContract` reports the storage layout check passed - My computer: - [ ] Is running in a safe location and using a trusted network - [ ] It exposes no services or ports diff --git a/justfile b/justfile index 654fdbb..e02be9a 100644 --- a/justfile +++ b/justfile @@ -8,16 +8,22 @@ docs: cd docs-gen && bun install && bash prepare-docs.sh && bun prepare-docs.js DEPLOY_SCRIPT := "script/Deploy.s.sol:Deploy" +NEW_VERSION_SCRIPT := "script/NewVersion.s.sol:NewVersion" + +# Dry-run the new-version script (no broadcast) — eyeball the printed multisig calldata +[group('upgrade')] +pre-new-version: + just dry-run {{ NEW_VERSION_SCRIPT }} # Publish a new SPP plugin version (deploys setup, prints DAO proposal calldata) [group('upgrade')] new-version *args: #!/usr/bin/env bash set -euo pipefail - source {{ ENV_RESOLVE_LIB }} && env_load_network + source {{ JUST_LIB }} && env_load_network mkdir -p logs LOG_FILE="logs/new-version-$NETWORK_NAME-$(date +"%y-%m-%d-%H-%M").log" just test 2>&1 | tee -a "$LOG_FILE" - just run script/NewVersion.s.sol:NewVersion {{ args }} 2>&1 | tee -a "$LOG_FILE" + just run {{ NEW_VERSION_SCRIPT }} {{ args }} 2>&1 | tee -a "$LOG_FILE" echo "Logs saved in $LOG_FILE" diff --git a/lib/just-foundry b/lib/just-foundry index 16ea7c2..59d13cc 160000 --- a/lib/just-foundry +++ b/lib/just-foundry @@ -1 +1 @@ -Subproject commit 16ea7c2817d04504731b09d36d967e471ebe026b +Subproject commit 59d13ccf7fbd0ab49764e154d85ac1bb12b1b43e diff --git a/lib/osx-commons b/lib/osx-commons index 98c2b96..7a6dc58 160000 --- a/lib/osx-commons +++ b/lib/osx-commons @@ -1 +1 @@ -Subproject commit 98c2b9672d07377e4b429595d21c1803c19581db +Subproject commit 7a6dc5839772e3b1de3c550fa2f405e11c6dd22b diff --git a/npm-artifacts/README.md b/npm-artifacts/README.md index a135933..df2a8f7 100644 --- a/npm-artifacts/README.md +++ b/npm-artifacts/README.md @@ -1,9 +1,12 @@ # Staged Proposal Processor plugin artifacts -This package contains the ABI of the Staged Proposal Processor plugin for OSx, as well as the address of its plugin repository on each supported network. Install it with: +This package contains the ABI definitions of the Staged Proposal Processor (SPP) plugin, as well as the address of its `PluginRepo` deployed on each network. + +Install it with: ```sh -pnpm install @aragon/staged-proposal-processor-plugin-artifacts +bun add @aragon/staged-proposal-processor-plugin-artifacts +# or: pnpm add @aragon/staged-proposal-processor-plugin-artifacts ``` ## Usage @@ -11,65 +14,71 @@ pnpm install @aragon/staged-proposal-processor-plugin-artifacts ```typescript // ABI definitions import { - StagedProposalProcessor, - StagedProposalProcessorSetup + StagedProposalProcessorABI, + StagedProposalProcessorSetupABI, + SPPRuleConditionABI } from "@aragon/staged-proposal-processor-plugin-artifacts"; +console.log("SPP ABI", StagedProposalProcessorABI); + // Plugin Repository addresses per-network import { addresses } from "@aragon/staged-proposal-processor-plugin-artifacts"; + +console.log(addresses.pluginRepo.mainnet); ``` -You can also open [addresses.json](https://github.com/aragon/staged-proposal-processor-plugin/blob/main/npm-artifacts/src/addresses.json) directly. +You can also open [addresses.json](./src/addresses.json) directly. ## Development -### Building the package +This package is built with [`just`](https://github.com/casey/just) and [`bun`](https://bun.sh). -Install the dependencies and generate the local ABI definitions. +### Refresh ABIs ```sh -pnpm install --ignore-scripts -pnpm run build +just abi # regenerate src/abi.ts from forge build artifacts at the repo root ``` -The `build` script will: -1. Move to `src`. -2. Install its dependencies. -3. Compile the contracts using Hardhat. -4. Generate their ABI. -5. Extract their ABI and embed it into on `npm/src/abi.ts`. +`src/abi.ts` is populated by `bash prepare-abi.sh`, which runs `forge build` at the repo root and emits one `export const ABI = [...] as const` per `src/` contract with a non-empty ABI. Bytecode is not emitted — use the ABI const + the address from `addresses.json` directly. -### Syncing the deployment addresses +### Sync addresses -Clone [OSx Commons](https://github.com/aragon/osx-commons) in a folder next to this repo. +`src/addresses.json` is the source of truth for the SPP `PluginRepo` address on each chain. The sync recipe overlays the latest deployment artifacts from a peer-directory clone of `aragon/protocol-factory` onto the existing JSON without dropping any networks already listed: ```sh -# cd npm-artifacts -pnpm run sync-addresses +just sync-addresses # ../../protocol-factory/artifacts/addresses--.json → src/addresses.json ``` -### Publishing +If a network has multiple `addresses--.json` files, the highest timestamp wins. Networks not present in `protocol-factory/artifacts/` are preserved. Same-network entries are overwritten with the freshest address. Output keys are sorted alphabetically. + +If you don't have `protocol-factory` checked out, edit `src/addresses.json` directly in your PR. -- Access the repo's GitHub Actions panel -- Click on "Publish Artifacts" -- Select the corresponding `release-v*` branch as the source +### Build + +```sh +just build +``` -This action will: -- Create a git tag like `v1.2`, following [package.json](./package.json)'s version field -- Publish the package to NPM +Regenerates `src/abi.ts`, installs dependencies via `bun`, then runs `tsc` to produce `dist/`. + +### Releasing + +Releases are PR-driven. Tag creation and NPM publishing are handled exclusively by CI — there is no manual release flow. + +1. Open a PR that bumps `version` in [`package.json`](./package.json). +2. Update [`CHANGELOG.md`](./CHANGELOG.md) in the same PR if relevant. If addresses changed, patch [`src/addresses.json`](./src/addresses.json). If contracts changed, regenerate `src/abi.ts` (`just abi`). +3. After review and merge to `main` or any `release-v*` branch, [`.github/workflows/release.yml`](../.github/workflows/release.yml) detects the new version, creates the `vX.Y.Z` tag, and runs `bun publish`. + +If the merged version already has a tag (e.g. an unrelated edit to `package.json`), the workflow exits cleanly without releasing. ## Documentation -You can find all documentation regarding how to use this plugin in [Aragon's documentation here](https://docs.aragon.org/spp/1.x/index.html). +[Aragon's developer portal](https://docs.aragon.org). ## Contributing -If you like what we're doing and would love to support, please review our `CONTRIBUTING_GUIDE.md` [here](https://github.com/aragon/staged-proposal-processor-plugin/blob/main/CONTRIBUTIONS.md). We'd love to build with you. +See [`CONTRIBUTIONS.md`](../CONTRIBUTIONS.md) in the main repository. ## Security -If you believe you've found a security issue, we encourage you to notify us. We welcome working with you to resolve the issue promptly. - -Security Contact Email: sirt@aragon.org - -Please do not use the issue tracker for security issues. +If you believe you've found a security issue, please email **sirt@aragon.org**. Don't use the public issue tracker. diff --git a/npm-artifacts/bun.lock b/npm-artifacts/bun.lock new file mode 100644 index 0000000..b857c7e --- /dev/null +++ b/npm-artifacts/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@aragon/staged-proposal-processor-plugin-artifacts", + "devDependencies": { + "typescript": "^5.7.3", + }, + }, + }, + "packages": { + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + } +} diff --git a/npm-artifacts/justfile b/npm-artifacts/justfile new file mode 100644 index 0000000..d19b36a --- /dev/null +++ b/npm-artifacts/justfile @@ -0,0 +1,23 @@ +default: help + +# Regenerate src/abi.ts from forge build artifacts (at the repo root). +[group('source')] +abi: + bash prepare-abi.sh + +# Regenerate src/addresses.json from ../protocol-factory artifacts (latest deployment per network). +[group('source')] +sync-addresses: + bash sync-addresses.sh ../../protocol-factory/artifacts ./src/addresses.json + +# Regenerate src/abi.ts, install deps, compile TS → dist/. Run before `bun publish`. +# Requires `bun` (https://bun.sh) — npm and yarn are not recommended. +[group('build')] +build: abi + rm -rf dist + bun install --frozen-lockfile + bun x tsc -p tsconfig.json + +# List available recipes. +help: + @just --list diff --git a/npm-artifacts/package.json b/npm-artifacts/package.json index 1864769..2d26ee0 100644 --- a/npm-artifacts/package.json +++ b/npm-artifacts/package.json @@ -18,12 +18,8 @@ "publishConfig": { "access": "public" }, - "scripts": { - "build": "pnpm run prepare-abi && rm -Rf dist && ./node_modules/.bin/tsc -p tsconfig.json", - "prepare-abi": "bash ./prepare-abi.sh", - "sync-addresses": "bash ./sync-addresses.sh ../../osx-commons/configs/src/deployments/json ./src/addresses.json" - }, + "scripts": {}, "devDependencies": { - "typescript": "^5.5.4" + "typescript": "^5.7.3" } } diff --git a/npm-artifacts/pnpm-lock.yaml b/npm-artifacts/pnpm-lock.yaml deleted file mode 100644 index 63eecf9..0000000 --- a/npm-artifacts/pnpm-lock.yaml +++ /dev/null @@ -1,24 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - typescript: - specifier: ^5.5.4 - version: 5.9.3 - -packages: - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - -snapshots: - - typescript@5.9.3: {} diff --git a/npm-artifacts/prepare-abi.sh b/npm-artifacts/prepare-abi.sh index 07dcd10..ea2138f 100644 --- a/npm-artifacts/prepare-abi.sh +++ b/npm-artifacts/prepare-abi.sh @@ -1,32 +1,48 @@ #!/usr/bin/env bash - -# Exit on error -set -e - -# Constants -CONTRACTS_FOLDER="../src" -BUILD_OUT_FOLDER="../out" -TARGET_ABI_FILE="./src/abi.ts" - -# Build contracts (script runs from npm-artifacts/, so root is one level up) -(cd .. && forge build) - -# Wipe the destination file -echo "// NOTE: Do not edit this file. It is generated automatically." > $TARGET_ABI_FILE - -# Extract the abi field and create a TS file -for SRC_CONTRACT_FILE in $(ls $CONTRACTS_FOLDER/*.sol ) -do - SRC_FILE_NAME=$(basename $(echo $SRC_CONTRACT_FILE)) - CONTRACT_NAME=${SRC_FILE_NAME%".sol"} - SRC_FILE_PATH=$BUILD_OUT_FOLDER/$SRC_FILE_NAME/${SRC_FILE_NAME%".sol"}.json - - ABI=$(bun -p "JSON.stringify(JSON.parse(fs.readFileSync(\"$SRC_FILE_PATH\").toString()).abi)") - - echo "const ${CONTRACT_NAME}ABI = $ABI as const;" >> $TARGET_ABI_FILE - echo "export {${CONTRACT_NAME}ABI};" >> $TARGET_ABI_FILE - - echo "" >> $TARGET_ABI_FILE +# Generate src/abi.ts from forge build artifacts at the repo root. +# One consolidated TS file, one `export const ABI = [...] as const;` +# entry per contract under `src/`. Bytecode is intentionally not emitted — +# consumers should import the ABI const directly and rely on deployed bytecode. + +set -euo pipefail + +# Pin sort order to byte-wise C collation so the generated file is identical +export LC_ALL=C + +cd "$(dirname "$0")" +REPO_ROOT="$(cd .. && pwd)" +TARGET="src/abi.ts" + +# Build at the repo root — uses the project's foundry.toml and remappings. +(cd "$REPO_ROOT" && forge build --quiet) + +{ + echo "// Auto-generated by: bash prepare-abi.sh" + echo "// Do not edit manually." + echo "" +} > "$TARGET" + +count=0 +skipped=0 +for sol in $(find "$REPO_ROOT/src" -type f -name "*.sol" | sort); do + file=$(basename "$sol") + contract=$(basename "$sol" .sol) + artifact="$REPO_ROOT/out/$file/$contract.json" + [[ -f "$artifact" ]] || continue + + abi=$(jq -c '.abi' "$artifact") + # Skip files with no public surface (free-function files, libraries with + # only internal/pure helpers, etc.). An empty ABI is useless to consumers. + if [[ "$abi" == "[]" ]]; then + skipped=$((skipped + 1)) + continue + fi + + { + echo "export const ${contract}ABI = $abi as const;" + echo "" + } >> "$TARGET" + count=$((count + 1)) done -echo "ABI prepared: $TARGET_ABI_FILE" +echo "Exported $count ABIs to $TARGET (skipped $skipped with empty ABI)" diff --git a/npm-artifacts/src/abi.ts b/npm-artifacts/src/abi.ts new file mode 100644 index 0000000..bd887dc --- /dev/null +++ b/npm-artifacts/src/abi.ts @@ -0,0 +1,13 @@ +// Auto-generated by: bash prepare-abi.sh +// Do not edit manually. + +export const StagedProposalProcessorABI = [{"type":"function","name":"SET_METADATA_PERMISSION_ID","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SET_TARGET_CONFIG_PERMISSION_ID","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"UPGRADE_PLUGIN_PERMISSION_ID","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"advanceProposal","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"canExecute","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"canProposalAdvance","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"cancel","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createProposal","inputs":[{"name":"_metadata","type":"bytes","internalType":"bytes"},{"name":"_actions","type":"tuple[]","internalType":"struct Action[]","components":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}]},{"name":"_allowFailureMap","type":"uint128","internalType":"uint128"},{"name":"_startDate","type":"uint64","internalType":"uint64"},{"name":"_proposalParams","type":"bytes[][]","internalType":"bytes[][]"}],"outputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"createProposal","inputs":[{"name":"_metadata","type":"bytes","internalType":"bytes"},{"name":"_actions","type":"tuple[]","internalType":"struct Action[]","components":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}]},{"name":"_startDate","type":"uint64","internalType":"uint64"},{"name":"","type":"uint64","internalType":"uint64"},{"name":"_data","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"customProposalParamsABI","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"pure"},{"type":"function","name":"dao","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IDAO"}],"stateMutability":"view"},{"type":"function","name":"edit","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"},{"name":"_metadata","type":"bytes","internalType":"bytes"},{"name":"_actions","type":"tuple[]","internalType":"struct Action[]","components":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"execute","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getBodyProposalId","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"},{"name":"_stageId","type":"uint16","internalType":"uint16"},{"name":"_body","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBodyResult","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"},{"name":"_stageId","type":"uint16","internalType":"uint16"},{"name":"_body","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint8","internalType":"enum StagedProposalProcessor.ResultType"}],"stateMutability":"view"},{"type":"function","name":"getCreateProposalParams","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"},{"name":"_stageId","type":"uint16","internalType":"uint16"},{"name":"_index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getCurrentConfigIndex","inputs":[],"outputs":[{"name":"","type":"uint16","internalType":"uint16"}],"stateMutability":"view"},{"type":"function","name":"getCurrentTargetConfig","inputs":[],"outputs":[{"name":"","type":"tuple","internalType":"struct IPlugin.TargetConfig","components":[{"name":"target","type":"address","internalType":"address"},{"name":"operation","type":"uint8","internalType":"enum IPlugin.Operation"}]}],"stateMutability":"view"},{"type":"function","name":"getMetadata","inputs":[],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getProposal","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct StagedProposalProcessor.Proposal","components":[{"name":"allowFailureMap","type":"uint128","internalType":"uint128"},{"name":"lastStageTransition","type":"uint64","internalType":"uint64"},{"name":"currentStage","type":"uint16","internalType":"uint16"},{"name":"stageConfigIndex","type":"uint16","internalType":"uint16"},{"name":"executed","type":"bool","internalType":"bool"},{"name":"canceled","type":"bool","internalType":"bool"},{"name":"creator","type":"address","internalType":"address"},{"name":"actions","type":"tuple[]","internalType":"struct Action[]","components":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}]},{"name":"targetConfig","type":"tuple","internalType":"struct IPlugin.TargetConfig","components":[{"name":"target","type":"address","internalType":"address"},{"name":"operation","type":"uint8","internalType":"enum IPlugin.Operation"}]}]}],"stateMutability":"view"},{"type":"function","name":"getProposalTally","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"},{"name":"_stageId","type":"uint16","internalType":"uint16"}],"outputs":[{"name":"approvals","type":"uint256","internalType":"uint256"},{"name":"vetoes","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStages","inputs":[{"name":"_index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple[]","internalType":"struct StagedProposalProcessor.Stage[]","components":[{"name":"bodies","type":"tuple[]","internalType":"struct StagedProposalProcessor.Body[]","components":[{"name":"addr","type":"address","internalType":"address"},{"name":"isManual","type":"bool","internalType":"bool"},{"name":"tryAdvance","type":"bool","internalType":"bool"},{"name":"resultType","type":"uint8","internalType":"enum StagedProposalProcessor.ResultType"}]},{"name":"maxAdvance","type":"uint64","internalType":"uint64"},{"name":"minAdvance","type":"uint64","internalType":"uint64"},{"name":"voteDuration","type":"uint64","internalType":"uint64"},{"name":"approvalThreshold","type":"uint16","internalType":"uint16"},{"name":"vetoThreshold","type":"uint16","internalType":"uint16"},{"name":"cancelable","type":"bool","internalType":"bool"},{"name":"editable","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getTargetConfig","inputs":[],"outputs":[{"name":"","type":"tuple","internalType":"struct IPlugin.TargetConfig","components":[{"name":"target","type":"address","internalType":"address"},{"name":"operation","type":"uint8","internalType":"enum IPlugin.Operation"}]}],"stateMutability":"view"},{"type":"function","name":"getTrustedForwarder","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"hasAdvancePermission","inputs":[{"name":"_account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hasExecutePermission","inputs":[{"name":"_account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hasSucceeded","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"implementation","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_dao","type":"address","internalType":"contract IDAO"},{"name":"_trustedForwarder","type":"address","internalType":"address"},{"name":"_stages","type":"tuple[]","internalType":"struct StagedProposalProcessor.Stage[]","components":[{"name":"bodies","type":"tuple[]","internalType":"struct StagedProposalProcessor.Body[]","components":[{"name":"addr","type":"address","internalType":"address"},{"name":"isManual","type":"bool","internalType":"bool"},{"name":"tryAdvance","type":"bool","internalType":"bool"},{"name":"resultType","type":"uint8","internalType":"enum StagedProposalProcessor.ResultType"}]},{"name":"maxAdvance","type":"uint64","internalType":"uint64"},{"name":"minAdvance","type":"uint64","internalType":"uint64"},{"name":"voteDuration","type":"uint64","internalType":"uint64"},{"name":"approvalThreshold","type":"uint16","internalType":"uint16"},{"name":"vetoThreshold","type":"uint16","internalType":"uint16"},{"name":"cancelable","type":"bool","internalType":"bool"},{"name":"editable","type":"bool","internalType":"bool"}]},{"name":"_pluginMetadata","type":"bytes","internalType":"bytes"},{"name":"_targetConfig","type":"tuple","internalType":"struct IPlugin.TargetConfig","components":[{"name":"target","type":"address","internalType":"address"},{"name":"operation","type":"uint8","internalType":"enum IPlugin.Operation"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isTrustedForwarder","inputs":[{"name":"_forwarder","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pluginType","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"enum IPlugin.PluginType"}],"stateMutability":"pure"},{"type":"function","name":"proposalCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"protocolVersion","inputs":[],"outputs":[{"name":"","type":"uint8[3]","internalType":"uint8[3]"}],"stateMutability":"pure"},{"type":"function","name":"proxiableUUID","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"reportProposalResult","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"},{"name":"_stageId","type":"uint16","internalType":"uint16"},{"name":"_resultType","type":"uint8","internalType":"enum StagedProposalProcessor.ResultType"},{"name":"_tryAdvance","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setMetadata","inputs":[{"name":"_metadata","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setTargetConfig","inputs":[{"name":"_targetConfig","type":"tuple","internalType":"struct IPlugin.TargetConfig","components":[{"name":"target","type":"address","internalType":"address"},{"name":"operation","type":"uint8","internalType":"enum IPlugin.Operation"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setTrustedForwarder","inputs":[{"name":"_forwarder","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"state","inputs":[{"name":"_proposalId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint8","internalType":"enum StagedProposalProcessor.ProposalState"}],"stateMutability":"view"},{"type":"function","name":"supportsInterface","inputs":[{"name":"_interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"updateStages","inputs":[{"name":"_stages","type":"tuple[]","internalType":"struct StagedProposalProcessor.Stage[]","components":[{"name":"bodies","type":"tuple[]","internalType":"struct StagedProposalProcessor.Body[]","components":[{"name":"addr","type":"address","internalType":"address"},{"name":"isManual","type":"bool","internalType":"bool"},{"name":"tryAdvance","type":"bool","internalType":"bool"},{"name":"resultType","type":"uint8","internalType":"enum StagedProposalProcessor.ResultType"}]},{"name":"maxAdvance","type":"uint64","internalType":"uint64"},{"name":"minAdvance","type":"uint64","internalType":"uint64"},{"name":"voteDuration","type":"uint64","internalType":"uint64"},{"name":"approvalThreshold","type":"uint16","internalType":"uint16"},{"name":"vetoThreshold","type":"uint16","internalType":"uint16"},{"name":"cancelable","type":"bool","internalType":"bool"},{"name":"editable","type":"bool","internalType":"bool"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"upgradeTo","inputs":[{"name":"newImplementation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"upgradeToAndCall","inputs":[{"name":"newImplementation","type":"address","internalType":"address"},{"name":"data","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"event","name":"AdminChanged","inputs":[{"name":"previousAdmin","type":"address","indexed":false,"internalType":"address"},{"name":"newAdmin","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"BeaconUpgraded","inputs":[{"name":"beacon","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint8","indexed":false,"internalType":"uint8"}],"anonymous":false},{"type":"event","name":"MetadataSet","inputs":[{"name":"metadata","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"ProposalAdvanced","inputs":[{"name":"proposalId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stageId","type":"uint16","indexed":true,"internalType":"uint16"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ProposalCanceled","inputs":[{"name":"proposalId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stageId","type":"uint16","indexed":true,"internalType":"uint16"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ProposalCreated","inputs":[{"name":"proposalId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"creator","type":"address","indexed":true,"internalType":"address"},{"name":"startDate","type":"uint64","indexed":false,"internalType":"uint64"},{"name":"endDate","type":"uint64","indexed":false,"internalType":"uint64"},{"name":"metadata","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"actions","type":"tuple[]","indexed":false,"internalType":"struct Action[]","components":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}]},{"name":"allowFailureMap","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProposalEdited","inputs":[{"name":"proposalId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stageId","type":"uint16","indexed":true,"internalType":"uint16"},{"name":"sender","type":"address","indexed":true,"internalType":"address"},{"name":"metadata","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"actions","type":"tuple[]","indexed":false,"internalType":"struct Action[]","components":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"ProposalExecuted","inputs":[{"name":"proposalId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProposalResultReported","inputs":[{"name":"proposalId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stageId","type":"uint16","indexed":true,"internalType":"uint16"},{"name":"body","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StagesUpdated","inputs":[{"name":"stages","type":"tuple[]","indexed":false,"internalType":"struct StagedProposalProcessor.Stage[]","components":[{"name":"bodies","type":"tuple[]","internalType":"struct StagedProposalProcessor.Body[]","components":[{"name":"addr","type":"address","internalType":"address"},{"name":"isManual","type":"bool","internalType":"bool"},{"name":"tryAdvance","type":"bool","internalType":"bool"},{"name":"resultType","type":"uint8","internalType":"enum StagedProposalProcessor.ResultType"}]},{"name":"maxAdvance","type":"uint64","internalType":"uint64"},{"name":"minAdvance","type":"uint64","internalType":"uint64"},{"name":"voteDuration","type":"uint64","internalType":"uint64"},{"name":"approvalThreshold","type":"uint16","internalType":"uint16"},{"name":"vetoThreshold","type":"uint16","internalType":"uint16"},{"name":"cancelable","type":"bool","internalType":"bool"},{"name":"editable","type":"bool","internalType":"bool"}]}],"anonymous":false},{"type":"event","name":"SubProposalCreated","inputs":[{"name":"proposalId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stageId","type":"uint16","indexed":true,"internalType":"uint16"},{"name":"body","type":"address","indexed":true,"internalType":"address"},{"name":"bodyProposalId","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"SubProposalNotCreated","inputs":[{"name":"proposalId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stageId","type":"uint16","indexed":true,"internalType":"uint16"},{"name":"body","type":"address","indexed":true,"internalType":"address"},{"name":"reason","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"TargetSet","inputs":[{"name":"newTargetConfig","type":"tuple","indexed":false,"internalType":"struct IPlugin.TargetConfig","components":[{"name":"target","type":"address","internalType":"address"},{"name":"operation","type":"uint8","internalType":"enum IPlugin.Operation"}]}],"anonymous":false},{"type":"event","name":"TrustedForwarderUpdated","inputs":[{"name":"forwarder","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Upgraded","inputs":[{"name":"implementation","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"AlreadyInitialized","inputs":[]},{"type":"error","name":"BodyResultTypeNotSet","inputs":[{"name":"body","type":"address","internalType":"address"}]},{"type":"error","name":"DaoUnauthorized","inputs":[{"name":"dao","type":"address","internalType":"address"},{"name":"where","type":"address","internalType":"address"},{"name":"who","type":"address","internalType":"address"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"DelegateCallFailed","inputs":[]},{"type":"error","name":"DuplicateBodyAddress","inputs":[{"name":"stageId","type":"uint256","internalType":"uint256"},{"name":"body","type":"address","internalType":"address"}]},{"type":"error","name":"FunctionDeprecated","inputs":[]},{"type":"error","name":"InsufficientGas","inputs":[]},{"type":"error","name":"InterfaceNotSupported","inputs":[]},{"type":"error","name":"InvalidTargetConfig","inputs":[{"name":"targetConfig","type":"tuple","internalType":"struct IPlugin.TargetConfig","components":[{"name":"target","type":"address","internalType":"address"},{"name":"operation","type":"uint8","internalType":"enum IPlugin.Operation"}]}]},{"type":"error","name":"NonexistentProposal","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ProposalAdvanceForbidden","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ProposalAlreadyExists","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ProposalCanNotBeCancelled","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"},{"name":"stageId","type":"uint16","internalType":"uint16"}]},{"type":"error","name":"ProposalCanNotBeEdited","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"},{"name":"stageId","type":"uint16","internalType":"uint16"}]},{"type":"error","name":"ProposalExecutionForbidden","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"StageCountZero","inputs":[]},{"type":"error","name":"StageDurationsInvalid","inputs":[]},{"type":"error","name":"StageIdInvalid","inputs":[{"name":"currentStageId","type":"uint64","internalType":"uint64"},{"name":"reportedStageId","type":"uint64","internalType":"uint64"}]},{"type":"error","name":"StageThresholdsInvalid","inputs":[]},{"type":"error","name":"StartDateInvalid","inputs":[{"name":"","type":"uint64","internalType":"uint64"}]},{"type":"error","name":"Uint16MaxSizeExceeded","inputs":[]},{"type":"error","name":"UnexpectedProposalState","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"},{"name":"currentState","type":"uint8","internalType":"uint8"},{"name":"allowedStates","type":"bytes32","internalType":"bytes32"}]}] as const; + +export const StagedProposalProcessorSetupABI = [{"type":"constructor","inputs":[{"name":"_spp","type":"address","internalType":"contract StagedProposalProcessor"}],"stateMutability":"nonpayable"},{"type":"function","name":"CLONES_SUPPORTED","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"CONDITION_IMPLEMENTATION","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"implementation","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"prepareInstallation","inputs":[{"name":"_dao","type":"address","internalType":"address"},{"name":"_installationParams","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"spp","type":"address","internalType":"address"},{"name":"preparedSetupData","type":"tuple","internalType":"struct IPluginSetup.PreparedSetupData","components":[{"name":"helpers","type":"address[]","internalType":"address[]"},{"name":"permissions","type":"tuple[]","internalType":"struct PermissionLib.MultiTargetPermission[]","components":[{"name":"operation","type":"uint8","internalType":"enum PermissionLib.Operation"},{"name":"where","type":"address","internalType":"address"},{"name":"who","type":"address","internalType":"address"},{"name":"condition","type":"address","internalType":"address"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]}]}],"stateMutability":"nonpayable"},{"type":"function","name":"prepareUninstallation","inputs":[{"name":"_dao","type":"address","internalType":"address"},{"name":"_payload","type":"tuple","internalType":"struct IPluginSetup.SetupPayload","components":[{"name":"plugin","type":"address","internalType":"address"},{"name":"currentHelpers","type":"address[]","internalType":"address[]"},{"name":"data","type":"bytes","internalType":"bytes"}]}],"outputs":[{"name":"permissions","type":"tuple[]","internalType":"struct PermissionLib.MultiTargetPermission[]","components":[{"name":"operation","type":"uint8","internalType":"enum PermissionLib.Operation"},{"name":"where","type":"address","internalType":"address"},{"name":"who","type":"address","internalType":"address"},{"name":"condition","type":"address","internalType":"address"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]}],"stateMutability":"pure"},{"type":"function","name":"prepareUpdate","inputs":[{"name":"_dao","type":"address","internalType":"address"},{"name":"_fromBuild","type":"uint16","internalType":"uint16"},{"name":"_payload","type":"tuple","internalType":"struct IPluginSetup.SetupPayload","components":[{"name":"plugin","type":"address","internalType":"address"},{"name":"currentHelpers","type":"address[]","internalType":"address[]"},{"name":"data","type":"bytes","internalType":"bytes"}]}],"outputs":[{"name":"initData","type":"bytes","internalType":"bytes"},{"name":"preparedSetupData","type":"tuple","internalType":"struct IPluginSetup.PreparedSetupData","components":[{"name":"helpers","type":"address[]","internalType":"address[]"},{"name":"permissions","type":"tuple[]","internalType":"struct PermissionLib.MultiTargetPermission[]","components":[{"name":"operation","type":"uint8","internalType":"enum PermissionLib.Operation"},{"name":"where","type":"address","internalType":"address"},{"name":"who","type":"address","internalType":"address"},{"name":"condition","type":"address","internalType":"address"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]}]}],"stateMutability":"nonpayable"},{"type":"function","name":"protocolVersion","inputs":[],"outputs":[{"name":"","type":"uint8[3]","internalType":"uint8[3]"}],"stateMutability":"pure"},{"type":"function","name":"supportsInterface","inputs":[{"name":"_interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"error","name":"InvalidUpdatePath","inputs":[{"name":"fromBuild","type":"uint16","internalType":"uint16"},{"name":"thisBuild","type":"uint16","internalType":"uint16"}]}] as const; + +export const ErrorsABI = [{"type":"error","name":"BodyResultTypeNotSet","inputs":[{"name":"body","type":"address","internalType":"address"}]},{"type":"error","name":"DuplicateBodyAddress","inputs":[{"name":"stageId","type":"uint256","internalType":"uint256"},{"name":"body","type":"address","internalType":"address"}]},{"type":"error","name":"EmptyMetadata","inputs":[]},{"type":"error","name":"IncorrectActionCount","inputs":[]},{"type":"error","name":"InsufficientGas","inputs":[]},{"type":"error","name":"InterfaceNotSupported","inputs":[]},{"type":"error","name":"InvalidCustomParamsForFirstStage","inputs":[]},{"type":"error","name":"NonexistentProposal","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ProposalAdvanceForbidden","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ProposalAlreadyCancelled","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ProposalAlreadyExists","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ProposalCanNotBeCancelled","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"},{"name":"stageId","type":"uint16","internalType":"uint16"}]},{"type":"error","name":"ProposalCanNotBeEdited","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"},{"name":"stageId","type":"uint16","internalType":"uint16"}]},{"type":"error","name":"ProposalExecutionForbidden","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"StageCountZero","inputs":[]},{"type":"error","name":"StageDurationsInvalid","inputs":[]},{"type":"error","name":"StageIdInvalid","inputs":[{"name":"currentStageId","type":"uint64","internalType":"uint64"},{"name":"reportedStageId","type":"uint64","internalType":"uint64"}]},{"type":"error","name":"StageThresholdsInvalid","inputs":[]},{"type":"error","name":"StartDateInvalid","inputs":[{"name":"","type":"uint64","internalType":"uint64"}]},{"type":"error","name":"Uint16MaxSizeExceeded","inputs":[]},{"type":"error","name":"UnexpectedProposalState","inputs":[{"name":"proposalId","type":"uint256","internalType":"uint256"},{"name":"currentState","type":"uint8","internalType":"uint8"},{"name":"allowedStates","type":"bytes32","internalType":"bytes32"}]}] as const; + +export const PluginSettingsABI = [{"type":"function","name":"BUILD_METADATA","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"PLACEHOLDER_BUILD_METADATA","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"PLUGIN_CONTRACT_NAME","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"PLUGIN_REPO_ENS_SUBDOMAIN_NAME","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"PLUGIN_SETUP_CONTRACT_NAME","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"PROPOSAL_METADATA","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"RELEASE_METADATA","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"VERSION_BUILD","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"type":"function","name":"VERSION_RELEASE","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"}] as const; + +export const SPPRuleConditionABI = [{"type":"constructor","inputs":[{"name":"_dao","type":"address","internalType":"address"},{"name":"_rules","type":"tuple[]","internalType":"struct RuledCondition.Rule[]","components":[{"name":"id","type":"uint8","internalType":"uint8"},{"name":"op","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint240","internalType":"uint240"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]}],"stateMutability":"nonpayable"},{"type":"function","name":"UPDATE_RULES_PERMISSION_ID","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"dao","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IDAO"}],"stateMutability":"view"},{"type":"function","name":"decodeRuleValue","inputs":[{"name":"_x","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"a","type":"uint32","internalType":"uint32"},{"name":"b","type":"uint32","internalType":"uint32"},{"name":"c","type":"uint32","internalType":"uint32"}],"stateMutability":"pure"},{"type":"function","name":"encodeIfElse","inputs":[{"name":"startingRuleIndex","type":"uint256","internalType":"uint256"},{"name":"successRuleIndex","type":"uint256","internalType":"uint256"},{"name":"failureRuleIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint240","internalType":"uint240"}],"stateMutability":"pure"},{"type":"function","name":"encodeLogicalOperator","inputs":[{"name":"ruleIndex1","type":"uint256","internalType":"uint256"},{"name":"ruleIndex2","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint240","internalType":"uint240"}],"stateMutability":"pure"},{"type":"function","name":"getRules","inputs":[],"outputs":[{"name":"","type":"tuple[]","internalType":"struct RuledCondition.Rule[]","components":[{"name":"id","type":"uint8","internalType":"uint8"},{"name":"op","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint240","internalType":"uint240"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_dao","type":"address","internalType":"address"},{"name":"_rules","type":"tuple[]","internalType":"struct RuledCondition.Rule[]","components":[{"name":"id","type":"uint8","internalType":"uint8"},{"name":"op","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint240","internalType":"uint240"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isGranted","inputs":[{"name":"_where","type":"address","internalType":"address"},{"name":"_who","type":"address","internalType":"address"},{"name":"_permissionId","type":"bytes32","internalType":"bytes32"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"isPermitted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"protocolVersion","inputs":[],"outputs":[{"name":"","type":"uint8[3]","internalType":"uint8[3]"}],"stateMutability":"pure"},{"type":"function","name":"supportsInterface","inputs":[{"name":"_interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"updateRules","inputs":[{"name":"_rules","type":"tuple[]","internalType":"struct RuledCondition.Rule[]","components":[{"name":"id","type":"uint8","internalType":"uint8"},{"name":"op","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint240","internalType":"uint240"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint8","indexed":false,"internalType":"uint8"}],"anonymous":false},{"type":"event","name":"RulesUpdated","inputs":[{"name":"rules","type":"tuple[]","indexed":false,"internalType":"struct RuledCondition.Rule[]","components":[{"name":"id","type":"uint8","internalType":"uint8"},{"name":"op","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint240","internalType":"uint240"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]}],"anonymous":false},{"type":"error","name":"DaoUnauthorized","inputs":[{"name":"dao","type":"address","internalType":"address"},{"name":"where","type":"address","internalType":"address"},{"name":"who","type":"address","internalType":"address"},{"name":"permissionId","type":"bytes32","internalType":"bytes32"}]}] as const; + diff --git a/npm-artifacts/sync-addresses.sh b/npm-artifacts/sync-addresses.sh index dc46812..9c132d9 100644 --- a/npm-artifacts/sync-addresses.sh +++ b/npm-artifacts/sync-addresses.sh @@ -1,115 +1,91 @@ #!/usr/bin/env bash +# Generate src/addresses.json from protocol-factory's deployment artifacts. +# +# protocol-factory/artifacts/ contains files of the form +# addresses--.json +# We pick the file with the highest timestamp suffix per network and extract +# `corePlugins.stagedProposalProcessorPluginRepo` from it. -# Sync the addresses from osx-commons/configs +set -euo pipefail usage() { - echo "Usage: $(basename "$0") " >&2 - echo " : Path to the directory containing the source JSON files." >&2 - echo " : Path to the addresses.json file to be created/overwritten." >&2 + echo "Usage: $(basename "$0") " >&2 + echo " e.g. $(basename "$0") ../../protocol-factory/artifacts ./src/addresses.json" >&2 } if [[ $# -ne 2 ]]; then - echo "Error: Expected 2 arguments." >&2 - usage - exit 1 + echo "Error: expected 2 arguments." >&2 + usage + exit 1 fi SOURCE_DIR="$1" DEST_FILE="$2" -UNSUPPORTED_NETWORKS=( - "goerli" - "baseGoerli" - "devSepolia" -) - -# Checks - if [[ ! -d "$SOURCE_DIR" ]]; then - echo "Error: Source directory '$SOURCE_DIR' not found." >&2 - exit 1 -fi - -if ! command -v jq &> /dev/null; then - echo "Error: 'jq' command not found. Please install jq." >&2 + echo "Error: source directory '$SOURCE_DIR' not found." >&2 exit 1 fi -if [ "$SOURCE_DIR" == "$(dirname $DEST_FILE)" ]; then - echo "Error: The destination file cannot be in the same path as the destination file" >&2 +if ! command -v jq &>/dev/null; then + echo "Error: 'jq' is required (sudo apt install jq / brew install jq)." >&2 exit 1 fi -# Helpers +cd "$(dirname "$0")" -containsElement () { - local seeking=$1; shift - local in=1 # Not found - for element; do - if [[ "$element" == "$seeking" ]]; then - in=0 # Found - break - fi - done - return $in -} - -networkAlias () { - local network="$1" +# Start from whatever's currently in DEST_FILE so we don't drop networks that +# aren't represented in protocol-factory's artifacts. Overlay the freshly +# resolved entries on top — same-network keys win the latest value. +if [[ -f "$DEST_FILE" ]]; then + BASE=$(cat "$DEST_FILE") +else + BASE='{"pluginRepo":{}}' +fi - if [[ "$network" == "baseMainnet" ]]; then printf "base" - elif [[ "$network" == "bscMainnet" ]]; then printf "bsc" - elif [[ "$network" == "modeMainnet" ]]; then printf "mode" - elif [[ "$network" == "zksyncMainnet" ]]; then printf "zksync" - else printf "$network" +# Group `addresses--.json` files by network, pick the highest ts per network. +declare -A LATEST +for path in "$SOURCE_DIR"/addresses-*-*.json; do + [[ -f "$path" ]] || continue + fname=$(basename "$path") + # strip `addresses-` prefix and `.json` suffix → `-` + rest=${fname#addresses-} + rest=${rest%.json} + network=${rest%-*} + ts=${rest##*-} + [[ "$ts" =~ ^[0-9]+$ ]] || { echo "Skipping malformed name: $fname" >&2; continue; } + + if [[ -z "${LATEST[$network]+x}" ]] || (( ts > ${LATEST[$network]##*|} )); then + LATEST[$network]="$path|$ts" fi -} - -# Ready - -echo "Processing $SOURCE_DIR" - -# Create a temporary file to store intermediate JSON structures -# Each line in this file will be a JSON object like: {"pluginRepo": {"network_name": "value"}} -TEMP_MERGE_FILE=$(mktemp) +done -# Clean the temp file when the script exits -trap 'rm -f "$TEMP_MERGE_FILE"' EXIT +if [[ ${#LATEST[@]} -eq 0 ]]; then + echo "Warning: no addresses-*-*.json files in '$SOURCE_DIR'; leaving $DEST_FILE untouched." >&2 + exit 0 +fi -# List source address files -find "$SOURCE_DIR" -maxdepth 1 -name '*.json' | sort | while read source_file; do - filename=$(basename "$source_file") - network="${filename%.json}" +# Build the overlay object as `{pluginRepo: {: , ...}}`. +OVERLAY='{"pluginRepo":{}}' +for network in "${!LATEST[@]}"; do + entry=${LATEST[$network]} + file=${entry%|*} + echo "Using $(basename "$file") for $network" - if containsElement "$network" "${UNSUPPORTED_NETWORKS[@]}"; then - echo "Skipping deprecated network: $network" + addr=$(jq -er '.corePlugins.stagedProposalProcessorPluginRepo // empty' "$file") || { + echo " Warning: corePlugins.stagedProposalProcessorPluginRepo missing in $(basename "$file"); skipping." >&2 continue - fi - - echo "Processing $filename:" - - # Extract the address - value=$(jq -er '.["v1.4.0"].StagedProposalProcessorRepoProxy.address // .["v1.3.0"].StagedProposalProcessorRepoProxy.address // empty' "$source_file") - jq_exit_code=$? - - if [[ $jq_exit_code -ne 0 || "$value" == "null" ]]; then - echo " Warning: Could not find 'StagedProposalProcessorRepoProxy' under 'v1.4.0' or 'v1.3.0' in '$filename'. Skipping." >&2 + } + if [[ -z "$addr" || "$addr" == "null" ]]; then + echo " Warning: empty SPP plugin repo address in $(basename "$file"); skipping." >&2 continue fi - echo " Found $value" - jq -n --arg network "$(networkAlias $network)" --arg value "$value" \ - '{pluginRepo: {($network): $value}}' >> "$TEMP_MERGE_FILE" + OVERLAY=$(jq --arg network "$network" --arg value "$addr" '.pluginRepo[$network] = $value' <<<"$OVERLAY") done -echo "Merging addresses..." -jq -s 'map(.pluginRepo) | add | {pluginRepo: .}' "$TEMP_MERGE_FILE" > "$DEST_FILE" - - -if [[ $? -ne 0 ]]; then - echo "Error: Failed to merge the values into '$DEST_FILE'" >&2 - exit 1 -fi +# Merge: BASE overlaid by OVERLAY, with deterministic alphabetical key order. +jq --sort-keys -n --argjson base "$BASE" --argjson overlay "$OVERLAY" \ + '{pluginRepo: ($base.pluginRepo + $overlay.pluginRepo)}' > "$DEST_FILE" -echo "Addresses written to '$DEST_FILE'" -exit 0 +echo "Addresses merged into '$DEST_FILE'" diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 5f80eb3..1d0897e 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -10,6 +10,9 @@ import {StagedProposalProcessorSetup as SPPSetup} from "../src/StagedProposalPro import {PluginRepo} from "@aragon/osx/framework/plugin/repo/PluginRepo.sol"; import {PluginRepoFactory} from "@aragon/osx/framework/plugin/repo/PluginRepoFactory.sol"; +import { + PlaceholderSetup +} from "@aragon/osx/framework/plugin/repo/placeholder/PlaceholderSetup.sol"; import {PermissionLib} from "@aragon/osx-commons-contracts/src/permission/PermissionLib.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; @@ -18,6 +21,8 @@ contract Deploy is BaseScript { error InvalidVersionBuild(uint8 build, uint8 latestBuild); error VersionPublishFailed(); + PlaceholderSetup public placeholderSetup; + function run() external { address pluginRepoFactory = vm.envAddress("PLUGIN_REPO_FACTORY_ADDRESS"); address managementDao = vm.envAddress("MANAGEMENT_DAO_ADDRESS"); @@ -37,6 +42,20 @@ contract Deploy is BaseScript { revert InvalidVersionBuild(PluginSettings.VERSION_BUILD, uint8(latestBuild)); } + // Fill builds 1..VERSION_BUILD-1 with PlaceholderSetup so build numbers stay + // aligned across networks (a fresh chain still ends up with build N == VERSION_BUILD). + if (PluginSettings.VERSION_BUILD > latestBuild + 1) { + placeholderSetup = new PlaceholderSetup(); + for (uint8 i = uint8(latestBuild) + 1; i < PluginSettings.VERSION_BUILD; ++i) { + sppRepo.createVersion( + PluginSettings.VERSION_RELEASE, + address(placeholderSetup), + bytes(PluginSettings.PLACEHOLDER_BUILD_METADATA), + bytes(PluginSettings.RELEASE_METADATA) + ); + } + } + sppRepo.createVersion( PluginSettings.VERSION_RELEASE, address(sppSetup), @@ -59,6 +78,9 @@ contract Deploy is BaseScript { console.log("- SPP PluginRepo: ", address(sppRepo)); console.log("- SPP PluginSetup: ", address(sppSetup)); console.log("- Implementation: ", sppSetup.implementation()); + if (address(placeholderSetup) != address(0)) { + console.log("- PlaceholderSetup: ", address(placeholderSetup)); + } console.log( "- Version: ", _versionString(PluginSettings.VERSION_RELEASE, PluginSettings.VERSION_BUILD) diff --git a/script/NewVersion.s.sol b/script/NewVersion.s.sol index 6df6661..948e348 100644 --- a/script/NewVersion.s.sol +++ b/script/NewVersion.s.sol @@ -9,33 +9,112 @@ import {StagedProposalProcessor as SPP} from "../src/StagedProposalProcessor.sol import {StagedProposalProcessorSetup as SPPSetup} from "../src/StagedProposalProcessorSetup.sol"; import {PluginRepo} from "@aragon/osx/framework/plugin/repo/PluginRepo.sol"; +import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; +import {IPluginSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/IPluginSetup.sol"; -/// @notice Deploys a new SPPSetup implementation and prints the DAO proposal calldata. -/// Submit the printed calldata as a management DAO proposal to publish the new version. +/// @dev Minimal subset of the management DAO Multisig plugin ABI used by this script. +/// Mirrors the 7-arg `createProposal` overload (selector `0xfbd56e41`); using this typed +/// interface with `abi.encodeCall` makes the compiler enforce the signature match instead +/// of trusting a string we hand to `abi.encodeWithSignature`. +interface IManagementDaoMultisig { + function createProposal( + bytes calldata _metadata, + Action[] calldata _actions, + uint256 _allowFailureMap, + bool _approveProposal, + bool _tryExecution, + uint64 _startDate, + uint64 _endDate + ) external returns (uint256 proposalId); +} + +/// @notice Deploys a new SPPSetup implementation and prints both the inner +/// `createVersion` action and the outer management DAO multisig +/// `createProposal` calldata. Submit the printed multisig calldata from any +/// listed multisig member to publish this version. contract NewVersion is BaseScript { function run() external { sppRepo = PluginRepo(vm.envAddress("SPP_PLUGIN_REPO_ADDRESS")); + address managementDaoMultisig = vm.envAddress("MANAGEMENT_DAO_MULTISIG_ADDRESS"); + + // Reuse the previous build's plugin implementation. v1.2's bytecode is identical to v1.1's + SPP existingImpl = _readLatestImplementation(); vm.startBroadcast(deployerPrivateKey); - sppSetup = new SPPSetup(new SPP()); + sppSetup = new SPPSetup(existingImpl); vm.stopBroadcast(); - console.log("- SPP PluginSetup: ", address(sppSetup)); + console.log("- SPP PluginSetup: ", address(sppSetup)); console.log( - "- Version: ", + "- Version: ", _versionString(PluginSettings.VERSION_RELEASE, PluginSettings.VERSION_BUILD) ); - console.log("\nDAO proposal to publish this version:"); - console.log(" to: ", address(sppRepo)); - console.log(" value: ", uint256(0)); - console.logBytes( - abi.encodeWithSelector( - sppRepo.createVersion.selector, + + bytes memory createVersionData = abi.encodeCall( + sppRepo.createVersion, + ( PluginSettings.VERSION_RELEASE, address(sppSetup), - PluginSettings.BUILD_METADATA, - PluginSettings.RELEASE_METADATA + bytes(PluginSettings.BUILD_METADATA), + bytes(PluginSettings.RELEASE_METADATA) ) ); + + console.log("\nDAO action to publish this version:"); + console.log(" to: ", address(sppRepo)); + console.log(" value: 0"); + console.log(" data: "); + console.logBytes(createVersionData); + + // Wrap the action in a management DAO multisig proposal. The 7-arg + // `createProposal` is the multisig-specific overload; passing + // `_approveProposal=true` means the submitter also casts their vote + // in the same transaction. + Action[] memory actions = new Action[](1); + actions[0] = Action({to: address(sppRepo), value: 0, data: createVersionData}); + + bytes memory metadata = bytes(PluginSettings.PROPOSAL_METADATA); + uint64 endDate = uint64(vm.envOr("PROPOSAL_END_DATE", block.timestamp + 30 days)); + bytes memory multisigCalldata = abi.encodeCall( + IManagementDaoMultisig.createProposal, + ( + metadata, + actions, + uint256(0), // _allowFailureMap + true, // _approveProposal + false, // _tryExecution + uint64(0), // _startDate (0 = now, evaluated at submission time) + endDate + ) + ); + + // The Multisig derives proposalId as + // keccak256(abi.encode(chainid, block.number, multisig, keccak256(abi.encode(actions, metadata)))) + // (see Multisig.createProposal -> Proposal._createProposalId in osx-commons). + // Submission block isn't known at script time, so we print the deterministic + // salt; the actual proposalId is also surfaced via the `ProposalCreated` event + // on the submission tx receipt. + bytes32 proposalSalt = keccak256(abi.encode(actions, metadata)); + + console.log("\nManagement DAO multisig proposal to publish this version:"); + console.log(" to: ", managementDaoMultisig); + console.log(" value: 0"); + console.log(" data: "); + console.logBytes(multisigCalldata); + console.log("\n proposal metadata: ", PluginSettings.PROPOSAL_METADATA); + console.log(" defaults: allowFailureMap=0, approveProposal=true, tryExecution=false, startDate=0"); + console.log(" endDate (unix): ", uint256(endDate)); + console.log("\n proposal id (deterministic salt):"); + console.logBytes32(proposalSalt); + console.log(" full id = keccak256(abi.encode(chainid, block.number @ submission, multisig, salt))"); + console.log(" or read it from the `ProposalCreated` event on the submission tx receipt."); + } + + function _readLatestImplementation() internal view returns (SPP) { + uint8 latestRelease = sppRepo.latestRelease(); + uint16 latestBuild = uint16(sppRepo.buildCount(latestRelease)); + PluginRepo.Tag memory latestTag = PluginRepo.Tag({release: latestRelease, build: latestBuild}); + address latestSetup = sppRepo.getVersion(latestTag).pluginSetup; + return SPP(IPluginSetup(latestSetup).implementation()); } } diff --git a/script/new-version-proposal-metadata.json b/script/new-version-proposal-metadata.json new file mode 100644 index 0000000..962e946 --- /dev/null +++ b/script/new-version-proposal-metadata.json @@ -0,0 +1,15 @@ +{ + "title": "Publish StagedProposalProcessor v1.2", + "summary": "Publishes build 2 of the StagedProposalProcessor (release 1) on the SPP repo.", + "description": "Calls `createVersion` on the SPP plugin repo to publish v1.2 (release 1, build 2).\n\nBuild 2 is recompiled against an amended `osx-commons` `RuledCondition`. The installation parameters are unchanged versus build 1.\n\nExisting v1.1 installations can update in place via the setup's `prepareUpdate` flow, which deploys a fresh `SPPRuleCondition` seeded with the existing rules and migrates the `CREATE_PROPOSAL` and `UPDATE_RULES` permissions from the old helper to the new one.", + "resources": [ + { + "name": "changelog", + "url": "https://github.com/aragon/staged-proposal-processor-plugin/blob/main/CHANGELOG.md" + }, + { + "name": "audit report", + "url": "https://github.com/aragon/osx/tree/main/audits" + } + ] +} diff --git a/src/StagedProposalProcessorSetup.sol b/src/StagedProposalProcessorSetup.sol index 7929554..8bbd463 100644 --- a/src/StagedProposalProcessorSetup.sol +++ b/src/StagedProposalProcessorSetup.sol @@ -20,7 +20,7 @@ import { /// @title StagedProposalProcessorSetup /// @author Aragon X - 2024 /// @notice The setup contract of the `StagedProposalProcessor` plugin. -/// @dev Release 1, Build 1 +/// @dev Release 1, Build 2 contract StagedProposalProcessorSetup is PluginUpgradeableSetup { using ProxyLib for address; @@ -91,14 +91,71 @@ contract StagedProposalProcessorSetup is PluginUpgradeableSetup { } /// @inheritdoc IPluginSetup - /// @dev The default implementation for the initial build 1 that reverts because no earlier build exists. + /// @dev v1.1 → v1.2: deploys a fresh `SPPRuleCondition` seeded with the existing rules and migrates + /// `CREATE_PROPOSAL_PERMISSION` (on the plugin) and `UPDATE_RULES_PERMISSION` (on the helper) from + /// the old condition to the new one. The plugin proxy itself is upgraded to the new implementation + /// by the `PluginSetupProcessor` automatically; no reinitializer is required because no new storage + /// is introduced in build 2. Existing rules are read from the old helper, so no caller-supplied data + /// is required — `_payload.data` is ignored. function prepareUpdate( address _dao, uint16 _fromBuild, SetupPayload calldata _payload - ) external pure virtual returns (bytes memory, PreparedSetupData memory) { - (_dao, _fromBuild, _payload); - revert InvalidUpdatePath({fromBuild: 0, thisBuild: 1}); + ) external virtual override returns (bytes memory initData, PreparedSetupData memory preparedSetupData) { + if (_fromBuild != 1) { + revert InvalidUpdatePath({fromBuild: _fromBuild, thisBuild: 2}); + } + + address oldCondition = _payload.currentHelpers[0]; + RuledCondition.Rule[] memory rules = SPPRuleCondition(oldCondition).getRules(); + + bytes memory conditionInitData = abi.encodeCall( + SPPRuleCondition.initialize, + (_dao, rules) + ); + address newCondition = CLONES_SUPPORTED + ? CONDITION_IMPLEMENTATION.deployMinimalProxy(conditionInitData) + : CONDITION_IMPLEMENTATION.deployUUPSProxy(conditionInitData); + + preparedSetupData.helpers = new address[](1); + preparedSetupData.helpers[0] = newCondition; + + preparedSetupData.permissions = new PermissionLib.MultiTargetPermission[](4); + + // Move CREATE_PROPOSAL_PERMISSION on the plugin from the old condition to the new one. + preparedSetupData.permissions[0] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _payload.plugin, + who: ANY_ADDR, + condition: oldCondition, + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + preparedSetupData.permissions[1] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.GrantWithCondition, + where: _payload.plugin, + who: ANY_ADDR, + condition: newCondition, + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + + // Move UPDATE_RULES_PERMISSION (DAO is the rule manager) from the old condition to the new one. + preparedSetupData.permissions[2] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: oldCondition, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: Permissions.UPDATE_RULES_PERMISSION_ID + }); + preparedSetupData.permissions[3] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: newCondition, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: Permissions.UPDATE_RULES_PERMISSION_ID + }); + + // initData stays empty — applyUpdate triggers the proxy implementation upgrade on its own. + initData = ""; } /// @inheritdoc IPluginSetup diff --git a/src/build-metadata.json b/src/build-metadata.json index 7b8633c..e1f14dc 100644 --- a/src/build-metadata.json +++ b/src/build-metadata.json @@ -1,6 +1,6 @@ { "ui": {}, - "change": "Initial build.", + "change": "Recompiled against an amended osx-commons RuledCondition. Adds an in-place v1.1 -> v1.2 update path: the setup's prepareUpdate deploys a fresh SPPRuleCondition seeded with the existing rules and migrates CREATE_PROPOSAL / UPDATE_RULES permissions from the old helper to the new one. The setup interface for new installations is unchanged versus build 1.", "pluginSetup": { "prepareInstallation": { "description": "The information required for the installation of build 1.", diff --git a/src/utils/PluginSettings.sol b/src/utils/PluginSettings.sol index a8679a1..08323e0 100644 --- a/src/utils/PluginSettings.sol +++ b/src/utils/PluginSettings.sol @@ -14,13 +14,29 @@ library PluginSettings { // Specify the version of your plugin that you are currently working on. The first version is v1.1. // For more details, visit https://devs.aragon.org/docs/osx/how-it-works/framework/plugin-management/plugin-repo. uint8 public constant VERSION_RELEASE = 1; - uint8 public constant VERSION_BUILD = 1; + uint8 public constant VERSION_BUILD = 2; - // 1. upload build-metadata and release-metadata jsons to the IPFS. - // 2. use ethers to convert it to utf8 bytes: - // ethers.utils.hexlify(ethers.utils.toUtf8Bytes(`ipfs://${cid}`)) - // 3. Copy/paste the bytes into BUILD_METADATA and RELEASE_METADATA - - string public constant BUILD_METADATA = "ipfs://bafkreifia6hhz7klfbaqawd4vcplkoiesycbmrf5c2x24zfuivyn35mfsu"; - string public constant RELEASE_METADATA = "ipfs://bafkreif23p6yw325rkwwlhgkudiasvq64lonqmfnt7ls5ksfam5hedcb4m"; + // Per-build flow when bumping VERSION_BUILD (or VERSION_RELEASE): + // 1. Edit the matching JSON file: + // - `src/build-metadata.json` → BUILD_METADATA + // - `script/new-version-proposal-metadata.json` → PROPOSAL_METADATA + // - `src/release-metadata.json` (only on a new release) → RELEASE_METADATA + // 2. Pin via `just ipfs-pin `. + // 3. Paste the returned `ipfs://` into the matching constant below. + + string public constant BUILD_METADATA = + "ipfs://QmaxGSvvnTAZcDLYz2BMtaXmcx3i1GcaKGaxNEpfQe3Vyv"; + string public constant RELEASE_METADATA = + "ipfs://bafkreif23p6yw325rkwwlhgkudiasvq64lonqmfnt7ls5ksfam5hedcb4m"; + + /// @notice Title/summary/description/resources JSON pinned for this version's management DAO proposal. + /// @dev Re-pin and update on every VERSION_BUILD bump. Source: `script/new-version-proposal-metadata.json`. + string public constant PROPOSAL_METADATA = + "ipfs://QmTS3Nrjrs8nuMeqUqSRjBxbGUhZB4nW6N1GiK8vFmfDcD"; + + /// @notice Aragon's canonical empty-schema placeholder build metadata, used when filling skipped builds + /// on a fresh-network deploy so on-chain build numbers stay aligned across networks. + /// @dev Content-addressed; the file at `lib/osx/.../placeholder/placeholder-build-metadata.json` always pins to this CID. + string public constant PLACEHOLDER_BUILD_METADATA = + "ipfs://QmZDx8G5xuF9vqVbFGZ3KhF5nioL8gXwV3JbsEsSHvNMiz"; } diff --git a/test/fork/ForkBaseTest.t.sol b/test/fork/ForkBaseTest.t.sol index 7a10518..00aaf1c 100644 --- a/test/fork/ForkBaseTest.t.sol +++ b/test/fork/ForkBaseTest.t.sol @@ -83,13 +83,20 @@ contract ForkBaseTest is Assertions, Constants, Events, Fuzzers, Test { target = new Target(); trustedForwarder = new TrustedForwarder(); - // publish new spp version - sppSetup = new SPPSetup(new SPP()); // Check release number uint256 latestRelease = sppRepo.latestRelease(); uint256 latestBuild = sppRepo.buildCount(uint8(latestRelease)); + // Publish a test build that reuses the latest published SPP implementation. Mirrors + // NewVersion.s.sol so that PSP.applyUpdate sees `currentImpl == newImpl` and skips + // the proxy upgrade — i.e., per-DAO upgrades work without UPGRADE_PLUGIN_PERMISSION. + address latestSetup = sppRepo + .getVersion(PluginRepo.Tag({release: uint8(latestRelease), build: uint16(latestBuild)})) + .pluginSetup; + SPP existingImpl = SPP(IPluginSetup(latestSetup).implementation()); + sppSetup = new SPPSetup(existingImpl); + // create plugin version resetPrank(managementDao); sppRepo.createVersion( diff --git a/test/fork/upgradeV1_1ToV1_2.t.sol b/test/fork/upgradeV1_1ToV1_2.t.sol new file mode 100644 index 0000000..92fe643 --- /dev/null +++ b/test/fork/upgradeV1_1ToV1_2.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import {ForkBaseTest} from "./ForkBaseTest.t.sol"; +import {Permissions} from "../../src/libraries/Permissions.sol"; +import {SPPRuleCondition} from "../../src/utils/SPPRuleCondition.sol"; +import {StagedProposalProcessor as SPP} from "../../src/StagedProposalProcessor.sol"; + +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {DAOFactory} from "@aragon/osx/framework/dao/DAOFactory.sol"; +import { + hashHelpers, + PluginSetupRef +} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessorHelpers.sol"; +import {PluginRepo} from "@aragon/osx/framework/plugin/repo/PluginRepo.sol"; +import {IPlugin} from "@aragon/osx-commons-contracts/src/plugin/IPlugin.sol"; +import {IPluginSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/IPluginSetup.sol"; +import {PluginSetupProcessor} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol"; +import { + RuledCondition +} from "@aragon/osx-commons-contracts/src/permission/condition/extensions/RuledCondition.sol"; +import { + AddressCheckConditionMock +} from "@aragon/osx-commons-contracts/src/mocks/permission/condition/AddressCheckConditionMock.sol"; + +/// @notice Forks a network where v1.1 is deployed (set RPC_URL accordingly), installs the SPP at +/// build 1, exercises the v1.1 → v1.2 upgrade through the PSP, and asserts that the helper has +/// been swapped, the rules preserved, and the IF_ELSE `_where`/`_who` swap fixed end-to-end. Run +/// with e.g. `RPC_URL= just test-fork --match-test test_upgradeFromBuild1`. +contract UpgradeV1_1ToV1_2_ForkTest is ForkBaseTest { + DAO internal dao; + address internal installedAdminPlugin; + + // Op enum and rule-id values mirrored as raw uint8 to keep the test independent of internal + // enum ordering — a change there would itself be a bug. + uint8 internal constant OP_RET = 7; + uint8 internal constant OP_IF_ELSE = 12; + uint8 internal constant LOGIC_OP_RULE_ID = 203; + uint8 internal constant VALUE_RULE_ID = 204; + + function setUp() public override { + super.setUp(); + + DAOFactory.InstalledPlugin[] memory installed; + (dao, installed) = _createDummyDaoAdmin(); + installedAdminPlugin = installed[0].plugin; + } + + function test_upgradeFromBuild1_replacesHelper_preservesRules_andFixesIfElseSwap() external { + // ---- 1. Install the SPP at build 1 (the v1.1 setup that lives on the fork). ---- + PluginSetupRef memory build1Ref = PluginSetupRef({ + versionTag: PluginRepo.Tag({release: 1, build: 1}), + pluginSetupRepo: sppRepo + }); + + (address plugin, address[] memory helpers) = _installSPPAtRef( + dao, + _prepareSimpleInstallData(), + build1Ref + ); + address oldCondition = helpers[0]; + assertEq(SPPRuleCondition(oldCondition).getRules().length, 0, "starts with no rules"); + + // ---- 2. Seed the old condition with an IF_ELSE rule whose predicate is asymmetric in + // (`_where`, `_who`). Build 1 ships with the swap bug, so the predicate sees the + // arguments in the wrong order and the IF_ELSE routes to the failure branch. ---- + AddressCheckConditionMock asymCondition = new AddressCheckConditionMock(); + address aliceWho = makeAddr("aliceWho"); + asymCondition.setExpected(plugin, aliceWho); + + RuledCondition.Rule[] memory rules = new RuledCondition.Rule[](4); + rules[0] = RuledCondition.Rule({ + id: LOGIC_OP_RULE_ID, + op: OP_IF_ELSE, + value: SPPRuleCondition(oldCondition).encodeIfElse(1, 2, 3), + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + rules[1] = RuledCondition.Rule({ + id: 202, // CONDITION_RULE_ID + op: 1, // EQ + value: uint160(address(asymCondition)), + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + rules[2] = RuledCondition.Rule({ + id: VALUE_RULE_ID, + op: OP_RET, + value: 1, + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + rules[3] = RuledCondition.Rule({ + id: VALUE_RULE_ID, + op: OP_RET, + value: 0, + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + + resetPrank(address(dao)); + SPPRuleCondition(oldCondition).updateRules(rules); + resetPrank(deployer); + + // Bug witness on v1.1: `isGranted(plugin, aliceWho, ...)` should evaluate the predicate + // with `(_where=plugin, _who=aliceWho)` and route to the success branch (return true). + // Because of the swap, the predicate sees `(_where=aliceWho, _who=plugin)`, doesn't + // match the expected pair, and the IF_ELSE returns false instead. + assertFalse( + SPPRuleCondition(oldCondition).isGranted( + plugin, + aliceWho, + Permissions.CREATE_PROPOSAL_PERMISSION_ID, + bytes("") + ), + "v1.1: IF_ELSE swap bug returns false where it should return true" + ); + + // PSP rejects an update in the same block as the install. + vm.roll(block.number + 1); + + // ---- 3. prepareUpdate(1 -> 2). The new setup published in setUp handles the migration. ---- + PluginSetupRef memory build2Ref = PluginSetupRef({ + versionTag: PluginRepo.Tag({release: 1, build: 2}), + pluginSetupRepo: sppRepo + }); + + ( + bytes memory initData, + IPluginSetup.PreparedSetupData memory preparedSetupData + ) = psp.prepareUpdate( + address(dao), + PluginSetupProcessor.PrepareUpdateParams({ + currentVersionTag: build1Ref.versionTag, + newVersionTag: build2Ref.versionTag, + pluginSetupRepo: sppRepo, + setupPayload: IPluginSetup.SetupPayload({ + plugin: plugin, + currentHelpers: helpers, + data: "" + }) + }) + ); + + address newCondition = preparedSetupData.helpers[0]; + assertNotEq(newCondition, oldCondition, "helper replaced"); + assertEq(initData.length, 0, "initData empty (no reinitializer)"); + assertEq(preparedSetupData.permissions.length, 4, "four permission migrations"); + + // ---- 4. applyUpdate. NewVersion.s.sol publishes v1.2 with the same `IMPLEMENTATION` + // as v1.1 (bytecode is identical), so PSP.applyUpdate sees the proxy already + // points at the right impl and skips the upgrade — no UPGRADE_PLUGIN_PERMISSION + // bracket needed. The PSP only needs ROOT temporarily to apply the helper-swap + // permissions. ---- + resetPrank(address(dao)); + dao.grant(address(dao), address(psp), dao.ROOT_PERMISSION_ID()); + + psp.applyUpdate( + address(dao), + PluginSetupProcessor.ApplyUpdateParams({ + plugin: plugin, + pluginSetupRef: build2Ref, + initData: initData, + permissions: preparedSetupData.permissions, + helpersHash: hashHelpers(preparedSetupData.helpers) + }) + ); + + dao.revoke(address(dao), address(psp), dao.ROOT_PERMISSION_ID()); + resetPrank(deployer); + + // ---- 5. Post-upgrade assertions: rules preserved, permissions migrated, bug fixed. ---- + assertEq(SPPRuleCondition(newCondition).getRules(), rules, "rules preserved on new helper"); + + assertTrue( + dao.hasPermission( + newCondition, + address(dao), + Permissions.UPDATE_RULES_PERMISSION_ID, + bytes("") + ), + "new helper grants UPDATE_RULES to DAO" + ); + assertFalse( + dao.hasPermission( + oldCondition, + address(dao), + Permissions.UPDATE_RULES_PERMISSION_ID, + bytes("") + ), + "old helper no longer grants UPDATE_RULES to DAO" + ); + + // The same call that returned the wrong answer on v1.1 must now return the right one. + assertTrue( + SPPRuleCondition(newCondition).isGranted( + plugin, + aliceWho, + Permissions.CREATE_PROPOSAL_PERMISSION_ID, + bytes("") + ), + "v1.2: IF_ELSE predicate now evaluates with the correct (_where, _who) order" + ); + } + + function _prepareSimpleInstallData() internal view returns (bytes memory) { + SPP.Stage[] memory stages = new SPP.Stage[](1); + SPP.Body[] memory bodies = new SPP.Body[](1); + bodies[0] = SPP.Body({ + addr: installedAdminPlugin, + isManual: true, + tryAdvance: true, + resultType: SPP.ResultType.Approval + }); + stages[0] = SPP.Stage({ + bodies: bodies, + maxAdvance: 100, + minAdvance: 30, + voteDuration: 10, + approvalThreshold: 1, + vetoThreshold: 0, + cancelable: false, + editable: false + }); + + return + abi.encode( + "dummy spp metadata", + stages, + new RuledCondition.Rule[](0), + IPlugin.TargetConfig({target: address(0), operation: IPlugin.Operation.Call}) + ); + } + + function _installSPPAtRef( + DAO _dao, + bytes memory _data, + PluginSetupRef memory _ref + ) internal returns (address plugin, address[] memory helpers) { + resetPrank(address(_dao)); + + IPluginSetup.PreparedSetupData memory preparedSetupData; + (plugin, preparedSetupData) = psp.prepareInstallation( + address(_dao), + PluginSetupProcessor.PrepareInstallationParams(_ref, _data) + ); + + helpers = preparedSetupData.helpers; + + _dao.grant(address(_dao), address(psp), _dao.ROOT_PERMISSION_ID()); + + psp.applyInstallation( + address(_dao), + PluginSetupProcessor.ApplyInstallationParams( + _ref, + plugin, + preparedSetupData.permissions, + hashHelpers(preparedSetupData.helpers) + ) + ); + + _dao.revoke(address(_dao), address(psp), _dao.ROOT_PERMISSION_ID()); + resetPrank(deployer); + } +} diff --git a/test/integration/concrete/upgradeability/upgradeability.t.sol b/test/integration/concrete/upgradeability/upgradeability.t.sol index f235df6..2222419 100644 --- a/test/integration/concrete/upgradeability/upgradeability.t.sol +++ b/test/integration/concrete/upgradeability/upgradeability.t.sol @@ -84,5 +84,5 @@ contract Upgradeability_SPP_IntegrationTest is BaseTest { } // Storage layout compatibility (SPPStorageV1 → StagedProposalProcessor) is - // validated separately: just validate-upgrade SPPStorageV1 StagedProposalProcessor + // validated separately: just check-upgrade SPPStorageV1 StagedProposalProcessor } diff --git a/test/unit/sppRuleCondition/isGranted/isGranted.t.sol b/test/unit/sppRuleCondition/isGranted/isGranted.t.sol index e36d8f9..5ab3eb2 100644 --- a/test/unit/sppRuleCondition/isGranted/isGranted.t.sol +++ b/test/unit/sppRuleCondition/isGranted/isGranted.t.sol @@ -9,8 +9,15 @@ import {PluginACondition} from "../../../utils/dummy-plugins/PluginA/PluginACond import { RuledCondition } from "@aragon/osx-commons-contracts/src/permission/condition/extensions/RuledCondition.sol"; +import { + AddressCheckConditionMock +} from "@aragon/osx-commons-contracts/src/mocks/permission/condition/AddressCheckConditionMock.sol"; contract IsGranted_SPPRuleCondition_UnitTest is RuleConditionConfiguredTest { + // Rule IDs not exposed via the SPP test Constants + uint8 internal constant LOGIC_OP_RULE_ID = 203; + uint8 internal constant VALUE_RULE_ID = 204; + function test_WhenRulesAreEmpty() external view { // it should return true. @@ -65,4 +72,107 @@ contract IsGranted_SPPRuleCondition_UnitTest is RuleConditionConfiguredTest { ) ); } + + AddressCheckConditionMock internal addressCheckCondition; + + modifier whenRuleIsIfElseWithAsymmetricPredicate() { + addressCheckCondition = new AddressCheckConditionMock(); + // The predicate is true only when (_where == sppPlugin, _who == alice). + // Swapping the two would compare (alice, sppPlugin) against the same + // expected pair and return false. + addressCheckCondition.setExpected(address(sppPlugin), users.alice); + + SPPRuleCondition.Rule[] memory rules = new SPPRuleCondition.Rule[](4); + + // rule 0: IF_ELSE(predicate=1, success=2, failure=3) — entry point. + rules[0] = RuledCondition.Rule({ + id: LOGIC_OP_RULE_ID, + op: uint8(RuledCondition.Op.IF_ELSE), + value: ruleCondition.encodeIfElse(1, 2, 3), + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + + // rule 1: predicate — asymmetric condition that cares about both addresses. + rules[1] = RuledCondition.Rule({ + id: CONDITION_RULE_ID, + op: uint8(RuledCondition.Op.EQ), + value: uint160(address(addressCheckCondition)), + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + + // rule 2: success branch — VALUE_RULE_ID + RET with value=1 → always true. + rules[2] = RuledCondition.Rule({ + id: VALUE_RULE_ID, + op: uint8(RuledCondition.Op.RET), + value: 1, + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + + // rule 3: failure branch — VALUE_RULE_ID + RET with value=0 → always false. + rules[3] = RuledCondition.Rule({ + id: VALUE_RULE_ID, + op: uint8(RuledCondition.Op.RET), + value: 0, + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + + ruleCondition.updateRules(rules); + _; + } + + function test_WhenIfElsePredicateMatches_ItRoutesToSuccessBranch() + external + whenRuleIsIfElseWithAsymmetricPredicate + { + // it should evaluate the predicate with (_where, _who) in the correct + // order, take the success branch, and return true. + + assertTrue( + ruleCondition.isGranted( + address(sppPlugin), // _where — matches expectedWhere + users.alice, // _who — matches expectedWho + Permissions.CREATE_PROPOSAL_PERMISSION_ID, + new bytes(0) + ), + "IF_ELSE predicate should match and route to success branch" + ); + } + + function test_WhenIfElsePredicateDoesNotMatch_ItRoutesToFailureBranch() + external + whenRuleIsIfElseWithAsymmetricPredicate + { + // it should take the failure branch when the predicate is false. + + assertFalse( + ruleCondition.isGranted( + address(sppPlugin), + users.bob, // _who differs from expectedWho + Permissions.CREATE_PROPOSAL_PERMISSION_ID, + new bytes(0) + ), + "IF_ELSE predicate should not match and route to failure branch" + ); + } + + function test_WhenIfElseCallerSwapsWhereAndWho_PredicateMustNotMatch() + external + whenRuleIsIfElseWithAsymmetricPredicate + { + // If the caller passes the addresses in the opposite order, the predicate + // sees (_where=alice, _who=sppPlugin), which must not match the expected + // (sppPlugin, alice) pair. With the bug present, _evalLogic would + // swap them back internally and the predicate would incorrectly + // evaluate to true, taking the success branch. + + assertFalse( + ruleCondition.isGranted( + users.alice, // _where (intentionally swapped) + address(sppPlugin), // _who (intentionally swapped) + Permissions.CREATE_PROPOSAL_PERMISSION_ID, + new bytes(0) + ), + "Swapped where/who must not satisfy the asymmetric IF_ELSE predicate" + ); + } } diff --git a/test/unit/stagedProposalProcessorSetup/concrete/prepareUpdate/prepareUpdate.t.sol b/test/unit/stagedProposalProcessorSetup/concrete/prepareUpdate/prepareUpdate.t.sol index 2396294..a78f2c9 100644 --- a/test/unit/stagedProposalProcessorSetup/concrete/prepareUpdate/prepareUpdate.t.sol +++ b/test/unit/stagedProposalProcessorSetup/concrete/prepareUpdate/prepareUpdate.t.sol @@ -2,15 +2,21 @@ pragma solidity ^0.8.18; import {BaseTest} from "../../../../BaseTest.t.sol"; +import {Permissions} from "../../../../../src/libraries/Permissions.sol"; +import {SPPRuleCondition} from "../../../../../src/utils/SPPRuleCondition.sol"; import {StagedProposalProcessor as SPP} from "../../../../../src/StagedProposalProcessor.sol"; import { StagedProposalProcessorSetup as SPPSetup } from "../../../../../src/StagedProposalProcessorSetup.sol"; +import {PermissionLib} from "@aragon/osx-commons-contracts/src/permission/PermissionLib.sol"; import { PluginUpgradeableSetup } from "@aragon/osx-commons-contracts/src/plugin/setup/PluginUpgradeableSetup.sol"; import {IPluginSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/IPluginSetup.sol"; +import { + RuledCondition +} from "@aragon/osx-commons-contracts/src/permission/condition/extensions/RuledCondition.sol"; contract PrepareUpdate_SPPSetup_UnitTest is BaseTest { SPPSetup sppSetup; @@ -18,25 +24,186 @@ contract PrepareUpdate_SPPSetup_UnitTest is BaseTest { function setUp() public override { super.setUp(); - // deploy SPPSetup contract. sppSetup = new SPPSetup(new SPP()); } - function test_RevertWhen_PreparingUpdate() external { - // it should revert. + function test_RevertWhen_FromBuildIsNotOne() external { + // it should revert for any build other than 1 — there is only one supported update path. + + IPluginSetup.SetupPayload memory payload = IPluginSetup.SetupPayload({ + plugin: address(0), + currentHelpers: new address[](0), + data: "" + }); + + uint16[3] memory invalidFromBuilds = [uint16(0), uint16(2), uint16(3)]; + for (uint256 i = 0; i < invalidFromBuilds.length; i++) { + vm.expectRevert( + abi.encodeWithSelector( + PluginUpgradeableSetup.InvalidUpdatePath.selector, + invalidFromBuilds[i], + 2 + ) + ); + sppSetup.prepareUpdate(address(dao), invalidFromBuilds[i], payload); + } + } + + function test_WhenFromBuildIsOne() external { + // it should deploy a new condition seeded with the existing rules, + // return it as the single helper, and emit empty initData. + + SPPRuleCondition oldCondition = _deployOldConditionWithRules(); + address fakePlugin = makeAddr("fakePlugin"); + + address[] memory currentHelpers = new address[](1); + currentHelpers[0] = address(oldCondition); + IPluginSetup.SetupPayload memory payload = IPluginSetup.SetupPayload({ + plugin: fakePlugin, + currentHelpers: currentHelpers, + data: "" + }); + + (bytes memory initData, IPluginSetup.PreparedSetupData memory setupData) = sppSetup + .prepareUpdate(address(dao), 1, payload); + + // initData stays empty: no reinitializer needed. + assertEq(initData.length, 0, "initData should be empty"); + + // a brand new helper is returned (not the old one). + assertEq(setupData.helpers.length, 1, "helpers length"); + assertNotEq(setupData.helpers[0], address(0), "helper non-zero"); + assertNotEq(setupData.helpers[0], address(oldCondition), "helper differs from old"); + + // the new helper carries the same rules as the old one. + RuledCondition.Rule[] memory oldRules = oldCondition.getRules(); + RuledCondition.Rule[] memory newRules = SPPRuleCondition(setupData.helpers[0]).getRules(); + assertEq(oldRules, newRules, "rules copied to new condition"); + } + + function test_WhenFromBuildIsOne_ItMigratesPermissions() external { + // it should revoke CREATE_PROPOSAL/UPDATE_RULES from the old condition + // and grant them on the new one, in that order. + + SPPRuleCondition oldCondition = _deployOldConditionWithRules(); + address fakePlugin = makeAddr("fakePlugin"); + + address[] memory currentHelpers = new address[](1); + currentHelpers[0] = address(oldCondition); + IPluginSetup.SetupPayload memory payload = IPluginSetup.SetupPayload({ + plugin: fakePlugin, + currentHelpers: currentHelpers, + data: "" + }); + + (, IPluginSetup.PreparedSetupData memory setupData) = sppSetup.prepareUpdate( + address(dao), + 1, + payload + ); + address newCondition = setupData.helpers[0]; + + assertEq(setupData.permissions.length, 4, "four permission migrations expected"); + + PermissionLib.MultiTargetPermission memory revokeCreate = setupData.permissions[0]; + assertEq( + uint256(revokeCreate.operation), + uint256(PermissionLib.Operation.Revoke), + "[0] operation" + ); + assertEq(revokeCreate.where, fakePlugin, "[0] where"); + assertEq(revokeCreate.who, ANY_ADDR, "[0] who"); + assertEq(revokeCreate.condition, address(oldCondition), "[0] condition"); + assertEq( + revokeCreate.permissionId, + Permissions.CREATE_PROPOSAL_PERMISSION_ID, + "[0] permissionId" + ); + + PermissionLib.MultiTargetPermission memory grantCreate = setupData.permissions[1]; + assertEq( + uint256(grantCreate.operation), + uint256(PermissionLib.Operation.GrantWithCondition), + "[1] operation" + ); + assertEq(grantCreate.where, fakePlugin, "[1] where"); + assertEq(grantCreate.who, ANY_ADDR, "[1] who"); + assertEq(grantCreate.condition, newCondition, "[1] condition"); + assertEq( + grantCreate.permissionId, + Permissions.CREATE_PROPOSAL_PERMISSION_ID, + "[1] permissionId" + ); + + PermissionLib.MultiTargetPermission memory revokeUpdateRules = setupData.permissions[2]; + assertEq( + uint256(revokeUpdateRules.operation), + uint256(PermissionLib.Operation.Revoke), + "[2] operation" + ); + assertEq(revokeUpdateRules.where, address(oldCondition), "[2] where"); + assertEq(revokeUpdateRules.who, address(dao), "[2] who"); + assertEq( + revokeUpdateRules.permissionId, + Permissions.UPDATE_RULES_PERMISSION_ID, + "[2] permissionId" + ); + + PermissionLib.MultiTargetPermission memory grantUpdateRules = setupData.permissions[3]; + assertEq( + uint256(grantUpdateRules.operation), + uint256(PermissionLib.Operation.Grant), + "[3] operation" + ); + assertEq(grantUpdateRules.where, newCondition, "[3] where"); + assertEq(grantUpdateRules.who, address(dao), "[3] who"); + assertEq( + grantUpdateRules.permissionId, + Permissions.UPDATE_RULES_PERMISSION_ID, + "[3] permissionId" + ); + } + + function test_WhenFromBuildIsOneAndRulesAreEmpty() external { + // it should still produce a valid update with an empty rules set on the new helper. - // reverts due to there is no build before current one. - vm.expectRevert( - abi.encodeWithSelector(PluginUpgradeableSetup.InvalidUpdatePath.selector, 0, 1) + SPPRuleCondition oldCondition = new SPPRuleCondition( + address(dao), + new RuledCondition.Rule[](0) ); - sppSetup.prepareUpdate( + + address[] memory currentHelpers = new address[](1); + currentHelpers[0] = address(oldCondition); + IPluginSetup.SetupPayload memory payload = IPluginSetup.SetupPayload({ + plugin: makeAddr("fakePlugin"), + currentHelpers: currentHelpers, + data: "" + }); + + (, IPluginSetup.PreparedSetupData memory setupData) = sppSetup.prepareUpdate( address(dao), - 0, - IPluginSetup.SetupPayload({ - plugin: address(0), - currentHelpers: new address[](0), - data: "" - }) + 1, + payload ); + + assertEq(SPPRuleCondition(setupData.helpers[0]).getRules().length, 0, "rules empty"); + } + + function _deployOldConditionWithRules() private returns (SPPRuleCondition oldCondition) { + RuledCondition.Rule[] memory rules = new RuledCondition.Rule[](2); + rules[0] = RuledCondition.Rule({ + id: 204, // VALUE_RULE_ID + op: 7, // RET + value: 1, + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + rules[1] = RuledCondition.Rule({ + id: 200, // BLOCK_NUMBER_RULE_ID + op: 5, // GTE + value: 100, + permissionId: Permissions.CREATE_PROPOSAL_PERMISSION_ID + }); + + oldCondition = new SPPRuleCondition(address(dao), rules); } } diff --git a/test/unit/stagedProposalProcessorSetup/concrete/prepareUpdate/prepareUpdate.tree b/test/unit/stagedProposalProcessorSetup/concrete/prepareUpdate/prepareUpdate.tree index c73fb1f..672c7a1 100644 --- a/test/unit/stagedProposalProcessorSetup/concrete/prepareUpdate/prepareUpdate.tree +++ b/test/unit/stagedProposalProcessorSetup/concrete/prepareUpdate/prepareUpdate.tree @@ -1,3 +1,11 @@ PrepareUpdate_SPPSetup_UnitTest -└── when preparing update - └── it should revert. \ No newline at end of file +├── when fromBuild is not 1 +│ └── it should revert with InvalidUpdatePath(fromBuild, 2). +└── when fromBuild is 1 + ├── it should deploy a new condition seeded with the existing rules + │ and return it as the single helper, with empty initData. + ├── it should migrate CREATE_PROPOSAL and UPDATE_RULES permissions + │ from the old condition to the new one (4 entries, in order: + │ Revoke create, GrantWithCondition create, Revoke update-rules, Grant update-rules). + └── when the old condition has no rules + └── it should still succeed and emit an empty rules set on the new helper.