Deploying the contracts
How the Clear Protocol gets onto a chain: the scripts in deploy/, the order they
run in, the wiring after them, and handing the keys over. Who is meant to hold each role once it is
done is in Governance.
On this page
Base Sepolia carries the whole stack. Base carries the savings and send rails only (ClearUSD,
ESADepositVault, ClaimEscrow), Ethereum Sepolia the cross-chain CLRUSD pool. The addresses are
in the root README's Deployed addresses.
Before you start
Test first. npm run test runs every suite, and test/deploy/CreditCore.deploy.spec.ts runs
script 20 itself against a fresh chain — the wiring is the part most likely to deploy cleanly and
then refuse the first draw.
Configure the root .env (from .env.example):
DEPLOYER_PRIVATE_KEYALCHEMY_API_KEYETHERSCAN_API_KEYEach script reads more of its own; they are listed below and at the top of every script.
Networks are named in hardhat.config.ts: base-sepolia, base, sepolia, mainnet,
arbitrum, polygon and a few older testnets. Every command takes --network <name>.
Where addresses go. Every script records what it deploys in
deployments/<network>/<Contract>.json — address and ABI — through deploy/helpers.ts, and reads
earlier contracts from the same place. That folder is the record of what is deployed; commit it
with the deployment. Scripts 20–24 are idempotent: anything already recorded is reused, and
every wiring call is a set, so a failed run can be run again.
The order
Run the scripts in number order. Each group needs the ones above it.
npx hardhat run deploy/<script>.ts --network base-sepoliaThe Deed track — 00–06
00_deploy_ValidatorRegistryValidatorRegistry01_deploy_MetadataRendererMetadataRenderer02_deploy_ValidatorValidator03_deploy_DeedNFTDeedNFT04_deploy_FundManagerFundManager — 5% commission, capped at 10%05_deploy_SubdivideSubdivide06_deploy_FractionalizeFractionTokenFactory, then FractionalizeUse deploy/deploy_all.ts for 00–05. It deploys and wires those contracts in one run, then
06 adds fractionalization. The numbered scripts 00–04 do not match the current contracts:
they call roles and functions the contracts don't have (for example OPERATOR_ROLE on ValidatorRegistry,
ADMIN_ROLE on DeedNFT and MetadataRenderer) and would fail. What deploy_all.ts wires, and
what it leaves for an admin to set, is in
The Deed track's contracts.
Reserve, pricing and the first bonds — 07–10
07_deploy_TokenRegistryTokenRegistry, with the network's tokens08_deploy_AssurancePoolAssurancePool (UUPS)09_deploy_AssuranceOracleAssuranceOracle, and points the pool at it10_deploy_BurnerBondFactoryBurnerBondFactory and BurnerBondDepositdeploy/deploy_burner_bonds.ts runs 07–10 in one go (npm run deploy:burner-bonds:sepolia).
Send and savings — 11–19
11_deploy_ClaimEscrowClaimEscrow, and grants SETTLER_ROLE to the relayerSEND_PAYOUT_TREASURY, SEND_RELAYER_ADDRESS12_deploy_ClearUSDClearUSD — the original CLRUSD, a plain deployment behind no proxyCLRUSD_ADMIN, CLRUSD_MAX_SUPPLY, CLRUSD_PREMINT, CLRUSD_USE_CREATE2, CLRUSD_CREATE2_*13_deploy_ESADepositVaultESADepositVault (UUPS). Backs ClearUSDUpgradeable when one is recorded, else ClearUSDVAULT_CLRUSD, CLRUSD_ADDRESS, ESA_VAULT_DEPOSIT_TOKEN, SAVINGS_RELAYER_ADDRESS14_register_CLRUSD_in_registryTokenRegistryCLRUSD_ADDRESS, TOKEN_REGISTRY_ADDRESS, CLRUSD_REMOTE_*15_deploy_CLRUSDTokenPoolCCIP_ROUTER, CCIP_RMN_PROXY16_deploy_AccessManagerAccessManager17_deploy_MembershipRegistryMembershipRegistry18_configure_AccessManagerMembershipRegistryAccessManager at the registry19_deploy_SavingsIntentFactorySavingsIntentFactory, for gasless depositsSAVINGS_FACTORY_ADMIN, SAVINGS_RELAYER_ADDRESSEach has an npm script as well — npm run deploy:clrusd -- base-sepolia and so on; the list is in
the root README. CCIP lanes are configured after 15: npm run clrusd:ccip:configure, then
npm run clrusd:validate. The full CLRUSD runbook is
CLRUSD and the ESA vault.
Credit, capital and offers — 20–24
20_deploy_CreditCoreClearCredit (the ledger), NetworkRegistry, CollateralRegistry, LimitCalculator, RevolvingIssuer, TermIssuer — all UUPS — reusing the AccessManager. Registers the issuers against the AssurancePool and oracle if they exist, and says so if they do notCARRY_TREASURY — where carry lands. The deployer stands in on a testnet; on mainnet it must be the multisig21_deploy_ClearUSDUpgradeableClearUSDUpgradeable, the CLRUSD that carries the redemption lockCLRUSD_ADMIN, CLRUSD_ADMIN_DELAY, CLRUSD_MAX_SUPPLY22_deploy_BondsAndPoolBondVault, LendingPool, PayoutPool, MerchantRegistry, Liquidator, and the BurnerBondFactory if none is recorded. Registers bonds and pool shares as collateral, with different haircutsCOOP_TREASURY, RESERVE_TOKEN, BOND_BASE_URI23_deploy_BondCollectionBurnerBond collection for one token, its BurnerBondDeposit, and the BondValuer that prices it. Registers BOND collateral at 95%BOND_TOKEN, BOND_TOKEN_NAME, BOND_TOKEN_SYMBOL, BOND_YIELD_BPS, BOND_MAX_DISCOUNT, BOND_MAX_MATURITY, BOND_BASE_URI24_deploy_OffersIssuerOffersIssuer, registered against the ledger, its line opened at the capOFFERS_CAP, OFFER_AMOUNT, OFFER_EXPIRY_DAYS, OFFERS_TREASURYMoving the vault to ClearUSDUpgradeable is its own step, after 21.
ESADepositVault.setClrusd refuses while any of the old token is outstanding, so the order is:
redeem, then re-point. scripts/verify_vault_parity.ts checks a replacement vault is configured
like the one it replaces.
Wiring after the deploy
The scripts deploy and register. These grant the roles and set the values that make it work:
scripts/grant_relayer_roles.tsRELAYER_ADDRESSscripts/grant_claim_escrow_settler.tsClaimEscrow's settler (npm run send:grant-settler:base-sepolia)CLAIM_ESCROW_NEW_SETTLER, or the relayer from the API's envscripts/grant_collateral_operator.tsOPERATORscripts/grant_pool_borrower.tsLendingPoolCARD_SETTLER_ADDRESSscripts/configure-carry-recipients.mjsPayoutPool. Run with node; reports unless given --applyRPC_URL_84532, the issuer and pool addressesscripts/set_target_rtd.tsTARGET_RTDscripts/register_merchant.tsMERCHANT, optionally DISCOUNT_BPS, APPROVAL_CAP, PAYOUT_WINDOWscripts/set_merchant_active.tsMERCHANT, ACTIVEscripts/fund_payout_pool.tsPayoutPoolFUND_USDCscripts/sync_savings_collateral.tsWALLET or WALLETSscripts/command/approve_treasury_caps.tsTREASURY_CAPS_DRY_RUN to look firstRun each as npx hardhat run <script> --network base-sepolia with its variables in front, unless the
table says otherwise. The usage is at the top of every script.
Pointing the apps at it
The API reads its addresses from apps/api/src/config/contracts.ts. Each has a Base Sepolia default
there, and an environment variable named for the contract and the chain wins over it —
PAYOUT_POOL_84532, OFFERS_ISSUER_84532, and so on. OffersIssuer has no default: set
OFFERS_ISSUER_84532 or the API runs without offers. The member app takes CLRUSD and vault
addresses as VITE_ variables per chain (VITE_CLRUSD_84532, VITE_ESA_VAULT_84532).
scripts/update-app-contracts.ts predates the monorepo — it writes to app/, which no longer
exists. Do not use it.
Handing over the keys
Until this is done, the deployer key holds everything. Two steps, each rehearsed against a fork of the live network first:
- 1.Operational roles to the wallets that do each job —
scripts/command/ops-roles.ts, rehearsed withscripts/command/drill_ops_roles.ts. The deployer stops being a live key. Which role goes to which job isscripts/command/ops-roles.map.json: each role goes to a job's wallet (<JOB>_ADDRESSin the environment, e.g.CREDIT_OPERATOR_ADDRESS), to the Safe (SAFE_ADDRESS), or is renounced because nothing off-chain uses it. Itscheckslist names roles one contract needs on another;FIX_CHECKS=1withMODE=rungrants the missing ones. No deploy script grantsLendingPool'sLIQUIDATOR_ROLEto theLiquidator; only this step does, and until it has runLiquidator.liquidateSharesreverts (Settlement → LendingPool). - 2.Admin to the Ops Safe —
scripts/command/safe-handover.ts, rehearsed withscripts/command/drill_safe_handover.ts. It surveys who holds each admin role andowner(), grants each to the Safe, and only then has the deployer renounce. It moves admin roles only; operational roles stay unless named inINCLUDE_ROLES. Contracts that want ownership accepted (Ownable2Step, andClearUSDUpgradeable's delayed admin) need a Safe transaction to finish.
SAFE_ADDRESS=0x… DEPLOYER=0x… npx hardhat run scripts/command/safe-handover.ts --network base-sepolia # survey
SAFE_ADDRESS=0x… MODE=run CONFIRM=handover npx hardhat run scripts/command/safe-handover.ts --network base-sepolia # do itRun without MODE=run, each script only reads the chain: it needs no key and sends nothing.
safe-handover.tslists every contract deployed on Base Sepolia, which of its roles the deployer and the Ops Safe each hold (marking the admin roles), who owns it and any owner still to accept, and the steps a hand-over would still take.ops-roles.tslists each role in the map, whether the deployer and that job's wallet hold it now, and where it is meant to go. It also reports whether each contract holds the roles in the map'scheckslist.
Both read the Base Sepolia deployment list (deployments/base-sepolia) and ask only about the
addresses they are given: the deployer, the Safe and the job wallets. They answer "does this wallet
hold it?", not "who else does?"; for that, read the contracts' RoleGranted and RoleRevoked
events (On-chain authority).
DEPLOYER=0x… CREDIT_OPERATOR_ADDRESS=0x… … npx hardhat run scripts/command/ops-roles.ts --network base-sepolia # survey
MODE=run CONFIRM=ops-roles FIX_CHECKS=1 CREDIT_OPERATOR_ADDRESS=0x… … npx hardhat run scripts/command/ops-roles.ts --network base-sepolia # do itWho holds what afterwards, and why, is in Governance.
Upgrades
Upgradeable contracts keep their proxy address; only the implementation changes. Upgrade authority sits with each contract's admin — once that is the Safe, an upgrade is a Safe transaction, not something the deployer key can do.
- The Deed track and the reserve:
deploy/upgrade/— check first, then execute:CONTRACT_NAME=DeedNFT PROXY_ADDRESS=0x… npm run upgrade:check -- base-sepolia CONTRACT_NAME=DeedNFT PROXY_ADDRESS=0x… npm run upgrade:execute -- base-sepolia - The credit stack: one script per change in
scripts/upgrade_*.ts, each named for what it ships (upgrade_bond_backstops.ts,upgrade_card_settlement.ts,upgrade_offers_owner.ts, …). Each says at the top which proxies it upgrades and what it wires afterwards.
Checking it
scripts/drill_redemption.tsrehearses a merchant redemption against a fork of what is deployednpm run clrusd:validatechecks the CLRUSD, vault and CCIP setupnpm run verify:deployments:sepoliachecks the reserve and the first bond contracts only (07–10); it predates everything after- Verifying source on Basescan:
npx hardhat verify --network base-sepolia <address> - The Command Center's Build page compares each deployed contract's code with this branch, from
scripts/command/code-fingerprints.mjs; CI fails if that file is stale after a contract change