import { zeroAddress, type Address, type Hex, type AbiFunction } from "viem"; import type { LucidlyClient } from "./client.js"; import type { DeployParams, Deployed } from "./types.js"; import { Roles } from "./types.js"; import { selector } from "./abis.js"; import { RolesAuthority, BoringVault, ManagerWithMerkleVerification, AccountantWithRateProviders, TellerWithMultiAssetSupport, } from "./artifacts.js"; import { tokenDecimals } from "./chains.js"; import { SYUSD_ROOT } from "./syusdBaseline.js"; import { regenerateSyUsdBaseline } from "./syusd.js"; import { LucidlyFullSyUsdDecoder } from "./decoderArtifacts.js"; import { preflightDeploy, type PreflightReport } from "./safety.js"; import { SafetyError } from "./errors.js"; const DEAD: Address = "0x000000000000000000000000000000000000dEaD"; export interface VaultView { vault: Address; name: string; symbol: string; decimals: number; totalSupply: bigint; rate: bigint; } export class VaultModule { constructor(private readonly c: LucidlyClient) {} setActive(vault: Address): void { this.c.setActiveVault(vault); } /** * Deploy the full Boring Vault stack and wire all roles, matching the proven * Foundry engine (DeployLucidlyVault.s.sol) step-for-step. */ /** Run the pre-mainnet deploy checklist without broadcasting. */ async preflight(params: DeployParams): Promise { return preflightDeploy(this.c, params); } async deploy(params: DeployParams): Promise { const { account } = this.c.requireSigner("vault.deploy"); const admin = account.address; const chain = this.c.chainConfig; // Pre-mainnet safety checklist — blocking errors stop the deploy unless allowUnsafe. const report = await preflightDeploy(this.c, params); if (report.errors.length > 0 && !params.allowUnsafe) throw new SafetyError(report.errors); const base = this.c.resolveToken(params.baseAsset); const baseDecimals = tokenDecimals(chain, params.baseAsset); const decimals = params.decimals ?? baseDecimals ?? 18; const weth = chain.tokens.WETH.address; const balancerVault = chain.contracts.balancerVault; const depositAssets = (params.allowedDepositAssets ?? [params.baseAsset]).map((a) => this.c.resolveToken(a), ); const initialRate = params.initialRate ?? 10n ** BigInt(decimals); const bounds = params.exchangeRateBounds ?? { upperBps: 50, lowerBps: 50, minUpdateDelay: 3600 }; const upperBound = 10_000 + bounds.upperBps; // 1e4 scale: 10050 = +0.5% const lowerBound = 10_000 - bounds.lowerBps; // 9950 = -0.5% const platformFee = params.managementFeeBps ?? 0; const performanceFee = params.performanceFeeBps ?? 0; const shareLock = BigInt(params.shareLockSeconds ?? 0); const owner = params.owner ? this.c.resolveToken(params.owner) : admin; const payout = params.payout ? this.c.resolveToken(params.payout) : owner; const strategist = params.strategist ? this.c.resolveToken(params.strategist) : zeroAddress; const rateUpdater = params.rateUpdater ? this.c.resolveToken(params.rateUpdater) : zeroAddress; const solver = params.solver ? this.c.resolveToken(params.solver) : zeroAddress; const allowPublicDeposits = params.allowPublicDeposits ?? true; const finalizeOwnership = params.finalizeOwnership ?? false; // 1) RolesAuthority (admin owns it during setup) const rolesAuthority = await this.c.deployContract({ abi: RolesAuthority.abi, bytecode: RolesAuthority.bytecode, args: [admin, zeroAddress], }); // 2) BoringVault const vault = await this.c.deployContract({ abi: BoringVault.abi, bytecode: BoringVault.bytecode, args: [admin, params.name, params.symbol, decimals], }); // 3) Manager / Accountant / Teller const manager = await this.c.deployContract({ abi: ManagerWithMerkleVerification.abi, bytecode: ManagerWithMerkleVerification.bytecode, args: [admin, vault, balancerVault], }); const accountant = await this.c.deployContract({ abi: AccountantWithRateProviders.abi, bytecode: AccountantWithRateProviders.bytecode, args: [admin, vault, payout, initialRate, base, upperBound, lowerBound, bounds.minUpdateDelay, platformFee, performanceFee], }); const teller = await this.c.deployContract({ abi: TellerWithMultiAssetSupport.abi, bytecode: TellerWithMultiAssetSupport.bytecode, args: [admin, vault, accountant, weth], }); const d: Deployed = { rolesAuthority, vault, manager, accountant, teller }; // 4) Point each contract at the shared RolesAuthority for (const target of [vault, manager, accountant, teller]) { await this.c.sendAndWait({ address: target, abi: BoringVault.abi, // setAuthority is inherited from solmate Auth on all four functionName: "setAuthority", args: [rolesAuthority], }); } await this.wireCapabilities(d); await this.wireUsers(d, { strategist, rateUpdater, solver }); await this.configure(d, { depositAssets, shareLock, allowPublicDeposits }); // Seed the syUSD baseline strategy whitelist as the initial manage root (default on). // admin is still each contract's owner here, so it can call setManageRoot directly. const seedMode = params.syUsdSeedMode ?? (params.seedSyUsdLeaves === false ? "none" : "repointed"); if (seedMode !== "none") { const rootRecipient = strategist !== zeroAddress ? strategist : admin; let root = SYUSD_ROOT; if (seedMode === "repointed") { // Deploy this vault's own full syUSD decoder (all 47 signatures) and regenerate every // syUSD leaf against it (decoder field -> this decoder, recipients -> this vault). const nfpm = chain.contracts.uniswapV3PositionManager ?? zeroAddress; const odos = chain.contracts.odosRouter ?? zeroAddress; const magpie = chain.contracts.magpieRouter ?? zeroAddress; const ownDecoder = await this.c.deployContract({ abi: LucidlyFullSyUsdDecoder.abi, bytecode: LucidlyFullSyUsdDecoder.bytecode, args: [nfpm, odos, magpie], }); d.syUsdDecoder = ownDecoder; root = regenerateSyUsdBaseline({ vault, decoder: ownDecoder, accountant }).root; } await this.c.sendAndWait({ address: manager, abi: ManagerWithMerkleVerification.abi, functionName: "setManageRoot", args: [rootRecipient, root], }); } // Ownership if (finalizeOwnership) { await this.finalize(owner, d); } else { await this.setUserRole(rolesAuthority, admin, Roles.OWNER); } this.c.setActiveDeployment(d); return d; } /** * Irreversibly hand control to `owner` (a multisig/timelock): grant it the OWNER role, renounce * each contract's Auth ownership (so only the shared RolesAuthority governs), then transfer the * RolesAuthority itself to `owner`. Run this *after* any admin-only setup that still needs the * deployer to own the RolesAuthority — e.g. seeding the root, wiring a queue, enabling integrations. */ async finalize(owner: Address | string, d?: Deployed): Promise { const { account } = this.c.requireSigner("vault.finalize"); const dep = d ?? this.c.activeDeployment; if (!dep) throw new Error("vault.finalize: no deployment (pass one or setActiveDeployment first)"); const o = this.c.resolveToken(owner); await this.setUserRole(dep.rolesAuthority, o, Roles.OWNER); for (const target of [dep.vault, dep.manager, dep.accountant, dep.teller]) { await this.c.sendAndWait({ address: target, abi: BoringVault.abi, functionName: "transferOwnership", args: [zeroAddress] }); } // Drop the deployer's OWNER role if it was granted during admin-only setup, so no EOA retains // control after hand-off (no-op when the deployer never held it). if (o.toLowerCase() !== account.address.toLowerCase()) { await this.c.sendAndWait({ address: dep.rolesAuthority, abi: RolesAuthority.abi, functionName: "setUserRole", args: [account.address, Roles.OWNER, false], }); } await this.c.sendAndWait({ address: dep.rolesAuthority, abi: RolesAuthority.abi, functionName: "transferOwnership", args: [o] }); } // ---- wiring helpers (mirror the engine) ---- private async setRoleCapability(ra: Address, role: number, target: Address, sel: Hex) { await this.c.sendAndWait({ address: ra, abi: RolesAuthority.abi, functionName: "setRoleCapability", args: [role, target, sel, true], }); } private async setUserRole(ra: Address, user: Address, role: number) { await this.c.sendAndWait({ address: ra, abi: RolesAuthority.abi, functionName: "setUserRole", args: [user, role, true], }); } private async wireCapabilities(d: Deployed) { const ra = d.rolesAuthority; const isAddr = (fn: AbiFunction) => fn.inputs[0]?.type === "address"; const isAddrArr = (fn: AbiFunction) => fn.inputs[0]?.type === "address[]"; const manageSingle = selector(BoringVault.abi, "manage", isAddr); const manageBatch = selector(BoringVault.abi, "manage", isAddrArr); const enterSel = selector(BoringVault.abi, "enter"); const exitSel = selector(BoringVault.abi, "exit"); const setBeforeTransferHook = selector(BoringVault.abi, "setBeforeTransferHook"); const manageMerkle = selector(ManagerWithMerkleVerification.abi, "manageVaultWithMerkleVerification"); const setManageRoot = selector(ManagerWithMerkleVerification.abi, "setManageRoot"); const bulkDeposit = selector(TellerWithMultiAssetSupport.abi, "bulkDeposit"); const bulkWithdraw = selector(TellerWithMultiAssetSupport.abi, "bulkWithdraw"); const updateAssetData = selector(TellerWithMultiAssetSupport.abi, "updateAssetData"); const setShareLockPeriod = selector(TellerWithMultiAssetSupport.abi, "setShareLockPeriod"); const updateExchangeRate = selector(AccountantWithRateProviders.abi, "updateExchangeRate"); const setRateProviderData = selector(AccountantWithRateProviders.abi, "setRateProviderData"); const updateDelay = selector(AccountantWithRateProviders.abi, "updateDelay"); const updateUpper = selector(AccountantWithRateProviders.abi, "updateUpper"); const updateLower = selector(AccountantWithRateProviders.abi, "updateLower"); const updatePlatformFee = selector(AccountantWithRateProviders.abi, "updatePlatformFee"); const updatePayoutAddress = selector(AccountantWithRateProviders.abi, "updatePayoutAddress"); // MANAGER -> vault.manage (both overloads) await this.setRoleCapability(ra, Roles.MANAGER, d.vault, manageSingle); await this.setRoleCapability(ra, Roles.MANAGER, d.vault, manageBatch); // MINTER / BURNER -> vault.enter / vault.exit await this.setRoleCapability(ra, Roles.MINTER, d.vault, enterSel); await this.setRoleCapability(ra, Roles.BURNER, d.vault, exitSel); // Strategy execution await this.setRoleCapability(ra, Roles.MANAGER_INTERNAL, d.manager, manageMerkle); await this.setRoleCapability(ra, Roles.STRATEGIST, d.manager, manageMerkle); // Solver -> bulk deposit/withdraw await this.setRoleCapability(ra, Roles.SOLVER, d.teller, bulkDeposit); await this.setRoleCapability(ra, Roles.SOLVER, d.teller, bulkWithdraw); // NAV updates await this.setRoleCapability(ra, Roles.UPDATE_EXCHANGE_RATE, d.accountant, updateExchangeRate); // OWNER -> administrative surface await this.setRoleCapability(ra, Roles.OWNER, d.manager, setManageRoot); await this.setRoleCapability(ra, Roles.OWNER, d.teller, updateAssetData); await this.setRoleCapability(ra, Roles.OWNER, d.teller, setShareLockPeriod); await this.setRoleCapability(ra, Roles.OWNER, d.vault, setBeforeTransferHook); await this.setRoleCapability(ra, Roles.OWNER, d.accountant, setRateProviderData); await this.setRoleCapability(ra, Roles.OWNER, d.accountant, updateDelay); await this.setRoleCapability(ra, Roles.OWNER, d.accountant, updateUpper); await this.setRoleCapability(ra, Roles.OWNER, d.accountant, updateLower); await this.setRoleCapability(ra, Roles.OWNER, d.accountant, updatePlatformFee); await this.setRoleCapability(ra, Roles.OWNER, d.accountant, updatePayoutAddress); } private async wireUsers(d: Deployed, u: { strategist: Address; rateUpdater: Address; solver: Address }) { const ra = d.rolesAuthority; await this.setUserRole(ra, d.manager, Roles.MANAGER); await this.setUserRole(ra, d.manager, Roles.MANAGER_INTERNAL); await this.setUserRole(ra, d.teller, Roles.MINTER); await this.setUserRole(ra, d.teller, Roles.BURNER); if (u.strategist !== zeroAddress) await this.setUserRole(ra, u.strategist, Roles.STRATEGIST); if (u.rateUpdater !== zeroAddress) await this.setUserRole(ra, u.rateUpdater, Roles.UPDATE_EXCHANGE_RATE); if (u.solver !== zeroAddress) await this.setUserRole(ra, u.solver, Roles.SOLVER); } private async configure( d: Deployed, cfg: { depositAssets: Address[]; shareLock: bigint; allowPublicDeposits: boolean }, ) { for (const asset of cfg.depositAssets) { await this.c.sendAndWait({ address: d.teller, abi: TellerWithMultiAssetSupport.abi, functionName: "updateAssetData", args: [asset, true, true, 0], }); } await this.c.sendAndWait({ address: d.teller, abi: TellerWithMultiAssetSupport.abi, functionName: "setShareLockPeriod", args: [cfg.shareLock], }); if (cfg.allowPublicDeposits) { const depositSel = selector(TellerWithMultiAssetSupport.abi, "deposit"); const depositPermitSel = selector(TellerWithMultiAssetSupport.abi, "depositWithPermit"); for (const sel of [depositSel, depositPermitSel]) { await this.c.sendAndWait({ address: d.rolesAuthority, abi: RolesAuthority.abi, functionName: "setPublicCapability", args: [d.teller, sel, true], }); } } } // ---- reads ---- async view(vault?: Address): Promise { const v = this.c.requireVault("vault.view", vault); const [name, symbol, decimals, totalSupply] = await Promise.all([ this.c.read({ address: v, abi: BoringVault.abi, functionName: "name" }), this.c.read({ address: v, abi: BoringVault.abi, functionName: "symbol" }), this.c.read({ address: v, abi: BoringVault.abi, functionName: "decimals" }), this.c.read({ address: v, abi: BoringVault.abi, functionName: "totalSupply" }), ]); const rate = await this.c.accountant.getRate(); return { vault: v, name, symbol, decimals: Number(decimals), totalSupply, rate }; } /** AUM in base-asset units = totalSupply * rate / 10**shareDecimals. */ async aum(vault?: Address): Promise { const view = await this.view(vault); return (view.totalSupply * view.rate) / 10n ** BigInt(view.decimals); } }