This repository contains tools that make on-chain governance safer, including automated scripts that apply checks to live proposals to allow for better informed voting.
# Install root + frontend dependencies
bun run setup
# Copy and configure environment variables
cp .env.example .env
# Edit .env with your Tenderly credentials and RPC URLs# Run a simple simulation to verify setup
SIM_NAME=uni-transfer bun startReports are saved to reports/ folder.
# Load local RPC / explorer env first if your shell has not already done so
set -a; source .env; set +a
# Confirm the legacy BNB Wormhole receiver assumptions still match live chain state
bun run test:wormhole:bnb-legacy:liveThis check is intentionally opt-in. It validates the BNB authority ownership, confirms the modern receiver getters still revert, and verifies the legacy sequence storage slot remains readable onchain.
# Run simulation and start the frontend UI
SIM_NAME=uni-transfer bun run proposeOpen http://localhost:3000 to view the simulation results.
Copy sims/uni-transfer.sim.ts as a template:
cp sims/uni-transfer.sim.ts sims/my-proposal.sim.ts
# Edit my-proposal.sim.ts with your proposal details
SIM_NAME=my-proposal bun run propose# Check a specific Uniswap proposal by ID
./run-proposal.sh 94
# Or for other DAOs, set environment variables
DAO_NAME=Compound GOVERNOR_ADDRESS=0xc0Da02939E1441F497fd74F78cE7Decb17B66529 bun run check-proposal 43Every few hours a GitHub workflow is run which simulates all proposals for each DAO defined in governance-checks.yaml.
Reports for each proposal are saved as Markdown files associated with the workflow run.
To view the reports, navigate to this repo's Actions, select a workflow, and download the attached artifacts.
This will download a zip file containing all reports, where you can find the report you're interested in and open it in your favorite markdown viewer.
Soon, alternative viewing options will be available so you don't need to download the files.
If running the simulations locally, you can find the reports in the reports folder.
Some notes on the outputs of reports:
- If a transaction reverts, that will be reported in the state changes section
- State changes and events around the proposal execution process, such as the
ExecuteTransactionevent andqueuedTransactionsstate changes, are omitted from reports to reduce noise - Slither analysis for the timelock, governor proxy, and governor implementation is skipped to reduce noise in the output. Note that skipping analysis for the implementation on historical proposals requires an archive node, and a warning will be shown if archive data is required not available
- ETH balance changes are reported in a dedicated section, showing transfers and net balance changes for each address involved
- Permission changes (ownership transfers, role grants/revokes, timelock admin changes) are detected and surfaced as warnings, and also emitted as structured data in
structuredReport.permissionsDiff
Seatbelt writes a local cache/ directory to reduce repeated calls to external services (block explorers, Sourcify, etc.). This is used both locally and in CI (the Governance Checks workflow caches cache/ between runs).
cache/abis/: ABI JSON fetched from the configured block explorercache/verification/: contract verification status (Sourcify / block explorer)cache/contract-names/: contract names fetched from the block explorer (used when Tenderly metadata is missing)
Cache refresh behavior:
- Verification results are re-checked over time (unverified entries expire after ~24h; verified entries after ~30d)
- Contract-name entries expire after ~30d
- Stale cache entries are deleted opportunistically when read
When running simulations locally, Seatbelt writes public/simulation-results.json for the frontend. The report.structuredReport object is a stable, machine-readable representation of the report.
For the full JSON schema (including cross-chain preview fields) and consumer guidance, see docs/API.md.
If present, this is an array of permission changes detected during the simulation. Each entry has a kind plus additional fields:
ownership_transferred:{ contractAddress, previous?, next, via }role_granted/role_revoked:{ contractAddress, role: { id, name }, account, sender }timelock_admin_changed/timelock_pending_admin_changed:{ contractAddress, previous?, next, via }
This repository also includes a frontend application that allows you to visualize simulation results and create proposals.
To run the frontend with simulation results:
-
Install dependencies (once per clone):
bun run setup
-
Provide simulation results (choose one):
# Option A (recommended): run a real simulation (requires Tenderly + RPCs) SIM_NAME=uni-transfer bun run sim -
Start the frontend:
bun run propose
-
Or do both in one command:
# Run specific simulation and start frontend SIM_NAME=uni-transfer bun run propose
The frontend will be available at http://localhost:3000.
The frontend reads frontend/public/simulation-results.json via GET /api/simulation-results.
To avoid accidentally wedging the dev server/browser with oversized artifacts, the API enforces a max file size
(override with SIMULATION_RESULTS_MAX_BYTES). By default, the API also omits the markdown payload from the
response (sets report.markdownReport to ""); pass ?includeMarkdown=1 to include it.
If you deploy the frontend publicly, add platform-level rate limiting + error-rate monitoring for /api/*.
For a quick, deterministic end-to-end demo of the wallet propose → execute wiring (without Tenderly):
bun run e2e:localThis will:
- start an Anvil chain on
http://127.0.0.1:8545(chain id31337) - deploy a mock governor + target contract
- write
frontend/public/simulation-results.jsonwith a real proposal payload - start the frontend dev server configured to use that local chain
In the browser:
- Open
http://localhost:3000/action - In your wallet, switch to Localhost 31337 (RPC
http://127.0.0.1:8545) - Click Propose and confirm the tx
- In another terminal:
bun run e2e:local:set-proposed - Reload
/action, then click Execute
Notes:
- If port
8545is already in use, the script will error unless you setE2E_LOCAL_ALLOW_FALLBACK_PORT=1. - WalletConnect options in RainbowKit require
NEXT_PUBLIC_PROJECT_ID.
The frontend allows you to:
- View simulation results including state changes, events, and checks the same way reports are visualized
- Connect your wallet to sign and submit proposals using the proposal data
You will need to set up a .env.local file in the frontend folder according to the example .env.local
To add a DAO to CI, submit a pull request that adds the desired DAO_NAME and GOVERNOR_ADDRESS
to the matrix section of .github/workflows/governance-checks.yaml.
Note that currently only Compound GovernorBravo and OpenZeppelin style governors are supported.
First, create a file called .env with the following environment variables:
# Etherscan API Key, used when running Slither.
ETHERSCAN_API_KEY=yourEtherscanApiKey
# URL to your Ethereum mainnet RPC (required).
MAINNET_RPC_URL=yourMainnetRpcUrl
# URL to your Arbitrum mainnet RPC (required for cross-chain).
ARBITRUM_RPC_URL=yourArbitrumRpcUrl
# Tenderly access token.
# Access token is obtained from the Tenderly UI via Account > Authorization > Generate Access Token.
TENDERLY_ACCESS_TOKEN=yourAccessToken
# Tenderly user name.
# User name can be found in the URL of your project: https://dashboard.tenderly.co/<userName>/<project_slug>/transactions
# This is `me` for personal accounts.
TENDERLY_USER=userName
# Tenderly project slug.
# Project slug can be found in the URL of your project: https://dashboard.tenderly.co/<userName>/<project_slug>/transactions.
# The name of your tenderly project may not always be your project slug,
# and the project slug can sometimes just be `project`.
TENDERLY_PROJECT_SLUG=projectName
# Define the DAO name and the address of its governor.
DAO_NAME=Uniswap
GOVERNOR_ADDRESS=0x408ED6354d4973f66138C91495F2f2FCbd8724C3After running a simulation to generate frontend/public/simulation-results.json (see Running Simulations below), use the managed relay by default (no local Vercel setup required):
# Validate artifact only (no publish)
bun upload --validate-only
# Validate + publish via managed relay (default)
bun upload --publishA successful publish returns relay response URLs (publishId, deploymentUrl, artifactUrl, metadataUrl, and viewerUrl when configured).
For share links, use:
<viewerUrl>/p/<publishId>(preferred)<viewerUrl>?artifact=<artifactUrl>(automatic fallback if publish lookup is unavailable)
Notes:
artifactUrlpoints to the publishedsimulation-results.json.deploymentUrl(and its/) is the artifact deployment root and may render a publish landing page, not the canonical frontend viewer.
For complete publish docs (custom artifact paths, relay override, troubleshooting, and fallback guidance), see:
docs/PUBLISH_QUICKSTART.md(primary user guide)docs/PUBLISH_CONTRACT.md(publish contract)docs/PUBLISH_RELAY_OPS.md(relay runtime/ops)
There are two modes of operation:
- Run
bun startto simulate and run checks on all Governor proposals. - Alternatively, create a file called
<analysisName>.sim.tsand run a specific simulation withSIM_NAME=analysisName bun start. See the*.sim.tsfiles in thesimsfolder for examples.
When running either of those two modes locally, reports will be saved into a reports/ folder in the root of the repository.
The specific path will be ./reports/${daoName}/${governorAddress}/${proposalId}.${extension}.
The reports/ folder is gitignored, so when searching for reports in this directory your editor may hide the files by default.
To run the tests:
cd checks
bun testOr to run a specific test file:
cd checks
bun test tests/check-eth-balance-changes.test.tsCurrently, there is a test for the ETH balance changes check, which verifies that the check correctly identifies and reports ETH transfers and balance changes. As new checks are added or existing checks are modified, corresponding tests should be added to ensure their functionality. The test framework is set up to use Bun's built-in testing capabilities and can be extended to cover additional checks in the future.
Seatbelt supports any Governor Bravo or OpenZeppelin Governor contract, making it suitable for governance safety analysis across the ecosystem.
To use Seatbelt with a different DAO:
- Fork this repository to your organization
- Update environment variables in your
.envfile:DAO_NAME=YourDAO GOVERNOR_ADDRESS=0x... # Your governor contract address - Test the setup with a known proposal:
bun start
- Governor Bravo: Used by Compound, Uniswap, and many others
- OpenZeppelin Governor: Modern governor standard
The system automatically detects the governor type, so no manual configuration is needed.
Compound:
DAO_NAME=Compound
GOVERNOR_ADDRESS=0xc0Da02939E1441F497fd74F78cE7Decb17B66529
# Check all proposals
bun start
# Check specific proposal
bun run check-proposal 43Custom DAO:
DAO_NAME=YourDAO
GOVERNOR_ADDRESS=0x... # Your governor address
bun startFor hypothetical or new proposals, create a simulation file in sims/:
// sims/my-proposal.sim.ts
export const config = {
type: 'proposed', // or 'executed'
daoName: 'Compound',
governorAddress: '0xc0Da02939E1441F497fd74F78cE7Decb17B66529',
governorType: 'bravo',
// ... proposal details
};Then run: SIM_NAME=my-proposal bun start
To verify your setup works:
# Test with existing simulation
SIM_NAME=compound-43 bun startThis repository uses Husky to run secretlint before each commit, preventing accidental secret leaks. Hooks install automatically when you run bun install.
See SECURITY.md for our security policy and vulnerability reporting process.