import { decodeEventLog, keccak256, stringToHex, type Address, type Hex, } from "viem"; import type { LucidlyClient } from "./client.js"; import type { TxResult } from "./types.js"; /** * LucidlyFactoryV2 — the permissionless vault factory (v2.4.2 is the current live version). Same * address on every chain (Arachnid CREATE2 — verified live on Ethereum, Base, Arbitrum, Optimism), * so callers don't need to plumb a per-chain address through. One `deployVault(params)` tx deploys * the full stack (RolesAuthority, BoringVault, Manager, Accountant, Teller, PolicyManager + DC-15's * BoringOnChainQueue + BoringSolver + DC-16's per-vault MorphoFlashLoanAdapter), grants the right * roles to each contract, finalises ownership to the registry owner, and emits `VaultDeployed`. * The deployer becomes the strategist; the registry owner becomes the vault owner and the * streaming-mgmt-fee recipient. * * v2.4.2 additions over v2.3: * - DC-14: strategist receives STRATEGIST_ROOT_ROLE → can call manager.setManageRoot in ONE tx * (off-chain Merkle root seed instead of N sequential addRegistryPolicy calls). * - DC-15: each vault gets its own BoringOnChainQueue + BoringSolver — queued user withdrawals * via queue.requestOnChainWithdraw, fulfilment via solver.boringRedeemSelfSolve. * - DC-16: each vault gets its own MorphoFlashLoanAdapter (wired to morpho/vault/manager * immutables) — flashloan strategies work from tx-1 with zero post-deploy ops. * * v2.4.3 additions over v2.4.2: * - DC-17: when allowPublicDeposits=true the factory now sets the deposit/depositWithPermit * public capability on the per-vault RolesAuthority — fixes the UNAUTHORIZED revert that * prevented non-owner EOAs from depositing into "public" v2.4.2 vaults. * * Mirrors `window.LucidlyChain.deployVault` in the Pro UI byte-for-byte (same DeployParams tuple, * same FACTORY address, same VaultDeployed shape), so the two surfaces produce identical vaults. * * v2.4.2 (0xFC93bC7e…) and v2.3 (0x6612E3DE…) are fully deprecated. SDK 0.3.1 only targets v2.4.3. */ export const FACTORY_V2_ADDRESS = "0xF13ca1C318fD9bDd851F5C1334A1dd35302684fd" as Address; /** DeployParams — must match LucidlyFactoryV2.DeployParams exactly. */ export interface FactoryV2DeployParams { /** CREATE2 salt — defaults to keccak256(`lucidly:${name}:${symbol}:${chainId}`) if omitted. */ salt?: Hex; name: string; symbol: string; baseAsset: Address; /** Initial NAV in baseAsset decimals (USDC = 6 → 1_000_000 = 1.0). */ startingRate: bigint; /** Upper bound on per-update rate change, on the 1e4 scale (10_050 = +0.5%). */ upperBoundBps: number; lowerBoundBps: number; minUpdateDelay: number; performanceFeeBps: number; shareLockSeconds: bigint; rateUpdater: Address; solver: Address; depositAssets: readonly Address[]; allowPublicDeposits: boolean; } /** Tuple components in the exact order LucidlyFactoryV2.DeployParams declares them. */ const DEPLOY_PARAMS_TUPLE = { name: "p", type: "tuple", components: [ { name: "salt", type: "bytes32" }, { name: "name", type: "string" }, { name: "symbol", type: "string" }, { name: "baseAsset", type: "address" }, { name: "startingRate", type: "uint96" }, { name: "upperBoundBps", type: "uint16" }, { name: "lowerBoundBps", type: "uint16" }, { name: "minUpdateDelay", type: "uint24" }, { name: "performanceFeeBps", type: "uint16" }, { name: "shareLockSeconds", type: "uint64" }, { name: "rateUpdater", type: "address" }, { name: "solver", type: "address" }, { name: "depositAssets", type: "address[]" }, { name: "allowPublicDeposits", type: "bool" }, ], } as const; export const FACTORY_V2_ABI = [ { type: "function", name: "deployVault", stateMutability: "nonpayable", inputs: [DEPLOY_PARAMS_TUPLE], outputs: [] }, ] as const; const VAULT_DEPLOYED_EVENT = { type: "event", name: "VaultDeployed", inputs: [ { name: "strategist", type: "address", indexed: true }, { name: "salt", type: "bytes32", indexed: true }, { name: "vault", type: "address", indexed: false }, { name: "rolesAuthority", type: "address", indexed: false }, { name: "manager", type: "address", indexed: false }, { name: "accountant", type: "address", indexed: false }, { name: "teller", type: "address", indexed: false }, { name: "policyManager", type: "address", indexed: false }, { name: "baseAsset", type: "address", indexed: false }, // v2.4.2 (DC-15 + DC-16): per-vault queue, solver, and Morpho flashloan adapter. // flashLoanAdapter is zero on chains where factory.morpho is unset (no Morpho deployed). { name: "queue", type: "address", indexed: false }, { name: "solver", type: "address", indexed: false }, { name: "flashLoanAdapter", type: "address", indexed: false }, ], } as const; /** Parsed vault stack returned from a successful `deploy()`. */ export interface FactoryV2DeployResult { vault: Address; strategist: Address; rolesAuthority: Address; manager: Address; accountant: Address; teller: Address; policyManager: Address; baseAsset: Address; /** v2.4.2 (DC-15): per-vault BoringOnChainQueue — users call requestOnChainWithdraw here. */ queue: Address; /** v2.4.2 (DC-15): per-vault BoringSolver — strategist + off-chain bots call boringRedeemSelfSolve. */ solver: Address; /** v2.4.2 (DC-16): per-vault MorphoFlashLoanAdapter. Zero on chains without Morpho. */ flashLoanAdapter: Address; salt: Hex; txHash: Hex; } export class FactoryV2Module { constructor(private readonly client: LucidlyClient) {} /** Chain-invariant factory address. Exposed for callers that need to display it. */ address(): Address { return FACTORY_V2_ADDRESS; } /** * Deploy a new v2.3 vault. Sends `factory.deployVault(params)`, waits for the receipt, decodes * `VaultDeployed`, and returns the parsed stack. Requires a signer. */ async deploy(params: FactoryV2DeployParams): Promise { const salt = params.salt ?? (keccak256(stringToHex(`lucidly:${params.name}:${params.symbol}:${this.client.chainConfig.id}`)) as Hex); const args = [ { salt, name: params.name, symbol: params.symbol, baseAsset: params.baseAsset, startingRate: params.startingRate, upperBoundBps: params.upperBoundBps, lowerBoundBps: params.lowerBoundBps, minUpdateDelay: params.minUpdateDelay, performanceFeeBps: params.performanceFeeBps, shareLockSeconds: params.shareLockSeconds, rateUpdater: params.rateUpdater, solver: params.solver, depositAssets: params.depositAssets, allowPublicDeposits: params.allowPublicDeposits, }, ] as const; const receipt = await this.client.sendAndWait({ address: FACTORY_V2_ADDRESS, abi: FACTORY_V2_ABI, functionName: "deployVault", args, }); // VaultDeployed event topic — same for every chain. for (const log of receipt.logs) { try { const decoded = decodeEventLog({ abi: [VAULT_DEPLOYED_EVENT], data: log.data, topics: log.topics, }); if (decoded.eventName !== "VaultDeployed") continue; const a = decoded.args as unknown as { strategist: Address; salt: Hex; vault: Address; rolesAuthority: Address; manager: Address; accountant: Address; teller: Address; policyManager: Address; baseAsset: Address; queue: Address; solver: Address; flashLoanAdapter: Address; }; return { ...a, txHash: receipt.transactionHash }; } catch { /* not the VaultDeployed log — skip */ } } throw new Error("deployVault succeeded but no VaultDeployed log was found"); } /** Tx-only deploy: send and return { hash, wait } without parsing logs. */ async deployTx(params: FactoryV2DeployParams): Promise { const salt = params.salt ?? (keccak256(stringToHex(`lucidly:${params.name}:${params.symbol}:${this.client.chainConfig.id}`)) as Hex); const hash = await this.client.send({ address: FACTORY_V2_ADDRESS, abi: FACTORY_V2_ABI, functionName: "deployVault", args: [{ ...params, salt }] as const, }); return this.client.wrapTx(hash); } }