#!/usr/bin/env node /** * Create and seed official DODO DVM pools for supported ALL Mainnet replacement rows. * * Default mode is dry-run/read-only. Pass --broadcast to send transactions. * This script uses only factories from config/official-protocol-integration-sources.json. */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { resolve } from "node:path"; import { Contract, ContractFactory, JsonRpcProvider, Wallet, ethers, isAddress, parseUnits } from "ethers"; const repoRoot = resolve(new URL("../..", import.meta.url).pathname); const matrixPath = resolve(repoRoot, "config/all-mainnet-pool-creation-matrix.json"); const sourcesPath = resolve(repoRoot, "config/official-protocol-integration-sources.json"); const outJson = resolve(repoRoot, "reports/status/all-mainnet-official-dodo-migration-latest.json"); const outMd = resolve(repoRoot, "reports/status/all-mainnet-official-dodo-migration-latest.md"); const seederArtifactPaths = [ resolve(repoRoot, "smom-dbis-138/out/scopes/dex/DODOAtomicSeeder.sol/DODOAtomicSeeder.json"), resolve(repoRoot, "smom-dbis-138/out/DODOAtomicSeeder.sol/DODOAtomicSeeder.json"), ]; const args = new Set(process.argv.slice(2)); const broadcast = args.has("--broadcast"); const skipSeed = args.has("--skip-seed"); const targetChainsArg = process.argv.find((arg) => arg.startsWith("--chains=")); const targetChains = targetChainsArg ? new Set(targetChainsArg.slice("--chains=".length).split(",").map((value) => Number(value.trim())).filter(Boolean)) : null; const targetPoolArg = process.argv.find((arg) => arg.startsWith("--pool-id=")); const targetPoolId = targetPoolArg ? targetPoolArg.slice("--pool-id=".length) : null; const seedUsdArg = process.argv.find((arg) => arg.startsWith("--seed-usd=")); const seedUsd = seedUsdArg ? Number(seedUsdArg.slice("--seed-usd=".length)) : 1000; const useRowK = args.has("--use-row-k"); const DVM_FACTORY_ABI = [ "event NewDVM(address baseToken,address quoteToken,address creator,address dvm)", "function createDODOVendingMachine(address baseToken,address quoteToken,uint256 lpFeeRate,uint256 i,uint256 k,bool isOpenTWAP) returns (address newVendingMachine)", "function getDODOPoolBidirection(address token0,address token1) view returns (address[] baseToken0Machines,address[] baseToken1Machines)", ]; const DVM_ABI = [ "function _BASE_TOKEN_() view returns (address)", "function _QUOTE_TOKEN_() view returns (address)", "function _LP_FEE_RATE_() view returns (uint256)", "function getMidPrice() view returns (uint256)", "function getVaultReserve() view returns (uint256,uint256)", "function buyShares(address to) returns (uint256 baseShare,uint256 quoteShare,uint256 lpShare)", "function totalSupply() view returns (uint256)", ]; const ERC20_ABI = [ "function balanceOf(address) view returns (uint256)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", "function allowance(address owner,address spender) view returns (uint256)", "function approve(address spender,uint256 amount) returns (bool)", "function transfer(address to,uint256 amount) returns (bool)", ]; const SEEDER_ABI = [ "function seed(address pool,address baseToken,address quoteToken,uint256 baseAmount,uint256 quoteAmount,address recipient) returns (uint256,uint256,uint256)", ]; const ZERO = "0x0000000000000000000000000000000000000000"; const chainRpcCandidates = { 1: ["ETHEREUM_MAINNET_RPC", "ETH_MAINNET_RPC_URL", "MAINNET_RPC_URL"], 10: ["OPTIMISM_MAINNET_RPC", "OPTIMISM_RPC_URL", "OPTIMISM_RPC"], 56: ["BSC_MAINNET_RPC", "BSC_RPC_URL", "BSC_RPC"], 137: ["POLYGON_MAINNET_RPC", "POLYGON_RPC_URL", "POLYGON_RPC"], 8453: ["BASE_MAINNET_RPC", "BASE_RPC_URL", "BASE_RPC"], 42161: ["ARBITRUM_MAINNET_RPC", "ARBITRUM_RPC_URL", "ARBITRUM_RPC"], 43114: ["AVALANCHE_MAINNET_RPC", "AVALANCHE_RPC_URL", "AVALANCHE_RPC"], }; function readJson(path) { return JSON.parse(readFileSync(path, "utf8")); } const seederDeployments = new Map(); function seederArtifactPath() { return seederArtifactPaths.find((path) => existsSync(path)) || null; } function canSeedAtomically(chainId, env) { return Boolean(envSeederAddress(chainId, env) || seederArtifactPath()); } function loadEnvFile(path, env) { if (!existsSync(path)) return; for (const line of readFileSync(path, "utf8").split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue; const index = trimmed.indexOf("="); const key = trimmed.slice(0, index).replace(/^export\s+/, "").trim(); let value = trimmed.slice(index + 1).trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1); value = value.replace(/\s+#.*$/, ""); value = value.replace(/\$\{([^}:]+)(:-([^}]*))?\}/g, (_, name, _fallback, fallbackValue) => ( env[name] ?? process.env[name] ?? fallbackValue ?? "" )); if (value.includes("${") || value === "0x" || value === "") continue; env[key] ??= value; } } function loadEnv() { const env = { ...process.env }; loadEnvFile(resolve(repoRoot, ".env"), env); loadEnvFile(resolve(repoRoot, "smom-dbis-138/.env"), env); loadEnvFile(resolve(homedir(), ".secure-secrets/private-keys.env"), env); if (!env.PRIVATE_KEY && env.DEPLOYER_PRIVATE_KEY) env.PRIVATE_KEY = env.DEPLOYER_PRIVATE_KEY; return env; } function rpcForChain(chainId, env) { for (const key of chainRpcCandidates[chainId] || []) { if (env[key]) return { key, url: env[key] }; } return { key: null, url: null }; } function pair(row) { return `${row.baseToken?.symbol || "?"}/${row.quoteToken?.symbol || "?"}`; } function normalize(address) { return ethers.getAddress(address); } function feeRate18(feeBps) { return (BigInt(feeBps || 0) * 10n ** 18n) / 10_000n; } function kValue(row) { if (!useRowK) return 0n; const k = row.k ?? 0; if (typeof k === "string" && k.includes(".")) return parseUnits(k, 18); if (typeof k === "number" && !Number.isInteger(k)) return parseUnits(String(k), 18); return BigInt(k); } function seedAmountFor(decimals) { if (!Number.isFinite(seedUsd) || seedUsd <= 0) return 0n; return parseUnits(String(seedUsd), Number(decimals)); } function markdownTable(headers, rows) { return [ `| ${headers.join(" | ")} |`, `| ${headers.map(() => "---").join(" | ")} |`, ...rows.map((row) => `| ${row.map((cell) => String(cell ?? "").replace(/\|/g, "\\|")).join(" | ")} |`), ].join("\n"); } async function waitTx(tx) { const receipt = await tx.wait(); if (receipt?.status !== 1) throw new Error(`transaction reverted: ${tx.hash}`); return receipt; } function envSeederAddress(chainId, env) { return env[`DODO_ATOMIC_SEEDER_${chainId}`] || env[`DODO_ATOMIC_SEEDER_CHAIN_${chainId}`] || null; } async function ensureSeeder(chainId, env, signer, result) { if (seederDeployments.has(chainId)) return seederDeployments.get(chainId); const configured = envSeederAddress(chainId, env); if (configured) { seederDeployments.set(chainId, normalize(configured)); return normalize(configured); } if (!broadcast) { result.actions.push("would_deploy_dodo_atomic_seeder"); return null; } const artifactPath = seederArtifactPath(); if (!artifactPath) { result.blockers.push("missing_dodo_atomic_seeder_artifact"); return null; } const artifact = readJson(artifactPath); const factory = new ContractFactory(artifact.abi, artifact.bytecode.object || artifact.bytecode, signer); const contract = await factory.deploy(); result.transactions.push({ action: "deploy_dodo_atomic_seeder", txHash: contract.deploymentTransaction()?.hash || null }); await contract.waitForDeployment(); const address = await contract.getAddress(); seederDeployments.set(chainId, normalize(address)); result.deployedSeederAddress = normalize(address); return normalize(address); } async function ensureAllowance(token, owner, spender, amount, result, label) { const allowance = await token.allowance(owner, spender); if (allowance >= amount) return; const tx = await token.approve(spender, amount); result.transactions.push({ action: `approve_${label}_to_atomic_seeder`, txHash: tx.hash }); await waitTx(tx); } async function discover(factory, baseToken, quoteToken) { const [forward, reverse] = await factory.getDODOPoolBidirection(baseToken, quoteToken); return { forward: [...forward].filter((address) => address !== ZERO), reverse: [...reverse].filter((address) => address !== ZERO), }; } async function attachSeedPlan(result, row, provider, signer) { const operator = signer?.address || result.operator; if (!operator) { result.blockers.push("missing_operator_for_seed_balance_check"); return; } const base = new Contract(normalize(row.baseToken.address), ERC20_ABI, signer || provider); const quote = new Contract(normalize(row.quoteToken.address), ERC20_ABI, signer || provider); const [baseDecimals, quoteDecimals, baseBalance, quoteBalance] = await Promise.all([ base.decimals(), quote.decimals(), base.balanceOf(operator), quote.balanceOf(operator), ]); const baseSeed = seedAmountFor(baseDecimals); const quoteSeed = seedAmountFor(quoteDecimals); result.seedPlan = { targetSeedUsdPerSide: seedUsd, baseSeedRaw: baseSeed.toString(), quoteSeedRaw: quoteSeed.toString(), operatorBaseRaw: baseBalance.toString(), operatorQuoteRaw: quoteBalance.toString(), }; if (baseBalance < baseSeed) result.blockers.push(`insufficient_operator_base:${row.baseToken.symbol}`); if (quoteBalance < quoteSeed) result.blockers.push(`insufficient_operator_quote:${row.quoteToken.symbol}`); } async function processRow(row, source, env) { const chainId = Number(row.chainId); const baseToken = row.baseToken?.address; const quoteToken = row.quoteToken?.address; const officialFactory = source.chainFactories?.[String(chainId)]?.dvmFactory || null; const unsupportedStatus = source.unsupportedOrUnverifiedChains?.[String(chainId)]?.status || null; const result = { poolId: row.poolId, chainId, network: row.network, pair: pair(row), officialFactory, baseToken, quoteToken, mode: broadcast ? "broadcast" : "dry_run", status: "blocked", actions: [], blockers: [], transactions: [], officialPoolAddress: null, seedPlan: null, }; if (!officialFactory) { result.blockers.push(unsupportedStatus || "official_factory_not_available"); result.status = "unsupported"; return result; } if (!isAddress(baseToken) || !isAddress(quoteToken)) result.blockers.push("invalid_token_address"); const rpc = rpcForChain(chainId, env); if (!rpc.url) result.blockers.push("missing_rpc_url"); if (!env.PRIVATE_KEY && broadcast) result.blockers.push("missing_private_key_for_broadcast"); if (broadcast && !skipSeed && !canSeedAtomically(chainId, env)) result.blockers.push("missing_dodo_atomic_seeder_artifact"); if (result.blockers.length) return result; const provider = new JsonRpcProvider(rpc.url, chainId, { staticNetwork: true }); const signer = env.PRIVATE_KEY ? new Wallet(env.PRIVATE_KEY, provider) : null; const operator = signer?.address || env.DEPLOYER_ADDRESS || env.SIGNER_ADDRESS || null; result.operator = operator; result.rpcEnvKey = rpc.key; const factoryCode = await provider.getCode(officialFactory); if (!factoryCode || factoryCode === "0x") { result.blockers.push("official_factory_has_no_code_on_rpc"); return result; } const factory = new Contract(officialFactory, DVM_FACTORY_ABI, signer || provider); const before = await discover(factory, normalize(baseToken), normalize(quoteToken)); result.preExistingForwardPools = before.forward; result.preExistingReversePools = before.reverse; let poolAddress = before.forward[0] || before.reverse[0] || null; const createParams = { lpFeeRate: feeRate18(row.feeBps ?? 3).toString(), initialPrice: (10n ** 18n).toString(), k: kValue(row).toString(), isOpenTWAP: false, }; result.createParams = createParams; if (!poolAddress) { if (!broadcast) { await attachSeedPlan(result, row, provider, signer); result.actions.push("would_create_official_dvm_pool"); if (!skipSeed) result.actions.push("would_approve_and_atomic_seed"); result.status = result.blockers.length ? "blocked_create_seed" : "ready_to_create_and_seed"; return result; } const tx = await factory.createDODOVendingMachine( normalize(baseToken), normalize(quoteToken), createParams.lpFeeRate, createParams.initialPrice, createParams.k, createParams.isOpenTWAP, ); result.transactions.push({ action: "createDODOVendingMachine", txHash: tx.hash }); const receipt = await waitTx(tx); const after = await discover(factory, normalize(baseToken), normalize(quoteToken)); poolAddress = after.forward[0] || after.reverse[0] || null; result.postCreateForwardPools = after.forward; result.postCreateReversePools = after.reverse; result.createBlockNumber = receipt.blockNumber; } if (!poolAddress) { result.blockers.push("official_pool_not_discovered_after_create"); return result; } result.officialPoolAddress = normalize(poolAddress); const pool = new Contract(result.officialPoolAddress, DVM_ABI, signer || provider); const [actualBase, actualQuote, midPrice, reserves, totalSupply] = await Promise.all([ pool._BASE_TOKEN_(), pool._QUOTE_TOKEN_(), pool.getMidPrice().catch(() => null), pool.getVaultReserve().catch(() => null), pool.totalSupply().catch(() => null), ]); result.poolSurface = { baseToken: actualBase, quoteToken: actualQuote, midPrice: midPrice?.toString() || null, baseReserve: reserves ? reserves[0].toString() : null, quoteReserve: reserves ? reserves[1].toString() : null, totalSupply: totalSupply?.toString() || null, }; if (normalize(actualBase) !== normalize(baseToken) || normalize(actualQuote) !== normalize(quoteToken)) { result.blockers.push("official_pool_token_mismatch"); return result; } if (skipSeed) { result.status = "created_or_existing_unseeded"; result.actions.push("seed_skipped"); return result; } await attachSeedPlan(result, row, provider, signer); if (result.blockers.length) return result; if (!broadcast) { result.actions.push("would_approve_and_atomic_seed"); result.status = "ready_to_seed"; return result; } const base = new Contract(normalize(baseToken), ERC20_ABI, signer || provider); const quote = new Contract(normalize(quoteToken), ERC20_ABI, signer || provider); const baseSeed = BigInt(result.seedPlan.baseSeedRaw); const quoteSeed = BigInt(result.seedPlan.quoteSeedRaw); const seederAddress = await ensureSeeder(chainId, env, signer, result); if (!seederAddress) return result; result.atomicSeederAddress = seederAddress; await ensureAllowance(base, operator, seederAddress, baseSeed, result, "base"); await ensureAllowance(quote, operator, seederAddress, quoteSeed, result, "quote"); const seeder = new Contract(seederAddress, SEEDER_ABI, signer); const seedTx = await seeder.seed( result.officialPoolAddress, normalize(baseToken), normalize(quoteToken), baseSeed, quoteSeed, operator, ); result.transactions.push({ action: "atomic_seed_buyShares", txHash: seedTx.hash }); await waitTx(seedTx); const finalReserves = await pool.getVaultReserve(); const finalSupply = await pool.totalSupply(); result.finalPoolSurface = { baseReserve: finalReserves[0].toString(), quoteReserve: finalReserves[1].toString(), totalSupply: finalSupply.toString(), }; result.status = "official_pool_created_or_existing_and_seeded"; return result; } const matrix = readJson(matrixPath); const sources = readJson(sourcesPath); const env = loadEnv(); const dodoSource = sources.protocols?.dodo_pmm; if (!dodoSource) throw new Error("Missing dodo_pmm source registry entry"); const candidateRows = matrix.rows.filter((row) => ( row.requiredForSpend === true && row.protocol === "dodo_pmm" && row.replacementEvidence && (!targetChains || targetChains.has(Number(row.chainId))) && (!targetPoolId || row.poolId === targetPoolId) )); const rows = []; for (const row of candidateRows) { rows.push(await processRow(row, dodoSource, env)); } const generatedAt = new Date().toISOString(); const summary = rows.reduce((acc, row) => { acc[row.status] = (acc[row.status] || 0) + 1; return acc; }, {}); const report = { generatedAt, mode: broadcast ? "broadcast" : "dry_run", seedUsdPerSide: skipSeed ? null : seedUsd, matrixFile: "config/all-mainnet-pool-creation-matrix.json", officialSource: "config/official-protocol-integration-sources.json#protocols.dodo_pmm", summary, rows, }; const md = [ "# ALL Mainnet Official DODO Migration Execution", "", `- Generated: \`${generatedAt}\``, `- Mode: \`${report.mode}\``, `- Seed USD per side: \`${report.seedUsdPerSide ?? "skipped"}\``, "", markdownTable(["Status", "Count"], Object.entries(summary)), "", markdownTable( ["Pool", "Chain", "Pair", "Status", "Official Pool", "Tx Count", "Blockers"], rows.map((row) => [ row.poolId, row.chainId, row.pair, row.status, row.officialPoolAddress || "", row.transactions?.length || 0, row.blockers.join(", "), ]), ), "", ].join("\n"); mkdirSync(resolve(repoRoot, "reports/status"), { recursive: true }); writeFileSync(outJson, `${JSON.stringify(report, null, 2)}\n`); writeFileSync(outMd, `${md}\n`); console.log(`[OK] Official DODO migration ${report.mode} written: ${Object.entries(summary).map(([k, v]) => `${k}=${v}`).join(", ")}`);