import { maxUint256, type Address } from "viem"; import type { LucidlyClient } from "./client.js"; import type { TxResult } from "./types.js"; import { Roles } from "./types.js"; import { selector } from "./abis.js"; import { NoActiveVaultError } from "./errors.js"; import { RolesAuthority } from "./artifacts.js"; import { AtomicQueue, LucidlyRedeemSolver } from "./queueArtifacts.js"; const ERC20_APPROVE_ABI = [ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ type: "bool" }], }, ] as const; /** Addresses of a deployed queue + its redeem solver. */ export interface QueueDeployment { queue: Address; solver: Address; } /** Decoded {@link AtomicQueue} request. */ export interface AtomicRequestView { /** unix seconds after which the request can no longer be solved. */ deadline: bigint; /** price of 1.0 whole share in `want` base units (the user's limit; fills at this or better). */ atomicPrice: bigint; /** shares the user is offering to redeem. */ offerAmount: bigint; /** true while a solve is mid-flight for this request. */ inSolve: boolean; } /** * Queued (asynchronous) redemptions via the Se7en-Seas AtomicQueue — the same mechanism syUSD * uses for withdrawals. Users post an {@link AtomicRequest} (offer shares, want an asset, at a * limit price by a deadline) and approve the queue to pull their shares. A SOLVER later unwinds * positions to free liquidity and fulfills a batch atomically through {@link LucidlyRedeemSolver}, * which burns the shares through the Teller and pays each user their `want` at the request price. * * This is the right path for vaults holding illiquid positions (Pendle, locked LPs, etc.) where the * instant Teller `bulkWithdraw` would revert for lack of free `want` liquidity. */ export class QueueModule { constructor(private readonly c: LucidlyClient) {} private _queue?: Address; private _solver?: Address; /** Point this module at an already-deployed queue + solver (e.g. a shared, multi-vault queue). */ attach(d: QueueDeployment): void { this._queue = d.queue; this._solver = d.solver; } get address(): Address | undefined { return this._queue ?? this.c.activeDeployment?.queue; } get solverAddress(): Address | undefined { return this._solver ?? this.c.activeDeployment?.redeemSolver; } private requireQueue(op: string, override?: Address): Address { const q = override ?? this.address; if (!q) throw new NoActiveVaultError(op + " (no queue; run queue.deployQueue() or queue.attach())"); return q; } private requireSolver(op: string, override?: Address): Address { const s = override ?? this.solverAddress; if (!s) throw new NoActiveVaultError(op + " (no redeem solver; run queue.deployQueue() or queue.attach())"); return s; } /** * Deploy an AtomicQueue + a LucidlyRedeemSolver and wire them into the active vault's * RolesAuthority: `updateAtomicRequest` becomes public (any holder can queue a request) and the * solver contract is granted SOLVER so it can drive `solve` and the Teller's `bulkWithdraw`. * * @param opts.operator EOA allowed to run solves and that supplies `want` top-up liquidity * (defaults to the signer). @param opts.owner queue admin able to pause (defaults to signer). * Run before finalizing vault ownership, or have the multisig owner perform the wiring. */ async deployQueue( opts: { operator?: Address; owner?: Address; rolesAuthority?: Address } = {}, ): Promise { const { account } = this.c.requireSigner("queue.deployQueue"); const admin = account.address; const ra = this.c.requireRolesAuthority("queue.deployQueue", opts.rolesAuthority); const owner = opts.owner ?? admin; const operator = opts.operator ?? admin; const queue = await this.c.deployContract({ abi: AtomicQueue.abi, bytecode: AtomicQueue.bytecode, args: [owner, ra], }); const solver = await this.c.deployContract({ abi: LucidlyRedeemSolver.abi, bytecode: LucidlyRedeemSolver.bytecode, args: [operator], }); const updateSel = selector(AtomicQueue.abi, "updateAtomicRequest"); const solveSel = selector(AtomicQueue.abi, "solve"); // Any share holder can post/maintain a request. await this.c.sendAndWait({ address: ra, abi: RolesAuthority.abi, functionName: "setPublicCapability", args: [queue, updateSel, true], }); // SOLVER role may call queue.solve; grant it to the solver contract (it also needs SOLVER for // the Teller's bulkWithdraw, a capability already wired at deploy). await this.c.sendAndWait({ address: ra, abi: RolesAuthority.abi, functionName: "setRoleCapability", args: [Roles.SOLVER, queue, solveSel, true], }); await this.c.sendAndWait({ address: ra, abi: RolesAuthority.abi, functionName: "setUserRole", args: [solver, Roles.SOLVER, true], }); this._queue = queue; this._solver = solver; const d = this.c.activeDeployment; if (d) { d.queue = queue; d.redeemSolver = solver; this.c.setActiveDeployment(d); } return { queue, solver }; } /** * Post or update a redemption request. The caller offers `amount` of `offer` (the vault share * token, default = active vault) for `want`, at `price` (want base units per 1.0 whole share) and * must approve the queue to pull the shares (done automatically unless autoApprove=false). */ async request( p: { want: Address | string; amount: bigint; price: bigint; offer?: Address | string; deadline?: bigint | number; }, opts: { queue?: Address; autoApprove?: boolean } = {}, ): Promise { const q = this.requireQueue("queue.request", opts.queue); const offer = p.offer ? this.c.resolveToken(p.offer) : this.c.requireVault("queue.request"); const want = this.c.resolveToken(p.want); const deadline = BigInt(p.deadline ?? Math.floor(Date.now() / 1000) + 86_400); if (opts.autoApprove !== false) { await this.c.sendAndWait({ address: offer, abi: ERC20_APPROVE_ABI, functionName: "approve", args: [q, p.amount], }); } const hash = await this.c.send({ address: q, abi: AtomicQueue.abi, functionName: "updateAtomicRequest", args: [offer, want, { deadline, atomicPrice: p.price, offerAmount: p.amount, inSolve: false }], }); return this.c.wrapTx(hash); } /** Read a user's current request for an (offer, want) pair. */ async getRequest( user: Address, want: Address | string, offer?: Address | string, queue?: Address, ): Promise { const q = this.requireQueue("queue.getRequest", queue); const o = offer ? this.c.resolveToken(offer) : this.c.requireVault("queue.getRequest"); const w = this.c.resolveToken(want); const r = await this.c.read<{ deadline: bigint; atomicPrice: bigint; offerAmount: bigint; inSolve: boolean }>({ address: q, abi: AtomicQueue.abi, functionName: "getUserAtomicRequest", args: [user, o, w], }); return { deadline: BigInt(r.deadline), atomicPrice: BigInt(r.atomicPrice), offerAmount: BigInt(r.offerAmount), inSolve: r.inSolve, }; } /** True if the request is currently solvable (approved, funded, live, nonzero price/amount). */ async isRequestValid( user: Address, want: Address | string, offer?: Address | string, queue?: Address, ): Promise { const q = this.requireQueue("queue.isRequestValid", queue); const o = offer ? this.c.resolveToken(offer) : this.c.requireVault("queue.isRequestValid"); const req = await this.getRequest(user, want, o, q); return this.c.read({ address: q, abi: AtomicQueue.abi, functionName: "isAtomicRequestValid", args: [o, user, req], }); } /** * Solver path: fulfill a batch of requests atomically. The solver contract pulls each user's * shares, the Teller burns them for `want` (sending proceeds to the operator), the operator tops * up the exact `want` owed, and the queue distributes it to users at their request price. * * @param p.minAssetsOut floor on Teller bulkWithdraw proceeds (solver slippage guard). * @param p.maxAssets cap on total `want` paid out across the batch (defaults to unbounded). * The operator must approve `want` to the solver contract (done automatically). */ async solve( p: { want: Address | string; users: Address[]; offer?: Address | string; minAssetsOut?: bigint; maxAssets?: bigint; }, opts: { queue?: Address; solver?: Address; teller?: Address; autoApprove?: boolean } = {}, ): Promise { const q = this.requireQueue("queue.solve", opts.queue); const solver = this.requireSolver("queue.solve", opts.solver); const teller = this.c.requireTeller("queue.solve", opts.teller); const offer = p.offer ? this.c.resolveToken(p.offer) : this.c.requireVault("queue.solve"); const want = this.c.resolveToken(p.want); const maxAssets = p.maxAssets ?? maxUint256; if (opts.autoApprove !== false) { await this.c.sendAndWait({ address: want, abi: ERC20_APPROVE_ABI, functionName: "approve", args: [solver, maxUint256], }); } const hash = await this.c.send({ address: solver, abi: LucidlyRedeemSolver.abi, functionName: "redeemSolve", args: [q, offer, want, p.users, p.minAssetsOut ?? 0n, maxAssets, teller], }); return this.c.wrapTx(hash); } }