Deploy

This page is for people shipping code.

Foundry, solc 0.8.28, optimizer 200, EVM cancun (Arc mainnet executes PUSH0, TSTORE and MCOPY; checked with an eth_call create on 2026-09-16).

curl -L https://foundry.paradigm.xyz | bash
foundryup
cd contracts
forge install foundry-rs/forge-std --no-commit
forge install OpenZeppelin/openzeppelin-contracts@v5.2.0 --no-commit
forge test -vv

Arc mainnet (5042)

Everything reads kleos/.env:

npm run launch:dry
npm run launch:mainnet
npm run set:token -- 0x<token> [--price <usdc per KLEOS>] [--dry-run]

The launch token on mainnet is arc.sol at the repo root: a fee-on-transfer ERC-20 whose tax applies only to trades against its own Uniswap V2 pair, with configurable decimals and no burn(). The stack handles all of that:

What launch:mainnet does, in order:

  1. Checks the RPC is chain 5042 and that USDC 0x3600…0000, the ERC-8004 identity registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 and Uniswap V2 Router02 0x1f7d7550… all have code.
  2. Refuses if deployments/5042.json already has a non-zero escrow (override with FORCE=1).
  3. Simulates script/Deploy.s.sol against live state and reads forge's worst-case fee estimate. Needs the deployer balance to be at least 1.25x that. At a 110 gwei base fee that is about 2.3 USDC for 10.17M gas; send 10 USDC to be safe.
  4. Broadcasts with --slow. The script itself asserts the wiring before it returns (staking.kleos / treasury.kleos match the configured token or are both empty, staking.treasury, escrow holds SLASHER_ROLE and RECORDER_ROLE, registry points at the mainnet identity, treasury points at the router, deployer holds no role when KLEOS_ADMIN is set) and writes deployments/5042.json.
  5. node scripts/sync-addresses.mjs 5042 copies the JSON into packages/shared/src/addresses.ts and docs/addresses.md.
  6. Re-reads the same facts from the live chain with cast call and fails loudly on any mismatch.
  7. Source verification through Sourcify (forge verify-contract --verifier sourcify, constructor args rebuilt from the deployment JSON). Blockscout imports from Sourcify; its own API is behind a Cloudflare challenge that rejects CLI traffic. Re-run any time with npm run verify:mainnet.

set:token then: checks the token has code and reads name/symbol/decimals, checks the signer is DEFAULT_ADMIN of staking, simulates script/ConfigureToken.s.sol, broadcasts, re-reads staking.kleos, treasury.kleos, minListingStake and escrow.kleosPerUsdc with cast call, writes the token into deployments/5042.json and syncs addresses.

Then rebuild the web app (npm run build -w @kleos/web) so it ships the new addresses. The web app and MCP also read KleosStaking.kleos() and the token's decimals() live, so they work as soon as set:token lands. The MCP server and seller demo default to chain 5042.

Liquidity for buyback-and-burn

The arc.sol token creates and seeds its own KLEOS/USDC pair in opentrade(); nothing else is needed. For the plain reference token, KleosTreasury.buybackAndBurn still needs a KLEOS/USDC pair. Set KLEOS_LIQ (base units, default 1,000,000 whole KLEOS) and USDC_LIQ (6 decimals) in .env and run from the wallet holding both:

npm run seed:liquidity                 # simulate
npm run seed:liquidity -- --broadcast  # send

Testnet (5042002)

Same script; it picks the testnet ERC-8004 registry 0x8004A818… by chain id. There is no official Uniswap V2 router on testnet, so treasury.router is zero unless you pass UNI_V2_ROUTER.

cd contracts && forge script script/Deploy.s.sol --rpc-url https://rpc.testnet.arc.io --broadcast
npm run sync:addresses 5042002

Local Anvil (31337)

script/DeployLocal.s.sol deploys mock USDC, a mock identity registry, a mock router and the stack. Addresses are deterministic from Anvil account 0; deployments/31337.json already matches.

Tests against real Arc state

cd contracts
forge test                                                                              # unit
forge test --match-contract ArcMainnetForkTest --fork-url https://rpc.mainnet.arc.io -vv   # full launch path on a mainnet fork (reference token)
forge test --match-contract ArcTaxTokenForkTest --fork-url https://rpc.mainnet.arc.io -vv   # empty slot -> arc.sol token launched on the real router -> set:token -> buyback
forge test --match-contract ArcForkTest --fork-url https://rpc.testnet.arc.io -vv          # USDC + identity smoke on testnet

ArcTaxTokenForkTest runs the exact mainnet order: deploy the stack with an empty token slot, deploy a copy of arc.sol (test/helpers/ArcTaxToken.sol, only the pragma changed) against the real Router02 so it creates its pair on the real factory, fund it with USDC and call opentrade(), run ConfigureToken, then stake, list, complete, reject-and-slash at the reference price, and buy back 100 USDC through the real pair. With the 20% initial buy tax the treasury received about 3.17M of a 9-decimal token and sent all of it to 0x…dEaD, while 0.79M landed in the token contract as tax.

The other mainnet fork suite drives the real Deploy script with the reference token, then registers on the real ERC-8004 registry, stakes, lists, funds a job with real USDC bytecode, completes (99.50 / 0.50 split), rejects (10 KLEOS slash into the treasury), seeds a pool through the real router and burns KLEOS with buybackAndBurn. Arc's USDC keeps balances as native balance and moves them through chain precompiles at 0x1800…0000; test/helpers/ArcNativeUsdc.sol stubs only that primitive so the FiatToken code runs unchanged in the fork.