import type { LucidlyClient } from "./client.js"; import type { DeployParams } from "./types.js"; /** The audited Se7en-Seas Boring Vault commit the contract artifacts are pinned to. */ export const PINNED_BORING_VAULT_COMMIT = "0e23e7fd3a9a7735bd3fea61dd33c1700e75c528"; /** Chain ids treated as mainnet (real funds) — stricter preflight. */ const MAINNET_IDS = new Set([1, 8453, 42161]); export interface PreflightIssue { level: "error" | "warning"; code: string; message: string; } export interface PreflightReport { ok: boolean; chain: string; mainnet: boolean; errors: PreflightIssue[]; warnings: PreflightIssue[]; } /** * Pre-mainnet deploy checks (the PRODUCTION.md security checklist, enforced in code). * Errors block a deploy (unless params.allowUnsafe); warnings are advisory. */ export async function preflightDeploy(c: LucidlyClient, params: DeployParams): Promise { const chain = c.chainConfig; const mainnet = MAINNET_IDS.has(chain.id); const errors: PreflightIssue[] = []; const warnings: PreflightIssue[] = []; const E = (code: string, message: string) => errors.push({ level: "error", code, message }); const W = (code: string, message: string) => warnings.push({ level: "warning", code, message }); const finalize = params.finalizeOwnership ?? false; const owner = params.owner ? c.resolveToken(params.owner) : undefined; const strategist = params.strategist ? c.resolveToken(params.strategist) : undefined; const bounds = params.exchangeRateBounds ?? { upperBps: 50, lowerBps: 50, minUpdateDelay: 3600 }; const shareLock = params.shareLockSeconds ?? 0; const platformFee = params.managementFeeBps ?? 0; const perfFee = params.performanceFeeBps ?? 0; const publicDeposits = params.allowPublicDeposits ?? true; // --- Ownership: EOA ownership is allowed; a multisig/timelock is recommended (advisory only) --- if (finalize) { if (!owner) { W("OWNER_IS_EOA", "finalizeOwnership with no explicit owner hands control to the deployer EOA — allowed, but a multisig/timelock is recommended for production."); } else { const code = await c.publicClient.getBytecode({ address: owner }); // An EIP-7702 delegated EOA carries a 23-byte 0xef0100
designator — still an EOA. const isDelegatedEoa = !!code && code.toLowerCase().startsWith("0xef0100"); if (!code || code === "0x" || isDelegatedEoa) { W( "OWNER_IS_EOA", `owner ${owner} is ${isDelegatedEoa ? "an EIP-7702 delegated EOA" : "an EOA"} — allowed, but a multisig/timelock is recommended for production ownership.`, ); } } } else if (mainnet) { W( "OWNERSHIP_NOT_FINALIZED", "Deploying to a mainnet without finalizeOwnership — the deployer EOA keeps OWNER control. Hand off to a multisig/timelock before going live.", ); } // --- Key separation --- if (owner && strategist && owner.toLowerCase() === strategist.toLowerCase()) { W("STRATEGIST_EQUALS_OWNER", "STRATEGIST and OWNER are the same address — keep strategist keys separate from owner/admin keys."); } // --- Exchange-rate guardrails --- if (mainnet && (bounds.upperBps === 0 || bounds.lowerBps === 0)) { W("RATE_BOUNDS_ZERO", "An allowedExchangeRateChange bound is 0 — rate moves in that direction are blocked. Set sane bounds."); } if (bounds.upperBps > 2000 || bounds.lowerBps > 2000) { W("RATE_BOUNDS_WIDE", "Exchange-rate bounds exceed 20% per update — unusually wide; confirm this is intended."); } if (mainnet && bounds.minUpdateDelay === 0) { W("MIN_UPDATE_DELAY_ZERO", "minUpdateDelay is 0 — no throttle between rate updates on mainnet."); } // --- Anti-MEV share lock --- if (mainnet && shareLock === 0 && publicDeposits) { W("SHARE_LOCK_ZERO", "shareLockSeconds is 0 with public deposits — no anti-MEV deposit lock on mainnet."); } // --- Fee sanity --- if (platformFee > 2000 || perfFee > 5000) { W("FEES_HIGH", "Configured fees look high (platform > 20% or performance > 50%); confirm the bps values."); } return { ok: errors.length === 0, chain: chain.name, mainnet, errors, warnings }; } /** Format a report for logging/CLI. */ export function formatPreflight(r: PreflightReport): string { const lines = [`preflight (${r.chain}${r.mainnet ? ", mainnet" : ""}): ${r.ok ? "OK" : "BLOCKED"}`]; for (const e of r.errors) lines.push(` ERROR [${e.code}] ${e.message}`); for (const w of r.warnings) lines.push(` warn [${w.code}] ${w.message}`); return lines.join("\n"); }