diff --git a/ts/packages/defi-cli/src/commands/lending.ts b/ts/packages/defi-cli/src/commands/lending.ts index da446e9..e98637d 100644 --- a/ts/packages/defi-cli/src/commands/lending.ts +++ b/ts/packages/defi-cli/src/commands/lending.ts @@ -2,7 +2,7 @@ import type { Command } from "commander"; import type { OutputMode } from "../output.js"; import type { Executor } from "../executor.js"; import { printOutput } from "../output.js"; -import { InterestRateMode } from "@hypurrquant/defi-core"; +import { InterestRateMode, type MorphoMarketId } from "@hypurrquant/defi-core"; import type { Address } from "viem"; import { maxUint256 } from "viem"; import { createLending } from "@hypurrquant/defi-protocols"; @@ -59,6 +59,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make .requiredOption("--protocol ", "Protocol slug") .requiredOption("--asset ", "Token symbol or address") .requiredOption("--amount ", "Amount to supply in wei (or 'max')") + .option("--market ", "Morpho Blue marketId (32-byte hex) — required for direct Morpho markets, ignored elsewhere") .option("--on-behalf-of
", "On behalf of address") .action(async (opts) => { const executor = makeExecutor(); @@ -67,7 +68,10 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make const adapter = createLending(ctx.protocol!, ctx.rpcUrl); const asset = resolveTokenAddress(ctx.registry, ctx.chainName, opts.asset); const onBehalfOf = resolveWallet(opts.onBehalfOf); - const tx = await adapter.buildSupply({ protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), on_behalf_of: onBehalfOf }); + const tx = await adapter.buildSupply({ + protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), on_behalf_of: onBehalfOf, + market_id: opts.market as MorphoMarketId | undefined, + }); const result = await executor.execute(tx); printOutput(result, getOpts()); }); @@ -78,6 +82,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make .requiredOption("--asset ", "Token symbol or address") .requiredOption("--amount ", "Amount in wei (or 'max')") .option("--rate-mode ", "variable or stable", "variable") + .option("--market ", "Morpho Blue marketId (32-byte hex) — required for direct Morpho markets, ignored elsewhere") .option("--on-behalf-of
", "On behalf of address") .action(async (opts) => { const executor = makeExecutor(); @@ -90,6 +95,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), interest_rate_mode: opts.rateMode === "stable" ? InterestRateMode.Stable : InterestRateMode.Variable, on_behalf_of: onBehalfOf, + market_id: opts.market as MorphoMarketId | undefined, }); const result = await executor.execute(tx); printOutput(result, getOpts()); @@ -101,6 +107,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make .requiredOption("--asset ", "Token symbol or address") .requiredOption("--amount ", "Amount in wei (or 'max')") .option("--rate-mode ", "variable or stable", "variable") + .option("--market ", "Morpho Blue marketId (32-byte hex) — required for direct Morpho markets, ignored elsewhere") .option("--on-behalf-of
", "On behalf of address") .action(async (opts) => { const executor = makeExecutor(); @@ -113,6 +120,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), interest_rate_mode: opts.rateMode === "stable" ? InterestRateMode.Stable : InterestRateMode.Variable, on_behalf_of: onBehalfOf, + market_id: opts.market as MorphoMarketId | undefined, }); const result = await executor.execute(tx); printOutput(result, getOpts()); @@ -123,6 +131,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make .requiredOption("--protocol ", "Protocol slug") .requiredOption("--asset ", "Token symbol or address") .requiredOption("--amount ", "Amount in wei (or 'max')") + .option("--market ", "Morpho Blue marketId (32-byte hex) — required for direct Morpho markets, ignored elsewhere") .option("--to
", "Recipient address") .action(async (opts) => { const executor = makeExecutor(); @@ -131,7 +140,10 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make const adapter = createLending(ctx.protocol!, ctx.rpcUrl); const asset = resolveTokenAddress(ctx.registry, ctx.chainName, opts.asset); const to = resolveWallet(opts.to); - const tx = await adapter.buildWithdraw({ protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), to }); + const tx = await adapter.buildWithdraw({ + protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), to, + market_id: opts.market as MorphoMarketId | undefined, + }); const result = await executor.execute(tx); printOutput(result, getOpts()); }); @@ -192,4 +204,67 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make const result = await executor.execute(tx); printOutput(result, getOpts()); }); + + lending.command("supply-collateral") + .description("Supply the collateral side of a Morpho Blue market (different selector from supply)") + .requiredOption("--protocol ", "Protocol slug (must be a Morpho Blue adapter)") + .requiredOption("--asset ", "Collateral token symbol or address") + .requiredOption("--amount ", "Amount in wei (or 'max')") + .requiredOption("--market ", "32-byte Morpho marketId (find via Morpho API)") + .option("--on-behalf-of
", "On behalf of address") + .action(async (opts) => { + const executor = makeExecutor(); + const ctx = resolveContext(parent, getOpts, opts.protocol); + if (!ctx) return; + const adapter = createLending(ctx.protocol!, ctx.rpcUrl); + if (typeof adapter.buildSupplyCollateral !== "function") { + printOutput({ + error: `[${ctx.protocol!.name}] adapter does not implement buildSupplyCollateral. ` + + `Only Morpho Blue forks expose this; Aave V3 / Compound use plain supply.`, + }, getOpts()); + return; + } + const asset = resolveTokenAddress(ctx.registry, ctx.chainName, opts.asset); + const onBehalfOf = resolveWallet(opts.onBehalfOf); + const tx = await adapter.buildSupplyCollateral({ + protocol: ctx.protocol!.name, + asset, + amount: parseAmount(opts.amount), + on_behalf_of: onBehalfOf, + market_id: opts.market as MorphoMarketId, + }); + const result = await executor.execute(tx); + printOutput(result, getOpts()); + }); + + lending.command("withdraw-collateral") + .description("Withdraw the collateral side of a Morpho Blue market") + .requiredOption("--protocol ", "Protocol slug (must be a Morpho Blue adapter)") + .requiredOption("--asset ", "Collateral token symbol or address") + .requiredOption("--amount ", "Amount in wei (or 'max')") + .requiredOption("--market ", "32-byte Morpho marketId") + .option("--to
", "Recipient address") + .action(async (opts) => { + const executor = makeExecutor(); + const ctx = resolveContext(parent, getOpts, opts.protocol); + if (!ctx) return; + const adapter = createLending(ctx.protocol!, ctx.rpcUrl); + if (typeof adapter.buildWithdrawCollateral !== "function") { + printOutput({ + error: `[${ctx.protocol!.name}] adapter does not implement buildWithdrawCollateral.`, + }, getOpts()); + return; + } + const asset = resolveTokenAddress(ctx.registry, ctx.chainName, opts.asset); + const to = resolveWallet(opts.to); + const tx = await adapter.buildWithdrawCollateral({ + protocol: ctx.protocol!.name, + asset, + amount: parseAmount(opts.amount), + to, + market_id: opts.market as MorphoMarketId, + }); + const result = await executor.execute(tx); + printOutput(result, getOpts()); + }); } diff --git a/ts/packages/defi-core/src/traits/lending.ts b/ts/packages/defi-core/src/traits/lending.ts index 4b51bad..37bc312 100644 --- a/ts/packages/defi-core/src/traits/lending.ts +++ b/ts/packages/defi-core/src/traits/lending.ts @@ -4,6 +4,8 @@ import type { BorrowParams, RepayParams, WithdrawParams, + SupplyCollateralParams, + WithdrawCollateralParams, LendingRates, UserPosition, DeFiTx, @@ -35,4 +37,18 @@ export interface ILending { * eMode concept leave this undefined. */ buildSetEMode?(categoryId: number): Promise; + + /** + * Optional — supply the *collateral* side of a Morpho Blue market + * (separate selector from `supply`, which is the loan-asset LP path). + * Aave V3 collapses both into supply/withdraw, so its adapter leaves + * this undefined. Morpho Blue's adapter requires `params.market_id`. + */ + buildSupplyCollateral?(params: SupplyCollateralParams): Promise; + + /** + * Optional — withdraw the collateral side of a Morpho Blue market. + * Aave V3 leaves this undefined; Morpho Blue requires market_id. + */ + buildWithdrawCollateral?(params: WithdrawCollateralParams): Promise; } diff --git a/ts/packages/defi-core/src/types.ts b/ts/packages/defi-core/src/types.ts index f7cb683..9a99410 100644 --- a/ts/packages/defi-core/src/types.ts +++ b/ts/packages/defi-core/src/types.ts @@ -170,11 +170,21 @@ export interface RemoveLiquidityParams { // === Lending Types === +/** + * Optional 32-byte Morpho Blue marketId. When provided, Morpho-style + * adapters resolve the full MarketParams via `idToMarketParams(id)` + * instead of falling back to a stub. Aave V3 / Compound V2 / Compound V3 + * adapters ignore this field — they identify positions by reserve + * address alone. + */ +export type MorphoMarketId = `0x${string}`; + export interface SupplyParams { protocol: string; asset: Address; amount: bigint; on_behalf_of: Address; + market_id?: MorphoMarketId; } export interface BorrowParams { @@ -183,6 +193,7 @@ export interface BorrowParams { amount: bigint; interest_rate_mode: InterestRateMode; on_behalf_of: Address; + market_id?: MorphoMarketId; } /** Interest rate mode (serde: snake_case) */ @@ -197,6 +208,7 @@ export interface RepayParams { amount: bigint; interest_rate_mode: InterestRateMode; on_behalf_of: Address; + market_id?: MorphoMarketId; } export interface WithdrawParams { @@ -204,6 +216,29 @@ export interface WithdrawParams { asset: Address; amount: bigint; to: Address; + market_id?: MorphoMarketId; +} + +/** + * Morpho Blue distinguishes loan-side liquidity (supply / withdraw) from + * collateral-side liquidity (supplyCollateral / withdrawCollateral). + * Aave V3 collapses both into supply/withdraw; Morpho needs the dedicated + * params type because the underlying selector and accounting differ. + */ +export interface SupplyCollateralParams { + protocol: string; + asset: Address; + amount: bigint; + on_behalf_of: Address; + market_id: MorphoMarketId; +} + +export interface WithdrawCollateralParams { + protocol: string; + asset: Address; + amount: bigint; + to: Address; + market_id: MorphoMarketId; } export interface LendingRates { diff --git a/ts/packages/defi-protocols/src/lending/morpho.test.ts b/ts/packages/defi-protocols/src/lending/morpho.test.ts new file mode 100644 index 0000000..a148494 --- /dev/null +++ b/ts/packages/defi-protocols/src/lending/morpho.test.ts @@ -0,0 +1,243 @@ +/** + * Morpho Blue adapter regression tests — verifies that supply/borrow/ + * repay/withdraw plus the new supplyCollateral/withdrawCollateral + * methods all encode the right calldata when a marketId is pinned. + * + * Pre-2026-05-07 the adapter shipped a `defaultMarketParams(asset)` + * stub that returned all-zero MarketParams; every direct-Morpho-Blue + * tx therefore reverted on-chain. This file pins the post-fix contract: + * caller passes a 32-byte marketId, the adapter dynamically resolves + * MarketParams via `Morpho.idToMarketParams(id)`, and the calldata + * matches the resolved tuple. + */ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { decodeFunctionData, parseAbi } from "viem"; +import type { Address, Hex } from "viem"; +import { ProtocolCategory, InterestRateMode, type ProtocolEntry } from "@hypurrquant/defi-core"; + +const MORPHO = "0x6c247b1F6182318877311737BaC0844bAa518F5e" as Address; +const LOAN_TOKEN = "0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a" as Address; // AUSD +const COLLAT_TOKEN = "0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A" as Address; // WMON +const ORACLE = "0x409b68a5986a84fD90761BAEb34cB242a2ee02eF" as Address; +const IRM = "0x09475a3D6eA8c314c592b1a3799bDE044E2F400F" as Address; +const LLTV = 770000000000000000n; // 77% +const MARKET_ID = "0xfa0b720389b546fcf8562c18cda8c00460072b63776add7fbfe8cd4f06d7c3ba" as `0x${string}`; +const ON_BEHALF = "0x000000000000000000000000000000000000dEaD" as Address; + +function makeEntry(): ProtocolEntry { + return { + name: "Morpho Blue Monad", + slug: "morpho-blue-monad", + category: ProtocolCategory.Lending, + interface: "morpho_blue", + chain: "monad", + contracts: { morpho_blue: MORPHO }, + } as ProtocolEntry; +} + +const readContractMock = vi.fn(); + +vi.mock("viem", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createPublicClient: () => ({ readContract: readContractMock }), + http: () => () => ({}), + }; +}); + +const SUPPLY_ABI = parseAbi([ + "function supply((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data)", +]); +const BORROW_ABI = parseAbi([ + "function borrow((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver)", +]); +const REPAY_ABI = parseAbi([ + "function repay((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data)", +]); +const WITHDRAW_ABI = parseAbi([ + "function withdraw((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver)", +]); +const SUPPLY_COLLATERAL_ABI = parseAbi([ + "function supplyCollateral((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, address onBehalf, bytes data)", +]); +const WITHDRAW_COLLATERAL_ABI = parseAbi([ + "function withdrawCollateral((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, address onBehalf, address receiver)", +]); + +function mockMarketParamsResolution(): void { + // idToMarketParams returns the canonical AUSD/WMON tuple + readContractMock.mockResolvedValueOnce([LOAN_TOKEN, COLLAT_TOKEN, ORACLE, IRM, LLTV]); +} + +interface MarketTuple { + loanToken: Address; + collateralToken: Address; + oracle: Address; + irm: Address; + lltv: bigint; +} + +function expectMarketParamsMatch(tuple: MarketTuple): void { + expect(tuple.loanToken).toBe(LOAN_TOKEN); + expect(tuple.collateralToken).toBe(COLLAT_TOKEN); + expect(tuple.oracle).toBe(ORACLE); + expect(tuple.irm).toBe(IRM); + expect(tuple.lltv).toBe(LLTV); +} + +describe("MorphoBlueAdapter — direct-market path with marketId", () => { + beforeEach(() => readContractMock.mockReset()); + + it("buildSupply with marketId resolves MarketParams and encodes loan-side supply", async () => { + mockMarketParamsResolution(); + const { MorphoBlueAdapter } = await import("./morpho.js"); + const adapter = new MorphoBlueAdapter(makeEntry(), "https://example/monad"); + const tx = await adapter.buildSupply({ + protocol: "Morpho Blue Monad", + asset: LOAN_TOKEN, + amount: 1_000_000n, + on_behalf_of: ON_BEHALF, + market_id: MARKET_ID, + }); + expect(tx.to).toBe(MORPHO); + const decoded = decodeFunctionData({ abi: SUPPLY_ABI, data: tx.data as Hex }); + expectMarketParamsMatch(decoded.args[0] as MarketTuple); + expect(decoded.args[1]).toBe(1_000_000n); // assets + expect(decoded.args[2]).toBe(0n); // shares (0 means amount-based) + expect(decoded.args[3]).toBe(ON_BEHALF); + expect(tx.approvals).toEqual([{ token: LOAN_TOKEN, spender: MORPHO, amount: 1_000_000n }]); + }); + + it("buildBorrow with marketId encodes a Pool.borrow call against the resolved tuple", async () => { + mockMarketParamsResolution(); + const { MorphoBlueAdapter } = await import("./morpho.js"); + const adapter = new MorphoBlueAdapter(makeEntry(), "https://example/monad"); + const tx = await adapter.buildBorrow({ + protocol: "Morpho Blue Monad", + asset: LOAN_TOKEN, + amount: 50_000n, + interest_rate_mode: InterestRateMode.Variable, + on_behalf_of: ON_BEHALF, + market_id: MARKET_ID, + }); + expect(tx.to).toBe(MORPHO); + const decoded = decodeFunctionData({ abi: BORROW_ABI, data: tx.data as Hex }); + expectMarketParamsMatch(decoded.args[0] as MarketTuple); + expect(decoded.args[1]).toBe(50_000n); + expect(decoded.args[3]).toBe(ON_BEHALF); + expect(decoded.args[4]).toBe(ON_BEHALF); // receiver + }); + + it("buildBorrow without marketId throws a clear DefiError", async () => { + const { MorphoBlueAdapter } = await import("./morpho.js"); + const adapter = new MorphoBlueAdapter(makeEntry(), "https://example/monad"); + await expect( + adapter.buildBorrow({ + protocol: "Morpho Blue Monad", + asset: LOAN_TOKEN, + amount: 1n, + interest_rate_mode: InterestRateMode.Variable, + on_behalf_of: ON_BEHALF, + }), + ).rejects.toThrow(/marketId/); + }); + + it("buildRepay with marketId attaches approvals[]", async () => { + mockMarketParamsResolution(); + const { MorphoBlueAdapter } = await import("./morpho.js"); + const adapter = new MorphoBlueAdapter(makeEntry(), "https://example/monad"); + const tx = await adapter.buildRepay({ + protocol: "Morpho Blue Monad", + asset: LOAN_TOKEN, + amount: 25_000n, + interest_rate_mode: InterestRateMode.Variable, + on_behalf_of: ON_BEHALF, + market_id: MARKET_ID, + }); + const decoded = decodeFunctionData({ abi: REPAY_ABI, data: tx.data as Hex }); + expectMarketParamsMatch(decoded.args[0] as MarketTuple); + expect(tx.approvals).toEqual([{ token: LOAN_TOKEN, spender: MORPHO, amount: 25_000n }]); + }); + + it("buildWithdraw with marketId routes loan-side withdrawal to Morpho.withdraw", async () => { + mockMarketParamsResolution(); + const { MorphoBlueAdapter } = await import("./morpho.js"); + const adapter = new MorphoBlueAdapter(makeEntry(), "https://example/monad"); + const tx = await adapter.buildWithdraw({ + protocol: "Morpho Blue Monad", + asset: LOAN_TOKEN, + amount: 10_000n, + to: ON_BEHALF, + market_id: MARKET_ID, + }); + expect(tx.to).toBe(MORPHO); + const decoded = decodeFunctionData({ abi: WITHDRAW_ABI, data: tx.data as Hex }); + expectMarketParamsMatch(decoded.args[0] as MarketTuple); + expect(decoded.args[3]).toBe(ON_BEHALF); // onBehalf = to + expect(decoded.args[4]).toBe(ON_BEHALF); // receiver = to + }); + + it("buildSupplyCollateral encodes supplyCollateral with the resolved tuple", async () => { + mockMarketParamsResolution(); + const { MorphoBlueAdapter } = await import("./morpho.js"); + const adapter = new MorphoBlueAdapter(makeEntry(), "https://example/monad"); + const tx = await adapter.buildSupplyCollateral!({ + protocol: "Morpho Blue Monad", + asset: COLLAT_TOKEN, + amount: 500_000n, + on_behalf_of: ON_BEHALF, + market_id: MARKET_ID, + }); + expect(tx.to).toBe(MORPHO); + const decoded = decodeFunctionData({ abi: SUPPLY_COLLATERAL_ABI, data: tx.data as Hex }); + expectMarketParamsMatch(decoded.args[0] as MarketTuple); + expect(decoded.args[1]).toBe(500_000n); + expect(decoded.args[2]).toBe(ON_BEHALF); + expect(tx.approvals).toEqual([{ token: COLLAT_TOKEN, spender: MORPHO, amount: 500_000n }]); + }); + + it("buildWithdrawCollateral encodes withdrawCollateral", async () => { + mockMarketParamsResolution(); + const { MorphoBlueAdapter } = await import("./morpho.js"); + const adapter = new MorphoBlueAdapter(makeEntry(), "https://example/monad"); + const tx = await adapter.buildWithdrawCollateral!({ + protocol: "Morpho Blue Monad", + asset: COLLAT_TOKEN, + amount: 500_000n, + to: ON_BEHALF, + market_id: MARKET_ID, + }); + expect(tx.to).toBe(MORPHO); + const decoded = decodeFunctionData({ abi: WITHDRAW_COLLATERAL_ABI, data: tx.data as Hex }); + expectMarketParamsMatch(decoded.args[0] as MarketTuple); + expect(decoded.args[1]).toBe(500_000n); + expect(decoded.args[2]).toBe(ON_BEHALF); + expect(decoded.args[3]).toBe(ON_BEHALF); + }); + + it("rejects when idToMarketParams returns an empty tuple (zero-init guard)", async () => { + // Pre-fix bug class: passing a marketId that the deployment doesn't + // know about caused the adapter to silently send all-zero params and + // revert on-chain. Now we throw a clear error before the user spends + // gas on an unknown market. + readContractMock.mockResolvedValueOnce([ + "0x0000000000000000000000000000000000000000" as Address, + "0x0000000000000000000000000000000000000000" as Address, + "0x0000000000000000000000000000000000000000" as Address, + "0x0000000000000000000000000000000000000000" as Address, + 0n, + ]); + const { MorphoBlueAdapter } = await import("./morpho.js"); + const adapter = new MorphoBlueAdapter(makeEntry(), "https://example/monad"); + await expect( + adapter.buildSupply({ + protocol: "Morpho Blue Monad", + asset: LOAN_TOKEN, + amount: 1n, + on_behalf_of: ON_BEHALF, + market_id: "0x0000000000000000000000000000000000000000000000000000000000000000", + }), + ).rejects.toThrow(/empty MarketParams|registered market/); + }); +}); diff --git a/ts/packages/defi-protocols/src/lending/morpho.ts b/ts/packages/defi-protocols/src/lending/morpho.ts index ab6ac9c..4b6985e 100644 --- a/ts/packages/defi-protocols/src/lending/morpho.ts +++ b/ts/packages/defi-protocols/src/lending/morpho.ts @@ -1,4 +1,4 @@ -import { parseAbi, encodeFunctionData, decodeFunctionResult, zeroAddress } from "viem"; +import { parseAbi, encodeFunctionData, decodeFunctionResult, zeroAddress, createPublicClient, http } from "viem"; import type { Address, Hex } from "viem"; import type { ILending } from "@hypurrquant/defi-core"; import { @@ -10,6 +10,8 @@ import { type BorrowParams, type RepayParams, type WithdrawParams, + type SupplyCollateralParams, + type WithdrawCollateralParams, type LendingRates, type UserPosition, type DeFiTx, @@ -19,9 +21,11 @@ const MORPHO_ABI = parseAbi([ "function market(bytes32 id) external view returns (uint128 totalSupplyAssets, uint128 totalSupplyShares, uint128 totalBorrowAssets, uint128 totalBorrowShares, uint128 lastUpdate, uint128 fee)", "function idToMarketParams(bytes32 id) external view returns (address loanToken, address collateralToken, address oracle, address irm, uint256 lltv)", "function supply((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) external returns (uint256 assetsSupplied, uint256 sharesSupplied)", + "function supplyCollateral((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, address onBehalf, bytes data) external", "function borrow((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) external returns (uint256 assetsBorrowed, uint256 sharesBorrowed)", "function repay((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) external returns (uint256 assetsRepaid, uint256 sharesRepaid)", "function withdraw((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) external returns (uint256 assetsWithdrawn, uint256 sharesWithdrawn)", + "function withdrawCollateral((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, address onBehalf, address receiver) external", ]); const META_MORPHO_ABI = parseAbi([ @@ -55,16 +59,6 @@ type MarketParams = { lltv: bigint; }; -function defaultMarketParams(loanToken: Address = zeroAddress as Address): MarketParams { - return { - loanToken, - collateralToken: zeroAddress as Address, - oracle: zeroAddress as Address, - irm: zeroAddress as Address, - lltv: 0n, - }; -} - function decodeMarket(data: Hex | null): [bigint, bigint, bigint, bigint, bigint, bigint] | null { if (!data) return null; try { @@ -150,7 +144,62 @@ export class MorphoBlueAdapter implements ILending { return this.protocolName; } + /** + * Resolve a Morpho Blue marketId into the full MarketParams tuple by + * calling Morpho.idToMarketParams(id). Used by every direct-market + * method (supply / borrow / repay / withdraw / supplyCollateral / + * withdrawCollateral) so the caller only has to pass the 32-byte + * marketId — same shape as the Morpho UI / API. + */ + private async resolveMarketParams(marketId: `0x${string}`): Promise { + if (!this.rpcUrl) { + throw DefiError.rpcError( + `[${this.protocolName}] No RPC URL configured — cannot resolve marketId ${marketId}`, + ); + } + const client = createPublicClient({ transport: http(this.rpcUrl) }); + let result: readonly [Address, Address, Address, Address, bigint]; + try { + result = await client.readContract({ + address: this.morpho, + abi: MORPHO_ABI, + functionName: "idToMarketParams", + args: [marketId], + }) as readonly [Address, Address, Address, Address, bigint]; + } catch (e) { + throw DefiError.rpcError( + `[${this.protocolName}] idToMarketParams(${marketId}) failed: ${e}`, + ); + } + const [loanToken, collateralToken, oracle, irm, lltv] = result; + if (loanToken === zeroAddress || collateralToken === zeroAddress || lltv === 0n) { + throw DefiError.invalidParam( + `[${this.protocolName}] marketId ${marketId} resolves to an empty MarketParams ` + + `(loan=${loanToken}, collateral=${collateralToken}, lltv=${lltv}). ` + + `Verify the id matches a registered market on this chain.`, + ); + } + return { loanToken, collateralToken, oracle, irm, lltv }; + } + async buildSupply(params: SupplyParams): Promise { + // Direct Morpho Blue market (loan-side LP) when caller pins marketId. + if (params.market_id) { + const market = await this.resolveMarketParams(params.market_id); + const data = encodeFunctionData({ + abi: MORPHO_ABI, + functionName: "supply", + args: [market, params.amount, 0n, params.on_behalf_of, "0x"], + }); + return { + description: `[${this.protocolName}] Supply ${params.amount} of ${params.asset} to market ${params.market_id.slice(0, 10)}…`, + to: this.morpho, + data, + value: 0n, + gas_estimate: 350_000, + approvals: [{ token: params.asset, spender: this.morpho, amount: params.amount }], + }; + } const vault = await this.resolveVault(params.asset); if (vault) { const data = encodeFunctionData({ @@ -167,46 +216,83 @@ export class MorphoBlueAdapter implements ILending { approvals: [{ token: params.asset, spender: vault, amount: params.amount }], }; } - const market = defaultMarketParams(params.asset); + throw DefiError.invalidParam( + `[${this.protocolName}] supply requires either a registered MetaMorpho vault for ` + + `${params.asset} or an explicit --market . The legacy zero-MarketParams ` + + `stub was removed (it always reverted on-chain).`, + ); + } + + async buildBorrow(params: BorrowParams): Promise { + if (!params.market_id) { + throw DefiError.invalidParam( + `[${this.protocolName}] Morpho Blue borrow requires --market . ` + + `Find one via the Morpho API (https://blue-api.morpho.org/graphql).`, + ); + } + const market = await this.resolveMarketParams(params.market_id); const data = encodeFunctionData({ abi: MORPHO_ABI, - functionName: "supply", + functionName: "borrow", + args: [market, params.amount, 0n, params.on_behalf_of, params.on_behalf_of], + }); + return { + description: `[${this.protocolName}] Borrow ${params.amount} of ${params.asset} from market ${params.market_id.slice(0, 10)}…`, + to: this.morpho, + data, + value: 0n, + gas_estimate: 400_000, + }; + } + + async buildRepay(params: RepayParams): Promise { + if (!params.market_id) { + throw DefiError.invalidParam( + `[${this.protocolName}] Morpho Blue repay requires --market .`, + ); + } + const market = await this.resolveMarketParams(params.market_id); + const data = encodeFunctionData({ + abi: MORPHO_ABI, + functionName: "repay", args: [market, params.amount, 0n, params.on_behalf_of, "0x"], }); return { - description: `[${this.protocolName}] Supply ${params.amount} to Morpho market`, + description: `[${this.protocolName}] Repay ${params.amount} of ${params.asset} to market ${params.market_id.slice(0, 10)}…`, to: this.morpho, data, value: 0n, - gas_estimate: 300_000, + gas_estimate: 350_000, + approvals: [{ token: params.asset, spender: this.morpho, amount: params.amount }], }; } - async buildBorrow(params: BorrowParams): Promise { - const market = defaultMarketParams(params.asset); + async buildSupplyCollateral(params: SupplyCollateralParams): Promise { + const market = await this.resolveMarketParams(params.market_id); const data = encodeFunctionData({ abi: MORPHO_ABI, - functionName: "borrow", - args: [market, params.amount, 0n, params.on_behalf_of, params.on_behalf_of], + functionName: "supplyCollateral", + args: [market, params.amount, params.on_behalf_of, "0x"], }); return { - description: `[${this.protocolName}] Borrow ${params.amount} from Morpho market`, + description: `[${this.protocolName}] Supply collateral ${params.amount} of ${params.asset} to market ${params.market_id.slice(0, 10)}…`, to: this.morpho, data, value: 0n, gas_estimate: 350_000, + approvals: [{ token: params.asset, spender: this.morpho, amount: params.amount }], }; } - async buildRepay(params: RepayParams): Promise { - const market = defaultMarketParams(params.asset); + async buildWithdrawCollateral(params: WithdrawCollateralParams): Promise { + const market = await this.resolveMarketParams(params.market_id); const data = encodeFunctionData({ abi: MORPHO_ABI, - functionName: "repay", - args: [market, params.amount, 0n, params.on_behalf_of, "0x"], + functionName: "withdrawCollateral", + args: [market, params.amount, params.to, params.to], }); return { - description: `[${this.protocolName}] Repay ${params.amount} to Morpho market`, + description: `[${this.protocolName}] Withdraw collateral ${params.amount} of ${params.asset} from market ${params.market_id.slice(0, 10)}…`, to: this.morpho, data, value: 0n, @@ -215,6 +301,22 @@ export class MorphoBlueAdapter implements ILending { } async buildWithdraw(params: WithdrawParams): Promise { + // Direct Morpho Blue market (loan-side withdrawal) when caller pins marketId. + if (params.market_id) { + const market = await this.resolveMarketParams(params.market_id); + const data = encodeFunctionData({ + abi: MORPHO_ABI, + functionName: "withdraw", + args: [market, params.amount, 0n, params.to, params.to], + }); + return { + description: `[${this.protocolName}] Withdraw ${params.amount} of ${params.asset} from market ${params.market_id.slice(0, 10)}…`, + to: this.morpho, + data, + value: 0n, + gas_estimate: 300_000, + }; + } const vault = await this.resolveVault(params.asset); if (vault) { if (params.amount === MAX_UINT256) { @@ -243,19 +345,11 @@ export class MorphoBlueAdapter implements ILending { to: vault, data, value: 0n, gas_estimate: 400_000, }; } - const market = defaultMarketParams(params.asset); - const data = encodeFunctionData({ - abi: MORPHO_ABI, - functionName: "withdraw", - args: [market, params.amount, 0n, params.to, params.to], - }); - return { - description: `[${this.protocolName}] Withdraw ${params.amount} from Morpho market`, - to: this.morpho, - data, - value: 0n, - gas_estimate: 250_000, - }; + throw DefiError.invalidParam( + `[${this.protocolName}] withdraw requires either a registered MetaMorpho vault for ` + + `${params.asset} or an explicit --market . The legacy zero-MarketParams ` + + `stub was removed (it always reverted on-chain).`, + ); } async getRates(asset: Address): Promise {