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
Everything reads kleos/.env:
PRIVATE_KEY: deployer. Pays gas in USDC.KLEOS_ADMIN (optional): cold wallet that receives every DEFAULT_ADMIN_ROLE / ADMIN_ROLE on staking, reputation and escrow. The deployer renounces its temporary roles inside the same script run. Leave it unset and the deployer is the admin.KLEOS_TOKEN (optional): an existing KLEOS ERC-20 to wire during deploy. Leave unset to deploy with an empty token slot and wire it later.DEPLOY_TEST_TOKEN=1 (optional, testnet/local): mint the plain KleosToken (KLEOS_SUPPLY, default 1,000,000,000) instead.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:
KleosStaking.setToken and KleosTreasury.setToken are one-shot, admin only, and require code at the address.decimals() in setToken; stake() credits the balance delta, so a taxed transfer can never inflate stake. Wallet-to-contract transfers are untaxed by that token, so staking, unstaking and slashing move the full amount.KleosEscrow.kleosPerUsdc is set from --price (base units per 1e-6 USDC = 10^decimals / price). Default price 1 USDC per KLEOS.buybackAndBurn calls swapExactTokensForTokensSupportingFeeOnTransferTokens, measures what actually arrived, checks it against minOut, tries burn(), and falls back to a transfer to 0x000…dEaD. burnHeld() does the same for slashed stake already in the treasury._maxTxAmount / _maxWalletSize (2.5% of supply until removelimit()) apply to the treasury as a buyer; keep single buybacks under that.What launch:mainnet does, in order:
0x3600…0000, the ERC-8004 identity registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 and Uniswap V2 Router02 0x1f7d7550… all have code.deployments/5042.json already has a non-zero escrow (override with FORCE=1).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.--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.node scripts/sync-addresses.mjs 5042 copies the JSON into packages/shared/src/addresses.ts and docs/addresses.md.cast call and fails loudly on any mismatch.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.
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
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
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.
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.