From 159e95d3d84d9255971e739e3cde7c19882a6459 Mon Sep 17 00:00:00 2001 From: lucas Date: Tue, 7 Jul 2026 15:51:15 -0300 Subject: [PATCH] chore: remove .claude/skills (moved to private blockful-skills repo) The dao-scaffold and proposal-review skills now live in blockful/blockful-skills under .claude/skills/tech/. --- .claude/skills/dao-scaffold/SKILL.md | 81 ------- .claude/skills/proposal-review/SKILL.md | 82 -------- .../proposal-review/assertion-baseline.md | 41 ---- .../skills/proposal-review/draft-review.md | 181 ---------------- .claude/skills/proposal-review/live-review.md | 127 ----------- .../proposal-review/pre-draft-review.md | 155 -------------- .claude/skills/proposal-review/reference.md | 65 ------ .../scripts/fetchLiveProposal.js | 199 ------------------ .../scripts/fetchTallyDraft.js | 151 ------------- .../skills/proposal-review/troubleshooting.md | 50 ----- 10 files changed, 1132 deletions(-) delete mode 100644 .claude/skills/dao-scaffold/SKILL.md delete mode 100644 .claude/skills/proposal-review/SKILL.md delete mode 100644 .claude/skills/proposal-review/assertion-baseline.md delete mode 100644 .claude/skills/proposal-review/draft-review.md delete mode 100644 .claude/skills/proposal-review/live-review.md delete mode 100644 .claude/skills/proposal-review/pre-draft-review.md delete mode 100644 .claude/skills/proposal-review/reference.md delete mode 100644 .claude/skills/proposal-review/scripts/fetchLiveProposal.js delete mode 100644 .claude/skills/proposal-review/scripts/fetchTallyDraft.js delete mode 100644 .claude/skills/proposal-review/troubleshooting.md diff --git a/.claude/skills/dao-scaffold/SKILL.md b/.claude/skills/dao-scaffold/SKILL.md deleted file mode 100644 index 17d4bf3..0000000 --- a/.claude/skills/dao-scaffold/SKILL.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -name: dao-scaffold -description: - Scaffold a new DAO into the governance calldata verification system. Creates directory structure, interfaces, base - test class, and registry entry. Use when adding support for a new DAO. -disable-model-invocation: true -argument-hint: ---- - -# DAO Scaffold - -Scaffold the complete directory structure and boilerplate for adding a new DAO. - -**Full guide:** `docs/ADDING_A_NEW_DAO.md` - -## Prerequisites - -Before starting, gather: - -- DAO name and governance type (OZ Governor+Timelock or Azorius) -- Governor/Azorius contract address -- Timelock/Treasury contract address -- Governance token address -- Chain (mainnet, arbitrum, etc.) -- Tally slug (from URL: `tally.xyz/gov/{slug}`) -- Proposer address with enough tokens -- 10+ voter addresses that together achieve quorum - -## Steps - -### 1. Create directory structure - -```bash -mkdir -p src/$ARGUMENTS/interfaces src/$ARGUMENTS/helpers src/$ARGUMENTS/proposals -``` - -### 2. Add remapping - -Append to `remappings.txt`: - -``` -@$ARGUMENTS/=src/$ARGUMENTS/ -``` - -### 3. Extract interfaces - -```bash -cast interface GOVERNOR_ADDRESS --chain mainnet -n IGovernor > src/$ARGUMENTS/interfaces/IGovernor.sol -cast interface TIMELOCK_ADDRESS --chain mainnet -n ITimelock > src/$ARGUMENTS/interfaces/ITimelock.sol -cast interface TOKEN_ADDRESS --chain mainnet -n IToken > src/$ARGUMENTS/interfaces/IToken.sol -``` - -Clean up: add SPDX header, set `pragma solidity >=0.8.25 <0.9.0;`, keep only needed functions. - -### 4. Write base test class - -For **Governor+Timelock DAOs**: use `src/ens/ens.t.sol` as the template. For **Azorius DAOs**: use -`src/shutter/shutter.t.sol` as the template. - -Create `src/$ARGUMENTS/$ARGUMENTS.t.sol` inheriting `CalldataComparison` from `@contracts/base/CalldataComparison.sol`. - -### 5. Add to DAO registry - -Add a new key under `daos` in `src/dao-registry.json`. See `docs/ADDING_A_NEW_DAO.md` for the full schema. - -### 6. Verify - -```bash -forge build --skip script -``` - -### 7. Validate with first proposal - -Find a recently executed proposal on Tally, fetch its data, write a test, verify it passes. - -### 8. Commit - -```bash -git add src/$ARGUMENTS/ remappings.txt src/dao-registry.json -git commit -m "feat($ARGUMENTS): scaffold DAO governance test infrastructure" -``` diff --git a/.claude/skills/proposal-review/SKILL.md b/.claude/skills/proposal-review/SKILL.md deleted file mode 100644 index 363aaac..0000000 --- a/.claude/skills/proposal-review/SKILL.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -name: proposal-review -description: - Review a DAO governance proposal. Use when the user shares a Tally URL (live or draft), asks to review a proposal, or - wants to verify calldata. Detects the phase automatically from the URL and runs the full review workflow. -argument-hint: [TALLY_URL] ---- - -# Proposal Review - -End-to-end calldata security review of a DAO governance proposal. - -**Input:** $ARGUMENTS - -## Critical Objective - -This system tests proposals that control millions/billions of dollars in DAO treasuries. A false positive (approving bad -calldata) is the **worst possible outcome**. - -- Build `_generateCallData()` from **manual derivation** of proposal intent and Solidity interfaces. -- **Never** copy from `proposalCalldata.json`. It is validation, not source. -- No opaque hex blobs. Every selector from `Interface.method.selector`. Every address from a named constant. -- The calldata comparison (`_compareLiveCalldata`) validates your manual derivation against the JSON. If they mismatch, - **stop — this is a security finding**. -- Both `_beforeProposal()` and `_afterExecution()` must contain substantive state checks. Empty hooks are never - acceptable. - -## Step 1: Detect Phase - -Parse the Tally URL to determine the review phase: - -| URL Pattern | Phase | What It Means | -| --------------------- | ------------- | -------------------------------------------------------- | -| Contains `/proposal/` | **Live** | Proposal is on-chain, submitted to the Governor | -| Contains `/draft/` | **Draft** | Proposal exists as a Tally draft, not yet on-chain | -| No URL provided | **Pre-draft** | Proposal is being discussed/designed, no Tally entry yet | - -## Step 2: Follow Phase-Specific Workflow - -Based on the detected phase, read and follow the corresponding workflow file: - -- **Live:** Read [live-review.md](live-review.md) — fetch on-chain data, update test, verify calldata matches -- **Draft:** Read [draft-review.md](draft-review.md) — fetch draft data, write test, verify calldata matches -- **Pre-draft:** Read [pre-draft-review.md](pre-draft-review.md) — create test from proposal spec, verify execution - -Each workflow file has the complete step-by-step process for that phase. - -## Step 3: Verify Assertions - -Before publishing any approval, verify the assertion baseline. Read [assertion-baseline.md](assertion-baseline.md) for: - -- What `_beforeProposal()` must contain -- What `_afterExecution()` must contain -- Required assertion patterns per proposal type -- Anti-patterns to avoid - -## Step 4: Produce Security Report - -After the test passes, produce a structured report: - -1. **Proposal Summary** — What it does (1-3 sentences) -2. **Calldata Verification** — PASS/FAIL per executable call with target and selector -3. **Assertion Results** — What `_beforeProposal()` and `_afterExecution()` checked -4. **Findings** — CRITICAL / IMPORTANT / INFO -5. **Recommendation** — APPROVE / REJECT / NEEDS_REVIEW -6. **Reproduction** — `git clone` + `forge test` commands - -## Reference Data - -For key addresses, helpers, inherited state, selectors, and troubleshooting, see [reference.md](reference.md). - -## Fetch Scripts - -The skill bundles two fetch scripts: - -```bash -# Fetch live proposal data (auto-detects DAO from Tally URL) -node ${CLAUDE_SKILL_DIR}/scripts/fetchLiveProposal.js - -# Fetch draft proposal data -node ${CLAUDE_SKILL_DIR}/scripts/fetchTallyDraft.js -``` diff --git a/.claude/skills/proposal-review/assertion-baseline.md b/.claude/skills/proposal-review/assertion-baseline.md deleted file mode 100644 index 296d0d6..0000000 --- a/.claude/skills/proposal-review/assertion-baseline.md +++ /dev/null @@ -1,41 +0,0 @@ -# Minimum Assertion Baseline - -Every proposal test MUST include meaningful assertions in both `_beforeProposal()` and `_afterExecution()`. Passing -execution alone is not sufficient — assertions prove the proposal achieves its stated intent and does not silently -mis-configure state. - -## `_beforeProposal()` — capture and verify pre-state - -At minimum: - -1. **Snapshot values that will change.** Store balances, owners, config values, or permissions into state variables so - `_afterExecution()` can compare against them. -2. **Assert preconditions hold.** If the proposal assumes the timelock owns a contract, assert that. If it assumes a - permission does NOT exist yet, assert that too. This catches stale fork blocks or incorrect assumptions. -3. **For permission proposals:** exercise permissions that will be revoked (they should succeed) and attempt permissions - that will be added (they should revert). This creates a before/after proof. - -## `_afterExecution()` — verify every claimed effect - -At minimum, assert **one check per executable call** in the proposal. The categories below are not exhaustive but cover -the most common proposal types: - -| Proposal type | Required assertions | -| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Token transfer** | Recipient balance increased by exact amount. Sender balance decreased by exact amount (or at least changed). Use `assertEq` on the delta, not just `assertNotEq`. | -| **Ownership / registry change** | New owner or registry value matches expected address. Old value no longer applies. | -| **Permission grant (Zodiac Roles, access control)** | The granted action succeeds when executed by the authorized actor. A similar action with wrong parameters reverts (negative test). | -| **Permission revocation** | The revoked action reverts with the expected error selector. | -| **Configuration change** (flow rates, parameters, upgrades) | New config value matches expected. Old value no longer returned. | -| **Contract deployment / upgrade** | New contract address is non-zero. Key interface calls return expected values (e.g., `owner()`, `version()`). | -| **ENS name operations** | `ensRegistry.owner(namehash(...))` or resolver returns expected value. | - -## Anti-patterns to avoid - -- **Empty hooks.** `_beforeProposal() {}` or `_afterExecution() {}` is never acceptable for a review to be published. -- **Only `assertNotEq`.** This proves something changed but not that it changed correctly. Always pair with an - `assertEq` on the expected value. -- **No negative tests for permission changes.** If a proposal grants scoped permissions, test that out-of-scope - parameters revert. If it revokes permissions, test that the revoked action fails. - -These assertions carry forward from pre-draft through draft to live stages. Writing them early saves rework later. diff --git a/.claude/skills/proposal-review/draft-review.md b/.claude/skills/proposal-review/draft-review.md deleted file mode 100644 index 40e7a86..0000000 --- a/.claude/skills/proposal-review/draft-review.md +++ /dev/null @@ -1,181 +0,0 @@ -# Draft Calldata Review - -Use this workflow when a proposal exists as a **Tally draft** (URL contains `/draft/`). This covers fetching the draft -data, writing or updating the test, and verifying calldata. - -## 1. Create Branch (if new) - -```bash -git checkout -b ens/ep-topic-name -``` - -If continuing from a pre-draft, use the existing branch. - -## 2. Fetch Draft Proposal Data - -```bash -node ${CLAUDE_SKILL_DIR}/scripts/fetchTallyDraft.js -``` - -Examples: - -```bash -node ${CLAUDE_SKILL_DIR}/scripts/fetchTallyDraft.js https://www.tally.xyz/gov/ens/draft/2786603872288769996 src/ens/proposals/ep-topic-name -node ${CLAUDE_SKILL_DIR}/scripts/fetchTallyDraft.js 2786603872288769996 src/ens/proposals/ep-topic-name -``` - -This creates: - -- `proposalCalldata.json` — executable calls from the draft -- `proposalDescription.md` — proposal description - -## 3. Write or Update Test File - -Create `calldataCheck.t.sol` (or update the existing one from the pre-draft phase). - -### Inherited State from `ENS_Governance` - -The base contract (`src/ens/ens.t.sol`) provides these variables — do NOT redeclare them: - -| Variable | Type | Address | Notes | -| ------------------------------------------------------------- | ----------- | -------------------------------------------- | ---------------------------------- | -| `ensToken` | `IENSToken` | `0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72` | ENS governance token | -| `governor` | `IGovernor` | `0x323A76393544d5ecca80cd6ef2A560C6a395b7E3` | ENS Governor contract | -| `timelock` | `ITimelock` | `0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7` | ENS Timelock (= wallet.ensdao.eth) | -| `proposer` | `address` | Set by `_proposer()` | Proposal submitter | -| `voters` | `address[]` | Set by `_voters()` | Default voter set with quorum | -| `targets`, `values`, `signatures`, `calldatas`, `description` | — | — | Proposal parameters | - -**Important**: Use `address(timelock)` instead of hardcoding the timelock/wallet address. - -### Template - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.25 <0.9.0; - -import { ENS_Governance } from "@ens/ens.t.sol"; -// Import relevant interfaces from @ens/interfaces/ - -contract Proposal_ENS_EP_Topic_Name_Draft_Test is ENS_Governance { - - function _selectFork() public override { - vm.createSelectFork({ blockNumber: RECENT_BLOCK, urlOrAlias: "mainnet" }); - } - - function _proposer() public pure override returns (address) { - return PROPOSER_ADDRESS; // From Tally draft - } - - function _beforeProposal() public override { - // Capture state before execution — see assertion-baseline.md - } - - function _generateCallData() - public - override - returns ( - address[] memory, - uint256[] memory, - string[] memory, - bytes[] memory, - string memory - ) - { - uint256 numTransactions = N; - - targets = new address[](numTransactions); - values = new uint256[](numTransactions); - calldatas = new bytes[](numTransactions); - signatures = new string[](numTransactions); - - // Reconstruct calldata manually from spec + interfaces. - // Result must then match proposalCalldata.json. - targets[0] = TARGET_ADDRESS; - calldatas[0] = abi.encodeWithSelector(...); - values[0] = 0; - signatures[0] = ""; - - description = getDescriptionFromMarkdown(); - - return (targets, values, signatures, calldatas, description); - } - - function _afterExecution() public override { - // Assert expected state changes — see assertion-baseline.md - } - - function _isProposalSubmitted() public pure override returns (bool) { - return false; // Draft — not yet on-chain - } - - function dirPath() public pure override returns (string memory) { - return "src/ens/proposals/ep-topic-name"; - } -} -``` - -### What changes from pre-draft - -| Field | Pre-draft | Draft | -| ------------- | --------------------- | ----------------------------------------------------------------------------------------------- | -| `description` | Hardcoded placeholder | `getDescriptionFromMarkdown()` | -| `dirPath()` | `""` | `"src/ens/proposals/ep-topic-name"` **(MANDATORY — comparison is silently skipped without it)** | -| `_proposer()` | Default | From Tally draft | - -> **WARNING**: If `dirPath()` returns `""` and `proposalCalldata.json` exists, the calldata comparison is silently -> skipped — the test will pass without verifying calldata. This is a false positive risk. Always set `dirPath()` for -> draft reviews. - -### What the test does - -1. Simulates the full governance lifecycle (propose -> vote -> queue -> execute) -2. Runs `_beforeProposal()` and `_afterExecution()` assertions -3. Compares manually generated calldata against `proposalCalldata.json` - -**If step 3 fails, do not approve the proposal calldata. Report the mismatch as a finding.** This is a security check, -not a flaky test — treat any mismatch as critical until investigated. - -## 4. Run Test - -```bash -forge test --match-path "src/ens/proposals/ep-topic-name/*" -vv -``` - -## 5. Commit and PR - -```bash -git add src/ens/proposals/ep-topic-name/ -git commit -m "chore(ens): add draft calldata review for EP X.Y — topic-name" -git push origin ens/ep-topic-name -``` - -Open PR targeting `main`. Merge after review. - -## 6. Post to Forum - -```markdown -## Draft proposal calldata security review - -The calldata draft executes successfully and achieves the expected outcome of the proposal. All simulations and tests -are available -[here](https://github.com/blockful/dao-proposals/blob/COMMIT_HASH/src/ens/proposals/ep-topic-name/calldataCheck.t.sol). - -To verify locally: - -1. Clone: `git clone https://github.com/blockful/dao-proposals.git` -2. Checkout: `git checkout SHORT_HASH` -3. Run: `forge test --match-path "src/ens/proposals/ep-topic-name/*" -vv` -``` - -## 7. Transitioning to Live - -When the proposal is submitted on-chain, re-run `/proposal-review` with the live Tally URL. Changes needed: - -1. Rename directory to `ep-X-Y` if it now has a number -2. Fetch live data with the live URL -3. Update `_isProposalSubmitted()` to return `true` -4. Update `_selectFork()` with the proposal creation block from `proposalCalldata.json` -5. Update `_proposer()` with the on-chain proposer -6. Update `dirPath()` if the directory was renamed -7. Fix the description if needed (see [troubleshooting.md](troubleshooting.md)) diff --git a/.claude/skills/proposal-review/live-review.md b/.claude/skills/proposal-review/live-review.md deleted file mode 100644 index 9a5fcd1..0000000 --- a/.claude/skills/proposal-review/live-review.md +++ /dev/null @@ -1,127 +0,0 @@ -# Live Calldata Review - -Use this workflow when a proposal is **live on-chain** (submitted to the ENS Governor). This covers fetching the -on-chain data, updating the test, and verifying the live calldata. - -## 1. Create Branch (if new) - -```bash -git checkout -b ens/ep-X-Y -``` - -If continuing from a draft, rename the directory first: - -```bash -mv src/ens/proposals/ep-topic-name src/ens/proposals/ep-X-Y -``` - -## 2. Fetch Live Proposal Data - -```bash -node ${CLAUDE_SKILL_DIR}/scripts/fetchLiveProposal.js -``` - -Examples: - -```bash -node ${CLAUDE_SKILL_DIR}/scripts/fetchLiveProposal.js https://www.tally.xyz/gov/ens/proposal/10731397... src/ens/proposals/ep-6-32 -node ${CLAUDE_SKILL_DIR}/scripts/fetchLiveProposal.js 107313977323541760723614084561841045035159333942448750767795024713131429640046 src/ens/proposals/ep-6-32 -``` - -This overwrites: - -- `proposalCalldata.json` — executable calls with block info -- `proposalDescription.md` — proposal description - -**Important**: The description from Tally may differ from the on-chain description (trailing whitespace, encoding). If -the test fails with "Governor: unknown proposal id", see [troubleshooting.md](troubleshooting.md). - -## 3. Update Test File - -Update the existing `calldataCheck.t.sol` with these changes: - -### What changes from draft to live - -| Field | Draft | Live | -| ------------------------ | --------------- | ------------------------------------------------- | -| `_isProposalSubmitted()` | `false` | `true` | -| `_selectFork()` | Recent block | Proposal creation block (from JSON `blockNumber`) | -| `_proposer()` | Draft proposer | On-chain proposer (from Tally) | -| `dirPath()` | May need update | `"src/ens/proposals/ep-X-Y"` | -| Contract name | `_Draft_Test` | `_Test` | - -### Template - -```solidity -contract Proposal_ENS_EP_X_Y_Test is ENS_Governance { - - function _selectFork() public override { - // Use blockNumber from proposalCalldata.json - vm.createSelectFork({ blockNumber: BLOCK_NUMBER, urlOrAlias: "mainnet" }); - } - - function _proposer() public pure override returns (address) { - return PROPOSER_ADDRESS; // On-chain proposer - } - - // ... _beforeProposal, _generateCallData, _afterExecution stay the same ... - - function _isProposalSubmitted() public pure override returns (bool) { - return true; // Live proposal - } - - function dirPath() public pure override returns (string memory) { - return "src/ens/proposals/ep-X-Y"; - } -} -``` - -### What the test does - -1. Computes `proposalId` from the generated calldata + description hash -2. Verifies the proposal exists on-chain (if the hash doesn't match, you get "Governor: unknown proposal id") -3. Simulates voting, queuing, and execution -4. Runs `_beforeProposal()` and `_afterExecution()` assertions -5. Compares manually generated calldata against `proposalCalldata.json` - -**If step 5 fails, do not approve the proposal calldata. Report the mismatch as a finding.** - -## 4. Run Test - -```bash -forge test --match-path "src/ens/proposals/ep-X-Y/*" -vv -``` - -## 5. Commit and PR - -```bash -git add src/ens/proposals/ep-X-Y/ -git commit -m "test(ens): EP X.Y — update to live proposal" -git push origin ens/ep-X-Y -``` - -Open PR targeting `main`. Merge after review. - -## 6. Post to Forum - -```markdown -## Live proposal calldata security verification - -This proposal is finally [live](https://anticapture.com/ens/governance/proposal/ONCHAIN_ID)! - -Calldata executed the expected outcome. The simulation and tests of the **live** proposal can be found -[here](https://github.com/blockful/dao-proposals/blob/COMMIT_HASH/src/ens/proposals/ep-X-Y/calldataCheck.t.sol). - -To verify locally: - -1. Clone: `git clone https://github.com/blockful/dao-proposals.git` -2. Checkout: `git checkout SHORT_HASH` -3. Run: `forge test --match-path "src/ens/proposals/ep-X-Y/*" -vv` -``` - -Replace: - -- `ONCHAIN_ID` — the on-chain proposal ID (from `proposalCalldata.json`) -- `COMMIT_HASH` — full commit hash from the merged PR -- `SHORT_HASH` — first 7 characters -- `ep-X-Y` — the proposal number diff --git a/.claude/skills/proposal-review/pre-draft-review.md b/.claude/skills/proposal-review/pre-draft-review.md deleted file mode 100644 index 54477f2..0000000 --- a/.claude/skills/proposal-review/pre-draft-review.md +++ /dev/null @@ -1,155 +0,0 @@ -# Pre-Draft Proposal Review - -Use this workflow when a proposal is being **discussed or designed** but has no Tally draft yet. This covers deploying -custom contracts, testing an idea, or building calldata before it goes to Tally. - -## 1. Create Branch - -```bash -git checkout -b ens/ep-topic-name -``` - -Use a descriptive name (e.g., `ens/ep-registrar-manager-endowment`, `ens/ep-enable-security-controllers`). - -## 2. Create Proposal Directory - -```bash -mkdir -p src/ens/proposals/ep-topic-name -``` - -No `proposalCalldata.json` or `proposalDescription.md` yet — those come later when the draft is created on Tally. - -## 3. (Optional) Add Custom Contracts - -If the proposal deploys new contracts, place them in a `contracts/` subdirectory: - -``` -src/ens/proposals/ep-topic-name/ - contracts/ - MyContract.sol - MyContract.t.sol # Unit tests for the contract - calldataCheck.t.sol # Proposal governance test -``` - -## 4. Write Test File - -Create `calldataCheck.t.sol` extending `ENS_Governance`. - -### Inherited State from `ENS_Governance` - -The base contract (`src/ens/ens.t.sol`) provides these variables via `setUp()` — do NOT redeclare them: - -| Variable | Type | Address | Notes | -| ------------------------------------------------------------- | ----------- | -------------------------------------------- | ---------------------------------- | -| `ensToken` | `IENSToken` | `0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72` | ENS governance token | -| `governor` | `IGovernor` | `0x323A76393544d5ecca80cd6ef2A560C6a395b7E3` | ENS Governor contract | -| `timelock` | `ITimelock` | `0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7` | ENS Timelock (= wallet.ensdao.eth) | -| `proposer` | `address` | Set by `_proposer()` | Proposal submitter | -| `voters` | `address[]` | Set by `_voters()` | Default voter set with quorum | -| `targets`, `values`, `signatures`, `calldatas`, `description` | — | — | Proposal parameters | - -**Important**: `address(timelock)` is `wallet.ensdao.eth` (`0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7`). - -### Template - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.25 <0.9.0; - -import { ENS_Governance } from "@ens/ens.t.sol"; -// Import relevant interfaces / custom contracts - -contract Proposal_ENS_EP_Topic_Name_Test is ENS_Governance { - - function setUp() public override { - super.setUp(); - // Deploy custom contracts here if needed - } - - function _selectFork() public override { - vm.createSelectFork({ blockNumber: RECENT_BLOCK, urlOrAlias: "mainnet" }); - } - - function _proposer() public pure override returns (address) { - return 0x5BFCB4BE4d7B43437d5A0c57E908c048a4418390; // fireeyesdao.eth (default) - } - - function _beforeProposal() public override { - // Capture state before execution — see assertion-baseline.md - } - - function _generateCallData() - public - override - returns ( - address[] memory, - uint256[] memory, - string[] memory, - bytes[] memory, - string memory - ) - { - uint256 numTransactions = N; - - targets = new address[](numTransactions); - values = new uint256[](numTransactions); - calldatas = new bytes[](numTransactions); - signatures = new string[](numTransactions); - - // Build transactions manually from interfaces — NO hex blobs - targets[0] = TARGET_ADDRESS; - calldatas[0] = abi.encodeWithSelector(IContract.method.selector, args); - values[0] = 0; - signatures[0] = ""; - - description = "Pre-draft: proposal description TBD"; - - return (targets, values, signatures, calldatas, description); - } - - function _afterExecution() public override { - // Assert expected state changes — see assertion-baseline.md - } - - function _isProposalSubmitted() public pure override returns (bool) { - return false; - } - - function dirPath() public pure override returns (string memory) { - return ""; // No JSON/md files yet — skip calldata comparison - } -} -``` - -### Key Points - -- `_isProposalSubmitted()` returns `false` — the test will submit the proposal via `governor.propose()` -- `dirPath()` returns `""` — no `proposalCalldata.json` exists yet, so calldata comparison is skipped -- `description` is a placeholder — it will be replaced when the draft goes to Tally -- Use `setUp()` override with `super.setUp()` to deploy custom contracts -- All selectors derived from interfaces (`.selector`), never hardcoded hex - -## 5. Run Test - -```bash -forge test --match-contract Proposal_ENS_EP_Topic_Name_Test -vvv -``` - -## 6. Commit - -```bash -git add src/ens/proposals/ep-topic-name/ -git commit -m "chore(ens): add pre-draft calldata review for EP topic-name" -git push origin ens/ep-topic-name -``` - -## 7. Transitioning to Draft - -When the proposal is created on Tally, re-run `/proposal-review` with the draft URL. Changes needed: - -| Field | Pre-draft | Draft | -| ------------- | --------------------- | ----------------------------------------------------------- | -| `description` | Hardcoded placeholder | `getDescriptionFromMarkdown()` | -| `dirPath()` | `""` | `"src/ens/proposals/ep-topic-name"` | -| `_proposer()` | Default | From Tally draft | -| New files | None | `proposalCalldata.json`, `proposalDescription.md` (fetched) | diff --git a/.claude/skills/proposal-review/reference.md b/.claude/skills/proposal-review/reference.md deleted file mode 100644 index 16beb82..0000000 --- a/.claude/skills/proposal-review/reference.md +++ /dev/null @@ -1,65 +0,0 @@ -# Review Reference - -Shared reference data for all proposal calldata reviews. - -## ENS Key Addresses - -| Contract | Address | -| -------------------------------- | -------------------------------------------- | -| ENS Token | `0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72` | -| ENS Governor | `0x323A76393544d5ecca80cd6ef2A560C6a395b7E3` | -| ENS Timelock (wallet.ensdao.eth) | `0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7` | -| ENS Endowment Safe | `0x4F2083f5fBede34C2714aFfb3105539775f7FE64` | -| ENS Root | `0xaB528d626EC275E3faD363fF1393A41F581c5897` | -| ENS Registry | `0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e` | -| Zodiac Roles V2 | `0x703806E61847984346d2D7DDd853049627e50A40` | -| USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | -| Meta-Gov Multisig | `0x91c32893216dE3eA0a55ABb9851f581d4503d39b` | -| Ecosystem Multisig | `0x2686A8919Df194aA7673244549E68D42C1685d03` | -| Public Goods Multisig | `0xcD42b4c4D102cc22864e3A1341Bb0529c17fD87d` | - -For the full list, see `src/ens/Constants.sol` (ENSConstants library): - -```solidity -import { ENSConstants } from "@ens/Constants.sol"; -// Use: ENSConstants.USDC, ENSConstants.TIMELOCK, ENSConstants.META_GOV_MULTISIG, etc. -``` - -For other DAOs, see `src/dao-registry.json`. - -## Available Helpers - -| Helper | Import | When to Use | -| ------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `SafeHelper` | `@ens/helpers/SafeHelper.sol` | Proposal calls the Endowment Safe (`execTransaction`). Provides `endowmentSafe`, `_buildSafeExecCalldata()`, `_buildSafeExecDelegateCalldata()`. | -| `ZodiacRolesHelper` | `@ens/helpers/ZodiacRolesHelper.sol` | Proposal modifies Zodiac Roles permissions. Provides `roles`, `karpatkey`, `MANAGER_ROLE`, `_safeExecuteTransaction()`, `_expectConditionViolation()`. | -| `MultiSendHelper` | `@ens/helpers/MultiSendHelper.sol` | Proposal batches multiple Safe transactions via MultiSend. Provides `_packCall()`, `_buildSafeMultiSendCalldata()`. Extends SafeHelper. | - -### When to use which helper - -- **Proposal calls the Endowment Safe**: inherit `SafeHelper` -- **Proposal modifies Zodiac Roles permissions**: inherit `ZodiacRolesHelper` -- **Proposal does both**: inherit both -- **Proposal batches Safe calls via MultiSend**: inherit `MultiSendHelper` (extends SafeHelper) -- **Simple governance proposal** (token transfers, ENS registry ops): no helpers needed - -## Decimal Reference - -| Token | Decimals | Example | -| -------- | -------- | ----------------------------- | -| USDC | 6 | `900_000 * 10**6` = 900K USDC | -| USDT | 6 | `100_000 * 10**6` = 100K USDT | -| ETH/WETH | 18 | `1 ether` = 1 ETH | -| ENS | 18 | `100_000 * 10**18` = 100K ENS | - -## Common Function Selectors - -Always derive from interfaces: `IERC20.transfer.selector`, `IWETH.deposit.selector`, etc. Never hardcode hex bytes4 -values. - -| Selector | Function | -| ------------ | -------------------------------------------------------------------------------------------- | -| `0xa9059cbb` | `transfer(address,uint256)` | -| `0x095ea7b3` | `approve(address,uint256)` | -| `0x23b872dd` | `transferFrom(address,address,uint256)` | -| `0x6a761202` | `execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)` | diff --git a/.claude/skills/proposal-review/scripts/fetchLiveProposal.js b/.claude/skills/proposal-review/scripts/fetchLiveProposal.js deleted file mode 100644 index fd5abf3..0000000 --- a/.claude/skills/proposal-review/scripts/fetchLiveProposal.js +++ /dev/null @@ -1,199 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const TALLY_API_URL = 'https://api.tally.xyz/query'; -const API_KEY = '365b418f59bd6dc4a0d7f23c2e8c12d982f156e9069695a6f0a2dcc3232448df'; - -// Build slug -> governorId lookup from dao-registry.json -function loadGovernorLookup() { - const registryPath = path.resolve(__dirname, '..', 'dao-registry.json'); - const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); - const lookup = {}; - for (const [key, dao] of Object.entries(registry.daos)) { - if (dao.tallySlug && dao.contracts.governor) { - lookup[dao.tallySlug] = `eip155:1:${dao.contracts.governor}`; - } - } - return lookup; -} - -function usage() { - console.log('Usage: node fetchLiveProposal.js '); - console.log(''); - console.log('Examples:'); - console.log(' node src/utils/fetchLiveProposal.js https://www.tally.xyz/gov/ens/proposal/10731397... src/ens/proposals/ep-6-32'); - console.log(' node src/utils/fetchLiveProposal.js https://www.tally.xyz/gov/uniswap/proposal/123... src/uniswap/proposals/proposal-123'); - console.log(' node src/utils/fetchLiveProposal.js 107313977323541760723614084561841045035159333942448750767795024713131429640046 src/ens/proposals/ep-6-32'); - process.exit(1); -} - -function parseOnchainId(input) { - // Accept full Tally proposal URL or raw on-chain ID - const urlMatch = input.match(/\/proposal\/(\d+)/); - if (urlMatch) return urlMatch[1]; - if (/^\d+$/.test(input)) return input; - console.error(`Error: Cannot parse on-chain proposal ID from "${input}"`); - process.exit(1); -} - -function extractSlugFromUrl(input) { - // Extract DAO slug from Tally URL: /gov//proposal/... - const slugMatch = input.match(/\/gov\/([^/]+)\/proposal\//); - if (slugMatch) return slugMatch[1]; - return null; -} - -function resolveGovernorId(input) { - const lookup = loadGovernorLookup(); - const slug = extractSlugFromUrl(input); - - if (slug) { - if (!lookup[slug]) { - console.error(`Error: DAO slug "${slug}" not found in dao-registry.json`); - console.error(`Available DAOs: ${Object.keys(lookup).join(', ')}`); - process.exit(1); - } - console.log(`Detected DAO: ${slug}`); - return { governorId: lookup[slug], slug }; - } - - // Raw numeric ID — fall back to ENS for backwards compatibility - console.log(`Detected DAO: ens (default — no slug in input)`); - return { governorId: lookup['ens'], slug: 'ens' }; -} - -async function fetchLiveProposal(onchainId, outputDir, governorId) { - const query = ` -query ProposalDetails($input: ProposalInput!) { - proposal(input: $input) { - id - onchainId - createdAt - block { number timestamp } - start { ... on Block { number timestamp } } - end { ... on Block { number timestamp } } - proposer { address name } - metadata { description } - executableCalls { value target calldata } - } -} -`; - - const variables = { - input: { - governorId: governorId, - onchainId: onchainId - } - }; - - try { - console.log(`Fetching live proposal ${onchainId.substring(0, 20)}... from Tally API...`); - - const response = await fetch(TALLY_API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Api-Key': API_KEY - }, - body: JSON.stringify({ query, variables }) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - if (data.errors) { - throw new Error(`GraphQL error: ${JSON.stringify(data.errors)}`); - } - - const proposal = data.data.proposal; - - if (!proposal) { - throw new Error(`Proposal not found for onchainId: ${onchainId}`); - } - - const executableCalls = proposal.executableCalls; - const description = proposal.metadata?.description || ''; - - if (!executableCalls || executableCalls.length === 0) { - throw new Error('No executable calls found in the proposal'); - } - - console.log(`Found ${executableCalls.length} executable call(s)`); - - // Log proposal information - console.log(`\nLive Proposal Information:`); - console.log(` Tally ID: ${proposal.id}`); - console.log(` Onchain ID: ${proposal.onchainId}`); - console.log(` Created at block: ${proposal.block?.number} (${proposal.block?.timestamp})`); - console.log(` Voting start block: ${proposal.start?.number} (${proposal.start?.timestamp})`); - console.log(` Voting end block: ${proposal.end?.number} (${proposal.end?.timestamp})`); - if (proposal.proposer) { - console.log(` Proposer: ${proposal.proposer.name || 'Unknown'} (${proposal.proposer.address})`); - } - - // Create the calldata structure - const calldataJson = { - proposalId: proposal.onchainId || onchainId, - blockNumber: proposal.block?.number, - votingStart: proposal.start?.number, - votingEnd: proposal.end?.number, - createdAt: proposal.createdAt, - executableCalls: executableCalls.map(call => ({ - target: call.target, - calldata: call.calldata, - value: call.value || "0" - })) - }; - - // Ensure output directory exists - const resolvedDir = path.resolve(outputDir); - fs.mkdirSync(resolvedDir, { recursive: true }); - - const jsonPath = path.join(resolvedDir, 'proposalCalldata.json'); - const mdPath = path.join(resolvedDir, 'proposalDescription.md'); - - fs.writeFileSync(jsonPath, JSON.stringify(calldataJson, null, 2)); - console.log(`\nWrote ${jsonPath}`); - - fs.writeFileSync(mdPath, description); - console.log(`Wrote ${mdPath}`); - - // Log each call for verification - executableCalls.forEach((call, index) => { - console.log(`\nCall ${index + 1}:`); - console.log(` Target: ${call.target}`); - console.log(` Value: ${call.value || "0"}`); - console.log(` Calldata: ${call.calldata.substring(0, 66)}...`); - }); - - console.log('\nDone!'); - console.log(`\nIMPORTANT: The description from Tally may differ from the on-chain description`); - console.log(`(trailing whitespace, encoding). If the test fails with "Governor: unknown proposal id",`); - console.log(`extract the exact description from the ProposalCreated event on-chain.`); - - } catch (error) { - console.error('Error fetching live proposal:', error.message); - process.exit(1); - } -} - -if (require.main === module) { - if (typeof fetch === 'undefined') { - console.error('This script requires Node.js 18+ for fetch support'); - process.exit(1); - } - - const args = process.argv.slice(2); - if (args.length < 2) usage(); - - const { governorId, slug } = resolveGovernorId(args[0]); - const onchainId = parseOnchainId(args[0]); - const outputDir = args[1]; - - fetchLiveProposal(onchainId, outputDir, governorId); -} - -module.exports = { fetchLiveProposal }; diff --git a/.claude/skills/proposal-review/scripts/fetchTallyDraft.js b/.claude/skills/proposal-review/scripts/fetchTallyDraft.js deleted file mode 100644 index 502a54b..0000000 --- a/.claude/skills/proposal-review/scripts/fetchTallyDraft.js +++ /dev/null @@ -1,151 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const TALLY_API_URL = 'https://api.tally.xyz/query'; -const API_KEY = '365b418f59bd6dc4a0d7f23c2e8c12d982f156e9069695a6f0a2dcc3232448df'; - -function usage() { - console.log('Usage: node fetchTallyDraft.js '); - console.log(''); - console.log('Examples:'); - console.log(' node src/utils/fetchTallyDraft.js https://www.tally.xyz/gov/ens/draft/2786603872288769996 src/ens/proposals/ep-6-32'); - console.log(' node src/utils/fetchTallyDraft.js 2786603872288769996 src/ens/proposals/ep-6-32'); - process.exit(1); -} - -function parseDraftId(input) { - // Accept full Tally draft URL or raw ID - const urlMatch = input.match(/\/draft\/(\d+)/); - if (urlMatch) return urlMatch[1]; - if (/^\d+$/.test(input)) return input; - console.error(`Error: Cannot parse draft ID from "${input}"`); - process.exit(1); -} - -async function fetchTallyDraft(draftId, outputDir) { - const query = ` -query Proposal { - proposal(input: {id: "${draftId}", isLatest: true}) { - id - createdAt - creator { - address - name - } - executableCalls { - target - calldata - value - } - metadata { - description - } - } -} -`; - - try { - console.log(`Fetching draft proposal ${draftId} from Tally API...`); - - const response = await fetch(TALLY_API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Api-Key': API_KEY - }, - body: JSON.stringify({ query }) - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`Response body: ${errorText}`); - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - if (data.errors) { - throw new Error(`GraphQL error: ${JSON.stringify(data.errors)}`); - } - - const proposal = data.data.proposal; - - if (!proposal) { - throw new Error(`Draft proposal ${draftId} not found`); - } - - const executableCalls = proposal.executableCalls; - const description = proposal.metadata?.description || ''; - - if (!executableCalls || executableCalls.length === 0) { - throw new Error('No executable calls found in the draft proposal'); - } - - console.log(`Found ${executableCalls.length} executable call(s)`); - - // Log draft information - console.log(`\nDraft Proposal Information:`); - console.log(` ID: ${proposal.id || draftId}`); - if (proposal.createdAt) console.log(` Created: ${proposal.createdAt}`); - if (proposal.creator) { - console.log(` Proposer: ${proposal.creator.name || proposal.creator.address || 'Unknown'}`); - console.log(` Address: ${proposal.creator.address || 'Unknown'}`); - } - - // Create the calldata structure - const calldataJson = { - proposalId: draftId, - type: 'draft', - executableCalls: executableCalls.map(call => ({ - target: call.target, - calldata: call.calldata, - value: call.value || "0", - signature: "" // Tally drafts don't include signatures - })) - }; - - // Ensure output directory exists - const resolvedDir = path.resolve(outputDir); - fs.mkdirSync(resolvedDir, { recursive: true }); - - const jsonPath = path.join(resolvedDir, 'proposalCalldata.json'); - const mdPath = path.join(resolvedDir, 'proposalDescription.md'); - - fs.writeFileSync(jsonPath, JSON.stringify(calldataJson, null, 2)); - console.log(`\nWrote ${jsonPath}`); - - fs.writeFileSync(mdPath, description); - console.log(`Wrote ${mdPath}`); - - // Log each call for verification - executableCalls.forEach((call, index) => { - console.log(`\nCall ${index + 1}:`); - console.log(` Target: ${call.target}`); - console.log(` Value: ${call.value || "0"}`); - console.log(` Calldata: ${call.calldata.substring(0, 66)}...`); - }); - - console.log('\nDone!'); - - } catch (error) { - console.error('Error fetching draft proposal:', error.message); - process.exit(1); - } -} - -if (require.main === module) { - if (typeof fetch === 'undefined') { - console.error('This script requires Node.js 18+ for fetch support'); - process.exit(1); - } - - const args = process.argv.slice(2); - if (args.length < 2) usage(); - - const draftId = parseDraftId(args[0]); - const outputDir = args[1]; - - fetchTallyDraft(draftId, outputDir); -} - -module.exports = { fetchTallyDraft }; diff --git a/.claude/skills/proposal-review/troubleshooting.md b/.claude/skills/proposal-review/troubleshooting.md deleted file mode 100644 index 100ffda..0000000 --- a/.claude/skills/proposal-review/troubleshooting.md +++ /dev/null @@ -1,50 +0,0 @@ -# Troubleshooting - -Common issues encountered during proposal calldata reviews. - -## Description Mismatch ("Governor: unknown proposal id") - -The description hash doesn't match the on-chain proposal. This happens when the Tally API returns a slightly different -description than what was submitted on-chain (e.g., trailing newline). - -**Fix**: Extract the exact on-chain description from the `ProposalCreated` event: - -```bash -# Get the event from the proposal creation block (blockNumber from proposalCalldata.json) -cast logs \ - --from-block BLOCK_NUMBER --to-block BLOCK_NUMBER \ - --address 0x323A76393544d5ecca80cd6ef2A560C6a395b7E3 \ - "ProposalCreated(uint256,address,address[],uint256[],string[],bytes[],uint256,uint256,string)" \ - --rpc-url mainnet -``` - -Then decode the description from the event data and overwrite `proposalDescription.md` with the exact bytes. - -## Calldata Mismatch - -Treat any mismatch as a **critical finding** until proven otherwise. Do not publish approval text while mismatch exists. - -1. Check decimal places (USDC: 6, ETH/ENS: 18). See [reference.md](reference.md) decimal table. -2. Verify address checksums -3. Ensure parameter order matches function signature -4. For drafts: the draft may have been updated on Tally — refetch with the same command - -## Stack Too Deep - -```bash -forge test --match-contract Proposal_ENS_EP_X_Y_Test --skip FileName -vvv -``` - -## Fork Block Issues - -Always use the `blockNumber` from `proposalCalldata.json` in `_selectFork()`. This ensures the fork is at the same state -as when the proposal was created. - -For pre-draft reviews (no JSON), use a recent mainnet block. - -## Compilation Errors - -- **Wrong pragma**: Must be `>=0.8.25 <0.9.0` -- **Import not found**: Check `remappings.txt` — ENS uses `@ens/`, Uniswap uses `@uniswap/` -- **Redeclared variable**: Do NOT redeclare `targets`, `values`, `calldatas`, `signatures`, `description`, `ensToken`, - `governor`, `timelock` — these are inherited from the base class