A minimal version of all curve infrastructure for AMM in one place.
- Stableswap - pools for 2 tokens with similar value (1~1)
- Twocrypto - pools for 2 different tokens
- Tricrypto - pools for USD-pegged coins combined with any coins
- Deposit and Stake Zap - for depositing and staking LPs in one tx
- Meta Zap - for easy exchange between LP and underlying tokens
- Router - router contract for executing complicated trades using different places
- Agent and relayer for governance from mainnet DAO
- Vault for fee receiving from pools
- Reward-only gauge for incentives
- Metaregistry for AMMs
- Address provider that has all address of factories/DAO/tokens:
- 2: "Exchange Router"
- 4: "Fee Distributor"
- 7: "Metaregistry"
- 11: "TricryptoNG Factory"
- 12: "StableswapNG Factory"
- 13: "TwocryptoNG Factory"
- 18: "Spot Rate Provider"
- 19: "CRV Token"
- 20: "Gauge Factory"
- 21: "Ownership Admin"
- 22: "Parameter Admin"
- 23: "Emergency Admin"
- 24: "CurveDAO Vault"
- 25: "crvUSD Token"
- 26: "Deposit and Stake Zap"
- 27: "Stableswap Meta Zap"
Put settings file ("env") into settings directory. Example. It contains RPC url for target chain.
Project requires Python 3.11+
Install dependencies using poetry
pip install poetry==1.8.3
poetry install
Scaffold one:
python manage.py init prod/mychain
It asks for each field, reads chain_id from the RPC you give it, checks multicall3 exists
on that chain, and refuses to write anything ChainConfig would reject.
Or copy the example into settings/chains as {chain_name}.yaml and fill it in by hand.
- network_name - chain name
- chain_id - chain id
- rollup_type - can be op_stack, arb_orbit, polygon_cdk, zksync, taiko or not_rollup. Zksync rollups currently aren't fully supported
- native_wrapped_token - address of native wrapped token (can be non-eth token)
- dao - params of contracts already present on chain (script will deploy x-gov contracts, CRV and crvUSD should be bridged using native bridges)
Integration parameters
- layer - chain layer (general info)
- native_currency_symbol - symbol of native token
- public_rpc_url - rpc used in UI (only public)
Make sure you have funds at your account for gas at target chain.
- Export private key to env (don't store it in file!)
export DEPLOYER_EOA_PRIVATE_KEY={your key}
- Run deployment (replace chain_name with name of target chain you want to deploy - make sure you added chain config for this chain in previous step!)
python manage.py deploy all devnet/chain_config_filename.yaml
The path is relative to settings/chains and includes the directory.
To see what that would do first:
python manage.py deploy all prod/sonic.yaml --dry-run
Reports every contract it would deploy, upgrade or reuse, and which steps it would skip.
Needs no key, no RPC and no settings/env. Exits non-zero if anything would stop the deploy.
Or via Docker, which builds the environment for you:
docker compose up --build
Upon success, script will generate deployment file with address and other info in deployments directory. File will have the same name as chain. ABI is stored in abi folder. Deployments are reusable, so if something fails, it can be fixed and rerun.
python manage.py verify prod/sonic # what would be submitted, and where
python manage.py verify prod/sonic --submit # send them (needs ETHERSCAN_API_KEY)
Etherscan's v2 API takes Vyper through codeformat=vyper-json, on one endpoint keyed by
chain id, so a single key covers every chain it lists. The deployment file already records
what an explorer asks for — compiler version, evm version, constructor args and the source
path — so this assembles a payload rather than gathering facts.
The payload states no optimisation mode on purpose. deploy all compiles with
evm_version alone, so a # pragma optimize in the source is what chose the mode on chain,
and Vyper rejects a setting that contradicts one.
Chains missing from the Etherscan v2 chainlist fall back to Blockscout, which takes the same
standard JSON, needs no key, and matches on runtime rather than creation bytecode. It is a
fallback rather than a choice: a chain Etherscan lists is never probed for Blockscout. The
instance is asked whether it is one — most are white-labelled, so the hostname does not say —
and it publishes the compiler strings it accepts, so v0.3.10+commit.91361694 is looked up
rather than guessed.
Already-verified contracts are detected before submitting, because instances disagree about
what a re-submission means: some answer "verification started", others a bare 404.
Blueprints are skipped on every explorer: what sits on chain for one is the initcode, which nothing verifies against a source file.
When infra is deployed, run
python manage.py deploy test_pools {chain_name}
to deploy test tokens and pools + add liquidity and permorm a swap in test pool. WARNING!: these are test tokens, don't use mocks in production.
deployments/ is the cross-chain Curve address registry. Two generated files make it
readable without walking the tree or calling the GitHub API with a token:
- registry/index.json - every chain, its
configblock verbatim, and every recorded address flattened toamm.stableswap.factorykeys. - registry/schema.json - JSON Schema for a deployment file, generated from the pydantic models.
They live outside deployments/ on purpose: that directory contains only chain folders, and
consumers enumerate it.
Both are generated. Regenerate after changing any deployment file:
python manage.py index # write both
python manage.py index --check # fail if either is stale (CI runs this)
The index covers every recorded chain, including the ones this repo did not deploy — they are
hand-maintained for curve-api-core, so a registry without them would be less useful than the
directory it replaces. Each carries deployed_by_core, set from the same rule status uses to
decide what it checks, so filter on that rather than maintaining a list.
config goes out verbatim rather than as a chosen subset. curve-api-core reads config.*
straight off the file, so an allowlist here would serve a key it starts reading as undefined
until someone noticed — which had already happened to reference_token_addresses.
The index is the whole chain list in one unauthenticated request:
curl -s https://raw.githubusercontent.com/curvefi/curve-core/main/registry/index.json
Contract entries carry the address only. Anything that needs a row's version or deployment
metadata should fetch that chain's file by its file_path, which is also a raw request and
also needs no token.
Both artifacts are also published on GitHub Pages, alongside one searchable page over them — every chain, what it has per contract family, and every address linked to its explorer:
python manage.py site # write site/
CI builds and deploys it on every push to main that touches a deployment. site/ is not
committed: it is derived from registry/index.json, which is, so the HTML adds nothing to
review. The page renders from the committed artifact rather than rebuilding the index, so the
table and the JSON served beside it cannot describe different data.
It reports what is recorded, not what is live — status --onchain is what checks that.
To see what is deployed and what would change if the deployer ran again:
python manage.py status # every chain, every offline check
python manage.py status --brief # one line per finding
python manage.py status --summary # one-row-per-chain matrix
python manage.py status --chain prod/sonic # one chain
python manage.py status --only pending # a single check
python manage.py status --json out.json # machine-readable
--brief prints one line per finding with no explanations, and hides findings whose check
could not run - it says how many, so a shorter report never quietly becomes an emptier one.
--summary and --brief are alternative renderings and cannot be combined.
On a pull request, CI runs this against the branch and against its base and comments with the
difference, so a fix needs no bookkeeping and a fault already on the base branch never fails a
branch that did not cause it. New CONFIG or REQUIRED findings fail the build; everything
else reports. To see the same locally:
python manage.py status --json head.json
python manage.py compare base.json head.json
Read-only — it never sends a transaction, and manage.py skips the boa connection for this
command, so it runs on a fresh clone with no settings/env and no DEPLOYER_EOA_PRIVATE_KEY.
Exits 1 when anything is reported, so it can be used as a CI gate.
Scope is what curve-core deploys. avalanche, fantom and x_layer predate this repo — they
have no settings/chains config and no provenance on any row, so no deploy can
target them and every check ignores them.
Each check is derived from the deployer's own code rather than reimplemented, so the report
cannot drift from what deploy all actually does:
| Check | Reports | Derived from |
|---|---|---|
PENDING |
A newer _v_NNN.vy sits in contracts than the version recorded for a chain. deploy all applies these automatically — adding a contract file is enough to change what every chain gets, so run this before merging one. |
fetch_latest_contract(), version_a_gt_version_b() |
CONFIG |
A settings/chains file that ChainConfig rejects. deploy all cannot start on these, and since the config is copied into the deployment file it is usually the root cause of the matching REQUIRED finding. |
get_chain_settings() |
CONTRACTS |
Version constants the blueprint path cannot parse, contracts declaring none at all, files named so that fetch_latest_contract can never select them, folders holding two unrelated contracts that compete for one slot on their _v_NNN digits, and abi/ entries that no longer match a contract path. |
fetch_latest_contract(), the regex in deployment_file.py |
SCHEMA |
Keys in a deployment file that no model declares. Pydantic ignores them and the deployer rewrites files through model_dump(), so they are deleted the next time that chain is touched. |
the models' own model_fields |
REQUIRED |
Files that fail validation — the deployer cannot read or update that chain at all. | DeploymentConfig.model_validate() |
COVERAGE |
Chain configs with no deployment, deployments with no chain config, and file_name collisions (that field is curve-api-core's blockchain id, so a collision means one file shadows the other). |
|
INTEGRITY |
Admin roles that are null, shared between roles, or collapsed onto one address. |
Each needs working RPCs, so each is behind its own flag:
python manage.py status --onchain # every recorded address has bytecode
python manage.py status --wiring # factory pointers and ownership match the file
python manage.py status --bytecode # recompile and compare against deployed code
--changed-since REF narrows any of them to the chains whose deployment file differs from
REF, measured from the merge base so work that landed on the base branch meanwhile is not
counted, and against the working tree so an uncommitted edit still shows. CI runs it on every
pull request that touches deployments and comments with what it found on those
chains — a hand-edited address is exactly what no offline check can catch.
python manage.py status --changed-since origin/main --onchain --wiring
A probe that fails on congestion — 429, 503, a timeout — is retried with backoff, since
a dozen workers opening at once earns a rate limit from an endpoint that is perfectly
healthy. What survives that is reported two ways: an endpoint that answered nothing for a
reason waiting cannot fix is a broken public_rpc_url and a finding in its own right, while
anything else leaves those addresses unverified rather than judged.
--bytecode is the only check that proves contract_path / contract_version /
evm_version describe what is really on chain. Normal contracts must match by prefix (the
tail is immutables and constructor args); blueprints must match blueprint_bytecode minus
the 10-byte EIP-5202 wrapper that deploy_via_create2 prepends. It is slow — compilation
is cached per source, but it recompiles every distinct contract.
Vyper hashes the source into the deploy bytecode, so editing a .vy file changes what a
blueprint puts on chain even when the change is only whitespace — runtime bytecode, and so
every normal contract, is unaffected. That is why end-of-file-fixer skips
contracts: 110 of the 632 recorded rows are blueprints.
A chain deviates when someone touches it, not when someone opens a PR here, so the on-chain
checks also run on a schedule and keep one GitHub issue in sync with what they find. Every
probe goes to the public_rpc_url the deployment file already records, so the job needs no
secrets — and an endpoint that answers nothing is reported as a deviation in its own right,
since that is the URL the UI reads.
python manage.py status --onchain --wiring --json onchain.json
python manage.py monitor onchain.json --previous issue.md --body body.md --delta delta.md
The nightly run includes --bytecode, which is why it is the slow one: it recompiles every
distinct contract. CI caches ~/.vvm keyed on the deployment files, and installs exactly the
versions they record — check_bytecode refuses to fetch a missing compiler, since vvm would
query GitHub's release list once per contract.
The issue opens on a prod deviation, comments when that set changes, and closes itself when prod is clean again. Devnet deviations are listed in the body but never open or close it: a wiped testnet is real information and not a 6am alert, and devnet churn alone would keep the issue open permanently. Probes that got no answer are listed the same way, for the same reason — a public endpoint that rate-limits tonight and answers tomorrow would otherwise notify the team every other night.
--body is empty when prod is clean and --delta is empty when nothing changed, which is
how the workflow decides whether to close and whether to comment. Coverage is counted in
chains rather than findings, and a chain some other finding already names is not counted as
unchecked — one unreachable chain otherwise reports itself several times over.