Skip to content

Repository files navigation

Curve Core

A minimal version of all curve infrastructure for AMM in one place.

Structure

AMM

  • 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

Helpers

  • 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

Governance

  • Agent and relayer for governance from mainnet DAO
  • Vault for fee receiving from pools

Gauge

  • Reward-only gauge for incentives

Registries

  • 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"

Deployment

Set up environment

Env file

Put settings file ("env") into settings directory. Example. It contains RPC url for target chain.

Set up Python environment

Project requires Python 3.11+

Install dependencies using poetry

pip install poetry==1.8.3
poetry install

Chain params file

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)

Deployment

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

Deployment results

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.

Verifying on explorers

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.

Deploy test pools

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.

Consuming the registry

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 config block verbatim, and every recorded address flattened to amm.stableswap.factory keys.
  • 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.

The registry as a page

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.

Deployment status and drift

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.

On-chain checks

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.

Nightly monitor

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.

About

Deploy a standardised set of smart contracts powering Curve.fi with a simple CLI command.

Topics

Resources

Stars

35 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages