Canonical Aragon addresses and ABIs — one JSON per chain, one JSON per contract. Consumed as a git submodule, no NPM, no build step.
Two symmetrical trees:
addresses/—<chainId>.jsonper chain, with<network>.jsonsymlinks for grep-friendly names. Every file has the same shape (Zod source of truth inscripts/schema.ts).abi/—<component>/<Contract>.jsonper contract, plus generatedindex.tsfiles with viem-compatibleas constbindings. Every version referenced byaddresses/has its own<component>/v<version>/folder; the component root mirrors the latest one.
git submodule add https://github.com/aragon/artifacts-hub lib/artifacts-hubConcrete example — resolve a plugin's current build from the address book, pair it with the ABI, make a typed viem call:
import mainnet from "./lib/artifacts-hub/addresses/mainnet.json";
import { MultisigABI } from "./lib/artifacts-hub/abi/multisig";
import { createPublicClient, http } from "viem";
import { mainnet as mainnetChain } from "viem/chains";
// From the address book:
const currentBuild = mainnet.plugins.multisig.versions.find(v => v.current)!;
// currentBuild.setup + currentBuild.implementation are the current setup contract
// and its plugin implementation on this chain.
const client = createPublicClient({ chain: mainnetChain, transport: http() });
const proposalCount = await client.readContract({
address: "0x…YourMultisigDAOInstance…",
abi: MultisigABI,
functionName: "proposalCount",
});Alternative import shapes:
// Grab several components under a single namespace import
import { admin, multisig, tokenVoting } from "./lib/artifacts-hub/abi";
admin.AdminABI; tokenVoting.TokenVotingABI;
// Plain JSON when you don't need viem's `as const` inference
import Multisig from "./lib/artifacts-hub/abi/multisig/Multisig.json";
// A specific version: same shape, under v<release>.<build> (plugins) or v<protocolVersion> (osx)
import { TokenVotingABI } from "./lib/artifacts-hub/abi/token-voting/v1.3";Every file has the same top-level shape. Common lookups:
| Want | Path |
|---|---|
| Chain identity | chainId, network |
| Current OSx version | osx.versions.find(v => v.current) |
| DAO implementation (cloned by DAOFactory) | osx.versions.find(v => v.current).core.daoBase |
| Management DAO on this chain | management.dao |
| Chain's ENSRegistry | ens.registry |
| Latest plugin build (setup + impl) | plugins["<slug>"].versions.find(v => v.current) |
| All historical plugin builds | plugins["<slug>"].versions |
| Installable plugin builds | plugins["<slug>"].versions.filter(v => !v.placeholder) |
| Governance token base (ERC20) | plugins["token-voting"].other.governanceERC20 |
| Current SafeOwner condition factory | conditions.factories.find(f => f.current).address |
| ProtocolFactory (when the chain has one) | deployers.protocolFactory |
Addresses are lowercased on parse — never rely on checksum casing for identity. Arrays are ascending, and the current entry carries current: true.
Placeholder builds. Chains that start a plugin at a later build publish the earlier build numbers with the OSx PlaceholderSetup, so build numbers line up across chains. Those entries carry placeholder: true: they are not the plugin, can't be installed and are never current. ingest and refresh-plugin detect them on-chain (the setup reverts prepareInstallation with PlaceholderSetupCannotBeUsed()). implementation is omitted when a setup has none (placeholders, zkSync Admin); it is never 0x0.
Schema outline:
osx.versions[]— one entry per protocol version deployed on the chain.core.*= the framework contracts,helpers.*= auxiliary singletons (globalExecutor).management.*— the OSx-managing DAO + its multisig plugin instance.ens.*— Aragon's ENS stack (registry, plugin- and dao-subdomain registrars, public resolver).plugins.<slug>— one entry per plugin (admin,multisig,token-voting,spp,lock-to-vote,crosschain) withrepo,ens,maintainer, versioned setup+impl, and optionalother{}for plugin-scoped auxiliary addresses.conditions.factories[]— versioned condition factories deployed on the chain.deployers.*— one-shot deployment tools (currently justprotocolFactory).deprecated: true(optional, top-level) — the chain is retired. Entries stay so consumers can still resolve historical addresses, butjust coverageexcludes it from the required-slot gate and new plugin ingest should not target it.
See addresses/README.md for the tree layout and file-naming rules; scripts/schema.ts is the Zod source of truth.
One folder per source component; one JSON per contract. Slugs match addresses/ where applicable:
| slug | source repo |
|---|---|
osx |
aragon/osx |
admin |
aragon/admin-plugin |
multisig |
aragon/multisig-plugin |
token-voting |
aragon/token-voting-plugin |
spp |
aragon/staged-proposal-processor-plugin |
lock-to-vote |
aragon/lock-to-vote-plugin |
crosschain |
aragon/crosschain |
conditions |
aragon/condition-library |
protocol-factory |
aragon/protocol-factory |
Versions live next to the latest:
abi/<slug>/v<version>/<Contract>.json # the ABI at that version
abi/<slug>/v<version>/index.ts # generated, same exports as the root
abi/<slug>/<Contract>.json # generated copy of the latest version folder
abi/<slug>/index.ts # export * from "./<latest>/index.ts"
Plugins use v<release>.<build>, matching plugins.<slug>.versions in the address book (abi/multisig/v1.2). OSx uses its protocol version (abi/osx/v1.4.0). Components without an on-chain version (crosschain, conditions, protocol-factory) start at v1.1. A version folder can gain contracts over time, but an ABI already in it never changes: a changed ABI means a new version.
Placeholder builds (placeholder: true in the address book) point to the OSx PlaceholderSetup, not to the plugin, so abi/<slug>/v<version> does not describe them. just verify-abi skips and lists them per chain.
Each folder carries a generated index.ts exporting every contract as <Contract>ABI with as const (viem-friendly). The top-level abi/index.ts re-exports each component under a camelCase namespace (lock-to-vote → lockToVote, etc.).
Bytecode is deliberately omitted — pair a JSON ABI with the on-chain address from addresses/, or forge build the source repo when you need artefacts.
See abi/README.md for consuming patterns.
Every chain deployed via ProtocolFactory reconstructs from a single address:
just ingest <chainId> <rpcUrl> <protocolFactoryAddress> [network]That reads getDeployment() on the PF, calls protocolVersion() on the DAOFactory, enumerates every plugin repo's versions on-chain, resolves ENS, and writes addresses/<chainId>.json + the network symlink. Re-running is idempotent — same PF address, same output (modulo new plugin builds).
For chains that predate the ProtocolFactory (mainnet, arbitrum, base, …), the address book is hand-managed. Edit the JSON directly and let just validate + just coverage gate the changes.
Plugin repos emit a self-describing envelope after each on-chain deployment:
<plugin-repo>/artifacts/artifacts-<network>-<timestamp>.json
The envelope's plugin subtree is exactly the AddressBook Plugin schema, so a
future ingest step can drop it straight into addresses/<chainId>.json under
plugins.<slug> — no re-derivation, no per-plugin adapter code.
{
"chainId": 11155111,
"network": "sepolia",
"timestamp": 1789660800,
"slug": "crosschain",
"plugin": {
"repo": "0xaa77…2603",
"ens": "crosschain.plugin.dao.eth",
"maintainer": "0xca83…d324",
"versions": [
{ "release": 1, "build": 1,
"setup": "0xdce3…ae2e",
"implementation": "0x817b…866f",
"current": true }
]
}
}Zod source of truth: PluginArtifact in scripts/schema.ts.
Reserved slugs (canonical ENS + display label) live in
scripts/lib/plugin-catalog.ts.
just import-plugin merges those envelopes into addresses/<chainId>.json
under plugins.<slug>:
just import-plugin path/to/artifacts-<network>-<timestamp>.json # one file
just import-plugin path/to/plugin-artifacts-directory # sweep every *.json in it (timestamp-ascending)
just import-plugin --dry-run <path> # preview only, no writesInvariants the script enforces (source: scripts/import-plugin.ts):
- Identity fields (
repo,ens,maintainer) are constants per (chainId, slug). Any mismatch against an existing entry is a hard error — ownership transfers or address changes belong in a reviewed PR, not through import. - Versions are additive. Same
(release, build)with the same(setup, implementation, other)is a no-op (re-runs are safe). Same(release, build)with different addresses is a hard error. current: trueis derived: after any merge, only the highest non-placeholder(release, build)carries it.versions[]is re-sorted ascending.- Missing fields are adopted. A known version that lacks
implementationorplaceholder: truepicks it up from the incoming data (reported as an update). A placeholder with an implementation in the book is a hard error. - Chain must be tracked (
addresses/<chainId>.jsonmust exist) and must not be markeddeprecated: true. Ingest new chains withjust ingestfirst. - Final book is re-parsed through Zod before writing, so addresses land canonically lowercased regardless of the artifact's casing.
Directory mode + timestamp-ascending order means re-sweeping the same folder is a total no-op — safe to run in CI on every push, or as a follow-up to a batch of deploys.
just import-plugin only ever adds what's in the envelope, which is fine when
you've captured every deploy through the artifact flow. When a plugin repo
already has versions on-chain that predate the envelope flow (or that were
published through some other path), just refresh-plugin reads them straight
from the PluginRepo and merges them in:
just refresh-plugin <slug> <chainId> <rpcUrl>
just refresh-plugin --dry-run <slug> <chainId> <rpcUrl>It looks up plugins.<slug>.repo in addresses/<chainId>.json, calls the
same on-chain enumerator just ingest uses (walking latestRelease() +
buildCount(release) and reading getVersion(tag) per build), then hands
the result to the same merge core as just import-plugin — so identity
mismatches, (release, build) collisions with different addresses, and
deprecated-chain refusals all behave identically. New builds append and
current: true re-stamps on the highest non-placeholder (release, build), and placeholder builds are flagged as they are read.
Use it whenever a chain's plugins.<slug>.versions looks short — typically
right after adopting the envelope flow on a plugin that has historical
builds, or as a periodic sanity sweep across all chains.
Every version folder is pinned in abi/sources.json:
"spp": {
"v1.3": { "source": "git:https://github.com/aragon/staged-proposal-processor-plugin#5715646f…" }
},
"multisig": {
"v1.2": { "source": "npm:@aragon/osx-ethers@1.3.0", "contracts": ["Multisig", "MultisigSetup", "IMultisig"] }
}git:<https url>#<sha>: cloned into a temp dir,forge build, and every contract, interface and library declared undersrc/with a non-empty ABI is taken. Abstract contracts are skipped: they can't be deployed, and their functions are already in the ABI of every contract that extends them. Needsgitandforge. A repo or submodule that isn't reachable over HTTPS (private) is cloned from the sibling checkout../<repo>instead, so keep one next to this repo.npm:<package>@<version>: for builds that predate the Foundry repos (Hardhat era). Reads<Name>ABIexports or legacy TypeChain<Name>__factory.abi.contracts(optional) narrows a source that bundles other components. npm packages carry no "abstract" information, so their list must leave abstract contracts out.
To add a version: add its entry to sources.json, then
just import-abi <slug> <version> # fetch into abi/<slug>/<version>/, regenerate index.ts + root copies
just verify-abi # check the new ABI against every deployed setup/implementationimport-abi never changes an existing ABI: identical files are a no-op, new contracts are added, and a different ABI for an existing contract is a hard error. To replace a wrong file on purpose, delete it in a reviewed commit and re-import.
verify-abi takes RPC URLs from a temporary clone of aragon/just-foundry (networks/<network>.env) and checks that every function selector of the version's ABI is present in the deployed code. Deprecated chains are skipped.
just generate-abi rebuilds every generated file from the version folders; just generate-abi --check (part of just validate) fails when one is stale.
just validate # Zod-validate every addresses/*.json + abi/ generated files up to date
just test # unit tests for scripts/
just coverage # per-network table + section gap summary
just coverage --gaps # invert to slot-centric view
just coverage --json # machine-readable output for scripting
just ingest <chainId> <rpcUrl> <pfAddress> [network]
just import-plugin <path> # merge PluginArtifact envelope(s) into addresses/<chainId>.json
just refresh-plugin <slug> <chainId> <rpcUrl> # enumerate all on-chain versions of a plugin, merge into the book
just import-abi [slug] [version] # fetch the ABI versions pinned in abi/sources.json
just verify-abi [network] # check versioned ABIs against deployed plugin contracts
just generate-abi [--check] # regenerate abi/**/index.ts + root copies of the latest versionjust coverage exits non-zero when a chain is missing a required slot; optional gaps (e.g. deployers.protocolFactory on chains that predate the PF concept) don't fail the gate. See scripts/coverage.ts for the exact required/optional split.