All checks were successful
Deploy to Phoenix / validate (push) Successful in 1m21s
Deploy to Phoenix / deploy (push) Successful in 48s
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Successful in 1m20s
phoenix-deploy Deployed to cloudflare-sync
Deploy to Phoenix / cloudflare (push) Successful in 40s
455 lines
18 KiB
JavaScript
455 lines
18 KiB
JavaScript
#!/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(", ")}`);
|