From 88f591e9ab929b525858a317b9fe879c65b632e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Tue, 27 Oct 2026 22:45:30 +0700 Subject: [PATCH 01/11] Add the DAO section: voting power, delegation, voting, proposals and verification on api3.eth --- README.md | 1 + docs/.vitepress/config.js | 6 ++++ docs/dao/delegation.md | 57 +++++++++++++++++++++++++++++++ docs/dao/index.md | 32 ++++++++++++++++++ docs/dao/proposals.md | 71 +++++++++++++++++++++++++++++++++++++++ docs/dao/sidebar.js | 26 ++++++++++++++ docs/dao/verification.md | 29 ++++++++++++++++ docs/dao/voting-power.md | 64 +++++++++++++++++++++++++++++++++++ docs/dao/voting.md | 33 ++++++++++++++++++ docs/index.md | 3 ++ 10 files changed, 322 insertions(+) create mode 100644 docs/dao/delegation.md create mode 100644 docs/dao/index.md create mode 100644 docs/dao/proposals.md create mode 100644 docs/dao/sidebar.js create mode 100644 docs/dao/verification.md create mode 100644 docs/dao/voting-power.md create mode 100644 docs/dao/voting.md diff --git a/README.md b/README.md index 98b4a80f..0e308172 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ New to API3 data feeds? Start with the [Quickstart](https://docs.api3.org/dapps/ - **[dApps](https://docs.api3.org/dapps/)**: activating a data feed on [API3 Market](https://market.api3.org), reading it from a contract through `Api3ReaderProxyV1`, integration and security considerations, and how OEV Rewards pay dApps for using the feeds. - **[OEV](https://docs.api3.org/oev/)**: what Oracle Extractable Value is, how API3 data feeds and OEV feeds work at the contract level, and how searchers use the public Signed APIs. - **[Curation](https://docs.api3.org/curation/)**: the Morpho vaults API3 curates, their roles and operations, risk management and disclosure. +- **[DAO](https://docs.api3.org/dao/)**: governance on the `api3.eth` Snapshot space, what counts as voting power, delegation, voting, posting a proposal and how a result is verified before execution. ## AI assistants diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 309cd20a..756255cd 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -84,6 +84,7 @@ module.exports = { '/dapps/': require('../dapps/sidebar.js'), '/oev/': require('../oev/sidebar.js'), '/curation/': require('../curation/sidebar.js'), + '/dao/': require('../dao/sidebar.js'), }, nav: nav(), }, @@ -107,5 +108,10 @@ function nav() { link: '/curation/', activeMatch: '/curation/.*', }, + { + text: 'DAO', + link: '/dao/', + activeMatch: '/dao/.*', + }, ]; } diff --git a/docs/dao/delegation.md b/docs/dao/delegation.md new file mode 100644 index 00000000..4dff510a --- /dev/null +++ b/docs/dao/delegation.md @@ -0,0 +1,57 @@ +--- +title: Delegation +pageHeader: DAO +outline: deep +--- + + + +# Delegation + +Delegation hands your voting power to another address, which votes with it. +There are two kinds, and they behave differently. + +| | Pool delegation | Snapshot delegation | +| -------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------ | +| Made in | The DAO dashboard | The [Delegates](https://snapshot.box/#/s:api3.eth/delegates) page of the space | +| What moves | Your stake in the pool | Everything that counts for you, stake delegated to you in the pool included | +| Follows a delegate who delegates on Snapshot | Yes | No | +| Changing it | A transaction, at most once a week | A transaction | + +## Pool delegation + +Pool delegation is made in the DAO dashboard and carries over for the stake only. +If you delegated your stake in the pool, it keeps voting with your delegate. +Your wallet balance, deposits, unstaking amount and collateral still vote with you. +The pool allows one delegation change a week. + +The dashboard dates from the Aragon-based DAO (see [Before Snapshot](/dao/#before-snapshot)), and the [legacy documentation](https://old-docs.api3.org/dao-members/dashboard/voting.html) describes delegating in it. + +## Snapshot delegation + +Snapshot delegation moves everything you hold to your delegate, stake that others delegated to you in the pool included. +You then vote with zero. +It is recorded in Snapshot's delegate registry on Ethereum mainnet, so setting, changing or clearing it is a transaction. + +A delegation can be set under `api3.eth` or under all spaces. +A delegation under `api3.eth` overrides one under all spaces. + +To delegate, open the [Delegates](https://snapshot.box/#/s:api3.eth/delegates) page of the space, connect your wallet, choose the delegate and sign the transaction. + +## Details that matter + +- Snapshot delegation does not chain. + If you delegate to someone who has delegated to a third person, your power stays with your delegate, who votes with it. + Only their own holdings move on. +- Pool delegation does pass through. + If you delegated your stake in the pool to someone who delegates on Snapshot, your stake follows them to their delegate. +- A pool delegation to someone who has since delegated inside the pool counts for nobody. + The pool gives a delegating user no voting power at all, received delegations included, and does not pass them on. + Re-delegate or undelegate in the pool to fix it. +- Voting yourself after delegating does nothing. + Your own power is zero while the delegation stands. + Clear it before the proposal's block to vote yourself. +- Changing or clearing a delegation counts from the next proposal's block. + A proposal already posted keeps the delegations as they were at its block. +- To put everything with one delegate, delegate on Snapshot. + Pool delegation alone leaves the rest of your holdings with you. diff --git a/docs/dao/index.md b/docs/dao/index.md new file mode 100644 index 00000000..b7ae422c --- /dev/null +++ b/docs/dao/index.md @@ -0,0 +1,32 @@ +--- +title: Overview +pageHeader: DAO +outline: deep +--- + + + +# DAO + +The API3 DAO governs through the Snapshot space [api3.eth](https://snapshot.box/#/s:api3.eth) on Ethereum mainnet. +Holders of the API3 token vote there, and the API3 multisigs execute a proposal once it has passed and its result has been verified. + +::: info 💡 Tip + +For quick reference, you can copy-paste [`llms-full.txt`](https://docs.api3.org/llms-full.txt) to your choice of AI assistant. + +::: + +## How it works + +- [Voting power](/dao/voting-power) is one vote per API3, counted in your wallet, in the DAO pool, during unstaking and as collateral in the Kabu API3/USDC market on Morpho, at the block a proposal names. +- [Delegation](/dao/delegation) hands your voting power to another address, either for your stake alone, inside the pool, or for everything you hold, on Snapshot. +- [Voting](/dao/voting) is a gasless signature on snapshot.box, For or Against, while the proposal is open. +- [Proposals](/dao/proposals) can be posted by anyone with 500,000 API3 of voting power, run for seven days and pass with at least 10,000,000 API3 in favour and more votes for than against. +- [Verification](/dao/verification) recomputes a result from the chain before the API3 multisigs execute it. + +## Before Snapshot + +The DAO moved its governance from the Aragon-based DAO contracts to Snapshot. +The DAO pool from that period remains in use: API3 staked in it still counts as voting power and pool delegations still move the stake, but staking is no longer needed to vote. +The [legacy documentation](https://old-docs.api3.org/dao-members/) describes the DAO pool, the dashboard and the governance contracts of the Aragon-based DAO. diff --git a/docs/dao/proposals.md b/docs/dao/proposals.md new file mode 100644 index 00000000..25e7ef1e --- /dev/null +++ b/docs/dao/proposals.md @@ -0,0 +1,71 @@ +--- +title: Proposals +pageHeader: DAO +outline: deep +--- + + + +# Proposals + +## Who can post + +Posting a proposal takes 500,000 API3 of voting power, held or delegated to you. +Snapshot checks it at the latest block when it receives the proposal, and the [verification](/dao/verification) checks it again at the proposal's snapshot block. + +## Posting + +Proposals are discussed on the [API3 Discord](https://discord.gg/api3dao). + +1. Open [New proposal](https://snapshot.box/#/s:api3.eth/create) in the space and connect the wallet that holds the voting power. +2. Write the title and the body. + The voting system is basic voting with the choices For and Against. +3. Leave the snapshot block at its default, the latest block (see [Snapshot block](#snapshot-block)). +4. Publish by signing. + Voting starts at once. + +To post from a Safe, open snapshot.box in the Safe app and collect the signatures within a day, since the proposal has to reach Snapshot within a day of the signed timestamp. +The author can delete their own proposal. + +## Snapshot block + +The author picks the block when posting, and Snapshot only checks that the block exists. +The DAO's rule is that the block is not later than the signed timestamp and at most 7,200 blocks, about a day, before the proposal was posted. +A proposal measured at a block outside this window is not verified and is not executed. +The signed timestamp has to precede Snapshot's receipt of the proposal by at most a day, since voting starts at the signed timestamp and a backdated signature would shorten the seven days. + +## Timing + +Voting starts when the proposal is posted, with no delay, and runs seven days. + +## Passing + +A proposal passes when at least 10,000,000 API3 was cast in favour and there are more votes for than against. + +Snapshot's quorum bar counts every vote cast, against included, while the DAO's rule counts the votes in favour only. +The bar on snapshot.box can therefore show the quorum as met before the rule is. +The [verification](/dao/verification) applies the rule. + +## Execution + +Within seven days of passing, the API3 multisigs verify the result and execute the proposal. + +A proposal that changes the rules of governance executes 30 days after passing, and a proposal that cancels such a change executes within seven days. +The rules of governance are the voting rules, the DAO's mandate, the charter, the voting power strategy, the space settings, the verification tool, the `api3.eth` name and which multisig holds the treasury. +Whether a proposal is a rule change follows from its content, not from anything the author declares. + +## Space settings + +The settings of the `api3.eth` space that carry these rules, as set on Snapshot: + +| Setting | Value | +| ------------------ | ------------------------- | +| Network | Ethereum mainnet | +| Voting type | Basic | +| Proposal threshold | 500,000 API3 | +| Voting delay | None | +| Voting period | 7 days | +| Quorum | 10,000,000 API3 in favour | + +Snapshot enforces the threshold, the voting type and the timing. +The quorum in favour, the majority and the 30 days of a rule change are checked by the API3 multisigs before executing, with the [verification](/dao/verification). diff --git a/docs/dao/sidebar.js b/docs/dao/sidebar.js new file mode 100644 index 00000000..2a3a443f --- /dev/null +++ b/docs/dao/sidebar.js @@ -0,0 +1,26 @@ +module.exports = [ + { + text: 'Overview', + link: '/dao/', + }, + { + text: 'Voting power', + link: '/dao/voting-power', + }, + { + text: 'Delegation', + link: '/dao/delegation', + }, + { + text: 'Voting', + link: '/dao/voting', + }, + { + text: 'Proposals', + link: '/dao/proposals', + }, + { + text: 'Verification', + link: '/dao/verification', + }, +]; diff --git a/docs/dao/verification.md b/docs/dao/verification.md new file mode 100644 index 00000000..0a4599cc --- /dev/null +++ b/docs/dao/verification.md @@ -0,0 +1,29 @@ +--- +title: Verification +pageHeader: DAO +outline: deep +--- + + + +# Verification + +The API3 multisigs execute only a result they recomputed from the chain, never one read off a website. +The [Snapshot voting verifier](https://github.com/api3dao/snapshot-voting-verifier) is the open source tool for that recomputation. +Anyone can run it on any proposal of the space, and its README says how. + +## What it establishes + +Given a proposal, the tool reads every vote the Snapshot hub lists and establishes, independently of Snapshot's own computation, that: + +- Every vote was signed by the voter it is listed under, names one of the proposal's choices and was cast while the vote was open. +- Every voter's power is what the [voting power](/dao/voting-power) rules give them at the proposal's block, delegations included. +- The tally follows from those powers. +- The proposal met the posting rules: the proposer's 500,000 API3 at the proposal's block, a snapshot block and a signed timestamp within their windows (see [Snapshot block](/dao/proposals#snapshot-block)), and seven days of voting with no delay. + +It then judges the result on what it recomputed: at least 10,000,000 API3 in favour and more votes for than against (see [Passing](/dao/proposals#passing)). +The run ends by saying whether the result is verified, and lists every problem it found. +A proposal that fails a check is not executed. + +The tool does not judge whether a proposal is a change of the rules of governance. +The multisig signers read that from the proposal's content (see [Execution](/dao/proposals#execution)). diff --git a/docs/dao/voting-power.md b/docs/dao/voting-power.md new file mode 100644 index 00000000..544ce419 --- /dev/null +++ b/docs/dao/voting-power.md @@ -0,0 +1,64 @@ +--- +title: Voting power +pageHeader: DAO +outline: deep +--- + + + +# Voting power + +One API3 is one vote, wherever it sits. +Staking is not needed to vote, and voting has no minimum. +Snapshot shows voting power under the symbol vpAPI3. + +## What counts + +At the block a proposal is measured at, your voting power is the sum of: + +- API3 in your wallet. +- API3 staked in the DAO pool, locked rewards included. + Stake you delegated inside the pool counts for your delegate instead, and stake delegated to you counts for you unless you delegated inside the pool yourself. +- API3 deposited in the pool but not staked, including API3 whose unstaking has completed but was not withdrawn yet. +- API3 scheduled for unstaking, during the waiting period. +- API3 posted as collateral in the Kabu API3/USDC market on Morpho. + +Nothing else counts: API3 on an exchange, in a liquidity pool, lent out, bridged to another chain or held in any other contract. + +[Snapshot delegation](/dao/delegation#snapshot-delegation) applies on top of this sum: a delegation moves all of it to the delegate. + +## When it is measured + +Each proposal names a block, chosen by its author when posting. +Voting power is read at that block and does not change for the rest of the vote. +Tokens bought, staked, moved or delegated after that block count from the next proposal on. +The block is at most 7,200 blocks, about a day, before the proposal was posted (see [Snapshot block](/dao/proposals#snapshot-block)). + +## How it is computed + +The space computes voting power with Snapshot's `with-delegation` strategy, which applies Snapshot delegation on top of five sources read on Ethereum mainnet. +The strategy is set in the [space settings](https://snapshot.box/#/s:api3.eth/settings) on Snapshot, and the [verification](/dao/verification) recomputes it from the chain. + +| Source | Contract | Read | +| ------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| API3 in the wallet | API3 token `0x0b38210ea11411557c13457D4dA7dC6ea731B88a` | The token balance | +| API3 staked | API3 pool `0x6dd655f10d4b9E242aE186D9050B68F725c76d76` | `userVotingPower`, the user's own shares plus the shares delegated to them in the pool, converted to API3 with `totalStake` over `totalShares` | +| API3 deposited | API3 pool | `users`, the `unstaked` field | +| API3 unstaking | API3 pool | `users`, the `unstakeAmount` field | +| API3 as Morpho collateral | Morpho `0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb` | `position`, the `collateral` field, in the Kabu API3/USDC market `0x70dfbd2705d10d5dfb4d89e07e2bb860b5a624ce2f97e209ab9f5308bf6c67a1` | + +Reading `userVotingPower` rather than the user's own stake is what makes pool delegation apply: the pool reports a delegating user's shares under the delegate and gives the delegating user zero. + +## Other cases + +- API3 held for you by an exchange or a custodian is in their wallet, not yours, and does not vote as yours. +- A Safe or another contract wallet has voting power like any wallet and votes by signing through the Safe app (see [Voting](/dao/voting)). +- Collateral on Morpho counts for the owner of the position, in the Kabu API3/USDC market only. + Borrowing against it changes nothing. +- An API3 multisig that pool delegations name votes with the stake delegated to it. +- The API3 multisig that holds the DAO's own API3 has its voting power delegated to a dead address and does not vote. + +## Checking your voting power + +Open a proposal on [snapshot.box](https://snapshot.box/#/s:api3.eth): the vote dialog shows your voting power at the proposal's block. +The [verification tool](/dao/verification) recomputes it from the chain for any address and block. diff --git a/docs/dao/voting.md b/docs/dao/voting.md new file mode 100644 index 00000000..8f49c50c --- /dev/null +++ b/docs/dao/voting.md @@ -0,0 +1,33 @@ +--- +title: Voting +pageHeader: DAO +outline: deep +--- + + + +# Voting + +Proposals of the API3 DAO are listed in the [api3.eth](https://snapshot.box/#/s:api3.eth) space on snapshot.box. +Voting opens when a proposal is posted and closes seven days later. + +## Casting a vote + +1. Open the proposal on snapshot.box and connect the wallet that holds the voting power. +2. Choose For or Against. +3. Sign the vote. + It is a signature, not a transaction, and costs no gas. + +Your vote appears in the proposal's vote list with the voting power it carries. +Votes are public while the proposal is open. + +## Rules to know + +- Your voting power is read at the block the proposal names, so tokens or delegations changed after that block do not count (see [Voting power](/dao/voting-power#when-it-is-measured)). +- Voting has no minimum. +- If you delegated on Snapshot, your own power is zero and voting yourself does nothing. + Clear the delegation before the proposal's block to vote yourself (see [Delegation](/dao/delegation)). +- Abstain is not on the ballot. + The ballot shows For and Against, the abstain option is hidden, and an abstain cast through the Snapshot API counts for nothing. +- A Safe or another contract wallet votes by signing through the Safe app. +- A proposal passes with at least 10,000,000 API3 in favour and more votes for than against (see [Proposals](/dao/proposals#passing)). diff --git a/docs/index.md b/docs/index.md index 267113b2..8b0b9f6b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,4 +19,7 @@ hero: - theme: brand text: About curation link: /curation/ + - theme: brand + text: About the DAO + link: /dao/ --- From 0b709cc69a6ab639c0c01f12807fd38ece7f7324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Tue, 27 Oct 2026 22:45:30 +0700 Subject: [PATCH 02/11] Check the DAO pages against the api3.eth space on the Snapshot hub --- .github/workflows/dao-space.yml | 25 ++++++ CONTRIBUTING.md | 6 ++ scripts/check-dao-space.js | 139 ++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 .github/workflows/dao-space.yml create mode 100644 scripts/check-dao-space.js diff --git a/.github/workflows/dao-space.yml b/.github/workflows/dao-space.yml new file mode 100644 index 00000000..8a49462d --- /dev/null +++ b/.github/workflows/dao-space.yml @@ -0,0 +1,25 @@ +name: Check the DAO pages against the Snapshot space + +on: + pull_request: + paths: + - 'docs/dao/**' + - 'scripts/check-dao-space.js' + - '.github/workflows/dao-space.yml' + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: '^24.18.0' + + - name: Compare the pages with the space + run: node scripts/check-dao-space.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ffe03ae..e9a380c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,12 @@ node ./libs/link-validator.js http://localhost:8082 ./docs/.vitepress/dist/ Hosts that block automated requests are listed in [`libs/link-validator-ignore.json`](./libs/link-validator-ignore.json). +The DAO pages repeat what the `api3.eth` space on Snapshot is set to: the contracts, methods and market its voting power strategy reads, and the proposal threshold, voting type, delay, period and quorum. [`scripts/check-dao-space.js`](./scripts/check-dao-space.js) reads the space from the Snapshot hub and fails when the pages differ from it. The [workflow](./.github/workflows/dao-space.yml) runs it on pull requests that touch `docs/dao` and once a week, so a settings change on Snapshot shows up as a failing run. Reproduce it locally with: + +```sh +node scripts/check-dao-space.js +``` + ## Issues and pull requests Check the existing issues before opening a new one. Open pull requests against `main` and reference the issue in the description, for example `Closes #1`. CI runs the format check, the build and the link validator on every pull request, and the preview workflow posts a link to a Firebase preview of the site. Merging to `main` deploys the live site. diff --git a/scripts/check-dao-space.js b/scripts/check-dao-space.js new file mode 100644 index 00000000..9a1dc763 --- /dev/null +++ b/scripts/check-dao-space.js @@ -0,0 +1,139 @@ +// Checks the DAO pages against the live api3.eth space on the Snapshot hub. +// The voting power page has to name every contract, method, field and market +// the space's strategies read, and nothing else, and the proposals page has to +// carry the threshold, voting type, delay, period and quorum the space is set to. +const fs = require('fs'); +const path = require('path'); + +const HUB = 'https://hub.snapshot.org/graphql'; +const SPACE = 'api3.eth'; +const daoDir = path.join(__dirname, '..', 'docs', 'dao'); + +const query = `{ + space(id: "${SPACE}") { + network + strategies { name network params } + voting { type delay period quorum } + validation { name params } + } +}`; + +async function fetchSpace() { + const response = await fetch(HUB, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ query }), + }); + if (!response.ok) { + throw new Error(`The hub returned ${response.status}`); + } + const { data, errors } = await response.json(); + if (errors) { + throw new Error(`The hub returned errors: ${JSON.stringify(errors)}`); + } + if (!data.space) { + throw new Error(`The hub has no space ${SPACE}`); + } + return data.space; +} + +// Every value a strategy reads: contract addresses, bytes32 arguments such as +// a market id, method names and output fields. +function collectReads(value, reads = new Set()) { + if (Array.isArray(value)) { + value.forEach((item) => collectReads(item, reads)); + } else if (value && typeof value === 'object') { + for (const [key, item] of Object.entries(value)) { + if (key === 'address' || key === 'output') { + reads.add(item); + } else if (key === 'methodABI') { + reads.add(item.name); + } else if (key === 'args') { + item.filter((arg) => /^0x[0-9a-fA-F]{64}$/.test(arg)).forEach((arg) => reads.add(arg)); + } else { + collectReads(item, reads); + } + } + } + return reads; +} + +function formatAmount(amount) { + return `${Number(amount).toLocaleString('en-US')} API3`; +} + +function formatDays(seconds) { + const days = seconds / 86400; + return `${Number.isInteger(days) ? days : days.toFixed(2)} days`; +} + +function escapeRegExp(text) { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function hasRow(page, setting, value) { + const row = new RegExp(`^\\|\\s*${escapeRegExp(setting)}\\s*\\|\\s*${escapeRegExp(value)}\\s*\\|\\s*$`, 'm'); + return row.test(page); +} + +async function main() { + const space = await fetchSpace(); + const problems = []; + const votingPowerPage = fs.readFileSync(path.join(daoDir, 'voting-power.md'), 'utf-8'); + const proposalsPage = fs.readFileSync(path.join(daoDir, 'proposals.md'), 'utf-8'); + + if (space.network !== '1') { + problems.push(`The space is on network ${space.network}, the pages say Ethereum mainnet`); + } + if (space.strategies.length !== 1 || space.strategies[0].name !== 'with-delegation') { + problems.push( + `The space runs ${space.strategies.map((strategy) => strategy.name).join(', ')}, the voting power page describes one with-delegation strategy` + ); + } + const sources = space.strategies[0]?.params?.strategies ?? []; + if (sources.length !== 5) { + problems.push(`The strategy reads ${sources.length} sources, the voting power page describes five`); + } + + const reads = collectReads(space.strategies); + for (const read of reads) { + if (!votingPowerPage.includes(read)) { + problems.push(`voting-power.md does not mention ${read}, which the space's strategies read`); + } + } + for (const hex of new Set(votingPowerPage.match(/0x[0-9a-fA-F]{40,}/g) ?? [])) { + if (!reads.has(hex)) { + problems.push(`voting-power.md lists ${hex}, which the space's strategies do not read`); + } + } + + if (space.validation.name !== 'basic') { + problems.push( + `The space validates proposals with ${space.validation.name}, the proposals page describes a basic threshold` + ); + } + const rows = { + 'Voting type': space.voting.type === 'basic' ? 'Basic' : String(space.voting.type), + 'Proposal threshold': formatAmount(space.validation.params?.minScore), + 'Voting delay': space.voting.delay ? formatDays(space.voting.delay) : 'None', + 'Voting period': formatDays(space.voting.period), + Quorum: `${formatAmount(space.voting.quorum)} in favour`, + }; + for (const [setting, value] of Object.entries(rows)) { + if (!hasRow(proposalsPage, setting, value)) { + problems.push(`proposals.md has no settings row "${setting}" with the value "${value}" the space is set to`); + } + } + + if (problems.length > 0) { + console.error(`The DAO pages differ from the ${SPACE} space on the hub:`); + problems.forEach((problem) => console.error(`- ${problem}`)); + process.exit(1); + } + console.log(`The DAO pages match the ${SPACE} space on the hub.`); +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +}); From 1a75dca69096f9fb9401dc4db7409e1b9dc4b39f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Tue, 27 Oct 2026 23:45:19 +0700 Subject: [PATCH 03/11] Fold delegation into voting power, name the legacy DAO, say voting is off-chain --- .github/workflows/dao-space.yml | 1 + docs/dao/delegation.md | 57 --------------------------------- docs/dao/index.md | 13 ++++---- docs/dao/sidebar.js | 4 --- docs/dao/verification.md | 10 ++++-- docs/dao/voting-power.md | 54 ++++++++++++++++++++++++++++++- docs/dao/voting.md | 2 +- 7 files changed, 69 insertions(+), 72 deletions(-) delete mode 100644 docs/dao/delegation.md diff --git a/.github/workflows/dao-space.yml b/.github/workflows/dao-space.yml index 8a49462d..854b639e 100644 --- a/.github/workflows/dao-space.yml +++ b/.github/workflows/dao-space.yml @@ -7,6 +7,7 @@ on: - 'scripts/check-dao-space.js' - '.github/workflows/dao-space.yml' schedule: + # Once a week, Monday 06:00 UTC - cron: '0 6 * * 1' workflow_dispatch: diff --git a/docs/dao/delegation.md b/docs/dao/delegation.md deleted file mode 100644 index 4dff510a..00000000 --- a/docs/dao/delegation.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Delegation -pageHeader: DAO -outline: deep ---- - - - -# Delegation - -Delegation hands your voting power to another address, which votes with it. -There are two kinds, and they behave differently. - -| | Pool delegation | Snapshot delegation | -| -------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------ | -| Made in | The DAO dashboard | The [Delegates](https://snapshot.box/#/s:api3.eth/delegates) page of the space | -| What moves | Your stake in the pool | Everything that counts for you, stake delegated to you in the pool included | -| Follows a delegate who delegates on Snapshot | Yes | No | -| Changing it | A transaction, at most once a week | A transaction | - -## Pool delegation - -Pool delegation is made in the DAO dashboard and carries over for the stake only. -If you delegated your stake in the pool, it keeps voting with your delegate. -Your wallet balance, deposits, unstaking amount and collateral still vote with you. -The pool allows one delegation change a week. - -The dashboard dates from the Aragon-based DAO (see [Before Snapshot](/dao/#before-snapshot)), and the [legacy documentation](https://old-docs.api3.org/dao-members/dashboard/voting.html) describes delegating in it. - -## Snapshot delegation - -Snapshot delegation moves everything you hold to your delegate, stake that others delegated to you in the pool included. -You then vote with zero. -It is recorded in Snapshot's delegate registry on Ethereum mainnet, so setting, changing or clearing it is a transaction. - -A delegation can be set under `api3.eth` or under all spaces. -A delegation under `api3.eth` overrides one under all spaces. - -To delegate, open the [Delegates](https://snapshot.box/#/s:api3.eth/delegates) page of the space, connect your wallet, choose the delegate and sign the transaction. - -## Details that matter - -- Snapshot delegation does not chain. - If you delegate to someone who has delegated to a third person, your power stays with your delegate, who votes with it. - Only their own holdings move on. -- Pool delegation does pass through. - If you delegated your stake in the pool to someone who delegates on Snapshot, your stake follows them to their delegate. -- A pool delegation to someone who has since delegated inside the pool counts for nobody. - The pool gives a delegating user no voting power at all, received delegations included, and does not pass them on. - Re-delegate or undelegate in the pool to fix it. -- Voting yourself after delegating does nothing. - Your own power is zero while the delegation stands. - Clear it before the proposal's block to vote yourself. -- Changing or clearing a delegation counts from the next proposal's block. - A proposal already posted keeps the delegations as they were at its block. -- To put everything with one delegate, delegate on Snapshot. - Pool delegation alone leaves the rest of your holdings with you. diff --git a/docs/dao/index.md b/docs/dao/index.md index b7ae422c..7d560c8d 100644 --- a/docs/dao/index.md +++ b/docs/dao/index.md @@ -8,8 +8,9 @@ outline: deep # DAO -The API3 DAO governs through the Snapshot space [api3.eth](https://snapshot.box/#/s:api3.eth) on Ethereum mainnet. -Holders of the API3 token vote there, and the API3 multisigs execute a proposal once it has passed and its result has been verified. +The API3 DAO governs through the Snapshot space [api3.eth](https://snapshot.box/#/s:api3.eth), where holders of the API3 token vote on proposals. +Voting is off-chain: a vote is a signed message that Snapshot collects, and the voting power behind it is read from Ethereum mainnet at the block the proposal names. +The API3 multisigs execute a proposal once it has passed and its result has been verified. ::: info 💡 Tip @@ -20,13 +21,13 @@ For quick reference, you can copy-paste [`llms-full.txt`](https://docs.api3.org/ ## How it works - [Voting power](/dao/voting-power) is one vote per API3, counted in your wallet, in the DAO pool, during unstaking and as collateral in the Kabu API3/USDC market on Morpho, at the block a proposal names. -- [Delegation](/dao/delegation) hands your voting power to another address, either for your stake alone, inside the pool, or for everything you hold, on Snapshot. + [Delegation](/dao/voting-power#delegation) hands it to another address, either for your stake alone, inside the pool, or for everything you hold, on Snapshot. - [Voting](/dao/voting) is a gasless signature on snapshot.box, For or Against, while the proposal is open. - [Proposals](/dao/proposals) can be posted by anyone with 500,000 API3 of voting power, run for seven days and pass with at least 10,000,000 API3 in favour and more votes for than against. - [Verification](/dao/verification) recomputes a result from the chain before the API3 multisigs execute it. ## Before Snapshot -The DAO moved its governance from the Aragon-based DAO contracts to Snapshot. -The DAO pool from that period remains in use: API3 staked in it still counts as voting power and pool delegations still move the stake, but staking is no longer needed to vote. -The [legacy documentation](https://old-docs.api3.org/dao-members/) describes the DAO pool, the dashboard and the governance contracts of the Aragon-based DAO. +The DAO moved its governance from the legacy DAO, built on Aragon contracts, to Snapshot. +The DAO pool of the legacy DAO remains in use: API3 staked in it still counts as voting power and pool delegations still move the stake, but staking is no longer needed to vote. +[Its documentation](https://old-docs.api3.org/dao-members/) describes the DAO pool, the legacy DAO dashboard and the governance contracts. diff --git a/docs/dao/sidebar.js b/docs/dao/sidebar.js index 2a3a443f..60de4cd4 100644 --- a/docs/dao/sidebar.js +++ b/docs/dao/sidebar.js @@ -7,10 +7,6 @@ module.exports = [ text: 'Voting power', link: '/dao/voting-power', }, - { - text: 'Delegation', - link: '/dao/delegation', - }, { text: 'Voting', link: '/dao/voting', diff --git a/docs/dao/verification.md b/docs/dao/verification.md index 0a4599cc..ff155753 100644 --- a/docs/dao/verification.md +++ b/docs/dao/verification.md @@ -8,9 +8,13 @@ outline: deep # Verification -The API3 multisigs execute only a result they recomputed from the chain, never one read off a website. -The [Snapshot voting verifier](https://github.com/api3dao/snapshot-voting-verifier) is the open source tool for that recomputation. -Anyone can run it on any proposal of the space, and its README says how. +The DAO relies on Snapshot to run its votes. +The space settings, the strategies that compute voting power and the hub that collects and tallies the votes are Snapshot's, and the DAO trusts them to work as described. +It verifies them all the same: before executing a proposal, the API3 multisigs recompute its result from the chain with the [Snapshot voting verifier](https://github.com/api3dao/snapshot-voting-verifier), never taking a result off a website. +The tool is open source, anyone can run it on any proposal of the space, and its README says how. + +The DAO's rules also go further than Snapshot's settings can express, such as counting only the votes in favour toward the quorum and the window for the snapshot block. +The verification enforces those too, so a proposal that Snapshot shows as passed can still fail a check and not be executed. ## What it establishes diff --git a/docs/dao/voting-power.md b/docs/dao/voting-power.md index 544ce419..a07af52b 100644 --- a/docs/dao/voting-power.md +++ b/docs/dao/voting-power.md @@ -25,7 +25,7 @@ At the block a proposal is measured at, your voting power is the sum of: Nothing else counts: API3 on an exchange, in a liquidity pool, lent out, bridged to another chain or held in any other contract. -[Snapshot delegation](/dao/delegation#snapshot-delegation) applies on top of this sum: a delegation moves all of it to the delegate. +[Snapshot delegation](#snapshot-delegation) applies on top of this sum: a delegation moves all of it to the delegate. ## When it is measured @@ -49,6 +49,58 @@ The strategy is set in the [space settings](https://snapshot.box/#/s:api3.eth/se Reading `userVotingPower` rather than the user's own stake is what makes pool delegation apply: the pool reports a delegating user's shares under the delegate and gives the delegating user zero. +## Delegation + +Delegation hands your voting power to another address, which votes with it. +There are two kinds, and they behave differently. + +| | Pool delegation | Snapshot delegation | +| -------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ | +| Made in | The legacy DAO dashboard | The [Delegates](https://snapshot.box/#/s:api3.eth/delegates) page of the space | +| What moves | Your stake in the pool | Everything that counts for you, stake delegated to you in the pool included | +| Follows a delegate who delegates on Snapshot | Yes | No | +| Changing it | A transaction, then a week before the next change | A transaction | + +### Pool delegation + +Pool delegation is made in the legacy DAO dashboard and carries over for the stake only. +If you delegated your stake in the pool, it keeps voting with your delegate. +Your wallet balance, deposits, unstaking amount and collateral still vote with you. + +The pool refuses a delegation change within a week of the last one. +After delegating, you wait a week before you can undelegate or change the delegate, and after undelegating, a week before you can delegate again. + +The dashboard dates from the legacy DAO (see [Before Snapshot](/dao/#before-snapshot)), and [its documentation](https://old-docs.api3.org/dao-members/dashboard/voting.html) describes delegating in it. + +### Snapshot delegation + +Snapshot delegation moves everything you hold to your delegate, stake that others delegated to you in the pool included. +You then vote with zero. +It is recorded in Snapshot's delegate registry on Ethereum mainnet, so setting, changing or clearing it is a transaction. + +A delegation can be set under `api3.eth` or under all spaces. +A delegation under `api3.eth` overrides one under all spaces. + +To delegate, open the [Delegates](https://snapshot.box/#/s:api3.eth/delegates) page of the space, connect your wallet, choose the delegate and sign the transaction. + +### Details that matter + +- Snapshot delegation does not chain. + If you delegate to someone who has delegated to a third person, your power stays with your delegate, who votes with it. + Only their own holdings move on. +- Pool delegation does pass through. + If you delegated your stake in the pool to someone who delegates on Snapshot, your stake follows them to their delegate. +- A pool delegation to someone who has since delegated inside the pool counts for nobody. + The pool gives a delegating user no voting power at all, received delegations included, and does not pass them on. + Re-delegate or undelegate in the pool to fix it. +- Voting yourself after delegating does nothing. + Your own power is zero while the delegation stands. + Clear it before the proposal's block to vote yourself. +- Changing or clearing a delegation counts from the next proposal's block. + A proposal already posted keeps the delegations as they were at its block. +- To put everything with one delegate, delegate on Snapshot. + Pool delegation alone leaves the rest of your holdings with you. + ## Other cases - API3 held for you by an exchange or a custodian is in their wallet, not yours, and does not vote as yours. diff --git a/docs/dao/voting.md b/docs/dao/voting.md index 8f49c50c..928a1c40 100644 --- a/docs/dao/voting.md +++ b/docs/dao/voting.md @@ -26,7 +26,7 @@ Votes are public while the proposal is open. - Your voting power is read at the block the proposal names, so tokens or delegations changed after that block do not count (see [Voting power](/dao/voting-power#when-it-is-measured)). - Voting has no minimum. - If you delegated on Snapshot, your own power is zero and voting yourself does nothing. - Clear the delegation before the proposal's block to vote yourself (see [Delegation](/dao/delegation)). + Clear the delegation before the proposal's block to vote yourself (see [Delegation](/dao/voting-power#delegation)). - Abstain is not on the ballot. The ballot shows For and Against, the abstain option is hidden, and an abstain cast through the Snapshot API counts for nothing. - A Safe or another contract wallet votes by signing through the Safe app. From 638be24110cc943f83c7b1158d0cf6583a78ad18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Wed, 28 Oct 2026 00:25:05 +0700 Subject: [PATCH 04/11] Fold proposals into voting, rename verification, drop the slogan and the legacy repetition --- docs/dao/index.md | 16 ++++----- docs/dao/proposals.md | 71 -------------------------------------- docs/dao/sidebar.js | 6 +--- docs/dao/verification.md | 10 +++--- docs/dao/voting-power.md | 26 +++++++------- docs/dao/voting.md | 64 +++++++++++++++++++++++++++++++--- scripts/check-dao-space.js | 8 ++--- 7 files changed, 90 insertions(+), 111 deletions(-) delete mode 100644 docs/dao/proposals.md diff --git a/docs/dao/index.md b/docs/dao/index.md index 7d560c8d..37210ea4 100644 --- a/docs/dao/index.md +++ b/docs/dao/index.md @@ -20,14 +20,14 @@ For quick reference, you can copy-paste [`llms-full.txt`](https://docs.api3.org/ ## How it works -- [Voting power](/dao/voting-power) is one vote per API3, counted in your wallet, in the DAO pool, during unstaking and as collateral in the Kabu API3/USDC market on Morpho, at the block a proposal names. +- [Voting power](/dao/voting-power) is counted per API3 in your wallet, in the DAO pool, during unstaking and as collateral in the Kabu API3/USDC market on Morpho, at the block a proposal names. [Delegation](/dao/voting-power#delegation) hands it to another address, either for your stake alone, inside the pool, or for everything you hold, on Snapshot. -- [Voting](/dao/voting) is a gasless signature on snapshot.box, For or Against, while the proposal is open. -- [Proposals](/dao/proposals) can be posted by anyone with 500,000 API3 of voting power, run for seven days and pass with at least 10,000,000 API3 in favour and more votes for than against. -- [Verification](/dao/verification) recomputes a result from the chain before the API3 multisigs execute it. +- [Voting](/dao/voting) is a gasless signature on snapshot.box, For or Against, during the seven days a proposal is open. + Anyone with 500,000 API3 of voting power can [post a proposal](/dao/voting#posting-a-proposal), and it passes with at least 10,000,000 API3 in favour and more votes for than against. +- [Independent verification](/dao/verification) recomputes a result from the chain before the API3 multisigs execute it. -## Before Snapshot +## Legacy DAO -The DAO moved its governance from the legacy DAO, built on Aragon contracts, to Snapshot. -The DAO pool of the legacy DAO remains in use: API3 staked in it still counts as voting power and pool delegations still move the stake, but staking is no longer needed to vote. -[Its documentation](https://old-docs.api3.org/dao-members/) describes the DAO pool, the legacy DAO dashboard and the governance contracts. +Governance used to run on Aragon contracts, with a staking pool and a dashboard for it. +The pool is still in use: stake in it counts as voting power and pool delegations still apply to the stake, but staking is no longer needed to vote. +The [legacy documentation](https://old-docs.api3.org/dao-members/) covers the pool, the dashboard and the contracts. diff --git a/docs/dao/proposals.md b/docs/dao/proposals.md deleted file mode 100644 index 25e7ef1e..00000000 --- a/docs/dao/proposals.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Proposals -pageHeader: DAO -outline: deep ---- - - - -# Proposals - -## Who can post - -Posting a proposal takes 500,000 API3 of voting power, held or delegated to you. -Snapshot checks it at the latest block when it receives the proposal, and the [verification](/dao/verification) checks it again at the proposal's snapshot block. - -## Posting - -Proposals are discussed on the [API3 Discord](https://discord.gg/api3dao). - -1. Open [New proposal](https://snapshot.box/#/s:api3.eth/create) in the space and connect the wallet that holds the voting power. -2. Write the title and the body. - The voting system is basic voting with the choices For and Against. -3. Leave the snapshot block at its default, the latest block (see [Snapshot block](#snapshot-block)). -4. Publish by signing. - Voting starts at once. - -To post from a Safe, open snapshot.box in the Safe app and collect the signatures within a day, since the proposal has to reach Snapshot within a day of the signed timestamp. -The author can delete their own proposal. - -## Snapshot block - -The author picks the block when posting, and Snapshot only checks that the block exists. -The DAO's rule is that the block is not later than the signed timestamp and at most 7,200 blocks, about a day, before the proposal was posted. -A proposal measured at a block outside this window is not verified and is not executed. -The signed timestamp has to precede Snapshot's receipt of the proposal by at most a day, since voting starts at the signed timestamp and a backdated signature would shorten the seven days. - -## Timing - -Voting starts when the proposal is posted, with no delay, and runs seven days. - -## Passing - -A proposal passes when at least 10,000,000 API3 was cast in favour and there are more votes for than against. - -Snapshot's quorum bar counts every vote cast, against included, while the DAO's rule counts the votes in favour only. -The bar on snapshot.box can therefore show the quorum as met before the rule is. -The [verification](/dao/verification) applies the rule. - -## Execution - -Within seven days of passing, the API3 multisigs verify the result and execute the proposal. - -A proposal that changes the rules of governance executes 30 days after passing, and a proposal that cancels such a change executes within seven days. -The rules of governance are the voting rules, the DAO's mandate, the charter, the voting power strategy, the space settings, the verification tool, the `api3.eth` name and which multisig holds the treasury. -Whether a proposal is a rule change follows from its content, not from anything the author declares. - -## Space settings - -The settings of the `api3.eth` space that carry these rules, as set on Snapshot: - -| Setting | Value | -| ------------------ | ------------------------- | -| Network | Ethereum mainnet | -| Voting type | Basic | -| Proposal threshold | 500,000 API3 | -| Voting delay | None | -| Voting period | 7 days | -| Quorum | 10,000,000 API3 in favour | - -Snapshot enforces the threshold, the voting type and the timing. -The quorum in favour, the majority and the 30 days of a rule change are checked by the API3 multisigs before executing, with the [verification](/dao/verification). diff --git a/docs/dao/sidebar.js b/docs/dao/sidebar.js index 60de4cd4..0d7e3b85 100644 --- a/docs/dao/sidebar.js +++ b/docs/dao/sidebar.js @@ -12,11 +12,7 @@ module.exports = [ link: '/dao/voting', }, { - text: 'Proposals', - link: '/dao/proposals', - }, - { - text: 'Verification', + text: 'Independent verification', link: '/dao/verification', }, ]; diff --git a/docs/dao/verification.md b/docs/dao/verification.md index ff155753..f02a8de1 100644 --- a/docs/dao/verification.md +++ b/docs/dao/verification.md @@ -1,12 +1,12 @@ --- -title: Verification +title: Independent verification pageHeader: DAO outline: deep --- -# Verification +# Independent verification The DAO relies on Snapshot to run its votes. The space settings, the strategies that compute voting power and the hub that collects and tallies the votes are Snapshot's, and the DAO trusts them to work as described. @@ -23,11 +23,11 @@ Given a proposal, the tool reads every vote the Snapshot hub lists and establish - Every vote was signed by the voter it is listed under, names one of the proposal's choices and was cast while the vote was open. - Every voter's power is what the [voting power](/dao/voting-power) rules give them at the proposal's block, delegations included. - The tally follows from those powers. -- The proposal met the posting rules: the proposer's 500,000 API3 at the proposal's block, a snapshot block and a signed timestamp within their windows (see [Snapshot block](/dao/proposals#snapshot-block)), and seven days of voting with no delay. +- The proposal met the posting rules: the proposer's 500,000 API3 at the proposal's block, a snapshot block and a signed timestamp within their windows (see [Snapshot block](/dao/voting#snapshot-block)), and seven days of voting with no delay. -It then judges the result on what it recomputed: at least 10,000,000 API3 in favour and more votes for than against (see [Passing](/dao/proposals#passing)). +It then judges the result on what it recomputed: at least 10,000,000 API3 in favour and more votes for than against (see [Passing](/dao/voting#passing)). The run ends by saying whether the result is verified, and lists every problem it found. A proposal that fails a check is not executed. The tool does not judge whether a proposal is a change of the rules of governance. -The multisig signers read that from the proposal's content (see [Execution](/dao/proposals#execution)). +The multisig signers read that from the proposal's content (see [Execution](/dao/voting#execution)). diff --git a/docs/dao/voting-power.md b/docs/dao/voting-power.md index a07af52b..2655b667 100644 --- a/docs/dao/voting-power.md +++ b/docs/dao/voting-power.md @@ -8,8 +8,9 @@ outline: deep # Voting power -One API3 is one vote, wherever it sits. -Staking is not needed to vote, and voting has no minimum. +Voting power is counted in API3: each API3 held in one of the places below is one vote. +Staking is not needed to vote, and any amount votes. +The 500,000 API3 threshold applies to posting a proposal, not to voting. Snapshot shows voting power under the symbol vpAPI3. ## What counts @@ -25,19 +26,21 @@ At the block a proposal is measured at, your voting power is the sum of: Nothing else counts: API3 on an exchange, in a liquidity pool, lent out, bridged to another chain or held in any other contract. -[Snapshot delegation](#snapshot-delegation) applies on top of this sum: a delegation moves all of it to the delegate. +[Snapshot delegation](#snapshot-delegation) applies on top of this sum: a delegation gives all of it to the delegate. ## When it is measured Each proposal names a block, chosen by its author when posting. Voting power is read at that block and does not change for the rest of the vote. Tokens bought, staked, moved or delegated after that block count from the next proposal on. -The block is at most 7,200 blocks, about a day, before the proposal was posted (see [Snapshot block](/dao/proposals#snapshot-block)). +The block is at most 7,200 blocks, about a day, before the proposal was posted (see [Snapshot block](/dao/voting#snapshot-block)). + +The vote dialog of a proposal on snapshot.box shows your voting power at its block. ## How it is computed The space computes voting power with Snapshot's `with-delegation` strategy, which applies Snapshot delegation on top of five sources read on Ethereum mainnet. -The strategy is set in the [space settings](https://snapshot.box/#/s:api3.eth/settings) on Snapshot, and the [verification](/dao/verification) recomputes it from the chain. +The strategy is set in the [space settings](https://snapshot.box/#/s:api3.eth/settings) on Snapshot. | Source | Contract | Read | | ------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | @@ -57,7 +60,7 @@ There are two kinds, and they behave differently. | | Pool delegation | Snapshot delegation | | -------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ | | Made in | The legacy DAO dashboard | The [Delegates](https://snapshot.box/#/s:api3.eth/delegates) page of the space | -| What moves | Your stake in the pool | Everything that counts for you, stake delegated to you in the pool included | +| What is delegated | Your stake in the pool | Everything that counts for you, stake delegated to you in the pool included | | Follows a delegate who delegates on Snapshot | Yes | No | | Changing it | A transaction, then a week before the next change | A transaction | @@ -70,11 +73,11 @@ Your wallet balance, deposits, unstaking amount and collateral still vote with y The pool refuses a delegation change within a week of the last one. After delegating, you wait a week before you can undelegate or change the delegate, and after undelegating, a week before you can delegate again. -The dashboard dates from the legacy DAO (see [Before Snapshot](/dao/#before-snapshot)), and [its documentation](https://old-docs.api3.org/dao-members/dashboard/voting.html) describes delegating in it. +The dashboard dates from the legacy DAO, and [its documentation](https://old-docs.api3.org/dao-members/dashboard/voting.html) describes delegating in it. ### Snapshot delegation -Snapshot delegation moves everything you hold to your delegate, stake that others delegated to you in the pool included. +Snapshot delegation gives your delegate everything that counts for you, stake that others delegated to you in the pool included. You then vote with zero. It is recorded in Snapshot's delegate registry on Ethereum mainnet, so setting, changing or clearing it is a transaction. @@ -87,7 +90,7 @@ To delegate, open the [Delegates](https://snapshot.box/#/s:api3.eth/delegates) p - Snapshot delegation does not chain. If you delegate to someone who has delegated to a third person, your power stays with your delegate, who votes with it. - Only their own holdings move on. + Only their own holdings go on to the third person. - Pool delegation does pass through. If you delegated your stake in the pool to someone who delegates on Snapshot, your stake follows them to their delegate. - A pool delegation to someone who has since delegated inside the pool counts for nobody. @@ -109,8 +112,3 @@ To delegate, open the [Delegates](https://snapshot.box/#/s:api3.eth/delegates) p Borrowing against it changes nothing. - An API3 multisig that pool delegations name votes with the stake delegated to it. - The API3 multisig that holds the DAO's own API3 has its voting power delegated to a dead address and does not vote. - -## Checking your voting power - -Open a proposal on [snapshot.box](https://snapshot.box/#/s:api3.eth): the vote dialog shows your voting power at the proposal's block. -The [verification tool](/dao/verification) recomputes it from the chain for any address and block. diff --git a/docs/dao/voting.md b/docs/dao/voting.md index 928a1c40..b47a0ba7 100644 --- a/docs/dao/voting.md +++ b/docs/dao/voting.md @@ -9,7 +9,7 @@ outline: deep # Voting Proposals of the API3 DAO are listed in the [api3.eth](https://snapshot.box/#/s:api3.eth) space on snapshot.box. -Voting opens when a proposal is posted and closes seven days later. +Voting opens when a proposal is posted, with no delay, and closes seven days later. ## Casting a vote @@ -21,13 +21,69 @@ Voting opens when a proposal is posted and closes seven days later. Your vote appears in the proposal's vote list with the voting power it carries. Votes are public while the proposal is open. -## Rules to know +## What to know - Your voting power is read at the block the proposal names, so tokens or delegations changed after that block do not count (see [Voting power](/dao/voting-power#when-it-is-measured)). -- Voting has no minimum. +- Any amount of voting power votes. + The 500,000 API3 threshold applies to posting a proposal, not to voting. - If you delegated on Snapshot, your own power is zero and voting yourself does nothing. Clear the delegation before the proposal's block to vote yourself (see [Delegation](/dao/voting-power#delegation)). - Abstain is not on the ballot. The ballot shows For and Against, the abstain option is hidden, and an abstain cast through the Snapshot API counts for nothing. - A Safe or another contract wallet votes by signing through the Safe app. -- A proposal passes with at least 10,000,000 API3 in favour and more votes for than against (see [Proposals](/dao/proposals#passing)). + +## Posting a proposal + +Posting a proposal takes 500,000 API3 of voting power, held or delegated to you. +Snapshot checks it at the latest block when it receives the proposal, and the [independent verification](/dao/verification) checks it again at the proposal's snapshot block. + +Proposals are discussed on the [API3 Discord](https://discord.gg/api3dao). + +1. Open [New proposal](https://snapshot.box/#/s:api3.eth/create) in the space and connect the wallet that holds the voting power. +2. Write the title and the body. + The voting system is basic voting with the choices For and Against. +3. Leave the snapshot block at its default, the latest block (see [Snapshot block](#snapshot-block)). +4. Publish by signing. + Voting starts at once. + +To post from a Safe, open snapshot.box in the Safe app and collect the signatures within a day, since the proposal has to reach Snapshot within a day of the signed timestamp. +The author can delete their own proposal. + +### Snapshot block + +The author picks the block when posting, and Snapshot only checks that the block exists. +The DAO's rule is that the block is not later than the signed timestamp and at most 7,200 blocks, about a day, before the proposal was posted. +A proposal measured at a block outside this window is not verified and is not executed. +The signed timestamp has to precede Snapshot's receipt of the proposal by at most a day, since voting starts at the signed timestamp and a backdated signature would shorten the seven days. + +## Passing + +A proposal passes when at least 10,000,000 API3 was cast in favour and there are more votes for than against. + +Snapshot's quorum bar counts every vote cast, against included, while the DAO's rule counts the votes in favour only. +The bar on snapshot.box can therefore show the quorum as met before the rule is. +The [independent verification](/dao/verification) applies the rule. + +## Execution + +Within seven days of passing, the API3 multisigs verify the result and execute the proposal. + +A proposal that changes the rules of governance executes 30 days after passing, and a proposal that cancels such a change executes within seven days. +The rules of governance are the voting rules, the DAO's mandate, the charter, the voting power strategy, the space settings, the verification tool, the `api3.eth` name and which multisig holds the treasury. +Whether a proposal is a rule change follows from its content, not from anything the author declares. + +## Space settings + +The settings of the `api3.eth` space that carry these rules, as set on Snapshot: + +| Setting | Value | +| ------------------ | ------------------------- | +| Network | Ethereum mainnet | +| Voting type | Basic | +| Proposal threshold | 500,000 API3 | +| Voting delay | None | +| Voting period | 7 days | +| Quorum | 10,000,000 API3 in favour | + +Snapshot enforces the threshold, the voting type and the timing. +The quorum in favour, the majority and the 30 days of a rule change are checked by the API3 multisigs before executing, with the [independent verification](/dao/verification). diff --git a/scripts/check-dao-space.js b/scripts/check-dao-space.js index 9a1dc763..a7acec0c 100644 --- a/scripts/check-dao-space.js +++ b/scripts/check-dao-space.js @@ -1,6 +1,6 @@ // Checks the DAO pages against the live api3.eth space on the Snapshot hub. // The voting power page has to name every contract, method, field and market -// the space's strategies read, and nothing else, and the proposals page has to +// the space's strategies read, and nothing else, and the voting page has to // carry the threshold, voting type, delay, period and quorum the space is set to. const fs = require('fs'); const path = require('path'); @@ -80,7 +80,7 @@ async function main() { const space = await fetchSpace(); const problems = []; const votingPowerPage = fs.readFileSync(path.join(daoDir, 'voting-power.md'), 'utf-8'); - const proposalsPage = fs.readFileSync(path.join(daoDir, 'proposals.md'), 'utf-8'); + const votingPage = fs.readFileSync(path.join(daoDir, 'voting.md'), 'utf-8'); if (space.network !== '1') { problems.push(`The space is on network ${space.network}, the pages say Ethereum mainnet`); @@ -120,8 +120,8 @@ async function main() { Quorum: `${formatAmount(space.voting.quorum)} in favour`, }; for (const [setting, value] of Object.entries(rows)) { - if (!hasRow(proposalsPage, setting, value)) { - problems.push(`proposals.md has no settings row "${setting}" with the value "${value}" the space is set to`); + if (!hasRow(votingPage, setting, value)) { + problems.push(`voting.md has no settings row "${setting}" with the value "${value}" the space is set to`); } } From b847905dca882ad0d0dcbd8fe1bfb05088a0d78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Wed, 28 Oct 2026 00:49:22 +0700 Subject: [PATCH 05/11] Add the API3 token page and point the legacy links at dao-docs.api3.org --- docs/dao/api3-token.md | 47 ++++++++++++++++++++++++++++++++++++++ docs/dao/index.md | 4 ++-- docs/dao/sidebar.js | 4 ++++ docs/dao/voting-power.md | 2 +- scripts/check-dao-space.js | 13 +++++++++++ 5 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 docs/dao/api3-token.md diff --git a/docs/dao/api3-token.md b/docs/dao/api3-token.md new file mode 100644 index 00000000..92cec5c9 --- /dev/null +++ b/docs/dao/api3-token.md @@ -0,0 +1,47 @@ +--- +title: API3 token +pageHeader: DAO +outline: deep +--- + + + +# API3 token + +API3 is the governance token of the API3 DAO, an ERC-20 token on Ethereum mainnet. + +| Property | Value | +| ------------ | --------------------------------------------------------------------------------------------------------------------- | +| Contract | [`0x0b38210ea11411557c13457D4dA7dC6ea731B88a`](https://etherscan.io/token/0x0b38210ea11411557c13457D4dA7dC6ea731B88a) | +| Symbol | API3 | +| Decimals | 18 | +| Total supply | 200,000,000 API3 | + +## Supply + +The contract minted 100,000,000 API3 when it was deployed. +Minting is restricted to addresses its owner marks as minters, and the only minter it ever had was the DAO pool, which minted the staking rewards of the legacy DAO. + +The migration to Snapshot closed the supply. +The pool's minter status was revoked, a final mint to the DAO's treasury multisig brought the supply to 200,000,000 API3, and the contract's ownership was transferred to the dead address, so no address can mint and none can be made a minter. + +Any holder can burn their own API3 by opting in as a burner on the contract and calling `burn`. +Nothing can mint it back, so the supply can only fall from here. + +## Governance + +Each API3 in your wallet, in the DAO pool, scheduled for unstaking or posted as collateral in the Kabu API3/USDC market on Morpho is one vote in the `api3.eth` space (see [Voting power](/dao/voting-power)). +API3 anywhere else, on an exchange, in a liquidity pool or bridged to another chain, does not vote. +Posting a proposal takes 500,000 API3 of voting power (see [Posting a proposal](/dao/voting#posting-a-proposal)). + +## Staking + +The DAO pool of the legacy DAO holds staked API3. +Staking rewards ended with the migration and the pool mints nothing further. +Staking is no longer needed to vote, staked API3 keeps voting as described under [Voting power](/dao/voting-power#what-counts), and unstaking and withdrawing work as before. +The [legacy documentation](https://dao-docs.api3.org/members/staking.html) describes the pool. + +## Collateral on Morpho + +The [Kabu API3/USDC market](https://app.morpho.org/ethereum/market/0x70dfbd2705d10d5dfb4d89e07e2bb860b5a624ce2f97e209ab9f5308bf6c67a1) on Morpho takes API3 as collateral for borrowing USDC. +API3 posted there counts as voting power for the owner of the position, borrowed or not. diff --git a/docs/dao/index.md b/docs/dao/index.md index 37210ea4..9d6fe3d5 100644 --- a/docs/dao/index.md +++ b/docs/dao/index.md @@ -8,7 +8,7 @@ outline: deep # DAO -The API3 DAO governs through the Snapshot space [api3.eth](https://snapshot.box/#/s:api3.eth), where holders of the API3 token vote on proposals. +The API3 DAO governs through the Snapshot space [api3.eth](https://snapshot.box/#/s:api3.eth), where holders of the [API3 token](/dao/api3-token) vote on proposals. Voting is off-chain: a vote is a signed message that Snapshot collects, and the voting power behind it is read from Ethereum mainnet at the block the proposal names. The API3 multisigs execute a proposal once it has passed and its result has been verified. @@ -30,4 +30,4 @@ For quick reference, you can copy-paste [`llms-full.txt`](https://docs.api3.org/ Governance used to run on Aragon contracts, with a staking pool and a dashboard for it. The pool is still in use: stake in it counts as voting power and pool delegations still apply to the stake, but staking is no longer needed to vote. -The [legacy documentation](https://old-docs.api3.org/dao-members/) covers the pool, the dashboard and the contracts. +The [legacy documentation](https://dao-docs.api3.org/) covers the pool, the dashboard and the contracts. diff --git a/docs/dao/sidebar.js b/docs/dao/sidebar.js index 0d7e3b85..ef481edb 100644 --- a/docs/dao/sidebar.js +++ b/docs/dao/sidebar.js @@ -3,6 +3,10 @@ module.exports = [ text: 'Overview', link: '/dao/', }, + { + text: 'API3 token', + link: '/dao/api3-token', + }, { text: 'Voting power', link: '/dao/voting-power', diff --git a/docs/dao/voting-power.md b/docs/dao/voting-power.md index 2655b667..8f4775d0 100644 --- a/docs/dao/voting-power.md +++ b/docs/dao/voting-power.md @@ -73,7 +73,7 @@ Your wallet balance, deposits, unstaking amount and collateral still vote with y The pool refuses a delegation change within a week of the last one. After delegating, you wait a week before you can undelegate or change the delegate, and after undelegating, a week before you can delegate again. -The dashboard dates from the legacy DAO, and [its documentation](https://old-docs.api3.org/dao-members/dashboard/voting.html) describes delegating in it. +The dashboard dates from the legacy DAO, and [its documentation](https://dao-docs.api3.org/members/voting.html) describes delegating in it. ### Snapshot delegation diff --git a/scripts/check-dao-space.js b/scripts/check-dao-space.js index a7acec0c..646d58a1 100644 --- a/scripts/check-dao-space.js +++ b/scripts/check-dao-space.js @@ -107,6 +107,19 @@ async function main() { } } + const tokenPage = fs.readFileSync(path.join(daoDir, 'api3-token.md'), 'utf-8'); + const tokenAddress = sources.find((source) => source.name === 'erc20-balance-of')?.params?.address; + if (!tokenAddress) { + problems.push('The strategy has no erc20-balance-of source, the token page describes the token it reads'); + } else if (!tokenPage.includes(tokenAddress)) { + problems.push(`api3-token.md does not name the token ${tokenAddress} the space reads`); + } + for (const hex of new Set(tokenPage.match(/0x[0-9a-fA-F]{40,}/g) ?? [])) { + if (!reads.has(hex)) { + problems.push(`api3-token.md lists ${hex}, which the space's strategies do not read`); + } + } + if (space.validation.name !== 'basic') { problems.push( `The space validates proposals with ${space.validation.name}, the proposals page describes a basic threshold` From cb2303e179f73f8c60e5dc48401ef76bdcb3fac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Wed, 28 Oct 2026 10:38:34 +0700 Subject: [PATCH 06/11] Clarify DAO voting and Snapshot assumptions --- docs/dao/api3-token.md | 6 ++-- docs/dao/index.md | 16 ++++++++++ docs/dao/sidebar.js | 4 +++ docs/dao/snapshot-assumptions.md | 52 ++++++++++++++++++++++++++++++++ docs/dao/verification.md | 15 ++++----- docs/dao/voting-power.md | 49 ++++++++++++++++++++---------- docs/dao/voting.md | 44 ++++++++++++++++----------- 7 files changed, 144 insertions(+), 42 deletions(-) create mode 100644 docs/dao/snapshot-assumptions.md diff --git a/docs/dao/api3-token.md b/docs/dao/api3-token.md index 92cec5c9..ca20088e 100644 --- a/docs/dao/api3-token.md +++ b/docs/dao/api3-token.md @@ -20,7 +20,8 @@ API3 is the governance token of the API3 DAO, an ERC-20 token on Ethereum mainne ## Supply The contract minted 100,000,000 API3 when it was deployed. -Minting is restricted to addresses its owner marks as minters, and the only minter it ever had was the DAO pool, which minted the staking rewards of the legacy DAO. +Minting is restricted to addresses its owner marks as minters. +The DAO pool minted the staking rewards of the legacy DAO. The migration to Snapshot closed the supply. The pool's minter status was revoked, a final mint to the DAO's treasury multisig brought the supply to 200,000,000 API3, and the contract's ownership was transferred to the dead address, so no address can mint and none can be made a minter. @@ -31,7 +32,8 @@ Nothing can mint it back, so the supply can only fall from here. ## Governance Each API3 in your wallet, in the DAO pool, scheduled for unstaking or posted as collateral in the Kabu API3/USDC market on Morpho is one vote in the `api3.eth` space (see [Voting power](/dao/voting-power)). -API3 anywhere else, on an exchange, in a liquidity pool or bridged to another chain, does not vote. +Exchange balances, liquidity pool tokens and tokens bridged to another chain do not count as your voting power. +Other contracts can explicitly delegate their own API3 token balance on Snapshot (see [Voting power](/dao/voting-power#what-counts)). Posting a proposal takes 500,000 API3 of voting power (see [Posting a proposal](/dao/voting#posting-a-proposal)). ## Staking diff --git a/docs/dao/index.md b/docs/dao/index.md index 9d6fe3d5..0542cf9c 100644 --- a/docs/dao/index.md +++ b/docs/dao/index.md @@ -25,6 +25,22 @@ For quick reference, you can copy-paste [`llms-full.txt`](https://docs.api3.org/ - [Voting](/dao/voting) is a gasless signature on snapshot.box, For or Against, during the seven days a proposal is open. Anyone with 500,000 API3 of voting power can [post a proposal](/dao/voting#posting-a-proposal), and it passes with at least 10,000,000 API3 in favour and more votes for than against. - [Independent verification](/dao/verification) recomputes a result from the chain before the API3 multisigs execute it. + See [Snapshot assumptions](/dao/snapshot-assumptions) for the remaining trust in vote collection and the space's settings. + +## Governing proposal + +The primary DAO migration proposal in the [legacy DAO dashboard](https://api3.eth.limo/#/history) sets out the governance rules, mandate and charter. +These pages explain how to participate under those rules. + +## DAO multisigs + +| Multisig | Role | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| [Vault](https://etherscan.io/address/0x8e03609ed680B237C7b6f8020472CA0687308b24) | Controls `api3.eth` and its Snapshot settings. Posts proposals and votes with the pool stake delegated to it. | +| [Agent](https://etherscan.io/address/0xBBE0dE9757F93e3306aDBfeBE906AB285EDD13DA) | Executes operational proposals. Its own voting power is delegated to a dead address. | +| [Treasury](https://etherscan.io/address/0xb7fa2Cb302C5dC127e61F27b92beC411cbf0AE4A) | Holds the DAO's treasury API3. Its own voting power is delegated to a dead address, and it does not post proposals. | + +Changes to the Snapshot settings and other rules of governance follow the [rule-change execution period](/dao/voting#execution). ## Legacy DAO diff --git a/docs/dao/sidebar.js b/docs/dao/sidebar.js index ef481edb..2105c802 100644 --- a/docs/dao/sidebar.js +++ b/docs/dao/sidebar.js @@ -19,4 +19,8 @@ module.exports = [ text: 'Independent verification', link: '/dao/verification', }, + { + text: 'Snapshot assumptions', + link: '/dao/snapshot-assumptions', + }, ]; diff --git a/docs/dao/snapshot-assumptions.md b/docs/dao/snapshot-assumptions.md new file mode 100644 index 00000000..f87b6a8b --- /dev/null +++ b/docs/dao/snapshot-assumptions.md @@ -0,0 +1,52 @@ +--- +title: Snapshot assumptions +pageHeader: DAO +outline: deep +--- + + + +# Snapshot assumptions + +Snapshot voting is off-chain. +The [independent verification](/dao/verification) recomputes voting power and the tally, while vote collection still relies on Snapshot. + +## Vote collection + +The Snapshot sequencer receives signed votes and decides whether they arrived before the deadline. +The hub supplies the list of accepted votes. +A signature proves who signed a vote and what they signed, including its timestamp, but does not independently prove when Snapshot received it. + +Snapshot cannot forge a voter's signature. +It could omit a signed vote or accept one after the deadline with a backdated signed timestamp. +Recomputing the final tally alone cannot rule those cases out. +Voters should check that their vote appears in the proposal's vote list. + +The verification tool archives the vote list on each run and compares it with the previous archive. +Running it at least daily while voting is open, and again at the close, helps detect votes that were inserted, removed or changed. +Those archives improve visibility into changes, but do not remove the trust in Snapshot's receipt and inclusion of votes. + +## Proposal timing + +Snapshot sets a proposal's creation time when the sequencer accepts it. +If its signed start time is in the past, the sequencer moves the start forward and keeps the full configured voting period. +For `api3.eth`, voting starts without a delay and runs for seven days. +Time spent collecting Safe confirmations does not shorten that period. + +The snapshot block is still the block chosen when preparing the proposal. +It can become too old while confirmations are collected, so a proposal must also meet the DAO's [snapshot block window](/dao/voting#snapshot-block). + +## Voting power and settings + +The verifier reads balances and contract state from an Ethereum archive RPC at the proposal's snapshot block, and delegations from the delegate registry's events. +It does not use Snapshot's score API or delegation subgraph for that recomputation. +The RPC must serve the correct Ethereum history. + +The recomputation uses the strategies recorded on the proposal. +Agreement with those strategies does not itself establish that a strategy or settings change was authorized. +The API3 multisigs also check that the proposal uses the DAO's approved governance rules before executing it. + +## Execution + +A passed Snapshot vote does not automatically move funds or change contracts. +The [DAO multisigs](/dao/#dao-multisigs) execute the verified result under the [governing proposal](/dao/#governing-proposal), including the execution period for a change to the rules of governance. diff --git a/docs/dao/verification.md b/docs/dao/verification.md index f02a8de1..17d1915f 100644 --- a/docs/dao/verification.md +++ b/docs/dao/verification.md @@ -8,10 +8,9 @@ outline: deep # Independent verification -The DAO relies on Snapshot to run its votes. -The space settings, the strategies that compute voting power and the hub that collects and tallies the votes are Snapshot's, and the DAO trusts them to work as described. -It verifies them all the same: before executing a proposal, the API3 multisigs recompute its result from the chain with the [Snapshot voting verifier](https://github.com/api3dao/snapshot-voting-verifier), never taking a result off a website. -The tool is open source, anyone can run it on any proposal of the space, and its README says how. +Before executing a proposal, the API3 multisigs recompute its result from the chain with the [Snapshot voting verifier](https://github.com/api3dao/snapshot-voting-verifier), never taking a result off a website. +The tool's source is public, anyone can run it on any proposal of the space, and its README says how. +The remaining trust in Snapshot's vote collection and settings is described under [Snapshot assumptions](/dao/snapshot-assumptions). The DAO's rules also go further than Snapshot's settings can express, such as counting only the votes in favour toward the quorum and the window for the snapshot block. The verification enforces those too, so a proposal that Snapshot shows as passed can still fail a check and not be executed. @@ -20,13 +19,15 @@ The verification enforces those too, so a proposal that Snapshot shows as passed Given a proposal, the tool reads every vote the Snapshot hub lists and establishes, independently of Snapshot's own computation, that: -- Every vote was signed by the voter it is listed under, names one of the proposal's choices and was cast while the vote was open. -- Every voter's power is what the [voting power](/dao/voting-power) rules give them at the proposal's block, delegations included. +- Every listed vote was signed by the voter it is listed under, names one of the proposal's choices and has a signed timestamp within the voting window. +- Every listed voter's power matches the proposal's recorded strategies at the proposal's block, delegations included (see [Voting power](/dao/voting-power)). - The tally follows from those powers. -- The proposal met the posting rules: the proposer's 500,000 API3 at the proposal's block, a snapshot block and a signed timestamp within their windows (see [Snapshot block](/dao/voting#snapshot-block)), and seven days of voting with no delay. +- The proposal met the posting rules: the proposer's 500,000 API3 at the proposal's block, a snapshot block within its window (see [Snapshot block](/dao/voting#snapshot-block)), and seven days of voting with no delay. It then judges the result on what it recomputed: at least 10,000,000 API3 in favour and more votes for than against (see [Passing](/dao/voting#passing)). The run ends by saying whether the result is verified, and lists every problem it found. +`VERIFIED` means the listed votes and tally match the recomputation. +A verified result can still be a rejected proposal, so execution also requires `Outcome: passed`. A proposal that fails a check is not executed. The tool does not judge whether a proposal is a change of the rules of governance. diff --git a/docs/dao/voting-power.md b/docs/dao/voting-power.md index 8f4775d0..afffede1 100644 --- a/docs/dao/voting-power.md +++ b/docs/dao/voting-power.md @@ -15,7 +15,7 @@ Snapshot shows voting power under the symbol vpAPI3. ## What counts -At the block a proposal is measured at, your voting power is the sum of: +At the block a proposal is measured at, your base voting power is the sum of: - API3 in your wallet. - API3 staked in the DAO pool, locked rewards included. @@ -24,9 +24,14 @@ At the block a proposal is measured at, your voting power is the sum of: - API3 scheduled for unstaking, during the waiting period. - API3 posted as collateral in the Kabu API3/USDC market on Morpho. -Nothing else counts: API3 on an exchange, in a liquidity pool, lent out, bridged to another chain or held in any other contract. +API3 held for you by an exchange or custodian does not count as yours. +Neither do liquidity pool tokens, lending receipts or tokens bridged to another chain. + +API3 held by other contracts counts for you only if the contract explicitly delegates its own API3 token balance to you on Snapshot. +Owning a share of a contract does not give you voting power over its underlying assets. [Snapshot delegation](#snapshot-delegation) applies on top of this sum: a delegation gives all of it to the delegate. +Power delegated to you on Snapshot is added separately and is not delegated onward. ## When it is measured @@ -59,26 +64,36 @@ There are two kinds, and they behave differently. | | Pool delegation | Snapshot delegation | | -------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ | -| Made in | The legacy DAO dashboard | The [Delegates](https://snapshot.box/#/s:api3.eth/delegates) page of the space | -| What is delegated | Your stake in the pool | Everything that counts for you, stake delegated to you in the pool included | +| Made in | The pool contract | The [Delegates](https://snapshot.box/#/s:api3.eth/delegates) page of the space | +| What is delegated | Your stake in the pool | Your base voting power, stake delegated to you in the pool included | | Follows a delegate who delegates on Snapshot | Yes | No | | Changing it | A transaction, then a week before the next change | A transaction | ### Pool delegation -Pool delegation is made in the legacy DAO dashboard and carries over for the stake only. +Existing pool delegations carry over for the stake only. If you delegated your stake in the pool, it keeps voting with your delegate. Your wallet balance, deposits, unstaking amount and collateral still vote with you. The pool refuses a delegation change within a week of the last one. After delegating, you wait a week before you can undelegate or change the delegate, and after undelegating, a week before you can delegate again. -The dashboard dates from the legacy DAO, and [its documentation](https://dao-docs.api3.org/members/voting.html) describes delegating in it. +The legacy dashboard is retained for unstaking and withdrawing. +To inspect or change a pool delegation, open the [pool contract on Etherscan](https://etherscan.io/address/0x6dd655f10d4b9E242aE186D9050B68F725c76d76#code): + +1. Under **Read Contract**, call `userDelegate` with your wallet address to see its current delegate. + The zero address means no pool delegation. +2. Under **Write Contract**, connect that wallet and call `undelegateVotingPower` to clear the delegation, or `delegateVotingPower` with the new delegate's address to change it. + These are transactions and cost gas. + +The one-week cooldown applies to either change. +The pool refuses a new delegate who is already delegating inside the pool. ### Snapshot delegation -Snapshot delegation gives your delegate everything that counts for you, stake that others delegated to you in the pool included. -You then vote with zero. +Snapshot delegation gives your delegate your base voting power, stake that others delegated to you in the pool included. +You can still vote with power delegated to you on Snapshot, because Snapshot delegation does not chain. +If you have no incoming Snapshot delegations, your voting power is zero. It is recorded in Snapshot's delegate registry on Ethereum mainnet, so setting, changing or clearing it is a transaction. A delegation can be set under `api3.eth` or under all spaces. @@ -96,19 +111,21 @@ To delegate, open the [Delegates](https://snapshot.box/#/s:api3.eth/delegates) p - A pool delegation to someone who has since delegated inside the pool counts for nobody. The pool gives a delegating user no voting power at all, received delegations included, and does not pass them on. Re-delegate or undelegate in the pool to fix it. -- Voting yourself after delegating does nothing. - Your own power is zero while the delegation stands. - Clear it before the proposal's block to vote yourself. +- Voting yourself after delegating does not take your base voting power back from your delegate. + You can still use any incoming Snapshot delegations. + To vote with your own holdings again, clear the applicable space and all-spaces delegations before the proposal's block. - Changing or clearing a delegation counts from the next proposal's block. A proposal already posted keeps the delegations as they were at its block. -- To put everything with one delegate, delegate on Snapshot. - Pool delegation alone leaves the rest of your holdings with you. +- To put your own holdings with one delegate, clear any outgoing pool delegation first, then delegate on Snapshot. + While a pool delegation stands, that stake follows the pool delegate's Snapshot delegation, not yours. + Incoming Snapshot delegations stay with you and cannot be passed on. ## Other cases - API3 held for you by an exchange or a custodian is in their wallet, not yours, and does not vote as yours. -- A Safe or another contract wallet has voting power like any wallet and votes by signing through the Safe app (see [Voting](/dao/voting)). +- A Safe can vote directly through the Safe app (see [Voting](/dao/voting)). + Other contract wallets need a compatible signing method or must explicitly delegate their own API3 balance. - Collateral on Morpho counts for the owner of the position, in the Kabu API3/USDC market only. Borrowing against it changes nothing. -- An API3 multisig that pool delegations name votes with the stake delegated to it. -- The API3 multisig that holds the DAO's own API3 has its voting power delegated to a dead address and does not vote. +- The Vault votes with the pool stake delegated to it. + The Agent and Treasury delegate their own voting power to a dead address and do not vote (see [DAO multisigs](/dao/#dao-multisigs)). diff --git a/docs/dao/voting.md b/docs/dao/voting.md index b47a0ba7..3bcea458 100644 --- a/docs/dao/voting.md +++ b/docs/dao/voting.md @@ -9,7 +9,7 @@ outline: deep # Voting Proposals of the API3 DAO are listed in the [api3.eth](https://snapshot.box/#/s:api3.eth) space on snapshot.box. -Voting opens when a proposal is posted, with no delay, and closes seven days later. +Voting opens when Snapshot's sequencer accepts a proposal, with no delay, and closes seven days later. ## Casting a vote @@ -26,11 +26,14 @@ Votes are public while the proposal is open. - Your voting power is read at the block the proposal names, so tokens or delegations changed after that block do not count (see [Voting power](/dao/voting-power#when-it-is-measured)). - Any amount of voting power votes. The 500,000 API3 threshold applies to posting a proposal, not to voting. -- If you delegated on Snapshot, your own power is zero and voting yourself does nothing. - Clear the delegation before the proposal's block to vote yourself (see [Delegation](/dao/voting-power#delegation)). +- If you delegated on Snapshot, your base voting power goes to your delegate. + You can still vote with incoming Snapshot delegations. + Clear your delegation before the proposal's block to vote with your own holdings (see [Delegation](/dao/voting-power#delegation)). - Abstain is not on the ballot. The ballot shows For and Against, the abstain option is hidden, and an abstain cast through the Snapshot API counts for nothing. -- A Safe or another contract wallet votes by signing through the Safe app. +- To vote from a Safe, open Snapshot inside the Safe app and collect the required confirmations. + Confirming a Safe message alone does not guarantee that it was delivered to Snapshot. + Check that the vote appears in the proposal's vote list before voting closes. ## Posting a proposal @@ -44,17 +47,20 @@ Proposals are discussed on the [API3 Discord](https://discord.gg/api3dao). The voting system is basic voting with the choices For and Against. 3. Leave the snapshot block at its default, the latest block (see [Snapshot block](#snapshot-block)). 4. Publish by signing. - Voting starts at once. + Voting starts when the sequencer accepts the proposal. -To post from a Safe, open snapshot.box in the Safe app and collect the signatures within a day, since the proposal has to reach Snapshot within a day of the signed timestamp. +To post from a Safe, open snapshot.box in the Safe app, collect the required confirmations and deliver the signed proposal to Snapshot. +Check that the proposal appears in the space. +The snapshot block is fixed when preparing the proposal, so collect the confirmations and submit it while that block is still within the [allowed window](#snapshot-block). The author can delete their own proposal. ### Snapshot block The author picks the block when posting, and Snapshot only checks that the block exists. -The DAO's rule is that the block is not later than the signed timestamp and at most 7,200 blocks, about a day, before the proposal was posted. +The DAO's rule is that the block is not later than the proposal's creation and at most 7,200 blocks, about a day, before the proposal was posted. A proposal measured at a block outside this window is not verified and is not executed. -The signed timestamp has to precede Snapshot's receipt of the proposal by at most a day, since voting starts at the signed timestamp and a backdated signature would shorten the seven days. +Snapshot sets the creation time when the sequencer accepts the proposal. +A signed start time in the past is moved forward so that voting still runs for the full seven days (see [Snapshot assumptions](/dao/snapshot-assumptions#proposal-timing)). ## Passing @@ -69,21 +75,25 @@ The [independent verification](/dao/verification) applies the rule. Within seven days of passing, the API3 multisigs verify the result and execute the proposal. A proposal that changes the rules of governance executes 30 days after passing, and a proposal that cancels such a change executes within seven days. -The rules of governance are the voting rules, the DAO's mandate, the charter, the voting power strategy, the space settings, the verification tool, the `api3.eth` name and which multisig holds the treasury. +The rules of governance are the voting rules, the DAO's [mandate and charter](/dao/#governing-proposal), the voting power strategy, the space settings, the verification tool, the `api3.eth` name and which multisig holds the treasury. Whether a proposal is a rule change follows from its content, not from anything the author declares. ## Space settings The settings of the `api3.eth` space that carry these rules, as set on Snapshot: -| Setting | Value | -| ------------------ | ------------------------- | -| Network | Ethereum mainnet | -| Voting type | Basic | -| Proposal threshold | 500,000 API3 | -| Voting delay | None | -| Voting period | 7 days | -| Quorum | 10,000,000 API3 in favour | +| Setting | Value | +| ------------------ | ---------------------------- | +| Network | Ethereum mainnet | +| Voting type | Basic | +| Proposal threshold | 500,000 API3 | +| Voting delay | None | +| Voting period | 7 days | +| Quorum | 10,000,000 API3 in favour | +| Abstain | Hidden | +| Vote privacy | Public | +| Who can vote | Any voting power holder | +| Posting access | Anyone meeting the threshold | Snapshot enforces the threshold, the voting type and the timing. The quorum in favour, the majority and the 30 days of a rule change are checked by the API3 multisigs before executing, with the [independent verification](/dao/verification). From adf534efbfe71148daff311fd494bad4a401ad07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Wed, 28 Oct 2026 10:38:34 +0700 Subject: [PATCH 07/11] Check the full documented Snapshot configuration --- .github/workflows/dao-space.yml | 6 +- CONTRIBUTING.md | 3 +- package.json | 1 + scripts/check-dao-space.js | 90 +++++++--- scripts/check-dao-space.test.js | 222 +++++++++++++++++++++++ scripts/fixtures/dao-space.json | 306 ++++++++++++++++++++++++++++++++ 6 files changed, 602 insertions(+), 26 deletions(-) create mode 100644 scripts/check-dao-space.test.js create mode 100644 scripts/fixtures/dao-space.json diff --git a/.github/workflows/dao-space.yml b/.github/workflows/dao-space.yml index 854b639e..76421364 100644 --- a/.github/workflows/dao-space.yml +++ b/.github/workflows/dao-space.yml @@ -4,7 +4,8 @@ on: pull_request: paths: - 'docs/dao/**' - - 'scripts/check-dao-space.js' + - 'scripts/check-dao-space*.js' + - 'scripts/fixtures/dao-space.json' - '.github/workflows/dao-space.yml' schedule: # Once a week, Monday 06:00 UTC @@ -22,5 +23,8 @@ jobs: with: node-version: '^24.18.0' + - name: Test the settings checker + run: node --test scripts/check-dao-space.test.js + - name: Compare the pages with the space run: node scripts/check-dao-space.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9a380c7..11bfbceb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,9 +39,10 @@ node ./libs/link-validator.js http://localhost:8082 ./docs/.vitepress/dist/ Hosts that block automated requests are listed in [`libs/link-validator-ignore.json`](./libs/link-validator-ignore.json). -The DAO pages repeat what the `api3.eth` space on Snapshot is set to: the contracts, methods and market its voting power strategy reads, and the proposal threshold, voting type, delay, period and quorum. [`scripts/check-dao-space.js`](./scripts/check-dao-space.js) reads the space from the Snapshot hub and fails when the pages differ from it. The [workflow](./.github/workflows/dao-space.yml) runs it on pull requests that touch `docs/dao` and once a week, so a settings change on Snapshot shows up as a failing run. Reproduce it locally with: +The DAO pages describe the `api3.eth` space on Snapshot. [`scripts/fixtures/dao-space.json`](./scripts/fixtures/dao-space.json) records its documented configuration, including the complete voting power strategy parameters, privacy, eligibility and multisigs. [`scripts/check-dao-space.js`](./scripts/check-dao-space.js) compares the hub's settings with that configuration and checks the contracts, reads and settings named on the pages. Update the configuration and the pages together when an approved governance change takes effect, never just to make the check pass. The [workflow](./.github/workflows/dao-space.yml) tests the checker and runs the comparison on relevant pull requests and once a week. Reproduce it locally with: ```sh +pnpm test node scripts/check-dao-space.js ``` diff --git a/package.json b/package.json index 9e171bab..5663d918 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "format": "prettier --write --cache \"./**/*.{js,vue,md,json,yaml}\" --log-level silent", "format:check": "prettier --check --cache \"./**/*.{js,vue,md,json,yaml}\"", "lint": "pnpm format:check", + "test": "node --test scripts/*.test.js", "generate-llms-files": "node scripts/generate-llms-files.js", "prepare": "husky", "firebase:emulator": "pnpm docs:build && firebase emulators:start" diff --git a/scripts/check-dao-space.js b/scripts/check-dao-space.js index 646d58a1..11d0b338 100644 --- a/scripts/check-dao-space.js +++ b/scripts/check-dao-space.js @@ -1,9 +1,10 @@ // Checks the DAO pages against the live api3.eth space on the Snapshot hub. -// The voting power page has to name every contract, method, field and market -// the space's strategies read, and nothing else, and the voting page has to -// carry the threshold, voting type, delay, period and quorum the space is set to. +// Pin the complete strategy parameters and participation rules as well as +// checking the contracts, reads and settings named on the pages. const fs = require('fs'); const path = require('path'); +const { isDeepStrictEqual } = require('node:util'); +const documentedSpace = require('./fixtures/dao-space.json'); const HUB = 'https://hub.snapshot.org/graphql'; const SPACE = 'api3.eth'; @@ -12,9 +13,16 @@ const daoDir = path.join(__dirname, '..', 'docs', 'dao'); const query = `{ space(id: "${SPACE}") { network + symbol strategies { name network params } - voting { type delay period quorum } + voting { type delay period quorum hideAbstain privacy } validation { name params } + voteValidation { name params } + filters { onlyMembers } + admins + members + moderators + treasuries { name address network } } }`; @@ -23,6 +31,7 @@ async function fetchSpace() { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ query }), + signal: AbortSignal.timeout(30000), }); if (!response.ok) { throw new Error(`The hub returned ${response.status}`); @@ -76,26 +85,37 @@ function hasRow(page, setting, value) { return row.test(page); } -async function main() { - const space = await fetchSpace(); +function checkSpace(space, pages) { const problems = []; - const votingPowerPage = fs.readFileSync(path.join(daoDir, 'voting-power.md'), 'utf-8'); - const votingPage = fs.readFileSync(path.join(daoDir, 'voting.md'), 'utf-8'); + const { votingPowerPage, votingPage, tokenPage, overviewPage } = pages; + + for (const [setting, expected] of Object.entries(documentedSpace)) { + if (!isDeepStrictEqual(space[setting], expected)) { + problems.push(`${setting} differs from the documented configuration in scripts/fixtures/dao-space.json`); + } + } + + for (const { name, address } of documentedSpace.treasuries) { + if (!overviewPage.includes(`[${name}](https://etherscan.io/address/${address})`)) { + problems.push(`index.md does not link ${name} to its documented multisig ${address}`); + } + } if (space.network !== '1') { problems.push(`The space is on network ${space.network}, the pages say Ethereum mainnet`); } - if (space.strategies.length !== 1 || space.strategies[0].name !== 'with-delegation') { + const strategies = space.strategies ?? []; + if (strategies.length !== 1 || strategies[0].name !== 'with-delegation') { problems.push( - `The space runs ${space.strategies.map((strategy) => strategy.name).join(', ')}, the voting power page describes one with-delegation strategy` + `The space runs ${strategies.map((strategy) => strategy.name).join(', ')}, the voting power page describes one with-delegation strategy` ); } - const sources = space.strategies[0]?.params?.strategies ?? []; + const sources = strategies[0]?.params?.strategies ?? []; if (sources.length !== 5) { problems.push(`The strategy reads ${sources.length} sources, the voting power page describes five`); } - const reads = collectReads(space.strategies); + const reads = collectReads(strategies); for (const read of reads) { if (!votingPowerPage.includes(read)) { problems.push(`voting-power.md does not mention ${read}, which the space's strategies read`); @@ -107,7 +127,6 @@ async function main() { } } - const tokenPage = fs.readFileSync(path.join(daoDir, 'api3-token.md'), 'utf-8'); const tokenAddress = sources.find((source) => source.name === 'erc20-balance-of')?.params?.address; if (!tokenAddress) { problems.push('The strategy has no erc20-balance-of source, the token page describes the token it reads'); @@ -120,17 +139,23 @@ async function main() { } } - if (space.validation.name !== 'basic') { + if (space.validation?.name !== 'basic') { problems.push( - `The space validates proposals with ${space.validation.name}, the proposals page describes a basic threshold` + `The space validates proposals with ${space.validation?.name}, the voting page describes a basic threshold` ); } + const voting = space.voting ?? {}; const rows = { - 'Voting type': space.voting.type === 'basic' ? 'Basic' : String(space.voting.type), - 'Proposal threshold': formatAmount(space.validation.params?.minScore), - 'Voting delay': space.voting.delay ? formatDays(space.voting.delay) : 'None', - 'Voting period': formatDays(space.voting.period), - Quorum: `${formatAmount(space.voting.quorum)} in favour`, + 'Voting type': voting.type === 'basic' ? 'Basic' : String(voting.type), + 'Proposal threshold': formatAmount(space.validation?.params?.minScore), + 'Voting delay': voting.delay ? formatDays(voting.delay) : 'None', + 'Voting period': formatDays(voting.period), + Quorum: `${formatAmount(voting.quorum)} in favour`, + Abstain: voting.hideAbstain === true ? 'Hidden' : 'Visible', + 'Vote privacy': voting.privacy === '' ? 'Public' : String(voting.privacy), + 'Who can vote': + space.voteValidation?.name === 'any' ? 'Any voting power holder' : String(space.voteValidation?.name), + 'Posting access': space.filters?.onlyMembers === false ? 'Anyone meeting the threshold' : 'Members only', }; for (const [setting, value] of Object.entries(rows)) { if (!hasRow(votingPage, setting, value)) { @@ -138,6 +163,19 @@ async function main() { } } + return problems; +} + +async function main() { + const space = await fetchSpace(); + const readPage = (file) => fs.readFileSync(path.join(daoDir, file), 'utf-8'); + const problems = checkSpace(space, { + votingPowerPage: readPage('voting-power.md'), + votingPage: readPage('voting.md'), + tokenPage: readPage('api3-token.md'), + overviewPage: readPage('index.md'), + }); + if (problems.length > 0) { console.error(`The DAO pages differ from the ${SPACE} space on the hub:`); problems.forEach((problem) => console.error(`- ${problem}`)); @@ -146,7 +184,11 @@ async function main() { console.log(`The DAO pages match the ${SPACE} space on the hub.`); } -main().catch((error) => { - console.error(error.message); - process.exit(1); -}); +module.exports = { checkSpace }; + +if (require.main === module) { + main().catch((error) => { + console.error(error.message); + process.exit(1); + }); +} diff --git a/scripts/check-dao-space.test.js b/scripts/check-dao-space.test.js new file mode 100644 index 00000000..8e7062c1 --- /dev/null +++ b/scripts/check-dao-space.test.js @@ -0,0 +1,222 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { test } = require('node:test'); +const { checkSpace } = require('./check-dao-space'); +const documentedSpace = require('./fixtures/dao-space.json'); + +const readPage = (name) => fs.readFileSync(path.join(__dirname, '..', 'docs', 'dao', name), 'utf8'); +const pages = { + votingPowerPage: readPage('voting-power.md'), + votingPage: readPage('voting.md'), + tokenPage: readPage('api3-token.md'), + overviewPage: readPage('index.md'), +}; + +test('the documented configuration matches the DAO pages', () => { + assert.deepEqual(checkSpace(documentedSpace, pages), []); +}); + +const changes = [ + [ + 'stake multiplication becomes division', + 'strategies', + (space) => { + space.strategies[0].params.strategies[1].params.operands[0].strategy.params.operation = 'divide'; + }, + ], + [ + 'stake division uses reversed operands', + 'strategies', + (space) => { + space.strategies[0].params.strategies[1].params.operands.reverse(); + }, + ], + [ + 'token decimals change', + 'strategies', + (space) => { + space.strategies[0].params.strategies[0].params.decimals = 6; + }, + ], + [ + 'a strategy reads another network', + 'strategies', + (space) => { + space.strategies[0].params.strategies[0].network = '137'; + }, + ], + [ + 'delegation reads another space', + 'strategies', + (space) => { + space.strategies[0].params.delegationSpace = 'another.eth'; + }, + ], + [ + 'delegation reads another network', + 'strategies', + (space) => { + space.strategies[0].params.delegationNetwork = '137'; + }, + ], + [ + 'the Morpho market changes', + 'strategies', + (space) => { + space.strategies[0].params.strategies[4].params.args[0] = `0x${'1'.repeat(64)}`; + }, + ], + [ + 'a source is removed', + 'strategies', + (space) => { + space.strategies[0].params.strategies.pop(); + }, + ], + [ + 'a source is added', + 'strategies', + (space) => { + space.strategies[0].params.strategies.push(structuredClone(space.strategies[0].params.strategies[0])); + }, + ], + [ + 'abstain becomes visible', + 'voting', + (space) => { + space.voting.hideAbstain = false; + }, + ], + [ + 'authors can choose vote privacy', + 'voting', + (space) => { + space.voting.privacy = 'any'; + }, + ], + [ + 'votes become private', + 'voting', + (space) => { + space.voting.privacy = 'shutter'; + }, + ], + [ + 'voting is restricted', + 'voteValidation', + (space) => { + space.voteValidation = { name: 'whitelist', params: { addresses: [] } }; + }, + ], + [ + 'only members can post', + 'filters', + (space) => { + space.filters.onlyMembers = true; + }, + ], + [ + 'proposal validation uses different strategies', + 'validation', + (space) => { + space.validation.params.strategies = []; + }, + ], + [ + 'the posting threshold changes', + 'validation', + (space) => { + space.validation.params.minScore = 1; + }, + ], + [ + 'the voting type is not fixed', + 'voting', + (space) => { + space.voting.type = null; + }, + ], + [ + 'the voting period changes', + 'voting', + (space) => { + space.voting.period = 86400; + }, + ], + [ + 'the quorum changes', + 'voting', + (space) => { + space.voting.quorum = 0; + }, + ], + [ + 'the voting delay changes', + 'voting', + (space) => { + space.voting.delay = 3600; + }, + ], + [ + 'the Treasury address changes', + 'treasuries', + (space) => { + space.treasuries[2].address = space.treasuries[0].address; + }, + ], +]; + +for (const role of ['admins', 'members', 'moderators']) { + changes.push([ + `${role} gain access`, + role, + (space) => { + space[role].push(space.treasuries[0].address); + }, + ]); +} + +for (const [name, setting, mutate] of changes) { + test(`rejects when ${name}`, () => { + const space = structuredClone(documentedSpace); + mutate(space); + const problems = checkSpace(space, pages); + assert.ok( + problems.some((problem) => problem.startsWith(`${setting} differs`)), + problems.join('\n') + ); + }); +} + +test('rejects a stale threshold in the voting page even when the settings match', () => { + const votingPage = pages.votingPage.replace(/500,000 API3/g, '50,000 API3'); + assert.ok( + checkSpace(documentedSpace, { ...pages, votingPage }).some((problem) => problem.includes('Proposal threshold')) + ); +}); + +test('rejects a stale vote privacy description even when the settings match', () => { + const votingPage = pages.votingPage.replace('Public', 'Private'); + assert.ok(checkSpace(documentedSpace, { ...pages, votingPage }).some((problem) => problem.includes('Vote privacy'))); +}); + +test('rejects a missing pool read in the voting power page', () => { + const votingPowerPage = pages.votingPowerPage.replaceAll('userVotingPower', 'userStake'); + assert.ok( + checkSpace(documentedSpace, { ...pages, votingPowerPage }).some((problem) => problem.includes('userVotingPower')) + ); +}); + +test('rejects a different token contract in the token page', () => { + const tokenPage = pages.tokenPage.replaceAll( + documentedSpace.strategies[0].params.strategies[0].params.address, + `0x${'1'.repeat(40)}` + ); + assert.ok(checkSpace(documentedSpace, { ...pages, tokenPage }).some((problem) => problem.includes('api3-token.md'))); +}); + +test('rejects an incorrect Treasury link in the overview', () => { + const overviewPage = pages.overviewPage.replace(documentedSpace.treasuries[2].address, `0x${'1'.repeat(40)}`); + assert.ok(checkSpace(documentedSpace, { ...pages, overviewPage }).some((problem) => problem.includes('index.md'))); +}); diff --git a/scripts/fixtures/dao-space.json b/scripts/fixtures/dao-space.json new file mode 100644 index 00000000..354c5b93 --- /dev/null +++ b/scripts/fixtures/dao-space.json @@ -0,0 +1,306 @@ +{ + "network": "1", + "symbol": "vpAPI3", + "strategies": [ + { + "name": "with-delegation", + "network": "1", + "params": { + "symbol": "API3", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "symbol": "API3", + "address": "0x0b38210ea11411557c13457D4dA7dC6ea731B88a", + "decimals": 18 + } + }, + { + "name": "math", + "params": { + "symbol": "API3 (staked)", + "operation": "divide", + "operands": [ + { + "type": "strategy", + "strategy": { + "name": "math", + "params": { + "symbol": "API3 (staked, times total stake)", + "operation": "multiply", + "operands": [ + { + "type": "strategy", + "strategy": { + "name": "contract-call", + "params": { + "symbol": "Pool shares (own plus delegated)", + "address": "0x6dd655f10d4b9E242aE186D9050B68F725c76d76", + "decimals": 18, + "args": ["%{address}"], + "methodABI": { + "name": "userVotingPower", + "type": "function", + "stateMutability": "view", + "inputs": [ + { + "name": "userAddress", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + } + } + } + }, + { + "type": "strategy", + "strategy": { + "name": "contract-call", + "params": { + "symbol": "Pool total stake", + "address": "0x6dd655f10d4b9E242aE186D9050B68F725c76d76", + "decimals": 18, + "args": [], + "methodABI": { + "name": "totalStake", + "type": "function", + "stateMutability": "view", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + } + } + } + } + ] + } + } + }, + { + "type": "strategy", + "strategy": { + "name": "contract-call", + "params": { + "symbol": "Pool total shares", + "address": "0x6dd655f10d4b9E242aE186D9050B68F725c76d76", + "decimals": 18, + "args": [], + "methodABI": { + "name": "totalShares", + "type": "function", + "stateMutability": "view", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + } + } + } + } + ] + } + }, + { + "name": "contract-call", + "params": { + "symbol": "API3 (deposited)", + "address": "0x6dd655f10d4b9E242aE186D9050B68F725c76d76", + "decimals": 18, + "args": ["%{address}"], + "output": "unstaked", + "methodABI": { + "name": "users", + "type": "function", + "stateMutability": "view", + "inputs": [ + { + "name": "userAddress", + "type": "address" + } + ], + "outputs": [ + { + "name": "unstaked", + "type": "uint256" + }, + { + "name": "vesting", + "type": "uint256" + }, + { + "name": "unstakeAmount", + "type": "uint256" + }, + { + "name": "unstakeShares", + "type": "uint256" + }, + { + "name": "unstakeScheduledFor", + "type": "uint256" + }, + { + "name": "lastDelegationUpdateTimestamp", + "type": "uint256" + }, + { + "name": "lastProposalTimestamp", + "type": "uint256" + } + ] + } + } + }, + { + "name": "contract-call", + "params": { + "symbol": "API3 (unstaking)", + "address": "0x6dd655f10d4b9E242aE186D9050B68F725c76d76", + "decimals": 18, + "args": ["%{address}"], + "output": "unstakeAmount", + "methodABI": { + "name": "users", + "type": "function", + "stateMutability": "view", + "inputs": [ + { + "name": "userAddress", + "type": "address" + } + ], + "outputs": [ + { + "name": "unstaked", + "type": "uint256" + }, + { + "name": "vesting", + "type": "uint256" + }, + { + "name": "unstakeAmount", + "type": "uint256" + }, + { + "name": "unstakeShares", + "type": "uint256" + }, + { + "name": "unstakeScheduledFor", + "type": "uint256" + }, + { + "name": "lastDelegationUpdateTimestamp", + "type": "uint256" + }, + { + "name": "lastProposalTimestamp", + "type": "uint256" + } + ] + } + } + }, + { + "name": "contract-call", + "params": { + "symbol": "API3 (Morpho collateral)", + "address": "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb", + "decimals": 18, + "args": [ + "0x70dfbd2705d10d5dfb4d89e07e2bb860b5a624ce2f97e209ab9f5308bf6c67a1", + "%{address}" + ], + "output": "collateral", + "methodABI": { + "name": "position", + "type": "function", + "stateMutability": "view", + "inputs": [ + { + "name": "id", + "type": "bytes32" + }, + { + "name": "user", + "type": "address" + } + ], + "outputs": [ + { + "name": "supplyShares", + "type": "uint256" + }, + { + "name": "borrowShares", + "type": "uint128" + }, + { + "name": "collateral", + "type": "uint128" + } + ] + } + } + } + ] + } + } + ], + "voting": { + "type": "basic", + "delay": 0, + "period": 604800, + "quorum": 10000000, + "hideAbstain": true, + "privacy": "" + }, + "validation": { + "name": "basic", + "params": { + "minScore": 500000 + } + }, + "voteValidation": { + "name": "any", + "params": {} + }, + "filters": { + "onlyMembers": false + }, + "admins": [], + "members": [], + "moderators": [], + "treasuries": [ + { + "name": "Vault", + "address": "0x8e03609ed680B237C7b6f8020472CA0687308b24", + "network": "1" + }, + { + "name": "Agent", + "address": "0xBBE0dE9757F93e3306aDBfeBE906AB285EDD13DA", + "network": "1" + }, + { + "name": "Treasury", + "address": "0xb7fa2Cb302C5dC127e61F27b92beC411cbf0AE4A", + "network": "1" + } + ] +} From 0c7487b1dde164c67591c2f102b6bc2b3703857e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Wed, 28 Oct 2026 12:10:11 +0700 Subject: [PATCH 08/11] Fold the Snapshot assumptions into verification, treasuries section, governing proposal link, keep the DAO timing rules --- docs/dao/api3-token.md | 2 +- docs/dao/index.md | 18 ++++++----- docs/dao/sidebar.js | 4 --- docs/dao/snapshot-assumptions.md | 52 -------------------------------- docs/dao/verification.md | 21 +++++++------ docs/dao/voting-power.md | 6 ++-- docs/dao/voting.md | 14 ++++----- 7 files changed, 33 insertions(+), 84 deletions(-) delete mode 100644 docs/dao/snapshot-assumptions.md diff --git a/docs/dao/api3-token.md b/docs/dao/api3-token.md index ca20088e..75651b62 100644 --- a/docs/dao/api3-token.md +++ b/docs/dao/api3-token.md @@ -24,7 +24,7 @@ Minting is restricted to addresses its owner marks as minters. The DAO pool minted the staking rewards of the legacy DAO. The migration to Snapshot closed the supply. -The pool's minter status was revoked, a final mint to the DAO's treasury multisig brought the supply to 200,000,000 API3, and the contract's ownership was transferred to the dead address, so no address can mint and none can be made a minter. +The pool's minter status was revoked, a final mint to the DAO's [Treasury multisig](/dao/#treasuries) brought the supply to 200,000,000 API3, and the contract's ownership was transferred to the dead address, so no address can mint and none can be made a minter. Any holder can burn their own API3 by opting in as a burner on the contract and calling `burn`. Nothing can mint it back, so the supply can only fall from here. diff --git a/docs/dao/index.md b/docs/dao/index.md index 0542cf9c..db4493d5 100644 --- a/docs/dao/index.md +++ b/docs/dao/index.md @@ -25,20 +25,22 @@ For quick reference, you can copy-paste [`llms-full.txt`](https://docs.api3.org/ - [Voting](/dao/voting) is a gasless signature on snapshot.box, For or Against, during the seven days a proposal is open. Anyone with 500,000 API3 of voting power can [post a proposal](/dao/voting#posting-a-proposal), and it passes with at least 10,000,000 API3 in favour and more votes for than against. - [Independent verification](/dao/verification) recomputes a result from the chain before the API3 multisigs execute it. - See [Snapshot assumptions](/dao/snapshot-assumptions) for the remaining trust in vote collection and the space's settings. ## Governing proposal -The primary DAO migration proposal in the [legacy DAO dashboard](https://api3.eth.limo/#/history) sets out the governance rules, mandate and charter. +The [DAO migration proposal](https://api3.eth.limo/#/history/primary-17), passed in the legacy DAO, sets out the governance rules, the mandate and the charter. These pages explain how to participate under those rules. -## DAO multisigs +## Treasuries -| Multisig | Role | -| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| [Vault](https://etherscan.io/address/0x8e03609ed680B237C7b6f8020472CA0687308b24) | Controls `api3.eth` and its Snapshot settings. Posts proposals and votes with the pool stake delegated to it. | -| [Agent](https://etherscan.io/address/0xBBE0dE9757F93e3306aDBfeBE906AB285EDD13DA) | Executes operational proposals. Its own voting power is delegated to a dead address. | -| [Treasury](https://etherscan.io/address/0xb7fa2Cb302C5dC127e61F27b92beC411cbf0AE4A) | Holds the DAO's treasury API3. Its own voting power is delegated to a dead address, and it does not post proposals. | +The space lists three multisigs as its treasuries. +They hold the DAO's assets and execute what passes (see [Execution](/dao/voting#execution)). + +| Multisig | Role | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| [Vault](https://etherscan.io/address/0x8e03609ed680B237C7b6f8020472CA0687308b24) | Controls `api3.eth` and its Snapshot settings. Posts proposals and votes with the pool stake delegated to it. | +| [Agent](https://etherscan.io/address/0xBBE0dE9757F93e3306aDBfeBE906AB285EDD13DA) | Holds the DAO's assets other than API3. Its own voting power is delegated to a dead address. | +| [Treasury](https://etherscan.io/address/0xb7fa2Cb302C5dC127e61F27b92beC411cbf0AE4A) | Holds the DAO's API3. Its voting power is delegated to a dead address, and it does not post proposals. | Changes to the Snapshot settings and other rules of governance follow the [rule-change execution period](/dao/voting#execution). diff --git a/docs/dao/sidebar.js b/docs/dao/sidebar.js index 2105c802..ef481edb 100644 --- a/docs/dao/sidebar.js +++ b/docs/dao/sidebar.js @@ -19,8 +19,4 @@ module.exports = [ text: 'Independent verification', link: '/dao/verification', }, - { - text: 'Snapshot assumptions', - link: '/dao/snapshot-assumptions', - }, ]; diff --git a/docs/dao/snapshot-assumptions.md b/docs/dao/snapshot-assumptions.md deleted file mode 100644 index f87b6a8b..00000000 --- a/docs/dao/snapshot-assumptions.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Snapshot assumptions -pageHeader: DAO -outline: deep ---- - - - -# Snapshot assumptions - -Snapshot voting is off-chain. -The [independent verification](/dao/verification) recomputes voting power and the tally, while vote collection still relies on Snapshot. - -## Vote collection - -The Snapshot sequencer receives signed votes and decides whether they arrived before the deadline. -The hub supplies the list of accepted votes. -A signature proves who signed a vote and what they signed, including its timestamp, but does not independently prove when Snapshot received it. - -Snapshot cannot forge a voter's signature. -It could omit a signed vote or accept one after the deadline with a backdated signed timestamp. -Recomputing the final tally alone cannot rule those cases out. -Voters should check that their vote appears in the proposal's vote list. - -The verification tool archives the vote list on each run and compares it with the previous archive. -Running it at least daily while voting is open, and again at the close, helps detect votes that were inserted, removed or changed. -Those archives improve visibility into changes, but do not remove the trust in Snapshot's receipt and inclusion of votes. - -## Proposal timing - -Snapshot sets a proposal's creation time when the sequencer accepts it. -If its signed start time is in the past, the sequencer moves the start forward and keeps the full configured voting period. -For `api3.eth`, voting starts without a delay and runs for seven days. -Time spent collecting Safe confirmations does not shorten that period. - -The snapshot block is still the block chosen when preparing the proposal. -It can become too old while confirmations are collected, so a proposal must also meet the DAO's [snapshot block window](/dao/voting#snapshot-block). - -## Voting power and settings - -The verifier reads balances and contract state from an Ethereum archive RPC at the proposal's snapshot block, and delegations from the delegate registry's events. -It does not use Snapshot's score API or delegation subgraph for that recomputation. -The RPC must serve the correct Ethereum history. - -The recomputation uses the strategies recorded on the proposal. -Agreement with those strategies does not itself establish that a strategy or settings change was authorized. -The API3 multisigs also check that the proposal uses the DAO's approved governance rules before executing it. - -## Execution - -A passed Snapshot vote does not automatically move funds or change contracts. -The [DAO multisigs](/dao/#dao-multisigs) execute the verified result under the [governing proposal](/dao/#governing-proposal), including the execution period for a change to the rules of governance. diff --git a/docs/dao/verification.md b/docs/dao/verification.md index 17d1915f..856c90c3 100644 --- a/docs/dao/verification.md +++ b/docs/dao/verification.md @@ -8,9 +8,14 @@ outline: deep # Independent verification -Before executing a proposal, the API3 multisigs recompute its result from the chain with the [Snapshot voting verifier](https://github.com/api3dao/snapshot-voting-verifier), never taking a result off a website. +The DAO relies on Snapshot to run its votes. +The space settings, the strategies that compute voting power and the sequencer and hub that collect and tally the votes are Snapshot's, and the DAO trusts them to work as described. +A vote's signature proves who signed it and what they signed, not when Snapshot received it: Snapshot cannot forge a vote, but it could omit a signed vote or accept one after the deadline with a backdated timestamp. +Voters should check that their vote appears in the proposal's vote list while voting is open. + +The DAO verifies the rest. +A passed vote moves nothing by itself: before executing a proposal, the [API3 multisigs](/dao/#treasuries) recompute its result from the chain with the [Snapshot voting verifier](https://github.com/api3dao/snapshot-voting-verifier), never taking a result off a website. The tool's source is public, anyone can run it on any proposal of the space, and its README says how. -The remaining trust in Snapshot's vote collection and settings is described under [Snapshot assumptions](/dao/snapshot-assumptions). The DAO's rules also go further than Snapshot's settings can express, such as counting only the votes in favour toward the quorum and the window for the snapshot block. The verification enforces those too, so a proposal that Snapshot shows as passed can still fail a check and not be executed. @@ -20,15 +25,13 @@ The verification enforces those too, so a proposal that Snapshot shows as passed Given a proposal, the tool reads every vote the Snapshot hub lists and establishes, independently of Snapshot's own computation, that: - Every listed vote was signed by the voter it is listed under, names one of the proposal's choices and has a signed timestamp within the voting window. -- Every listed voter's power matches the proposal's recorded strategies at the proposal's block, delegations included (see [Voting power](/dao/voting-power)). +- Every listed voter's power matches the proposal's recorded strategies at the proposal's block, delegations included, recomputed from an Ethereum archive node and the delegate registry rather than from Snapshot's scoring service (see [Voting power](/dao/voting-power)). - The tally follows from those powers. -- The proposal met the posting rules: the proposer's 500,000 API3 at the proposal's block, a snapshot block within its window (see [Snapshot block](/dao/voting#snapshot-block)), and seven days of voting with no delay. +- The proposal met the posting rules: the proposer's 500,000 API3 at the proposal's block, a snapshot block and a signed timestamp within their windows (see [Snapshot block](/dao/voting#snapshot-block)), and seven days of voting with no delay. It then judges the result on what it recomputed: at least 10,000,000 API3 in favour and more votes for than against (see [Passing](/dao/voting#passing)). The run ends by saying whether the result is verified, and lists every problem it found. -`VERIFIED` means the listed votes and tally match the recomputation. -A verified result can still be a rejected proposal, so execution also requires `Outcome: passed`. -A proposal that fails a check is not executed. +A verified result is one whose votes and tally match the recomputation, so it can still be a rejected proposal, and only a verified and passed proposal is executed. -The tool does not judge whether a proposal is a change of the rules of governance. -The multisig signers read that from the proposal's content (see [Execution](/dao/voting#execution)). +Matching the recorded strategies does not by itself establish that a change to the strategies or the space settings was authorized, and the tool does not judge whether a proposal is a change of the rules of governance. +The multisig signers check both from the proposal's content (see [Execution](/dao/voting#execution)). diff --git a/docs/dao/voting-power.md b/docs/dao/voting-power.md index afffede1..9567de77 100644 --- a/docs/dao/voting-power.md +++ b/docs/dao/voting-power.md @@ -78,8 +78,8 @@ Your wallet balance, deposits, unstaking amount and collateral still vote with y The pool refuses a delegation change within a week of the last one. After delegating, you wait a week before you can undelegate or change the delegate, and after undelegating, a week before you can delegate again. -The legacy dashboard is retained for unstaking and withdrawing. -To inspect or change a pool delegation, open the [pool contract on Etherscan](https://etherscan.io/address/0x6dd655f10d4b9E242aE186D9050B68F725c76d76#code): +The legacy dashboard is retained for unstaking and withdrawing, and [its documentation](https://dao-docs.api3.org/members/voting.html) describes how pool delegation worked there. +To inspect or change a pool delegation now, open the [pool contract on Etherscan](https://etherscan.io/address/0x6dd655f10d4b9E242aE186D9050B68F725c76d76#code): 1. Under **Read Contract**, call `userDelegate` with your wallet address to see its current delegate. The zero address means no pool delegation. @@ -128,4 +128,4 @@ To delegate, open the [Delegates](https://snapshot.box/#/s:api3.eth/delegates) p - Collateral on Morpho counts for the owner of the position, in the Kabu API3/USDC market only. Borrowing against it changes nothing. - The Vault votes with the pool stake delegated to it. - The Agent and Treasury delegate their own voting power to a dead address and do not vote (see [DAO multisigs](/dao/#dao-multisigs)). + The Agent and Treasury delegate their own voting power to a dead address and do not vote (see [Treasuries](/dao/#treasuries)). diff --git a/docs/dao/voting.md b/docs/dao/voting.md index 3bcea458..02c54267 100644 --- a/docs/dao/voting.md +++ b/docs/dao/voting.md @@ -49,18 +49,18 @@ Proposals are discussed on the [API3 Discord](https://discord.gg/api3dao). 4. Publish by signing. Voting starts when the sequencer accepts the proposal. -To post from a Safe, open snapshot.box in the Safe app, collect the required confirmations and deliver the signed proposal to Snapshot. +To post from a Safe, open snapshot.box in the Safe app, collect the required confirmations within a day of the first signature and deliver the signed proposal to Snapshot. Check that the proposal appears in the space. -The snapshot block is fixed when preparing the proposal, so collect the confirmations and submit it while that block is still within the [allowed window](#snapshot-block). The author can delete their own proposal. ### Snapshot block The author picks the block when posting, and Snapshot only checks that the block exists. -The DAO's rule is that the block is not later than the proposal's creation and at most 7,200 blocks, about a day, before the proposal was posted. -A proposal measured at a block outside this window is not verified and is not executed. -Snapshot sets the creation time when the sequencer accepts the proposal. -A signed start time in the past is moved forward so that voting still runs for the full seven days (see [Snapshot assumptions](/dao/snapshot-assumptions#proposal-timing)). +The DAO's rule is that the block is not later than the signed timestamp and at most 7,200 blocks, about a day, before the proposal was posted, and that the proposal reaches Snapshot within a day of its signed timestamp. +A proposal outside these windows is not verified and is not executed. + +Snapshot sets the proposal's creation time when its sequencer accepts it and moves a signed start time in the past forward, so voting always runs the full seven days from acceptance. +The snapshot block is fixed when the proposal is prepared, so a Safe has to collect its confirmations and deliver the proposal while the block is still within the window. ## Passing @@ -72,7 +72,7 @@ The [independent verification](/dao/verification) applies the rule. ## Execution -Within seven days of passing, the API3 multisigs verify the result and execute the proposal. +Within seven days of passing, the [API3 multisigs](/dao/#treasuries) verify the result and execute the proposal. A proposal that changes the rules of governance executes 30 days after passing, and a proposal that cancels such a change executes within seven days. The rules of governance are the voting rules, the DAO's [mandate and charter](/dao/#governing-proposal), the voting power strategy, the space settings, the verification tool, the `api3.eth` name and which multisig holds the treasury. From d56c87d43978a6c66e37cf6e902308e0e98461f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Wed, 28 Oct 2026 17:42:19 +0700 Subject: [PATCH 09/11] The Vault holds the assets other than API3, the Agent runs operations --- docs/dao/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/dao/index.md b/docs/dao/index.md index db4493d5..53976d98 100644 --- a/docs/dao/index.md +++ b/docs/dao/index.md @@ -36,11 +36,11 @@ These pages explain how to participate under those rules. The space lists three multisigs as its treasuries. They hold the DAO's assets and execute what passes (see [Execution](/dao/voting#execution)). -| Multisig | Role | -| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -| [Vault](https://etherscan.io/address/0x8e03609ed680B237C7b6f8020472CA0687308b24) | Controls `api3.eth` and its Snapshot settings. Posts proposals and votes with the pool stake delegated to it. | -| [Agent](https://etherscan.io/address/0xBBE0dE9757F93e3306aDBfeBE906AB285EDD13DA) | Holds the DAO's assets other than API3. Its own voting power is delegated to a dead address. | -| [Treasury](https://etherscan.io/address/0xb7fa2Cb302C5dC127e61F27b92beC411cbf0AE4A) | Holds the DAO's API3. Its voting power is delegated to a dead address, and it does not post proposals. | +| Multisig | Role | +| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Vault](https://etherscan.io/address/0x8e03609ed680B237C7b6f8020472CA0687308b24) | Holds the DAO's assets other than API3 and controls `api3.eth` and its Snapshot settings. Posts proposals and votes with the pool stake delegated to it. | +| [Agent](https://etherscan.io/address/0xBBE0dE9757F93e3306aDBfeBE906AB285EDD13DA) | Runs day-to-day operations and holds only what they need. Its own voting power is delegated to a dead address. | +| [Treasury](https://etherscan.io/address/0xb7fa2Cb302C5dC127e61F27b92beC411cbf0AE4A) | Holds the DAO's API3. Its voting power is delegated to a dead address, and it does not post proposals. | Changes to the Snapshot settings and other rules of governance follow the [rule-change execution period](/dao/voting#execution). From eea29068debeb3b1d009d16f2c9aa1bbb91cf00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Wed, 28 Oct 2026 17:42:19 +0700 Subject: [PATCH 10/11] The hub serves a zero voting delay as null --- scripts/fixtures/dao-space.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fixtures/dao-space.json b/scripts/fixtures/dao-space.json index 354c5b93..4ecf7983 100644 --- a/scripts/fixtures/dao-space.json +++ b/scripts/fixtures/dao-space.json @@ -264,7 +264,7 @@ ], "voting": { "type": "basic", - "delay": 0, + "delay": null, "period": 604800, "quorum": 10000000, "hideAbstain": true, From af2c78c507e9250c5746e158c81a2da9b12d8843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Wed, 28 Oct 2026 18:26:14 +0700 Subject: [PATCH 11/11] The snapshot block window is the only timing rule on the block --- docs/dao/verification.md | 2 +- docs/dao/voting.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dao/verification.md b/docs/dao/verification.md index 856c90c3..14622b50 100644 --- a/docs/dao/verification.md +++ b/docs/dao/verification.md @@ -27,7 +27,7 @@ Given a proposal, the tool reads every vote the Snapshot hub lists and establish - Every listed vote was signed by the voter it is listed under, names one of the proposal's choices and has a signed timestamp within the voting window. - Every listed voter's power matches the proposal's recorded strategies at the proposal's block, delegations included, recomputed from an Ethereum archive node and the delegate registry rather than from Snapshot's scoring service (see [Voting power](/dao/voting-power)). - The tally follows from those powers. -- The proposal met the posting rules: the proposer's 500,000 API3 at the proposal's block, a snapshot block and a signed timestamp within their windows (see [Snapshot block](/dao/voting#snapshot-block)), and seven days of voting with no delay. +- The proposal met the posting rules: the proposer's 500,000 API3 at the proposal's block, a snapshot block within its window (see [Snapshot block](/dao/voting#snapshot-block)), and seven days of voting with no delay. It then judges the result on what it recomputed: at least 10,000,000 API3 in favour and more votes for than against (see [Passing](/dao/voting#passing)). The run ends by saying whether the result is verified, and lists every problem it found. diff --git a/docs/dao/voting.md b/docs/dao/voting.md index 02c54267..28023b83 100644 --- a/docs/dao/voting.md +++ b/docs/dao/voting.md @@ -56,8 +56,8 @@ The author can delete their own proposal. ### Snapshot block The author picks the block when posting, and Snapshot only checks that the block exists. -The DAO's rule is that the block is not later than the signed timestamp and at most 7,200 blocks, about a day, before the proposal was posted, and that the proposal reaches Snapshot within a day of its signed timestamp. -A proposal outside these windows is not verified and is not executed. +The DAO's rule is that the block is at most 7,200 blocks, about a day, before Snapshot received the proposal. +A proposal outside this window is not verified and is not executed. Snapshot sets the proposal's creation time when its sequencer accepts it and moves a signed start time in the past forward, so voting always runs the full seven days from acceptance. The snapshot block is fixed when the proposal is prepared, so a Safe has to collect its confirmations and deliver the proposal while the block is still within the window.