From 1fa5ff21c9809a582b3d9393446892828788e8d9 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 07:26:32 +0000 Subject: [PATCH 01/41] feat(morpho-sdk): support Public Allocator V2 reallocations --- .changeset/brave-vaults-reallocate.md | 5 + packages/morpho-sdk/AGENTS.md | 6 +- packages/morpho-sdk/src/abis.ts | 79 ++++++++ packages/morpho-sdk/src/actions/AGENTS.md | 2 +- .../src/actions/blue/borrow.test.ts | 165 +++++++++++++++- .../morpho-sdk/src/actions/blue/borrow.ts | 4 +- .../actions/blue/buildReallocationActions.ts | 82 +++++--- .../morpho-sdk/src/actions/blue/refinance.ts | 4 +- .../actions/blue/supplyCollateralBorrow.ts | 4 +- .../morpho-sdk/src/actions/blue/withdraw.ts | 4 +- .../morpho-sdk/src/bundler/actions.test.ts | 176 +++++++++++++++++- packages/morpho-sdk/src/bundler/actions.ts | 122 +++++++++++- packages/morpho-sdk/src/bundler/types.ts | 24 +++ packages/morpho-sdk/src/entities/blue/blue.ts | 17 +- packages/morpho-sdk/src/helpers/validate.ts | 53 +++++- packages/morpho-sdk/src/types/AGENTS.md | 7 +- packages/morpho-sdk/src/types/error.ts | 30 +++ .../morpho-sdk/src/types/sharedLiquidity.ts | 42 +++++ 18 files changed, 767 insertions(+), 59 deletions(-) create mode 100644 .changeset/brave-vaults-reallocate.md diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md new file mode 100644 index 000000000..3d21230e7 --- /dev/null +++ b/.changeset/brave-vaults-reallocate.md @@ -0,0 +1,5 @@ +--- +"@morpho-org/morpho-sdk": minor +--- + +Add Blue Public Allocator V2 market and idle reallocations to Blue borrow, supply-collateral-borrow, loan-asset withdraw, and refinance flows while preserving PublicAllocator V1 inputs. diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index c90ca65c8..152556a83 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -26,7 +26,8 @@ Protocol terms used across this package's docs and JSDoc: - **VaultV2** — successor vault with adapter-based liquidity routing and `forceDeallocate`. - **bundler3** — the bundler entry point; receives a sequence of adapter actions in one transaction. - **GeneralAdapter1** — the bundler-side adapter that holds approvals/auth and executes Morpho calls on the user's behalf. Required as the spender for ERC-20 approvals on every bundled path; required as authorized operator on Morpho for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, and `withdraw` (the supplier-side path). -- **PublicAllocator** — Morpho contract that lets vault curators move liquidity between markets within a vault (`reallocateTo(...)`). +- **PublicAllocator V1** — MetaMorpho allocator that moves liquidity from one or more sorted source markets into a target via `reallocateTo(...)`; each call pays one `fee`. +- **BluePublicAllocator V2** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered; each call pays its own `nativePenalty`. - **VaultExitBundlesV1** — standalone periphery for exiting an illiquid VaultV1 or single-adapter VaultV2 into idle underlying assets and/or Morpho Blue supply positions. ### Bundler actions @@ -39,7 +40,8 @@ The action verbs an integrator sees in the bundle (`BundlerAction.encode...`): - **`erc20TransferFrom`** — pulls user-approved tokens into the bundler. - **`nativeTransfer` + `wrapNative`** — pair that converts an attached native amount (`tx.value`) into the chain's wNative for a deposit/supply path. - **`forceDeallocate`** — VaultV2 multicall entry that pulls liquidity out of a specific adapter before withdraw/redeem. -- **`reallocateTo`** — `PublicAllocator` call that shifts liquidity between markets in a curator vault before a borrow or a loan-asset withdraw. +- **`reallocateTo`** — PublicAllocator V1 call that shifts liquidity from sorted source markets into the target market. +- **`bluePublicAllocatorV2Reallocate` / `bluePublicAllocatorV2AllocateFromIdle`** — BluePublicAllocator V2 calls that move one market source or vault idle liquidity into the enclosing Blue action's target market. Both target an explicit allocator address and carry one native penalty. ### Constants and conventions diff --git a/packages/morpho-sdk/src/abis.ts b/packages/morpho-sdk/src/abis.ts index 69467b3a8..7c6d97b14 100644 --- a/packages/morpho-sdk/src/abis.ts +++ b/packages/morpho-sdk/src/abis.ts @@ -202,6 +202,85 @@ export const vaultExitBundlesV1Abi = [ }, ] as const satisfies Abi; +/** ABI for Blue Public Allocator V2 user reallocation calls. */ +export const bluePublicAllocatorV2Abi = [ + { + type: "function", + name: "reallocate", + inputs: [ + { name: "vault", type: "address", internalType: "address" }, + { + name: "deallocateAdapter", + type: "address", + internalType: "address", + }, + { + name: "deallocateMarketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { + name: "collateralToken", + type: "address", + internalType: "address", + }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "allocateAdapter", type: "address", internalType: "address" }, + { + name: "allocateMarketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { + name: "collateralToken", + type: "address", + internalType: "address", + }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "assets", type: "uint128", internalType: "uint128" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "allocateFromIdle", + inputs: [ + { name: "vault", type: "address", internalType: "address" }, + { name: "adapter", type: "address", internalType: "address" }, + { + name: "marketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { + name: "collateralToken", + type: "address", + internalType: "address", + }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "assets", type: "uint128", internalType: "uint128" }, + ], + outputs: [], + stateMutability: "payable", + }, +] as const; + /** ABI for the Bundler3 multicall contract. */ export const bundler3Abi = [ { diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index d46293bd8..ad5bc14ba 100644 --- a/packages/morpho-sdk/src/actions/AGENTS.md +++ b/packages/morpho-sdk/src/actions/AGENTS.md @@ -24,7 +24,7 @@ Only valid for assets/collateral configured as wNative. When `nativeAmount > 0`: ## Shared liquidity / reallocations (canonical statement) -`blueBorrow` and `blueSupplyCollateralBorrow` accept optional `reallocations: VaultReallocation[]`. Each reallocation becomes a `PublicAllocator.reallocateTo(vault, fee, withdrawals, targetMarket)` bundler action **before** `morphoBorrow`. `BundlerAction.encodeBundle` includes those fees in `tx.value`. Validation: `helpers/validateReallocations`. Other layer docs link here rather than restating these rules. +`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Untagged `VaultReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action. Tagged `BluePublicAllocatorV2Reallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call pays its own `nativePenalty`. V2 sources are not sorted and idle uses no synthetic zero-address market. `BundlerAction.encodeBundle` sums V1 fees and V2 penalties into `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations`. ## Discriminated unions diff --git a/packages/morpho-sdk/src/actions/blue/borrow.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.test.ts index 0013e9d00..eebc2b7a6 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.test.ts @@ -1,5 +1,5 @@ import { getChainAddresses } from "@morpho-org/blue-sdk"; -import { parseUnits } from "viem"; +import { decodeFunctionData, maxUint128, parseUnits } from "viem"; import { mainnet } from "viem/chains"; import { describe, expect } from "vitest"; import { @@ -10,8 +10,16 @@ import { SteakhouseUsdcVaultV1 } from "../../../test/fixtures/vaultV1.js"; import { test } from "../../../test/setup.js"; import { + bluePublicAllocatorV2Abi, + bundler3Abi, + generalAdapter1Abi, +} from "../../abis.js"; +import { + type BlueReallocation, + InputExceedsMaxError, NegativeInputError, NonPositiveInputError, + ReallocationWithdrawalOnTargetMarketError, type VaultReallocation, } from "../../types/index.js"; import { blueBorrow } from "./borrow.js"; @@ -80,6 +88,161 @@ describe("blueBorrow unit tests", () => { expect(tx.action.args.reallocationFee).toBe(reallocationFee); }); + test("orders mixed V1 and V2 reallocations before borrow and sums all native costs", async ({ + client, + }) => { + const allocator = "0x0000000000000000000000000000000000000011"; + const sourceAdapter = "0x0000000000000000000000000000000000000012"; + const targetAdapter = "0x0000000000000000000000000000000000000013"; + const reallocations: readonly BlueReallocation[] = [ + { + vault: SteakhouseUsdcVaultV1.address, + fee: 2n, + withdrawals: [{ marketParams: WbtcUsdcSourceMarket, amount: 1n }], + }, + { + type: "publicAllocatorV2", + allocator, + vault: SteakhouseUsdcVaultV1.address, + from: { + type: "market", + adapter: sourceAdapter, + marketParams: WbtcUsdcSourceMarket, + }, + to: { adapter: targetAdapter }, + assets: 3n, + nativePenalty: 5n, + }, + { + type: "publicAllocatorV2", + allocator, + vault: SteakhouseUsdcVaultV1.address, + from: { type: "idle" }, + to: { adapter: targetAdapter }, + assets: 7n, + nativePenalty: 11n, + }, + ]; + + const tx = blueBorrow({ + market: { chainId: mainnet.id, marketParams: WethUsdsBlue }, + args: { + amount: 1n, + minSharePrice: 0n, + receiver: client.account.address, + reallocations, + }, + }); + + expect(tx.value).toBe(18n); + expect(tx.action.args.reallocationFee).toBe(18n); + const bundle = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); + const calls = bundle.args[0] ?? []; + expect(calls).toHaveLength(4); + expect(calls.slice(0, 3).map((call) => call.value)).toEqual([2n, 5n, 11n]); + expect(calls.slice(0, 3).map((call) => call.skipRevert)).toEqual([ + false, + false, + false, + ]); + expect( + decodeFunctionData({ + abi: bluePublicAllocatorV2Abi, + data: calls[1]!.data, + }).functionName, + ).toBe("reallocate"); + const idleCall = decodeFunctionData({ + abi: bluePublicAllocatorV2Abi, + data: calls[2]!.data, + }); + expect(idleCall.functionName).toBe("allocateFromIdle"); + expect(idleCall.args[0]).toBe(SteakhouseUsdcVaultV1.address); + expect(idleCall.args[1]).toBe(targetAdapter); + expect(idleCall.args[2]).toMatchObject({ + loanToken: WethUsdsBlue.loanToken, + collateralToken: WethUsdsBlue.collateralToken, + oracle: WethUsdsBlue.oracle, + irm: WethUsdsBlue.irm, + lltv: WethUsdsBlue.lltv, + }); + expect(idleCall.args[3]).toBe(7n); + expect( + decodeFunctionData({ abi: generalAdapter1Abi, data: calls[3]!.data }) + .functionName, + ).toBe("morphoBorrow"); + }); + + test.each([ + { + name: "negative penalty", + values: { assets: 1n, nativePenalty: -1n }, + ErrorClass: NegativeInputError, + }, + { + name: "zero assets", + values: { assets: 0n, nativePenalty: 0n }, + ErrorClass: NonPositiveInputError, + }, + { + name: "uint128 overflow", + values: { assets: maxUint128 + 1n, nativePenalty: 0n }, + ErrorClass: InputExceedsMaxError, + }, + ])("rejects Public Allocator V2 $name", ({ values, ErrorClass }) => { + expect(() => + blueBorrow({ + market: { chainId: mainnet.id, marketParams: WethUsdsBlue }, + args: { + amount: 1n, + minSharePrice: 0n, + receiver: "0x0000000000000000000000000000000000000001", + reallocations: [ + { + type: "publicAllocatorV2", + allocator: "0x0000000000000000000000000000000000000011", + vault: SteakhouseUsdcVaultV1.address, + from: { type: "idle" }, + to: { + adapter: "0x0000000000000000000000000000000000000012", + }, + ...values, + }, + ], + }, + }), + ).toThrow(ErrorClass); + }); + + test("rejects a Public Allocator V2 source equal to the target market", () => { + expect(() => + blueBorrow({ + market: { chainId: mainnet.id, marketParams: WethUsdsBlue }, + args: { + amount: 1n, + minSharePrice: 0n, + receiver: "0x0000000000000000000000000000000000000001", + reallocations: [ + { + type: "publicAllocatorV2", + allocator: "0x0000000000000000000000000000000000000011", + vault: SteakhouseUsdcVaultV1.address, + from: { + type: "market", + adapter: "0x0000000000000000000000000000000000000012", + marketParams: WethUsdsBlue, + }, + to: { + adapter: "0x0000000000000000000000000000000000000013", + }, + assets: 1n, + nativePenalty: 0n, + }, + ], + }, + }), + ).toThrow(ReallocationWithdrawalOnTargetMarketError); + }); + test("should throw NonPositiveInputError when amount is zero", async ({ client, }) => { diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index 8176d6d94..87b3eeebe 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -6,11 +6,11 @@ import { addTransactionMetadata } from "../../helpers/index.js"; import { type AuthorizationRequirementSignature, type BlueBorrowAction, + type BlueReallocation, type Metadata, NegativeInputError, NonPositiveInputError, type Transaction, - type VaultReallocation, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; import { buildReallocationActions } from "./buildReallocationActions.js"; @@ -29,7 +29,7 @@ export interface BlueBorrowParams { /** Minimum borrow share price (in ray). Protects against share price manipulation. */ minSharePrice: bigint; /** Vault reallocations to execute before borrowing (computed by entity). */ - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 991a1f66a..9e97f8f4b 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -1,50 +1,86 @@ import type { MarketParams } from "@morpho-org/blue-sdk"; import type { Action } from "../../bundler/index.js"; import { validateReallocations } from "../../helpers/index.js"; -import type { VaultReallocation } from "../../types/index.js"; +import type { BlueReallocation } from "../../types/index.js"; /** - * Builds reallocation bundler actions and computes the total fee. + * Builds V1 and Blue Public Allocator V2 reallocation actions and computes their native cost. * - * Validates the reallocations, then encodes each as a `reallocateTo` action against the target - * market. Returns `{ actions: [], fee: 0n }` for an empty input — callers gate on - * `reallocations.length > 0` and skip the call entirely in that case. Internal helper — - * consumed by the Blue actions that accept reallocations; not re-exported on the public surface. + * V1 entries preserve their `reallocateTo` ABI and validation. Each V2 entry maps 1:1 to either + * `reallocate` for a market source or `allocateFromIdle` for an idle source. The enclosing Blue + * action supplies the target market parameters. * - * @param reallocations - The vault reallocations to encode. - * @param targetMarketParams - The target market params the freed liquidity is destined for. - * @returns The encoded `reallocateTo` actions and the summed reallocation fee in native tokens. - * @throws {NegativeInputError} when any reallocation fee is negative. - * @throws {EmptyReallocationWithdrawalsError} when any reallocation has no withdrawals. - * @throws {NonPositiveInputError} when any withdrawal amount is non-positive. - * @throws {ReallocationWithdrawalOnTargetMarketError} when a withdrawal references the target market. - * @throws {UnsortedReallocationWithdrawalsError} when withdrawals within a reallocation are not - * strictly sorted by market id. + * @param reallocations - V1 and V2 reallocations in execution order. + * @param targetMarketParams - Target market params derived from the enclosing Blue action. + * @returns Encoded actions and the sum of V1 fees plus V2 native penalties. + * @throws {NegativeInputError} when a V1 fee or V2 native penalty is negative. + * @throws {EmptyReallocationWithdrawalsError} when a V1 reallocation has no withdrawals. + * @throws {NonPositiveInputError} when a V1 withdrawal or V2 asset amount is non-positive. + * @throws {InputExceedsMaxError} when a V2 asset amount exceeds `uint128`. + * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. + * @throws {UnsortedReallocationWithdrawalsError} when V1 withdrawals are not strictly market-id sorted. * @internal */ export const buildReallocationActions = ( - reallocations: readonly VaultReallocation[], + reallocations: readonly BlueReallocation[], targetMarketParams: MarketParams, ): { readonly actions: Action[]; readonly fee: bigint } => { validateReallocations(reallocations, targetMarketParams.id); - const fee = reallocations.reduce((sum, r) => sum + r.fee, 0n); + let fee = 0n; const actions: Action[] = []; - for (const r of reallocations) { + for (const reallocation of reallocations) { + if ("type" in reallocation && reallocation.type === "publicAllocatorV2") { + if (reallocation.from.type === "market") { + actions.push({ + type: "bluePublicAllocatorV2Reallocate", + args: [ + reallocation.allocator, + reallocation.vault, + reallocation.from.adapter, + reallocation.from.marketParams, + reallocation.to.adapter, + targetMarketParams, + reallocation.assets, + reallocation.nativePenalty, + false, + ], + }); + } else { + actions.push({ + type: "bluePublicAllocatorV2AllocateFromIdle", + args: [ + reallocation.allocator, + reallocation.vault, + reallocation.to.adapter, + targetMarketParams, + reallocation.assets, + reallocation.nativePenalty, + false, + ], + }); + } + fee += reallocation.nativePenalty; + continue; + } + + if (!("withdrawals" in reallocation)) continue; + actions.push({ type: "reallocateTo", args: [ - r.vault, - r.fee, - r.withdrawals.map((w) => ({ - marketParams: w.marketParams, - amount: w.amount, + reallocation.vault, + reallocation.fee, + reallocation.withdrawals.map((withdrawal) => ({ + marketParams: withdrawal.marketParams, + amount: withdrawal.amount, })), targetMarketParams, false, ], }); + fee += reallocation.fee; } return { actions, fee }; diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index 287f79555..267a6ae6e 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -5,6 +5,7 @@ import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; import { type AuthorizationRequirementSignature, + type BlueReallocation, type BlueRefinanceAction, type Metadata, NegativeInputError, @@ -13,7 +14,6 @@ import { RefinanceSharesMissingBorrowAssetsError, RefinanceTokenMismatchError, type Transaction, - type VaultReallocation, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; import { buildReallocationActions } from "./buildReallocationActions.js"; @@ -45,7 +45,7 @@ export interface BlueRefinanceParams { /** Maximum repay share price on the source market (in ray); must be > 0 when a repay leg exists. */ maxRepaySharePrice: bigint; /** PublicAllocator reallocations into the target market, run before the bundle. Fees add to `tx.value`. */ - targetReallocations?: readonly VaultReallocation[]; + targetReallocations?: readonly BlueReallocation[]; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 276a5e304..0cdc0b9e7 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -5,6 +5,7 @@ import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; import { type AuthorizationRequirementSignature, + type BlueReallocation, type BlueSupplyCollateralBorrowAction, type DepositAmountArgs, type Metadata, @@ -12,7 +13,6 @@ import { NonPositiveInputError, type PermitRequirementSignature, type Transaction, - type VaultReallocation, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; import { buildAssetFundingActions } from "./buildAssetFundingActions.js"; @@ -36,7 +36,7 @@ export interface BlueSupplyCollateralBorrowParams { /** Optional pre-signed permit/permit2 approval for the collateral transfer. */ requirementSignature?: PermitRequirementSignature; /** Vault reallocations to execute before borrowing (computed by entity). */ - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index 5245da888..79b06f400 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -5,13 +5,13 @@ import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; import { type AuthorizationRequirementSignature, + type BlueReallocation, type BlueWithdrawAction, type Metadata, MutuallyExclusiveWithdrawAmountsError, NegativeInputError, NonPositiveInputError, type Transaction, - type VaultReallocation, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; import { buildReallocationActions } from "./buildReallocationActions.js"; @@ -36,7 +36,7 @@ export interface BlueWithdrawParams { * `MorphoBlue.getReallocations({ operation: "withdraw", amount })` or directly via * `computeReallocations({ operation: "withdraw", amount, ... })`. */ - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a diff --git a/packages/morpho-sdk/src/bundler/actions.test.ts b/packages/morpho-sdk/src/bundler/actions.test.ts index ad10d7f0c..606251094 100644 --- a/packages/morpho-sdk/src/bundler/actions.test.ts +++ b/packages/morpho-sdk/src/bundler/actions.test.ts @@ -22,7 +22,12 @@ import { zeroHash, } from "viem"; import { describe, expect, test } from "vitest"; -import { bundler3Abi, coreAdapterAbi, generalAdapter1Abi } from "../abis.js"; +import { + bluePublicAllocatorV2Abi, + bundler3Abi, + coreAdapterAbi, + generalAdapter1Abi, +} from "../abis.js"; import { BundlerErrors } from "../types/index.js"; import { type Action, @@ -46,6 +51,9 @@ describe("BundlerAction", () => { const adapter = "0x0000000000000000000000000000000000000004"; const erc4626 = "0x0000000000000000000000000000000000000005"; const vault = "0x0000000000000000000000000000000000000006"; + const allocator = "0x0000000000000000000000000000000000000011"; + const deallocateAdapter = "0x0000000000000000000000000000000000000012"; + const allocateAdapter = "0x0000000000000000000000000000000000000013"; const loanToken = "0x0000000000000000000000000000000000000007"; const collateralToken = "0x0000000000000000000000000000000000000008"; const oracle = "0x0000000000000000000000000000000000000009"; @@ -342,6 +350,42 @@ describe("BundlerAction", () => { args, }) satisfies Action, ), + fc + .tuple( + addressArbitrary, + addressArbitrary, + addressArbitrary, + marketArbitrary, + addressArbitrary, + marketArbitrary, + amountArbitrary, + amountArbitrary, + skipRevertArbitrary, + ) + .map( + (args) => + ({ + type: "bluePublicAllocatorV2Reallocate", + args, + }) satisfies Action, + ), + fc + .tuple( + addressArbitrary, + addressArbitrary, + addressArbitrary, + marketArbitrary, + amountArbitrary, + amountArbitrary, + skipRevertArbitrary, + ) + .map( + (args) => + ({ + type: "bluePublicAllocatorV2AllocateFromIdle", + args, + }) satisfies Action, + ), fc.tuple(amountArbitrary, addressArbitrary, skipRevertArbitrary).map( (args) => ({ @@ -570,6 +614,35 @@ describe("BundlerAction", () => { expect(calls[0]?.value).toBe(5n); }); + test("encodeBundle aggregates Blue Public Allocator V2 native penalties", () => { + const tx = BundlerAction.encodeBundle(chainId, [ + { + type: "bluePublicAllocatorV2Reallocate", + args: [ + allocator, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 1n, + 2n, + false, + ], + }, + { + type: "bluePublicAllocatorV2AllocateFromIdle", + args: [allocator, vault, allocateAdapter, market, 3n, 4n, false], + }, + ]); + + expect(tx.value).toBe(6n); + + const decoded = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); + expect(decoded.functionName).toBe("multicall"); + expect((decoded.args[0] ?? []).map((call) => call.value)).toEqual([2n, 4n]); + }); + test("encodeBundle includes callback action values in transaction value", () => { const tx = BundlerAction.encodeBundle(chainId, [ { @@ -972,6 +1045,50 @@ describe("BundlerAction", () => { false, ), ], + [ + "bluePublicAllocatorV2Reallocate", + { + type: "bluePublicAllocatorV2Reallocate", + args: [ + allocator, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 20n, + 21n, + false, + ], + }, + BundlerAction.bluePublicAllocatorV2Reallocate( + allocator, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 20n, + 21n, + false, + ), + ], + [ + "bluePublicAllocatorV2AllocateFromIdle", + { + type: "bluePublicAllocatorV2AllocateFromIdle", + args: [allocator, vault, allocateAdapter, market, 22n, 23n, false], + }, + BundlerAction.bluePublicAllocatorV2AllocateFromIdle( + allocator, + vault, + allocateAdapter, + market, + 22n, + 23n, + false, + ), + ], [ "wrapNative", { @@ -1445,6 +1562,63 @@ describe("BundlerAction", () => { expect(decoded.args).toEqual([vault, withdrawals, market]); }); + test("bluePublicAllocatorV2Reallocate", () => { + const call = onlyCall( + BundlerAction.bluePublicAllocatorV2Reallocate( + allocator, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 1n, + 2n, + true, + ), + ); + const decoded = decodeFunctionData({ + abi: bluePublicAllocatorV2Abi, + data: call.data, + }); + + expect(call.to).toBe(allocator); + expect(call.value).toBe(2n); + expect(call.skipRevert).toBe(true); + expect(decoded.functionName).toBe("reallocate"); + expect(decoded.args).toEqual([ + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 1n, + ]); + }); + + test("bluePublicAllocatorV2AllocateFromIdle", () => { + const call = onlyCall( + BundlerAction.bluePublicAllocatorV2AllocateFromIdle( + allocator, + vault, + allocateAdapter, + market, + 1n, + 2n, + true, + ), + ); + const decoded = decodeFunctionData({ + abi: bluePublicAllocatorV2Abi, + data: call.data, + }); + + expect(call.to).toBe(allocator); + expect(call.value).toBe(2n); + expect(call.skipRevert).toBe(true); + expect(decoded.functionName).toBe("allocateFromIdle"); + expect(decoded.args).toEqual([vault, allocateAdapter, market, 1n]); + }); + test("wrapNative", () => { const call = onlyCall( BundlerAction.wrapNative(chainId, 1n, recipient, true), diff --git a/packages/morpho-sdk/src/bundler/actions.ts b/packages/morpho-sdk/src/bundler/actions.ts index 6d27997ac..c1d0c5429 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -20,7 +20,12 @@ import { serializeSignature, zeroHash, } from "viem"; -import { bundler3Abi, coreAdapterAbi, generalAdapter1Abi } from "../abis.js"; +import { + bluePublicAllocatorV2Abi, + bundler3Abi, + coreAdapterAbi, + generalAdapter1Abi, +} from "../abis.js"; import { BundlerErrors } from "../types/error.js"; import type { Action, @@ -342,6 +347,12 @@ export namespace BundlerAction { case "reallocateTo": { return BundlerAction.publicAllocatorReallocateTo(chainId, ...args); } + case "bluePublicAllocatorV2Reallocate": { + return BundlerAction.bluePublicAllocatorV2Reallocate(...args); + } + case "bluePublicAllocatorV2AllocateFromIdle": { + return BundlerAction.bluePublicAllocatorV2AllocateFromIdle(...args); + } case "wrapNative": { return BundlerAction.wrapNative(chainId, ...args); } @@ -1441,6 +1452,115 @@ export namespace BundlerAction { ]; } + /** + * Encodes a Blue Public Allocator V2 market-to-market reallocation. + * + * @param allocator - Explicit Blue Public Allocator V2 contract address. + * @param vault - Vault whose liquidity is reallocated. + * @param deallocateAdapter - Vault V2 adapter supplying the source market. + * @param deallocateMarket - Source Morpho Blue market parameters. + * @param allocateAdapter - Vault V2 adapter supplying the target market. + * @param allocateMarket - Target Morpho Blue market parameters. + * @param assets - Assets to reallocate, bounded by `uint128` by the high-level action. + * @param nativePenalty - Native penalty paid to the allocator. + * @param skipRevert - Whether Bundler3 should tolerate a revert. + * @returns One encoded call targeting the explicit allocator. + * @example + * ```ts + * const calls = BundlerAction.bluePublicAllocatorV2Reallocate( + * allocator, + * vault, + * sourceAdapter, + * sourceMarket, + * targetAdapter, + * targetMarket, + * 1_000_000n, + * 10n, + * ); + * ``` + */ + // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call + export function bluePublicAllocatorV2Reallocate( + allocator: Address, + vault: Address, + deallocateAdapter: Address, + deallocateMarket: InputMarketParams, + allocateAdapter: Address, + allocateMarket: InputMarketParams, + assets: bigint, + nativePenalty: bigint, + skipRevert = false, + ): BundlerCall[] { + return [ + { + to: allocator, + data: encodeFunctionData({ + abi: bluePublicAllocatorV2Abi, + functionName: "reallocate", + args: [ + vault, + deallocateAdapter, + deallocateMarket, + allocateAdapter, + allocateMarket, + assets, + ], + }), + value: nativePenalty, + skipRevert, + callbackHash: zeroHash, + }, + ]; + } + + /** + * Encodes a Blue Public Allocator V2 allocation from vault idle liquidity. + * + * @param allocator - Explicit Blue Public Allocator V2 contract address. + * @param vault - Vault whose idle liquidity is allocated. + * @param adapter - Vault V2 adapter supplying the target market. + * @param market - Target Morpho Blue market parameters. + * @param assets - Assets to allocate, bounded by `uint128` by the high-level action. + * @param nativePenalty - Native penalty paid to the allocator. + * @param skipRevert - Whether Bundler3 should tolerate a revert. + * @returns One encoded call targeting the explicit allocator. + * @example + * ```ts + * const calls = BundlerAction.bluePublicAllocatorV2AllocateFromIdle( + * allocator, + * vault, + * targetAdapter, + * targetMarket, + * 1_000_000n, + * 10n, + * ); + * ``` + */ + // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call + export function bluePublicAllocatorV2AllocateFromIdle( + allocator: Address, + vault: Address, + adapter: Address, + market: InputMarketParams, + assets: bigint, + nativePenalty: bigint, + skipRevert = false, + ): BundlerCall[] { + return [ + { + to: allocator, + data: encodeFunctionData({ + abi: bluePublicAllocatorV2Abi, + functionName: "allocateFromIdle", + args: [vault, adapter, market, assets], + }), + value: nativePenalty, + skipRevert, + callbackHash: zeroHash, + }, + ]; + } + /** * Encodes a GeneralAdapter1 native-token wrap call. * diff --git a/packages/morpho-sdk/src/bundler/types.ts b/packages/morpho-sdk/src/bundler/types.ts index f07ea6718..3b6c965d3 100644 --- a/packages/morpho-sdk/src/bundler/types.ts +++ b/packages/morpho-sdk/src/bundler/types.ts @@ -210,6 +210,30 @@ export interface ActionArgs { skipRevert?: boolean, ]; + /** Blue Public Allocator V2 market-to-market reallocation with an explicit allocator address and native penalty. */ + readonly bluePublicAllocatorV2Reallocate: [ + allocator: Address, + vault: Address, + deallocateAdapter: Address, + deallocateMarket: InputMarketParams, + allocateAdapter: Address, + allocateMarket: InputMarketParams, + assets: bigint, + nativePenalty: bigint, + skipRevert?: boolean, + ]; + + /** Blue Public Allocator V2 idle-to-market allocation with an explicit allocator address and native penalty. */ + readonly bluePublicAllocatorV2AllocateFromIdle: [ + allocator: Address, + vault: Address, + adapter: Address, + market: InputMarketParams, + assets: bigint, + nativePenalty: bigint, + skipRevert?: boolean, + ]; + /** GeneralAdapter1 native wrap of `amount` to `recipient`; `skipRevert` controls Bundler3 revert handling. */ readonly wrapNative: [ amount: bigint, diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 5e17fa2e7..f82ac9f45 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -54,6 +54,7 @@ import { type AssetsOrSharesArgs, type BlueAuthorizationAction, type BlueBorrowAction, + type BlueReallocation, type BlueRefinanceAction, type BlueRepayAction, type BlueRepayWithdrawCollateralAction, @@ -206,7 +207,7 @@ export interface BlueActions { receiver?: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; } & AssetsOrSharesArgs, ) => { buildTx: ( @@ -240,7 +241,7 @@ export interface BlueActions { amount: bigint; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; }) => { buildTx: ( signatures?: readonly RequirementSignature[], @@ -383,7 +384,7 @@ export interface BlueActions { positionData: AccrualPosition; borrowAmount: bigint; slippageTolerance?: bigint; - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; } & DepositAmountArgs, ) => { buildTx: ( @@ -439,7 +440,7 @@ export interface BlueActions { borrowAssets?: bigint; borrowShares?: bigint; slippageTolerance?: bigint; - targetReallocations?: readonly VaultReallocation[]; + targetReallocations?: readonly BlueReallocation[]; }) => { buildTx: ( signatures?: readonly RequirementSignature[], @@ -642,7 +643,7 @@ export class MorphoBlue implements BlueActions { receiver?: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; } & AssetsOrSharesArgs, ) { validateChainId(this.client.viemClient.chain?.id, this.chainId); @@ -812,7 +813,7 @@ export class MorphoBlue implements BlueActions { userAddress: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; }) { validateChainId(this.client.viemClient.chain?.id, this.chainId); @@ -1306,7 +1307,7 @@ export class MorphoBlue implements BlueActions { positionData: AccrualPosition; borrowAmount: bigint; slippageTolerance?: bigint; - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; } & DepositAmountArgs) { validateChainId(this.client.viemClient.chain?.id, this.chainId); @@ -1429,7 +1430,7 @@ export class MorphoBlue implements BlueActions { borrowAssets?: bigint; borrowShares?: bigint; slippageTolerance?: bigint; - targetReallocations?: readonly VaultReallocation[]; + targetReallocations?: readonly BlueReallocation[]; }) { validateChainId(this.client.viemClient.chain?.id, this.chainId); validateSlippageTolerance(slippageTolerance); diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index 4d024de11..02930baa2 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -7,15 +7,17 @@ import { } from "@morpho-org/blue-sdk"; import type { MarketInput as MidnightMarketInput } from "@morpho-org/midnight-sdk"; import { isDefined } from "@morpho-org/morpho-ts"; -import { type Address, isAddressEqual } from "viem"; +import { type Address, isAddressEqual, maxUint128 } from "viem"; import { AccrualPositionUserMismatchError, AddressMismatchError, + type BlueReallocation, BorrowExceedsSafeLtvError, ChainIdMismatchError, ChainWNativeMissingError, EmptyReallocationWithdrawalsError, ExcessiveSlippageToleranceError, + InputExceedsMaxError, MarketIdMismatchError, MissingClientPropertyError, MissingMarketPriceError, @@ -26,7 +28,6 @@ import { RepayExceedsDebtError, RepaySharesExceedDebtError, UnsortedReallocationWithdrawalsError, - type VaultReallocation, WithdrawExceedsCollateralError, WithdrawExceedsSupplyError, WithdrawMakesPositionUnhealthyError, @@ -323,23 +324,26 @@ export const validateRepayShares = (params: { }; /** - * Validates that vault reallocations are well-formed. + * Validates that Public Allocator V1 and Blue Public Allocator V2 reallocations are well-formed. * - * Enforces the following invariants for each {@link VaultReallocation}: + * V1 entries preserve the following invariants: * - `fee` must be non-negative. * - `withdrawals` must be non-empty. * - Every withdrawal `amount` must be strictly positive. - * - No withdrawal may target `targetMarketId` (the operation's target market — the market being - * borrowed from for `borrow`, or being withdrawn from for `withdraw`). - * - Withdrawal market IDs must be strictly ascending (required by `PublicAllocator.reallocateTo`). + * - No withdrawal may target `targetMarketId`. + * - Withdrawal market IDs must be strictly ascending. + * + * V2 entries enforce non-negative `nativePenalty`, positive `uint128`-bounded `assets`, and a + * market source distinct from `targetMarketId`. Idle sources have no market or sorting rule. * * @param reallocations - The reallocations to validate. * @param targetMarketId - The ID of the operation's target market. No withdrawal may reference this market. * @returns Nothing when every reallocation is valid. * @throws {NegativeInputError} when a reallocation fee is negative. * @throws {EmptyReallocationWithdrawalsError} when a reallocation has no withdrawals. - * @throws {NonPositiveInputError} when a withdrawal amount is non-positive. - * @throws {ReallocationWithdrawalOnTargetMarketError} when a withdrawal references the target market. + * @throws {NonPositiveInputError} when a withdrawal or V2 asset amount is non-positive. + * @throws {InputExceedsMaxError} when a V2 asset amount exceeds `uint128`. + * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when withdrawals are not strictly market-id sorted. * @example * ```ts @@ -351,10 +355,39 @@ export const validateRepayShares = (params: { * ``` */ export const validateReallocations = ( - reallocations: readonly VaultReallocation[], + reallocations: readonly BlueReallocation[], targetMarketId: MarketId, ): void => { for (const r of reallocations) { + if ("type" in r && r.type === "publicAllocatorV2") { + if (r.nativePenalty < 0n) { + throw new NegativeInputError( + "reallocation.nativePenalty", + r.nativePenalty, + ); + } + if (r.assets <= 0n) { + throw new NonPositiveInputError("reallocation.assets", r.assets); + } + if (r.assets > maxUint128) { + throw new InputExceedsMaxError({ + field: "reallocation.assets", + value: r.assets, + max: maxUint128, + }); + } + if ( + r.from.type === "market" && + r.from.marketParams.id === targetMarketId + ) { + throw new ReallocationWithdrawalOnTargetMarketError( + r.vault, + r.from.marketParams.id, + ); + } + continue; + } + if (!("withdrawals" in r)) continue; if (r.fee < 0n) { throw new NegativeInputError("reallocation.fee", r.fee); } diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index b8c41ec5b..50042b023 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -18,10 +18,9 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. ## Shared liquidity (`sharedLiquidity.ts`) -- `ReallocationWithdrawal` — source market + amount. -- `VaultReallocation` — vault address + fee + withdrawals. - -Both map directly to `PublicAllocator.reallocateTo()` arguments. +- `VaultReallocation` — untagged PublicAllocator V1 vault address + fee + sorted withdrawals; maps to `reallocateTo()`. +- `BluePublicAllocatorV2Reallocation` — tagged V2 allocator/vault/source/target-adapter/assets/native-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. +- `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. ## Errors (`error.ts`) diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index e5aa55870..ca3aa82b9 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -284,6 +284,36 @@ export class VaultExitBundlesV1PermitMismatchError extends Error { } } +/** Thrown when an integer input exceeds its protocol-defined maximum. */ +export class InputExceedsMaxError extends Error { + /** + * @param params - Maximum-bound validation details. + * @param params.field - Public input field whose value is invalid. + * @param params.value - Supplied value. + * @param params.max - Largest accepted value. + */ + public constructor(params: { + readonly field: string; + readonly value: bigint; + readonly max: bigint; + }) { + super( + `Input "${params.field}" must be at most "${params.max}", got "${params.value}".`, + ); + this.field = params.field; + this.value = params.value; + this.max = params.max; + this.name = "InputExceedsMaxError"; + } + + /** Public input field whose value is invalid. */ + public readonly field: string; + /** Supplied value. */ + public readonly value: bigint; + /** Largest accepted value. */ + public readonly max: bigint; +} + /** @deprecated Use {@link NonPositiveInputError}. */ export const NonPositiveAssetAmountError = NonPositiveInputError; /** @deprecated Use {@link NonPositiveInputError}. */ diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 77d6499fe..63325fd71 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -81,6 +81,48 @@ export interface VaultReallocation { readonly withdrawals: readonly ReallocationWithdrawal[]; } +/** Source of a Blue Public Allocator V2 reallocation. */ +export type BluePublicAllocatorV2Source = + | { + /** Reallocate from a Morpho Blue market. */ + readonly type: "market"; + /** Vault V2 adapter supplying the source market. */ + readonly adapter: Address; + /** Source market parameters. */ + readonly marketParams: MarketParams; + } + | { + /** Allocate from vault idle liquidity without a synthetic market. */ + readonly type: "idle"; + }; + +/** + * One Blue Public Allocator V2 contract call performed before a Blue action. + * + * The target market parameters are derived from the enclosing Blue action. + */ +export interface BluePublicAllocatorV2Reallocation { + /** Explicit allocator contract address because V2 has no deployment registry entry. */ + readonly allocator: Address; + /** Discriminator separating V2 reallocations from untagged V1 reallocations. */ + readonly type: "publicAllocatorV2"; + /** Vault whose liquidity is moved. */ + readonly vault: Address; + /** Liquidity source. */ + readonly from: BluePublicAllocatorV2Source; + /** Target Vault V2 adapter; the target market comes from the enclosing action. */ + readonly to: { readonly adapter: Address }; + /** Asset amount, which must fit in `uint128`. */ + readonly assets: bigint; + /** Native penalty paid for this individual allocator call. */ + readonly nativePenalty: bigint; +} + +/** Additive union accepted by Blue actions that support V1 or V2 reallocations. */ +export type BlueReallocation = + | VaultReallocation + | BluePublicAllocatorV2Reallocation; + /** * Options for computing vault reallocations via the public allocator. * From 77d49acada4c10a45aba9da1e8dd25a60784e133 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 07:34:26 +0000 Subject: [PATCH 02/41] docs(morpho-sdk): document allocator V2 overflow errors --- packages/morpho-sdk/src/actions/blue/borrow.ts | 1 + packages/morpho-sdk/src/actions/blue/refinance.ts | 1 + .../morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts | 1 + packages/morpho-sdk/src/actions/blue/withdraw.ts | 1 + packages/morpho-sdk/src/entities/blue/blue.ts | 4 ++++ 5 files changed, 8 insertions(+) diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index 87b3eeebe..3944dec48 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -65,6 +65,7 @@ export interface BlueBorrowParams { * typed `action` discriminator the simulation layer consumes. * @throws {NonPositiveInputError} when `amount <= 0n` or any reallocation withdrawal amount * is non-positive. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {NegativeInputError} when `minSharePrice < 0n` or any reallocation fee is negative. * @throws {EmptyReallocationWithdrawalsError} from `buildReallocationActions` when any * `reallocation.withdrawals` is empty. diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index 267a6ae6e..a1cfe3b2c 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -111,6 +111,7 @@ export interface BlueRefinanceParams { * repay); in shares mode the entity passes both. Caller-facing mutual exclusivity is enforced at the entity layer. * @throws {NonPositiveInputError} when `collateralAmount <= 0n`, a repay leg has a non-positive * `maxRepaySharePrice`, or any reallocation withdrawal amount is non-positive. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {NegativeInputError} when `borrowAssets`, `borrowShares`, `minBorrowSharePrice`, * `maxRepaySharePrice`, or any reallocation fee is negative. * @throws {RefinanceSameMarketError} when source and target market ids are equal. diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 0cdc0b9e7..02661dcdb 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -83,6 +83,7 @@ export interface BlueSupplyCollateralBorrowParams { * fee is negative. * @throws {NonPositiveInputError} when `borrowAmount <= 0n`, both collateral amounts resolve to * zero, or any reallocation withdrawal amount is non-positive. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {ChainWNativeMissingError} when `nativeAmount > 0n` but the chain has no configured wNative. * @throws {NativeAmountOnNonWNativeAssetError} when `nativeAmount > 0n` but the collateral * token is not the chain's wNative. diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index 79b06f400..2e4b34147 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -84,6 +84,7 @@ export interface BlueWithdrawParams { * is negative. * @throws {NonPositiveInputError} when both `assets` and `shares` are zero or any reallocation * withdrawal amount is non-positive. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {MutuallyExclusiveWithdrawAmountsError} when both `assets` and `shares` are non-zero. * @throws {EmptyReallocationWithdrawalsError} when any reallocation has no withdrawals. * @throws {ReallocationWithdrawalOnTargetMarketError} when a reallocation withdrawal references diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index f82ac9f45..9cf96ae89 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -200,6 +200,7 @@ export interface BlueActions { * * @param params - Withdraw parameters including pre-fetched `positionData`. * @returns Object with `buildTx` and `getRequirements`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. */ withdraw: ( params: { @@ -235,6 +236,7 @@ export interface BlueActions { * * @param params - Borrow parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. */ borrow: (params: { userAddress: Address; @@ -377,6 +379,7 @@ export interface BlueActions { * * @param params - Combined parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. */ supplyCollateralBorrow: ( params: { @@ -428,6 +431,7 @@ export interface BlueActions { * @param params.slippageTolerance - WAD slippage tolerance. Defaults to `DEFAULT_SLIPPAGE_TOLERANCE`. * @param params.targetReallocations - PublicAllocator reallocations into the target market. * @returns Object with `buildTx` and `getRequirements`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. */ refinance: (params: { userAddress: Address; From 77714ea7ff8391dcdf7c8df773f7726991bc87ac Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 07:42:52 +0000 Subject: [PATCH 03/41] fix(morpho-sdk): validate allocator V2 source types --- .../src/actions/blue/borrow.test.ts | 27 +++++++++++++++++++ .../morpho-sdk/src/actions/blue/borrow.ts | 1 + .../actions/blue/buildReallocationActions.ts | 1 + .../morpho-sdk/src/actions/blue/refinance.ts | 1 + .../actions/blue/supplyCollateralBorrow.ts | 1 + .../morpho-sdk/src/actions/blue/withdraw.ts | 1 + packages/morpho-sdk/src/entities/blue/blue.ts | 4 +++ packages/morpho-sdk/src/helpers/validate.ts | 6 +++++ packages/morpho-sdk/src/types/error.ts | 22 +++++++++++++++ 9 files changed, 64 insertions(+) diff --git a/packages/morpho-sdk/src/actions/blue/borrow.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.test.ts index eebc2b7a6..18d5cd372 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.test.ts @@ -17,6 +17,7 @@ import { import { type BlueReallocation, InputExceedsMaxError, + InvalidReallocationSourceTypeError, NegativeInputError, NonPositiveInputError, ReallocationWithdrawalOnTargetMarketError, @@ -243,6 +244,32 @@ describe("blueBorrow unit tests", () => { ).toThrow(ReallocationWithdrawalOnTargetMarketError); }); + test("rejects an unknown Public Allocator V2 source discriminator", () => { + expect(() => + blueBorrow({ + market: { chainId: mainnet.id, marketParams: WethUsdsBlue }, + args: { + amount: 1n, + minSharePrice: 0n, + receiver: "0x0000000000000000000000000000000000000001", + reallocations: [ + { + type: "publicAllocatorV2", + allocator: "0x0000000000000000000000000000000000000011", + vault: SteakhouseUsdcVaultV1.address, + from: { type: "marketTypo" }, + to: { + adapter: "0x0000000000000000000000000000000000000013", + }, + assets: 1n, + nativePenalty: 0n, + } as unknown as BlueReallocation, + ], + }, + }), + ).toThrow(InvalidReallocationSourceTypeError); + }); + test("should throw NonPositiveInputError when amount is zero", async ({ client, }) => { diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index 3944dec48..ffceb9674 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -66,6 +66,7 @@ export interface BlueBorrowParams { * @throws {NonPositiveInputError} when `amount <= 0n` or any reallocation withdrawal amount * is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {NegativeInputError} when `minSharePrice < 0n` or any reallocation fee is negative. * @throws {EmptyReallocationWithdrawalsError} from `buildReallocationActions` when any * `reallocation.withdrawals` is empty. diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 9e97f8f4b..4e5de3a08 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -17,6 +17,7 @@ import type { BlueReallocation } from "../../types/index.js"; * @throws {EmptyReallocationWithdrawalsError} when a V1 reallocation has no withdrawals. * @throws {NonPositiveInputError} when a V1 withdrawal or V2 asset amount is non-positive. * @throws {InputExceedsMaxError} when a V2 asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when V1 withdrawals are not strictly market-id sorted. * @internal diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index a1cfe3b2c..6315ed624 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -112,6 +112,7 @@ export interface BlueRefinanceParams { * @throws {NonPositiveInputError} when `collateralAmount <= 0n`, a repay leg has a non-positive * `maxRepaySharePrice`, or any reallocation withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {NegativeInputError} when `borrowAssets`, `borrowShares`, `minBorrowSharePrice`, * `maxRepaySharePrice`, or any reallocation fee is negative. * @throws {RefinanceSameMarketError} when source and target market ids are equal. diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 02661dcdb..57d4fd99c 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -84,6 +84,7 @@ export interface BlueSupplyCollateralBorrowParams { * @throws {NonPositiveInputError} when `borrowAmount <= 0n`, both collateral amounts resolve to * zero, or any reallocation withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {ChainWNativeMissingError} when `nativeAmount > 0n` but the chain has no configured wNative. * @throws {NativeAmountOnNonWNativeAssetError} when `nativeAmount > 0n` but the collateral * token is not the chain's wNative. diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index 2e4b34147..5452af32c 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -85,6 +85,7 @@ export interface BlueWithdrawParams { * @throws {NonPositiveInputError} when both `assets` and `shares` are zero or any reallocation * withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {MutuallyExclusiveWithdrawAmountsError} when both `assets` and `shares` are non-zero. * @throws {EmptyReallocationWithdrawalsError} when any reallocation has no withdrawals. * @throws {ReallocationWithdrawalOnTargetMarketError} when a reallocation withdrawal references diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 9cf96ae89..30d2031e5 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -201,6 +201,7 @@ export interface BlueActions { * @param params - Withdraw parameters including pre-fetched `positionData`. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. */ withdraw: ( params: { @@ -237,6 +238,7 @@ export interface BlueActions { * @param params - Borrow parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. */ borrow: (params: { userAddress: Address; @@ -380,6 +382,7 @@ export interface BlueActions { * @param params - Combined parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. */ supplyCollateralBorrow: ( params: { @@ -432,6 +435,7 @@ export interface BlueActions { * @param params.targetReallocations - PublicAllocator reallocations into the target market. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. */ refinance: (params: { userAddress: Address; diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index 02930baa2..c2282ab6c 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -18,6 +18,7 @@ import { EmptyReallocationWithdrawalsError, ExcessiveSlippageToleranceError, InputExceedsMaxError, + InvalidReallocationSourceTypeError, MarketIdMismatchError, MissingClientPropertyError, MissingMarketPriceError, @@ -343,6 +344,7 @@ export const validateRepayShares = (params: { * @throws {EmptyReallocationWithdrawalsError} when a reallocation has no withdrawals. * @throws {NonPositiveInputError} when a withdrawal or V2 asset amount is non-positive. * @throws {InputExceedsMaxError} when a V2 asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when withdrawals are not strictly market-id sorted. * @example @@ -360,6 +362,10 @@ export const validateReallocations = ( ): void => { for (const r of reallocations) { if ("type" in r && r.type === "publicAllocatorV2") { + const sourceType: string = r.from.type; + if (sourceType !== "market" && sourceType !== "idle") { + throw new InvalidReallocationSourceTypeError(sourceType); + } if (r.nativePenalty < 0n) { throw new NegativeInputError( "reallocation.nativePenalty", diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index ca3aa82b9..94e58baa9 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -798,6 +798,28 @@ export class ReallocationWithdrawalOnTargetMarketError extends Error { } } +/** + * Thrown when a Blue Public Allocator V2 source has an unknown discriminator. + * + * @example + * ```ts + * import { InvalidReallocationSourceTypeError } from "@morpho-org/morpho-sdk"; + * + * const error = new InvalidReallocationSourceTypeError("marketTypo"); + * ``` + */ +export class InvalidReallocationSourceTypeError extends Error { + /** + * @param sourceType - Invalid runtime value received for `reallocation.from.type`. + */ + public constructor(public readonly sourceType: string) { + super( + `Reallocation source type must be "market" or "idle", got "${sourceType}".`, + ); + this.name = "InvalidReallocationSourceTypeError"; + } +} + /** Thrown when reallocation withdrawals within a vault are not strictly sorted by market id. */ export class UnsortedReallocationWithdrawalsError extends Error { constructor(vault: string, marketId: string) { From bb5ce3ee2de46d8d0a7d2cdc583ca4eb0994f376 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Thu, 6 Aug 2026 11:46:17 +0200 Subject: [PATCH 04/41] fix: harden Public Allocator V2 support --- .changeset/brave-vaults-reallocate.md | 4 +- packages/blue-sdk-viem/package.json | 2 +- packages/blue-sdk-viem/src/abis.ts | 1 + packages/morpho-sdk/BUNDLER3.md | 22 +- packages/morpho-sdk/README.md | 11 +- packages/morpho-sdk/src/abis.ts | 80 +------- packages/morpho-sdk/src/actions/AGENTS.md | 2 +- .../morpho-sdk/src/actions/blue/AGENTS.md | 10 +- .../blue/borrow.publicAllocatorV2.test.ts | 122 +++++++++++ .../src/actions/blue/borrow.test.ts | 192 +----------------- .../morpho-sdk/src/actions/blue/borrow.ts | 15 +- .../actions/blue/buildReallocationActions.ts | 5 +- .../morpho-sdk/src/actions/blue/refinance.ts | 12 +- .../actions/blue/supplyCollateralBorrow.ts | 19 +- .../morpho-sdk/src/actions/blue/withdraw.ts | 23 ++- .../morpho-sdk/src/bundler/actions.test.ts | 8 +- packages/morpho-sdk/src/bundler/actions.ts | 41 +++- packages/morpho-sdk/src/entities/blue/blue.ts | 24 ++- .../morpho-sdk/src/helpers/validate.test.ts | 104 +++++++++- packages/morpho-sdk/src/helpers/validate.ts | 15 +- packages/morpho-sdk/src/types/AGENTS.md | 6 +- packages/morpho-sdk/src/types/error.ts | 25 +++ .../morpho-sdk/src/types/sharedLiquidity.ts | 9 +- packages/morpho-ts/src/abis.ts | 157 +++++++++----- 24 files changed, 504 insertions(+), 405 deletions(-) create mode 100644 packages/morpho-sdk/src/actions/blue/borrow.publicAllocatorV2.test.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 3d21230e7..283aa25ba 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -1,5 +1,7 @@ --- +"@morpho-org/morpho-ts": minor +"@morpho-org/blue-sdk-viem": minor "@morpho-org/morpho-sdk": minor --- -Add Blue Public Allocator V2 market and idle reallocations to Blue borrow, supply-collateral-borrow, loan-asset withdraw, and refinance flows while preserving PublicAllocator V1 inputs. +Add the canonical Blue Public Allocator V2 ABI to `morpho-ts`, re-export it from `blue-sdk-viem` and `morpho-sdk`, and expose market and idle reallocations through Blue borrow, supply-collateral-borrow, loan-asset withdraw, and refinance flows while preserving PublicAllocator V1 inputs. diff --git a/packages/blue-sdk-viem/package.json b/packages/blue-sdk-viem/package.json index 29cadd0b4..affb44bd3 100644 --- a/packages/blue-sdk-viem/package.json +++ b/packages/blue-sdk-viem/package.json @@ -31,7 +31,7 @@ }, "peerDependencies": { "@morpho-org/blue-sdk": "^6.4.0", - "@morpho-org/morpho-ts": "^2.7.0", + "@morpho-org/morpho-ts": "^2.9.0", "viem": "^2.0.0" }, "devDependencies": { diff --git a/packages/blue-sdk-viem/src/abis.ts b/packages/blue-sdk-viem/src/abis.ts index 4be94d4ff..7c0491900 100644 --- a/packages/blue-sdk-viem/src/abis.ts +++ b/packages/blue-sdk-viem/src/abis.ts @@ -1,4 +1,5 @@ export { + bluePublicAllocatorV2Abi, erc2612Abi, erc5267Abi, metaMorphoAbi, diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index 849a9e1a9..43c4401c5 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -15,8 +15,10 @@ Instead of exposing the user directly to target contracts (ERC-4626 vault, Morph - receives the user's ERC20 tokens (`erc20TransferFrom`, `permit`, `approve2` / `transferFrom2`), - wraps native into wNative (`nativeTransfer` + `wrapNative`), - calls `erc4626Deposit(vault, assets, maxSharePrice, recipient)` enforcing `maxSharePrice` **on-chain**, -- executes `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral` on Morpho Blue, -- forwards `reallocateTo` calls to the `PublicAllocator` for shared liquidity. +- executes `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral` on Morpho Blue on the user's behalf. + +Bundler3 also calls allocator contracts directly for shared liquidity: `reallocateTo` on Public +Allocator V1 and `reallocate` or `allocateFromIdle` on Blue Public Allocator V2. The **spender** of every approval / permit / permit2 is therefore **always** `generalAdapter1`, never the vault or Morpho directly. See [src/actions/requirements/getRequirements.ts](src/actions/requirements/getRequirements.ts) and the "Requirements System" section of [ARCHITECTURE.md](ARCHITECTURE.md#requirements-system). @@ -24,11 +26,11 @@ The **spender** of every approval / permit / permit2 is therefore **always** `ge The value of the Bundler3 + GeneralAdapter1 pairing rests on three properties: -1. **Composition of elementary actions.** Each step (`nativeTransfer`, `wrapNative`, `erc20TransferFrom`, `permit`, `approve2`, `transferFrom2`, `erc4626Deposit`, `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral`, `reallocateTo`) is an independent building block. The SDK **composes** them in an explicit order to build a business flow. +1. **Composition of elementary actions.** Each step (`nativeTransfer`, `wrapNative`, `erc20TransferFrom`, `permit`, `approve2`, `transferFrom2`, `erc4626Deposit`, `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral`, `reallocateTo`, `bluePublicAllocatorV2Reallocate`, `bluePublicAllocatorV2AllocateFromIdle`) is an independent building block. The SDK **composes** them in an explicit order to build a business flow. 2. **Atomicity.** The entire bundle either succeeds or reverts as one. No intermediate state is exposed to MEV bots or other transactions. 3. **Simplified approval UX.** A user approves _a single spender_ (GeneralAdapter1) for the entire protocol surface — rather than one approval per V1/V2 vault or per Morpho contract. -Concretely, `blueSupplyCollateralBorrow` is not a new contract: it is simply the composition `erc20TransferFrom` + `morphoSupplyCollateral` + `morphoBorrow` inside a single bundle. Same story for `repayWithdrawCollateral`, or for a borrow that must first trigger `reallocateTo` calls through the PublicAllocator. The business logic lives in the **order and selection of actions**, not in a dedicated contract. +Concretely, `blueSupplyCollateralBorrow` is not a new contract: it is simply the composition `erc20TransferFrom` + `morphoSupplyCollateral` + `morphoBorrow` inside a single bundle. Same story for `repayWithdrawCollateral`, or for a borrow that must first trigger Public Allocator V1 or V2 calls. The business logic lives in the **order and selection of actions**, not in a dedicated contract. ## Flows overview @@ -37,8 +39,8 @@ Concretely, `blueSupplyCollateralBorrow` is not a new contract: it is simply the | VaultV1 `deposit` | Bundler3 → GeneralAdapter1 | _(opt)_ `nativeTransfer` + `wrapNative` → `erc20TransferFrom` / `permit` / `approve2`+`transferFrom2` → `erc4626Deposit` | | VaultV2 `deposit` | Bundler3 → GeneralAdapter1 | same as VaultV1 | | Blue `supplyCollateral` | Bundler3 → GeneralAdapter1 | _(opt)_ `nativeTransfer` + `wrapNative` → `erc20TransferFrom` → `morphoSupplyCollateral` | -| Blue `borrow` | Bundler3 → GeneralAdapter1 | _(opt)_ `reallocateTo`×N → `morphoBorrow` _(requires `setAuthorization` for GA1 on Morpho)_ | -| Blue `supplyCollateralBorrow` | Bundler3 → GeneralAdapter1 | `erc20TransferFrom` → `morphoSupplyCollateral` → _(opt)_ `reallocateTo`×N → `morphoBorrow` | +| Blue `borrow` | Bundler3 → GeneralAdapter1 | _(opt)_ allocator reallocations → `morphoBorrow` _(requires `setAuthorization` for GA1 on Morpho)_ | +| Blue `supplyCollateralBorrow` | Bundler3 → GeneralAdapter1 | `erc20TransferFrom` → `morphoSupplyCollateral` → _(opt)_ allocator reallocations → `morphoBorrow` | | Blue `repay` | Bundler3 → GeneralAdapter1 | `erc20TransferFrom` → `morphoRepay` (by `assets` or by `shares`) | | Blue `repayWithdrawCollateral` | Bundler3 → GeneralAdapter1 | `erc20TransferFrom` → `morphoRepay` → `morphoWithdrawCollateral` _(repay **before** withdraw, order is critical)_ | | VaultV1 `withdraw` / `redeem` | **Direct vault call** | _(no bundler, no adapter)_ | @@ -66,7 +68,11 @@ For every ERC-4626 deposit (VaultV1 / VaultV2), GeneralAdapter1 calls `erc4626De ### 4. Shared liquidity without an ad-hoc contract -`VaultReallocation`s are encoded as plain `reallocateTo` bundler actions (PublicAllocator). They are **prepended to the bundle** (borrow) or **inserted between supply-collateral and borrow** (`supplyCollateralBorrow`), and `BundlerAction.encodeBundle` aggregates native fees into `tx.value`. No extra off-chain machinery: everything flows through the same bundler-action composition. +`BlueReallocation`s encode as Public Allocator V1 `reallocateTo` calls or Blue Public Allocator V2 +`reallocate`/`allocateFromIdle` calls. They are **prepended to the bundle** (borrow and withdraw) or +**inserted between supply-collateral and borrow** (`supplyCollateralBorrow`). +`BundlerAction.encodeBundle` aggregates V1 fees and V2 native penalties into `tx.value`. No extra +off-chain machinery is required: everything flows through the same bundler-action composition. ### 5. A single approval surface @@ -101,7 +107,7 @@ This is the main design caveat. For the following operations the SDK emits a **d - **Blue authorization for GA1 required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`.** A user who has never granted it will receive a requirement through [`getBlueAuthorizationRequirement`](src/actions/requirements/blue/getBlueAuthorizationRequirement.ts). Without signature support, this is a `setAuthorization` transaction to execute beforehand. With `supportSignature`, this is a signable requirement; pass the resulting `AuthorizationRequirementSignature` to `buildTx`, which folds it into the bundle as `setAuthorizationWithSig`. - **Critical order in `repayWithdrawCollateral`**: `morphoRepay` **must** precede `morphoWithdrawCollateral` in the bundle, otherwise the position is deemed unhealthy at withdraw time and the tx reverts. - **Builder must equal signer.** Bundler actions reference accounts in two different ways: some take an explicit `onBehalf` and act on `userAddress` (e.g. `morphoRepay`), others act implicitly on the **initiator** — the `msg.sender` of `bundler3.multicall`, i.e. the EOA signing the tx, not the adapter — (e.g. `erc20TransferFrom`, `morphoWithdrawCollateral`, the latter exposing no `onBehalf` parameter on GA1). `repayWithdrawCollateral` is the canonical example: the repay leg targets `userAddress` while the transfer-from and the withdraw target the initiator. If the address that built the tx (and filled `userAddress`) is not the address that signs/executes it, the bundle would repay one account's debt while pulling tokens from and withdrawing collateral against the signer. Transaction builders do not validate this at build time — callers MUST keep `userAddress` aligned with the signing account. The signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) take a `WalletClient` and enforce this at `sign()` time via `validateUserAddress` (throws `MissingClientPropertyError` / `AddressMismatchError`). -- **Tricky `tx.value`**: whenever a `nativeAmount` or a `reallocateTo` (native fee) is involved, `BundlerAction.encodeBundle` computes `tx.value`. Do not overwrite it on the caller side. +- **Tricky `tx.value`**: whenever a `nativeAmount`, V1 `reallocateTo` fee, or V2 native penalty is involved, `BundlerAction.encodeBundle` computes `tx.value`. Do not overwrite it on the caller side. - **Chain-specific Bundler3 address**: always resolve through `getChainAddresses(chainId)` and validate that the viem client's `chainId` matches the params. ## Code references diff --git a/packages/morpho-sdk/README.md b/packages/morpho-sdk/README.md index d3728be6c..f40a07dad 100644 --- a/packages/morpho-sdk/README.md +++ b/packages/morpho-sdk/README.md @@ -234,12 +234,13 @@ graph LR M1S -->|nativeWrap? + erc20TransferFrom + morphoSupply| B3[Bundler3] M1SC -->|erc20TransferFrom + morphoSupplyCollateral| B3 - M1B -->|reallocateTo? + morphoBorrow| B3 - M1SCB -->|transfer + supplyCollateral + reallocateTo? + borrow| B3 - M1W -->|reallocateTo? + morphoWithdraw| B3 - M1RF -->|reallocateTo? + supplyCollateral callback: borrow + repay + withdrawCollateral| B3 + M1B -->|allocator reallocation? + morphoBorrow| B3 + M1SCB -->|transfer + supplyCollateral + allocator reallocation? + borrow| B3 + M1W -->|allocator reallocation? + morphoWithdraw| B3 + M1RF -->|allocator reallocation? + supplyCollateral callback: borrow + repay + withdrawCollateral| B3 - B3 -.->|reallocateTo| PA[PublicAllocator] + B3 -.->|reallocateTo| PA1[PublicAllocator V1] + B3 -.->|reallocate / allocateFromIdle| PA2[Blue Public Allocator V2] end subgraph Midnight Flow diff --git a/packages/morpho-sdk/src/abis.ts b/packages/morpho-sdk/src/abis.ts index 7c6d97b14..52c5dee9c 100644 --- a/packages/morpho-sdk/src/abis.ts +++ b/packages/morpho-sdk/src/abis.ts @@ -5,6 +5,7 @@ export { adaptiveCurveIrmAbi, blueAbi, blueOracleAbi, + bluePublicAllocatorV2Abi, erc2612Abi, erc5267Abi, metaMorphoAbi, @@ -202,85 +203,6 @@ export const vaultExitBundlesV1Abi = [ }, ] as const satisfies Abi; -/** ABI for Blue Public Allocator V2 user reallocation calls. */ -export const bluePublicAllocatorV2Abi = [ - { - type: "function", - name: "reallocate", - inputs: [ - { name: "vault", type: "address", internalType: "address" }, - { - name: "deallocateAdapter", - type: "address", - internalType: "address", - }, - { - name: "deallocateMarketParams", - type: "tuple", - internalType: "struct MarketParams", - components: [ - { name: "loanToken", type: "address", internalType: "address" }, - { - name: "collateralToken", - type: "address", - internalType: "address", - }, - { name: "oracle", type: "address", internalType: "address" }, - { name: "irm", type: "address", internalType: "address" }, - { name: "lltv", type: "uint256", internalType: "uint256" }, - ], - }, - { name: "allocateAdapter", type: "address", internalType: "address" }, - { - name: "allocateMarketParams", - type: "tuple", - internalType: "struct MarketParams", - components: [ - { name: "loanToken", type: "address", internalType: "address" }, - { - name: "collateralToken", - type: "address", - internalType: "address", - }, - { name: "oracle", type: "address", internalType: "address" }, - { name: "irm", type: "address", internalType: "address" }, - { name: "lltv", type: "uint256", internalType: "uint256" }, - ], - }, - { name: "assets", type: "uint128", internalType: "uint128" }, - ], - outputs: [], - stateMutability: "payable", - }, - { - type: "function", - name: "allocateFromIdle", - inputs: [ - { name: "vault", type: "address", internalType: "address" }, - { name: "adapter", type: "address", internalType: "address" }, - { - name: "marketParams", - type: "tuple", - internalType: "struct MarketParams", - components: [ - { name: "loanToken", type: "address", internalType: "address" }, - { - name: "collateralToken", - type: "address", - internalType: "address", - }, - { name: "oracle", type: "address", internalType: "address" }, - { name: "irm", type: "address", internalType: "address" }, - { name: "lltv", type: "uint256", internalType: "uint256" }, - ], - }, - { name: "assets", type: "uint128", internalType: "uint128" }, - ], - outputs: [], - stateMutability: "payable", - }, -] as const; - /** ABI for the Bundler3 multicall contract. */ export const bundler3Abi = [ { diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index ad5bc14ba..6ade28c0a 100644 --- a/packages/morpho-sdk/src/actions/AGENTS.md +++ b/packages/morpho-sdk/src/actions/AGENTS.md @@ -24,7 +24,7 @@ Only valid for assets/collateral configured as wNative. When `nativeAmount > 0`: ## Shared liquidity / reallocations (canonical statement) -`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Untagged `VaultReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action. Tagged `BluePublicAllocatorV2Reallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call pays its own `nativePenalty`. V2 sources are not sorted and idle uses no synthetic zero-address market. `BundlerAction.encodeBundle` sums V1 fees and V2 penalties into `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations`. +`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action. Tagged `BluePublicAllocatorV2Reallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call pays its own `nativePenalty`. V2 sources are not sorted and idle uses no synthetic zero-address market. `BundlerAction.encodeBundle` sums V1 fees and V2 penalties into `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level or V2-source discriminators. ## Discriminated unions diff --git a/packages/morpho-sdk/src/actions/blue/AGENTS.md b/packages/morpho-sdk/src/actions/blue/AGENTS.md index 775e8b12e..4a5d3dbcf 100644 --- a/packages/morpho-sdk/src/actions/blue/AGENTS.md +++ b/packages/morpho-sdk/src/actions/blue/AGENTS.md @@ -28,17 +28,19 @@ ERC-20 approval spender is **GeneralAdapter1** for any bundled path — never th | `supplyCollateral` (ERC-20) | `erc20TransferFrom → morphoSupplyCollateral` | | `supplyCollateral` (native) | `nativeTransfer → wrapNative → [erc20TransferFrom?] → morphoSupplyCollateral` | | `borrow` | `morphoBorrow` | -| `borrow` (with reallocations) | `[reallocateTo × N] → morphoBorrow` | +| `borrow` (with reallocations) | `[allocator reallocation × N] → morphoBorrow` | | `supplyCollateralBorrow` | `[nativeWrap?] → [erc20Transfer?] → morphoSupplyCollateral → morphoBorrow` | -| `supplyCollateralBorrow` (with reallocations) | `[nativeWrap?] → [erc20Transfer?] → morphoSupplyCollateral → [reallocateTo × N] → morphoBorrow` | +| `supplyCollateralBorrow` (with reallocations) | `[nativeWrap?] → [erc20Transfer?] → morphoSupplyCollateral → [allocator reallocation × N] → morphoBorrow` | | `repay` (ERC-20) | `[erc20TransferFrom \| permit/permit2] → morphoRepay → [erc20Transfer skim (shares mode)]` | | `repay` (native) | `nativeTransfer → wrapNative → [erc20TransferFrom?] → morphoRepay → [skim (shares mode)]` | | `repayWithdrawCollateral` (ERC-20) | `[erc20TransferFrom \| permit/permit2] → morphoRepay → [skim (shares mode)] → morphoWithdrawCollateral` | | `repayWithdrawCollateral` (native) | `nativeTransfer → wrapNative → [erc20TransferFrom?] → morphoRepay → [skim (shares mode)] → morphoWithdrawCollateral` | | `withdraw` | `morphoWithdraw` | -| `withdraw` (with reallocations) | `[reallocateTo × N] → morphoWithdraw` | +| `withdraw` (with reallocations) | `[allocator reallocation × N] → morphoWithdraw` | -`BundlerAction.encodeBundle` derives `tx.value` from native wrapping calls and reallocation fees. +An allocator reallocation is V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` according to +the `BlueReallocation` discriminator. `BundlerAction.encodeBundle` derives `tx.value` from native +wrapping calls, V1 fees, and V2 native penalties. ## Mode and ordering rules diff --git a/packages/morpho-sdk/src/actions/blue/borrow.publicAllocatorV2.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.publicAllocatorV2.test.ts new file mode 100644 index 000000000..607e35678 --- /dev/null +++ b/packages/morpho-sdk/src/actions/blue/borrow.publicAllocatorV2.test.ts @@ -0,0 +1,122 @@ +import { ChainId, MarketParams } from "@morpho-org/blue-sdk"; +import { bluePublicAllocatorV2Abi as canonicalBluePublicAllocatorV2Abi } from "@morpho-org/blue-sdk-viem"; +import { decodeFunctionData } from "viem"; +import { describe, expect, test } from "vitest"; +import { + bluePublicAllocatorV2Abi, + bundler3Abi, + generalAdapter1Abi, +} from "../../abis.js"; +import type { BlueReallocation } from "../../types/index.js"; +import { blueBorrow } from "./borrow.js"; + +const allocator = "0x0000000000000000000000000000000000000011"; +const vault = "0x0000000000000000000000000000000000000012"; +const sourceAdapter = "0x0000000000000000000000000000000000000013"; +const targetAdapter = "0x0000000000000000000000000000000000000014"; +const receiver = "0x0000000000000000000000000000000000000015"; + +const targetMarket = new MarketParams({ + loanToken: "0x0000000000000000000000000000000000000021", + collateralToken: "0x0000000000000000000000000000000000000022", + oracle: "0x0000000000000000000000000000000000000023", + irm: "0x0000000000000000000000000000000000000024", + lltv: 860_000000000000000000n, +}); + +const sourceMarket = new MarketParams({ + loanToken: targetMarket.loanToken, + collateralToken: "0x0000000000000000000000000000000000000032", + oracle: "0x0000000000000000000000000000000000000033", + irm: targetMarket.irm, + lltv: targetMarket.lltv, +}); + +describe("blueBorrow Public Allocator V2", () => { + test("default", () => { + const reallocations: readonly BlueReallocation[] = [ + { + type: "publicAllocatorV1", + vault, + fee: 2n, + withdrawals: [{ marketParams: sourceMarket, amount: 1n }], + }, + { + type: "publicAllocatorV2", + allocator, + vault, + from: { + type: "market", + adapter: sourceAdapter, + marketParams: sourceMarket, + }, + to: { adapter: targetAdapter }, + assets: 3n, + nativePenalty: 5n, + }, + { + type: "publicAllocatorV2", + allocator, + vault, + from: { type: "idle" }, + to: { adapter: targetAdapter }, + assets: 7n, + nativePenalty: 11n, + }, + ]; + + const tx = blueBorrow({ + market: { chainId: ChainId.EthMainnet, marketParams: targetMarket }, + args: { + amount: 1n, + minSharePrice: 0n, + receiver, + reallocations, + }, + }); + + expect(tx.value).toBe(18n); + expect(tx.action.args.reallocationFee).toBe(18n); + + const bundle = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); + const calls = bundle.args[0] ?? []; + expect(calls).toHaveLength(4); + expect(calls.slice(0, 3).map((call) => call.value)).toEqual([2n, 5n, 11n]); + expect(calls.slice(0, 3).map((call) => call.skipRevert)).toEqual([ + false, + false, + false, + ]); + + expect( + decodeFunctionData({ + abi: bluePublicAllocatorV2Abi, + data: calls[1]!.data, + }).functionName, + ).toBe("reallocate"); + + const idleCall = decodeFunctionData({ + abi: bluePublicAllocatorV2Abi, + data: calls[2]!.data, + }); + expect(idleCall.functionName).toBe("allocateFromIdle"); + expect(idleCall.args[0]).toBe(vault); + expect(idleCall.args[1]).toBe(targetAdapter); + expect(idleCall.args[2]).toMatchObject({ + loanToken: targetMarket.loanToken, + collateralToken: targetMarket.collateralToken, + oracle: targetMarket.oracle, + irm: targetMarket.irm, + lltv: targetMarket.lltv, + }); + expect(idleCall.args[3]).toBe(7n); + expect( + decodeFunctionData({ abi: generalAdapter1Abi, data: calls[3]!.data }) + .functionName, + ).toBe("morphoBorrow"); + }); + + test("re-exports the canonical ABI", () => { + expect(bluePublicAllocatorV2Abi).toBe(canonicalBluePublicAllocatorV2Abi); + }); +}); diff --git a/packages/morpho-sdk/src/actions/blue/borrow.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.test.ts index 18d5cd372..0013e9d00 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.test.ts @@ -1,5 +1,5 @@ import { getChainAddresses } from "@morpho-org/blue-sdk"; -import { decodeFunctionData, maxUint128, parseUnits } from "viem"; +import { parseUnits } from "viem"; import { mainnet } from "viem/chains"; import { describe, expect } from "vitest"; import { @@ -10,17 +10,8 @@ import { SteakhouseUsdcVaultV1 } from "../../../test/fixtures/vaultV1.js"; import { test } from "../../../test/setup.js"; import { - bluePublicAllocatorV2Abi, - bundler3Abi, - generalAdapter1Abi, -} from "../../abis.js"; -import { - type BlueReallocation, - InputExceedsMaxError, - InvalidReallocationSourceTypeError, NegativeInputError, NonPositiveInputError, - ReallocationWithdrawalOnTargetMarketError, type VaultReallocation, } from "../../types/index.js"; import { blueBorrow } from "./borrow.js"; @@ -89,187 +80,6 @@ describe("blueBorrow unit tests", () => { expect(tx.action.args.reallocationFee).toBe(reallocationFee); }); - test("orders mixed V1 and V2 reallocations before borrow and sums all native costs", async ({ - client, - }) => { - const allocator = "0x0000000000000000000000000000000000000011"; - const sourceAdapter = "0x0000000000000000000000000000000000000012"; - const targetAdapter = "0x0000000000000000000000000000000000000013"; - const reallocations: readonly BlueReallocation[] = [ - { - vault: SteakhouseUsdcVaultV1.address, - fee: 2n, - withdrawals: [{ marketParams: WbtcUsdcSourceMarket, amount: 1n }], - }, - { - type: "publicAllocatorV2", - allocator, - vault: SteakhouseUsdcVaultV1.address, - from: { - type: "market", - adapter: sourceAdapter, - marketParams: WbtcUsdcSourceMarket, - }, - to: { adapter: targetAdapter }, - assets: 3n, - nativePenalty: 5n, - }, - { - type: "publicAllocatorV2", - allocator, - vault: SteakhouseUsdcVaultV1.address, - from: { type: "idle" }, - to: { adapter: targetAdapter }, - assets: 7n, - nativePenalty: 11n, - }, - ]; - - const tx = blueBorrow({ - market: { chainId: mainnet.id, marketParams: WethUsdsBlue }, - args: { - amount: 1n, - minSharePrice: 0n, - receiver: client.account.address, - reallocations, - }, - }); - - expect(tx.value).toBe(18n); - expect(tx.action.args.reallocationFee).toBe(18n); - const bundle = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); - const calls = bundle.args[0] ?? []; - expect(calls).toHaveLength(4); - expect(calls.slice(0, 3).map((call) => call.value)).toEqual([2n, 5n, 11n]); - expect(calls.slice(0, 3).map((call) => call.skipRevert)).toEqual([ - false, - false, - false, - ]); - expect( - decodeFunctionData({ - abi: bluePublicAllocatorV2Abi, - data: calls[1]!.data, - }).functionName, - ).toBe("reallocate"); - const idleCall = decodeFunctionData({ - abi: bluePublicAllocatorV2Abi, - data: calls[2]!.data, - }); - expect(idleCall.functionName).toBe("allocateFromIdle"); - expect(idleCall.args[0]).toBe(SteakhouseUsdcVaultV1.address); - expect(idleCall.args[1]).toBe(targetAdapter); - expect(idleCall.args[2]).toMatchObject({ - loanToken: WethUsdsBlue.loanToken, - collateralToken: WethUsdsBlue.collateralToken, - oracle: WethUsdsBlue.oracle, - irm: WethUsdsBlue.irm, - lltv: WethUsdsBlue.lltv, - }); - expect(idleCall.args[3]).toBe(7n); - expect( - decodeFunctionData({ abi: generalAdapter1Abi, data: calls[3]!.data }) - .functionName, - ).toBe("morphoBorrow"); - }); - - test.each([ - { - name: "negative penalty", - values: { assets: 1n, nativePenalty: -1n }, - ErrorClass: NegativeInputError, - }, - { - name: "zero assets", - values: { assets: 0n, nativePenalty: 0n }, - ErrorClass: NonPositiveInputError, - }, - { - name: "uint128 overflow", - values: { assets: maxUint128 + 1n, nativePenalty: 0n }, - ErrorClass: InputExceedsMaxError, - }, - ])("rejects Public Allocator V2 $name", ({ values, ErrorClass }) => { - expect(() => - blueBorrow({ - market: { chainId: mainnet.id, marketParams: WethUsdsBlue }, - args: { - amount: 1n, - minSharePrice: 0n, - receiver: "0x0000000000000000000000000000000000000001", - reallocations: [ - { - type: "publicAllocatorV2", - allocator: "0x0000000000000000000000000000000000000011", - vault: SteakhouseUsdcVaultV1.address, - from: { type: "idle" }, - to: { - adapter: "0x0000000000000000000000000000000000000012", - }, - ...values, - }, - ], - }, - }), - ).toThrow(ErrorClass); - }); - - test("rejects a Public Allocator V2 source equal to the target market", () => { - expect(() => - blueBorrow({ - market: { chainId: mainnet.id, marketParams: WethUsdsBlue }, - args: { - amount: 1n, - minSharePrice: 0n, - receiver: "0x0000000000000000000000000000000000000001", - reallocations: [ - { - type: "publicAllocatorV2", - allocator: "0x0000000000000000000000000000000000000011", - vault: SteakhouseUsdcVaultV1.address, - from: { - type: "market", - adapter: "0x0000000000000000000000000000000000000012", - marketParams: WethUsdsBlue, - }, - to: { - adapter: "0x0000000000000000000000000000000000000013", - }, - assets: 1n, - nativePenalty: 0n, - }, - ], - }, - }), - ).toThrow(ReallocationWithdrawalOnTargetMarketError); - }); - - test("rejects an unknown Public Allocator V2 source discriminator", () => { - expect(() => - blueBorrow({ - market: { chainId: mainnet.id, marketParams: WethUsdsBlue }, - args: { - amount: 1n, - minSharePrice: 0n, - receiver: "0x0000000000000000000000000000000000000001", - reallocations: [ - { - type: "publicAllocatorV2", - allocator: "0x0000000000000000000000000000000000000011", - vault: SteakhouseUsdcVaultV1.address, - from: { type: "marketTypo" }, - to: { - adapter: "0x0000000000000000000000000000000000000013", - }, - assets: 1n, - nativePenalty: 0n, - } as unknown as BlueReallocation, - ], - }, - }), - ).toThrow(InvalidReallocationSourceTypeError); - }); - test("should throw NonPositiveInputError when amount is zero", async ({ client, }) => { diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index ffceb9674..6051c1847 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -28,7 +28,7 @@ export interface BlueBorrowParams { receiver: Address; /** Minimum borrow share price (in ray). Protects against share price manipulation. */ minSharePrice: bigint; - /** Vault reallocations to execute before borrowing (computed by entity). */ + /** Public Allocator V1 or V2 reallocations to execute before borrowing. */ reallocations?: readonly BlueReallocation[]; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is @@ -47,17 +47,17 @@ export interface BlueBorrowParams { * `onBehalf`. Uses `minSharePrice` to protect against share price manipulation between * transaction construction and execution. * - * When `reallocations` are provided, `reallocateTo` actions are prepended to the bundle, moving - * liquidity from other markets via the PublicAllocator before borrowing. Reallocation fees - * accumulate in `tx.value`. + * When `reallocations` are provided, Public Allocator V1 entries encode `reallocateTo`, while V2 + * market and idle entries encode `reallocate` and `allocateFromIdle`. The calls run before the + * borrow, and V1 fees plus V2 native penalties accumulate in `tx.value`. * * @param params.market.chainId - The chain the market lives on. * @param params.market.marketParams - Market params (loanToken, collateralToken, oracle, irm, lltv). * @param params.args.amount - Loan asset amount to borrow, in the loan token's smallest unit. * @param params.args.receiver - Address that receives the borrowed assets. * @param params.args.minSharePrice - Minimum borrow share price (in ray). Slippage protection. - * @param params.args.reallocations - Optional vault reallocations to execute before borrowing, - * computed by the entity layer. + * @param params.args.reallocations - Optional Public Allocator V1 or V2 reallocations to execute + * before borrowing. * @param params.args.authorizationSignature - Optional signed Morpho authorization; when present, * a `setAuthorizationWithSig` call is prepended to the bundle. * @param params.metadata - Optional analytics metadata attached to the bundle. @@ -67,7 +67,8 @@ export interface BlueBorrowParams { * is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. - * @throws {NegativeInputError} when `minSharePrice < 0n` or any reallocation fee is negative. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {NegativeInputError} when `minSharePrice < 0n`, a V1 fee, or a V2 native penalty is negative. * @throws {EmptyReallocationWithdrawalsError} from `buildReallocationActions` when any * `reallocation.withdrawals` is empty. * @throws {ReallocationWithdrawalOnTargetMarketError} from `buildReallocationActions` when any diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 4e5de3a08..56aa2ab14 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -18,6 +18,7 @@ import type { BlueReallocation } from "../../types/index.js"; * @throws {NonPositiveInputError} when a V1 withdrawal or V2 asset amount is non-positive. * @throws {InputExceedsMaxError} when a V2 asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when V1 withdrawals are not strictly market-id sorted. * @internal @@ -32,7 +33,7 @@ export const buildReallocationActions = ( const actions: Action[] = []; for (const reallocation of reallocations) { - if ("type" in reallocation && reallocation.type === "publicAllocatorV2") { + if (reallocation.type === "publicAllocatorV2") { if (reallocation.from.type === "market") { actions.push({ type: "bluePublicAllocatorV2Reallocate", @@ -66,8 +67,6 @@ export const buildReallocationActions = ( continue; } - if (!("withdrawals" in reallocation)) continue; - actions.push({ type: "reallocateTo", args: [ diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index 6315ed624..72006bf76 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -44,7 +44,7 @@ export interface BlueRefinanceParams { minBorrowSharePrice: bigint; /** Maximum repay share price on the source market (in ray); must be > 0 when a repay leg exists. */ maxRepaySharePrice: bigint; - /** PublicAllocator reallocations into the target market, run before the bundle. Fees add to `tx.value`. */ + /** Public Allocator V1 or V2 reallocations into the target market, run before the supply leg. */ targetReallocations?: readonly BlueReallocation[]; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is @@ -67,8 +67,8 @@ export interface BlueRefinanceParams { * Bundle shape (callback contents depend on borrow mode): * * ```text - * // optional: one reallocateTo per targetReallocations entry, run first - * reallocateTo(vault_i, fee_i, withdrawals_i, target, false), + * // optional targetReallocations run first: + * reallocateTo(...) | reallocate(...) | allocateFromIdle(...), * * morphoSupplyCollateral(target, collateralAmount, user, [ * // omitted in collat-only mode @@ -102,7 +102,8 @@ export interface BlueRefinanceParams { * @param params.args.borrowShares - Borrow shares to repay on the source; exclusive with `borrowAssets`. Defaults to `0n`. * @param params.args.minBorrowSharePrice - Minimum borrow share price (ray) on the target. * @param params.args.maxRepaySharePrice - Maximum repay share price (ray) on the source. - * @param params.args.targetReallocations - PublicAllocator reallocations into the target, run before the supply leg. + * @param params.args.targetReallocations - Public Allocator V1 or V2 reallocations into the target, + * run before the supply leg. V1 fees and V2 native penalties add to `tx.value`. * @param params.args.authorizationSignature - Optional signed Morpho authorization; when present, * a `setAuthorizationWithSig` call is prepended to the bundle. * @param params.metadata - Optional analytics metadata appended to `tx.data`. @@ -113,8 +114,9 @@ export interface BlueRefinanceParams { * `maxRepaySharePrice`, or any reallocation withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {NegativeInputError} when `borrowAssets`, `borrowShares`, `minBorrowSharePrice`, - * `maxRepaySharePrice`, or any reallocation fee is negative. + * `maxRepaySharePrice`, a V1 fee, or a V2 native penalty is negative. * @throws {RefinanceSameMarketError} when source and target market ids are equal. * @throws {RefinanceTokenMismatchError} when source and target do not share both tokens. * @throws {RefinanceSharesMissingBorrowAssetsError} when `borrowShares > 0n` but `borrowAssets` is omitted or non-positive. diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 57d4fd99c..7e34babcc 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -35,7 +35,7 @@ export interface BlueSupplyCollateralBorrowParams { minSharePrice: bigint; /** Optional pre-signed permit/permit2 approval for the collateral transfer. */ requirementSignature?: PermitRequirementSignature; - /** Vault reallocations to execute before borrowing (computed by entity). */ + /** Public Allocator V1 or V2 reallocations to execute before borrowing. */ reallocations?: readonly BlueReallocation[]; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is @@ -50,9 +50,11 @@ export interface BlueSupplyCollateralBorrowParams { /** * Prepares an atomic supply-collateral-and-borrow transaction for a Morpho Blue market. * - * Routed through bundler3: collateral transfer → `morphoSupplyCollateral` → optional - * `reallocateTo` calls → `morphoBorrow`. When `nativeAmount > 0`, native ETH is wrapped via - * `GeneralAdapter1.wrapNative()` before the supply leg. + * Routed through bundler3: collateral transfer → `morphoSupplyCollateral` → optional Public + * Allocator calls → `morphoBorrow`. V1 entries encode `reallocateTo`; V2 market and idle entries + * encode `reallocate` and `allocateFromIdle`. When `nativeAmount > 0`, native ETH is wrapped via + * `GeneralAdapter1.wrapNative()` before the supply leg. V1 fees and V2 native penalties add to + * `tx.value`. * * Prerequisite: `GeneralAdapter1` must be authorized on Morpho to borrow on behalf of the user. * Use `getRequirements()` on the entity to check and obtain the authorization transaction. @@ -72,19 +74,20 @@ export interface BlueSupplyCollateralBorrowParams { * collateral transfer. * @param params.args.nativeAmount - Optional amount of native token to wrap into wNative for the * collateral supply. Requires the collateral token to be the chain's wNative. - * @param params.args.reallocations - Optional vault reallocations to execute between the supply - * and borrow legs, computed by the entity layer. + * @param params.args.reallocations - Optional Public Allocator V1 or V2 reallocations to execute + * between the supply and borrow legs. * @param params.args.authorizationSignature - Optional signed Morpho authorization; when present, * a `setAuthorizationWithSig` call is prepended to the bundle. * @param params.metadata - Optional analytics metadata attached to the bundle. * @returns A deep-frozen `Transaction` with `to`, `value`, * `data`, and the typed `action` discriminator the simulation layer consumes. - * @throws {NegativeInputError} when `amount`, `nativeAmount`, `minSharePrice`, or any reallocation - * fee is negative. + * @throws {NegativeInputError} when `amount`, `nativeAmount`, `minSharePrice`, a V1 fee, or a V2 + * native penalty is negative. * @throws {NonPositiveInputError} when `borrowAmount <= 0n`, both collateral amounts resolve to * zero, or any reallocation withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {ChainWNativeMissingError} when `nativeAmount > 0n` but the chain has no configured wNative. * @throws {NativeAmountOnNonWNativeAssetError} when `nativeAmount > 0n` but the collateral * token is not the chain's wNative. diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index 5452af32c..e0deefb1d 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -32,9 +32,9 @@ export interface BlueWithdrawParams { /** Minimum withdraw share price (in ray). Slippage protection. */ minSharePrice: bigint; /** - * Vault reallocations to execute before withdrawing. Compute via - * `MorphoBlue.getReallocations({ operation: "withdraw", amount })` or directly via - * `computeReallocations({ operation: "withdraw", amount, ... })`. + * Public Allocator V1 or V2 reallocations to execute before withdrawing. V1 entries can be + * computed via `MorphoBlue.getReallocations({ operation: "withdraw", amount })` or directly + * via `computeReallocations({ operation: "withdraw", amount, ... })`. */ reallocations?: readonly BlueReallocation[]; /** @@ -56,10 +56,10 @@ export interface BlueWithdrawParams { * - **By shares** (`assets = 0, shares > 0`): burns an exact share count (typical for a full * supplier position close; immune to interest accrual between tx construction and execution). * - * When `reallocations` are provided, `reallocateTo` actions are prepended to the bundle, moving - * liquidity from other markets into this one via the PublicAllocator before withdrawing. - * Reallocation fees accumulate in `tx.value`. The on-chain `morphoWithdraw` sends the assets - * computed on-chain directly to `receiver`; no skim is required. + * When `reallocations` are provided, V1 entries encode `reallocateTo`, while V2 market and idle + * entries encode `reallocate` and `allocateFromIdle`. The calls run before the withdraw, and V1 + * fees plus V2 native penalties accumulate in `tx.value`. The on-chain `morphoWithdraw` sends + * the assets computed on-chain directly to `receiver`; no skim is required. * * The withdraw is performed on behalf of the transaction initiator (signer) — there is no * separate `onBehalf` field; mirror `blueBorrow`. The entity layer keeps `receiver` aligned @@ -73,19 +73,20 @@ export interface BlueWithdrawParams { * @param params.args.receiver - Address that receives the withdrawn assets. * @param params.args.minSharePrice - Minimum acceptable withdraw share price (in ray). Slippage * protection. - * @param params.args.reallocations - Optional vault reallocations to execute before withdrawing, - * computed by the entity layer. + * @param params.args.reallocations - Optional Public Allocator V1 or V2 reallocations to execute + * before withdrawing. * @param params.args.authorizationSignature - Optional signed Morpho authorization; when present, * a `setAuthorizationWithSig` call is prepended to the bundle. * @param params.metadata - Optional analytics metadata attached to the bundle. * @returns A deep-frozen `Transaction` with `to`, `value`, `data`, and * the typed `action` discriminator the simulation layer consumes. - * @throws {NegativeInputError} when `assets`, `shares`, `minSharePrice`, or any reallocation fee - * is negative. + * @throws {NegativeInputError} when `assets`, `shares`, `minSharePrice`, a V1 fee, or a V2 native + * penalty is negative. * @throws {NonPositiveInputError} when both `assets` and `shares` are zero or any reallocation * withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {MutuallyExclusiveWithdrawAmountsError} when both `assets` and `shares` are non-zero. * @throws {EmptyReallocationWithdrawalsError} when any reallocation has no withdrawals. * @throws {ReallocationWithdrawalOnTargetMarketError} when a reallocation withdrawal references diff --git a/packages/morpho-sdk/src/bundler/actions.test.ts b/packages/morpho-sdk/src/bundler/actions.test.ts index 606251094..5706d3d3a 100644 --- a/packages/morpho-sdk/src/bundler/actions.test.ts +++ b/packages/morpho-sdk/src/bundler/actions.test.ts @@ -5,6 +5,7 @@ import { } from "@morpho-org/blue-sdk"; import { blueAbi, + bluePublicAllocatorV2Abi, erc2612Abi, permit2Abi, publicAllocatorAbi, @@ -22,12 +23,7 @@ import { zeroHash, } from "viem"; import { describe, expect, test } from "vitest"; -import { - bluePublicAllocatorV2Abi, - bundler3Abi, - coreAdapterAbi, - generalAdapter1Abi, -} from "../abis.js"; +import { bundler3Abi, coreAdapterAbi, generalAdapter1Abi } from "../abis.js"; import { BundlerErrors } from "../types/index.js"; import { type Action, diff --git a/packages/morpho-sdk/src/bundler/actions.ts b/packages/morpho-sdk/src/bundler/actions.ts index c1d0c5429..e369f4c3a 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -4,6 +4,7 @@ import { } from "@morpho-org/blue-sdk"; import { blueAbi, + bluePublicAllocatorV2Abi, erc2612Abi, permit2Abi, publicAllocatorAbi, @@ -20,12 +21,7 @@ import { serializeSignature, zeroHash, } from "viem"; -import { - bluePublicAllocatorV2Abi, - bundler3Abi, - coreAdapterAbi, - generalAdapter1Abi, -} from "../abis.js"; +import { bundler3Abi, coreAdapterAbi, generalAdapter1Abi } from "../abis.js"; import { BundlerErrors } from "../types/error.js"; import type { Action, @@ -1467,6 +1463,24 @@ export namespace BundlerAction { * @returns One encoded call targeting the explicit allocator. * @example * ```ts + * import { BundlerAction } from "@morpho-org/morpho-sdk/bundler"; + * + * const allocator = "0x0000000000000000000000000000000000000001"; + * const vault = "0x0000000000000000000000000000000000000002"; + * const sourceAdapter = "0x0000000000000000000000000000000000000003"; + * const targetAdapter = "0x0000000000000000000000000000000000000004"; + * const sourceMarket = { + * loanToken: "0x0000000000000000000000000000000000000005", + * collateralToken: "0x0000000000000000000000000000000000000006", + * oracle: "0x0000000000000000000000000000000000000007", + * irm: "0x0000000000000000000000000000000000000008", + * lltv: 860_000000000000000000n, + * }; + * const targetMarket = { + * ...sourceMarket, + * collateralToken: "0x0000000000000000000000000000000000000009", + * }; + * * const calls = BundlerAction.bluePublicAllocatorV2Reallocate( * allocator, * vault, @@ -1477,6 +1491,7 @@ export namespace BundlerAction { * 1_000_000n, * 10n, * ); + * // calls[0] targets `allocator` with `value: 10n` and `reallocate` calldata. * ``` */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call @@ -1526,6 +1541,19 @@ export namespace BundlerAction { * @returns One encoded call targeting the explicit allocator. * @example * ```ts + * import { BundlerAction } from "@morpho-org/morpho-sdk/bundler"; + * + * const allocator = "0x0000000000000000000000000000000000000001"; + * const vault = "0x0000000000000000000000000000000000000002"; + * const targetAdapter = "0x0000000000000000000000000000000000000003"; + * const targetMarket = { + * loanToken: "0x0000000000000000000000000000000000000004", + * collateralToken: "0x0000000000000000000000000000000000000005", + * oracle: "0x0000000000000000000000000000000000000006", + * irm: "0x0000000000000000000000000000000000000007", + * lltv: 860_000000000000000000n, + * }; + * * const calls = BundlerAction.bluePublicAllocatorV2AllocateFromIdle( * allocator, * vault, @@ -1534,6 +1562,7 @@ export namespace BundlerAction { * 1_000_000n, * 10n, * ); + * // calls[0] targets `allocator` with `value: 10n` and `allocateFromIdle` calldata. * ``` */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 30d2031e5..c73ccba76 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -188,9 +188,9 @@ export interface BlueActions { * * Computes `minSharePrice` from market supply state and `slippageTolerance`. * - * When `reallocations` is provided, `reallocateTo` actions are prepended to the bundle, - * moving liquidity from other markets via the PublicAllocator before withdrawing — used to - * unblock withdraws that exceed on-market liquidity. + * When `reallocations` is provided, V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` + * actions are prepended to move liquidity before withdrawing. V1 fees and V2 native penalties + * add to the transaction value. * * `getRequirements` returns `morpho.setAuthorization(generalAdapter1, true)` if GA1 is not * yet authorized on Morpho (returns `[]` when already authorized), since the bundler calls @@ -202,6 +202,7 @@ export interface BlueActions { * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ withdraw: ( params: { @@ -227,8 +228,9 @@ export interface BlueActions { * Validates position health with LLTV buffer (0.5%) using the pre-fetched `positionData`. * Computes `minSharePrice` from market borrow state and `slippageTolerance`. * - * When `reallocations` is provided, `reallocateTo` actions are prepended to the bundle, - * moving liquidity from other markets via the PublicAllocator before borrowing. + * When `reallocations` is provided, V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` + * actions are prepended before borrowing. V1 fees and V2 native penalties add to the + * transaction value. * * `getRequirements` returns `morpho.setAuthorization(generalAdapter1, true)` if not yet authorized, * since borrowing through bundler3 requires GeneralAdapter1 authorization on Morpho. @@ -239,6 +241,7 @@ export interface BlueActions { * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ borrow: (params: { userAddress: Address; @@ -370,8 +373,9 @@ export interface BlueActions { * Routed through the bundler. Validates position health with LLTV buffer * to prevent instant liquidation on new positions near the LLTV threshold. * - * When `reallocations` is provided, `reallocateTo` actions are prepended before - * `morphoBorrow` in the bundle. + * When `reallocations` is provided, V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` + * actions run between the collateral supply and `morphoBorrow`. V1 fees and V2 native penalties + * add to the transaction value. * * `getRequirements` returns in parallel: * - ERC20 approval or permit for collateral token (to GeneralAdapter1). @@ -383,6 +387,7 @@ export interface BlueActions { * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ supplyCollateralBorrow: ( params: { @@ -420,6 +425,8 @@ export interface BlueActions { * both the residual source and the aggregate target position stay within LLTV − buffer. Both * markets are forward-accrued to `now`; in shares mode the target borrow is overshot by * `slippageTolerance` and the callback sweeps the residual. + * Target reallocations run first as V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` + * actions; V1 fees and V2 native penalties add to the transaction value. * * `getRequirements` returns `morpho.setAuthorization(generalAdapter1, true)` when GA1 is not yet * authorized (a single global authorization covers both markets). @@ -432,10 +439,11 @@ export interface BlueActions { * @param params.borrowAssets - Loan assets to repay on source; exclusive with `borrowShares`. * @param params.borrowShares - Borrow shares to repay on source; exclusive with `borrowAssets`. * @param params.slippageTolerance - WAD slippage tolerance. Defaults to `DEFAULT_SLIPPAGE_TOLERANCE`. - * @param params.targetReallocations - PublicAllocator reallocations into the target market. + * @param params.targetReallocations - Public Allocator V1 or V2 reallocations into the target market. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ refinance: (params: { userAddress: Address; diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index 89032b49d..de321af59 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -7,7 +7,7 @@ import { MathLib, ORACLE_PRICE_SCALE, } from "@morpho-org/blue-sdk"; -import type { Address } from "viem"; +import { type Address, maxUint128 } from "viem"; import { mainnet } from "viem/chains"; import { describe, expect, test } from "vitest"; import { CbbtcUsdcBlue, WethUsdsBlue } from "../../test/fixtures/blue.js"; @@ -18,11 +18,16 @@ import { import { AccrualPositionUserMismatchError, AddressMismatchError, + type BluePublicAllocatorV2Reallocation, + type BlueReallocation, BorrowExceedsSafeLtvError, ChainIdMismatchError, ChainWNativeMissingError, EmptyReallocationWithdrawalsError, ExcessiveSlippageToleranceError, + InputExceedsMaxError, + InvalidReallocationSourceTypeError, + InvalidReallocationTypeError, MarketIdMismatchError, MissingClientPropertyError, MissingMarketPriceError, @@ -554,12 +559,109 @@ describe("validateReallocations", () => { withdrawals: [{ marketParams: sourceMarketA, amount: 10n ** 18n }], }; + const validV2Reallocation: BluePublicAllocatorV2Reallocation = { + type: "publicAllocatorV2", + allocator: USER_A, + vault: USER_B, + from: { type: "idle" }, + to: { adapter: USER_A }, + assets: 1n, + nativePenalty: 0n, + }; + test("should pass with valid reallocations", () => { expect(() => validateReallocations([validReallocation], targetMarketId), ).not.toThrow(); }); + test("behavior: accepts an explicitly tagged V1 reallocation", () => { + expect(() => + validateReallocations( + [{ ...validReallocation, type: "publicAllocatorV1" }], + targetMarketId, + ), + ).not.toThrow(); + }); + + test("behavior: accepts a valid V2 idle reallocation", () => { + expect(() => + validateReallocations([validV2Reallocation], targetMarketId), + ).not.toThrow(); + }); + + test.each([ + { + name: "negative native penalty", + reallocation: { ...validV2Reallocation, nativePenalty: -1n }, + ErrorClass: NegativeInputError, + }, + { + name: "zero assets", + reallocation: { ...validV2Reallocation, assets: 0n }, + ErrorClass: NonPositiveInputError, + }, + { + name: "uint128 asset overflow", + reallocation: { ...validV2Reallocation, assets: maxUint128 + 1n }, + ErrorClass: InputExceedsMaxError, + }, + ])("error: rejects V2 $name", ({ reallocation, ErrorClass }) => { + expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( + ErrorClass, + ); + }); + + test("error: ReallocationWithdrawalOnTargetMarketError for a V2 target-market source", () => { + expect(() => + validateReallocations( + [ + { + ...validV2Reallocation, + from: { + type: "market", + adapter: USER_A, + marketParams, + }, + } satisfies BluePublicAllocatorV2Reallocation, + ], + targetMarketId, + ), + ).toThrow(ReallocationWithdrawalOnTargetMarketError); + }); + + test("error: InvalidReallocationSourceTypeError", () => { + const reallocation = { + ...validV2Reallocation, + from: { type: "marketTypo" }, + } as unknown as BlueReallocation; + + expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( + InvalidReallocationSourceTypeError, + ); + }); + + test.each([ + { + name: "unknown top-level discriminator", + reallocation: { + ...validReallocation, + type: "publicAllocatorV3", + } as unknown as BlueReallocation, + }, + { + name: "untagged entry without V1 withdrawals", + reallocation: { + vault: USER_A, + fee: 0n, + } as unknown as BlueReallocation, + }, + ])("error: InvalidReallocationTypeError for $name", ({ reallocation }) => { + expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( + InvalidReallocationTypeError, + ); + }); + test("should throw NegativeInputError when fee is negative", () => { expect(() => validateReallocations( diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index c2282ab6c..ab35ab891 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -19,6 +19,7 @@ import { ExcessiveSlippageToleranceError, InputExceedsMaxError, InvalidReallocationSourceTypeError, + InvalidReallocationTypeError, MarketIdMismatchError, MissingClientPropertyError, MissingMarketPriceError, @@ -345,6 +346,7 @@ export const validateRepayShares = (params: { * @throws {NonPositiveInputError} when a withdrawal or V2 asset amount is non-positive. * @throws {InputExceedsMaxError} when a V2 asset amount exceeds `uint128`. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when withdrawals are not strictly market-id sorted. * @example @@ -361,7 +363,7 @@ export const validateReallocations = ( targetMarketId: MarketId, ): void => { for (const r of reallocations) { - if ("type" in r && r.type === "publicAllocatorV2") { + if (r.type === "publicAllocatorV2") { const sourceType: string = r.from.type; if (sourceType !== "market" && sourceType !== "idle") { throw new InvalidReallocationSourceTypeError(sourceType); @@ -393,7 +395,16 @@ export const validateReallocations = ( } continue; } - if (!("withdrawals" in r)) continue; + const reallocationType = r.type; + if ( + reallocationType !== undefined && + reallocationType !== "publicAllocatorV1" + ) { + throw new InvalidReallocationTypeError(reallocationType); + } + if (!("withdrawals" in r)) { + throw new InvalidReallocationTypeError(reallocationType); + } if (r.fee < 0n) { throw new NegativeInputError("reallocation.fee", r.fee); } diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index 50042b023..ee10ef5ae 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -18,7 +18,7 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. ## Shared liquidity (`sharedLiquidity.ts`) -- `VaultReallocation` — untagged PublicAllocator V1 vault address + fee + sorted withdrawals; maps to `reallocateTo()`. +- `VaultReallocation` — legacy-untagged or explicitly `publicAllocatorV1` vault address + fee + sorted withdrawals; maps to `reallocateTo()`. - `BluePublicAllocatorV2Reallocation` — tagged V2 allocator/vault/source/target-adapter/assets/native-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. - `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. @@ -26,9 +26,9 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. One class per error case. Never throw a generic `Error` from SDK source. -- **Generic input bounds:** `NegativeInputError` for values that must be non-negative and `NonPositiveInputError` for values that must be positive. Both expose the invalid `field` and `value`; reuse them across Vault, Blue, and Midnight instead of adding operation-specific scalar-bound errors. +- **Generic input bounds:** `NegativeInputError` for values that must be non-negative, `NonPositiveInputError` for values that must be positive, and `InputExceedsMaxError` for protocol-width upper bounds such as Public Allocator V2's `uint128` assets. All expose the invalid `field` and `value`; reuse them across Vault, Blue, and Midnight instead of adding operation-specific scalar-bound errors. - **Market-specific:** `BorrowExceedsSafeLtvError`, `MissingMarketPriceError`, `NativeAmountOnNonWNativeAssetError`, `MutuallyExclusiveWithdrawAmountsError`, `WithdrawExceedsSupplyError`, `WithdrawSharesExceedSupplyError`. -- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. +- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationTypeError` for an unknown top-level Public Allocator variant, `InvalidReallocationSourceTypeError` for an unknown V2 source, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. ## Adding a new operation diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 94e58baa9..062ac344d 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -798,6 +798,31 @@ export class ReallocationWithdrawalOnTargetMarketError extends Error { } } +/** + * Thrown when a Public Allocator reallocation has an unknown top-level discriminator. + * + * @example + * ```ts + * import { InvalidReallocationTypeError } from "@morpho-org/morpho-sdk"; + * + * const error = new InvalidReallocationTypeError("publicAllocatorV3"); + * ``` + */ +export class InvalidReallocationTypeError extends Error { + /** + * @param reallocationType - Invalid runtime value received for `reallocation.type`, or + * `undefined` when an untagged entry lacks the V1 `withdrawals` field. + */ + public constructor(public readonly reallocationType: string | undefined) { + super( + reallocationType === undefined + ? 'Reallocation must be an untagged Public Allocator V1 entry with "withdrawals" or specify type "publicAllocatorV1" or "publicAllocatorV2".' + : `Reallocation type must be "publicAllocatorV1" or "publicAllocatorV2", got "${reallocationType}".`, + ); + this.name = "InvalidReallocationTypeError"; + } +} + /** * Thrown when a Blue Public Allocator V2 source has an unknown discriminator. * diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 63325fd71..960ce46b0 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -74,6 +74,8 @@ export interface ReallocationWithdrawal { * Withdraws from source markets and supplies to the target market. */ export interface VaultReallocation { + /** Optional discriminator; omitted by legacy Public Allocator V1 callers. */ + readonly type?: "publicAllocatorV1"; readonly vault: Address; /** Fee in native token (ETH) paid to the PublicAllocator for this vault. */ readonly fee: bigint; @@ -118,7 +120,12 @@ export interface BluePublicAllocatorV2Reallocation { readonly nativePenalty: bigint; } -/** Additive union accepted by Blue actions that support V1 or V2 reallocations. */ +/** + * Reallocation accepted by Blue actions that support Public Allocator V1 or V2. + * + * V1 entries remain valid without a `type` field and may optionally use + * `type: "publicAllocatorV1"`; V2 entries use `type: "publicAllocatorV2"`. + */ export type BlueReallocation = | VaultReallocation | BluePublicAllocatorV2Reallocation; diff --git a/packages/morpho-ts/src/abis.ts b/packages/morpho-ts/src/abis.ts index 470e5652e..117583f3e 100644 --- a/packages/morpho-ts/src/abis.ts +++ b/packages/morpho-ts/src/abis.ts @@ -4038,6 +4038,34 @@ export const metaMorphoAbi = [ }, ] as const; +const morphoBlueMarketParamsAbiComponents = [ + { + internalType: "address", + name: "loanToken", + type: "address", + }, + { + internalType: "address", + name: "collateralToken", + type: "address", + }, + { + internalType: "address", + name: "oracle", + type: "address", + }, + { + internalType: "address", + name: "irm", + type: "address", + }, + { + internalType: "uint256", + name: "lltv", + type: "uint256", + }, +] as const; + /** PublicAllocator ABI used to read vault allocator configuration and flow caps. */ export const publicAllocatorAbi = [ { @@ -4442,33 +4470,7 @@ export const publicAllocatorAbi = [ { components: [ { - components: [ - { - internalType: "address", - name: "loanToken", - type: "address", - }, - { - internalType: "address", - name: "collateralToken", - type: "address", - }, - { - internalType: "address", - name: "oracle", - type: "address", - }, - { - internalType: "address", - name: "irm", - type: "address", - }, - { - internalType: "uint256", - name: "lltv", - type: "uint256", - }, - ], + components: morphoBlueMarketParamsAbiComponents, internalType: "struct MarketParams", name: "marketParams", type: "tuple", @@ -4484,33 +4486,7 @@ export const publicAllocatorAbi = [ type: "tuple[]", }, { - components: [ - { - internalType: "address", - name: "loanToken", - type: "address", - }, - { - internalType: "address", - name: "collateralToken", - type: "address", - }, - { - internalType: "address", - name: "oracle", - type: "address", - }, - { - internalType: "address", - name: "irm", - type: "address", - }, - { - internalType: "uint256", - name: "lltv", - type: "uint256", - }, - ], + components: morphoBlueMarketParamsAbiComponents, internalType: "struct MarketParams", name: "supplyMarketParams", type: "tuple", @@ -4619,6 +4595,79 @@ export const publicAllocatorAbi = [ }, ] as const; +/** Blue Public Allocator V2 ABI used for market and idle reallocations. */ +export const bluePublicAllocatorV2Abi = [ + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "deallocateAdapter", + type: "address", + }, + { + components: morphoBlueMarketParamsAbiComponents, + internalType: "struct MarketParams", + name: "deallocateMarketParams", + type: "tuple", + }, + { + internalType: "address", + name: "allocateAdapter", + type: "address", + }, + { + components: morphoBlueMarketParamsAbiComponents, + internalType: "struct MarketParams", + name: "allocateMarketParams", + type: "tuple", + }, + { + internalType: "uint128", + name: "assets", + type: "uint128", + }, + ], + name: "reallocate", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + components: morphoBlueMarketParamsAbiComponents, + internalType: "struct MarketParams", + name: "marketParams", + type: "tuple", + }, + { + internalType: "uint128", + name: "assets", + type: "uint128", + }, + ], + name: "allocateFromIdle", + outputs: [], + stateMutability: "payable", + type: "function", + }, +] as const; + /** Wrapped Backed token ABI used to discover permissioning controllers. */ export const wrappedBackedTokenAbi = [ { From 5ed0d2722c5659ce026d8fc8a6a1e823b66a2ca3 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Thu, 6 Aug 2026 13:52:00 +0200 Subject: [PATCH 05/41] refactor: align BluePublicAllocator naming --- .changeset/brave-vaults-reallocate.md | 2 +- packages/blue-sdk-viem/src/abis.ts | 2 +- packages/morpho-sdk/AGENTS.md | 4 +- packages/morpho-sdk/BUNDLER3.md | 20 +++--- packages/morpho-sdk/README.md | 2 +- packages/morpho-sdk/src/abis.ts | 2 +- packages/morpho-sdk/src/actions/AGENTS.md | 2 +- .../morpho-sdk/src/actions/blue/AGENTS.md | 7 ++- ....ts => borrow.bluePublicAllocator.test.ts} | 34 +++++++---- .../actions/blue/buildReallocationActions.ts | 31 +++++----- .../morpho-sdk/src/bundler/actions.test.ts | 36 +++++------ packages/morpho-sdk/src/bundler/actions.ts | 30 ++++----- packages/morpho-sdk/src/bundler/types.ts | 8 +-- .../morpho-sdk/src/helpers/validate.test.ts | 61 +++++++++++-------- packages/morpho-sdk/src/helpers/validate.ts | 15 ++--- packages/morpho-sdk/src/types/AGENTS.md | 6 +- packages/morpho-sdk/src/types/error.ts | 6 +- .../morpho-sdk/src/types/sharedLiquidity.ts | 23 +++---- packages/morpho-ts/src/abis.ts | 4 +- 19 files changed, 161 insertions(+), 134 deletions(-) rename packages/morpho-sdk/src/actions/blue/{borrow.publicAllocatorV2.test.ts => borrow.bluePublicAllocator.test.ts} (78%) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 283aa25ba..e23edacf6 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -4,4 +4,4 @@ "@morpho-org/morpho-sdk": minor --- -Add the canonical Blue Public Allocator V2 ABI to `morpho-ts`, re-export it from `blue-sdk-viem` and `morpho-sdk`, and expose market and idle reallocations through Blue borrow, supply-collateral-borrow, loan-asset withdraw, and refinance flows while preserving PublicAllocator V1 inputs. +Add the canonical Blue Public Allocator ABI to `morpho-ts`, re-export it from `blue-sdk-viem` and `morpho-sdk`, and expose market and idle reallocations through Blue borrow, supply-collateral-borrow, loan-asset withdraw, and refinance flows while preserving PublicAllocator V1 inputs. diff --git a/packages/blue-sdk-viem/src/abis.ts b/packages/blue-sdk-viem/src/abis.ts index 7c0491900..de24f05cf 100644 --- a/packages/blue-sdk-viem/src/abis.ts +++ b/packages/blue-sdk-viem/src/abis.ts @@ -1,5 +1,5 @@ export { - bluePublicAllocatorV2Abi, + bluePublicAllocatorAbi, erc2612Abi, erc5267Abi, metaMorphoAbi, diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index 152556a83..61a4f80ee 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -27,7 +27,7 @@ Protocol terms used across this package's docs and JSDoc: - **bundler3** — the bundler entry point; receives a sequence of adapter actions in one transaction. - **GeneralAdapter1** — the bundler-side adapter that holds approvals/auth and executes Morpho calls on the user's behalf. Required as the spender for ERC-20 approvals on every bundled path; required as authorized operator on Morpho for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, and `withdraw` (the supplier-side path). - **PublicAllocator V1** — MetaMorpho allocator that moves liquidity from one or more sorted source markets into a target via `reallocateTo(...)`; each call pays one `fee`. -- **BluePublicAllocator V2** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered; each call pays its own `nativePenalty`. +- **BluePublicAllocator** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered; each call pays its own `nativePenalty`. - **VaultExitBundlesV1** — standalone periphery for exiting an illiquid VaultV1 or single-adapter VaultV2 into idle underlying assets and/or Morpho Blue supply positions. ### Bundler actions @@ -41,7 +41,7 @@ The action verbs an integrator sees in the bundle (`BundlerAction.encode...`): - **`nativeTransfer` + `wrapNative`** — pair that converts an attached native amount (`tx.value`) into the chain's wNative for a deposit/supply path. - **`forceDeallocate`** — VaultV2 multicall entry that pulls liquidity out of a specific adapter before withdraw/redeem. - **`reallocateTo`** — PublicAllocator V1 call that shifts liquidity from sorted source markets into the target market. -- **`bluePublicAllocatorV2Reallocate` / `bluePublicAllocatorV2AllocateFromIdle`** — BluePublicAllocator V2 calls that move one market source or vault idle liquidity into the enclosing Blue action's target market. Both target an explicit allocator address and carry one native penalty. +- **`bluePublicAllocatorReallocate` / `bluePublicAllocatorAllocateFromIdle`** — BluePublicAllocator calls that move one market source or vault idle liquidity into the enclosing Blue action's target market. Both target an explicit allocator address and carry one native penalty. ### Constants and conventions diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index 43c4401c5..12765464c 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -18,7 +18,7 @@ Instead of exposing the user directly to target contracts (ERC-4626 vault, Morph - executes `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral` on Morpho Blue on the user's behalf. Bundler3 also calls allocator contracts directly for shared liquidity: `reallocateTo` on Public -Allocator V1 and `reallocate` or `allocateFromIdle` on Blue Public Allocator V2. +Allocator V1 and `reallocate` or `allocateFromIdle` on Blue Public Allocator. The **spender** of every approval / permit / permit2 is therefore **always** `generalAdapter1`, never the vault or Morpho directly. See [src/actions/requirements/getRequirements.ts](src/actions/requirements/getRequirements.ts) and the "Requirements System" section of [ARCHITECTURE.md](ARCHITECTURE.md#requirements-system). @@ -26,11 +26,11 @@ The **spender** of every approval / permit / permit2 is therefore **always** `ge The value of the Bundler3 + GeneralAdapter1 pairing rests on three properties: -1. **Composition of elementary actions.** Each step (`nativeTransfer`, `wrapNative`, `erc20TransferFrom`, `permit`, `approve2`, `transferFrom2`, `erc4626Deposit`, `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral`, `reallocateTo`, `bluePublicAllocatorV2Reallocate`, `bluePublicAllocatorV2AllocateFromIdle`) is an independent building block. The SDK **composes** them in an explicit order to build a business flow. +1. **Composition of elementary actions.** Each step (`nativeTransfer`, `wrapNative`, `erc20TransferFrom`, `permit`, `approve2`, `transferFrom2`, `erc4626Deposit`, `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral`, `reallocateTo`, `bluePublicAllocatorReallocate`, `bluePublicAllocatorAllocateFromIdle`) is an independent building block. The SDK **composes** them in an explicit order to build a business flow. 2. **Atomicity.** The entire bundle either succeeds or reverts as one. No intermediate state is exposed to MEV bots or other transactions. 3. **Simplified approval UX.** A user approves _a single spender_ (GeneralAdapter1) for the entire protocol surface — rather than one approval per V1/V2 vault or per Morpho contract. -Concretely, `blueSupplyCollateralBorrow` is not a new contract: it is simply the composition `erc20TransferFrom` + `morphoSupplyCollateral` + `morphoBorrow` inside a single bundle. Same story for `repayWithdrawCollateral`, or for a borrow that must first trigger Public Allocator V1 or V2 calls. The business logic lives in the **order and selection of actions**, not in a dedicated contract. +Concretely, `blueSupplyCollateralBorrow` is not a new contract: it is simply the composition `erc20TransferFrom` + `morphoSupplyCollateral` + `morphoBorrow` inside a single bundle. Same story for `repayWithdrawCollateral`, or for a borrow that must first trigger Public Allocator V1 or Blue Public Allocator calls. The business logic lives in the **order and selection of actions**, not in a dedicated contract. ## Flows overview @@ -68,11 +68,13 @@ For every ERC-4626 deposit (VaultV1 / VaultV2), GeneralAdapter1 calls `erc4626De ### 4. Shared liquidity without an ad-hoc contract -`BlueReallocation`s encode as Public Allocator V1 `reallocateTo` calls or Blue Public Allocator V2 -`reallocate`/`allocateFromIdle` calls. They are **prepended to the bundle** (borrow and withdraw) or -**inserted between supply-collateral and borrow** (`supplyCollateralBorrow`). -`BundlerAction.encodeBundle` aggregates V1 fees and V2 native penalties into `tx.value`. No extra -off-chain machinery is required: everything flows through the same bundler-action composition. +`BlueReallocation`s encode as Public Allocator V1 `reallocateTo` calls or Blue Public Allocator +`reallocate`/`allocateFromIdle` calls. The same array may contain both, so Vault V1 and Vault V2 +liquidity can be reallocated atomically in one Bundler3 transaction. They are **prepended to the +bundle** (borrow and withdraw) or **inserted between supply-collateral and borrow** +(`supplyCollateralBorrow`). `BundlerAction.encodeBundle` aggregates Public Allocator V1 fees and +Blue Public Allocator native penalties into `tx.value`. No extra off-chain machinery is required: +everything flows through the same bundler-action composition. ### 5. A single approval surface @@ -107,7 +109,7 @@ This is the main design caveat. For the following operations the SDK emits a **d - **Blue authorization for GA1 required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`.** A user who has never granted it will receive a requirement through [`getBlueAuthorizationRequirement`](src/actions/requirements/blue/getBlueAuthorizationRequirement.ts). Without signature support, this is a `setAuthorization` transaction to execute beforehand. With `supportSignature`, this is a signable requirement; pass the resulting `AuthorizationRequirementSignature` to `buildTx`, which folds it into the bundle as `setAuthorizationWithSig`. - **Critical order in `repayWithdrawCollateral`**: `morphoRepay` **must** precede `morphoWithdrawCollateral` in the bundle, otherwise the position is deemed unhealthy at withdraw time and the tx reverts. - **Builder must equal signer.** Bundler actions reference accounts in two different ways: some take an explicit `onBehalf` and act on `userAddress` (e.g. `morphoRepay`), others act implicitly on the **initiator** — the `msg.sender` of `bundler3.multicall`, i.e. the EOA signing the tx, not the adapter — (e.g. `erc20TransferFrom`, `morphoWithdrawCollateral`, the latter exposing no `onBehalf` parameter on GA1). `repayWithdrawCollateral` is the canonical example: the repay leg targets `userAddress` while the transfer-from and the withdraw target the initiator. If the address that built the tx (and filled `userAddress`) is not the address that signs/executes it, the bundle would repay one account's debt while pulling tokens from and withdrawing collateral against the signer. Transaction builders do not validate this at build time — callers MUST keep `userAddress` aligned with the signing account. The signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) take a `WalletClient` and enforce this at `sign()` time via `validateUserAddress` (throws `MissingClientPropertyError` / `AddressMismatchError`). -- **Tricky `tx.value`**: whenever a `nativeAmount`, V1 `reallocateTo` fee, or V2 native penalty is involved, `BundlerAction.encodeBundle` computes `tx.value`. Do not overwrite it on the caller side. +- **Tricky `tx.value`**: whenever a `nativeAmount`, Public Allocator V1 `reallocateTo` fee, or Blue Public Allocator native penalty is involved, `BundlerAction.encodeBundle` computes `tx.value`. Do not overwrite it on the caller side. - **Chain-specific Bundler3 address**: always resolve through `getChainAddresses(chainId)` and validate that the viem client's `chainId` matches the params. ## Code references diff --git a/packages/morpho-sdk/README.md b/packages/morpho-sdk/README.md index f40a07dad..2ce2e8888 100644 --- a/packages/morpho-sdk/README.md +++ b/packages/morpho-sdk/README.md @@ -240,7 +240,7 @@ graph LR M1RF -->|allocator reallocation? + supplyCollateral callback: borrow + repay + withdrawCollateral| B3 B3 -.->|reallocateTo| PA1[PublicAllocator V1] - B3 -.->|reallocate / allocateFromIdle| PA2[Blue Public Allocator V2] + B3 -.->|reallocate / allocateFromIdle| BPA[Blue Public Allocator] end subgraph Midnight Flow diff --git a/packages/morpho-sdk/src/abis.ts b/packages/morpho-sdk/src/abis.ts index 52c5dee9c..524b15731 100644 --- a/packages/morpho-sdk/src/abis.ts +++ b/packages/morpho-sdk/src/abis.ts @@ -5,7 +5,7 @@ export { adaptiveCurveIrmAbi, blueAbi, blueOracleAbi, - bluePublicAllocatorV2Abi, + bluePublicAllocatorAbi, erc2612Abi, erc5267Abi, metaMorphoAbi, diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index 6ade28c0a..306904bd7 100644 --- a/packages/morpho-sdk/src/actions/AGENTS.md +++ b/packages/morpho-sdk/src/actions/AGENTS.md @@ -24,7 +24,7 @@ Only valid for assets/collateral configured as wNative. When `nativeAmount > 0`: ## Shared liquidity / reallocations (canonical statement) -`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action. Tagged `BluePublicAllocatorV2Reallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call pays its own `nativePenalty`. V2 sources are not sorted and idle uses no synthetic zero-address market. `BundlerAction.encodeBundle` sums V1 fees and V2 penalties into `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level or V2-source discriminators. +`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action. Tagged `BluePublicAllocatorReallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call pays its own `nativePenalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. `BundlerAction.encodeBundle` sums V1 fees and BluePublicAllocator penalties into `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level or BluePublicAllocator-source discriminators. ## Discriminated unions diff --git a/packages/morpho-sdk/src/actions/blue/AGENTS.md b/packages/morpho-sdk/src/actions/blue/AGENTS.md index 4a5d3dbcf..febb29b49 100644 --- a/packages/morpho-sdk/src/actions/blue/AGENTS.md +++ b/packages/morpho-sdk/src/actions/blue/AGENTS.md @@ -38,9 +38,10 @@ ERC-20 approval spender is **GeneralAdapter1** for any bundled path — never th | `withdraw` | `morphoWithdraw` | | `withdraw` (with reallocations) | `[allocator reallocation × N] → morphoWithdraw` | -An allocator reallocation is V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` according to -the `BlueReallocation` discriminator. `BundlerAction.encodeBundle` derives `tx.value` from native -wrapping calls, V1 fees, and V2 native penalties. +An allocator reallocation is PublicAllocator V1 `reallocateTo` or BluePublicAllocator +`reallocate`/`allocateFromIdle` according to the `BlueReallocation` discriminator. One bundle may +mix both allocator contracts. `BundlerAction.encodeBundle` derives `tx.value` from native wrapping +calls, PublicAllocator V1 fees, and BluePublicAllocator native penalties. ## Mode and ordering rules diff --git a/packages/morpho-sdk/src/actions/blue/borrow.publicAllocatorV2.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts similarity index 78% rename from packages/morpho-sdk/src/actions/blue/borrow.publicAllocatorV2.test.ts rename to packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts index 607e35678..4fa35a8f6 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.publicAllocatorV2.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -1,20 +1,22 @@ import { ChainId, MarketParams } from "@morpho-org/blue-sdk"; -import { bluePublicAllocatorV2Abi as canonicalBluePublicAllocatorV2Abi } from "@morpho-org/blue-sdk-viem"; +import { bluePublicAllocatorAbi as canonicalBluePublicAllocatorAbi } from "@morpho-org/blue-sdk-viem"; import { decodeFunctionData } from "viem"; import { describe, expect, test } from "vitest"; import { - bluePublicAllocatorV2Abi, + bluePublicAllocatorAbi, bundler3Abi, generalAdapter1Abi, + publicAllocatorAbi, } from "../../abis.js"; import type { BlueReallocation } from "../../types/index.js"; import { blueBorrow } from "./borrow.js"; const allocator = "0x0000000000000000000000000000000000000011"; -const vault = "0x0000000000000000000000000000000000000012"; +const vaultV1 = "0x0000000000000000000000000000000000000012"; const sourceAdapter = "0x0000000000000000000000000000000000000013"; const targetAdapter = "0x0000000000000000000000000000000000000014"; const receiver = "0x0000000000000000000000000000000000000015"; +const vaultV2 = "0x0000000000000000000000000000000000000016"; const targetMarket = new MarketParams({ loanToken: "0x0000000000000000000000000000000000000021", @@ -32,19 +34,19 @@ const sourceMarket = new MarketParams({ lltv: targetMarket.lltv, }); -describe("blueBorrow Public Allocator V2", () => { +describe("blueBorrow Blue Public Allocator", () => { test("default", () => { const reallocations: readonly BlueReallocation[] = [ { type: "publicAllocatorV1", - vault, + vault: vaultV1, fee: 2n, withdrawals: [{ marketParams: sourceMarket, amount: 1n }], }, { - type: "publicAllocatorV2", + type: "bluePublicAllocator", allocator, - vault, + vault: vaultV2, from: { type: "market", adapter: sourceAdapter, @@ -55,9 +57,9 @@ describe("blueBorrow Public Allocator V2", () => { nativePenalty: 5n, }, { - type: "publicAllocatorV2", + type: "bluePublicAllocator", allocator, - vault, + vault: vaultV2, from: { type: "idle" }, to: { adapter: targetAdapter }, assets: 7n, @@ -88,19 +90,25 @@ describe("blueBorrow Public Allocator V2", () => { false, ]); + const publicAllocatorCall = decodeFunctionData({ + abi: publicAllocatorAbi, + data: calls[0]!.data, + }); + expect(publicAllocatorCall.functionName).toBe("reallocateTo"); + expect(publicAllocatorCall.args[0]).toBe(vaultV1); expect( decodeFunctionData({ - abi: bluePublicAllocatorV2Abi, + abi: bluePublicAllocatorAbi, data: calls[1]!.data, }).functionName, ).toBe("reallocate"); const idleCall = decodeFunctionData({ - abi: bluePublicAllocatorV2Abi, + abi: bluePublicAllocatorAbi, data: calls[2]!.data, }); expect(idleCall.functionName).toBe("allocateFromIdle"); - expect(idleCall.args[0]).toBe(vault); + expect(idleCall.args[0]).toBe(vaultV2); expect(idleCall.args[1]).toBe(targetAdapter); expect(idleCall.args[2]).toMatchObject({ loanToken: targetMarket.loanToken, @@ -117,6 +125,6 @@ describe("blueBorrow Public Allocator V2", () => { }); test("re-exports the canonical ABI", () => { - expect(bluePublicAllocatorV2Abi).toBe(canonicalBluePublicAllocatorV2Abi); + expect(bluePublicAllocatorAbi).toBe(canonicalBluePublicAllocatorAbi); }); }); diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 56aa2ab14..f98a4461a 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -4,23 +4,24 @@ import { validateReallocations } from "../../helpers/index.js"; import type { BlueReallocation } from "../../types/index.js"; /** - * Builds V1 and Blue Public Allocator V2 reallocation actions and computes their native cost. + * Builds Public Allocator V1 and Blue Public Allocator actions and computes their native cost. * - * V1 entries preserve their `reallocateTo` ABI and validation. Each V2 entry maps 1:1 to either - * `reallocate` for a market source or `allocateFromIdle` for an idle source. The enclosing Blue - * action supplies the target market parameters. + * PublicAllocator V1 entries preserve their `reallocateTo` ABI and validation. Each + * BluePublicAllocator entry maps 1:1 to either `reallocate` for a market source or + * `allocateFromIdle` for an idle source. The enclosing Blue action supplies the target market + * parameters. * - * @param reallocations - V1 and V2 reallocations in execution order. + * @param reallocations - PublicAllocator V1 and BluePublicAllocator reallocations in execution order. * @param targetMarketParams - Target market params derived from the enclosing Blue action. - * @returns Encoded actions and the sum of V1 fees plus V2 native penalties. - * @throws {NegativeInputError} when a V1 fee or V2 native penalty is negative. - * @throws {EmptyReallocationWithdrawalsError} when a V1 reallocation has no withdrawals. - * @throws {NonPositiveInputError} when a V1 withdrawal or V2 asset amount is non-positive. - * @throws {InputExceedsMaxError} when a V2 asset amount exceeds `uint128`. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @returns Encoded actions and the sum of PublicAllocator V1 fees plus BluePublicAllocator native penalties. + * @throws {NegativeInputError} when a PublicAllocator V1 fee or BluePublicAllocator native penalty is negative. + * @throws {EmptyReallocationWithdrawalsError} when a PublicAllocator V1 reallocation has no withdrawals. + * @throws {NonPositiveInputError} when a PublicAllocator V1 withdrawal or BluePublicAllocator asset amount is non-positive. + * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. - * @throws {UnsortedReallocationWithdrawalsError} when V1 withdrawals are not strictly market-id sorted. + * @throws {UnsortedReallocationWithdrawalsError} when PublicAllocator V1 withdrawals are not strictly market-id sorted. * @internal */ export const buildReallocationActions = ( @@ -33,10 +34,10 @@ export const buildReallocationActions = ( const actions: Action[] = []; for (const reallocation of reallocations) { - if (reallocation.type === "publicAllocatorV2") { + if (reallocation.type === "bluePublicAllocator") { if (reallocation.from.type === "market") { actions.push({ - type: "bluePublicAllocatorV2Reallocate", + type: "bluePublicAllocatorReallocate", args: [ reallocation.allocator, reallocation.vault, @@ -51,7 +52,7 @@ export const buildReallocationActions = ( }); } else { actions.push({ - type: "bluePublicAllocatorV2AllocateFromIdle", + type: "bluePublicAllocatorAllocateFromIdle", args: [ reallocation.allocator, reallocation.vault, diff --git a/packages/morpho-sdk/src/bundler/actions.test.ts b/packages/morpho-sdk/src/bundler/actions.test.ts index 5706d3d3a..49ecf7030 100644 --- a/packages/morpho-sdk/src/bundler/actions.test.ts +++ b/packages/morpho-sdk/src/bundler/actions.test.ts @@ -5,7 +5,7 @@ import { } from "@morpho-org/blue-sdk"; import { blueAbi, - bluePublicAllocatorV2Abi, + bluePublicAllocatorAbi, erc2612Abi, permit2Abi, publicAllocatorAbi, @@ -361,7 +361,7 @@ describe("BundlerAction", () => { .map( (args) => ({ - type: "bluePublicAllocatorV2Reallocate", + type: "bluePublicAllocatorReallocate", args, }) satisfies Action, ), @@ -378,7 +378,7 @@ describe("BundlerAction", () => { .map( (args) => ({ - type: "bluePublicAllocatorV2AllocateFromIdle", + type: "bluePublicAllocatorAllocateFromIdle", args, }) satisfies Action, ), @@ -610,10 +610,10 @@ describe("BundlerAction", () => { expect(calls[0]?.value).toBe(5n); }); - test("encodeBundle aggregates Blue Public Allocator V2 native penalties", () => { + test("encodeBundle aggregates Blue Public Allocator native penalties", () => { const tx = BundlerAction.encodeBundle(chainId, [ { - type: "bluePublicAllocatorV2Reallocate", + type: "bluePublicAllocatorReallocate", args: [ allocator, vault, @@ -627,7 +627,7 @@ describe("BundlerAction", () => { ], }, { - type: "bluePublicAllocatorV2AllocateFromIdle", + type: "bluePublicAllocatorAllocateFromIdle", args: [allocator, vault, allocateAdapter, market, 3n, 4n, false], }, ]); @@ -1042,9 +1042,9 @@ describe("BundlerAction", () => { ), ], [ - "bluePublicAllocatorV2Reallocate", + "bluePublicAllocatorReallocate", { - type: "bluePublicAllocatorV2Reallocate", + type: "bluePublicAllocatorReallocate", args: [ allocator, vault, @@ -1057,7 +1057,7 @@ describe("BundlerAction", () => { false, ], }, - BundlerAction.bluePublicAllocatorV2Reallocate( + BundlerAction.bluePublicAllocatorReallocate( allocator, vault, deallocateAdapter, @@ -1070,12 +1070,12 @@ describe("BundlerAction", () => { ), ], [ - "bluePublicAllocatorV2AllocateFromIdle", + "bluePublicAllocatorAllocateFromIdle", { - type: "bluePublicAllocatorV2AllocateFromIdle", + type: "bluePublicAllocatorAllocateFromIdle", args: [allocator, vault, allocateAdapter, market, 22n, 23n, false], }, - BundlerAction.bluePublicAllocatorV2AllocateFromIdle( + BundlerAction.bluePublicAllocatorAllocateFromIdle( allocator, vault, allocateAdapter, @@ -1558,9 +1558,9 @@ describe("BundlerAction", () => { expect(decoded.args).toEqual([vault, withdrawals, market]); }); - test("bluePublicAllocatorV2Reallocate", () => { + test("bluePublicAllocatorReallocate", () => { const call = onlyCall( - BundlerAction.bluePublicAllocatorV2Reallocate( + BundlerAction.bluePublicAllocatorReallocate( allocator, vault, deallocateAdapter, @@ -1573,7 +1573,7 @@ describe("BundlerAction", () => { ), ); const decoded = decodeFunctionData({ - abi: bluePublicAllocatorV2Abi, + abi: bluePublicAllocatorAbi, data: call.data, }); @@ -1591,9 +1591,9 @@ describe("BundlerAction", () => { ]); }); - test("bluePublicAllocatorV2AllocateFromIdle", () => { + test("bluePublicAllocatorAllocateFromIdle", () => { const call = onlyCall( - BundlerAction.bluePublicAllocatorV2AllocateFromIdle( + BundlerAction.bluePublicAllocatorAllocateFromIdle( allocator, vault, allocateAdapter, @@ -1604,7 +1604,7 @@ describe("BundlerAction", () => { ), ); const decoded = decodeFunctionData({ - abi: bluePublicAllocatorV2Abi, + abi: bluePublicAllocatorAbi, data: call.data, }); diff --git a/packages/morpho-sdk/src/bundler/actions.ts b/packages/morpho-sdk/src/bundler/actions.ts index e369f4c3a..3c96fbd18 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -4,7 +4,7 @@ import { } from "@morpho-org/blue-sdk"; import { blueAbi, - bluePublicAllocatorV2Abi, + bluePublicAllocatorAbi, erc2612Abi, permit2Abi, publicAllocatorAbi, @@ -343,11 +343,11 @@ export namespace BundlerAction { case "reallocateTo": { return BundlerAction.publicAllocatorReallocateTo(chainId, ...args); } - case "bluePublicAllocatorV2Reallocate": { - return BundlerAction.bluePublicAllocatorV2Reallocate(...args); + case "bluePublicAllocatorReallocate": { + return BundlerAction.bluePublicAllocatorReallocate(...args); } - case "bluePublicAllocatorV2AllocateFromIdle": { - return BundlerAction.bluePublicAllocatorV2AllocateFromIdle(...args); + case "bluePublicAllocatorAllocateFromIdle": { + return BundlerAction.bluePublicAllocatorAllocateFromIdle(...args); } case "wrapNative": { return BundlerAction.wrapNative(chainId, ...args); @@ -1449,9 +1449,9 @@ export namespace BundlerAction { } /** - * Encodes a Blue Public Allocator V2 market-to-market reallocation. + * Encodes a Blue Public Allocator market-to-market reallocation. * - * @param allocator - Explicit Blue Public Allocator V2 contract address. + * @param allocator - Explicit Blue Public Allocator contract address. * @param vault - Vault whose liquidity is reallocated. * @param deallocateAdapter - Vault V2 adapter supplying the source market. * @param deallocateMarket - Source Morpho Blue market parameters. @@ -1481,7 +1481,7 @@ export namespace BundlerAction { * collateralToken: "0x0000000000000000000000000000000000000009", * }; * - * const calls = BundlerAction.bluePublicAllocatorV2Reallocate( + * const calls = BundlerAction.bluePublicAllocatorReallocate( * allocator, * vault, * sourceAdapter, @@ -1495,7 +1495,7 @@ export namespace BundlerAction { * ``` */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call - export function bluePublicAllocatorV2Reallocate( + export function bluePublicAllocatorReallocate( allocator: Address, vault: Address, deallocateAdapter: Address, @@ -1510,7 +1510,7 @@ export namespace BundlerAction { { to: allocator, data: encodeFunctionData({ - abi: bluePublicAllocatorV2Abi, + abi: bluePublicAllocatorAbi, functionName: "reallocate", args: [ vault, @@ -1529,9 +1529,9 @@ export namespace BundlerAction { } /** - * Encodes a Blue Public Allocator V2 allocation from vault idle liquidity. + * Encodes a Blue Public Allocator allocation from vault idle liquidity. * - * @param allocator - Explicit Blue Public Allocator V2 contract address. + * @param allocator - Explicit Blue Public Allocator contract address. * @param vault - Vault whose idle liquidity is allocated. * @param adapter - Vault V2 adapter supplying the target market. * @param market - Target Morpho Blue market parameters. @@ -1554,7 +1554,7 @@ export namespace BundlerAction { * lltv: 860_000000000000000000n, * }; * - * const calls = BundlerAction.bluePublicAllocatorV2AllocateFromIdle( + * const calls = BundlerAction.bluePublicAllocatorAllocateFromIdle( * allocator, * vault, * targetAdapter, @@ -1566,7 +1566,7 @@ export namespace BundlerAction { * ``` */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call - export function bluePublicAllocatorV2AllocateFromIdle( + export function bluePublicAllocatorAllocateFromIdle( allocator: Address, vault: Address, adapter: Address, @@ -1579,7 +1579,7 @@ export namespace BundlerAction { { to: allocator, data: encodeFunctionData({ - abi: bluePublicAllocatorV2Abi, + abi: bluePublicAllocatorAbi, functionName: "allocateFromIdle", args: [vault, adapter, market, assets], }), diff --git a/packages/morpho-sdk/src/bundler/types.ts b/packages/morpho-sdk/src/bundler/types.ts index 3b6c965d3..22ded926a 100644 --- a/packages/morpho-sdk/src/bundler/types.ts +++ b/packages/morpho-sdk/src/bundler/types.ts @@ -210,8 +210,8 @@ export interface ActionArgs { skipRevert?: boolean, ]; - /** Blue Public Allocator V2 market-to-market reallocation with an explicit allocator address and native penalty. */ - readonly bluePublicAllocatorV2Reallocate: [ + /** Blue Public Allocator market-to-market reallocation with an explicit allocator address and native penalty. */ + readonly bluePublicAllocatorReallocate: [ allocator: Address, vault: Address, deallocateAdapter: Address, @@ -223,8 +223,8 @@ export interface ActionArgs { skipRevert?: boolean, ]; - /** Blue Public Allocator V2 idle-to-market allocation with an explicit allocator address and native penalty. */ - readonly bluePublicAllocatorV2AllocateFromIdle: [ + /** Blue Public Allocator idle-to-market allocation with an explicit allocator address and native penalty. */ + readonly bluePublicAllocatorAllocateFromIdle: [ allocator: Address, vault: Address, adapter: Address, diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index de321af59..385d92ef0 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -18,7 +18,7 @@ import { import { AccrualPositionUserMismatchError, AddressMismatchError, - type BluePublicAllocatorV2Reallocation, + type BluePublicAllocatorReallocation, type BlueReallocation, BorrowExceedsSafeLtvError, ChainIdMismatchError, @@ -559,15 +559,16 @@ describe("validateReallocations", () => { withdrawals: [{ marketParams: sourceMarketA, amount: 10n ** 18n }], }; - const validV2Reallocation: BluePublicAllocatorV2Reallocation = { - type: "publicAllocatorV2", - allocator: USER_A, - vault: USER_B, - from: { type: "idle" }, - to: { adapter: USER_A }, - assets: 1n, - nativePenalty: 0n, - }; + const validBluePublicAllocatorReallocation: BluePublicAllocatorReallocation = + { + type: "bluePublicAllocator", + allocator: USER_A, + vault: USER_B, + from: { type: "idle" }, + to: { adapter: USER_A }, + assets: 1n, + nativePenalty: 0n, + }; test("should pass with valid reallocations", () => { expect(() => @@ -584,46 +585,58 @@ describe("validateReallocations", () => { ).not.toThrow(); }); - test("behavior: accepts a valid V2 idle reallocation", () => { + test("behavior: accepts a valid Blue Public Allocator idle reallocation", () => { expect(() => - validateReallocations([validV2Reallocation], targetMarketId), + validateReallocations( + [validBluePublicAllocatorReallocation], + targetMarketId, + ), ).not.toThrow(); }); test.each([ { name: "negative native penalty", - reallocation: { ...validV2Reallocation, nativePenalty: -1n }, + reallocation: { + ...validBluePublicAllocatorReallocation, + nativePenalty: -1n, + }, ErrorClass: NegativeInputError, }, { name: "zero assets", - reallocation: { ...validV2Reallocation, assets: 0n }, + reallocation: { ...validBluePublicAllocatorReallocation, assets: 0n }, ErrorClass: NonPositiveInputError, }, { name: "uint128 asset overflow", - reallocation: { ...validV2Reallocation, assets: maxUint128 + 1n }, + reallocation: { + ...validBluePublicAllocatorReallocation, + assets: maxUint128 + 1n, + }, ErrorClass: InputExceedsMaxError, }, - ])("error: rejects V2 $name", ({ reallocation, ErrorClass }) => { - expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( - ErrorClass, - ); - }); + ])( + "error: rejects Blue Public Allocator $name", + ({ reallocation, ErrorClass }) => { + expect(() => + validateReallocations([reallocation], targetMarketId), + ).toThrow(ErrorClass); + }, + ); - test("error: ReallocationWithdrawalOnTargetMarketError for a V2 target-market source", () => { + test("error: ReallocationWithdrawalOnTargetMarketError for a Blue Public Allocator target-market source", () => { expect(() => validateReallocations( [ { - ...validV2Reallocation, + ...validBluePublicAllocatorReallocation, from: { type: "market", adapter: USER_A, marketParams, }, - } satisfies BluePublicAllocatorV2Reallocation, + } satisfies BluePublicAllocatorReallocation, ], targetMarketId, ), @@ -632,7 +645,7 @@ describe("validateReallocations", () => { test("error: InvalidReallocationSourceTypeError", () => { const reallocation = { - ...validV2Reallocation, + ...validBluePublicAllocatorReallocation, from: { type: "marketTypo" }, } as unknown as BlueReallocation; diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index ab35ab891..8271cfafc 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -326,7 +326,7 @@ export const validateRepayShares = (params: { }; /** - * Validates that Public Allocator V1 and Blue Public Allocator V2 reallocations are well-formed. + * Validates that Public Allocator V1 and Blue Public Allocator reallocations are well-formed. * * V1 entries preserve the following invariants: * - `fee` must be non-negative. @@ -335,17 +335,18 @@ export const validateRepayShares = (params: { * - No withdrawal may target `targetMarketId`. * - Withdrawal market IDs must be strictly ascending. * - * V2 entries enforce non-negative `nativePenalty`, positive `uint128`-bounded `assets`, and a - * market source distinct from `targetMarketId`. Idle sources have no market or sorting rule. + * BluePublicAllocator entries enforce non-negative `nativePenalty`, positive `uint128`-bounded + * `assets`, and a market source distinct from `targetMarketId`. Idle sources have no market or + * sorting rule. * * @param reallocations - The reallocations to validate. * @param targetMarketId - The ID of the operation's target market. No withdrawal may reference this market. * @returns Nothing when every reallocation is valid. * @throws {NegativeInputError} when a reallocation fee is negative. * @throws {EmptyReallocationWithdrawalsError} when a reallocation has no withdrawals. - * @throws {NonPositiveInputError} when a withdrawal or V2 asset amount is non-positive. - * @throws {InputExceedsMaxError} when a V2 asset amount exceeds `uint128`. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {NonPositiveInputError} when a withdrawal or BluePublicAllocator asset amount is non-positive. + * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128`. + * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when withdrawals are not strictly market-id sorted. @@ -363,7 +364,7 @@ export const validateReallocations = ( targetMarketId: MarketId, ): void => { for (const r of reallocations) { - if (r.type === "publicAllocatorV2") { + if (r.type === "bluePublicAllocator") { const sourceType: string = r.from.type; if (sourceType !== "market" && sourceType !== "idle") { throw new InvalidReallocationSourceTypeError(sourceType); diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index ee10ef5ae..c5ad9cdcb 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -19,16 +19,16 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. ## Shared liquidity (`sharedLiquidity.ts`) - `VaultReallocation` — legacy-untagged or explicitly `publicAllocatorV1` vault address + fee + sorted withdrawals; maps to `reallocateTo()`. -- `BluePublicAllocatorV2Reallocation` — tagged V2 allocator/vault/source/target-adapter/assets/native-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. +- `BluePublicAllocatorReallocation` — tagged BluePublicAllocator/vault/source/target-adapter/assets/native-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. - `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. ## Errors (`error.ts`) One class per error case. Never throw a generic `Error` from SDK source. -- **Generic input bounds:** `NegativeInputError` for values that must be non-negative, `NonPositiveInputError` for values that must be positive, and `InputExceedsMaxError` for protocol-width upper bounds such as Public Allocator V2's `uint128` assets. All expose the invalid `field` and `value`; reuse them across Vault, Blue, and Midnight instead of adding operation-specific scalar-bound errors. +- **Generic input bounds:** `NegativeInputError` for values that must be non-negative, `NonPositiveInputError` for values that must be positive, and `InputExceedsMaxError` for protocol-width upper bounds such as BluePublicAllocator's `uint128` assets. All expose the invalid `field` and `value`; reuse them across Vault, Blue, and Midnight instead of adding operation-specific scalar-bound errors. - **Market-specific:** `BorrowExceedsSafeLtvError`, `MissingMarketPriceError`, `NativeAmountOnNonWNativeAssetError`, `MutuallyExclusiveWithdrawAmountsError`, `WithdrawExceedsSupplyError`, `WithdrawSharesExceedSupplyError`. -- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationTypeError` for an unknown top-level Public Allocator variant, `InvalidReallocationSourceTypeError` for an unknown V2 source, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. +- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationTypeError` for an unknown top-level Public Allocator variant, `InvalidReallocationSourceTypeError` for an unknown BluePublicAllocator source, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. ## Adding a new operation diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 062ac344d..814187e77 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -816,15 +816,15 @@ export class InvalidReallocationTypeError extends Error { public constructor(public readonly reallocationType: string | undefined) { super( reallocationType === undefined - ? 'Reallocation must be an untagged Public Allocator V1 entry with "withdrawals" or specify type "publicAllocatorV1" or "publicAllocatorV2".' - : `Reallocation type must be "publicAllocatorV1" or "publicAllocatorV2", got "${reallocationType}".`, + ? 'Reallocation must be an untagged Public Allocator V1 entry with "withdrawals" or specify type "publicAllocatorV1" or "bluePublicAllocator".' + : `Reallocation type must be "publicAllocatorV1" or "bluePublicAllocator", got "${reallocationType}".`, ); this.name = "InvalidReallocationTypeError"; } } /** - * Thrown when a Blue Public Allocator V2 source has an unknown discriminator. + * Thrown when a Blue Public Allocator source has an unknown discriminator. * * @example * ```ts diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 960ce46b0..494afc5aa 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -83,8 +83,8 @@ export interface VaultReallocation { readonly withdrawals: readonly ReallocationWithdrawal[]; } -/** Source of a Blue Public Allocator V2 reallocation. */ -export type BluePublicAllocatorV2Source = +/** Source of a Blue Public Allocator reallocation. */ +export type BluePublicAllocatorSource = | { /** Reallocate from a Morpho Blue market. */ readonly type: "market"; @@ -99,19 +99,19 @@ export type BluePublicAllocatorV2Source = }; /** - * One Blue Public Allocator V2 contract call performed before a Blue action. + * One Blue Public Allocator contract call performed before a Blue action. * * The target market parameters are derived from the enclosing Blue action. */ -export interface BluePublicAllocatorV2Reallocation { - /** Explicit allocator contract address because V2 has no deployment registry entry. */ +export interface BluePublicAllocatorReallocation { + /** Explicit allocator contract address because BluePublicAllocator has no deployment registry entry. */ readonly allocator: Address; - /** Discriminator separating V2 reallocations from untagged V1 reallocations. */ - readonly type: "publicAllocatorV2"; + /** Discriminator separating BluePublicAllocator reallocations from PublicAllocator V1 reallocations. */ + readonly type: "bluePublicAllocator"; /** Vault whose liquidity is moved. */ readonly vault: Address; /** Liquidity source. */ - readonly from: BluePublicAllocatorV2Source; + readonly from: BluePublicAllocatorSource; /** Target Vault V2 adapter; the target market comes from the enclosing action. */ readonly to: { readonly adapter: Address }; /** Asset amount, which must fit in `uint128`. */ @@ -121,14 +121,15 @@ export interface BluePublicAllocatorV2Reallocation { } /** - * Reallocation accepted by Blue actions that support Public Allocator V1 or V2. + * Reallocation accepted by Blue actions that support PublicAllocator V1 or BluePublicAllocator. * * V1 entries remain valid without a `type` field and may optionally use - * `type: "publicAllocatorV1"`; V2 entries use `type: "publicAllocatorV2"`. + * `type: "publicAllocatorV1"`; Blue Public Allocator entries use + * `type: "bluePublicAllocator"`. */ export type BlueReallocation = | VaultReallocation - | BluePublicAllocatorV2Reallocation; + | BluePublicAllocatorReallocation; /** * Options for computing vault reallocations via the public allocator. diff --git a/packages/morpho-ts/src/abis.ts b/packages/morpho-ts/src/abis.ts index 117583f3e..71a262f0f 100644 --- a/packages/morpho-ts/src/abis.ts +++ b/packages/morpho-ts/src/abis.ts @@ -4595,8 +4595,8 @@ export const publicAllocatorAbi = [ }, ] as const; -/** Blue Public Allocator V2 ABI used for market and idle reallocations. */ -export const bluePublicAllocatorV2Abi = [ +/** Blue Public Allocator ABI used for market and idle reallocations. */ +export const bluePublicAllocatorAbi = [ { inputs: [ { From e75a1d89c5b14bfc6633602955595e0c9684def4 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Thu, 6 Aug 2026 16:00:21 +0200 Subject: [PATCH 06/41] feat: add Vault V2 shared liquidity support --- .changeset/brave-vaults-reallocate.md | 3 +- ...lt-v2-public-allocator-shared-liquidity.md | 857 +++++++--------- packages/blue-sdk-viem/AGENTS.md | 1 + .../GetVaultV2PublicAllocatorConfig.sol | 68 ++ .../interfaces/IBluePublicAllocator.sol | 13 + packages/blue-sdk-viem/package.json | 2 +- .../VaultV2PublicAllocatorConfig.test.ts | 216 ++++ .../vault-v2/VaultV2PublicAllocatorConfig.ts | 296 ++++++ .../blue-sdk-viem/src/fetch/vault-v2/index.ts | 1 + .../GetVaultV2PublicAllocatorConfig.ts | 124 +++ packages/blue-sdk/AGENTS.md | 2 + packages/blue-sdk/src/vault/v2/VaultV2.ts | 30 +- .../v2/VaultV2MorphoMarketV1AdapterV2.ts | 10 +- .../vault/v2/VaultV2PublicAllocatorConfig.ts | 31 + .../src/vault/v2/VaultV2Utils.test.ts | 60 ++ .../blue-sdk/src/vault/v2/VaultV2Utils.ts | 54 + packages/blue-sdk/src/vault/v2/index.ts | 2 + packages/morpho-sdk/AGENTS.md | 1 + packages/morpho-sdk/src/actions/AGENTS.md | 2 +- packages/morpho-sdk/src/entities/blue/blue.ts | 8 +- packages/morpho-sdk/src/entities/index.ts | 4 + .../entities/reallocationData.metrics.test.ts | 4 +- .../src/entities/reallocationData.test.ts | 20 + .../src/entities/reallocationData.ts | 25 +- .../entities/reallocationDataVaultV2.test.ts | 626 ++++++++++++ .../src/entities/reallocationDataVaultV2.ts | 923 ++++++++++++++++++ .../src/helpers/computeReallocations.test.ts | 6 +- .../src/helpers/computeReallocations.ts | 10 +- .../helpers/computeReallocationsVaultV2.ts | 138 +++ packages/morpho-sdk/src/helpers/index.ts | 1 + .../morpho-sdk/src/helpers/validate.test.ts | 41 +- packages/morpho-sdk/src/helpers/validate.ts | 3 +- packages/morpho-sdk/src/types/AGENTS.md | 4 +- packages/morpho-sdk/src/types/error.ts | 87 +- .../morpho-sdk/src/types/sharedLiquidity.ts | 30 +- packages/morpho-sdk/src/utils.ts | 2 + packages/morpho-ts/src/abis.ts | 101 ++ .../src/morpho-protocol-evm.ts | 2 +- 38 files changed, 3227 insertions(+), 581 deletions(-) create mode 100644 packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol create mode 100644 packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol create mode 100644 packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts create mode 100644 packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts create mode 100644 packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2Utils.test.ts create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2Utils.ts create mode 100644 packages/morpho-sdk/src/entities/reallocationDataVaultV2.test.ts create mode 100644 packages/morpho-sdk/src/entities/reallocationDataVaultV2.ts create mode 100644 packages/morpho-sdk/src/helpers/computeReallocationsVaultV2.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index e23edacf6..9d681360e 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -1,7 +1,8 @@ --- "@morpho-org/morpho-ts": minor +"@morpho-org/blue-sdk": minor "@morpho-org/blue-sdk-viem": minor "@morpho-org/morpho-sdk": minor --- -Add the canonical Blue Public Allocator ABI to `morpho-ts`, re-export it from `blue-sdk-viem` and `morpho-sdk`, and expose market and idle reallocations through Blue borrow, supply-collateral-borrow, loan-asset withdraw, and refinance flows while preserving PublicAllocator V1 inputs. +Add the canonical Blue Public Allocator ABI to `morpho-ts`; add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`; add explicit-allocator deployless and fallback reads to `blue-sdk-viem`; and expose Vault V2 shared-liquidity discovery, planning, metrics, and flat market/idle reallocations through `morpho-sdk` Blue flows. Canonicalize the V1 names as `computeVaultV1Reallocations` and `VaultV1BlueReallocation` while preserving their deprecated aliases. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index ece5bdbcf..4bc7003f9 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -1,576 +1,407 @@ # TIB-2026-07-29: Vault V2 public-allocator shared liquidity -| Field | Value | -| ---------- | ---------------------------------------------------------------------------- | -| **Status** | Proposed | -| **Date** | 2026-07-29 | -| **Author** | @foulques | -| **Scope** | Packages: `morpho-sdk`, `blue-sdk-viem`, `blue-sdk`, `morpho-ts` | - ---- +| Field | Value | +| ---------- | ---------------------------------------------------------------- | +| **Status** | Accepted | +| **Date** | 2026-07-29 | +| **Author** | @foulques | +| **Scope** | `morpho-sdk`, `blue-sdk-viem`, `blue-sdk`, and `morpho-ts` | ## Context -Integrators (frontends, allocators, risk dashboards) ask one question of a Morpho Blue market: -**"how much more can be borrowed here, counting liquidity a public allocator could pull in?"** - -Today the SDK answers that for **MetaMorpho Vault V1 only**. The engine lives in `morpho-sdk`: +The SDK already models liquidity that a MetaMorpho Vault V1 can move into a +Morpho Blue market through PublicAllocator V1: +```text +MorphoBlue.getReallocationData() + → ReallocationData.computeVaultV1Reallocations() # discovery + → computeReallocations() # borrow/withdraw planner + → VaultV1BlueReallocation[] + → PublicAllocator.reallocateTo(...) ``` -MorphoBlue.getReallocationData() → ReallocationData (entity, state container) - ↓ getMarketPublicReallocations() (greedy discovery) - computeReallocations() (helper, transactional planner) - ↓ - VaultReallocation[] → PublicAllocator.reallocateTo(...) -``` -plus two read-only metrics on the entity (`getPublicReallocationLiquidity`, -`getAvailableLiquidityToUtilization`, frozen in -[TIB-2026-06-16](./TIB-2026-06-16-shared-liquidity-target-utilization-metric.md)) and a -state-independent validator (`validateReallocations`). - -`morpho-org/vault-v2` has since shipped **`BluePublicAllocator`** -(`src/periphery/blue-public-allocator/BluePublicAllocator.sol`, last touched -`b41782590d3d33d8d836aedd233aaa72ac8b2aa2`, 2026-07-29) — the Vault V2 counterpart. It lets anyone -move a Vault V2's liquidity between Morpho Blue markets through `MorphoMarketV1AdapterV2` adapters, -or out of the vault's idle balance, in exchange for a per-call native penalty. - -The SDK has **no** surface for it: no ABI, no address key, no entity, no compute. As Vault V2 TVL -grows, a Blue market's reallocatable liquidity increasingly sits behind Vault V2 vaults, and the -V1-only engine silently under-reports it — a borrow that would succeed is quoted as impossible. - -This TIB freezes the design of the Vault V2 mirror. - -## Goals / Non-Goals - -**Goals** - -- Mirror the V1 shared-liquidity engine for Vault V2, named with a `VaultV2` **suffix**: - `computeReallocationsVaultV2`, `validateReallocationsVaultV2`, `ReallocationDataVaultV2` and its - three public methods. -- Model `BluePublicAllocator` faithfully: post-state `absoluteCap`, boolean `canDeallocate`, - `isActiveAdapter`, per-call `nativePenalty`, and the V2-only `allocateFromIdle` source. -- **Full-parity cap simulation.** Never emit a fee-bearing plan that reverts on-chain: simulate the - allocator's own cap *and* the vault's absolute + relative caps on all three allocation ids. -- Reuse the existing constants (`DEFAULT_WITHDRAWAL_TARGET_UTILIZATION`, - `DEFAULT_SUPPLY_TARGET_UTILIZATION`) and the existing `blue-sdk` id derivation - (`VaultV2MorphoMarketV1AdapterV2.ids`). No duplicated formula, no duplicated constant. - -**Non-Goals** - -- **No change to the V1 surface.** Nothing is renamed, deprecated, or deleted. `ReallocationData`, - `computeReallocations`, `validateReallocations` and every V1 type stay byte-identical. This is - purely additive — there is no breaking change and no migration. -- **No calldata.** No action, no bundler encoder, no `Transaction`. The engine returns a plan; it - does not encode one. -- **No entity wiring.** `MorphoBlue` and `MorphoVaultV2` are not modified. `MorphoBlue.getReallocationData()` - keeps its exact current behavior. Dispatching between V1 and V2 — and folding V2 reallocations into - `borrow` / `supplyCollateralBorrow` / `withdraw` / `refinance` — lands in the **blue bundles - migration** project, in its own PR. -- No combined V1+V2 liquidity metric (needs the entity dispatch above). -- No curator-facing setters (`setAbsoluteCap`, `setCanDeallocate`, `setIsActiveAdapter`, - `setNativePenalty`, `claimNativePenalty`). Reads only — matching V1, which never exposed - `setFlowCaps`. -- No `liquidity-sdk-viem` change; its `LiquidityLoader` stays V1-only. - -## Current Solution - -`ReallocationData` (`packages/morpho-sdk/src/entities/reallocationData.ts`) holds four maps — -`markets`, `vaults` (MetaMorpho `Vault`), `positions[vault][marketId]`, -`vaultMarketConfigs[vault][marketId]` — and drives a greedy loop. Per `(vault, sourceMarket)` -candidate it computes: -```ts -assets = min( - srcPosition.supplyAssets, - srcPosition.market.getWithdrawToUtilization(ceiling), // 90%, then 100% in phase 2 - suppliable, // target vault cap headroom, pending-cap aware - vaultPublicAllocatorConfig.maxIn ?? 0n, // flow cap IN, target market - srcConfig.publicAllocatorConfig.maxOut ?? 0n, // flow cap OUT, source market -) -``` +The historical names `getMarketPublicReallocations()` and +`VaultReallocation` remain as deprecated aliases for the prescribed +deprecation window. + +Vault V2 has a distinct `BluePublicAllocator` that can move one source market +or the vault's idle assets into one target Morpho market per call. Its cap and +accounting model is different enough that it needs a separate state simulator, +but the resulting calls can use the Blue action and Bundler3 integration that +already exists in this branch. + +This TIB freezes that Vault V2 design. + +## Goals + +- Add `ReallocationDataVaultV2.computeVaultV2Reallocations(...)` for greedy, + largest-first discovery. +- Add `computeReallocationsVaultV2(...)` for amount-aware borrow and withdraw + planning. +- Return flat, action-ready `VaultV2BlueReallocation[]`; one entry is exactly + one `reallocate(...)` or `allocateFromIdle(...)` call and pays one + `nativePenalty`. +- Keep `computeReallocations(...)` as the Vault V1 planner and make the + versioned V1 discovery/type names canonical. +- Simulate the allocator target cap, all three Vault V2 allocation caps, + source Blue liquidity and utilization, shared allocation IDs, untracked + interest, adapter permissions, idle liquidity, and `uint128` bounds. +- Reuse one combined `validateReallocations(...)` for the action-ready V1/V2 + union. + +## Non-goals + +- No BluePublicAllocator address registry entry. The allocator contract is an + explicit input to fetchers, state, and every returned call. +- No curator-facing setters such as `setAbsoluteCap`, `setCanDeallocate`, or + `setNativePenalty`. +- No `liquidity-sdk-viem` release. Its use of the deprecated V1 discovery + alias remains source-compatible. +- No penalty-efficiency optimizer. Candidates are ranked by obtainable assets. + +## Public API and naming + +| Concern | Canonical symbol | +| --- | --- | +| V1 discovery | `ReallocationData.computeVaultV1Reallocations(marketId, options?)` | +| V1 discovery compatibility | `ReallocationData.getMarketPublicReallocations(...)` (`@deprecated`) | +| V1 planner | `computeReallocations(...)` | +| V1 action input | `VaultV1BlueReallocation` | +| V1 type compatibility | `VaultReallocation` (`@deprecated` alias) | +| V2 state | `ReallocationDataVaultV2` / `InputReallocationDataVaultV2` | +| V2 discovery | `ReallocationDataVaultV2.computeVaultV2Reallocations(marketId, options?)` | +| V2 planner | `computeReallocationsVaultV2(...)` | +| V2 action input | `VaultV2BlueReallocation` | +| Shared action union | `BlueReallocation` | +| V2 options | `PublicAllocatorOptionsVaultV2`, `ReallocationComputeOptionsVaultV2` | +| V2 config | `VaultV2PublicAllocatorConfig`, `VaultV2MarketPublicAllocatorConfig` | +| Fetchers | `fetchVaultV2PublicAllocatorConfig`, `fetchVaultV2MarketPublicAllocatorConfig`, `fetchVaultV2PublicAllocatorData` | -then applies the largest candidate to a cloned state, flipping `maxIn`/`maxOut` and accruing the -vault fee, and repeats. `computeReallocations` wraps it in a two-phase planner (friendly at 90%, -aggressive at 100%), groups withdrawals by vault, caps them to the required amount, and emits -`VaultReallocation { vault, fee, withdrawals }` — one entry per `reallocateTo` call. +`BluePublicAllocatorReallocation` was unreleased relative to `origin/main` and +is renamed directly to `VaultV2BlueReallocation`; it has no compatibility +alias. -Nothing in that model transfers directly, because the V2 contract's cap model is structurally -different (see below). +The action-ready V2 shape is flat: -## Proposed Solution +```ts +export type BluePublicAllocatorSource = + | { + readonly type: "market"; + readonly adapter: Address; + readonly marketParams: MarketParams; + } + | { readonly type: "idle" }; + +export interface VaultV2BlueReallocation { + readonly allocator: Address; + readonly type: "bluePublicAllocator"; + readonly vault: Address; + readonly from: BluePublicAllocatorSource; + readonly to: { readonly adapter: Address }; + readonly assets: bigint; + readonly nativePenalty: bigint; +} +``` + +The target market parameters come from the enclosing Blue action. Existing +borrow, supply-collateral-borrow, loan-asset withdraw, and refinance builders +expand each V2 entry into an existing Bundler3 allocator action. The bundle's +native value is the sum of V1 fees and every retained V2 call's penalty. -### The contract, and what it changes +## Contract model -`BluePublicAllocator` is a singleton — no constructor, no immutables, no `MORPHO` reference. Every -entry point is gated on `IVaultV2(vault).isAllocator(msg.sender)`; there is no `admin` mapping. +The ABI is pinned from `morpho-org/vault-v2` at the same upstream revision as +the fork fixture documented under Dependencies. The relevant read and write +surface is: ```solidity mapping(address vault => mapping(bytes32 id => uint256)) public absoluteCap; -mapping(address vault => mapping(bytes32 id => bool)) public canDeallocate; +mapping(address vault => mapping(bytes32 id => bool)) public canDeallocate; mapping(address vault => mapping(address adapter => bool)) public isActiveAdapter; mapping(address vault => VaultData) public vaultData; -// VaultData reads used here: { bool canAllocateFromIdle; uint120 nativePenalty; } function reallocate( address vault, - address deallocateAdapter, MarketParams calldata deallocateMarketParams, - address allocateAdapter, MarketParams calldata allocateMarketParams, + address deallocateAdapter, + MarketParams calldata deallocateMarketParams, + address allocateAdapter, + MarketParams calldata allocateMarketParams, uint128 assets ) external payable; function allocateFromIdle( - address vault, address adapter, MarketParams calldata marketParams, uint128 assets + address vault, + address adapter, + MarketParams calldata marketParams, + uint128 assets ) external payable; ``` -`id` is `keccak256(abi.encode("this/marketParams", adapter, marketParams))` — already implemented -off-chain as `VaultV2MorphoMarketV1AdapterV2.marketParamsId(adapter, params)` -(`packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts:49`). Because the adapter address -is in the preimage, the same Blue market reached through two adapters has two distinct ids and two -distinct caps. - -Nine deltas from V1 drive the whole design. Items 4–7 were **verified against `VaultV2.sol` and -`MorphoMarketV1AdapterV2.sol` at `vault-v2@main`**, and two of them invalidate the intuitive port. - -1. **One source → one target per call.** No `Withdrawal[]`, so no ordering requirement and no - deduplication check. `nativePenalty` is charged **per call**, so N sources cost N × penalty. V1 - batched N withdrawals under a single fee — which is why V1's greedy loop ends with a - `(count − 1) × fee` refund. **That refund loop must not be ported.** -2. **`absoluteCap` is a post-state ceiling, not a consumable budget.** V1's `FlowCaps{maxIn, maxOut}` - shift on every call and deplete; V2 re-reads `IVaultV2.allocation(id)` and compares. There is no - flow bookkeeping to simulate — but also no rate limit, so a cap-compliant reallocation is - repeatable. -3. **No source-side amount limit** — only the `canDeallocate` boolean. A deallocatable market can be - fully drained in one call, subject to Blue's own liquidity. -4. **`allocation[id]` is rebased, not incremented.** `VaultV2.allocate`/`deallocate` apply the - adapter's returned `int256 change`, and `MorphoMarketV1AdapterV2` builds it as - `expectedSupplyAssets(marketId) − vault.allocation(marketParamsId)`. So after a leg, - `allocation(marketParamsId) == expectedSupplyAssets(marketId)` **exactly**. Three consequences: - - Define `untracked = expectedSupplyAssets(marketId) − allocation(marketParamsId) ≥ 0`, the - interest realized on the first touch. It **consumes target-side cap headroom even for a tiny - `assets`** — a market untouched for months can blow through its cap on a 1-wei reallocation. - - The same `change` is applied to all three ids, so `adapterId` and `collateralId` are `Σ` - aggregates whose non-touched components are stale. - - `allocation[id]` **never bounds `assets`** on the deallocate leg — the vault only requires - `allocation > 0`. The binding constraint is Blue's `supplyShares` underflow and - `INSUFFICIENT_LIQUIDITY`, both of which fire before the vault touches `caps`. -5. **Shared ids do not net to zero.** Source and target share `adapterId` when on the same adapter, - and share `collateralId` whenever the collateral token matches — *across adapters*, since the - adapter address is not in that preimage. On a shared id the pair nets to - `untracked_src + untracked_tgt > 0`. An intra-adapter rotation is therefore **not cap-neutral** - and can revert on `AbsoluteCapExceeded` / `RelativeCapExceeded`. Treating shared ids as cancelling - out is the single most tempting wrong simplification here. -6. **Vault caps are checked on `allocate` only**, on all three target ids, with three requires: - `absoluteCap > 0` (`ZeroAbsoluteCap`), `allocation ≤ absoluteCap`, and - `relativeCap == WAD || allocation ≤ firstTotalAssets.mulDivDown(relativeCap, WAD)`. Because the - deallocate leg has already landed in the same transaction, the allocate-leg check sees the - post-deallocation state. -7. **The relative-cap denominator is `firstTotalAssets`**, not `_totalAssets` — transient storage set - once per transaction inside `accrueInterest()` as - `min(realAssets, _totalAssets + _totalAssets·elapsed·maxRate/WAD)`. It is the anti-flashloan - mechanism, and it is **frozen for a whole bundled plan**. Note `deallocate` does *not* accrue; - `allocate` does. -8. **`allocateFromIdle` is a new liquidity source** with no source market: it spends the vault's idle - ERC-20 balance. Its gate failure reverts with `CannotDeallocate()` (reused, no dedicated error). - There is no public deallocate-to-idle — idle → market only, never back. -9. Active adapters **must** be `MorphoMarketV1AdapterV2` (contract natspec: otherwise "the public - allocator's absolute cap system could break"), and both legs require - `marketParams.irm == adaptiveCurveIrm` and `loanToken == asset`. - -### Public surface - -Suffix naming throughout, so every symbol reads as "the V1 thing, for Vault V2": - -| Layer | Symbol | -| --- | --- | -| Helper | `computeReallocationsVaultV2({ reallocationData, marketId, operation, amount, options })` | -| Helper | `validateReallocationsVaultV2(reallocations, target)` | -| Entity | `class ReallocationDataVaultV2` / `interface InputReallocationDataVaultV2` | -| Entity method | `getMarketPublicReallocationsVaultV2(marketId, options?)` | -| Entity method | `getPublicReallocationLiquidityVaultV2(marketId, options?)` | -| Entity method | `getAvailableLiquidityToUtilizationVaultV2(marketId, utilization?, options?)` | -| Types | `VaultReallocationVaultV2`, `ReallocationWithdrawalVaultV2`, `PublicReallocationVaultV2`, `PublicAllocatorOptionsVaultV2`, `ReallocationComputeOptionsVaultV2` | -| Config shapes | `VaultV2PublicAllocatorConfig`, `VaultV2MarketPublicAllocatorConfig` | -| Fetchers | `fetchVaultV2PublicAllocatorConfig`, `fetchVaultV2MarketPublicAllocatorConfig`, `fetchVaultV2PublicAllocatorData` | -| ABI / address | `bluePublicAllocatorAbi`, `ChainAddresses.bluePublicAllocator` | - -The config shapes use the `VaultV2` **prefix** rather than the suffix, because they live in -`blue-sdk/src/vault/v2/` alongside `VaultV2Adapter`, `VaultV2MorphoVaultV1Adapter` and -`AccrualVaultV2`. The suffix rule governs the mirrored *logic* surface; the prefix rule governs -`blue-sdk` entity shapes. They are plain readonly **interfaces**, not classes — no new class is -introduced and no `.fetch` augment is added. +One call has one source and one target. There is no V1-style withdrawal array, +ordering requirement, or multi-source fee refund. `nativePenalty` is charged +per call. -```ts -export interface PublicAllocatorOptionsVaultV2 { - readonly enabled?: boolean; - readonly timestamp?: BigIntish; - readonly reallocatableVaults?: readonly Address[]; -} -export type ReallocationComputeOptionsVaultV2 = PublicAllocatorOptionsVaultV2; +The allocator cap is a post-state ceiling on the target adapter's +`marketParamsId`, not a consumable flow budget. Source-side allocator state is +only `canDeallocate`. -/** One entry = one on-chain call. `idle` has no source market. */ -export type ReallocationWithdrawalVaultV2 = - | { readonly type: "market"; readonly adapter: Address; - readonly marketParams: MarketParams; readonly amount: bigint } - | { readonly type: "idle"; readonly amount: bigint }; +## Derived allocation IDs -export interface VaultReallocationVaultV2 { - readonly vault: Address; - readonly allocateAdapter: Address; - readonly allocateMarketParams: MarketParams; - /** Total native penalty: `nativePenalty × withdrawals.length`. */ - readonly fee: bigint; - readonly withdrawals: readonly ReallocationWithdrawalVaultV2[]; -} -``` - -`PublicAllocatorOptionsVaultV2` carries **only** the three live options. V1's four `@deprecated` -utilization knobs are not carried forward: the 90% source ceiling and 90% supply trigger are the -existing constants, and phase 2's 100% drain is an internal parameter, not public surface. +`VaultV2MorphoMarketV1AdapterV2.ids(params)` returns: -`VaultReallocationVaultV2` stays vault-grouped like V1 for shape symmetry, with the group key -`(vault, allocateAdapter)` — a vault may hold several adapters covering the target market, and each -call names its own `allocateAdapter`. `fee = nativePenalty × withdrawals.length` because each -withdrawal is exactly one call. The deferred encoder expands one group into N -`reallocate` / `allocateFromIdle` calls. +1. `adapterId(address)` — shared by every market on the adapter; +2. `collateralId(collateralToken)` — shared across adapters for the same + collateral; +3. `marketParamsId(adapter, params)` — unique to an adapter/market pair. -### State model: keyed by derived `bytes32` id - -`ReallocationDataVaultV2` keys cap state by the derived id, scoped per vault — **not** by -`(vault, adapter, marketId)`: +State is therefore keyed by `(vault, derivedId)`, not by a projected +`(vault, adapter, market)` tuple: ```ts export interface InputReallocationDataVaultV2 { readonly chainId: number; + readonly allocator: Address; readonly markets?: Readonly>; readonly vaults?: Readonly>; - /** Vault caps + live allocation per derived id. */ - readonly allocations?: Readonly>>>; - readonly publicAllocatorConfigs?: Readonly>; - readonly marketPublicAllocatorConfigs?: Readonly>>>; + readonly allocations?: Readonly< + Record>> + >; + readonly publicAllocatorConfigs?: Readonly< + Record + >; + readonly marketPublicAllocatorConfigs?: Readonly< + Record< + Address, + Readonly> + > + >; } ``` -Three reasons, in severity order: +The readonly config projections are self-identifying. Vault-wide state carries +`allocator`, `vault`, `canAllocateFromIdle`, and `nativePenalty`. Pair state +also carries `adapter`, `marketParamsId`, `absoluteCap`, `canDeallocate`, and +`isActiveAdapter`. -1. **The ids alias.** One `adapterId` is shared by every market on that adapter; one `collateralId` - is shared by every pair with the same collateral token, across adapters. A - `(vault, adapter, marketId)`-keyed map stores the same live `allocation` in N slots, so every - mutation needs fan-out writes and every read needs reconciliation — and the second withdrawal of - a plan reads a stale aggregate and busts the bucket. -2. **1:1 with storage.** All five relevant mappings are `mapping(bytes32 id => …)`. Any derived key - is a lossy re-projection. -3. It reuses the existing `IVaultV2Allocation` (`{ id, absoluteCap, relativeCap, allocation }`) - verbatim instead of exporting a duplicate shape (root §1). +## Fetching -`(vault, adapter, marketId)` remains the key for the *permission* projection, because that is what a -call takes. Note the asymmetry the contract dictates: the **allocator's** `absoluteCap` is checked on -**one** id (`marketParamsId`), while the **vault's** caps are checked on **all three**. +`bluePublicAllocatorAbi` includes the three allocator mapping reads and +`vaultData`. Fetchers always take the allocator address explicitly: -Everything derivable from `AccrualVaultV2` is derived, not fetched: candidate adapters -(`accrualAdapters[i].type`), source positions (`supplyShares` × `Market`), `MarketParams`, all three -ids (`adapter.ids(params)`), idle balance (`assetBalance`), `adaptiveCurveIrm`. +- `fetchVaultV2PublicAllocatorConfig(allocator, vault, client, parameters?)`; +- `fetchVaultV2MarketPublicAllocatorConfig(allocator, vault, adapter, + marketParamsId, client, parameters?)`; +- `fetchVaultV2PublicAllocatorData(allocator, hydratedVault, client, + parameters?)`. -### The per-candidate bound +The batched fetcher derives every supported adapter/market request and every +unique allocation ID from the hydrated `AccrualVaultV2`. It defaults to one +deployless read and falls back to equivalent direct reads unless deployless +mode is forced. No chain-address lookup occurs. -With `untracked(a, m) = expectedSupplyAssets(m) − allocation[marketParamsId(a, m)]` and -`A = firstTotalAssets` from one up-front `accrueInterest(timestamp)`, frozen for the plan: +Only `AccrualVaultV2MorphoMarketV1AdapterV2` adapters participate. Other +adapter classes are ignored even if an allocator reports them as active. -**Feasibility gates** — return `0n`, no amount: +## Cap headroom -- `isActiveAdapter[target]`, `isActiveAdapter[source]`, `canDeallocate[sourceMarketParamsId]`, - `canAllocateFromIdle` for an idle source. -- `marketParams.irm === adapter.adaptiveCurveIrm` on both legs. -- `sourceMarketParamsId !== targetMarketParamsId` — exclude the **pair**, not the market. The same - market on a different adapter is a legitimate source; a true self-reallocation is a - penalty-charging no-op that can also revert. -- `absoluteCap[t] > 0` for every target id (`ZeroAbsoluteCap`), `allocation[s] > 0` for every source - id (`ZeroAllocation`). -- For each id **shared** between source and target: - `allocation[id] + untracked_src + untracked_tgt ≤ min(absoluteCap[id], mulDivDown(A, relativeCap[id], WAD))`. - This is `assets`-independent, so it gates feasibility rather than bounding the amount. +`VaultV2Utils.allocationHeadroom(allocation, firstTotalAssets)` is the single +pure implementation of Vault V2 absolute/relative-cap capacity: -**Amount bounds** — `MathLib.min` of: +```text +absolute = zeroFloorSub(absoluteCap, allocation) +relative = zeroFloorSub(mulDivDown(firstTotalAssets, relativeCap, WAD), allocation) +headroom = relativeCap == WAD ? absolute : min(absolute, relative) +``` -| # | Bound | -| --- | --- | -| 1 | `MathLib.MAX_UINT_128` — `assets` is `uint128` on the allocator API | -| 2 | `zeroFloorSub(bpaAbsoluteCap[targetMarketParamsId], allocation[targetMarketParamsId] + untracked_tgt)` | -| 3 | for each target id **not** shared with the source: `allocationHeadroom(allocation[t], A) − untracked_tgt` | -| 4 | market source: `expectedSupplyAssets(sourceMarketId)` — *not* `allocation[id]` | -| 5 | market source: `sourceMarket.getWithdrawToUtilization(ceiling)` — SDK policy; at `WAD` this is exactly Blue's `totalSupply − totalBorrow` | -| 6 | idle source: `vault.assetBalance` | +It returns both the capacity and the binding `CapacityLimitReason`. +`AccrualVaultV2.maxDeposit` delegates to it, preserving its existing behavior. + +## Accrual and untracked interest + +Each considered vault is accrued once at the supplied timestamp. The accrued +vault's `_totalAssets` becomes the plan's frozen `firstTotalAssets` +denominator. Reallocation legs never change it. + +For adapter `a` and market `m`: + +```text +expectedSupplyAssets(a, m) = market.toSupplyAssets(adapter.supplyShares[m]) +untracked(a, m) = zeroFloorSub( + expectedSupplyAssets(a, m), + allocation[marketParamsId(a, m)] +) +``` + +On first touch, the adapter rebases allocation state to expected assets. The +same signed change is applied to all three derived IDs. Untracked interest is +therefore relevant to target cap checks and to shared-ID feasibility even for +a very small principal move. -`zeroFloorSub` on every cap term (caps may legally sit below the live allocation); `mulDivDown` for -relative caps — over-stating by 1 wei produces a `RelativeCapExceeded` revert. +## Candidate gates and bounds -### State transition +A candidate exists only when: -`applyPublicReallocationVaultV2` clones on write and applies the legs in contract order -(deallocate → allocate), so both the intermediate dip and the shared-id netting are faithful: +- the target and, for a market source, source adapters are supported + `MorphoMarketV1AdapterV2` instances owned by the vault; +- both markets use the vault asset as loan token and the adapter's + `adaptiveCurveIrm`; +- target/source adapters are active, source deallocation is permitted, or + idle allocation is permitted; +- all three target vault caps have a positive absolute cap; +- all three source allocations are non-zero for market sources; +- the source pair is not the exact target `(adapter, market)` pair. The same + Blue market through another adapter is valid. -| Field | market → market | idle → market | +For each allocation ID shared by the source and target, feasibility is checked +without principal cancellation: + +```text +allocation[id] + sourceUntracked + targetUntracked + <= min(absoluteCap[id], relativeCapAssets[id]) +``` + +For non-shared target IDs, principal is bounded by cap headroom after target +untracked interest. The final obtainable amount is the minimum of: + +- `MathLib.MAX_UINT_128`; +- target Morpho market `uint128` supply headroom (unless a same-market source + deallocation creates the headroom in the same call); +- allocator target-cap headroom; +- each non-shared target Vault V2 cap headroom; +- source expected supply assets; +- source Blue withdrawal capacity to the configured utilization ceiling; or +- the vault idle balance for an idle source. + +Caps below live allocation use zero-floor subtraction. A source allocation is +only a non-zero gate; it does not bound deallocation assets. + +## Greedy state transition + +Discovery computes the largest obtainable call across vaults and sources, +applies it to cloned state, and repeats until no candidate remains. Market +sources are applied in contract order: + +| State | market → market | idle → market | | --- | --- | --- | -| `allocations[vault][s]`, all 3 source ids | `+= change_src = −assets + untracked_src` | — | -| `allocations[vault][t]`, all 3 target ids | `+= change_tgt = assets + untracked_tgt` | same | -| `markets[srcId]`, `srcAdapter.supplyShares` | `withdraw(assets)` | — | -| `markets[tgtId]`, `tgtAdapter.supplyShares` | `supply(assets)` | same | -| `vault.assetBalance` | `+= assets + untracked_src`, then `−= assets` | `−= assets` | -| `vault._totalAssets`, `firstTotalAssets` | **unchanged** | **unchanged** | - -`_totalAssets` is written only by `accrueInterest` / `enter` / `exit`, and `firstTotalAssets` is -transient. **V1's closing `vaultData.totalAssets = withdrawQueue.reduce(…)` recomputation must not be -ported**: in V1 `totalAssets` is a derived mirror, in V2 it is stored, and recomputing it folds -per-leg Blue rounding into the relative-cap denominator on every greedy step. Because `A` is frozen, -relative headroom is monotonically non-increasing and the greedy loop provably terminates. - -### Planner and validator - -`computeReallocationsVaultV2` keeps V1's two-phase shape — friendly at -`DEFAULT_WITHDRAWAL_TARGET_UTILIZATION`, then aggressive at `MathLib.WAD` passed as an internal -parameter — the same `groupWithdrawalsByVault` / `capVaultWithdrawals` logic keyed on -`(vault, allocateAdapter)`, the same `getSupplyTargetUtilization` trigger, and the same -`InsufficientSharedLiquidityError` / `ReallocationWithdrawExceedsMarketSupplyError` throws. It drops -V1's terminal sort, which existed only to satisfy `reallocateTo`'s ascending-id requirement. - -`validateReallocationsVaultV2` stays state-independent like V1: non-negative `fee`, non-empty -`withdrawals`, positive `amount`, `amount ≤ MAX_UINT_128`, no withdrawal on the target -`(adapter, marketId)` pair, and **unique** sources per group (replacing V1's sortedness check). - -New error classes follow root §3 — one per failure mode, exported, instruction-shaped messages with -quoted interpolations. Missing-data classes extend `UnknownDataError` so the greedy scan skips the -candidate via `_try`; plan-level classes are thrown only from the validator and the state -transition. `UnsortedReallocationWithdrawalsError` gets no sibling. - -### Implementation Phases - -- **Phase 1 — ABI + address registry (`morpho-ts`).** Hand-pinned `bluePublicAllocatorAbi`; optional - `ChainAddresses.bluePublicAllocator` with **no chain entries** (the contract is undeployed), so - `getChainAddress` and `registerCustomAddresses` resolve it like any other key. Re-export from - `blue-sdk-viem/src/abis.ts` and `morpho-sdk/src/abis.ts` — one definition, per root §1. -- **Phase 2 — shared cap formula (`blue-sdk`).** Extract the absolute+relative cap arithmetic already - inside `AccrualVaultV2.maxDeposit` into a pure `VaultV2Utils.allocationHeadroom(allocation, firstTotalAssets)` - and have `maxDeposit` delegate. Behavior-preserving, no signature change, endorsed by root §1 - ("class methods delegate to pure `*Utils` namespace functions"). This is the **only** touch to - existing code in the whole change, and it exists so the cap formula cannot drift between - `maxDeposit` and the simulation. -- **Phase 3 — fetcher (`blue-sdk-viem`).** Config interfaces in `blue-sdk/src/vault/v2/`; a new - deployless query (`contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol` + - `interfaces/IBluePublicAllocator.sol`) and three fetchers in `src/fetch/vault-v2/`, with - `deployless = true` default and multicall fallback per package convention. The batched - `fetchVaultV2PublicAllocatorData` is what the simulation calls: one `eth_call` deployless, versus - `1 + 4V + 3C + 5·V·M` reads on multicall for V adapters, M markets each, C collateral tokens. - Throws `MissingBluePublicAllocatorAddressError` when the registry key is unset. -- **Phase 4 — types + errors (`morpho-sdk`).** `src/types/sharedLiquidityVaultV2.ts` and the new - error classes in `src/types/error.ts`. -- **Phase 5 — entity + planner (`morpho-sdk`).** `src/entities/reallocationDataVaultV2.ts`, - `src/helpers/computeReallocationsVaultV2.ts`, `validateReallocationsVaultV2` in - `src/helpers/validate.ts`, plus barrel and `src/utils.ts` exports mirroring V1's root/`utils` - symmetry. -- **Phase 6 — tests.** Colocated units for the pure surface; mock-transport units for the fetchers - (root §2 rule 6 permits `createMockClient` for shaped-response fetchers); Anvil fork tests for - every path whose correctness depends on real on-chain state. -- **Phase 7 — docs + changesets.** `AGENTS.md` glossary and layer docs; minor changesets for - `morpho-ts`, `blue-sdk`, `blue-sdk-viem`, `morpho-sdk`, plus the §7 dependent-bump audit. Also fix - the JSDoc at `packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts:159`, which - already mislabels the **V1** `reallocations` field as "Morpho Vault V2 reallocations" — harmless - today, actively misleading once a real V2 surface exists. - -### Test strategy - -Because `BluePublicAllocator` is undeployed, the fork test follows -[morpho-org/sdks#907](https://github.com/morpho-org/sdks/pull/907): an out-of-band-compiled artifact -committed under `test/fixtures/` with a provenance header (source repo @ commit SHA + path, solc -version, optimizer / viaIR / metadata / evmVersion settings, verbatim regeneration command, and a -"delete once deployed" note), deployed with the existing `client.deployContractWait`. No `.sol` -vendoring of the allocator into `contracts/`, and the ABI comes from `bluePublicAllocatorAbi` rather -than the fixture, so there is still one ABI definition. - -Six invariants get a test that fails if the invariant is removed (root §5): - -1. **Shared `collateralId` aliasing** — two sources and a target on the same collateral, with the - `collateralId` capped so only the first fits. -2. **Shared ids are not cap-neutral** — an intra-adapter rotation with `allocation[adapterId]` at its - cap must be rejected when `untracked_src + untracked_tgt` overflows it. -3. **`untracked` consumes target headroom** — fork at a block where - `allocation(id) < expectedSupplyAssets`, then `simulateContract` the computed plan. -4. **`allocation[id]` does not bound the deallocate leg** — a full drain of a market whose - `expectedSupplyAssets > allocation[id]` must succeed. -5. **`_totalAssets` / `firstTotalAssets` immutability** across the state transition, plus a - multi-step plan where a relative cap binds to the wei. -6. **Fee arithmetic** — three withdrawals from one vault ⇒ `fee === 3n * nativePenalty`. - -Plus: pair-vs-market exclusion in both directions, and two idle withdrawals not each claiming the -whole `assetBalance`. - -## Considered Alternatives - -### Alternative 1: extend `ReallocationData` with V2 support instead of a sibling class - -Parameterize the existing class over vault version, or widen its maps to hold `AccrualVaultV2` -alongside `Vault`. - -**Why rejected:** the two cap models share no arithmetic. V1 keys by `MarketId` and mutates -consumable `maxIn`/`maxOut`; V2 keys by a derived `bytes32`, mutates rebased aggregate allocations -across three aliasing ids, and has no flow budget at all. Every method body would fork on version -immediately. It would also mean retyping public maps on a shipped class — a major per root §7 — for -zero shared code. A sibling class keeps V1 byte-identical and makes the deferred v1/v2 dispatch a -straight swap at the call site. - -### Alternative 2: flat one-entry-per-call result shape - -Return `{ vault, deallocateAdapter, deallocateMarketParams, allocateAdapter, allocateMarketParams, assets, nativePenalty }[]` -— exactly 1:1 with the on-chain calls, since V2 has no batching. - -**Why rejected:** it breaks the V1/V2 signature symmetry that root §6 makes a first-class -requirement ("Identical signatures across V1/V2 where protocols overlap"), and it pushes per-vault -fee aggregation onto every caller. The vault-grouped shape keeps `VaultReallocation` and -`VaultReallocationVaultV2` swappable, with `fee` already totalled. The flat shape is recoverable by -flattening `withdrawals`; the grouping is not recoverable from the flat shape without a regroup pass. - -### Alternative 3: allocator-cap-only simulation - -Simulate only `absoluteCap` / `canDeallocate` / `isActiveAdapter` / `canAllocateFromIdle`, and let -the vault's own absolute and relative caps revert on-chain. - -**Why rejected:** it cuts the fetch cost by roughly `3·(V + C + V·M)` reads, but it emits -**fee-bearing plans that revert**. `computeReallocations` already refuses partial plans for an -unreachable operation precisely because the penalty is non-refundable -(`InsufficientSharedLiquidityError`); shipping a V2 planner that knowingly ignores half the binding -constraints would be a regression against V1, which does simulate vault caps and pending caps. The -deployless query collapses the read cost to a single `eth_call` anyway. - -### Alternative 4: treat shared ids as netting to zero - -Skip any target id that also appears among the source ids, on the reasoning that -`deallocate(−assets)` then `allocate(+assets)` cancels out. - -**Why rejected:** **factually wrong**, and wrong in the unsafe direction. Because the adapter rebases -to `expectedSupplyAssets`, a shared id nets to `untracked_src + untracked_tgt`, which is strictly -positive whenever either market has accrued interest since its last touch. Since caps are checked -only on the allocate leg — after the deallocate leg has landed — an intra-adapter or same-collateral -rotation can revert on `AbsoluteCapExceeded` even though the principal is unchanged. Shared ids are -therefore modelled as feasibility gates, not as cancelled terms. This was caught only by reading -`VaultV2.allocateInternal` and `MorphoMarketV1AdapterV2.allocate` line by line; it is recorded here -because it is the least intuitive part of the design. - -### Alternative 5: wire V2 reallocations into the transaction flows in the same change - -Add `reallocations?` to `blueBorrow` / `blueSupplyCollateralBorrow` / `blueWithdraw` / -`blueRefinance` and a `bluePublicAllocatorReallocate` bundler encoder now. - -**Why rejected:** the encoding question belongs to the blue bundles migration, which is deciding how -V1 and V2 reallocations coexist inside one bundle and how `tx.value` aggregates two penalty models. -Landing the encoder first would freeze that decision from the wrong end. Read + compute is -independently useful (dashboards and quoting need the metric, not calldata) and independently -reviewable — one concern per PR, root §8. - -### Alternative 6: carry V1's deprecated utilization options into the V2 options type - -Mirror `PublicAllocatorOptions` exactly, including `maxWithdrawalUtilization`, -`defaultMaxWithdrawalUtilization`, `supplyTargetUtilization`, `defaultSupplyTargetUtilization`. - -**Why rejected:** all four are `@deprecated` and slated for removal in the next major. Adding them to -a brand-new type would create four public options with a scheduled death date and would make the V2 -surface inherit a migration it never needed. The fixed 90%/90% policy plus an internal phase-2 -parameter is the end state V1 is heading toward; V2 starts there. - -## Assumptions & Constraints - -- **The contract is one day old, unaudited, and undeployed.** The last commit touching it is - `b41782590d3d33d8d836aedd233aaa72ac8b2aa2` ("rename"), and the surrounding commits that same - morning were still restructuring folders and adding `multicall`. The ABI and the test fixture pin a - commit SHA and must be regenerated together; nothing in CI detects upstream drift. -- **`ChainAddresses.bluePublicAllocator` is registered but empty.** Every fetcher throws - `MissingBluePublicAllocatorAddressError` until a deployment lands. Fork tests register the - fork-deployed address at runtime. -- **Simulation assumes one bundled transaction.** `firstTotalAssets` is transient, so a plan split - across transactions gets a fresh denominator per transaction. Accrual only raises it, so the drift - is safe-directional — the real transaction sees at least as much relative headroom as simulated. -- **`SharePriceAboveOne` (`mintedShares ≥ assets` on allocate) is not modelled.** Blue's virtual-share - scaling keeps it true for any realistic market. -- **`getWithdrawToUtilization` is a policy bound, not a contract bound.** The contract permits a full - drain; the 90% ceiling is the SDK's own conservatism, identical to V1's. -- Pass `options.timestamp` from the block used to fetch the state — same constraint as V1, where - accrual otherwise falls back to the target market's `lastUpdate`. -- Additive public surface only. Semver: **minor** for the four touched packages. `viem` stays the - only peer dependency of `morpho-sdk`; no new runtime dependencies. +| source derived IDs | `+= sourceUntracked - assets` | unchanged | +| target derived IDs | `+= targetUntracked + assets` | same | +| source market/shares | withdraw first | unchanged | +| target market/shares | supply second | supply | +| vault idle balance | receives and then spends the principal assets | spends principal | +| vault `_totalAssets` | unchanged | unchanged | + +Shared IDs are updated twice in that order. Penalties are never refunded or +folded into vault accounting. + +## Planner + +`computeReallocationsVaultV2` uses the same operation algebra and target +utilization calculation as V1: + +- borrow: `B' = B + amount`, `S' = S`; +- withdraw: `B' = B`, `S' = S - amount`. + +If the post-operation utilization is at most the fixed 90% target, it returns +no calls. Otherwise it discovers friendly sources using the fixed 90% source +ceiling. If the operation would still have `borrow > supply`, it continues +from the friendly post-state with an internal 100% source ceiling. + +The flat calls are capped in discovery order to the required amount. Every +retained call keeps its full `nativePenalty`. The planner throws: + +- `ReallocationWithdrawExceedsMarketSupplyError` when a requested withdraw is + impossible regardless of reallocations; +- `InsufficientSharedLiquidityError` when a fee-bearing partial plan cannot + cover the operation's absolute liquidity shortfall. + +## Validation and metrics + +The existing `validateReallocations` validates the combined `BlueReallocation` +union. A V2 market source is rejected only when both its adapter and market +match the target pair. The same market through another adapter is accepted. + +`ReallocationDataVaultV2` exposes: + +- `getPublicReallocationLiquidityVaultV2(...)`, which sums market and idle + candidates; and +- `getAvailableLiquidityToUtilizationVaultV2(...)`, which uses the same + target-utilization math as the V1 metric. + +Idle is included by default because it is immediately reallocatable +liquidity. + +## Alternatives rejected + +### Group V2 calls by vault + +Rejected because the contract accepts one source per call and charges one +penalty per call. A grouped SDK shape would require a second expansion model +and would obscure the exact transaction cost. Flat `VaultV2BlueReallocation` +is already accepted by the branch's Blue action builders. + +### Add a V2 validator + +Rejected because the action layer already consumes a discriminated V1/V2 +union. One validator is the single source of truth for amount bounds, source +tags, and target-pair exclusion. + +### Register a canonical allocator address + +Rejected because there is no canonical per-chain deployment to register. +Identity is explicit in config data and action inputs. + +### Copy V1's deprecated utilization options + +Rejected. V2 starts at the intended fixed-policy end state: 90% friendly +source and target thresholds plus an internal 100% fallback. + +## Compatibility and releases + +- `getMarketPublicReallocations` delegates to + `computeVaultV1Reallocations` and is marked deprecated. +- `VaultReallocation` aliases `VaultV1BlueReallocation` and is marked + deprecated. +- `computeReallocations` remains the V1 amount-aware planner. +- `BluePublicAllocatorReallocation` receives no alias because it was not part + of the published surface relative to `origin/main`. +- The feature is minor for `morpho-ts`, `blue-sdk`, `blue-sdk-viem`, and + `morpho-sdk`. +- `blue-sdk-viem` raises its `blue-sdk` peer range to the new minor. + +## Security and operational constraints + +- A plan is a block-state simulation, not an execution guarantee. Allocator + caps, shares, and market liquidity can be front-run. +- `msg.value` must cover each call's exact native penalty; a reverted call + still consumes gas. +- Pass `options.timestamp` from the block used to fetch state so market and + vault accrual share one reference point. +- Relative-cap arithmetic rounds down. Overstating by one wei can cause an + on-chain revert. +- The upstream ABI and fork fixture must stay pinned to the same Vault V2 + revision; generated queries alone do not detect upstream drift. ## Dependencies -- `morpho-org/vault-v2` @ `4c7c110a9a3c3ce1ec545fff3b8a832f16cedfcc` (repo HEAD at drafting) — - `BluePublicAllocator.sol`, `VaultV2.sol`, `MorphoMarketV1AdapterV2.sol`. -- Existing `blue-sdk` id derivation (`VaultV2MorphoMarketV1AdapterV2.adapterId` / `collateralId` / - `marketParamsId`) and `AccrualVaultV2`. -- Existing fork harness: `@morpho-org/test`'s `createViemTest` / `AnvilTestClient.deployContractWait`, - and `packages/blue-sdk-viem/test/utils.ts` (`deployVaultV2`, `deployMorphoMarketV1Adapter`, - `submitAndAccept`). -- **Blocks:** the blue bundles migration project, which consumes this engine to build the actual - reallocation bundles and the v1/v2 dispatch. - -## Security - -- **Non-refundable penalty.** `msg.value` must equal `nativePenalty` exactly — overpaying reverts, - and a reverted `reallocate` still cost gas. The planner therefore refuses partial plans for an - unreachable operation, mirroring V1's `InsufficientSharedLiquidityError`. -- **Front-runnable by anyone**, per the contract's own natspec: an allocate reverts if the vault cap - is filled first, and a deallocate reverts if shares stop covering assets. Neither is an SDK bug; - both must be surfaced in the JSDoc so integrators handle the revert rather than treat a computed - plan as guaranteed. -- **`BluePublicAllocator` opens relative-cap manipulation through short-term deposits** (capital - intensive, documented upstream). The SDK does not mitigate it; the simulation just uses - `firstTotalAssets`, the same anti-flashloan denominator the contract uses. -- **Non-conforming adapters are skipped, not trusted.** Only `MorphoMarketV1AdapterV2` adapters are - considered, because the contract's cap system is only sound for them. An `isActiveAdapter` adapter - of any other class is silently ignored rather than simulated with the wrong cap model. -- **Read-only change.** No signing, no calldata, no new authorization surface in this PR. - -## Future Considerations - -- **Entity dispatch and the combined metric.** Once `MorphoBlue` can fetch both engines, a single - "reallocatable liquidity" number spanning V1 and V2 vaults becomes the natural integrator-facing - metric. Deferred here because it requires the entity change this TIB excludes. -- **Bundler encoding** (`bluePublicAllocatorReallocate`, `allocateFromIdle`) and the `reallocations?` - parameters on the Blue flows — blue bundles migration. -- **Penalty-aware planning.** A plan pulling a dust amount from a fifth market pays a full extra - penalty. Largest-first greedy already tends to minimize call count, so this is deliberately not - optimized now; a `maxNativePenalty` budget or a dust floor is the obvious extension if real vaults - set non-trivial penalties. -- **Delete the test fixture** once `BluePublicAllocator` is deployed and its address is registered; - replace the runtime deploy with a pinned-block fork read. -- `packages/blue-sdk-viem/contracts/vault-v2/interfaces/IVaultV2.sol` has known drift from the pinned - `vaultV2Abi` (`sharesGate` / `setIsAdapter` / `abdicateSubmit` versus `receiveSharesGate` / - `addAdapter` / `abdicate`). It is inert for this change — those selectors are never called — and - fixing it would rewrite the `GetVaultV2` and `GetAccrualVaultV2` artifacts. Worth its own PR. - -## Open Questions - -- Should `getPublicReallocationLiquidityVaultV2` count idle by default? It is included here, on the - grounds that idle is genuinely reallocatable and excluding it under-reports — but a dashboard - showing "shared liquidity" may prefer to attribute idle separately. -- Should reallocation candidates be favored by penalty cost? Unlike PAV1's single per-vault fee, - PAV2 charges a penalty per market moved, so a plan spread across many markets is proportionally - more expensive for the borrower. This raises a product question: keep ranking candidates by size - (as V1 does today), or rank by liquidity obtained per unit of penalty. @Foulks-Plb has made a - first algorithm proposal to answer this and related product needs — the phase structure is - unchanged (Phase 1 Friendly, then Phase 2 Aggressive); the only difference is that each - reallocation is assigned a cost and the least-expensive reallocations are prioritized within each - phase. The idea is currently under discussion with the product team — see the - [Slack thread](https://morpholabs.slack.com/archives/C0AJMKR8VB9/p1785422085768909?thread_ts=1785398315.187139&cid=C0AJMKR8VB9). +- `morpho-org/vault-v2` commit + `4c7c110a9a3c3ce1ec545fff3b8a832f16cedfcc` for the pinned allocator fixture + and surrounding Vault V2 contracts. +- `BluePublicAllocator.sol` last-touch commit + `b41782590d3d33d8d836aedd233aaa72ac8b2aa2` for the allocator interface + described here. +- Existing `VaultV2MorphoMarketV1AdapterV2.ids`, `AccrualVaultV2`, Morpho Blue + `Market`, and Bundler3 allocator encoders. +- Existing Anvil fork harness from `@morpho-org/test`. ## References - [`BluePublicAllocator.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/periphery/blue-public-allocator/BluePublicAllocator.sol) - and [`src/periphery/README.md`](https://github.com/morpho-org/vault-v2/blob/main/src/periphery/README.md) -- [`VaultV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/VaultV2.sol) `allocateInternal` / `deallocateInternal` — the cap checks and the `int256 change` application -- [`MorphoMarketV1AdapterV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/adapters/MorphoMarketV1AdapterV2.sol) `allocate` / `deallocate` / `ids` — the rebase semantics -- [morpho-org/sdks#907](https://github.com/morpho-org/sdks/pull/907) — the undeployed-contract fork-test pattern this change reuses -- [TIB-2026-06-16](./TIB-2026-06-16-shared-liquidity-target-utilization-metric.md) — the V1 read-only metrics being mirrored -- `packages/morpho-sdk/src/entities/reallocationData.ts`, `src/helpers/computeReallocations.ts`, - `src/types/sharedLiquidity.ts` — the V1 engine -- `packages/blue-sdk/src/vault/v2/VaultV2.ts` (`AccrualVaultV2.maxDeposit`), - `VaultV2MorphoMarketV1AdapterV2.ts` (`ids`) — the reused cap formula and id derivation -- `packages/blue-sdk-viem/src/fetch/VaultMarketPublicAllocatorConfig.ts` — the V1 fetcher being mirrored -- Root [`AGENTS.md`](../../AGENTS.md) §1 (layering, single source of truth), §2 (forbidden patterns), - §3 (types, typed errors), §5 (testing, security invariants), §6 (JSDoc, V1/V2 signature parity), - §7 (semver, changesets, pinned ABIs/addresses) +- [`VaultV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/VaultV2.sol) +- [`MorphoMarketV1AdapterV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/adapters/MorphoMarketV1AdapterV2.sol) +- [TIB-2026-06-16 shared-liquidity target-utilization metric](./TIB-2026-06-16-shared-liquidity-target-utilization-metric.md) +- `packages/morpho-sdk/src/entities/reallocationData.ts` +- `packages/morpho-sdk/src/entities/reallocationDataVaultV2.ts` +- `packages/morpho-sdk/src/helpers/computeReallocations.ts` +- `packages/morpho-sdk/src/helpers/computeReallocationsVaultV2.ts` +- `packages/blue-sdk/src/vault/v2/VaultV2Utils.ts` +- `packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts` diff --git a/packages/blue-sdk-viem/AGENTS.md b/packages/blue-sdk-viem/AGENTS.md index 974c6a04d..b37a87a46 100644 --- a/packages/blue-sdk-viem/AGENTS.md +++ b/packages/blue-sdk-viem/AGENTS.md @@ -12,6 +12,7 @@ - Normalize unsafe user addresses with `safeGetAddress`, not lowercasing alone. - Typed-data helpers return `TypedDataDefinition`, e.g. `getPermitTypedData(...)`. - Re-export ABI literals from `@morpho-org/morpho-ts` when they exist there; keep local ABI declarations only for Blue-specific viem surfaces absent from `morpho-ts`. +- Vault V2 BluePublicAllocator fetchers always accept the allocator address explicitly; there is no chain-address registry entry. The hydrated-vault batch fetch derives supported adapter/market/allocation ids, defaults to one deployless read, and falls back to direct reads. ## Continuous Improvement diff --git a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol new file mode 100644 index 000000000..58c0d0691 --- /dev/null +++ b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {IBluePublicAllocator} from "./interfaces/IBluePublicAllocator.sol"; +import {IVaultV2} from "./interfaces/IVaultV2.sol"; + +struct VaultV2MarketPublicAllocatorRequest { + address adapter; + bytes32 marketParamsId; +} + +struct VaultV2MarketPublicAllocatorResponse { + address adapter; + bytes32 marketParamsId; + uint256 absoluteCap; + bool canDeallocate; + bool isActiveAdapter; +} + +struct VaultV2AllocationResponse { + bytes32 id; + uint256 absoluteCap; + uint256 relativeCap; + uint256 allocation; +} + +struct VaultV2PublicAllocatorResponse { + bool canAllocateFromIdle; + uint120 nativePenalty; + VaultV2MarketPublicAllocatorResponse[] marketConfigs; + VaultV2AllocationResponse[] allocations; +} + +contract GetVaultV2PublicAllocatorConfig { + function query( + IBluePublicAllocator allocator, + IVaultV2 vault, + VaultV2MarketPublicAllocatorRequest[] calldata marketRequests, + bytes32[] calldata allocationIds + ) external view returns (VaultV2PublicAllocatorResponse memory res) { + (res.canAllocateFromIdle, res.nativePenalty,) = allocator.vaultData(address(vault)); + + uint256 marketRequestsLength = marketRequests.length; + res.marketConfigs = new VaultV2MarketPublicAllocatorResponse[](marketRequestsLength); + for (uint256 i; i < marketRequestsLength; ++i) { + VaultV2MarketPublicAllocatorRequest calldata request = marketRequests[i]; + res.marketConfigs[i] = VaultV2MarketPublicAllocatorResponse({ + adapter: request.adapter, + marketParamsId: request.marketParamsId, + absoluteCap: allocator.absoluteCap(address(vault), request.marketParamsId), + canDeallocate: allocator.canDeallocate(address(vault), request.marketParamsId), + isActiveAdapter: allocator.isActiveAdapter(address(vault), request.adapter) + }); + } + + uint256 allocationIdsLength = allocationIds.length; + res.allocations = new VaultV2AllocationResponse[](allocationIdsLength); + for (uint256 i; i < allocationIdsLength; ++i) { + bytes32 id = allocationIds[i]; + res.allocations[i] = VaultV2AllocationResponse({ + id: id, + absoluteCap: vault.absoluteCap(id), + relativeCap: vault.relativeCap(id), + allocation: vault.allocation(id) + }); + } + } +} diff --git a/packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol b/packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol new file mode 100644 index 000000000..9f90c604e --- /dev/null +++ b/packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity ^0.8.0; + +interface IBluePublicAllocator { + function absoluteCap(address vault, bytes32 id) external view returns (uint256); + function canDeallocate(address vault, bytes32 id) external view returns (bool); + function isActiveAdapter(address vault, address adapter) external view returns (bool); + function vaultData(address vault) + external + view + returns (bool canAllocateFromIdle, uint120 nativePenalty, uint120 accruedNativePenalty); +} diff --git a/packages/blue-sdk-viem/package.json b/packages/blue-sdk-viem/package.json index affb44bd3..2cc01675d 100644 --- a/packages/blue-sdk-viem/package.json +++ b/packages/blue-sdk-viem/package.json @@ -30,7 +30,7 @@ "build:esm": "tsc --build tsconfig.build.esm.json && echo '{\"type\":\"module\"}' > lib/esm/package.json" }, "peerDependencies": { - "@morpho-org/blue-sdk": "^6.4.0", + "@morpho-org/blue-sdk": "^6.5.0", "@morpho-org/morpho-ts": "^2.9.0", "viem": "^2.0.0" }, diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts new file mode 100644 index 000000000..5092aee55 --- /dev/null +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts @@ -0,0 +1,216 @@ +import { + AccrualVaultV2, + AccrualVaultV2MorphoMarketV1AdapterV2, + Market, + MarketParams, + MathLib, +} from "@morpho-org/blue-sdk"; +import { createMockClient, mockRead } from "@morpho-org/test/mock"; +import type { Address } from "viem"; +import { zeroAddress } from "viem"; +import { mainnet } from "viem/chains"; +import { describe, expect, test } from "vitest"; +import { + mockDeploylessRead, + mockDeploylessReads, +} from "../../__test__/viem.js"; +import { bluePublicAllocatorAbi, vaultV2Abi } from "../../abis.js"; +import { abi as queryAbi } from "../../queries/vault-v2/GetVaultV2PublicAllocatorConfig.js"; +import { + fetchVaultV2MarketPublicAllocatorConfig, + fetchVaultV2PublicAllocatorConfig, + fetchVaultV2PublicAllocatorData, +} from "./VaultV2PublicAllocatorConfig.js"; + +const ALLOCATOR: Address = "0x0000000000000000000000000000000000000001"; +const VAULT: Address = "0x0000000000000000000000000000000000000002"; +const ADAPTER: Address = "0x0000000000000000000000000000000000000003"; +const ASSET: Address = "0x0000000000000000000000000000000000000004"; +const IRM: Address = "0x0000000000000000000000000000000000000005"; + +const marketParams = new MarketParams({ + loanToken: ASSET, + collateralToken: "0x0000000000000000000000000000000000000006", + oracle: "0x0000000000000000000000000000000000000007", + irm: IRM, + lltv: 860_000_000_000_000_000n, +}); +const market = new Market({ + params: marketParams, + totalSupplyAssets: 100n, + totalBorrowAssets: 0n, + totalSupplyShares: 100_000_000n, + totalBorrowShares: 0n, + lastUpdate: 1n, + fee: 0n, +}); +const adapter = new AccrualVaultV2MorphoMarketV1AdapterV2( + { + address: ADAPTER, + parentVault: VAULT, + skimRecipient: zeroAddress, + marketIds: [market.id], + adaptiveCurveIrm: IRM, + supplyShares: { [market.id]: market.totalSupplyShares }, + }, + [market], +); +const vault = new AccrualVaultV2( + { + address: VAULT, + asset: ASSET, + _totalAssets: 100n, + totalSupply: 100n, + virtualShares: 0n, + maxRate: 0n, + lastUpdate: 1n, + liquidityAdapter: zeroAddress, + liquidityData: "0x", + liquidityAllocations: undefined, + performanceFee: 0n, + managementFee: 0n, + performanceFeeRecipient: zeroAddress, + managementFeeRecipient: zeroAddress, + }, + undefined, + [adapter], + 0n, + {}, +); +const ids = adapter.ids(marketParams); +const marketParamsId = ids[2]; + +const expected = { + publicAllocatorConfig: { + allocator: ALLOCATOR, + vault: VAULT, + canAllocateFromIdle: true, + nativePenalty: 12n, + }, + marketPublicAllocatorConfigs: { + [marketParamsId]: { + allocator: ALLOCATOR, + vault: VAULT, + adapter: ADAPTER, + marketParamsId, + absoluteCap: 500n, + canDeallocate: true, + isActiveAdapter: true, + }, + }, + allocations: Object.fromEntries( + ids.map((id) => [ + id, + { + id, + absoluteCap: 1_000n, + relativeCap: MathLib.WAD, + allocation: 100n, + }, + ]), + ), +}; + +const mockDirectReads = (handle: ReturnType) => { + mockRead(handle, { + address: ALLOCATOR, + abi: bluePublicAllocatorAbi, + functionName: "vaultData", + result: [true, 12n, 34n], + }); + mockRead(handle, { + address: ALLOCATOR, + abi: bluePublicAllocatorAbi, + functionName: "absoluteCap", + result: 500n, + }); + mockRead(handle, { + address: ALLOCATOR, + abi: bluePublicAllocatorAbi, + functionName: "canDeallocate", + result: true, + }); + mockRead(handle, { + address: ALLOCATOR, + abi: bluePublicAllocatorAbi, + functionName: "isActiveAdapter", + result: true, + }); + mockRead(handle, { + address: VAULT, + abi: vaultV2Abi, + functionName: "absoluteCap", + result: 1_000n, + }); + mockRead(handle, { + address: VAULT, + abi: vaultV2Abi, + functionName: "relativeCap", + result: MathLib.WAD, + }); + mockRead(handle, { + address: VAULT, + abi: vaultV2Abi, + functionName: "allocation", + result: 100n, + }); +}; + +describe("Vault V2 public allocator fetchers", () => { + test("default: leaf fetchers preserve the explicit allocator identity", async () => { + const handle = createMockClient(mainnet); + mockDirectReads(handle); + + await expect( + fetchVaultV2PublicAllocatorConfig(ALLOCATOR, VAULT, handle.client), + ).resolves.toStrictEqual(expected.publicAllocatorConfig); + await expect( + fetchVaultV2MarketPublicAllocatorConfig( + ALLOCATOR, + VAULT, + ADAPTER, + marketParamsId, + handle.client, + ), + ).resolves.toStrictEqual( + expected.marketPublicAllocatorConfigs[marketParamsId], + ); + }); + + test("behavior: deployless batching returns all derived ids", async () => { + const handle = createMockClient(mainnet); + mockDeploylessRead(handle, queryAbi, "query", { + canAllocateFromIdle: true, + nativePenalty: 12n, + marketConfigs: [ + { + adapter: ADAPTER, + marketParamsId, + absoluteCap: 500n, + canDeallocate: true, + isActiveAdapter: true, + }, + ], + allocations: ids.map((id) => ({ + id, + absoluteCap: 1_000n, + relativeCap: MathLib.WAD, + allocation: 100n, + })), + }); + + await expect( + fetchVaultV2PublicAllocatorData(ALLOCATOR, vault, handle.client), + ).resolves.toStrictEqual(expected); + }); + + test("behavior: direct-read fallback matches deployless output", async () => { + const handle = createMockClient(mainnet); + mockDeploylessReads(handle, [new Error("deployless unavailable")]); + mockDirectReads(handle); + + await expect( + fetchVaultV2PublicAllocatorData(ALLOCATOR, vault, handle.client), + ).resolves.toStrictEqual(expected); + }); +}); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts new file mode 100644 index 000000000..52e7eaf93 --- /dev/null +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts @@ -0,0 +1,296 @@ +import { + type AccrualVaultV2, + AccrualVaultV2MorphoMarketV1AdapterV2, + type IVaultV2Allocation, + type VaultV2MarketPublicAllocatorConfig, + type VaultV2PublicAllocatorConfig, +} from "@morpho-org/blue-sdk"; +import type { Address, Client, Hash } from "viem"; +import { readContract } from "viem/actions"; +import { bluePublicAllocatorAbi, vaultV2Abi } from "../../abis.js"; +import { + abi, + code, +} from "../../queries/vault-v2/GetVaultV2PublicAllocatorConfig.js"; +import type { + DeploylessFetchParameters, + FetchParameters, +} from "../../types.js"; + +/** + * Fetches a Vault V2's BluePublicAllocator-wide configuration. + * + * @param allocator - Explicit BluePublicAllocator contract address. + * @param vault - Vault V2 address. + * @param client - Viem client used for the contract read. + * @param parameters.account - Optional account passed to viem calls. + * @param parameters.blockNumber - Optional block number for historical reads. + * @param parameters.blockTag - Optional block tag for historical reads. + * @param parameters.stateOverride - Optional viem state override. + * @returns The vault's idle-allocation permission and per-call native penalty. + * @example + * ```ts + * import { fetchVaultV2PublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; + * + * const config = await fetchVaultV2PublicAllocatorConfig(allocator, vault, client); + * ``` + */ +// biome-ignore lint/complexity/useMaxParams: identity fields mirror the allocator's mapping keys +export async function fetchVaultV2PublicAllocatorConfig( + allocator: Address, + vault: Address, + client: Client, + parameters: FetchParameters = {}, +): Promise { + const [canAllocateFromIdle, nativePenalty] = await readContract(client, { + ...parameters, + address: allocator, + abi: bluePublicAllocatorAbi, + functionName: "vaultData", + args: [vault], + }); + + return { + allocator, + vault, + canAllocateFromIdle, + nativePenalty, + }; +} + +/** + * Fetches BluePublicAllocator permissions and cap state for one Vault V2 adapter-market pair. + * + * @param allocator - Explicit BluePublicAllocator contract address. + * @param vault - Vault V2 address. + * @param adapter - MorphoMarketV1AdapterV2 address. + * @param marketParamsId - Adapter-scoped market-parameters id. + * @param client - Viem client used for contract reads. + * @param parameters.account - Optional account passed to viem calls. + * @param parameters.blockNumber - Optional block number for historical reads. + * @param parameters.blockTag - Optional block tag for historical reads. + * @param parameters.stateOverride - Optional viem state override. + * @returns The allocator cap and permissions for the adapter-market pair. + * @example + * ```ts + * import { fetchVaultV2MarketPublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; + * + * const config = await fetchVaultV2MarketPublicAllocatorConfig( + * allocator, + * vault, + * adapter, + * marketParamsId, + * client, + * ); + * ``` + */ +// biome-ignore lint/complexity/useMaxParams: identity fields mirror the allocator's mapping keys +export async function fetchVaultV2MarketPublicAllocatorConfig( + allocator: Address, + vault: Address, + adapter: Address, + marketParamsId: Hash, + client: Client, + parameters: FetchParameters = {}, +): Promise { + const [absoluteCap, canDeallocate, isActiveAdapter] = await Promise.all([ + readContract(client, { + ...parameters, + address: allocator, + abi: bluePublicAllocatorAbi, + functionName: "absoluteCap", + args: [vault, marketParamsId], + }), + readContract(client, { + ...parameters, + address: allocator, + abi: bluePublicAllocatorAbi, + functionName: "canDeallocate", + args: [vault, marketParamsId], + }), + readContract(client, { + ...parameters, + address: allocator, + abi: bluePublicAllocatorAbi, + functionName: "isActiveAdapter", + args: [vault, adapter], + }), + ]); + + return { + allocator, + vault, + adapter, + marketParamsId, + absoluteCap, + canDeallocate, + isActiveAdapter, + }; +} + +/** + * Fetches all BluePublicAllocator and Vault V2 cap data needed to simulate + * reallocations for one hydrated Vault V2. + * + * Only `VaultV2MorphoMarketV1AdapterV2` adapters participate. The function + * derives every adapter-market id and shared vault allocation id from the + * hydrated vault, uses one deployless `eth_call` by default, and falls back to + * direct reads unless deployless mode is forced. + * + * @param allocator - Explicit BluePublicAllocator contract address. + * @param vault - Hydrated Vault V2 whose accrued adapters provide the candidate markets. + * @param client - Viem client used for deployless or direct reads. + * @param parameters.account - Optional account passed to viem calls. + * @param parameters.blockNumber - Optional block number for historical reads. + * @param parameters.blockTag - Optional block tag for historical reads. + * @param parameters.stateOverride - Optional viem state override. + * @param parameters.deployless - Deployless mode; defaults to `true`, with direct-read fallback. + * @returns Vault-wide config, adapter-market configs keyed by `marketParamsId`, and allocations keyed by derived id. + * @example + * ```ts + * import { fetchVaultV2PublicAllocatorData } from "@morpho-org/blue-sdk-viem"; + * + * const data = await fetchVaultV2PublicAllocatorData(allocator, vault, client); + * ``` + */ +// biome-ignore lint/complexity/useMaxParams: follows the package's address/entity/client/options fetcher convention +export async function fetchVaultV2PublicAllocatorData( + allocator: Address, + vault: AccrualVaultV2, + client: Client, + { deployless = true, ...parameters }: DeploylessFetchParameters = {}, +) { + const marketRequests: { + readonly adapter: Address; + readonly marketParamsId: Hash; + }[] = []; + const allocationIds = new Set(); + + for (const adapter of vault.accrualAdapters) { + if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) continue; + + for (const market of adapter.markets) { + const ids = adapter.ids(market.params); + marketRequests.push({ + adapter: adapter.address, + marketParamsId: ids[2], + }); + for (const id of ids) allocationIds.add(id); + } + } + + const allocationIdList = [...allocationIds]; + + if (deployless) { + try { + const result = await readContract(client, { + ...parameters, + abi, + code, + functionName: "query", + args: [allocator, vault.address, marketRequests, allocationIdList], + }); + + const marketPublicAllocatorConfigs: Record< + Hash, + VaultV2MarketPublicAllocatorConfig | undefined + > = {}; + for (const config of result.marketConfigs) { + marketPublicAllocatorConfigs[config.marketParamsId] = { + allocator, + vault: vault.address, + ...config, + }; + } + + const allocations: Record = {}; + for (const allocation of result.allocations) { + allocations[allocation.id] = allocation; + } + + return { + publicAllocatorConfig: { + allocator, + vault: vault.address, + canAllocateFromIdle: result.canAllocateFromIdle, + nativePenalty: result.nativePenalty, + } satisfies VaultV2PublicAllocatorConfig, + marketPublicAllocatorConfigs, + allocations, + }; + } catch (error) { + if (deployless === "force") throw error; + // Fall back to direct reads when deployless execution is unavailable. + } + } + + const [publicAllocatorConfig, marketConfigs, allocationValues] = + await Promise.all([ + fetchVaultV2PublicAllocatorConfig( + allocator, + vault.address, + client, + parameters, + ), + Promise.all( + marketRequests.map(({ adapter, marketParamsId }) => + fetchVaultV2MarketPublicAllocatorConfig( + allocator, + vault.address, + adapter, + marketParamsId, + client, + parameters, + ), + ), + ), + Promise.all( + allocationIdList.map(async (id) => { + const [absoluteCap, relativeCap, allocation] = await Promise.all([ + readContract(client, { + ...parameters, + address: vault.address, + abi: vaultV2Abi, + functionName: "absoluteCap", + args: [id], + }), + readContract(client, { + ...parameters, + address: vault.address, + abi: vaultV2Abi, + functionName: "relativeCap", + args: [id], + }), + readContract(client, { + ...parameters, + address: vault.address, + abi: vaultV2Abi, + functionName: "allocation", + args: [id], + }), + ]); + + return { id, absoluteCap, relativeCap, allocation }; + }), + ), + ]); + + const marketPublicAllocatorConfigs: Record< + Hash, + VaultV2MarketPublicAllocatorConfig | undefined + > = {}; + for (const config of marketConfigs) { + marketPublicAllocatorConfigs[config.marketParamsId] = config; + } + + const allocations: Record = {}; + for (const allocation of allocationValues) { + allocations[allocation.id] = allocation; + } + + return { + publicAllocatorConfig, + marketPublicAllocatorConfigs, + allocations, + }; +} diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts index dc5d27071..1a8a3080a 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts @@ -3,3 +3,4 @@ export * from "./VaultV2Adapter.js"; export * from "./VaultV2MorphoMarketV1Adapter.js"; export * from "./VaultV2MorphoMarketV1AdapterV2.js"; export * from "./VaultV2MorphoVaultV1Adapter.js"; +export * from "./VaultV2PublicAllocatorConfig.js"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts new file mode 100644 index 000000000..9b98b54c1 --- /dev/null +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts @@ -0,0 +1,124 @@ +/** @internal Deployless `GetVaultV2PublicAllocatorConfig` query ABI. */ +export const abi = [ + { + inputs: [ + { + internalType: "contract IBluePublicAllocator", + name: "allocator", + type: "address", + }, + { + internalType: "contract IVaultV2", + name: "vault", + type: "address", + }, + { + components: [ + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + internalType: "bytes32", + name: "marketParamsId", + type: "bytes32", + }, + ], + internalType: "struct VaultV2MarketPublicAllocatorRequest[]", + name: "marketRequests", + type: "tuple[]", + }, + { + internalType: "bytes32[]", + name: "allocationIds", + type: "bytes32[]", + }, + ], + name: "query", + outputs: [ + { + components: [ + { + internalType: "bool", + name: "canAllocateFromIdle", + type: "bool", + }, + { + internalType: "uint120", + name: "nativePenalty", + type: "uint120", + }, + { + components: [ + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + internalType: "bytes32", + name: "marketParamsId", + type: "bytes32", + }, + { + internalType: "uint256", + name: "absoluteCap", + type: "uint256", + }, + { + internalType: "bool", + name: "canDeallocate", + type: "bool", + }, + { + internalType: "bool", + name: "isActiveAdapter", + type: "bool", + }, + ], + internalType: "struct VaultV2MarketPublicAllocatorResponse[]", + name: "marketConfigs", + type: "tuple[]", + }, + { + components: [ + { + internalType: "bytes32", + name: "id", + type: "bytes32", + }, + { + internalType: "uint256", + name: "absoluteCap", + type: "uint256", + }, + { + internalType: "uint256", + name: "relativeCap", + type: "uint256", + }, + { + internalType: "uint256", + name: "allocation", + type: "uint256", + }, + ], + internalType: "struct VaultV2AllocationResponse[]", + name: "allocations", + type: "tuple[]", + }, + ], + internalType: "struct VaultV2PublicAllocatorResponse", + name: "res", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +/** @internal Deployless `GetVaultV2PublicAllocatorConfig` query bytecode. */ +export const code = + "0x6080806040523460155761081c908161001a8239f35b5f80fdfe60a0806040526004361015610012575f80fd5b5f3560e01c635938912f14610025575f80fd5b34610309576080366003190112610309576004356001600160a01b038116608081905203610309576024356001600160a01b03811690819003610309576044359067ffffffffffffffff821161030957366023830112156103095781600401359167ffffffffffffffff8311610309573660248460061b83010111610309576064359367ffffffffffffffff851161030957366023860112156103095784600401359067ffffffffffffffff8211610309573660248360051b88010111610309576100ef81610703565b5f815260208101955f8752604082019660608852606083019260608452604051636b97fbcd60e11b81528760048201526060816024816080515afa8015610315575f915f916106a6575b506001600160781b031683521515815261015288610792565b61015f604051918261074f565b888152601f1961016e8a610792565b015f5b81811061067157505089525f5b88811015610398578060061b870190610199602483016107aa565b60405163011f009b60e31b81526001600160a01b038b166004820152604480850135602483018190529294919060209082908190810103816080515afa908115610315578c84915f93610361575b5060405163258969d960e11b81526001600160a01b03909116600482015260248101919091529160208380604481015b03816080515afa928315610315575f93610320575b50602461023991016107aa565b92604051936366faa83960e01b85528d600486015260018060a01b031660248501526020846044816080515afa938415610315575f946102c8575b50928492600196926102c1956040519461028d86610733565b898060a01b031685526020850152604084015215156060830152151560808201528d51906102bb83836107be565b526107be565b500161017e565b92959193506020833d821161030d575b816102e56020938361074f565b81010312610309576001956102c1946102fe8795610771565b955092965092610274565b5f80fd5b3d91506102d8565b6040513d5f823e3d90fd5b9092506020813d8211610359575b8161033b6020938361074f565b8101031261030957602461035161023992610771565b93915061022c565b3d915061032e565b925050506020813d8211610390575b8161037d6020938361074f565b810103126103095751828c6102176101e7565b3d9150610370565b50869550886103a686610792565b6103b3604051918261074f565b868152601f196103c288610792565b015f5b81811061064257505085525f5b868110156105405760248160051b860101359060405191632f0374dd60e21b83528060048401526020836024818d5afa928315610315575f9361050d575b5060405163a68bafa360e01b8152600481018290526020816024818e5afa8015610315575f906104db575b60405163c69507dd60e01b81526004810184905291506020826024818f5afa918215610315575f926104a5575b5091839161049e936001966040519361048085610703565b84526020840152604083015260608201528951906102bb83836107be565b50016103d2565b9150916020823d82116104d3575b816104c06020938361074f565b8101031261030957905190916001610468565b3d91506104b3565b506020813d8211610505575b816104f46020938361074f565b81010312610309576024905161043b565b3d91506104e7565b9092506020813d8211610538575b816105286020938361074f565b810103126103095751918a610410565b3d915061051b565b50604080516020808252935115158482015293516001600160781b0316908401525160806060840152805160a08401819052839260c084019287929101905f5b8181106105ef575050505190601f19838203016080840152602080835192838152019201905f5b8181106105b5575050500390f35b91935091602060806001926060875180518352848101518584015260408101516040840152015160608201520194019101918493926105a7565b825180516001600160a01b03168652602081810151818801526040808301519088015260608083015115159088015260809182015115159187019190915287965060a09095019490920191600101610580565b60209060405161065181610703565b5f81525f838201525f60408201525f6060820152828286010152016103c5565b60209060405161068081610733565b5f81525f838201525f60408201525f60608201525f608082015282828601015201610171565b9150506060813d6060116106fb575b816106c26060938361074f565b81010312610309576001600160781b036106db82610771565b6106f360406106ec6020860161077e565b940161077e565b509190610139565b3d91506106b5565b6080810190811067ffffffffffffffff82111761071f57604052565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761071f57604052565b90601f8019910116810190811067ffffffffffffffff82111761071f57604052565b5190811515820361030957565b51906001600160781b038216820361030957565b67ffffffffffffffff811161071f5760051b60200190565b356001600160a01b03811681036103095790565b80518210156107d25760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220047f1c0eede09b101d0fb8a8e9baf5e9bee81891a384b3c9f6d1cbccb91684ab64736f6c63430008240033"; diff --git a/packages/blue-sdk/AGENTS.md b/packages/blue-sdk/AGENTS.md index 31e1f3f49..bd0aa56f4 100644 --- a/packages/blue-sdk/AGENTS.md +++ b/packages/blue-sdk/AGENTS.md @@ -10,6 +10,8 @@ - Use `_try(accessor, UnknownError)` for optional domain lookups, not broad `catch`. - Protocol entity folders (`market/`, `vault/`, `token/`, `position/`, `holding/`, `user/`) own their classes and folder barrels. - Getters may throw typed `Unknown*Error`; nullable lookup paths should use `_try` or `tryGet*`-style helpers deliberately. +- Vault V2 absolute/relative allocation-cap math is canonical in `VaultV2Utils.allocationHeadroom`; consumers such as `AccrualVaultV2.maxDeposit` and shared-liquidity simulation delegate to it. +- Vault V2 BluePublicAllocator config interfaces are readonly identity-bearing projections: they include the explicit allocator and vault, plus the adapter and derived market-params id for pair-scoped state. ## Continuous Improvement diff --git a/packages/blue-sdk/src/vault/v2/VaultV2.ts b/packages/blue-sdk/src/vault/v2/VaultV2.ts index da75e3c9f..baf68d71b 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2.ts @@ -5,6 +5,7 @@ import { type IToken, WrappedToken } from "../../token/index.js"; import type { BigIntish } from "../../types.js"; import { type CapacityLimit, CapacityLimitReason } from "../../utils.js"; import type { IAccrualVaultV2Adapter } from "./VaultV2Adapter.js"; +import { VaultV2Utils } from "./VaultV2Utils.js"; /** Plain input shape for one Vault V2 liquidity allocation. */ export interface IVaultV2Allocation { @@ -189,28 +190,13 @@ export class AccrualVaultV2 extends VaultV2 implements IAccrualVaultV2 { // At this stage: `liquidityAdapterLimit.value <= assets` - for (const { absoluteCap, relativeCap, allocation } of this - .liquidityAllocations) { - // `absoluteCap` can be set lower than `allocation`. - const absoluteMaxDeposit = MathLib.zeroFloorSub(absoluteCap, allocation); - if (liquidityAdapterLimit.value > absoluteMaxDeposit) - liquidityAdapterLimit = { - value: absoluteMaxDeposit, - limiter: CapacityLimitReason.vaultV2_absoluteCap, - }; - - if (relativeCap !== MathLib.WAD) { - // `relativeCap` can be set lower than `allocation / _totalAssets`. - const relativeMaxDeposit = MathLib.zeroFloorSub( - MathLib.wMulDown(this._totalAssets, relativeCap), - allocation, - ); - if (liquidityAdapterLimit.value > relativeMaxDeposit) - liquidityAdapterLimit = { - value: relativeMaxDeposit, - limiter: CapacityLimitReason.vaultV2_relativeCap, - }; - } + for (const allocation of this.liquidityAllocations) { + const allocationLimit = VaultV2Utils.allocationHeadroom( + allocation, + this._totalAssets, + ); + if (liquidityAdapterLimit.value > allocationLimit.value) + liquidityAdapterLimit = allocationLimit; } return liquidityAdapterLimit; diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts index fa243d5b1..19f355a0a 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts @@ -1,4 +1,10 @@ -import { type Address, encodeAbiParameters, type Hex, keccak256 } from "viem"; +import { + type Address, + encodeAbiParameters, + type Hash, + type Hex, + keccak256, +} from "viem"; import { type Market, MarketParams, @@ -78,7 +84,7 @@ export class VaultV2MorphoMarketV1AdapterV2 this.supplyShares = supplyShares; } - public ids(params: MarketParams) { + public ids(params: MarketParams): readonly [Hash, Hash, Hash] { return [ this.adapterId, VaultV2MorphoMarketV1AdapterV2.collateralId(params.collateralToken), diff --git a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts new file mode 100644 index 000000000..23ee39e1b --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts @@ -0,0 +1,31 @@ +import type { Address, Hash } from "viem"; + +/** Public allocator configuration for one Vault V2. */ +export interface VaultV2PublicAllocatorConfig { + /** BluePublicAllocator contract from which the configuration was read. */ + readonly allocator: Address; + /** Configured Vault V2 address. */ + readonly vault: Address; + /** Whether the allocator may move the vault's idle assets into a Blue market. */ + readonly canAllocateFromIdle: boolean; + /** Native-token penalty charged for each allocator call. */ + readonly nativePenalty: bigint; +} + +/** Public allocator permissions and cap for one Vault V2 adapter-market pair. */ +export interface VaultV2MarketPublicAllocatorConfig { + /** BluePublicAllocator contract from which the configuration was read. */ + readonly allocator: Address; + /** Configured Vault V2 address. */ + readonly vault: Address; + /** Vault V2 MorphoMarketV1AdapterV2 address. */ + readonly adapter: Address; + /** Adapter-scoped `marketParamsId` used by the allocator mappings. */ + readonly marketParamsId: Hash; + /** Maximum post-state allocation accepted by the allocator. */ + readonly absoluteCap: bigint; + /** Whether the allocator may deallocate this adapter-market pair. */ + readonly canDeallocate: boolean; + /** Whether the allocator currently recognizes the adapter. */ + readonly isActiveAdapter: boolean; +} diff --git a/packages/blue-sdk/src/vault/v2/VaultV2Utils.test.ts b/packages/blue-sdk/src/vault/v2/VaultV2Utils.test.ts new file mode 100644 index 000000000..ac55b48ce --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2Utils.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, test } from "vitest"; +import { MathLib } from "../../math/index.js"; +import { CapacityLimitReason } from "../../utils.js"; +import { VaultV2Utils } from "./VaultV2Utils.js"; + +const allocation = { + id: `0x${"01".repeat(32)}` as const, + absoluteCap: 1_000n, + relativeCap: MathLib.WAD, + allocation: 400n, +}; + +describe("VaultV2Utils.allocationHeadroom", () => { + test("default: returns absolute-cap headroom", () => { + expect(VaultV2Utils.allocationHeadroom(allocation, 1_000n)).toStrictEqual({ + value: 600n, + limiter: CapacityLimitReason.vaultV2_absoluteCap, + }); + }); + + test("behavior: returns relative-cap headroom when it binds", () => { + expect( + VaultV2Utils.allocationHeadroom( + { ...allocation, relativeCap: MathLib.WAD / 2n }, + 1_000n, + ), + ).toStrictEqual({ + value: 100n, + limiter: CapacityLimitReason.vaultV2_relativeCap, + }); + }); + + test("behavior: floors caps below the live allocation at zero", () => { + expect( + VaultV2Utils.allocationHeadroom( + { + ...allocation, + absoluteCap: 300n, + relativeCap: MathLib.WAD / 4n, + }, + 1_000n, + ), + ).toStrictEqual({ + value: 0n, + limiter: CapacityLimitReason.vaultV2_absoluteCap, + }); + }); + + test("behavior: WAD relative caps do not constrain absolute headroom", () => { + expect( + VaultV2Utils.allocationHeadroom( + { ...allocation, absoluteCap: 2_000n, relativeCap: MathLib.WAD }, + 500n, + ), + ).toStrictEqual({ + value: 1_600n, + limiter: CapacityLimitReason.vaultV2_absoluteCap, + }); + }); +}); diff --git a/packages/blue-sdk/src/vault/v2/VaultV2Utils.ts b/packages/blue-sdk/src/vault/v2/VaultV2Utils.ts new file mode 100644 index 000000000..7b4423668 --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2Utils.ts @@ -0,0 +1,54 @@ +import { MathLib } from "../../math/index.js"; +import type { BigIntish } from "../../types.js"; +import { type CapacityLimit, CapacityLimitReason } from "../../utils.js"; +import type { IVaultV2Allocation } from "./VaultV2.js"; + +/** Deterministic helpers for Vault V2 allocation accounting. */ +export namespace VaultV2Utils { + /** + * Computes the remaining assets permitted by one Vault V2 allocation's + * absolute and relative caps. + * + * @param allocation - Current allocation and its absolute and relative caps. + * @param firstTotalAssets - Transaction-frozen Vault V2 total assets used as the relative-cap denominator. + * @returns The remaining allocation capacity and the cap that binds it. + * @example + * ```ts + * import { VaultV2Utils } from "@morpho-org/blue-sdk"; + * + * const headroom = VaultV2Utils.allocationHeadroom( + * { id: "0x0000000000000000000000000000000000000000000000000000000000000000", absoluteCap: 100n, relativeCap: 500000000000000000n, allocation: 40n }, + * 160n, + * ); + * // headroom.value === 40n + * ``` + */ + export function allocationHeadroom( + allocation: Readonly, + firstTotalAssets: BigIntish, + ): CapacityLimit { + const absoluteHeadroom = MathLib.zeroFloorSub( + allocation.absoluteCap, + allocation.allocation, + ); + let limit: CapacityLimit = { + value: absoluteHeadroom, + limiter: CapacityLimitReason.vaultV2_absoluteCap, + }; + + if (allocation.relativeCap !== MathLib.WAD) { + const relativeHeadroom = MathLib.zeroFloorSub( + MathLib.wMulDown(BigInt(firstTotalAssets), allocation.relativeCap), + allocation.allocation, + ); + if (relativeHeadroom < limit.value) { + limit = { + value: relativeHeadroom, + limiter: CapacityLimitReason.vaultV2_relativeCap, + }; + } + } + + return limit; + } +} diff --git a/packages/blue-sdk/src/vault/v2/index.ts b/packages/blue-sdk/src/vault/v2/index.ts index dc5d27071..ac16936a8 100644 --- a/packages/blue-sdk/src/vault/v2/index.ts +++ b/packages/blue-sdk/src/vault/v2/index.ts @@ -3,3 +3,5 @@ export * from "./VaultV2Adapter.js"; export * from "./VaultV2MorphoMarketV1Adapter.js"; export * from "./VaultV2MorphoMarketV1AdapterV2.js"; export * from "./VaultV2MorphoVaultV1Adapter.js"; +export * from "./VaultV2PublicAllocatorConfig.js"; +export * from "./VaultV2Utils.js"; diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index 61a4f80ee..a2e1442e1 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -29,6 +29,7 @@ Protocol terms used across this package's docs and JSDoc: - **PublicAllocator V1** — MetaMorpho allocator that moves liquidity from one or more sorted source markets into a target via `reallocateTo(...)`; each call pays one `fee`. - **BluePublicAllocator** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered; each call pays its own `nativePenalty`. - **VaultExitBundlesV1** — standalone periphery for exiting an illiquid VaultV1 or single-adapter VaultV2 into idle underlying assets and/or Morpho Blue supply positions. +- **Shared-liquidity naming** — `ReallocationData.computeVaultV1Reallocations` and `VaultV1BlueReallocation` are canonical for PublicAllocator V1 (`getMarketPublicReallocations` and `VaultReallocation` are deprecated aliases). `ReallocationDataVaultV2.computeVaultV2Reallocations` and `computeReallocationsVaultV2` produce flat, action-ready `VaultV2BlueReallocation` calls. ### Bundler actions diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index 306904bd7..b85ac93da 100644 --- a/packages/morpho-sdk/src/actions/AGENTS.md +++ b/packages/morpho-sdk/src/actions/AGENTS.md @@ -24,7 +24,7 @@ Only valid for assets/collateral configured as wNative. When `nativeAmount > 0`: ## Shared liquidity / reallocations (canonical statement) -`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action. Tagged `BluePublicAllocatorReallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call pays its own `nativePenalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. `BundlerAction.encodeBundle` sums V1 fees and BluePublicAllocator penalties into `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level or BluePublicAllocator-source discriminators. +`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultV1BlueReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. Tagged `VaultV2BlueReallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call pays its own `nativePenalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. `BundlerAction.encodeBundle` sums V1 fees and BluePublicAllocator penalties into `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level or BluePublicAllocator-source discriminators. ## Discriminated unions diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index c73ccba76..a53db18d2 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -85,7 +85,7 @@ import { type RequirementSignature, selectRequirementSignatures, type Transaction, - type VaultReallocation, + type VaultV1BlueReallocation, WithdrawExceedsCollateralError, } from "../../types/index.js"; import { ReallocationData } from "../reallocationData.js"; @@ -477,7 +477,7 @@ export interface BlueActions { * fees) into the resulting bundle. * * The returned reallocation data can be passed to {@link getReallocations} - * to compute the `VaultReallocation[]` array for `borrow()` or + * to compute the `VaultV1BlueReallocation[]` array for `borrow()` or * `supplyCollateralBorrow()`. * * **Stale data reverts on-chain (fail-safe).** @@ -537,7 +537,7 @@ export interface BlueActions { amount?: never; } ), - ) => readonly VaultReallocation[]; + ) => readonly VaultV1BlueReallocation[]; } export class MorphoBlue implements BlueActions { @@ -1816,7 +1816,7 @@ export class MorphoBlue implements BlueActions { amount?: never; } ), - ): readonly VaultReallocation[] { + ): readonly VaultV1BlueReallocation[] { validateChainId(params.reallocationData.chainId, this.chainId); const marketId = this.marketParams.id; diff --git a/packages/morpho-sdk/src/entities/index.ts b/packages/morpho-sdk/src/entities/index.ts index 2842fba0b..1bf8e9e30 100644 --- a/packages/morpho-sdk/src/entities/index.ts +++ b/packages/morpho-sdk/src/entities/index.ts @@ -71,5 +71,9 @@ export { type InputReallocationData, ReallocationData, } from "./reallocationData.js"; +export { + type InputReallocationDataVaultV2, + ReallocationDataVaultV2, +} from "./reallocationDataVaultV2.js"; export { MorphoVaultV1 } from "./vaultV1/index.js"; export { MorphoVaultV2 } from "./vaultV2/index.js"; diff --git a/packages/morpho-sdk/src/entities/reallocationData.metrics.test.ts b/packages/morpho-sdk/src/entities/reallocationData.metrics.test.ts index 4506378e9..acee98b1f 100644 --- a/packages/morpho-sdk/src/entities/reallocationData.metrics.test.ts +++ b/packages/morpho-sdk/src/entities/reallocationData.metrics.test.ts @@ -46,7 +46,7 @@ function makeData(targetMarket = makeMarket()) { } /** - * Stubs `getMarketPublicReallocations` so the metric methods can be unit-tested + * Stubs `computeVaultV1Reallocations` so the metric methods can be unit-tested * in isolation. The discovery algorithm itself is covered by * `reallocationData.test.ts`. The stub mimics the `enabled: false` short-circuit. */ @@ -55,7 +55,7 @@ function stubReallocations( withdrawals: readonly PublicReallocation[], ) { return vi - .spyOn(data, "getMarketPublicReallocations") + .spyOn(data, "computeVaultV1Reallocations") .mockImplementation((_marketId, options?: PublicAllocatorOptions) => ({ withdrawals: options?.enabled === false ? [] : withdrawals, data, diff --git a/packages/morpho-sdk/src/entities/reallocationData.test.ts b/packages/morpho-sdk/src/entities/reallocationData.test.ts index acf133906..763b23f59 100644 --- a/packages/morpho-sdk/src/entities/reallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/reallocationData.test.ts @@ -257,6 +257,26 @@ const applyPublicReallocation = ( }); describe("ReallocationData unit coverage", () => { + test("computeVaultV1Reallocations preserves the deprecated alias behavior", () => { + const input = { + targetSupply: 1_000n * MathLib.WAD, + targetBorrow: 500n * MathLib.WAD, + sourceSupply: 1_000n * MathLib.WAD, + sourceBorrow: 500n * MathLib.WAD, + }; + const canonical = new ReallocationData( + makeInput(input), + ).computeVaultV1Reallocations(targetParams.id, { timestamp: TIMESTAMP }); + const deprecated = new ReallocationData( + makeInput(input), + ).getMarketPublicReallocations(targetParams.id, { timestamp: TIMESTAMP }); + + expect(deprecated.withdrawals).toStrictEqual(canonical.withdrawals); + expect(deprecated.data.markets).toStrictEqual(canonical.data.markets); + expect(deprecated.data.positions).toStrictEqual(canonical.data.positions); + expect(deprecated.data.vaults).toStrictEqual(canonical.data.vaults); + }); + test("preserves documented entity fields when cloning inputs", () => { const eip5267Domain = new Eip5267Domain({ fields: "0x1f", diff --git a/packages/morpho-sdk/src/entities/reallocationData.ts b/packages/morpho-sdk/src/entities/reallocationData.ts index fb92da166..496adaab0 100644 --- a/packages/morpho-sdk/src/entities/reallocationData.ts +++ b/packages/morpho-sdk/src/entities/reallocationData.ts @@ -361,12 +361,12 @@ export class ReallocationData implements InputReallocationData { * const result: { * withdrawals: readonly PublicReallocation[]; * data: ReallocationData; - * } = reallocationData.getMarketPublicReallocations(marketParams.id, { + * } = reallocationData.computeVaultV1Reallocations(marketParams.id, { * timestamp: block.timestamp, * }); * ``` */ - public getMarketPublicReallocations( + public computeVaultV1Reallocations( marketId: MarketId, options: PublicAllocatorOptions = {}, ): { @@ -467,6 +467,22 @@ export class ReallocationData implements InputReallocationData { } } + /** + * Calculates Vault V1 public reallocations that can supply liquidity to `marketId`. + * + * @param marketId - Target market to supply with shared liquidity. + * @param options - Optional allocator discovery options. + * @returns Computed source-market withdrawals and the post-reallocation state. + * @throws {@link UnknownReallocationMarketError} when the target market is absent. + * @deprecated Use {@link computeVaultV1Reallocations} instead. + */ + public getMarketPublicReallocations( + marketId: MarketId, + options: PublicAllocatorOptions = {}, + ) { + return this.computeVaultV1Reallocations(marketId, options); + } + /** * Sums the public-allocator liquidity reallocatable into `marketId` from * sibling markets. @@ -510,10 +526,7 @@ export class ReallocationData implements InputReallocationData { marketId: MarketId, options?: PublicAllocatorOptions, ): bigint { - const { withdrawals } = this.getMarketPublicReallocations( - marketId, - options, - ); + const { withdrawals } = this.computeVaultV1Reallocations(marketId, options); return withdrawals.reduce((total, { assets }) => total + assets, 0n); } diff --git a/packages/morpho-sdk/src/entities/reallocationDataVaultV2.test.ts b/packages/morpho-sdk/src/entities/reallocationDataVaultV2.test.ts new file mode 100644 index 000000000..8fdd91c25 --- /dev/null +++ b/packages/morpho-sdk/src/entities/reallocationDataVaultV2.test.ts @@ -0,0 +1,626 @@ +import { + AccrualVaultV2, + AccrualVaultV2MorphoMarketV1AdapterV2, + ChainId, + type IVaultV2Allocation, + Market, + MarketParams, + MathLib, +} from "@morpho-org/blue-sdk"; +import type { Address, Hash } from "viem"; +import { zeroAddress } from "viem"; +import { describe, expect, test } from "vitest"; +import { blueBorrow } from "../actions/index.js"; +import { computeReallocationsVaultV2 } from "../helpers/index.js"; +import { + InsufficientSharedLiquidityError, + ReallocationWithdrawExceedsMarketSupplyError, +} from "../types/index.js"; +import { ReallocationDataVaultV2 } from "./reallocationDataVaultV2.js"; + +const TIMESTAMP = 1_700_000_000n; +const ALLOCATOR = "0x0000000000000000000000000000000000000001"; +const VAULT = "0x0000000000000000000000000000000000000002"; +const TARGET_ADAPTER = "0x0000000000000000000000000000000000000003"; +const SOURCE_ADAPTER = "0x0000000000000000000000000000000000000004"; +const LOAN_TOKEN = "0x0000000000000000000000000000000000000005"; +const IRM = "0x0000000000000000000000000000000000000006"; + +const targetParams = new MarketParams({ + loanToken: LOAN_TOKEN, + collateralToken: "0x0000000000000000000000000000000000000007", + oracle: "0x0000000000000000000000000000000000000008", + irm: IRM, + lltv: 860_000_000_000_000_000n, +}); + +const sourceParams = new MarketParams({ + loanToken: LOAN_TOKEN, + collateralToken: "0x0000000000000000000000000000000000000009", + oracle: "0x000000000000000000000000000000000000000A", + irm: IRM, + lltv: 860_000_000_000_000_000n, +}); + +const makeMarket = ({ + params, + supply, + borrow, +}: { + readonly params: MarketParams; + readonly supply: bigint; + readonly borrow: bigint; +}) => + new Market({ + params, + totalSupplyAssets: supply, + totalBorrowAssets: borrow, + totalSupplyShares: supply * 1_000_000n, + totalBorrowShares: borrow * 1_000_000n, + lastUpdate: TIMESTAMP, + fee: 0n, + }); + +interface FixtureOptions { + readonly sourceMarketParams?: MarketParams; + readonly sourceAdapter?: Address; + readonly sourceSupply?: bigint; + readonly sourceBorrow?: bigint; + readonly sourceUntracked?: bigint; + readonly targetSupply?: bigint; + readonly targetBorrow?: bigint; + readonly targetPositionAssets?: bigint; + readonly targetUntracked?: bigint; + readonly targetCaps?: readonly [ + { readonly absoluteCap: bigint; readonly relativeCap: bigint }, + { readonly absoluteCap: bigint; readonly relativeCap: bigint }, + { readonly absoluteCap: bigint; readonly relativeCap: bigint }, + ]; + readonly allocatorTargetCap?: bigint; + readonly firstTotalAssets?: bigint; + readonly idle?: bigint; + readonly canAllocateFromIdle?: boolean; + readonly canDeallocate?: boolean; + readonly nativePenalty?: bigint; +} + +const makeFixture = ({ + sourceMarketParams = sourceParams, + sourceAdapter: sourceAdapterAddress = SOURCE_ADAPTER, + sourceSupply = 1_000n, + sourceBorrow = 0n, + sourceUntracked = 0n, + targetSupply = 100n, + targetBorrow = 0n, + targetPositionAssets = 0n, + targetUntracked = 0n, + targetCaps = [ + { absoluteCap: 10_000n, relativeCap: MathLib.WAD }, + { absoluteCap: 10_000n, relativeCap: MathLib.WAD }, + { absoluteCap: 10_000n, relativeCap: MathLib.WAD }, + ], + allocatorTargetCap = 10_000n, + firstTotalAssets, + idle = 0n, + canAllocateFromIdle = true, + canDeallocate = true, + nativePenalty = 7n, +}: FixtureOptions = {}) => { + const sameMarket = sourceMarketParams.id === targetParams.id; + const targetMarket = makeMarket({ + params: targetParams, + supply: sameMarket ? sourceSupply : targetSupply, + borrow: sameMarket ? sourceBorrow : targetBorrow, + }); + const sourceMarket = sameMarket + ? targetMarket + : makeMarket({ + params: sourceMarketParams, + supply: sourceSupply, + borrow: sourceBorrow, + }); + const targetSupplyShares = targetMarket.toSupplyShares( + targetPositionAssets, + "Down", + ); + const sourceSupplyShares = sourceMarket.toSupplyShares(sourceSupply, "Down"); + const targetExpectedAssets = targetMarket.toSupplyAssets(targetSupplyShares); + const sourceExpectedAssets = sourceMarket.toSupplyAssets(sourceSupplyShares); + + const targetAdapter = new AccrualVaultV2MorphoMarketV1AdapterV2( + { + address: TARGET_ADAPTER, + parentVault: VAULT, + skimRecipient: zeroAddress, + marketIds: [targetMarket.id], + adaptiveCurveIrm: IRM, + supplyShares: { [targetMarket.id]: targetSupplyShares }, + }, + [targetMarket], + ); + const sourceAdapter = new AccrualVaultV2MorphoMarketV1AdapterV2( + { + address: sourceAdapterAddress, + parentVault: VAULT, + skimRecipient: zeroAddress, + marketIds: [sourceMarket.id], + adaptiveCurveIrm: IRM, + supplyShares: { [sourceMarket.id]: sourceSupplyShares }, + }, + [sourceMarket], + ); + const targetIds = targetAdapter.ids(targetMarket.params); + const sourceIds = sourceAdapter.ids(sourceMarket.params); + const allocations: Record = {}; + + const addAllocation = ({ + id, + allocation, + cap, + }: { + readonly id: Hash; + readonly allocation: bigint; + readonly cap: { + readonly absoluteCap: bigint; + readonly relativeCap: bigint; + }; + }) => { + const current = allocations[id]; + allocations[id] = { + id, + absoluteCap: cap.absoluteCap, + relativeCap: cap.relativeCap, + allocation: (current?.allocation ?? 0n) + allocation, + }; + }; + + for (const id of sourceIds) { + addAllocation({ + id, + allocation: sourceExpectedAssets - sourceUntracked, + cap: { + absoluteCap: 10_000n, + relativeCap: MathLib.WAD, + }, + }); + } + for (const [index, id] of targetIds.entries()) { + addAllocation({ + id, + allocation: targetExpectedAssets - targetUntracked, + cap: targetCaps[index]!, + }); + } + + const adapters = + sourceAdapterAddress === TARGET_ADAPTER + ? [ + new AccrualVaultV2MorphoMarketV1AdapterV2( + { + address: TARGET_ADAPTER, + parentVault: VAULT, + skimRecipient: zeroAddress, + marketIds: Array.from( + new Set([targetMarket.id, sourceMarket.id]), + ), + adaptiveCurveIrm: IRM, + supplyShares: { + [targetMarket.id]: targetSupplyShares, + [sourceMarket.id]: sourceSupplyShares, + }, + }, + sameMarket ? [targetMarket] : [targetMarket, sourceMarket], + ), + ] + : [targetAdapter, sourceAdapter]; + const totalAssets = + firstTotalAssets ?? sourceExpectedAssets + targetExpectedAssets + idle; + const vault = new AccrualVaultV2( + { + address: VAULT, + name: "Vault V2", + symbol: "v2", + decimals: 18, + asset: LOAN_TOKEN, + _totalAssets: totalAssets, + totalSupply: totalAssets, + virtualShares: 0n, + maxRate: 0n, + lastUpdate: TIMESTAMP, + liquidityAdapter: zeroAddress, + liquidityData: "0x", + liquidityAllocations: undefined, + performanceFee: 0n, + managementFee: 0n, + performanceFeeRecipient: zeroAddress, + managementFeeRecipient: zeroAddress, + }, + undefined, + adapters, + idle, + {}, + ); + + return { + data: new ReallocationDataVaultV2({ + chainId: ChainId.EthMainnet, + allocator: ALLOCATOR, + markets: { + [targetMarket.id]: targetMarket, + [sourceMarket.id]: sourceMarket, + }, + vaults: { [VAULT]: vault }, + allocations: { [VAULT]: allocations }, + publicAllocatorConfigs: { + [VAULT]: { + allocator: ALLOCATOR, + vault: VAULT, + canAllocateFromIdle, + nativePenalty, + }, + }, + marketPublicAllocatorConfigs: { + [VAULT]: { + [targetIds[2]]: { + allocator: ALLOCATOR, + vault: VAULT, + adapter: TARGET_ADAPTER, + marketParamsId: targetIds[2], + absoluteCap: allocatorTargetCap, + canDeallocate: false, + isActiveAdapter: true, + }, + [sourceIds[2]]: { + allocator: ALLOCATOR, + vault: VAULT, + adapter: sourceAdapterAddress, + marketParamsId: sourceIds[2], + absoluteCap: 0n, + canDeallocate, + isActiveAdapter: true, + }, + }, + }, + }), + sourceExpectedAssets, + sourceIds, + targetExpectedAssets, + targetIds, + }; +}; + +describe("ReallocationDataVaultV2.computeVaultV2Reallocations", () => { + test("default: returns an action-ready market reallocation and cloned post-state", () => { + const { data, sourceExpectedAssets, sourceIds, targetIds } = makeFixture(); + + const result = data.computeVaultV2Reallocations(targetParams.id); + + expect(result.reallocations).toStrictEqual([ + { + allocator: ALLOCATOR, + type: "bluePublicAllocator", + vault: VAULT, + from: { + type: "market", + adapter: SOURCE_ADAPTER, + marketParams: sourceParams, + }, + to: { adapter: TARGET_ADAPTER }, + assets: sourceExpectedAssets, + nativePenalty: 7n, + }, + ]); + expect(result.data).not.toBe(data); + expect(result.data.getAllocation(VAULT, sourceIds[2]).allocation).toBe(0n); + expect(result.data.getAllocation(VAULT, targetIds[2]).allocation).toBe( + sourceExpectedAssets, + ); + expect(result.data.getVault(VAULT)._totalAssets).toBe( + data.getVault(VAULT)._totalAssets, + ); + }); + + test("behavior: ranks market liquidity before idle and depletes both sources", () => { + const { data, sourceExpectedAssets } = makeFixture({ idle: 300n }); + + const result = data.computeVaultV2Reallocations(targetParams.id); + + expect( + result.reallocations.map(({ from, assets, nativePenalty }) => ({ + from: from.type, + assets, + nativePenalty, + })), + ).toStrictEqual([ + { from: "market", assets: sourceExpectedAssets, nativePenalty: 7n }, + { from: "idle", assets: 300n, nativePenalty: 7n }, + ]); + expect(result.data.getVault(VAULT).assetBalance).toBe(0n); + }); + + test("behavior: permits the target market through a different adapter", () => { + const { data, sourceExpectedAssets } = makeFixture({ + sourceMarketParams: targetParams, + }); + + expect( + data.computeVaultV2Reallocations(targetParams.id).reallocations, + ).toMatchObject([ + { + from: { type: "market", adapter: SOURCE_ADAPTER }, + to: { adapter: TARGET_ADAPTER }, + assets: sourceExpectedAssets, + }, + ]); + }); + + test("behavior: allows deallocation assets to exceed stored allocation", () => { + const { data, sourceExpectedAssets } = makeFixture({ + sourceUntracked: 900n, + }); + + const result = data.computeVaultV2Reallocations(targetParams.id); + + expect(result.reallocations[0]?.assets).toBe(sourceExpectedAssets); + expect(result.data.getVault(VAULT).assetBalance).toBe(0n); + }); + + test("behavior: target untracked interest consumes allocator headroom", () => { + const { data } = makeFixture({ + targetPositionAssets: 100n, + targetUntracked: 10n, + allocatorTargetCap: 100n, + }); + + expect( + data.computeVaultV2Reallocations(targetParams.id).reallocations, + ).toStrictEqual([]); + }); + + test("behavior: shared collateral ids retain both markets' untracked interest", () => { + const sharedCollateralSource = new MarketParams({ + ...sourceParams, + collateralToken: targetParams.collateralToken, + }); + const { data } = makeFixture({ + sourceMarketParams: sharedCollateralSource, + sourceUntracked: 10n, + targetPositionAssets: 100n, + targetUntracked: 20n, + targetCaps: [ + { absoluteCap: 10_000n, relativeCap: MathLib.WAD }, + { absoluteCap: 1_099n, relativeCap: MathLib.WAD }, + { absoluteCap: 10_000n, relativeCap: MathLib.WAD }, + ], + }); + + expect( + data.computeVaultV2Reallocations(targetParams.id).reallocations, + ).toStrictEqual([]); + }); + + test("behavior: freezes firstTotalAssets while applying relative caps", () => { + const { data } = makeFixture({ + firstTotalAssets: 1_000n, + targetCaps: [ + { absoluteCap: 10_000n, relativeCap: MathLib.WAD / 2n }, + { absoluteCap: 10_000n, relativeCap: MathLib.WAD / 2n }, + { absoluteCap: 10_000n, relativeCap: MathLib.WAD / 2n }, + ], + }); + + const result = data.computeVaultV2Reallocations(targetParams.id); + + expect(result.reallocations[0]?.assets).toBe(500n); + expect(result.data.getVault(VAULT)._totalAssets).toBe(1_000n); + }); + + test("behavior: caps each call at uint128", () => { + const sourceSupply = MathLib.MAX_UINT_128 + 10n; + const { data } = makeFixture({ + sourceSupply, + targetSupply: 0n, + firstTotalAssets: sourceSupply, + allocatorTargetCap: MathLib.MAX_UINT_256, + targetCaps: [ + { absoluteCap: MathLib.MAX_UINT_256, relativeCap: MathLib.WAD }, + { absoluteCap: MathLib.MAX_UINT_256, relativeCap: MathLib.WAD }, + { absoluteCap: MathLib.MAX_UINT_256, relativeCap: MathLib.WAD }, + ], + }); + + expect( + data.computeVaultV2Reallocations(targetParams.id).reallocations[0] + ?.assets, + ).toBe(MathLib.MAX_UINT_128); + }); + + test("behavior: rejects target market supply overflow", () => { + const { data } = makeFixture({ + targetSupply: MathLib.MAX_UINT_128, + firstTotalAssets: MathLib.MAX_UINT_128 + 1_000n, + allocatorTargetCap: MathLib.MAX_UINT_256, + targetCaps: [ + { absoluteCap: MathLib.MAX_UINT_256, relativeCap: MathLib.WAD }, + { absoluteCap: MathLib.MAX_UINT_256, relativeCap: MathLib.WAD }, + { absoluteCap: MathLib.MAX_UINT_256, relativeCap: MathLib.WAD }, + ], + }); + + expect( + data.computeVaultV2Reallocations(targetParams.id).reallocations, + ).toStrictEqual([]); + }); + + test("behavior: same-market deallocation creates target supply headroom", () => { + const { data } = makeFixture({ + sourceMarketParams: targetParams, + sourceSupply: MathLib.MAX_UINT_128, + firstTotalAssets: MathLib.MAX_UINT_128, + allocatorTargetCap: MathLib.MAX_UINT_256, + targetCaps: [ + { absoluteCap: MathLib.MAX_UINT_256, relativeCap: MathLib.WAD }, + { absoluteCap: MathLib.MAX_UINT_256, relativeCap: MathLib.WAD }, + { absoluteCap: MathLib.MAX_UINT_256, relativeCap: MathLib.WAD }, + ], + }); + + expect( + data.computeVaultV2Reallocations(targetParams.id).reallocations[0] + ?.assets, + ).toBe(MathLib.MAX_UINT_128); + }); + + test("behavior: disabled discovery returns no calls", () => { + const { data } = makeFixture(); + + expect( + data.computeVaultV2Reallocations(targetParams.id, { enabled: false }) + .reallocations, + ).toStrictEqual([]); + }); +}); + +describe("computeReallocationsVaultV2", () => { + test("default: caps friendly reallocations to the 90% target", () => { + const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 90n }); + + const reallocations = computeReallocationsVaultV2({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 20n, + }); + + expect(reallocations).toHaveLength(1); + expect(reallocations[0]?.assets).toBe(22n); + }); + + test("behavior: falls back to a 100% source-utilization ceiling", () => { + const { data } = makeFixture({ + targetSupply: 100n, + targetBorrow: 100n, + sourceSupply: 1_000n, + sourceBorrow: 950n, + }); + + const reallocations = computeReallocationsVaultV2({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 40n, + }); + + expect(reallocations[0]?.assets).toBe(40n); + }); + + test("behavior: plans a loan-asset withdraw", () => { + const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 90n }); + + const reallocations = computeReallocationsVaultV2({ + reallocationData: data, + marketId: targetParams.id, + operation: "withdraw", + amount: 10n, + }); + + expect(reallocations[0]?.assets).toBe(10n); + }); + + test("behavior: charges nativePenalty for every retained flat call", () => { + const { data } = makeFixture({ + targetSupply: 100n, + targetBorrow: 100n, + idle: 300n, + }); + const reallocations = computeReallocationsVaultV2({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 1_100n, + }); + + const tx = blueBorrow({ + market: { + chainId: ChainId.EthMainnet, + marketParams: targetParams, + }, + args: { + amount: 1_100n, + receiver: VAULT, + minSharePrice: 0n, + reallocations, + }, + }); + + expect(reallocations).toHaveLength(2); + expect(tx.value).toBe(14n); + }); + + test("error: InsufficientSharedLiquidityError rejects a partial plan", () => { + const { data } = makeFixture({ + targetSupply: 100n, + targetBorrow: 100n, + sourceSupply: 50n, + }); + + expect(() => + computeReallocationsVaultV2({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 100n, + }), + ).toThrow(InsufficientSharedLiquidityError); + }); + + test("error: ReallocationWithdrawExceedsMarketSupplyError", () => { + const { data } = makeFixture({ targetSupply: 100n }); + + expect(() => + computeReallocationsVaultV2({ + reallocationData: data, + marketId: targetParams.id, + operation: "withdraw", + amount: 101n, + }), + ).toThrow(ReallocationWithdrawExceedsMarketSupplyError); + }); + + test("behavior: disabled planning returns no calls", () => { + const { data } = makeFixture(); + + expect( + computeReallocationsVaultV2({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 1_000n, + options: { enabled: false }, + }), + ).toStrictEqual([]); + }); +}); + +describe("ReallocationDataVaultV2 liquidity metrics", () => { + test("default: sums idle and market liquidity in target-utilization math", () => { + const { data, sourceExpectedAssets } = makeFixture({ + targetSupply: 100n, + targetBorrow: 50n, + idle: 300n, + }); + + expect(data.getPublicReallocationLiquidityVaultV2(targetParams.id)).toBe( + sourceExpectedAssets + 300n, + ); + expect( + data.getAvailableLiquidityToUtilizationVaultV2(targetParams.id), + ).toBe(1_210n); + expect( + data.getAvailableLiquidityToUtilizationVaultV2( + targetParams.id, + (MathLib.WAD * 8n) / 10n, + ), + ).toBe(30n); + }); +}); diff --git a/packages/morpho-sdk/src/entities/reallocationDataVaultV2.ts b/packages/morpho-sdk/src/entities/reallocationDataVaultV2.ts new file mode 100644 index 000000000..850a51276 --- /dev/null +++ b/packages/morpho-sdk/src/entities/reallocationDataVaultV2.ts @@ -0,0 +1,923 @@ +import { + AccrualVaultV2, + AccrualVaultV2MorphoMarketV1AdapterV2, + type IVaultV2Allocation, + Market, + type MarketId, + MarketUtils, + MathLib, + UnknownDataError, + type VaultV2MarketPublicAllocatorConfig, + type VaultV2PublicAllocatorConfig, + VaultV2Utils, +} from "@morpho-org/blue-sdk"; +import { _try, bigIntComparator } from "@morpho-org/morpho-ts"; +import { type Address, type Hash, isAddressEqual } from "viem"; +import { + DEFAULT_SUPPLY_TARGET_UTILIZATION, + DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, +} from "../helpers/constant.js"; +import type { + PublicAllocatorOptionsVaultV2, + ReallocationComputeOptionsVaultV2, + VaultV2BlueReallocation, +} from "../types/index.js"; +import { + ReallocationAdapterSupplySharesUnderflowError, + ReallocationAllocationUnderflowError, + UnknownReallocationAdapterError, + UnknownReallocationAllocationError, + UnknownReallocationMarketError, + UnknownReallocationMarketPublicAllocatorConfigError, + UnknownReallocationPublicAllocatorConfigError, + UnknownReallocationVaultError, +} from "../types/index.js"; + +/** Input state required to simulate Vault V2 BluePublicAllocator reallocations. */ +export interface InputReallocationDataVaultV2 { + /** Chain id associated with the fetched state. */ + readonly chainId: number; + /** Explicit BluePublicAllocator contract used by every returned call. */ + readonly allocator: Address; + /** Markets indexed by market id. */ + readonly markets?: Readonly>; + /** Accrued Vault V2 entities indexed by vault address. */ + readonly vaults?: Readonly>; + /** Vault cap state indexed by vault address and derived allocation id. */ + readonly allocations?: Readonly< + Record< + Address, + Readonly> | undefined + > + >; + /** Vault-wide BluePublicAllocator configuration indexed by vault address. */ + readonly publicAllocatorConfigs?: Readonly< + Record + >; + /** Adapter-market BluePublicAllocator configuration indexed by vault and `marketParamsId`. */ + readonly marketPublicAllocatorConfigs?: Readonly< + Record< + Address, + | Readonly> + | undefined + > + >; +} + +type TargetContext = { + readonly adapter: AccrualVaultV2MorphoMarketV1AdapterV2; + readonly ids: readonly [Hash, Hash, Hash]; + readonly allocations: readonly IVaultV2Allocation[]; + readonly marketPublicAllocatorConfig: VaultV2MarketPublicAllocatorConfig; + readonly untracked: bigint; +}; + +const sameMarketId = (left: MarketId, right: MarketId) => + left.toLowerCase() === right.toLowerCase(); + +const cloneMarket = (market: Market) => new Market({ ...market }); + +const cloneAdapter = (adapter: AccrualVaultV2MorphoMarketV1AdapterV2) => + new AccrualVaultV2MorphoMarketV1AdapterV2( + { + address: adapter.address, + parentVault: adapter.parentVault, + skimRecipient: adapter.skimRecipient, + marketIds: [...adapter.marketIds], + adaptiveCurveIrm: adapter.adaptiveCurveIrm, + supplyShares: { ...adapter.supplyShares }, + }, + adapter.markets.map(cloneMarket), + ); + +const cloneVault = (vault: AccrualVaultV2) => { + const adapters = vault.accrualAdapters.map((adapter) => + adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2 + ? cloneAdapter(adapter) + : adapter, + ); + const liquidityAdapter = + vault.accrualLiquidityAdapter == null + ? undefined + : (adapters.find((adapter) => + isAddressEqual( + adapter.address, + vault.accrualLiquidityAdapter!.address, + ), + ) ?? vault.accrualLiquidityAdapter); + + return new AccrualVaultV2( + { + ...vault, + liquidityAllocations: vault.liquidityAllocations?.map((allocation) => ({ + ...allocation, + })), + }, + liquidityAdapter, + adapters, + vault.assetBalance, + { ...vault.forceDeallocatePenalties }, + ); +}; + +/** + * Immutable-by-convention state container for Vault V2 BluePublicAllocator simulations. + * + * Constructor inputs are cloned. Every simulated reallocation returns a new + * instance, while `firstTotalAssets` is represented by each accrued vault's + * frozen `_totalAssets` value for the duration of a plan. + * + * @example + * ```ts + * import { ReallocationDataVaultV2 } from "@morpho-org/morpho-sdk/entities"; + * + * const data = new ReallocationDataVaultV2(input); + * ``` + */ +export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { + /** Chain id associated with this snapshot. */ + public readonly chainId: number; + /** Explicit BluePublicAllocator address used in returned calls. */ + public readonly allocator: Address; + /** Markets indexed by market id. */ + public readonly markets: Record; + /** Vault V2 entities indexed by address. */ + public readonly vaults: Record; + /** Vault cap state indexed by vault and derived allocation id. */ + public readonly allocations: Record< + Address, + Record | undefined + >; + /** Vault-wide allocator configuration indexed by vault. */ + public readonly publicAllocatorConfigs: Record< + Address, + VaultV2PublicAllocatorConfig | undefined + >; + /** Adapter-market allocator configuration indexed by vault and market-params id. */ + public readonly marketPublicAllocatorConfigs: Record< + Address, + Record | undefined + >; + + /** + * Creates a cloned Vault V2 reallocation snapshot. + * + * @param input - State fetched at one consistent block. + */ + public constructor(input: InputReallocationDataVaultV2) { + this.chainId = input.chainId; + this.allocator = input.allocator; + this.markets = {}; + this.vaults = {}; + this.allocations = {}; + this.publicAllocatorConfigs = {}; + this.marketPublicAllocatorConfigs = {}; + + for (const [marketId, market] of Object.entries(input.markets ?? {}) as [ + MarketId, + Market | undefined, + ][]) { + this.markets[marketId] = market == null ? undefined : cloneMarket(market); + } + + for (const [address, vault] of Object.entries(input.vaults ?? {}) as [ + Address, + AccrualVaultV2 | undefined, + ][]) { + const clonedVault = vault == null ? undefined : cloneVault(vault); + this.vaults[address] = clonedVault; + + for (const adapter of clonedVault?.accrualAdapters ?? []) { + if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) + continue; + for (const market of adapter.markets) { + this.markets[market.id] ??= cloneMarket(market); + } + } + } + + for (const [vault, allocations] of Object.entries( + input.allocations ?? {}, + ) as [ + Address, + Readonly> | undefined, + ][]) { + this.allocations[vault] = {}; + for (const [id, allocation] of Object.entries(allocations ?? {}) as [ + Hash, + IVaultV2Allocation | undefined, + ][]) { + this.allocations[vault]![id] = + allocation == null ? undefined : { ...allocation }; + } + } + + for (const [vault, config] of Object.entries( + input.publicAllocatorConfigs ?? {}, + ) as [Address, VaultV2PublicAllocatorConfig | undefined][]) { + this.publicAllocatorConfigs[vault] = + config == null ? undefined : { ...config }; + } + + for (const [vault, configs] of Object.entries( + input.marketPublicAllocatorConfigs ?? {}, + ) as [ + Address, + ( + | Readonly> + | undefined + ), + ][]) { + this.marketPublicAllocatorConfigs[vault] = {}; + for (const [id, config] of Object.entries(configs ?? {}) as [ + Hash, + VaultV2MarketPublicAllocatorConfig | undefined, + ][]) { + this.marketPublicAllocatorConfigs[vault]![id] = + config == null ? undefined : { ...config }; + } + } + } + + /** + * Clones the complete simulation snapshot. + * + * @returns A deep clone of this simulation state. + * @example + * ```ts + * const next = data.clone(); + * ``` + */ + public clone() { + return new ReallocationDataVaultV2(this); + } + + /** + * Gets a market from this snapshot. + * + * @param marketId - Market id to read. + * @returns The market state. + * @throws {@link UnknownReallocationMarketError} when the market is absent. + * @example + * ```ts + * const market = data.getMarket(marketId); + * ``` + */ + public getMarket(marketId: MarketId) { + const market = this.markets[marketId]; + if (market == null) throw new UnknownReallocationMarketError(marketId); + return market; + } + + /** + * Gets a Vault V2 from this snapshot. + * + * @param vault - Vault V2 address. + * @returns The accrued Vault V2 state. + * @throws {@link UnknownReallocationVaultError} when the vault is absent. + * @example + * ```ts + * const vault = data.getVault(vaultAddress); + * ``` + */ + public getVault(vault: Address) { + const data = this.vaults[vault]; + if (data == null) throw new UnknownReallocationVaultError(vault); + return data; + } + + /** + * Gets one Vault V2 allocation record. + * + * @param vault - Vault V2 address. + * @param id - Derived allocation id. + * @returns The allocation and cap state. + * @throws {@link UnknownReallocationAllocationError} when the record is absent. + * @example + * ```ts + * const allocation = data.getAllocation(vaultAddress, allocationId); + * ``` + */ + public getAllocation(vault: Address, id: Hash) { + const allocation = this.allocations[vault]?.[id]; + if (allocation == null) + throw new UnknownReallocationAllocationError(vault, id); + return allocation; + } + + /** + * Gets one vault-wide BluePublicAllocator configuration. + * + * @param vault - Vault V2 address. + * @returns The vault-wide allocator configuration. + * @throws {@link UnknownReallocationPublicAllocatorConfigError} when it is absent. + * @example + * ```ts + * const config = data.getPublicAllocatorConfig(vaultAddress); + * ``` + */ + public getPublicAllocatorConfig(vault: Address) { + const config = this.publicAllocatorConfigs[vault]; + if (config == null) + throw new UnknownReallocationPublicAllocatorConfigError(vault); + return config; + } + + /** + * Gets one adapter-market BluePublicAllocator configuration. + * + * @param vault - Vault V2 address. + * @param marketParamsId - Adapter-scoped market-parameters id. + * @returns The allocator cap and permissions. + * @throws {@link UnknownReallocationMarketPublicAllocatorConfigError} when it is absent. + * @example + * ```ts + * const config = data.getMarketPublicAllocatorConfig(vaultAddress, marketParamsId); + * ``` + */ + public getMarketPublicAllocatorConfig(vault: Address, marketParamsId: Hash) { + const config = this.marketPublicAllocatorConfigs[vault]?.[marketParamsId]; + if (config == null) + throw new UnknownReallocationMarketPublicAllocatorConfigError( + vault, + marketParamsId, + ); + return config; + } + + /** + * Gets a supported MorphoMarketV1AdapterV2 from a Vault V2. + * + * @param vault - Vault V2 address. + * @param adapter - Adapter address. + * @returns The accrued adapter state. + * @throws {@link UnknownReallocationAdapterError} when it is absent or unsupported. + * @example + * ```ts + * const adapter = data.getAdapter(vaultAddress, adapterAddress); + * ``` + */ + public getAdapter(vault: Address, adapter: Address) { + const data = this.getVault(vault).accrualAdapters.find( + (candidate): candidate is AccrualVaultV2MorphoMarketV1AdapterV2 => + candidate instanceof AccrualVaultV2MorphoMarketV1AdapterV2 && + isAddressEqual(candidate.address, adapter), + ); + if (data == null) throw new UnknownReallocationAdapterError(vault, adapter); + return data; + } + + /** + * Computes every friendly Vault V2 BluePublicAllocator call currently + * available for a target market. + * + * The algorithm ranks action-ready calls by obtainable assets, includes idle + * liquidity, applies each winner to cloned state, and stops when every + * candidate is exhausted. Source markets are held below the SDK's default + * withdrawal-utilization ceiling. + * + * @param marketId - Target Blue market id. + * @param options - Optional timestamp, enable flag, and vault allowlist. + * @returns Flat action-ready reallocations and their post-simulation state. + * @throws {@link UnknownReallocationMarketError} when the target market is absent. + * @example + * ```ts + * import { ReallocationDataVaultV2 } from "@morpho-org/morpho-sdk/entities"; + * + * const data = new ReallocationDataVaultV2(input); + * const result = data.computeVaultV2Reallocations(targetMarketId, { timestamp }); + * ``` + */ + public computeVaultV2Reallocations( + marketId: MarketId, + options: PublicAllocatorOptionsVaultV2 = {}, + ) { + return this._computeVaultV2Reallocations({ + marketId, + maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, + options, + }); + } + + /** + * Computes Vault V2 reallocations using an explicit internal source-utilization ceiling. + * + * @param marketId - Target market id. + * @param maxWithdrawalUtilization - Source-market utilization ceiling. + * @param options - Discovery options. + * @returns Flat action-ready reallocations and post-simulation state. + * @internal + */ + public _computeVaultV2Reallocations({ + marketId, + maxWithdrawalUtilization, + options = {}, + }: { + readonly marketId: MarketId; + readonly maxWithdrawalUtilization: bigint; + readonly options?: PublicAllocatorOptionsVaultV2; + }): { + readonly reallocations: readonly VaultV2BlueReallocation[]; + readonly data: ReallocationDataVaultV2; + } { + if (options.enabled === false) return { reallocations: [], data: this }; + + const timestamp = BigInt( + options.timestamp ?? this.getMarket(marketId).lastUpdate, + ); + let data = this.accrue(timestamp); + const reallocations: VaultV2BlueReallocation[] = []; + const configuredVaults = Object.keys(data.vaults) as Address[]; + const vaultKeyByLower = new Map( + configuredVaults.map((vault) => [vault.toLowerCase(), vault]), + ); + const vaults = Array.from( + new Set( + (options.reallocatableVaults ?? configuredVaults) + .map((vault) => vaultKeyByLower.get(vault.toLowerCase())) + .filter((vault): vault is Address => vault != null), + ), + ); + + while (true) { + const candidates = vaults + .map((vault) => + data.getLargestVaultReallocation({ + vaultAddress: vault, + marketId, + maxWithdrawalUtilization, + }), + ) + .filter( + (candidate): candidate is VaultV2BlueReallocation => + candidate != null, + ) + .sort(bigIntComparator(({ assets }) => assets, "desc")); + + const largest = candidates[0]; + if (largest == null) return { reallocations, data }; + + reallocations.push(largest); + data = data.applyPublicReallocation({ + reallocation: largest, + targetMarketId: marketId, + timestamp, + }); + } + } + + /** + * Sums friendly Vault V2 shared liquidity available to a target market. + * + * @param marketId - Target Blue market id. + * @param options - Optional timestamp, enable flag, and vault allowlist. + * @returns Reallocatable market and idle assets, or `0n` when none are available. + * @throws {@link UnknownReallocationMarketError} when the target market is absent. + * @example + * ```ts + * const liquidity = data.getPublicReallocationLiquidityVaultV2(targetMarketId); + * ``` + */ + public getPublicReallocationLiquidityVaultV2( + marketId: MarketId, + options?: PublicAllocatorOptionsVaultV2, + ) { + return this.computeVaultV2Reallocations( + marketId, + options, + ).reallocations.reduce((total, { assets }) => total + assets, 0n); + } + + /** + * Computes borrow liquidity to a target utilization, including friendly + * Vault V2 public reallocations. + * + * @param marketId - Target Blue market id. + * @param utilization - Desired utilization, scaled by WAD. Defaults to 90%. + * @param options - Optional timestamp, enable flag, and vault allowlist. + * @returns Borrowable assets while remaining at or below `utilization`. + * @throws {@link UnknownReallocationMarketError} when the target market is absent. + * @example + * ```ts + * const liquidity = data.getAvailableLiquidityToUtilizationVaultV2(targetMarketId); + * ``` + */ + // biome-ignore lint/complexity/useMaxParams: mirrors the existing V1 metric API + public getAvailableLiquidityToUtilizationVaultV2( + marketId: MarketId, + utilization: bigint = DEFAULT_SUPPLY_TARGET_UTILIZATION, + options?: ReallocationComputeOptionsVaultV2, + ) { + const market = this.getMarket(marketId).accrueInterest(options?.timestamp); + if (DEFAULT_SUPPLY_TARGET_UTILIZATION > utilization) + return market.getBorrowToUtilization(utilization); + + const availableLiquidity = this.getPublicReallocationLiquidityVaultV2( + marketId, + options, + ); + return MarketUtils.getBorrowToUtilization( + { + totalSupplyAssets: market.totalSupplyAssets + availableLiquidity, + totalBorrowAssets: market.totalBorrowAssets, + }, + utilization, + ); + } + + private accrue(timestamp: bigint) { + const data = this.clone(); + + for (const [marketId, market] of Object.entries(data.markets) as [ + MarketId, + Market | undefined, + ][]) { + if (market != null) + data.markets[marketId] = market.accrueInterest(timestamp); + } + + for (const [address, vault] of Object.entries(data.vaults) as [ + Address, + AccrualVaultV2 | undefined, + ][]) { + if (vault == null) continue; + const accruedVault = vault.accrueInterest(timestamp).vault; + for (const adapter of accruedVault.accrualAdapters) { + if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) + continue; + adapter.markets = adapter.markets.map((market) => + data.getMarket(market.id), + ); + } + data.vaults[address] = accruedVault; + } + + return data; + } + + private getLargestVaultReallocation({ + vaultAddress, + marketId, + maxWithdrawalUtilization, + }: { + readonly vaultAddress: Address; + readonly marketId: MarketId; + readonly maxWithdrawalUtilization: bigint; + }) { + return _try(() => { + const vault = this.getVault(vaultAddress); + const publicAllocatorConfig = this.getPublicAllocatorConfig(vaultAddress); + if ( + !isAddressEqual(publicAllocatorConfig.allocator, this.allocator) || + !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) + ) + return; + + const targetMarket = this.getMarket(marketId); + const targetSupplyHeadroom = MathLib.zeroFloorSub( + MathLib.MAX_UINT_128, + targetMarket.totalSupplyAssets, + ); + const candidates: VaultV2BlueReallocation[] = []; + + for (const adapter of vault.accrualAdapters) { + if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) + continue; + if (!isAddressEqual(adapter.parentVault, vaultAddress)) continue; + if ( + !isAddressEqual(targetMarket.params.loanToken, vault.asset) || + !isAddressEqual(targetMarket.params.irm, adapter.adaptiveCurveIrm) + ) + continue; + if ( + !adapter.markets.some((market) => sameMarketId(market.id, marketId)) + ) + continue; + + const targetContext = _try((): TargetContext | undefined => { + const ids = adapter.ids(targetMarket.params); + const marketPublicAllocatorConfig = + this.getMarketPublicAllocatorConfig(vaultAddress, ids[2]); + if ( + !isAddressEqual( + marketPublicAllocatorConfig.allocator, + this.allocator, + ) || + !isAddressEqual(marketPublicAllocatorConfig.vault, vaultAddress) || + !isAddressEqual( + marketPublicAllocatorConfig.adapter, + adapter.address, + ) || + !marketPublicAllocatorConfig.isActiveAdapter + ) + return; + + const allocations = ids.map((id) => + this.getAllocation(vaultAddress, id), + ); + if (allocations.some(({ absoluteCap }) => absoluteCap === 0n)) return; + + const expectedSupplyAssets = targetMarket.toSupplyAssets( + adapter.supplyShares[marketId] ?? 0n, + ); + const untracked = MathLib.zeroFloorSub( + expectedSupplyAssets, + allocations[2]!.allocation, + ); + + return { + adapter, + ids, + allocations, + marketPublicAllocatorConfig, + untracked, + }; + }, UnknownDataError); + if (targetContext == null) continue; + + const targetMarketParamsAllocation = targetContext.allocations[2]!; + const allocatorHeadroom = MathLib.zeroFloorSub( + targetContext.marketPublicAllocatorConfig.absoluteCap, + targetMarketParamsAllocation.allocation + targetContext.untracked, + ); + + const getTargetCapHeadroom = ( + sourceIds: ReadonlySet, + sourceUntracked: bigint, + ) => { + let headroom = MathLib.MAX_UINT_256; + for (const [ + index, + allocation, + ] of targetContext.allocations.entries()) { + const id = targetContext.ids[index]!; + if (sourceIds.has(id)) { + const postAllocation = + allocation.allocation + + sourceUntracked + + targetContext.untracked; + const capacity = VaultV2Utils.allocationHeadroom( + allocation, + vault._totalAssets, + ); + if (postAllocation > allocation.allocation + capacity.value) + return; + continue; + } + + headroom = MathLib.min( + headroom, + MathLib.zeroFloorSub( + VaultV2Utils.allocationHeadroom(allocation, vault._totalAssets) + .value, + targetContext.untracked, + ), + ); + } + return headroom; + }; + + if (publicAllocatorConfig.canAllocateFromIdle) { + const targetHeadroom = getTargetCapHeadroom(new Set(), 0n); + if (targetHeadroom != null) { + const assets = MathLib.min( + MathLib.MAX_UINT_128, + targetSupplyHeadroom, + allocatorHeadroom, + targetHeadroom, + vault.assetBalance, + ); + if (assets > 0n) { + candidates.push({ + allocator: this.allocator, + type: "bluePublicAllocator", + vault: vaultAddress, + from: { type: "idle" }, + to: { adapter: targetContext.adapter.address }, + assets, + nativePenalty: publicAllocatorConfig.nativePenalty, + }); + } + } + } + + for (const sourceAdapter of vault.accrualAdapters) { + if (!(sourceAdapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) + continue; + if (!isAddressEqual(sourceAdapter.parentVault, vaultAddress)) + continue; + + for (const sourceMarketReference of sourceAdapter.markets) { + const sourceMarket = this.getMarket(sourceMarketReference.id); + if ( + !isAddressEqual(sourceMarket.params.loanToken, vault.asset) || + !isAddressEqual( + sourceMarket.params.irm, + sourceAdapter.adaptiveCurveIrm, + ) + ) + continue; + if ( + sameMarketId(sourceMarket.id, marketId) && + isAddressEqual( + sourceAdapter.address, + targetContext.adapter.address, + ) + ) + continue; + + const candidate = _try(() => { + const sourceIds = sourceAdapter.ids(sourceMarket.params); + const sourceConfig = this.getMarketPublicAllocatorConfig( + vaultAddress, + sourceIds[2], + ); + if ( + !isAddressEqual(sourceConfig.allocator, this.allocator) || + !isAddressEqual(sourceConfig.vault, vaultAddress) || + !isAddressEqual(sourceConfig.adapter, sourceAdapter.address) || + !sourceConfig.isActiveAdapter || + !sourceConfig.canDeallocate + ) + return; + + const sourceAllocations = sourceIds.map((id) => + this.getAllocation(vaultAddress, id), + ); + if (sourceAllocations.some(({ allocation }) => allocation === 0n)) + return; + + const expectedSupplyAssets = sourceMarket.toSupplyAssets( + sourceAdapter.supplyShares[sourceMarket.id] ?? 0n, + ); + const sourceUntracked = MathLib.zeroFloorSub( + expectedSupplyAssets, + sourceAllocations[2]!.allocation, + ); + const targetHeadroom = getTargetCapHeadroom( + new Set(sourceIds), + sourceUntracked, + ); + if (targetHeadroom == null) return; + + const assets = MathLib.min( + MathLib.MAX_UINT_128, + sameMarketId(sourceMarket.id, marketId) + ? MathLib.MAX_UINT_128 + : targetSupplyHeadroom, + allocatorHeadroom, + targetHeadroom, + expectedSupplyAssets, + sourceMarket.getWithdrawToUtilization(maxWithdrawalUtilization), + ); + if (assets <= 0n) return; + + return { + allocator: this.allocator, + type: "bluePublicAllocator", + vault: vaultAddress, + from: { + type: "market", + adapter: sourceAdapter.address, + marketParams: sourceMarket.params, + }, + to: { adapter: targetContext.adapter.address }, + assets, + nativePenalty: publicAllocatorConfig.nativePenalty, + } satisfies VaultV2BlueReallocation; + }, UnknownDataError); + if (candidate != null) candidates.push(candidate); + } + } + } + + return candidates.sort( + bigIntComparator(({ assets }) => assets, "desc"), + )[0]; + }, UnknownDataError); + } + + private applyPublicReallocation({ + reallocation, + targetMarketId, + timestamp, + }: { + readonly reallocation: VaultV2BlueReallocation; + readonly targetMarketId: MarketId; + readonly timestamp: bigint; + }) { + const data = this.clone(); + const vault = data.getVault(reallocation.vault); + const targetAdapter = data.getAdapter( + reallocation.vault, + reallocation.to.adapter, + ); + const targetMarket = data.getMarket(targetMarketId); + const targetIds = targetAdapter.ids(targetMarket.params); + + if (reallocation.from.type === "market") { + const sourceAdapter = data.getAdapter( + reallocation.vault, + reallocation.from.adapter, + ); + const sourceMarket = data.getMarket(reallocation.from.marketParams.id); + const sourceIds = sourceAdapter.ids(sourceMarket.params); + const currentSupplyShares = + sourceAdapter.supplyShares[sourceMarket.id] ?? 0n; + const withdrawal = sourceMarket.withdraw( + reallocation.assets, + 0n, + timestamp, + ); + if (withdrawal.shares > currentSupplyShares) { + throw new ReallocationAdapterSupplySharesUnderflowError({ + vault: reallocation.vault, + adapter: sourceAdapter.address, + marketId: sourceMarket.id, + supplyShares: currentSupplyShares, + withdrawnShares: withdrawal.shares, + }); + } + sourceAdapter.supplyShares[sourceMarket.id] = + currentSupplyShares - withdrawal.shares; + data.markets[sourceMarket.id] = withdrawal.market; + data.setAdapterMarket(sourceAdapter, withdrawal.market); + const sourceChange = + withdrawal.market.toSupplyAssets( + sourceAdapter.supplyShares[sourceMarket.id] ?? 0n, + ) - data.getAllocation(reallocation.vault, sourceIds[2]).allocation; + for (const id of sourceIds) { + data.addAllocationChange({ + vault: reallocation.vault, + id, + change: sourceChange, + }); + } + vault.assetBalance += reallocation.assets; + } + + const currentTargetMarket = data.getMarket(targetMarket.id); + const oldTargetAllocation = data.getAllocation( + reallocation.vault, + targetIds[2], + ).allocation; + const supply = currentTargetMarket.supply( + reallocation.assets, + 0n, + timestamp, + ); + const targetSupplyShares = + (targetAdapter.supplyShares[targetMarket.id] ?? 0n) + supply.shares; + targetAdapter.supplyShares[targetMarket.id] = targetSupplyShares; + data.markets[targetMarket.id] = supply.market; + data.setAdapterMarket(targetAdapter, supply.market); + + const targetChange = + supply.market.toSupplyAssets(targetSupplyShares) - oldTargetAllocation; + for (const id of targetIds) { + data.addAllocationChange({ + vault: reallocation.vault, + id, + change: targetChange, + }); + } + + vault.assetBalance -= reallocation.assets; + return data; + } + + private addAllocationChange({ + vault, + id, + change, + }: { + readonly vault: Address; + readonly id: Hash; + readonly change: bigint; + }) { + const allocation = this.getAllocation(vault, id); + const nextAllocation = allocation.allocation + change; + if (nextAllocation < 0n) { + throw new ReallocationAllocationUnderflowError({ + vault, + id, + allocation: allocation.allocation, + change, + }); + } + this.allocations[vault]![id] = { + ...allocation, + allocation: nextAllocation, + }; + } + + private setAdapterMarket( + adapter: AccrualVaultV2MorphoMarketV1AdapterV2, + market: Market, + ) { + const index = adapter.markets.findIndex((candidate) => + sameMarketId(candidate.id, market.id), + ); + if (index >= 0) adapter.markets[index] = market; + } +} diff --git a/packages/morpho-sdk/src/helpers/computeReallocations.test.ts b/packages/morpho-sdk/src/helpers/computeReallocations.test.ts index eb4152844..98da754d9 100644 --- a/packages/morpho-sdk/src/helpers/computeReallocations.test.ts +++ b/packages/morpho-sdk/src/helpers/computeReallocations.test.ts @@ -87,7 +87,7 @@ interface MockStateParams { * Creates a minimal mock ReallocationData. * * Only implements the methods computeReallocations actually calls: - * `getMarket`, `getMarketPublicReallocations`, and `getVault`. + * `getMarket`, `computeVaultV1Reallocations`, and `getVault`. */ function makeMockState({ targetMarket: tm = defaultTarget, @@ -109,7 +109,7 @@ function makeMockState({ id === tm.id && friendlyTargetMarket != null ? friendlyTargetMarket : markets.get(id)!, - getMarketPublicReallocations: () => ({ + computeVaultV1Reallocations: () => ({ withdrawals: [...aggressiveWithdrawals], data: {} as ReallocationData, }), @@ -121,7 +121,7 @@ function makeMockState({ if (m == null) throw new Error(`Mock: unknown market ${id}`); return m; }, - getMarketPublicReallocations: () => ({ + computeVaultV1Reallocations: () => ({ withdrawals: [...friendlyWithdrawals], data: friendlyData, }), diff --git a/packages/morpho-sdk/src/helpers/computeReallocations.ts b/packages/morpho-sdk/src/helpers/computeReallocations.ts index 9603b6440..09245cbf6 100644 --- a/packages/morpho-sdk/src/helpers/computeReallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeReallocations.ts @@ -7,7 +7,7 @@ import { type PublicReallocation, type ReallocationComputeOptions, ReallocationWithdrawExceedsMarketSupplyError, - type VaultReallocation, + type VaultV1BlueReallocation, } from "../types/index.js"; import { getSupplyTargetUtilization } from "./utilization.js"; import { compareMarketIds } from "./validate.js"; @@ -160,7 +160,7 @@ export const computeReallocations = ({ readonly operation: "borrow" | "withdraw"; readonly amount: bigint; readonly options?: ReallocationComputeOptions; -}): readonly VaultReallocation[] => { +}): readonly VaultV1BlueReallocation[] => { if (options?.enabled === false) return []; // ReallocationData does not retain the fetch block; pass that block timestamp @@ -210,7 +210,7 @@ export const computeReallocations = ({ // Phase 1: "friendly" reallocations respecting withdrawal utilization targets. const { withdrawals: friendlyWithdrawals, data: friendlyReallocationData } = - data.getMarketPublicReallocations(market.id, options); + data.computeVaultV1Reallocations(market.id, options); const withdrawals = [...friendlyWithdrawals]; @@ -232,7 +232,7 @@ export const computeReallocations = ({ // Phase 2: "aggressive" — fully withdraw from every market (100% utilization). requiredAssets = newTotalBorrowAssets - newTotalSupplyAssets; withdrawals.push( - ...friendlyReallocationData.getMarketPublicReallocations(market.id, { + ...friendlyReallocationData.computeVaultV1Reallocations(market.id, { ...options, defaultMaxWithdrawalUtilization: MathLib.WAD, maxWithdrawalUtilization: {}, @@ -283,7 +283,7 @@ export const computeReallocations = ({ }); } - // Transform into VaultReallocation[] format. + // Transform into VaultV1BlueReallocation[] format. return reallocations .filter(({ withdrawals: vaultWithdrawals }) => vaultWithdrawals.length > 0) .map(({ vault, withdrawals: vaultWithdrawals }) => ({ diff --git a/packages/morpho-sdk/src/helpers/computeReallocationsVaultV2.ts b/packages/morpho-sdk/src/helpers/computeReallocationsVaultV2.ts new file mode 100644 index 000000000..450e23263 --- /dev/null +++ b/packages/morpho-sdk/src/helpers/computeReallocationsVaultV2.ts @@ -0,0 +1,138 @@ +import { type MarketId, MarketUtils, MathLib } from "@morpho-org/blue-sdk"; +import type { ReallocationDataVaultV2 } from "../entities/reallocationDataVaultV2.js"; +import { + InsufficientSharedLiquidityError, + type ReallocationComputeOptionsVaultV2, + ReallocationWithdrawExceedsMarketSupplyError, + type VaultV2BlueReallocation, +} from "../types/index.js"; +import { DEFAULT_SUPPLY_TARGET_UTILIZATION } from "./constant.js"; + +/** + * Computes action-ready Vault V2 BluePublicAllocator reallocations for a Blue + * borrow or loan-asset withdraw. + * + * The planner first uses the friendly 90% source-utilization ceiling, then + * retries from the friendly post-state with a 100% ceiling when the operation + * would otherwise remain illiquid. It refuses fee-bearing partial plans that + * cannot cover the operation's absolute shortfall. + * + * @param params.reallocationData - Vault V2 reallocation state fetched at one block. + * @param params.marketId - Target Blue market id. + * @param params.operation - Operation driving the reallocation. + * @param params.amount - Borrow or withdraw amount. + * @param params.options - Optional timestamp, enable flag, and vault allowlist. + * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. + * @throws {@link InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. + * @throws {@link ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. + * @example + * ```ts + * import { computeReallocationsVaultV2 } from "@morpho-org/morpho-sdk"; + * + * const reallocations = computeReallocationsVaultV2({ + * reallocationData, + * marketId, + * operation: "borrow", + * amount: 1_000_000n, + * options: { timestamp }, + * }); + * ``` + */ +export const computeReallocationsVaultV2 = ({ + reallocationData: data, + marketId, + operation, + amount, + options, +}: { + readonly reallocationData: ReallocationDataVaultV2; + readonly marketId: MarketId; + readonly operation: "borrow" | "withdraw"; + readonly amount: bigint; + readonly options?: ReallocationComputeOptionsVaultV2; +}): readonly VaultV2BlueReallocation[] => { + if (options?.enabled === false) return []; + + const market = data.getMarket(marketId).accrueInterest(options?.timestamp); + if (operation === "withdraw" && amount > market.totalSupplyAssets) { + throw new ReallocationWithdrawExceedsMarketSupplyError({ + marketId, + withdrawAmount: amount, + totalSupplyAssets: market.totalSupplyAssets, + }); + } + + const newTotalBorrowAssets = + operation === "borrow" + ? market.totalBorrowAssets + amount + : market.totalBorrowAssets; + const newTotalSupplyAssets = + operation === "withdraw" + ? market.totalSupplyAssets - amount + : market.totalSupplyAssets; + + if ( + MarketUtils.getUtilization({ + totalSupplyAssets: newTotalSupplyAssets, + totalBorrowAssets: newTotalBorrowAssets, + }) <= DEFAULT_SUPPLY_TARGET_UTILIZATION + ) + return []; + + let requiredAssets = + MathLib.wDivDown(newTotalBorrowAssets, DEFAULT_SUPPLY_TARGET_UTILIZATION) - + newTotalSupplyAssets; + + const friendly = data.computeVaultV2Reallocations(marketId, options); + const discovered = [...friendly.reallocations]; + const friendlyMarket = friendly.data.getMarket(marketId); + const friendlyBorrow = + operation === "borrow" + ? friendlyMarket.totalBorrowAssets + amount + : friendlyMarket.totalBorrowAssets; + const friendlySupply = + operation === "withdraw" + ? friendlyMarket.totalSupplyAssets - amount + : friendlyMarket.totalSupplyAssets; + + if (friendlyBorrow > friendlySupply) { + requiredAssets = newTotalBorrowAssets - newTotalSupplyAssets; + discovered.push( + ...friendly.data._computeVaultV2Reallocations({ + marketId, + maxWithdrawalUtilization: MathLib.WAD, + options, + }).reallocations, + ); + } + + if (requiredAssets <= 0n) return []; + + const absoluteShortfall = + newTotalBorrowAssets > newTotalSupplyAssets + ? newTotalBorrowAssets - newTotalSupplyAssets + : 0n; + const reallocations: VaultV2BlueReallocation[] = []; + let remainingRequiredAssets = requiredAssets; + let totalReallocated = 0n; + + for (const reallocation of discovered) { + const assets = MathLib.min(reallocation.assets, remainingRequiredAssets); + if (assets <= 0n) continue; + + reallocations.push({ ...reallocation, assets }); + remainingRequiredAssets -= assets; + totalReallocated += assets; + if (remainingRequiredAssets === 0n) break; + } + + if (totalReallocated < absoluteShortfall) { + throw new InsufficientSharedLiquidityError({ + marketId, + shortfall: absoluteShortfall, + available: totalReallocated, + }); + } + + return reallocations; +}; diff --git a/packages/morpho-sdk/src/helpers/index.ts b/packages/morpho-sdk/src/helpers/index.ts index 7545cb273..558e4fe04 100644 --- a/packages/morpho-sdk/src/helpers/index.ts +++ b/packages/morpho-sdk/src/helpers/index.ts @@ -1,4 +1,5 @@ export { computeReallocations } from "./computeReallocations.js"; +export { computeReallocationsVaultV2 } from "./computeReallocationsVaultV2.js"; export { APPROVE_ONLY_ONCE_TOKENS, DEFAULT_LLTV_BUFFER, diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index 385d92ef0..4f8621a13 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -18,7 +18,6 @@ import { import { AccrualPositionUserMismatchError, AddressMismatchError, - type BluePublicAllocatorReallocation, type BlueReallocation, BorrowExceedsSafeLtvError, ChainIdMismatchError, @@ -39,6 +38,7 @@ import { RepaySharesExceedDebtError, UnsortedReallocationWithdrawalsError, type VaultReallocation, + type VaultV2BlueReallocation, WithdrawExceedsCollateralError, WithdrawExceedsSupplyError, WithdrawMakesPositionUnhealthyError, @@ -559,16 +559,15 @@ describe("validateReallocations", () => { withdrawals: [{ marketParams: sourceMarketA, amount: 10n ** 18n }], }; - const validBluePublicAllocatorReallocation: BluePublicAllocatorReallocation = - { - type: "bluePublicAllocator", - allocator: USER_A, - vault: USER_B, - from: { type: "idle" }, - to: { adapter: USER_A }, - assets: 1n, - nativePenalty: 0n, - }; + const validBluePublicAllocatorReallocation: VaultV2BlueReallocation = { + type: "bluePublicAllocator", + allocator: USER_A, + vault: USER_B, + from: { type: "idle" }, + to: { adapter: USER_A }, + assets: 1n, + nativePenalty: 0n, + }; test("should pass with valid reallocations", () => { expect(() => @@ -636,13 +635,31 @@ describe("validateReallocations", () => { adapter: USER_A, marketParams, }, - } satisfies BluePublicAllocatorReallocation, + } satisfies VaultV2BlueReallocation, ], targetMarketId, ), ).toThrow(ReallocationWithdrawalOnTargetMarketError); }); + test("behavior: allows the target market through a different Vault V2 adapter", () => { + expect(() => + validateReallocations( + [ + { + ...validBluePublicAllocatorReallocation, + from: { + type: "market", + adapter: USER_B, + marketParams, + }, + } satisfies VaultV2BlueReallocation, + ], + targetMarketId, + ), + ).not.toThrow(); + }); + test("error: InvalidReallocationSourceTypeError", () => { const reallocation = { ...validBluePublicAllocatorReallocation, diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index 8271cfafc..27c95ff14 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -387,7 +387,8 @@ export const validateReallocations = ( } if ( r.from.type === "market" && - r.from.marketParams.id === targetMarketId + r.from.marketParams.id === targetMarketId && + isAddressEqual(r.from.adapter, r.to.adapter) ) { throw new ReallocationWithdrawalOnTargetMarketError( r.vault, diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index c5ad9cdcb..27f3a274a 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -18,8 +18,8 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. ## Shared liquidity (`sharedLiquidity.ts`) -- `VaultReallocation` — legacy-untagged or explicitly `publicAllocatorV1` vault address + fee + sorted withdrawals; maps to `reallocateTo()`. -- `BluePublicAllocatorReallocation` — tagged BluePublicAllocator/vault/source/target-adapter/assets/native-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. +- `VaultV1BlueReallocation` — legacy-untagged or explicitly `publicAllocatorV1` vault address + fee + sorted withdrawals; maps to `reallocateTo()`. `VaultReallocation` is its deprecated compatibility alias. +- `VaultV2BlueReallocation` — tagged BluePublicAllocator/vault/source/target-adapter/assets/native-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. - `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. ## Errors (`error.ts`) diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 814187e77..7a2334b0a 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -1,5 +1,5 @@ import { type MarketId, UnknownDataError } from "@morpho-org/blue-sdk"; -import type { Address } from "viem"; +import type { Address, Hash } from "viem"; /** * Thrown when a morpho-sdk input that must be non-negative is negative. @@ -1036,6 +1036,91 @@ export class UnknownReallocationPositionError extends UnknownDataError { } } +/** Thrown when Vault V2 reallocation state does not contain a requested allocation id. */ +export class UnknownReallocationAllocationError extends UnknownDataError { + /** + * @param vault - Vault V2 address for the missing allocation. + * @param id - Missing Vault V2 allocation id. + */ + constructor( + public readonly vault: Address, + public readonly id: Hash, + ) { + super(`unknown reallocation allocation "${id}" for vault "${vault}"`); + } +} + +/** Thrown when Vault V2 reallocation state lacks the vault-wide allocator configuration. */ +export class UnknownReallocationPublicAllocatorConfigError extends UnknownDataError { + /** @param vault - Vault V2 address with missing allocator configuration. */ + constructor(public readonly vault: Address) { + super(`unknown public allocator configuration for vault "${vault}"`); + } +} + +/** Thrown when Vault V2 reallocation state lacks an adapter-market allocator configuration. */ +export class UnknownReallocationMarketPublicAllocatorConfigError extends UnknownDataError { + /** + * @param vault - Vault V2 address for the missing configuration. + * @param marketParamsId - Missing adapter-scoped market-parameters id. + */ + constructor( + public readonly vault: Address, + public readonly marketParamsId: Hash, + ) { + super( + `unknown public allocator configuration "${marketParamsId}" for vault "${vault}"`, + ); + } +} + +/** Thrown when Vault V2 reallocation state does not contain a requested adapter. */ +export class UnknownReallocationAdapterError extends UnknownDataError { + /** + * @param vault - Vault V2 address expected to own the adapter. + * @param adapter - Missing adapter address. + */ + constructor( + public readonly vault: Address, + public readonly adapter: Address, + ) { + super(`unknown reallocation adapter "${adapter}" for vault "${vault}"`); + } +} + +/** Thrown when a simulated Vault V2 allocation transition would underflow. */ +export class ReallocationAllocationUnderflowError extends Error { + constructor( + public readonly params: { + readonly vault: Address; + readonly id: Hash; + readonly allocation: bigint; + readonly change: bigint; + }, + ) { + super( + `Reallocation change "${params.change}" exceeds allocation "${params.allocation}" for id "${params.id}" on vault "${params.vault}". Refresh the reallocation data and recompute the plan.`, + ); + } +} + +/** Thrown when a simulated Vault V2 market withdrawal exceeds the adapter's supply shares. */ +export class ReallocationAdapterSupplySharesUnderflowError extends Error { + constructor( + public readonly params: { + readonly vault: Address; + readonly adapter: Address; + readonly marketId: MarketId; + readonly supplyShares: bigint; + readonly withdrawnShares: bigint; + }, + ) { + super( + `Reallocation withdraw shares "${params.withdrawnShares}" exceed adapter supply shares "${params.supplyShares}" on market "${params.marketId}" for adapter "${params.adapter}". Refresh the reallocation data and recompute the plan.`, + ); + } +} + /** Thrown when a Midnight amount exceeds the maximum offer-cap value accepted onchain. */ export class MidnightAmountExceedsMaxOfferCapError extends Error { constructor(params: { diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 494afc5aa..74f6cbc98 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -44,6 +44,18 @@ export interface PublicAllocatorOptions { readonly defaultMaxWithdrawalUtilization?: bigint; } +/** Options controlling Vault V2 BluePublicAllocator reallocation discovery. */ +export interface PublicAllocatorOptionsVaultV2 { + /** Whether Vault V2 public allocator discovery is enabled. */ + readonly enabled?: boolean; + + /** Timestamp at which market and Vault V2 interest is evaluated. */ + readonly timestamp?: BigIntish; + + /** Vault V2 addresses to consider. Defaults to every vault in the reallocation data. */ + readonly reallocatableVaults?: readonly Address[]; +} + /** * A computed source-market withdrawal before it is grouped by vault. */ @@ -73,7 +85,7 @@ export interface ReallocationWithdrawal { * Maps 1:1 to a `PublicAllocator.reallocateTo()` call. * Withdraws from source markets and supplies to the target market. */ -export interface VaultReallocation { +export interface VaultV1BlueReallocation { /** Optional discriminator; omitted by legacy Public Allocator V1 callers. */ readonly type?: "publicAllocatorV1"; readonly vault: Address; @@ -103,7 +115,7 @@ export type BluePublicAllocatorSource = * * The target market parameters are derived from the enclosing Blue action. */ -export interface BluePublicAllocatorReallocation { +export interface VaultV2BlueReallocation { /** Explicit allocator contract address because BluePublicAllocator has no deployment registry entry. */ readonly allocator: Address; /** Discriminator separating BluePublicAllocator reallocations from PublicAllocator V1 reallocations. */ @@ -128,8 +140,15 @@ export interface BluePublicAllocatorReallocation { * `type: "bluePublicAllocator"`. */ export type BlueReallocation = - | VaultReallocation - | BluePublicAllocatorReallocation; + | VaultV1BlueReallocation + | VaultV2BlueReallocation; + +/** + * Deprecated name for a Vault V1 Blue reallocation. + * + * @deprecated Use {@link VaultV1BlueReallocation} instead. + */ +export type VaultReallocation = VaultV1BlueReallocation; /** * Options for computing vault reallocations via the public allocator. @@ -162,3 +181,6 @@ export interface ReallocationComputeOptions extends PublicAllocatorOptions { */ readonly defaultSupplyTargetUtilization?: bigint; } + +/** Options for the Vault V2 borrow/withdraw reallocation planner. */ +export type ReallocationComputeOptionsVaultV2 = PublicAllocatorOptionsVaultV2; diff --git a/packages/morpho-sdk/src/utils.ts b/packages/morpho-sdk/src/utils.ts index 0d8f607da..b566fd639 100644 --- a/packages/morpho-sdk/src/utils.ts +++ b/packages/morpho-sdk/src/utils.ts @@ -6,6 +6,7 @@ export { MathLib, SharesMath, VaultUtils, + VaultV2Utils, } from "@morpho-org/blue-sdk"; export { decodeBytes32String, @@ -64,6 +65,7 @@ export { values, } from "@morpho-org/morpho-ts"; export { computeReallocations } from "./helpers/computeReallocations.js"; +export { computeReallocationsVaultV2 } from "./helpers/computeReallocationsVaultV2.js"; export { addTransactionMetadata } from "./helpers/metadata.js"; export { computeMaxRepaySharePrice, diff --git a/packages/morpho-ts/src/abis.ts b/packages/morpho-ts/src/abis.ts index 71a262f0f..ffe0479b7 100644 --- a/packages/morpho-ts/src/abis.ts +++ b/packages/morpho-ts/src/abis.ts @@ -4597,6 +4597,107 @@ export const publicAllocatorAbi = [ /** Blue Public Allocator ABI used for market and idle reallocations. */ export const bluePublicAllocatorAbi = [ + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bytes32", + name: "id", + type: "bytes32", + }, + ], + name: "absoluteCap", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bytes32", + name: "id", + type: "bytes32", + }, + ], + name: "canDeallocate", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + ], + name: "isActiveAdapter", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + ], + name: "vaultData", + outputs: [ + { + internalType: "bool", + name: "canAllocateFromIdle", + type: "bool", + }, + { + internalType: "uint120", + name: "nativePenalty", + type: "uint120", + }, + { + internalType: "uint120", + name: "accruedNativePenalty", + type: "uint120", + }, + ], + stateMutability: "view", + type: "function", + }, { inputs: [ { diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index f4d126bba..909c5a40d 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -156,7 +156,7 @@ export interface MorphoBorrowOptions { amount: number | bigint; /** The address on behalf of which the borrow operation should be performed. Must match the wallet account address when set. */ onBehalfOf?: string; - /** Optional Morpho Vault V2 reallocations to include in the borrow action. */ + /** Optional MetaMorpho Vault V1 reallocations to include in the borrow action. */ reallocations?: readonly VaultReallocation[]; /** Signature returned by a Morpho SDK authorization requirement, folded into the bundle as `setAuthorizationWithSig`. */ requirementSignature?: RequirementSignature; From 61221981d4bae4d6ce70bdd45da275076da5fe7a Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 7 Aug 2026 10:42:13 +0200 Subject: [PATCH 07/41] fix: simplify Vault V2 shared liquidity --- .changeset/brave-vaults-reallocate.md | 3 +- ...-19-marketv1-supply-withdraw-loan-asset.md | 16 ++-- ...red-liquidity-target-utilization-metric.md | 42 +++++----- ...lt-v2-public-allocator-shared-liquidity.md | 70 ++++++++++------ packages/blue-sdk-viem/src/abis.ts | 2 +- .../VaultV2PublicAllocatorConfig.test.ts | 10 +-- .../vault-v2/VaultV2PublicAllocatorConfig.ts | 10 +-- packages/liquidity-sdk-viem/src/loader.ts | 12 +-- packages/morpho-sdk/AGENTS.md | 6 +- packages/morpho-sdk/BUNDLER3.md | 2 +- packages/morpho-sdk/src/abis.ts | 2 +- .../blue/borrow.bluePublicAllocator.test.ts | 12 +-- .../actions/blue/buildReallocationActions.ts | 4 +- .../morpho-sdk/src/actions/blue/withdraw.ts | 2 +- .../morpho-sdk/src/bundler/actions.test.ts | 34 ++++---- packages/morpho-sdk/src/bundler/actions.ts | 28 ++++--- packages/morpho-sdk/src/bundler/types.ts | 8 +- packages/morpho-sdk/src/entities/AGENTS.md | 2 +- .../entities/blue/blue.reallocations.test.ts | 6 +- .../morpho-sdk/src/entities/blue/blue.test.ts | 4 +- packages/morpho-sdk/src/entities/blue/blue.ts | 24 +++--- packages/morpho-sdk/src/entities/index.ts | 14 ++-- ...> vaultV1ReallocationData.metrics.test.ts} | 12 +-- ...est.ts => vaultV1ReallocationData.test.ts} | 57 ++++++++----- ...tionData.ts => vaultV1ReallocationData.ts} | 36 +++++--- ...est.ts => vaultV2ReallocationData.test.ts} | 79 +++++++++++++++--- ...aVaultV2.ts => vaultV2ReallocationData.ts} | 38 +++++---- packages/morpho-sdk/src/helpers/AGENTS.md | 2 +- ...ts => computeVaultV1Reallocations.test.ts} | 83 ++++++++++--------- ...ions.ts => computeVaultV1Reallocations.ts} | 19 +++-- ...ltV2.ts => computeVaultV2Reallocations.ts} | 12 +-- packages/morpho-sdk/src/helpers/index.ts | 7 +- packages/morpho-sdk/src/types/error.ts | 4 +- .../morpho-sdk/src/types/sharedLiquidity.ts | 6 ++ packages/morpho-sdk/src/utils.ts | 7 +- .../reallocationData/publicAllocator.test.ts | 16 ++-- packages/morpho-ts/src/abis.ts | 4 +- 37 files changed, 417 insertions(+), 278 deletions(-) rename packages/morpho-sdk/src/entities/{reallocationData.metrics.test.ts => vaultV1ReallocationData.metrics.test.ts} (95%) rename packages/morpho-sdk/src/entities/{reallocationData.test.ts => vaultV1ReallocationData.test.ts} (94%) rename packages/morpho-sdk/src/entities/{reallocationData.ts => vaultV1ReallocationData.ts} (96%) rename packages/morpho-sdk/src/entities/{reallocationDataVaultV2.test.ts => vaultV2ReallocationData.test.ts} (89%) rename packages/morpho-sdk/src/entities/{reallocationDataVaultV2.ts => vaultV2ReallocationData.ts} (95%) rename packages/morpho-sdk/src/helpers/{computeReallocations.test.ts => computeVaultV1Reallocations.test.ts} (94%) rename packages/morpho-sdk/src/helpers/{computeReallocations.ts => computeVaultV1Reallocations.ts} (95%) rename packages/morpho-sdk/src/helpers/{computeReallocationsVaultV2.ts => computeVaultV2Reallocations.ts} (91%) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 9d681360e..97afd4575 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -3,6 +3,7 @@ "@morpho-org/blue-sdk": minor "@morpho-org/blue-sdk-viem": minor "@morpho-org/morpho-sdk": minor +"@morpho-org/liquidity-sdk-viem": patch --- -Add the canonical Blue Public Allocator ABI to `morpho-ts`; add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`; add explicit-allocator deployless and fallback reads to `blue-sdk-viem`; and expose Vault V2 shared-liquidity discovery, planning, metrics, and flat market/idle reallocations through `morpho-sdk` Blue flows. Canonicalize the V1 names as `computeVaultV1Reallocations` and `VaultV1BlueReallocation` while preserving their deprecated aliases. +Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`; add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`; add explicit-allocator deployless and fallback reads to `blue-sdk-viem`; and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases and migrate `liquidity-sdk-viem` to the canonical V1 state name. diff --git a/docs/tibs/TIB-2026-05-19-marketv1-supply-withdraw-loan-asset.md b/docs/tibs/TIB-2026-05-19-marketv1-supply-withdraw-loan-asset.md index 268e555db..fc1895e24 100644 --- a/docs/tibs/TIB-2026-05-19-marketv1-supply-withdraw-loan-asset.md +++ b/docs/tibs/TIB-2026-05-19-marketv1-supply-withdraw-loan-asset.md @@ -16,7 +16,7 @@ Two consequences: - Liquidity providers cannot participate in a Morpho market through the SDK without leaving the typed surface (no `Transaction`, no `getRequirements`, no PublicAllocator reallocation help). -- Suppliers who hit on-market illiquidity on a withdraw cannot reuse the SDK's shared-liquidity machinery (`getReallocationData` / `getReallocations` / `computeReallocations`) — that machinery is hard-coded to borrow semantics today. +- Suppliers who hit on-market illiquidity on a withdraw cannot reuse the SDK's shared-liquidity machinery (`getReallocationData` / `getReallocations` / `computeVaultV1Reallocations`) — that machinery is hard-coded to borrow semantics today. This TIB freezes the design decision for the missing pair before the implementation lands. @@ -28,7 +28,7 @@ This TIB freezes the design decision for the missing pair before the implementat - Route both through bundler3 / `GeneralAdapter1` (`morphoSupply` / `morphoWithdraw`) so they compose with the rest of the bundle action set. - Support **native ETH wrapping** on supply when the loan token is the chain's wNative — same contract as `marketV1SupplyCollateral`. - Support **optional PublicAllocator reallocations** on withdraw, so a withdraw whose amount exceeds on-market liquidity can succeed by first pulling liquidity from other markets of the same loan asset. -- Reuse `computeReallocations` for the withdraw direction (single source of truth), not a fork of the helper. +- Reuse `computeVaultV1Reallocations` for the withdraw direction (single source of truth), not a fork of the helper. - Maintain 100% JSDoc and tests (unit colocated + fork e2e) on the new surface in the same PR. **Non-Goals** @@ -68,10 +68,10 @@ A withdraw with native unwrap is **out of scope** for this PR. It requires routi PublicAllocator reallocations apply identically: they prepend `reallocateTo(vault, fee, withdrawals[], targetMarketParams)` actions before `morphoWithdraw`, and their fees accumulate in `tx.value`. `validateReallocations(target=withdrawMarketId)` is reused as-is — sort, no-target-market, non-empty, non-negative fee, strictly-ascending market IDs. -The shared-liquidity planner `computeReallocations` gains an `operation: "borrow" | "withdraw"` discriminator. The signature becomes: +The shared-liquidity planner `computeVaultV1Reallocations` gains an `operation: "borrow" | "withdraw"` discriminator. The signature becomes: ```ts -computeReallocations({ +computeVaultV1Reallocations({ reallocationData, marketId, operation: "borrow" | "withdraw", @@ -135,7 +135,7 @@ Messages follow the canonical `" . ."` sha - **Phase 1 — Types + errors.** Extend `src/types/action.ts` (action interfaces + `AssetsOrSharesArgs`) and `src/types/error.ts`. Unblocks barrel re-exports for the rest of the work. - **Phase 2 — Helpers.** Add `computeMaxSupplySharePrice` and `computeMinWithdrawSharePrice` in `src/helpers/slippage.ts`; `validateWithdrawAmount`, `validateWithdrawShares`, and the unified `validateNativeAsset` in `src/helpers/validate.ts`. Unit tests colocated. -- **Phase 3 — `computeReallocations` extension.** Add the `operation` discriminator; update the borrow caller to pass `"borrow"`; cover the withdraw branch with new tests. +- **Phase 3 — `computeVaultV1Reallocations` extension.** Add the `operation` discriminator; update the borrow caller to pass `"borrow"`; cover the withdraw branch with new tests. - **Phase 4 — Action builders.** `src/actions/marketV1/supply.ts` and `src/actions/marketV1/withdraw.ts` + colocated unit tests + barrel update. - **Phase 5 — Entity wiring.** Two new methods on `MorphoMarketV1` (`supply`, `withdraw`); generalize `getReallocations` to take `{ amount, operation }`. - **Phase 6 — Fork tests.** Anvil mainnet at the pinned block; reuse `CbbtcUsdcMarketV1`, `SteakhouseUsdcVaultV1`, `WbtcUsdcSourceMarket`, `WstethUsdcSourceMarket` from existing fixtures. Cover happy paths, modes, native, permit2, reallocation single/multi/fee, `InsufficientSharedLiquidityError`, missing `setAuthorization`. @@ -143,7 +143,7 @@ Messages follow the canonical `" . ."` sha ## Considered Alternatives -### Alternative 1: Fork `computeReallocations` into a withdraw-specific helper +### Alternative 1: Fork `computeVaultV1Reallocations` into a withdraw-specific helper Add `computeWithdrawReallocations` alongside the existing function. @@ -178,7 +178,7 @@ Bundle the native-unwrap path with `withdraw` to ship a complete native story. - **Slippage is bounded on both sides.** Supply uses `maxSharePriceE27 = (assets / shares) × (WAD + slippage)` (upper bound, RAY-scaled), so a malicious actor inflating the share price via a donation between transaction construction and execution cannot dilute the supplier. Withdraw uses `minSharePriceE27 = (assets / shares) × (WAD − slippage)` (lower bound), capping the loss to slippage tolerance. Both helpers cap at `MAX_ABSOLUTE_SHARE_PRICE` like the existing repay helper. - **Authorization is enforced.** `withdraw` requires `setAuthorization(generalAdapter1, true)` on Morpho. `getRequirements` returns the typed authorization tx so integrators send it before the bundle; if they don't, the bundle reverts on the Morpho-side auth check. -- **Reallocation fees are paid only when they can actually unblock the withdraw.** `computeReallocations` continues to throw `InsufficientSharedLiquidityError` when the aggregate reallocatable liquidity strictly under-covers the absolute shortfall — preventing the user from paying ETH fees to the PublicAllocator on a withdraw that would still revert. +- **Reallocation fees are paid only when they can actually unblock the withdraw.** `computeVaultV1Reallocations` continues to throw `InsufficientSharedLiquidityError` when the aggregate reallocatable liquidity strictly under-covers the absolute shortfall — preventing the user from paying ETH fees to the PublicAllocator on a withdraw that would still revert. - **`validateReallocations` is reused unchanged.** Strict-ascending market IDs, no withdrawal on the target market, non-empty withdrawals, non-negative fee. - **Input validation runs before any encoding.** Every error is a named class (`Error` subclass) that integrators can pattern-match on; messages never leak raw `Error` strings from upstream. @@ -194,7 +194,7 @@ Bundle the native-unwrap path with `withdraw` to ship a complete native story. - `packages/morpho-sdk/src/actions/marketV1/borrow.ts` — closest existing template (slippage + reallocation). - `packages/morpho-sdk/src/actions/marketV1/repay.ts` — assets/shares mode reference. - `packages/morpho-sdk/src/actions/marketV1/supplyCollateral.ts` — native wrap reference. -- `packages/morpho-sdk/src/helpers/computeReallocations.ts` — extended in Phase 3. +- `packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts` — extended in Phase 3. - `packages/morpho-sdk/src/helpers/slippage.ts` — extended in Phase 2. - [`Morpho.sol`](https://github.com/morpho-org/morpho-blue/blob/main/src/Morpho.sol) — `supply` / `withdraw` reference. - [`GeneralAdapter1.sol`](https://github.com/morpho-org/bundler3/blob/main/src/adapters/GeneralAdapter1.sol) — `morphoSupply` / `morphoWithdraw` reference. diff --git a/docs/tibs/TIB-2026-06-16-shared-liquidity-target-utilization-metric.md b/docs/tibs/TIB-2026-06-16-shared-liquidity-target-utilization-metric.md index 2c2ac12b4..575ea837b 100644 --- a/docs/tibs/TIB-2026-06-16-shared-liquidity-target-utilization-metric.md +++ b/docs/tibs/TIB-2026-06-16-shared-liquidity-target-utilization-metric.md @@ -13,34 +13,34 @@ Integrators (frontends, allocators, risk dashboards) repeatedly ask one question about a Morpho Blue market: **"how much can still be borrowed here before it gets unhealthy, counting liquidity the PublicAllocator could pull in from sibling markets?"** -`computeReallocations` already answers a *transactional* variant of this — given a concrete borrow/withdraw `amount`, it builds the `reallocateTo` calls and **throws** when liquidity is insufficient. That shape is wrong for a display metric: +`computeVaultV1Reallocations` already answers a *transactional* variant of this — given a concrete borrow/withdraw `amount`, it builds the `reallocateTo` calls and **throws** when liquidity is insufficient. That shape is wrong for a display metric: - It needs an `amount` the caller is trying to find in the first place. - It throws on insufficiency, so callers must wrap it in try/catch just to read a number. - It returns calldata, not a quantity. -This TIB freezes the design of two read-only metrics that answer the question directly, exposed as methods on the `ReallocationData` entity the caller already holds. +This TIB freezes the design of two read-only metrics that answer the question directly, exposed as methods on the `VaultV1ReallocationData` entity the caller already holds. ## Goals / Non-Goals **Goals** -- Add `ReallocationData.getPublicReallocationLiquidity(marketId, options?)`: the total liquidity the PublicAllocator can reallocate **into** a market from sibling markets — a `bigint` that never throws on insufficiency (returns `0n`). It still throws `UnknownReallocationMarketError` when the target market is absent. -- Add `ReallocationData.getAvailableLiquidityToTargetUtilization(marketId, targetUtilization?, options?)`: the liquidity available to bring a market to a target utilization — the max borrow keeping post-borrow utilization at or below the target on the **post-reallocation** supply (`getBorrowToUtilization({ supply + L, borrow }, targetUtilization)`) — also never throws on insufficiency (same absent-market exception). +- Add `VaultV1ReallocationData.getPublicReallocationLiquidity(marketId, options?)`: the total liquidity the PublicAllocator can reallocate **into** a market from sibling markets — a `bigint` that never throws on insufficiency (returns `0n`). It still throws `UnknownReallocationMarketError` when the target market is absent. +- Add `VaultV1ReallocationData.getAvailableLiquidityToTargetUtilization(marketId, targetUtilization?, options?)`: the liquidity available to bring a market to a target utilization — the max borrow keeping post-borrow utilization at or below the target on the **post-reallocation** supply (`getBorrowToUtilization({ supply + L, borrow }, targetUtilization)`) — also never throws on insufficiency (same absent-market exception). - Reuse the existing PublicAllocator discovery (`getMarketPublicReallocations`) — no fork of the reallocation algorithm. -- Share the supply-target-utilization resolution with `computeReallocations` instead of duplicating it. +- Share the supply-target-utilization resolution with `computeVaultV1Reallocations` instead of duplicating it. **Non-Goals** -- No calldata. These metrics never produce a transaction; `computeReallocations` remains the builder. -- No mutation of `computeReallocations`' behavior or its public options. (An earlier `maintainSupplyTargetUtilization` opt-in explored for this was dropped — see Considered Alternatives.) +- No calldata. These metrics never produce a transaction; `computeVaultV1Reallocations` remains the builder. +- No mutation of `computeVaultV1Reallocations`' behavior or its public options. (An earlier `maintainSupplyTargetUtilization` opt-in explored for this was dropped — see Considered Alternatives.) - No modeling of the borrowed amount as new supply. Borrowing raises `totalBorrowAssets` but not `totalSupplyAssets`, so the metric measures borrow `x` against the post-reallocation supply `S + L` (`(B + x) / (S + L) ≤ targetUtilization`) — see Assumptions. ## Proposed Solution -### Placement: methods on `ReallocationData`, not standalone helpers +### Placement: methods on `VaultV1ReallocationData`, not standalone helpers -Both metrics only read from a `ReallocationData` instance (`getMarket`, `getMarketPublicReallocations`) and return a derived `bigint`. They live as **methods on the entity** the caller already obtains from `MorphoBlue.getReallocationData(...)`, next to the `getMarketPublicReallocations` they wrap. They stay pure (no I/O, no mutation), consistent with the entity layer's "compute derived values" role. They are intentionally **not** `MorphoBlue` methods (no chainId/fetch coupling) and **not** free helpers (they belong with the state they read). +Both metrics only read from a `VaultV1ReallocationData` instance (`getMarket`, `getMarketPublicReallocations`) and return a derived `bigint`. They live as **methods on the entity** the caller already obtains from `MorphoBlue.getReallocationData(...)`, next to the `getMarketPublicReallocations` they wrap. They stay pure (no I/O, no mutation), consistent with the entity layer's "compute derived values" role. They are intentionally **not** `MorphoBlue` methods (no chainId/fetch coupling) and **not** free helpers (they belong with the state they read). ### `getPublicReallocationLiquidity` @@ -66,16 +66,16 @@ Two facts about the metric's meaning: 1. **Below the reallocation trigger, only own liquidity counts.** The PublicAllocator only reallocates once a market crosses its `supplyTargetUtilization`. If the caller asks for a target *below* that trigger, no reallocation would happen, so the answer is the market's own borrow headroom alone. 2. **Otherwise, borrow-to-target on the post-reallocation supply.** Reallocated supply `L` is added to the market's supply, so the borrowable amount is `getBorrowToUtilization({ supply + L, borrow }, targetUtilization)`. Below the target this equals own headroom + `targetUtilization · L` — `L` only contributes its scaled share, since it also raises the denominator. At or above the target, `zeroFloorSub` clamps to `0n` when `L` is too small to bring utilization back under the target; borrowing more would only push it further over. (The earlier `targetUtilization === market.utilization` special case is subsumed: there `ownHeadroom` is `0` and the formula returns `targetUtilization · L`.) -> Unlike the transactional `computeReallocations` fallback, this metric never relaxes the target market toward 100% and never force-drains source markets: it honours whatever source withdrawal cap the caller configures (friendly by default). +> Unlike the transactional `computeVaultV1Reallocations` fallback, this metric never relaxes the target market toward 100% and never force-drains source markets: it honours whatever source withdrawal cap the caller configures (friendly by default). ### Shared resolution helper -`computeReallocations` and `getAvailableLiquidityToTargetUtilization` both need the effective supply-target utilization for a market (per-market override → default override → `DEFAULT_SUPPLY_TARGET_UTILIZATION`). That resolution is factored into one helper, `getSupplyTargetUtilization(marketId, options)`, instead of being duplicated. +`computeVaultV1Reallocations` and `getAvailableLiquidityToTargetUtilization` both need the effective supply-target utilization for a market (per-market override → default override → `DEFAULT_SUPPLY_TARGET_UTILIZATION`). That resolution is factored into one helper, `getSupplyTargetUtilization(marketId, options)`, instead of being duplicated. ### Implementation ```ts -// ReallocationData.getAvailableLiquidityToTargetUtilization +// VaultV1ReallocationData.getAvailableLiquidityToTargetUtilization const market = this.getMarket(marketId).accrueInterest(options?.timestamp); const supplyTargetUtilization = getSupplyTargetUtilization(marketId, options); @@ -96,17 +96,17 @@ return MarketUtils.getBorrowToUtilization( // rul ## Considered Alternatives -### Alternative 1: Add an opt-in flag to `computeReallocations` +### Alternative 1: Add an opt-in flag to `computeVaultV1Reallocations` -The first iteration threaded a `maintainSupplyTargetUtilization` boolean through `ReallocationComputeOptions` and `computeReallocations` (holding the target market at its supply target instead of relaxing it to 100% in the aggressive phase). +The first iteration threaded a `maintainSupplyTargetUtilization` boolean through `ReallocationComputeOptions` and `computeVaultV1Reallocations` (holding the target market at its supply target instead of relaxing it to 100% in the aggressive phase). -**Why rejected:** The metric takes no borrow amount and never builds calldata, so it shares almost nothing with `computeReallocations`'s control flow. Bolting it on widened the builder's option surface and its phase-2 branch for a read-only concern. A read-only metric on `ReallocationData` is smaller, purer, and easier to test. The flag was fully reverted. +**Why rejected:** The metric takes no borrow amount and never builds calldata, so it shares almost nothing with `computeVaultV1Reallocations`'s control flow. Bolting it on widened the builder's option surface and its phase-2 branch for a read-only concern. A read-only metric on `VaultV1ReallocationData` is smaller, purer, and easier to test. The flag was fully reverted. ### Alternative 2: Standalone helpers in the `helpers/` layer The metrics were first shipped as free `compute*` helpers (`computeAvailableSharedLiquidity`, `computeAvailableLiquidityToTargetUtilization`) re-exported from the package root. -**Why rejected (review feedback):** they only operate on a `ReallocationData` instance and wrap its `getMarketPublicReallocations`, so they read more naturally as methods on that class (next to the data they consume) than as helpers that take the entity as an argument. Moving them also keeps the public helper surface minimal. +**Why rejected (review feedback):** they only operate on a `VaultV1ReallocationData` instance and wrap its `getMarketPublicReallocations`, so they read more naturally as methods on that class (next to the data they consume) than as helpers that take the entity as an argument. Moving them also keeps the public helper surface minimal. ### Alternative 3: Force-drain sources to 100% utilization @@ -123,14 +123,14 @@ Add the full reallocatable liquidity `L` to the own headroom without scaling. ## Assumptions & Constraints - **Exact for Morpho borrow semantics.** The return is `getBorrowToUtilization({ supply + L, borrow }, targetUtilization)` = `zeroFloorSub(wMulDown(supply + L, targetUtilization), borrow)`, the max borrow `x` keeping post-borrow utilization `(borrow + x) / (supply + L)` at or below the target. Borrowing raises `totalBorrowAssets` only, not `totalSupplyAssets`, so `x` is not in the denominator; an above-target market clamps to `0n` when reallocation cannot bring it back under. Below the target this equals own headroom + `targetUtilization · L` (to within 1 wei of fixed-point flooring). -- **Pass `options.timestamp` from the fetch block.** Accrual otherwise falls back to the target market's `lastUpdate`, which can diverge from the source rows' fetch block — same constraint as `computeReallocations`. -- Pure entity methods, no I/O, no mutation. Additive public surface (two new `ReallocationData` methods + one internal helper). Semver: **minor**. +- **Pass `options.timestamp` from the fetch block.** Accrual otherwise falls back to the target market's `lastUpdate`, which can diverge from the source rows' fetch block — same constraint as `computeVaultV1Reallocations`. +- Pure entity methods, no I/O, no mutation. Additive public surface (two new `VaultV1ReallocationData` methods + one internal helper). Semver: **minor**. - `viem` stays the only peer dep. No new runtime dependencies. ## References -- `packages/morpho-sdk/src/entities/reallocationData.ts` — `getPublicReallocationLiquidity`, `getAvailableLiquidityToTargetUtilization`, and the `getMarketPublicReallocations` discovery they reuse. -- `packages/morpho-sdk/src/helpers/utilization.ts` — `getSupplyTargetUtilization`, shared with `computeReallocations`. -- `packages/morpho-sdk/src/helpers/computeReallocations.ts` — the transactional counterpart (builds calldata, throws on insufficiency). +- `packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts` — `getPublicReallocationLiquidity`, `getAvailableLiquidityToTargetUtilization`, and the `getMarketPublicReallocations` discovery they reuse. +- `packages/morpho-sdk/src/helpers/utilization.ts` — `getSupplyTargetUtilization`, shared with `computeVaultV1Reallocations`. +- `packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts` — the transactional counterpart (builds calldata, throws on insufficiency). - `DEFAULT_SUPPLY_TARGET_UTILIZATION` (90.5%) / `DEFAULT_WITHDRAWAL_TARGET_UTILIZATION` (92%) in `src/helpers/constant.ts`. - Root [`AGENTS.md`](../../AGENTS.md) §1 (entity layer / purity), §3 (types), §5 (testing), §6 (JSDoc). diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index 4bc7003f9..ff6e01d1b 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -5,7 +5,7 @@ | **Status** | Accepted | | **Date** | 2026-07-29 | | **Author** | @foulques | -| **Scope** | `morpho-sdk`, `blue-sdk-viem`, `blue-sdk`, and `morpho-ts` | +| **Scope** | `morpho-sdk`, `liquidity-sdk-viem`, `blue-sdk-viem`, `blue-sdk`, and `morpho-ts` | ## Context @@ -14,13 +14,14 @@ Morpho Blue market through PublicAllocator V1: ```text MorphoBlue.getReallocationData() - → ReallocationData.computeVaultV1Reallocations() # discovery - → computeReallocations() # borrow/withdraw planner + → VaultV1ReallocationData.computeVaultV1Reallocations() # discovery + → computeVaultV1Reallocations() # borrow/withdraw planner → VaultV1BlueReallocation[] → PublicAllocator.reallocateTo(...) ``` -The historical names `getMarketPublicReallocations()` and +The historical V1 names `ReallocationData`, `InputReallocationData`, +`computeReallocations()`, `getMarketPublicReallocations()`, and `VaultReallocation` remain as deprecated aliases for the prescribed deprecation window. @@ -34,14 +35,14 @@ This TIB freezes that Vault V2 design. ## Goals -- Add `ReallocationDataVaultV2.computeVaultV2Reallocations(...)` for greedy, +- Add `VaultV2ReallocationData.computeVaultV2Reallocations(...)` for greedy, largest-first discovery. -- Add `computeReallocationsVaultV2(...)` for amount-aware borrow and withdraw +- Add `computeVaultV2Reallocations(...)` for amount-aware borrow and withdraw planning. - Return flat, action-ready `VaultV2BlueReallocation[]`; one entry is exactly one `reallocate(...)` or `allocateFromIdle(...)` call and pays one `nativePenalty`. -- Keep `computeReallocations(...)` as the Vault V1 planner and make the +- Keep `computeVaultV1Reallocations(...)` as the Vault V1 planner and make the versioned V1 discovery/type names canonical. - Simulate the allocator target cap, all three Vault V2 allocation caps, source Blue liquidity and utilization, shared allocation IDs, untracked @@ -55,23 +56,27 @@ This TIB freezes that Vault V2 design. explicit input to fetchers, state, and every returned call. - No curator-facing setters such as `setAbsoluteCap`, `setCanDeallocate`, or `setNativePenalty`. -- No `liquidity-sdk-viem` release. Its use of the deprecated V1 discovery - alias remains source-compatible. -- No penalty-efficiency optimizer. Candidates are ranked by obtainable assets. +- No penalty-efficiency optimizer beyond an explicit maximum native-penalty + filter. Retained candidates are ranked by obtainable assets. ## Public API and naming | Concern | Canonical symbol | | --- | --- | -| V1 discovery | `ReallocationData.computeVaultV1Reallocations(marketId, options?)` | -| V1 discovery compatibility | `ReallocationData.getMarketPublicReallocations(...)` (`@deprecated`) | -| V1 planner | `computeReallocations(...)` | +| V1 state | `VaultV1ReallocationData` / `InputVaultV1ReallocationData` | +| V1 state compatibility | `ReallocationData` / `InputReallocationData` (`@deprecated` aliases) | +| V1 discovery | `VaultV1ReallocationData.computeVaultV1Reallocations(marketId, options?)` | +| V1 discovery compatibility | `VaultV1ReallocationData.getMarketPublicReallocations(...)` (`@deprecated`) | +| V1 planner | `computeVaultV1Reallocations(...)` | +| V1 planner compatibility | `computeReallocations(...)` (`@deprecated` alias) | | V1 action input | `VaultV1BlueReallocation` | | V1 type compatibility | `VaultReallocation` (`@deprecated` alias) | -| V2 state | `ReallocationDataVaultV2` / `InputReallocationDataVaultV2` | -| V2 discovery | `ReallocationDataVaultV2.computeVaultV2Reallocations(marketId, options?)` | -| V2 planner | `computeReallocationsVaultV2(...)` | +| V2 state | `VaultV2ReallocationData` / `InputVaultV2ReallocationData` | +| V2 discovery | `VaultV2ReallocationData.computeVaultV2Reallocations(marketId, options?)` | +| V2 planner | `computeVaultV2Reallocations(...)` | | V2 action input | `VaultV2BlueReallocation` | +| V2 Bundler actions | `vaultV2BluePublicAllocatorReallocate`, `vaultV2BluePublicAllocatorAllocateFromIdle` | +| V2 allocator ABI | `vaultV2BluePublicAllocatorAbi` | | Shared action union | `BlueReallocation` | | V2 options | `PublicAllocatorOptionsVaultV2`, `ReallocationComputeOptionsVaultV2` | | V2 config | `VaultV2PublicAllocatorConfig`, `VaultV2MarketPublicAllocatorConfig` | @@ -79,7 +84,8 @@ This TIB freezes that Vault V2 design. `BluePublicAllocatorReallocation` was unreleased relative to `origin/main` and is renamed directly to `VaultV2BlueReallocation`; it has no compatibility -alias. +alias. The former unversioned V2 state, planner, ABI, and Bundler action names +were also unreleased and are renamed directly without aliases. The action-ready V2 shape is flat: @@ -158,7 +164,7 @@ State is therefore keyed by `(vault, derivedId)`, not by a projected `(vault, adapter, market)` tuple: ```ts -export interface InputReallocationDataVaultV2 { +export interface InputVaultV2ReallocationData { readonly chainId: number; readonly allocator: Address; readonly markets?: Readonly>; @@ -185,7 +191,7 @@ also carries `adapter`, `marketParamsId`, `absoluteCap`, `canDeallocate`, and ## Fetching -`bluePublicAllocatorAbi` includes the three allocator mapping reads and +`vaultV2BluePublicAllocatorAbi` includes the three allocator mapping reads and `vaultData`. Fetchers always take the allocator address explicitly: - `fetchVaultV2PublicAllocatorConfig(allocator, vault, client, parameters?)`; @@ -247,6 +253,8 @@ A candidate exists only when: `adaptiveCurveIrm`; - target/source adapters are active, source deallocation is permitted, or idle allocation is permitted; +- the vault's configured `nativePenalty` does not exceed + `options.maxNativePenalty` when that threshold is provided; - all three target vault caps have a positive absolute cap; - all three source allocations are non-zero for market sources; - the source pair is not the exact target `(adapter, market)` pair. The same @@ -295,7 +303,7 @@ folded into vault accounting. ## Planner -`computeReallocationsVaultV2` uses the same operation algebra and target +`computeVaultV2Reallocations` uses the same operation algebra and target utilization calculation as V1: - borrow: `B' = B + amount`, `S' = S`; @@ -304,7 +312,8 @@ utilization calculation as V1: If the post-operation utilization is at most the fixed 90% target, it returns no calls. Otherwise it discovers friendly sources using the fixed 90% source ceiling. If the operation would still have `borrow > supply`, it continues -from the friendly post-state with an internal 100% source ceiling. +from the friendly post-state with an internal 100% source ceiling. Both phases +ignore vaults above the configured `maxNativePenalty` threshold. The flat calls are capped in discovery order to the required amount. Every retained call keeps its full `nativePenalty`. The planner throws: @@ -320,7 +329,7 @@ The existing `validateReallocations` validates the combined `BlueReallocation` union. A V2 market source is rejected only when both its adapter and market match the target pair. The same market through another adapter is accepted. -`ReallocationDataVaultV2` exposes: +`VaultV2ReallocationData` exposes: - `getPublicReallocationLiquidityVaultV2(...)`, which sums market and idle candidates; and @@ -361,9 +370,16 @@ source and target thresholds plus an internal 100% fallback. `computeVaultV1Reallocations` and is marked deprecated. - `VaultReallocation` aliases `VaultV1BlueReallocation` and is marked deprecated. -- `computeReallocations` remains the V1 amount-aware planner. +- `ReallocationData` and `InputReallocationData` alias + `VaultV1ReallocationData` and `InputVaultV1ReallocationData`, respectively, + and are marked deprecated. +- `computeReallocations` aliases `computeVaultV1Reallocations` and is marked + deprecated. - `BluePublicAllocatorReallocation` receives no alias because it was not part of the published surface relative to `origin/main`. +- `liquidity-sdk-viem` migrates its public state declarations to + `VaultV1ReallocationData`; this is type-compatible with the deprecated class + alias and ships as a patch. - The feature is minor for `morpho-ts`, `blue-sdk`, `blue-sdk-viem`, and `morpho-sdk`. - `blue-sdk-viem` raises its `blue-sdk` peer range to the new minor. @@ -399,9 +415,9 @@ source and target thresholds plus an internal 100% fallback. - [`VaultV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/VaultV2.sol) - [`MorphoMarketV1AdapterV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/adapters/MorphoMarketV1AdapterV2.sol) - [TIB-2026-06-16 shared-liquidity target-utilization metric](./TIB-2026-06-16-shared-liquidity-target-utilization-metric.md) -- `packages/morpho-sdk/src/entities/reallocationData.ts` -- `packages/morpho-sdk/src/entities/reallocationDataVaultV2.ts` -- `packages/morpho-sdk/src/helpers/computeReallocations.ts` -- `packages/morpho-sdk/src/helpers/computeReallocationsVaultV2.ts` +- `packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts` +- `packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts` +- `packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts` +- `packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts` - `packages/blue-sdk/src/vault/v2/VaultV2Utils.ts` - `packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts` diff --git a/packages/blue-sdk-viem/src/abis.ts b/packages/blue-sdk-viem/src/abis.ts index de24f05cf..821a7845c 100644 --- a/packages/blue-sdk-viem/src/abis.ts +++ b/packages/blue-sdk-viem/src/abis.ts @@ -1,5 +1,4 @@ export { - bluePublicAllocatorAbi, erc2612Abi, erc5267Abi, metaMorphoAbi, @@ -16,6 +15,7 @@ export { vaultV1AdapterAbi, vaultV1AdapterFactoryAbi, vaultV2Abi, + vaultV2BluePublicAllocatorAbi, vaultV2FactoryAbi, whitelistControllerAggregatorV2Abi, wrappedBackedTokenAbi, diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts index 5092aee55..9873d7ec3 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts @@ -14,7 +14,7 @@ import { mockDeploylessRead, mockDeploylessReads, } from "../../__test__/viem.js"; -import { bluePublicAllocatorAbi, vaultV2Abi } from "../../abis.js"; +import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; import { abi as queryAbi } from "../../queries/vault-v2/GetVaultV2PublicAllocatorConfig.js"; import { fetchVaultV2MarketPublicAllocatorConfig, @@ -114,25 +114,25 @@ const expected = { const mockDirectReads = (handle: ReturnType) => { mockRead(handle, { address: ALLOCATOR, - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "vaultData", result: [true, 12n, 34n], }); mockRead(handle, { address: ALLOCATOR, - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "absoluteCap", result: 500n, }); mockRead(handle, { address: ALLOCATOR, - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "canDeallocate", result: true, }); mockRead(handle, { address: ALLOCATOR, - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "isActiveAdapter", result: true, }); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts index 52e7eaf93..a308f2bda 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts @@ -7,7 +7,7 @@ import { } from "@morpho-org/blue-sdk"; import type { Address, Client, Hash } from "viem"; import { readContract } from "viem/actions"; -import { bluePublicAllocatorAbi, vaultV2Abi } from "../../abis.js"; +import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; import { abi, code, @@ -45,7 +45,7 @@ export async function fetchVaultV2PublicAllocatorConfig( const [canAllocateFromIdle, nativePenalty] = await readContract(client, { ...parameters, address: allocator, - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "vaultData", args: [vault], }); @@ -97,21 +97,21 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( readContract(client, { ...parameters, address: allocator, - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "absoluteCap", args: [vault, marketParamsId], }), readContract(client, { ...parameters, address: allocator, - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "canDeallocate", args: [vault, marketParamsId], }), readContract(client, { ...parameters, address: allocator, - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "isActiveAdapter", args: [vault, adapter], }), diff --git a/packages/liquidity-sdk-viem/src/loader.ts b/packages/liquidity-sdk-viem/src/loader.ts index b824cafbf..af1530271 100644 --- a/packages/liquidity-sdk-viem/src/loader.ts +++ b/packages/liquidity-sdk-viem/src/loader.ts @@ -6,7 +6,7 @@ import { fetchVaultMarketConfig, } from "@morpho-org/blue-sdk-viem"; import type { PublicReallocation } from "@morpho-org/morpho-sdk"; -import { ReallocationData } from "@morpho-org/morpho-sdk/entities"; +import { VaultV1ReallocationData } from "@morpho-org/morpho-sdk/entities"; import { entries, fromEntries, isDefined } from "@morpho-org/morpho-ts"; import DataLoader from "dataloader"; import type { Chain, Client, Transport } from "viem"; @@ -42,8 +42,8 @@ export class LiquidityLoader { protected readonly dataLoader: DataLoader< MarketId, { - startState: ReallocationData; - endState: ReallocationData; + startState: VaultV1ReallocationData; + endState: VaultV1ReallocationData; withdrawals: readonly PublicReallocation[]; targetBorrowUtilization: bigint; } @@ -157,7 +157,7 @@ export class LiquidityLoader { ), ]); - const startState = new ReallocationData({ + const startState = new VaultV1ReallocationData({ chainId, markets: fromEntries(markets.map((market) => [market.id, market])), vaults: fromEntries(vaults.map((vault) => [vault.address, vault])), @@ -222,7 +222,7 @@ export class LiquidityLoader { * @param marketId - Target market id to plan withdrawals for. * @returns The start state, simulated end state, computed withdrawals, and target borrow utilization. * - * @remarks The returned `endState` is produced by `ReallocationData.getMarketPublicReallocations` + * @remarks The returned `endState` is produced by `VaultV1ReallocationData.getMarketPublicReallocations` * from onchain inputs fetched at one block, with reallocation headroom evaluated one hour after * that block timestamp. * @@ -244,7 +244,7 @@ export class LiquidityLoader { * const { withdrawals, endState } = await loader.fetch(marketId); * * // withdrawals: readonly PublicReallocation[] - * // endState: ReallocationData + * // endState: VaultV1ReallocationData * ``` */ public fetch(marketId: MarketId) { diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index a2e1442e1..d0d058170 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -27,9 +27,9 @@ Protocol terms used across this package's docs and JSDoc: - **bundler3** — the bundler entry point; receives a sequence of adapter actions in one transaction. - **GeneralAdapter1** — the bundler-side adapter that holds approvals/auth and executes Morpho calls on the user's behalf. Required as the spender for ERC-20 approvals on every bundled path; required as authorized operator on Morpho for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, and `withdraw` (the supplier-side path). - **PublicAllocator V1** — MetaMorpho allocator that moves liquidity from one or more sorted source markets into a target via `reallocateTo(...)`; each call pays one `fee`. -- **BluePublicAllocator** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered; each call pays its own `nativePenalty`. +- **BluePublicAllocator** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered; each call pays its own `nativePenalty`. Its canonical ABI export is `vaultV2BluePublicAllocatorAbi`. - **VaultExitBundlesV1** — standalone periphery for exiting an illiquid VaultV1 or single-adapter VaultV2 into idle underlying assets and/or Morpho Blue supply positions. -- **Shared-liquidity naming** — `ReallocationData.computeVaultV1Reallocations` and `VaultV1BlueReallocation` are canonical for PublicAllocator V1 (`getMarketPublicReallocations` and `VaultReallocation` are deprecated aliases). `ReallocationDataVaultV2.computeVaultV2Reallocations` and `computeReallocationsVaultV2` produce flat, action-ready `VaultV2BlueReallocation` calls. +- **Shared-liquidity naming** — `VaultV1ReallocationData`, `InputVaultV1ReallocationData`, `computeVaultV1Reallocations`, and `VaultV1BlueReallocation` are canonical for PublicAllocator V1. Their published predecessors (`ReallocationData`, `InputReallocationData`, `computeReallocations`, `getMarketPublicReallocations`, and `VaultReallocation`) remain deprecated aliases. `VaultV2ReallocationData.computeVaultV2Reallocations` and `computeVaultV2Reallocations` produce flat, action-ready `VaultV2BlueReallocation` calls. ### Bundler actions @@ -42,7 +42,7 @@ The action verbs an integrator sees in the bundle (`BundlerAction.encode...`): - **`nativeTransfer` + `wrapNative`** — pair that converts an attached native amount (`tx.value`) into the chain's wNative for a deposit/supply path. - **`forceDeallocate`** — VaultV2 multicall entry that pulls liquidity out of a specific adapter before withdraw/redeem. - **`reallocateTo`** — PublicAllocator V1 call that shifts liquidity from sorted source markets into the target market. -- **`bluePublicAllocatorReallocate` / `bluePublicAllocatorAllocateFromIdle`** — BluePublicAllocator calls that move one market source or vault idle liquidity into the enclosing Blue action's target market. Both target an explicit allocator address and carry one native penalty. +- **`vaultV2BluePublicAllocatorReallocate` / `vaultV2BluePublicAllocatorAllocateFromIdle`** — BluePublicAllocator calls that move one market source or vault idle liquidity into the enclosing Blue action's target market. Both target an explicit allocator address and carry one native penalty. ### Constants and conventions diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index 12765464c..b745d0da1 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -26,7 +26,7 @@ The **spender** of every approval / permit / permit2 is therefore **always** `ge The value of the Bundler3 + GeneralAdapter1 pairing rests on three properties: -1. **Composition of elementary actions.** Each step (`nativeTransfer`, `wrapNative`, `erc20TransferFrom`, `permit`, `approve2`, `transferFrom2`, `erc4626Deposit`, `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral`, `reallocateTo`, `bluePublicAllocatorReallocate`, `bluePublicAllocatorAllocateFromIdle`) is an independent building block. The SDK **composes** them in an explicit order to build a business flow. +1. **Composition of elementary actions.** Each step (`nativeTransfer`, `wrapNative`, `erc20TransferFrom`, `permit`, `approve2`, `transferFrom2`, `erc4626Deposit`, `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral`, `reallocateTo`, `vaultV2BluePublicAllocatorReallocate`, `vaultV2BluePublicAllocatorAllocateFromIdle`) is an independent building block. The SDK **composes** them in an explicit order to build a business flow. 2. **Atomicity.** The entire bundle either succeeds or reverts as one. No intermediate state is exposed to MEV bots or other transactions. 3. **Simplified approval UX.** A user approves _a single spender_ (GeneralAdapter1) for the entire protocol surface — rather than one approval per V1/V2 vault or per Morpho contract. diff --git a/packages/morpho-sdk/src/abis.ts b/packages/morpho-sdk/src/abis.ts index 524b15731..111286e1d 100644 --- a/packages/morpho-sdk/src/abis.ts +++ b/packages/morpho-sdk/src/abis.ts @@ -5,7 +5,6 @@ export { adaptiveCurveIrmAbi, blueAbi, blueOracleAbi, - bluePublicAllocatorAbi, erc2612Abi, erc5267Abi, metaMorphoAbi, @@ -24,6 +23,7 @@ export { vaultV1AdapterAbi, vaultV1AdapterFactoryAbi, vaultV2Abi, + vaultV2BluePublicAllocatorAbi, vaultV2FactoryAbi, whitelistControllerAggregatorV2Abi, wrappedBackedTokenAbi, diff --git a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts index 4fa35a8f6..6a9e56664 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -1,12 +1,12 @@ import { ChainId, MarketParams } from "@morpho-org/blue-sdk"; -import { bluePublicAllocatorAbi as canonicalBluePublicAllocatorAbi } from "@morpho-org/blue-sdk-viem"; +import { vaultV2BluePublicAllocatorAbi as canonicalVaultV2BluePublicAllocatorAbi } from "@morpho-org/blue-sdk-viem"; import { decodeFunctionData } from "viem"; import { describe, expect, test } from "vitest"; import { - bluePublicAllocatorAbi, bundler3Abi, generalAdapter1Abi, publicAllocatorAbi, + vaultV2BluePublicAllocatorAbi, } from "../../abis.js"; import type { BlueReallocation } from "../../types/index.js"; import { blueBorrow } from "./borrow.js"; @@ -98,13 +98,13 @@ describe("blueBorrow Blue Public Allocator", () => { expect(publicAllocatorCall.args[0]).toBe(vaultV1); expect( decodeFunctionData({ - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, data: calls[1]!.data, }).functionName, ).toBe("reallocate"); const idleCall = decodeFunctionData({ - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, data: calls[2]!.data, }); expect(idleCall.functionName).toBe("allocateFromIdle"); @@ -125,6 +125,8 @@ describe("blueBorrow Blue Public Allocator", () => { }); test("re-exports the canonical ABI", () => { - expect(bluePublicAllocatorAbi).toBe(canonicalBluePublicAllocatorAbi); + expect(vaultV2BluePublicAllocatorAbi).toBe( + canonicalVaultV2BluePublicAllocatorAbi, + ); }); }); diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index f98a4461a..4a707a94b 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -37,7 +37,7 @@ export const buildReallocationActions = ( if (reallocation.type === "bluePublicAllocator") { if (reallocation.from.type === "market") { actions.push({ - type: "bluePublicAllocatorReallocate", + type: "vaultV2BluePublicAllocatorReallocate", args: [ reallocation.allocator, reallocation.vault, @@ -52,7 +52,7 @@ export const buildReallocationActions = ( }); } else { actions.push({ - type: "bluePublicAllocatorAllocateFromIdle", + type: "vaultV2BluePublicAllocatorAllocateFromIdle", args: [ reallocation.allocator, reallocation.vault, diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index e0deefb1d..3bc5fcf55 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -34,7 +34,7 @@ export interface BlueWithdrawParams { /** * Public Allocator V1 or V2 reallocations to execute before withdrawing. V1 entries can be * computed via `MorphoBlue.getReallocations({ operation: "withdraw", amount })` or directly - * via `computeReallocations({ operation: "withdraw", amount, ... })`. + * via `computeVaultV1Reallocations({ operation: "withdraw", amount, ... })`. */ reallocations?: readonly BlueReallocation[]; /** diff --git a/packages/morpho-sdk/src/bundler/actions.test.ts b/packages/morpho-sdk/src/bundler/actions.test.ts index 49ecf7030..db6e75c2f 100644 --- a/packages/morpho-sdk/src/bundler/actions.test.ts +++ b/packages/morpho-sdk/src/bundler/actions.test.ts @@ -5,10 +5,10 @@ import { } from "@morpho-org/blue-sdk"; import { blueAbi, - bluePublicAllocatorAbi, erc2612Abi, permit2Abi, publicAllocatorAbi, + vaultV2BluePublicAllocatorAbi, } from "@morpho-org/blue-sdk-viem"; import fc from "fast-check"; import { @@ -361,7 +361,7 @@ describe("BundlerAction", () => { .map( (args) => ({ - type: "bluePublicAllocatorReallocate", + type: "vaultV2BluePublicAllocatorReallocate", args, }) satisfies Action, ), @@ -378,7 +378,7 @@ describe("BundlerAction", () => { .map( (args) => ({ - type: "bluePublicAllocatorAllocateFromIdle", + type: "vaultV2BluePublicAllocatorAllocateFromIdle", args, }) satisfies Action, ), @@ -613,7 +613,7 @@ describe("BundlerAction", () => { test("encodeBundle aggregates Blue Public Allocator native penalties", () => { const tx = BundlerAction.encodeBundle(chainId, [ { - type: "bluePublicAllocatorReallocate", + type: "vaultV2BluePublicAllocatorReallocate", args: [ allocator, vault, @@ -627,7 +627,7 @@ describe("BundlerAction", () => { ], }, { - type: "bluePublicAllocatorAllocateFromIdle", + type: "vaultV2BluePublicAllocatorAllocateFromIdle", args: [allocator, vault, allocateAdapter, market, 3n, 4n, false], }, ]); @@ -1042,9 +1042,9 @@ describe("BundlerAction", () => { ), ], [ - "bluePublicAllocatorReallocate", + "vaultV2BluePublicAllocatorReallocate", { - type: "bluePublicAllocatorReallocate", + type: "vaultV2BluePublicAllocatorReallocate", args: [ allocator, vault, @@ -1057,7 +1057,7 @@ describe("BundlerAction", () => { false, ], }, - BundlerAction.bluePublicAllocatorReallocate( + BundlerAction.vaultV2BluePublicAllocatorReallocate( allocator, vault, deallocateAdapter, @@ -1070,12 +1070,12 @@ describe("BundlerAction", () => { ), ], [ - "bluePublicAllocatorAllocateFromIdle", + "vaultV2BluePublicAllocatorAllocateFromIdle", { - type: "bluePublicAllocatorAllocateFromIdle", + type: "vaultV2BluePublicAllocatorAllocateFromIdle", args: [allocator, vault, allocateAdapter, market, 22n, 23n, false], }, - BundlerAction.bluePublicAllocatorAllocateFromIdle( + BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( allocator, vault, allocateAdapter, @@ -1558,9 +1558,9 @@ describe("BundlerAction", () => { expect(decoded.args).toEqual([vault, withdrawals, market]); }); - test("bluePublicAllocatorReallocate", () => { + test("vaultV2BluePublicAllocatorReallocate", () => { const call = onlyCall( - BundlerAction.bluePublicAllocatorReallocate( + BundlerAction.vaultV2BluePublicAllocatorReallocate( allocator, vault, deallocateAdapter, @@ -1573,7 +1573,7 @@ describe("BundlerAction", () => { ), ); const decoded = decodeFunctionData({ - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, data: call.data, }); @@ -1591,9 +1591,9 @@ describe("BundlerAction", () => { ]); }); - test("bluePublicAllocatorAllocateFromIdle", () => { + test("vaultV2BluePublicAllocatorAllocateFromIdle", () => { const call = onlyCall( - BundlerAction.bluePublicAllocatorAllocateFromIdle( + BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( allocator, vault, allocateAdapter, @@ -1604,7 +1604,7 @@ describe("BundlerAction", () => { ), ); const decoded = decodeFunctionData({ - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, data: call.data, }); diff --git a/packages/morpho-sdk/src/bundler/actions.ts b/packages/morpho-sdk/src/bundler/actions.ts index 3c96fbd18..56d7fb4b8 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -4,10 +4,10 @@ import { } from "@morpho-org/blue-sdk"; import { blueAbi, - bluePublicAllocatorAbi, erc2612Abi, permit2Abi, publicAllocatorAbi, + vaultV2BluePublicAllocatorAbi, } from "@morpho-org/blue-sdk-viem"; import { type Address, @@ -343,11 +343,13 @@ export namespace BundlerAction { case "reallocateTo": { return BundlerAction.publicAllocatorReallocateTo(chainId, ...args); } - case "bluePublicAllocatorReallocate": { - return BundlerAction.bluePublicAllocatorReallocate(...args); + case "vaultV2BluePublicAllocatorReallocate": { + return BundlerAction.vaultV2BluePublicAllocatorReallocate(...args); } - case "bluePublicAllocatorAllocateFromIdle": { - return BundlerAction.bluePublicAllocatorAllocateFromIdle(...args); + case "vaultV2BluePublicAllocatorAllocateFromIdle": { + return BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + ...args, + ); } case "wrapNative": { return BundlerAction.wrapNative(chainId, ...args); @@ -1449,7 +1451,7 @@ export namespace BundlerAction { } /** - * Encodes a Blue Public Allocator market-to-market reallocation. + * Encodes a Vault V2 Blue Public Allocator market-to-market reallocation. * * @param allocator - Explicit Blue Public Allocator contract address. * @param vault - Vault whose liquidity is reallocated. @@ -1481,7 +1483,7 @@ export namespace BundlerAction { * collateralToken: "0x0000000000000000000000000000000000000009", * }; * - * const calls = BundlerAction.bluePublicAllocatorReallocate( + * const calls = BundlerAction.vaultV2BluePublicAllocatorReallocate( * allocator, * vault, * sourceAdapter, @@ -1495,7 +1497,7 @@ export namespace BundlerAction { * ``` */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call - export function bluePublicAllocatorReallocate( + export function vaultV2BluePublicAllocatorReallocate( allocator: Address, vault: Address, deallocateAdapter: Address, @@ -1510,7 +1512,7 @@ export namespace BundlerAction { { to: allocator, data: encodeFunctionData({ - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "reallocate", args: [ vault, @@ -1529,7 +1531,7 @@ export namespace BundlerAction { } /** - * Encodes a Blue Public Allocator allocation from vault idle liquidity. + * Encodes a Vault V2 Blue Public Allocator allocation from vault idle liquidity. * * @param allocator - Explicit Blue Public Allocator contract address. * @param vault - Vault whose idle liquidity is allocated. @@ -1554,7 +1556,7 @@ export namespace BundlerAction { * lltv: 860_000000000000000000n, * }; * - * const calls = BundlerAction.bluePublicAllocatorAllocateFromIdle( + * const calls = BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( * allocator, * vault, * targetAdapter, @@ -1566,7 +1568,7 @@ export namespace BundlerAction { * ``` */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call - export function bluePublicAllocatorAllocateFromIdle( + export function vaultV2BluePublicAllocatorAllocateFromIdle( allocator: Address, vault: Address, adapter: Address, @@ -1579,7 +1581,7 @@ export namespace BundlerAction { { to: allocator, data: encodeFunctionData({ - abi: bluePublicAllocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "allocateFromIdle", args: [vault, adapter, market, assets], }), diff --git a/packages/morpho-sdk/src/bundler/types.ts b/packages/morpho-sdk/src/bundler/types.ts index 22ded926a..884ec0599 100644 --- a/packages/morpho-sdk/src/bundler/types.ts +++ b/packages/morpho-sdk/src/bundler/types.ts @@ -210,8 +210,8 @@ export interface ActionArgs { skipRevert?: boolean, ]; - /** Blue Public Allocator market-to-market reallocation with an explicit allocator address and native penalty. */ - readonly bluePublicAllocatorReallocate: [ + /** Vault V2 Blue Public Allocator market-to-market reallocation with an explicit allocator address and native penalty. */ + readonly vaultV2BluePublicAllocatorReallocate: [ allocator: Address, vault: Address, deallocateAdapter: Address, @@ -223,8 +223,8 @@ export interface ActionArgs { skipRevert?: boolean, ]; - /** Blue Public Allocator idle-to-market allocation with an explicit allocator address and native penalty. */ - readonly bluePublicAllocatorAllocateFromIdle: [ + /** Vault V2 Blue Public Allocator idle-to-market allocation with an explicit allocator address and native penalty. */ + readonly vaultV2BluePublicAllocatorAllocateFromIdle: [ allocator: Address, vault: Address, adapter: Address, diff --git a/packages/morpho-sdk/src/entities/AGENTS.md b/packages/morpho-sdk/src/entities/AGENTS.md index 051711630..c0c58805c 100644 --- a/packages/morpho-sdk/src/entities/AGENTS.md +++ b/packages/morpho-sdk/src/entities/AGENTS.md @@ -17,4 +17,4 @@ See [`packages/morpho-sdk/AGENTS.md`](../../AGENTS.md) routing summary. `MorphoBlue.borrow()`, `supplyCollateralBorrow()`, `withdraw()`, and `refinance()` accept optional reallocations. The entity validates their state-independent shape before returning requirements, and the pure action repeats the same validation before encoding. `getReallocationData` may fetch the inputs needed to compute reallocations, but action encoding stays outside the entity fetch path. -`ReallocationData` is the entity-level state container for public allocator simulations. Its public maps are readable snapshots for inspection; state transitions stay on its methods and return cloned `ReallocationData` instances. +`VaultV1ReallocationData` is the entity-level state container for PublicAllocator V1 simulations; `ReallocationData` remains its deprecated compatibility alias. `VaultV2ReallocationData` owns the separate BluePublicAllocator state model. Their public maps are readable snapshots for inspection; state transitions stay on their methods and return cloned instances of the same versioned class. diff --git a/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts b/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts index f1fc7869d..d5cc57727 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts @@ -4,7 +4,7 @@ import { describe, expect, test } from "vitest"; import { CbbtcUsdcBlue } from "../../../test/fixtures/blue.js"; import { morphoViemExtension } from "../../client/index.js"; import { ChainIdMismatchError } from "../../types/index.js"; -import { ReallocationData } from "../reallocationData.js"; +import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; describe("MorphoBlue.getReallocations", () => { test("error: ChainIdMismatchError when reallocation data chain differs from market chain", () => { @@ -17,7 +17,9 @@ describe("MorphoBlue.getReallocations", () => { expect(() => market.getReallocations({ - reallocationData: new ReallocationData({ chainId: mainnet.id + 1 }), + reallocationData: new VaultV1ReallocationData({ + chainId: mainnet.id + 1, + }), borrowAmount: 1n, }), ).toThrow(ChainIdMismatchError); diff --git a/packages/morpho-sdk/src/entities/blue/blue.test.ts b/packages/morpho-sdk/src/entities/blue/blue.test.ts index 82db773f0..b9c91925f 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.test.ts @@ -28,7 +28,7 @@ import { NonPositiveInputError, WithdrawExceedsCollateralError, } from "../../types/index.js"; -import { ReallocationData } from "../reallocationData.js"; +import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; const MARKET_PARAMS = new MarketParams(CbbtcUsdcBlue); const USER: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; @@ -675,7 +675,7 @@ describe("MorphoBlue validation", () => { expect( market.getReallocations({ - reallocationData: new ReallocationData({ chainId: mainnet.id }), + reallocationData: new VaultV1ReallocationData({ chainId: mainnet.id }), operation: "borrow", amount: 1n, options: { enabled: false }, diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index a53db18d2..d622c3653 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -36,7 +36,7 @@ import { computeMaxSupplySharePrice, computeMinBorrowSharePrice, computeMinWithdrawSharePrice, - computeReallocations, + computeVaultV1Reallocations, validateAccrualPosition, validateChainId, validateNativeAsset, @@ -88,7 +88,7 @@ import { type VaultV1BlueReallocation, WithdrawExceedsCollateralError, } from "../../types/index.js"; -import { ReallocationData } from "../reallocationData.js"; +import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; export interface BlueActions { /** @@ -467,7 +467,7 @@ export interface BlueActions { }; /** - * Fetches all on-chain data needed to construct a {@link ReallocationData} + * Fetches all on-chain data needed to construct a {@link VaultV1ReallocationData} * for computing vault reallocations via the public allocator. * * The target market is refetched internally at `block.number` so the @@ -484,7 +484,7 @@ export interface BlueActions { * * @param params.vaultAddresses - Addresses of MetaMorpho vaults that allocate to this market. * @param params.block - The block to fetch data at (number and timestamp). - * @returns A ReallocationData instance populated with all required data. + * @returns A VaultV1ReallocationData instance populated with all required data. * @throws {ChainIdMismatchError} when the client chain does not match this market. */ getReallocationData: (params: { @@ -493,7 +493,7 @@ export interface BlueActions { readonly number: bigint; readonly timestamp: bigint; }; - }) => Promise; + }) => Promise; /** * Computes vault reallocations for a borrow or withdraw on this market. @@ -522,7 +522,7 @@ export interface BlueActions { */ getReallocations: ( params: { - reallocationData: ReallocationData; + reallocationData: VaultV1ReallocationData; options?: ReallocationComputeOptions; } & ( | { @@ -1678,7 +1678,7 @@ export class MorphoBlue implements BlueActions { readonly number: bigint; readonly timestamp: bigint; }; - }): Promise { + }): Promise { validateChainId(this.client.viemClient.chain?.id, this.chainId); const client = this.client.viemClient; @@ -1742,7 +1742,7 @@ export class MorphoBlue implements BlueActions { ), ]); - // Assemble records for ReallocationData. + // Assemble records for VaultV1ReallocationData. const marketsRecord: Record = { [targetMarketId]: targetMarket, }; @@ -1771,7 +1771,7 @@ export class MorphoBlue implements BlueActions { (positionsRecord[vault] ??= {})[mid] = position; } - return new ReallocationData({ + return new VaultV1ReallocationData({ chainId: this.chainId, markets: marketsRecord, vaults: vaultsRecord, @@ -1801,7 +1801,7 @@ export class MorphoBlue implements BlueActions { */ getReallocations( params: { - reallocationData: ReallocationData; + reallocationData: VaultV1ReallocationData; options?: ReallocationComputeOptions; } & ( | { @@ -1823,7 +1823,7 @@ export class MorphoBlue implements BlueActions { const options = { enabled: true, ...params.options }; if (params.borrowAmount !== undefined) { - return computeReallocations({ + return computeVaultV1Reallocations({ reallocationData: params.reallocationData, marketId, operation: "borrow", @@ -1832,7 +1832,7 @@ export class MorphoBlue implements BlueActions { }); } - return computeReallocations({ + return computeVaultV1Reallocations({ reallocationData: params.reallocationData, marketId, operation: params.operation, diff --git a/packages/morpho-sdk/src/entities/index.ts b/packages/morpho-sdk/src/entities/index.ts index 1bf8e9e30..4d58432ac 100644 --- a/packages/morpho-sdk/src/entities/index.ts +++ b/packages/morpho-sdk/src/entities/index.ts @@ -67,13 +67,15 @@ export { } from "@morpho-org/blue-sdk"; export { MorphoBlue } from "./blue/index.js"; export * from "./midnight/index.js"; +export { MorphoVaultV1 } from "./vaultV1/index.js"; export { type InputReallocationData, + type InputVaultV1ReallocationData, ReallocationData, -} from "./reallocationData.js"; -export { - type InputReallocationDataVaultV2, - ReallocationDataVaultV2, -} from "./reallocationDataVaultV2.js"; -export { MorphoVaultV1 } from "./vaultV1/index.js"; + VaultV1ReallocationData, +} from "./vaultV1ReallocationData.js"; export { MorphoVaultV2 } from "./vaultV2/index.js"; +export { + type InputVaultV2ReallocationData, + VaultV2ReallocationData, +} from "./vaultV2ReallocationData.js"; diff --git a/packages/morpho-sdk/src/entities/reallocationData.metrics.test.ts b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.metrics.test.ts similarity index 95% rename from packages/morpho-sdk/src/entities/reallocationData.metrics.test.ts rename to packages/morpho-sdk/src/entities/vaultV1ReallocationData.metrics.test.ts index acee98b1f..e582be834 100644 --- a/packages/morpho-sdk/src/entities/reallocationData.metrics.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.metrics.test.ts @@ -8,7 +8,7 @@ import { type PublicReallocation, UnknownReallocationMarketError, } from "../types/index.js"; -import { ReallocationData } from "./reallocationData.js"; +import { VaultV1ReallocationData } from "./vaultV1ReallocationData.js"; // --- Constants --- @@ -37,9 +37,9 @@ function makeMarket(overrides?: { }); } -/** Real ReallocationData holding only the target market. */ +/** Real VaultV1ReallocationData holding only the target market. */ function makeData(targetMarket = makeMarket()) { - return new ReallocationData({ + return new VaultV1ReallocationData({ chainId: 1, markets: { [targetMarket.id]: targetMarket }, }); @@ -51,7 +51,7 @@ function makeData(targetMarket = makeMarket()) { * `reallocationData.test.ts`. The stub mimics the `enabled: false` short-circuit. */ function stubReallocations( - data: ReallocationData, + data: VaultV1ReallocationData, withdrawals: readonly PublicReallocation[], ) { return vi @@ -64,7 +64,7 @@ function stubReallocations( // --------------------------------------------------------------------------- -describe("ReallocationData.getPublicReallocationLiquidity", () => { +describe("VaultV1ReallocationData.getPublicReallocationLiquidity", () => { test("default: sums reallocatable withdrawals", () => { const data = makeData(); stubReallocations(data, [ @@ -103,7 +103,7 @@ describe("ReallocationData.getPublicReallocationLiquidity", () => { }); }); -describe("ReallocationData.getAvailableLiquidityToUtilization", () => { +describe("VaultV1ReallocationData.getAvailableLiquidityToUtilization", () => { test("default: own headroom + scaled available liquidity", () => { // 1000 supply / 500 borrow (50% util). ownHeadroom to 90% = 1000·0.9 − 500 = 400. // supplyTarget set to 90% (not > target) → scaled liquidity 0.9·200 = 180 is diff --git a/packages/morpho-sdk/src/entities/reallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.test.ts similarity index 94% rename from packages/morpho-sdk/src/entities/reallocationData.test.ts rename to packages/morpho-sdk/src/entities/vaultV1ReallocationData.test.ts index 763b23f59..5723315ec 100644 --- a/packages/morpho-sdk/src/entities/reallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.test.ts @@ -26,7 +26,11 @@ import { UnknownReallocationVaultError, UnknownReallocationVaultMarketConfigError, } from "../types/index.js"; -import { ReallocationData } from "./reallocationData.js"; +import { + type InputReallocationData, + ReallocationData, + VaultV1ReallocationData, +} from "./vaultV1ReallocationData.js"; const TIMESTAMP = 1_700_000_000n; const VAULT: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; @@ -239,14 +243,14 @@ type ApplyPublicReallocationParams = { readonly timestamp: bigint; }; -class TestReallocationData extends ReallocationData { +class TestReallocationData extends VaultV1ReallocationData { public applyPublicReallocationForTest(params: ApplyPublicReallocationParams) { return this.applyPublicReallocation(params); } } const applyPublicReallocation = ( - data: ReallocationData, + data: VaultV1ReallocationData, withdrawal: PublicReallocation, ) => new TestReallocationData(data).applyPublicReallocationForTest({ @@ -256,7 +260,16 @@ const applyPublicReallocation = ( timestamp: TIMESTAMP, }); -describe("ReallocationData unit coverage", () => { +describe("VaultV1ReallocationData unit coverage", () => { + test("behavior: preserves the deprecated Vault V1 class and input aliases", () => { + const input = { + chainId: ChainId.EthMainnet, + } satisfies InputReallocationData; + + expect(ReallocationData).toBe(VaultV1ReallocationData); + expect(new ReallocationData(input)).toBeInstanceOf(VaultV1ReallocationData); + }); + test("computeVaultV1Reallocations preserves the deprecated alias behavior", () => { const input = { targetSupply: 1_000n * MathLib.WAD, @@ -264,10 +277,10 @@ describe("ReallocationData unit coverage", () => { sourceSupply: 1_000n * MathLib.WAD, sourceBorrow: 500n * MathLib.WAD, }; - const canonical = new ReallocationData( + const canonical = new VaultV1ReallocationData( makeInput(input), ).computeVaultV1Reallocations(targetParams.id, { timestamp: TIMESTAMP }); - const deprecated = new ReallocationData( + const deprecated = new VaultV1ReallocationData( makeInput(input), ).getMarketPublicReallocations(targetParams.id, { timestamp: TIMESTAMP }); @@ -353,7 +366,7 @@ describe("ReallocationData unit coverage", () => { const position = new Position(positionInput); const vault = new Vault(vaultInput); const vaultMarketConfig = new VaultMarketConfig(vaultMarketConfigInput); - const data = new ReallocationData({ + const data = new VaultV1ReallocationData({ chainId: ChainId.EthMainnet, markets: { [targetParams.id]: market }, vaults: { [VAULT]: vault }, @@ -394,7 +407,7 @@ describe("ReallocationData unit coverage", () => { }); test("returns empty reallocations when disabled without reading missing target market", () => { - const data = new ReallocationData({ chainId: ChainId.EthMainnet }); + const data = new VaultV1ReallocationData({ chainId: ChainId.EthMainnet }); const missingMarket = `0x${"55".repeat(32)}` as MarketId; expect( @@ -403,7 +416,9 @@ describe("ReallocationData unit coverage", () => { }); test("clones inputs and exposes getters without sharing mutable entity instances", () => { - const emptyData = new ReallocationData({ chainId: ChainId.EthMainnet }); + const emptyData = new VaultV1ReallocationData({ + chainId: ChainId.EthMainnet, + }); expect(emptyData.markets).toEqual({}); expect(emptyData.vaults).toEqual({}); expect(emptyData.positions).toEqual({}); @@ -415,7 +430,7 @@ describe("ReallocationData unit coverage", () => { sourceSupply: 1000n * MathLib.WAD, sourceBorrow: 500n * MathLib.WAD, }); - const data = new ReallocationData({ + const data = new VaultV1ReallocationData({ ...input, markets: { ...input.markets, ["0x00" as MarketId]: undefined }, vaults: { ...input.vaults, [zeroAddress]: undefined }, @@ -485,7 +500,7 @@ describe("ReallocationData unit coverage", () => { maxIn: 0n, maxOut: 10n * MathLib.WAD, }); - const data = new ReallocationData(input); + const data = new VaultV1ReallocationData(input); expect( data.getMarketPublicReallocations(targetParams.id, { enabled: false }), @@ -609,7 +624,7 @@ describe("ReallocationData unit coverage", () => { }), }; - const reallocationResult = new ReallocationData( + const reallocationResult = new VaultV1ReallocationData( input, ).getMarketPublicReallocations(targetParams.id, { timestamp: TIMESTAMP, @@ -642,7 +657,7 @@ describe("ReallocationData unit coverage", () => { ).toBe(13n); expect( - new ReallocationData({ + new VaultV1ReallocationData({ ...input, vaultMarketConfigs: { [VAULT]: { @@ -662,7 +677,7 @@ describe("ReallocationData unit coverage", () => { ).toEqual([]); expect( - new ReallocationData({ + new VaultV1ReallocationData({ ...input, vaultMarketConfigs: { [VAULT]: { @@ -689,7 +704,7 @@ describe("ReallocationData unit coverage", () => { ]); expect( - new ReallocationData({ + new VaultV1ReallocationData({ ...input, vaultMarketConfigs: { [VAULT]: { @@ -731,7 +746,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaults: { [VAULT]: makeVault({ withoutPublicAllocatorConfig: true }), @@ -743,7 +758,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaultMarketConfigs: { [VAULT]: { @@ -764,7 +779,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaultMarketConfigs: { [VAULT]: { @@ -785,7 +800,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaultMarketConfigs: { [VAULT]: { @@ -806,7 +821,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaultMarketConfigs: { [VAULT]: { @@ -826,7 +841,7 @@ describe("ReallocationData unit coverage", () => { ).toThrow(DisabledReallocationMarketError); const sameMarketData = new TestReallocationData( - new ReallocationData(baseInput), + new VaultV1ReallocationData(baseInput), ).applyPublicReallocationForTest({ vault: VAULT, supplyMarketId: sourceParams.id, diff --git a/packages/morpho-sdk/src/entities/reallocationData.ts b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts similarity index 96% rename from packages/morpho-sdk/src/entities/reallocationData.ts rename to packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts index 496adaab0..2797a8e96 100644 --- a/packages/morpho-sdk/src/entities/reallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts @@ -32,9 +32,9 @@ import { } from "../types/index.js"; /** - * Input state required to construct {@link ReallocationData}. + * Input state required to construct {@link VaultV1ReallocationData}. */ -export interface InputReallocationData { +export interface InputVaultV1ReallocationData { /** Chain id associated with the fetched state. */ readonly chainId: number; @@ -55,6 +55,13 @@ export interface InputReallocationData { >; } +/** + * Deprecated input name for Vault V1 reallocation data. + * + * @deprecated Use {@link InputVaultV1ReallocationData} instead. + */ +export type InputReallocationData = InputVaultV1ReallocationData; + /** * Clones a market so simulated interest and liquidity changes never mutate caller input. * @@ -110,7 +117,7 @@ const cloneVaultMarketConfig = (config: VaultMarketConfig) => * @remarks * The class owns only the market, vault, position, vault-market-config, * and chain data needed by the shared-liquidity algorithm. Constructor inputs - * are cloned, and simulation steps return cloned `ReallocationData` instances + * are cloned, and simulation steps return cloned `VaultV1ReallocationData` instances * so fetched caller inputs are not mutated. * * Public records are exposed for inspection and snapshotting only. Treat @@ -118,7 +125,7 @@ const cloneVaultMarketConfig = (config: VaultMarketConfig) => * contract keyed by market id or address; use the getters for typed absence * errors and use simulation methods to produce updated state. */ -export class ReallocationData implements InputReallocationData { +export class VaultV1ReallocationData implements InputVaultV1ReallocationData { /** Chain id associated with the fetched reallocation data. */ public readonly chainId: number; @@ -146,7 +153,7 @@ export class ReallocationData implements InputReallocationData { * * @param input - Reallocation input data fetched at a consistent chain state. */ - constructor(input: InputReallocationData) { + constructor(input: InputVaultV1ReallocationData) { const { chainId, markets, vaults, positions, vaultMarketConfigs } = input; this.chainId = chainId; @@ -202,14 +209,14 @@ export class ReallocationData implements InputReallocationData { /** * Creates a deep clone of this reallocation state. * - * @returns A new `ReallocationData` instance with cloned entity objects. + * @returns A new `VaultV1ReallocationData` instance with cloned entity objects. */ public clone() { - return new ReallocationData(this); + return new VaultV1ReallocationData(this); } private forkAliasedState() { - const data = new ReallocationData({ chainId: this.chainId }); + const data = new VaultV1ReallocationData({ chainId: this.chainId }); Object.assign(data.markets, this.markets); Object.assign(data.vaults, this.vaults); @@ -343,7 +350,7 @@ export class ReallocationData implements InputReallocationData { * morphoViemExtension, * type PublicReallocation, * } from "@morpho-org/morpho-sdk"; - * import type { ReallocationData } from "@morpho-org/morpho-sdk/entities"; + * import type { VaultV1ReallocationData } from "@morpho-org/morpho-sdk/entities"; * * const client = createPublicClient({ * chain: mainnet, @@ -360,7 +367,7 @@ export class ReallocationData implements InputReallocationData { * * const result: { * withdrawals: readonly PublicReallocation[]; - * data: ReallocationData; + * data: VaultV1ReallocationData; * } = reallocationData.computeVaultV1Reallocations(marketParams.id, { * timestamp: block.timestamp, * }); @@ -371,7 +378,7 @@ export class ReallocationData implements InputReallocationData { options: PublicAllocatorOptions = {}, ): { readonly withdrawals: readonly PublicReallocation[]; - data: ReallocationData; + data: VaultV1ReallocationData; } { const { enabled = true, @@ -805,3 +812,10 @@ export class ReallocationData implements InputReallocationData { return data; } } + +/** + * Deprecated class name for Vault V1 reallocation data. + * + * @deprecated Use {@link VaultV1ReallocationData} instead. + */ +export { VaultV1ReallocationData as ReallocationData }; diff --git a/packages/morpho-sdk/src/entities/reallocationDataVaultV2.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts similarity index 89% rename from packages/morpho-sdk/src/entities/reallocationDataVaultV2.test.ts rename to packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index 8fdd91c25..9ab53171d 100644 --- a/packages/morpho-sdk/src/entities/reallocationDataVaultV2.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -11,12 +11,12 @@ import type { Address, Hash } from "viem"; import { zeroAddress } from "viem"; import { describe, expect, test } from "vitest"; import { blueBorrow } from "../actions/index.js"; -import { computeReallocationsVaultV2 } from "../helpers/index.js"; +import { computeVaultV2Reallocations } from "../helpers/index.js"; import { InsufficientSharedLiquidityError, ReallocationWithdrawExceedsMarketSupplyError, } from "../types/index.js"; -import { ReallocationDataVaultV2 } from "./reallocationDataVaultV2.js"; +import { VaultV2ReallocationData } from "./vaultV2ReallocationData.js"; const TIMESTAMP = 1_700_000_000n; const ALLOCATOR = "0x0000000000000000000000000000000000000001"; @@ -242,7 +242,7 @@ const makeFixture = ({ ); return { - data: new ReallocationDataVaultV2({ + data: new VaultV2ReallocationData({ chainId: ChainId.EthMainnet, allocator: ALLOCATOR, markets: { @@ -289,7 +289,7 @@ const makeFixture = ({ }; }; -describe("ReallocationDataVaultV2.computeVaultV2Reallocations", () => { +describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { test("default: returns an action-ready market reallocation and cloned post-state", () => { const { data, sourceExpectedAssets, sourceIds, targetIds } = makeFixture(); @@ -479,13 +479,39 @@ describe("ReallocationDataVaultV2.computeVaultV2Reallocations", () => { .reallocations, ).toStrictEqual([]); }); + + test("behavior: ignores vault liquidity above the native penalty threshold", () => { + const { data, sourceExpectedAssets } = makeFixture({ + idle: 300n, + nativePenalty: 8n, + }); + + expect( + data.computeVaultV2Reallocations(targetParams.id, { + maxNativePenalty: 7n, + }).reallocations, + ).toStrictEqual([]); + expect( + data + .computeVaultV2Reallocations(targetParams.id, { + maxNativePenalty: 8n, + }) + .reallocations.map(({ from, assets }) => ({ + from: from.type, + assets, + })), + ).toStrictEqual([ + { from: "market", assets: sourceExpectedAssets }, + { from: "idle", assets: 300n }, + ]); + }); }); -describe("computeReallocationsVaultV2", () => { +describe("computeVaultV2Reallocations", () => { test("default: caps friendly reallocations to the 90% target", () => { const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 90n }); - const reallocations = computeReallocationsVaultV2({ + const reallocations = computeVaultV2Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -504,7 +530,7 @@ describe("computeReallocationsVaultV2", () => { sourceBorrow: 950n, }); - const reallocations = computeReallocationsVaultV2({ + const reallocations = computeVaultV2Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -517,7 +543,7 @@ describe("computeReallocationsVaultV2", () => { test("behavior: plans a loan-asset withdraw", () => { const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 90n }); - const reallocations = computeReallocationsVaultV2({ + const reallocations = computeVaultV2Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "withdraw", @@ -533,7 +559,7 @@ describe("computeReallocationsVaultV2", () => { targetBorrow: 100n, idle: 300n, }); - const reallocations = computeReallocationsVaultV2({ + const reallocations = computeVaultV2Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -557,6 +583,33 @@ describe("computeReallocationsVaultV2", () => { expect(tx.value).toBe(14n); }); + test("behavior: excludes reallocations above the native penalty threshold", () => { + const { data } = makeFixture({ + targetSupply: 100n, + targetBorrow: 90n, + nativePenalty: 7n, + }); + + expect( + computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 1n, + options: { maxNativePenalty: 6n }, + }), + ).toStrictEqual([]); + expect( + computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 1n, + options: { maxNativePenalty: 7n }, + })[0]?.assets, + ).toBe(1n); + }); + test("error: InsufficientSharedLiquidityError rejects a partial plan", () => { const { data } = makeFixture({ targetSupply: 100n, @@ -565,7 +618,7 @@ describe("computeReallocationsVaultV2", () => { }); expect(() => - computeReallocationsVaultV2({ + computeVaultV2Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -578,7 +631,7 @@ describe("computeReallocationsVaultV2", () => { const { data } = makeFixture({ targetSupply: 100n }); expect(() => - computeReallocationsVaultV2({ + computeVaultV2Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "withdraw", @@ -591,7 +644,7 @@ describe("computeReallocationsVaultV2", () => { const { data } = makeFixture(); expect( - computeReallocationsVaultV2({ + computeVaultV2Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -602,7 +655,7 @@ describe("computeReallocationsVaultV2", () => { }); }); -describe("ReallocationDataVaultV2 liquidity metrics", () => { +describe("VaultV2ReallocationData liquidity metrics", () => { test("default: sums idle and market liquidity in target-utilization math", () => { const { data, sourceExpectedAssets } = makeFixture({ targetSupply: 100n, diff --git a/packages/morpho-sdk/src/entities/reallocationDataVaultV2.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts similarity index 95% rename from packages/morpho-sdk/src/entities/reallocationDataVaultV2.ts rename to packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index 850a51276..3abaf6263 100644 --- a/packages/morpho-sdk/src/entities/reallocationDataVaultV2.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -34,7 +34,7 @@ import { } from "../types/index.js"; /** Input state required to simulate Vault V2 BluePublicAllocator reallocations. */ -export interface InputReallocationDataVaultV2 { +export interface InputVaultV2ReallocationData { /** Chain id associated with the fetched state. */ readonly chainId: number; /** Explicit BluePublicAllocator contract used by every returned call. */ @@ -129,12 +129,12 @@ const cloneVault = (vault: AccrualVaultV2) => { * * @example * ```ts - * import { ReallocationDataVaultV2 } from "@morpho-org/morpho-sdk/entities"; + * import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; * - * const data = new ReallocationDataVaultV2(input); + * const data = new VaultV2ReallocationData(input); * ``` */ -export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { +export class VaultV2ReallocationData implements InputVaultV2ReallocationData { /** Chain id associated with this snapshot. */ public readonly chainId: number; /** Explicit BluePublicAllocator address used in returned calls. */ @@ -164,7 +164,7 @@ export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { * * @param input - State fetched at one consistent block. */ - public constructor(input: InputReallocationDataVaultV2) { + public constructor(input: InputVaultV2ReallocationData) { this.chainId = input.chainId; this.allocator = input.allocator; this.markets = {}; @@ -249,7 +249,7 @@ export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { * ``` */ public clone() { - return new ReallocationDataVaultV2(this); + return new VaultV2ReallocationData(this); } /** @@ -373,18 +373,19 @@ export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { * * The algorithm ranks action-ready calls by obtainable assets, includes idle * liquidity, applies each winner to cloned state, and stops when every - * candidate is exhausted. Source markets are held below the SDK's default - * withdrawal-utilization ceiling. + * candidate is exhausted. Vaults whose configured native penalty exceeds + * `options.maxNativePenalty` are ignored. Source markets are held below the + * SDK's default withdrawal-utilization ceiling. * * @param marketId - Target Blue market id. - * @param options - Optional timestamp, enable flag, and vault allowlist. + * @param options - Optional timestamp, enable flag, vault allowlist, and maximum native penalty. * @returns Flat action-ready reallocations and their post-simulation state. * @throws {@link UnknownReallocationMarketError} when the target market is absent. * @example * ```ts - * import { ReallocationDataVaultV2 } from "@morpho-org/morpho-sdk/entities"; + * import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; * - * const data = new ReallocationDataVaultV2(input); + * const data = new VaultV2ReallocationData(input); * const result = data.computeVaultV2Reallocations(targetMarketId, { timestamp }); * ``` */ @@ -404,7 +405,7 @@ export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { * * @param marketId - Target market id. * @param maxWithdrawalUtilization - Source-market utilization ceiling. - * @param options - Discovery options. + * @param options - Discovery options, including the maximum native penalty. * @returns Flat action-ready reallocations and post-simulation state. * @internal */ @@ -418,7 +419,7 @@ export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { readonly options?: PublicAllocatorOptionsVaultV2; }): { readonly reallocations: readonly VaultV2BlueReallocation[]; - readonly data: ReallocationDataVaultV2; + readonly data: VaultV2ReallocationData; } { if (options.enabled === false) return { reallocations: [], data: this }; @@ -446,6 +447,7 @@ export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { vaultAddress: vault, marketId, maxWithdrawalUtilization, + maxNativePenalty: options.maxNativePenalty, }), ) .filter( @@ -470,7 +472,7 @@ export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { * Sums friendly Vault V2 shared liquidity available to a target market. * * @param marketId - Target Blue market id. - * @param options - Optional timestamp, enable flag, and vault allowlist. + * @param options - Optional timestamp, enable flag, vault allowlist, and maximum native penalty. * @returns Reallocatable market and idle assets, or `0n` when none are available. * @throws {@link UnknownReallocationMarketError} when the target market is absent. * @example @@ -494,7 +496,7 @@ export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { * * @param marketId - Target Blue market id. * @param utilization - Desired utilization, scaled by WAD. Defaults to 90%. - * @param options - Optional timestamp, enable flag, and vault allowlist. + * @param options - Optional timestamp, enable flag, vault allowlist, and maximum native penalty. * @returns Borrowable assets while remaining at or below `utilization`. * @throws {@link UnknownReallocationMarketError} when the target market is absent. * @example @@ -559,17 +561,21 @@ export class ReallocationDataVaultV2 implements InputReallocationDataVaultV2 { vaultAddress, marketId, maxWithdrawalUtilization, + maxNativePenalty, }: { readonly vaultAddress: Address; readonly marketId: MarketId; readonly maxWithdrawalUtilization: bigint; + readonly maxNativePenalty?: bigint; }) { return _try(() => { const vault = this.getVault(vaultAddress); const publicAllocatorConfig = this.getPublicAllocatorConfig(vaultAddress); if ( !isAddressEqual(publicAllocatorConfig.allocator, this.allocator) || - !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) + !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) || + (maxNativePenalty != null && + publicAllocatorConfig.nativePenalty > maxNativePenalty) ) return; diff --git a/packages/morpho-sdk/src/helpers/AGENTS.md b/packages/morpho-sdk/src/helpers/AGENTS.md index 36494d266..1ee86af92 100644 --- a/packages/morpho-sdk/src/helpers/AGENTS.md +++ b/packages/morpho-sdk/src/helpers/AGENTS.md @@ -9,7 +9,7 @@ Per-function contracts (arguments, return shapes, behavior) live as JSDoc on eac - **Encoders** (ABI encoding plus input validation, no I/O) — e.g. `encodeForceDeallocateCall(deallocation, onBehalf)`. ABI-encodes a single `VaultV2.forceDeallocate` calldata entry and throws `NonPositiveInputError` on a non-positive `amount`. The `data` field carries ABI-encoded `MarketParams` for the Morpho Market V1 adapter, or empty bytes otherwise. Internal sub-helpers (e.g. `encodeDeallocateData`) are not exported. - **Validators** (pure, throw typed errors) — `validateReallocations(...)`, `validateSlippageTolerance(...)`, `validatePositionHealth(...)`. Each enforces a public-API invariant: see the `error.ts` exports for the full list of error classes a caller may pattern-match on. - **Math / share-price helpers** — `computeMaxRepaySharePrice`, `computeMinBorrowSharePrice`, etc. Use `MAX_SLIPPAGE_TOLERANCE` and cap at `MAX_ABSOLUTE_SHARE_PRICE`. -- **Shared-liquidity** — `computeReallocations` builds PublicAllocator reallocations for a borrow/withdraw (friendly phase respecting withdrawal-utilization targets, then an aggressive 100% fallback). `getSupplyTargetUtilization(marketId, options)` resolves the per-market → default → `DEFAULT_SUPPLY_TARGET_UTILIZATION` supply target (shared by `computeReallocations` and the entity metric). The read-only liquidity metrics live on the `ReallocationData` entity (`getPublicReallocationLiquidity` / `getAvailableLiquidityToUtilization`), not in this layer. +- **Shared-liquidity** — `computeVaultV1Reallocations` builds PublicAllocator V1 reallocations for a borrow/withdraw; `computeReallocations` remains its deprecated compatibility alias. `computeVaultV2Reallocations` plans BluePublicAllocator reallocations and applies the configured native-penalty threshold in both discovery phases. `getSupplyTargetUtilization(marketId, options)` resolves the per-market → default → `DEFAULT_SUPPLY_TARGET_UTILIZATION` supply target for V1. Read-only liquidity metrics live on the corresponding versioned reallocation-data entity, not in this layer. - **Metadata** — `addTransactionMetadata(tx, metadata)` appends hex-encoded analytics bytes to `tx.data`: an optional 4-byte unix timestamp followed by a 4-byte origin (timestamp is omitted when `metadata.timestamp` is falsy). Callers gate on `metadata` being provided; the helper itself is a no-op when `tx.data` is empty. ## Constants diff --git a/packages/morpho-sdk/src/helpers/computeReallocations.test.ts b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.test.ts similarity index 94% rename from packages/morpho-sdk/src/helpers/computeReallocations.test.ts rename to packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.test.ts index 98da754d9..c1a121b73 100644 --- a/packages/morpho-sdk/src/helpers/computeReallocations.test.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.test.ts @@ -12,13 +12,16 @@ import { WethUsdsBlue, WstethUsdcSourceMarket, } from "../../test/fixtures/blue.js"; -import type { ReallocationData } from "../entities/reallocationData.js"; +import type { VaultV1ReallocationData } from "../entities/vaultV1ReallocationData.js"; import { InsufficientSharedLiquidityError, MissingPublicAllocatorConfigError, ReallocationWithdrawExceedsMarketSupplyError, } from "../types/index.js"; -import { computeReallocations } from "./computeReallocations.js"; +import { + computeReallocations, + computeVaultV1Reallocations, +} from "./computeVaultV1Reallocations.js"; // --- Constants --- @@ -84,9 +87,9 @@ interface MockStateParams { } /** - * Creates a minimal mock ReallocationData. + * Creates a minimal mock VaultV1ReallocationData. * - * Only implements the methods computeReallocations actually calls: + * Only implements the methods computeVaultV1Reallocations actually calls: * `getMarket`, `computeVaultV1Reallocations`, and `getVault`. */ function makeMockState({ @@ -96,7 +99,7 @@ function makeMockState({ aggressiveWithdrawals = [], vaultFees = {}, extraMarkets = [], -}: MockStateParams = {}): ReallocationData { +}: MockStateParams = {}): VaultV1ReallocationData { const markets = new Map(); markets.set(tm.id, tm); markets.set(sourceA.id, sourceA); @@ -111,7 +114,7 @@ function makeMockState({ : markets.get(id)!, computeVaultV1Reallocations: () => ({ withdrawals: [...aggressiveWithdrawals], - data: {} as ReallocationData, + data: {} as VaultV1ReallocationData, }), }; @@ -130,18 +133,22 @@ function makeMockState({ ? { admin: vault, fee: vaultFees[vault]!, accruedFee: 0n } : undefined, }), - } as unknown as ReallocationData; + } as unknown as VaultV1ReallocationData; } // --------------------------------------------------------------------------- // Early returns // --------------------------------------------------------------------------- -describe("computeReallocations", () => { +describe("computeVaultV1Reallocations", () => { + test("behavior: preserves the deprecated planner alias", () => { + expect(computeReallocations).toBe(computeVaultV1Reallocations); + }); + describe("early returns", () => { test("should return empty when enabled is false", () => { - const result = computeReallocations({ - reallocationData: {} as ReallocationData, + const result = computeVaultV1Reallocations({ + reallocationData: {} as VaultV1ReallocationData, marketId: targetParams.id, operation: "borrow", amount: MathLib.WAD, @@ -153,7 +160,7 @@ describe("computeReallocations", () => { test("should return empty when post-borrow utilization is below supply target", () => { const data = makeMockState(); // Borrow 1 WAD: utilization ≈ 501/1000 = 50.1% < 90.5% - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -182,7 +189,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 1000n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -218,7 +225,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 1000n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -256,7 +263,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -293,7 +300,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -345,7 +352,7 @@ describe("computeReallocations", () => { ], }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -390,7 +397,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 500n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -436,7 +443,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -481,7 +488,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 1000n, [VAULT_B]: 2000n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -520,7 +527,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 1000n, [VAULT_B]: 2000n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -554,7 +561,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 1000n, [VAULT_B]: 2000n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -607,7 +614,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -640,7 +647,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -684,7 +691,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -701,7 +708,7 @@ describe("computeReallocations", () => { }); test("should return empty when required assets is non-positive", () => { - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: makeMockState(), marketId: targetParams.id, operation: "borrow", @@ -730,7 +737,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -762,7 +769,7 @@ describe("computeReallocations", () => { // Set target utilization very high (99%) — requires less reallocation. const highTarget = (99n * MathLib.WAD) / 100n; - const resultHigh = computeReallocations({ + const resultHigh = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -775,7 +782,7 @@ describe("computeReallocations", () => { // Set target utilization low (50%) — requires more reallocation. const lowTarget = MathLib.WAD / 2n; - const resultLow = computeReallocations({ + const resultLow = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -818,7 +825,7 @@ describe("computeReallocations", () => { }); try { - computeReallocations({ + computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -861,7 +868,7 @@ describe("computeReallocations", () => { }); try { - computeReallocations({ + computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -893,7 +900,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -923,7 +930,7 @@ describe("computeReallocations", () => { }); expect(() => - computeReallocations({ + computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "borrow", @@ -942,7 +949,7 @@ describe("computeReallocations", () => { test("should return empty when post-withdraw utilization is below supply target", () => { // Default market: S=1000, B=500. Withdraw 100 → S'=900, util = 500/900 ≈ 55.5% < 90.5%. const data = makeMockState(); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "withdraw", @@ -970,7 +977,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "withdraw", @@ -1009,7 +1016,7 @@ describe("computeReallocations", () => { vaultFees: { [VAULT_A]: 0n }, }); - const result = computeReallocations({ + const result = computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "withdraw", @@ -1054,7 +1061,7 @@ describe("computeReallocations", () => { }); expect(() => - computeReallocations({ + computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "withdraw", @@ -1072,7 +1079,7 @@ describe("computeReallocations", () => { const withdrawAmount = parseEther("1001"); try { - computeReallocations({ + computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "withdraw", @@ -1099,7 +1106,7 @@ describe("computeReallocations", () => { }); const data = makeMockState({ targetMarket: tm }); expect(() => - computeReallocations({ + computeVaultV1Reallocations({ reallocationData: data, marketId: targetParams.id, operation: "withdraw", diff --git a/packages/morpho-sdk/src/helpers/computeReallocations.ts b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts similarity index 95% rename from packages/morpho-sdk/src/helpers/computeReallocations.ts rename to packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts index 09245cbf6..543c4d335 100644 --- a/packages/morpho-sdk/src/helpers/computeReallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts @@ -1,6 +1,6 @@ import { type MarketId, MarketUtils, MathLib } from "@morpho-org/blue-sdk"; import type { Address } from "viem"; -import type { ReallocationData } from "../entities/reallocationData.js"; +import type { VaultV1ReallocationData } from "../entities/vaultV1ReallocationData.js"; import { InsufficientSharedLiquidityError, MissingPublicAllocatorConfigError, @@ -113,7 +113,7 @@ const capVaultWithdrawals = ( * import { mainnet } from "viem/chains"; * import { markets, vaults } from "@morpho-org/morpho-test"; * import { - * computeReallocations, + * computeVaultV1Reallocations, * morphoViemExtension, * } from "@morpho-org/morpho-sdk"; * @@ -131,7 +131,7 @@ const capVaultWithdrawals = ( * block: { number: block.number, timestamp: block.timestamp }, * }); * const borrowAmount = parseUnits("1000", 6); - * const reallocations = computeReallocations({ + * const reallocations = computeVaultV1Reallocations({ * reallocationData, * marketId: marketParams.id, * operation: "borrow", @@ -148,14 +148,14 @@ const capVaultWithdrawals = ( * // borrow.buildTx() includes any required PublicAllocator reallocations. * ``` */ -export const computeReallocations = ({ +export const computeVaultV1Reallocations = ({ reallocationData: data, marketId, operation, amount, options, }: { - readonly reallocationData: ReallocationData; + readonly reallocationData: VaultV1ReallocationData; readonly marketId: MarketId; readonly operation: "borrow" | "withdraw"; readonly amount: bigint; @@ -163,7 +163,7 @@ export const computeReallocations = ({ }): readonly VaultV1BlueReallocation[] => { if (options?.enabled === false) return []; - // ReallocationData does not retain the fetch block; pass that block timestamp + // VaultV1ReallocationData does not retain the fetch block; pass that block timestamp // to compute against the same accrued state, otherwise Market defaults to lastUpdate. const market = data.getMarket(marketId).accrueInterest(options?.timestamp); @@ -305,3 +305,10 @@ export const computeReallocations = ({ })), })); }; + +/** + * Deprecated name for the Vault V1 amount-aware reallocation planner. + * + * @deprecated Use {@link computeVaultV1Reallocations} instead. + */ +export const computeReallocations = computeVaultV1Reallocations; diff --git a/packages/morpho-sdk/src/helpers/computeReallocationsVaultV2.ts b/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts similarity index 91% rename from packages/morpho-sdk/src/helpers/computeReallocationsVaultV2.ts rename to packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts index 450e23263..4d7ae6fce 100644 --- a/packages/morpho-sdk/src/helpers/computeReallocationsVaultV2.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts @@ -1,5 +1,5 @@ import { type MarketId, MarketUtils, MathLib } from "@morpho-org/blue-sdk"; -import type { ReallocationDataVaultV2 } from "../entities/reallocationDataVaultV2.js"; +import type { VaultV2ReallocationData } from "../entities/vaultV2ReallocationData.js"; import { InsufficientSharedLiquidityError, type ReallocationComputeOptionsVaultV2, @@ -21,15 +21,15 @@ import { DEFAULT_SUPPLY_TARGET_UTILIZATION } from "./constant.js"; * @param params.marketId - Target Blue market id. * @param params.operation - Operation driving the reallocation. * @param params.amount - Borrow or withdraw amount. - * @param params.options - Optional timestamp, enable flag, and vault allowlist. + * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum native penalty. * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. * @throws {@link InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. * @throws {@link ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. * @example * ```ts - * import { computeReallocationsVaultV2 } from "@morpho-org/morpho-sdk"; + * import { computeVaultV2Reallocations } from "@morpho-org/morpho-sdk"; * - * const reallocations = computeReallocationsVaultV2({ + * const reallocations = computeVaultV2Reallocations({ * reallocationData, * marketId, * operation: "borrow", @@ -38,14 +38,14 @@ import { DEFAULT_SUPPLY_TARGET_UTILIZATION } from "./constant.js"; * }); * ``` */ -export const computeReallocationsVaultV2 = ({ +export const computeVaultV2Reallocations = ({ reallocationData: data, marketId, operation, amount, options, }: { - readonly reallocationData: ReallocationDataVaultV2; + readonly reallocationData: VaultV2ReallocationData; readonly marketId: MarketId; readonly operation: "borrow" | "withdraw"; readonly amount: bigint; diff --git a/packages/morpho-sdk/src/helpers/index.ts b/packages/morpho-sdk/src/helpers/index.ts index 558e4fe04..07001fbda 100644 --- a/packages/morpho-sdk/src/helpers/index.ts +++ b/packages/morpho-sdk/src/helpers/index.ts @@ -1,5 +1,8 @@ -export { computeReallocations } from "./computeReallocations.js"; -export { computeReallocationsVaultV2 } from "./computeReallocationsVaultV2.js"; +export { + computeReallocations, + computeVaultV1Reallocations, +} from "./computeVaultV1Reallocations.js"; +export { computeVaultV2Reallocations } from "./computeVaultV2Reallocations.js"; export { APPROVE_ONLY_ONCE_TOKENS, DEFAULT_LLTV_BUFFER, diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 7a2334b0a..6f933f56d 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -966,7 +966,7 @@ export class DisabledReallocationMarketError extends Error { } /** - * Thrown when shared liquidity selected by `computeReallocations` cannot cover + * Thrown when shared liquidity selected by `computeVaultV1Reallocations` cannot cover * the operation's absolute shortfall on the target market — the resulting * `morphoBorrow` or `morphoWithdraw` would still revert onchain. * @@ -1361,7 +1361,7 @@ export class WithdrawSharesExceedSupplyError extends Error { } /** - * Thrown when `computeReallocations` is called with a withdraw `amount` greater + * Thrown when `computeVaultV1Reallocations` is called with a withdraw `amount` greater * than the target market's current `totalSupplyAssets` — the post-withdraw * supply would be negative, making the on-chain `morphoWithdraw` revert * regardless of any reallocation. Caught here so callers do not pay diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 74f6cbc98..f368ace98 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -54,6 +54,12 @@ export interface PublicAllocatorOptionsVaultV2 { /** Vault V2 addresses to consider. Defaults to every vault in the reallocation data. */ readonly reallocatableVaults?: readonly Address[]; + + /** + * Maximum native-token penalty accepted for each BluePublicAllocator call. + * Vaults with a higher configured penalty are ignored. Defaults to no limit. + */ + readonly maxNativePenalty?: bigint; } /** diff --git a/packages/morpho-sdk/src/utils.ts b/packages/morpho-sdk/src/utils.ts index b566fd639..53bc46a56 100644 --- a/packages/morpho-sdk/src/utils.ts +++ b/packages/morpho-sdk/src/utils.ts @@ -64,8 +64,11 @@ export { transformValue, values, } from "@morpho-org/morpho-ts"; -export { computeReallocations } from "./helpers/computeReallocations.js"; -export { computeReallocationsVaultV2 } from "./helpers/computeReallocationsVaultV2.js"; +export { + computeReallocations, + computeVaultV1Reallocations, +} from "./helpers/computeVaultV1Reallocations.js"; +export { computeVaultV2Reallocations } from "./helpers/computeVaultV2Reallocations.js"; export { addTransactionMetadata } from "./helpers/metadata.js"; export { computeMaxRepaySharePrice, diff --git a/packages/morpho-sdk/test/reallocationData/publicAllocator.test.ts b/packages/morpho-sdk/test/reallocationData/publicAllocator.test.ts index b30042be9..4afe18cb4 100644 --- a/packages/morpho-sdk/test/reallocationData/publicAllocator.test.ts +++ b/packages/morpho-sdk/test/reallocationData/publicAllocator.test.ts @@ -12,9 +12,9 @@ import { import { type Address, parseEther, parseUnits, zeroAddress } from "viem"; import { describe, expect, test } from "vitest"; import { - type InputReallocationData, - ReallocationData, -} from "../../src/entities/reallocationData.js"; + type InputVaultV1ReallocationData, + VaultV1ReallocationData, +} from "../../src/entities/vaultV1ReallocationData.js"; const timestamp = 12345n; @@ -199,7 +199,7 @@ const makeConfig = ({ }); const makeFixture = () => - new ReallocationData({ + new VaultV1ReallocationData({ chainId: ChainId.EthMainnet, markets: { [marketA1.id]: marketA1, @@ -287,12 +287,12 @@ const makeFixture = () => }), }, }, - } satisfies InputReallocationData); + } satisfies InputVaultV1ReallocationData); -const liquidity = (data: ReallocationData, marketId: MarketId) => +const liquidity = (data: VaultV1ReallocationData, marketId: MarketId) => data.getMarket(marketId).liquidity; -describe("ReallocationData public allocator integration", () => { +describe("VaultV1ReallocationData public allocator integration", () => { test.each([ { marketId: marketA1.id, @@ -385,7 +385,7 @@ describe("ReallocationData public allocator integration", () => { fee: 0n, price: parseUnits("3", 18), }); - const fixture = new ReallocationData({ + const fixture = new VaultV1ReallocationData({ chainId: ChainId.EthMainnet, markets: { [idleMarket.id]: idleMarket, diff --git a/packages/morpho-ts/src/abis.ts b/packages/morpho-ts/src/abis.ts index ffe0479b7..537742e60 100644 --- a/packages/morpho-ts/src/abis.ts +++ b/packages/morpho-ts/src/abis.ts @@ -4595,8 +4595,8 @@ export const publicAllocatorAbi = [ }, ] as const; -/** Blue Public Allocator ABI used for market and idle reallocations. */ -export const bluePublicAllocatorAbi = [ +/** Vault V2 Blue Public Allocator ABI used for market and idle reallocations. */ +export const vaultV2BluePublicAllocatorAbi = [ { inputs: [ { From 50a225eab93e3ce781bba5158ae568885213c781 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 7 Aug 2026 10:56:14 +0200 Subject: [PATCH 08/41] fix: centralize market params ABI and preserve TIBs --- .../pr-review-engine/agents/documentation.md | 6 ++- .changeset/brave-vaults-reallocate.md | 2 +- AGENTS.md | 1 + ...-19-marketv1-supply-withdraw-loan-asset.md | 16 +++---- ...red-liquidity-target-utilization-metric.md | 42 ++++++++-------- packages/blue-sdk/AGENTS.md | 1 + packages/blue-sdk/package.json | 2 +- .../blue-sdk/src/market/MarketParams.test.ts | 5 ++ packages/blue-sdk/src/market/MarketParams.ts | 12 +---- packages/morpho-ts/AGENTS.md | 1 + packages/morpho-ts/src/abis.ts | 48 +++++++------------ 11 files changed, 62 insertions(+), 74 deletions(-) diff --git a/.agents/pr-review-engine/agents/documentation.md b/.agents/pr-review-engine/agents/documentation.md index 475eaed3c..52d85d578 100644 --- a/.agents/pr-review-engine/agents/documentation.md +++ b/.agents/pr-review-engine/agents/documentation.md @@ -11,6 +11,7 @@ focus: | 2. Markdown documentation accuracy across the repo (README, AGENTS.md, MISSION.md, docs/**, .agents/**). 3. Pointer / link integrity for every internal reference touched by the diff. 4. AGENTS.md ↔ persona backlink consistency. + 5. Immutability of implemented TIBs already present on the target branch. canonical-rules: docs/jsdoc-style.md --- @@ -45,8 +46,11 @@ Files in scope (read each one whose content is in the diff OR which references s - `.agents/pr-review-engine/SKILL.md`, `.agents/pr-review-engine/agents/*.md`, `.agents/pr-review-engine/references/*.md`, `.agents/commands/*.md`. - Any `*.md` colocated with a package (`packages//*.md`). +Implemented TIBs already present on the target branch are exempt from current-code freshness and pointer-renaming checks below. Their implementation-time symbols, examples, and paths are historical evidence, even when the current code has moved on. + For each Markdown file affected, flag: +- **Implemented TIB rewrites.** A TIB already present on the target branch is a historical implementation-time record: do not update its prose, examples, symbols, or paths to follow later code. Changed decisions require a new superseding TIB; operational clarifications belong in a dated addendum. A TIB introduced for the current implementation may still be updated before it lands. - **Stale prose.** A statement that no longer matches the code after the diff — e.g. README documents a function that was removed/renamed; AGENTS.md lists a rule the code change just violated; an example that no longer compiles. - **Out-of-sync inventories.** A file enumerating personas, packages, slash commands, scripts, supported chains, etc. that no longer matches reality after the diff. E.g. a README that lists "supported chains: mainnet, base" while the diff just added arbitrum. - **Cross-doc consistency.** When the diff changes a rule in `AGENTS.md`, every persona that enforces it (per the backlink `> Applied by personas: …`) should reflect the new rule. When the diff renames a section heading in `AGENTS.md`, every doc that references that section by title needs an update. @@ -59,7 +63,7 @@ For every Markdown link, path reference, or symbol pointer in the changed files - **Internal Markdown links must resolve.** `[label](./path/to/file.md)` — the path must exist. Anchors `#section-name` must match a heading in the target file (slugified — GitHub's convention). - **Path references in prose must resolve.** Lines like `Reference \`docs/jsdoc-style.md\`` or `Read \`.agents/pr-review-engine/agents/web3-security.md\`` are pointers; the file must exist. - **Frontmatter references must resolve.** Persona frontmatter (`applies:`, `trigger:`, `canonical-rules:`, `out-of-scope:` mentions) must reference real `AGENTS.md` sections, real flag names from `.agents/pr-review-engine/SKILL.md` Step 4, and real file paths. -- **Renames cascade.** If the diff renames or moves a file (detect via `git diff --name-status --find-renames`), every reference to the old path in any tracked Markdown / persona / skill / command file must be updated. Grep for the old basename in the repo and surface unresolved hits. +- **Renames cascade.** If the diff renames or moves a file (detect via `git diff --name-status --find-renames`), every reference to the old path in current Markdown / persona / skill / command files must be updated. Grep for the old basename in the repo and surface unresolved hits, excluding implemented TIBs already present on the target branch. - **Removed exports / removed files.** If the diff removes a public export or a file, grep the repo for references and flag any that survive. ## 4. AGENTS.md ↔ persona backlink consistency diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 97afd4575..3f0e09b3e 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -6,4 +6,4 @@ "@morpho-org/liquidity-sdk-viem": patch --- -Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`; add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`; add explicit-allocator deployless and fallback reads to `blue-sdk-viem`; and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases and migrate `liquidity-sdk-viem` to the canonical V1 state name. +Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases and migrate `liquidity-sdk-viem` to the canonical V1 state name. diff --git a/AGENTS.md b/AGENTS.md index 9eb7a3002..587bef625 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -156,6 +156,7 @@ A scannable list of patterns reviewers reject. Most are review-only today (per t - `@throws` for each typed error class an integrator may pattern-match on. - One `@example` block with realistic working code: imports, client setup, the call, expected return. - **AI-legibility is first-class.** Identical signatures across V1/V2 where protocols overlap. Discriminated unions with obvious `type` tags. Deterministic outputs verifiable byte-for-byte. Error messages read like instructions an agent can act on without guessing. Protocol-specific terms (`LLTV buffer`, `wNative`, `GeneralAdapter1`, `bundler3`, `PublicAllocator`, `MetaMorpho`, `Permit2`, `WAD`) live in the [`packages/morpho-sdk/AGENTS.md`](./packages/morpho-sdk/AGENTS.md) glossary. +- **Implemented TIBs are historical records.** Do not rewrite a TIB already present on the target branch to follow later code, symbol, or path changes. Keep the TIB's implementation-time names and examples intact. A changed decision gets a new superseding TIB; an operational clarification gets a dated addendum. Only a TIB introduced for the current implementation may be kept in sync with that implementation before it lands. - **TypeDoc-generated reference** published per release. - **Feedback loop:** if the same question is asked twice, the answer goes into the relevant `AGENTS.md` or JSDoc on the export it concerns. diff --git a/docs/tibs/TIB-2026-05-19-marketv1-supply-withdraw-loan-asset.md b/docs/tibs/TIB-2026-05-19-marketv1-supply-withdraw-loan-asset.md index fc1895e24..268e555db 100644 --- a/docs/tibs/TIB-2026-05-19-marketv1-supply-withdraw-loan-asset.md +++ b/docs/tibs/TIB-2026-05-19-marketv1-supply-withdraw-loan-asset.md @@ -16,7 +16,7 @@ Two consequences: - Liquidity providers cannot participate in a Morpho market through the SDK without leaving the typed surface (no `Transaction`, no `getRequirements`, no PublicAllocator reallocation help). -- Suppliers who hit on-market illiquidity on a withdraw cannot reuse the SDK's shared-liquidity machinery (`getReallocationData` / `getReallocations` / `computeVaultV1Reallocations`) — that machinery is hard-coded to borrow semantics today. +- Suppliers who hit on-market illiquidity on a withdraw cannot reuse the SDK's shared-liquidity machinery (`getReallocationData` / `getReallocations` / `computeReallocations`) — that machinery is hard-coded to borrow semantics today. This TIB freezes the design decision for the missing pair before the implementation lands. @@ -28,7 +28,7 @@ This TIB freezes the design decision for the missing pair before the implementat - Route both through bundler3 / `GeneralAdapter1` (`morphoSupply` / `morphoWithdraw`) so they compose with the rest of the bundle action set. - Support **native ETH wrapping** on supply when the loan token is the chain's wNative — same contract as `marketV1SupplyCollateral`. - Support **optional PublicAllocator reallocations** on withdraw, so a withdraw whose amount exceeds on-market liquidity can succeed by first pulling liquidity from other markets of the same loan asset. -- Reuse `computeVaultV1Reallocations` for the withdraw direction (single source of truth), not a fork of the helper. +- Reuse `computeReallocations` for the withdraw direction (single source of truth), not a fork of the helper. - Maintain 100% JSDoc and tests (unit colocated + fork e2e) on the new surface in the same PR. **Non-Goals** @@ -68,10 +68,10 @@ A withdraw with native unwrap is **out of scope** for this PR. It requires routi PublicAllocator reallocations apply identically: they prepend `reallocateTo(vault, fee, withdrawals[], targetMarketParams)` actions before `morphoWithdraw`, and their fees accumulate in `tx.value`. `validateReallocations(target=withdrawMarketId)` is reused as-is — sort, no-target-market, non-empty, non-negative fee, strictly-ascending market IDs. -The shared-liquidity planner `computeVaultV1Reallocations` gains an `operation: "borrow" | "withdraw"` discriminator. The signature becomes: +The shared-liquidity planner `computeReallocations` gains an `operation: "borrow" | "withdraw"` discriminator. The signature becomes: ```ts -computeVaultV1Reallocations({ +computeReallocations({ reallocationData, marketId, operation: "borrow" | "withdraw", @@ -135,7 +135,7 @@ Messages follow the canonical `" . ."` sha - **Phase 1 — Types + errors.** Extend `src/types/action.ts` (action interfaces + `AssetsOrSharesArgs`) and `src/types/error.ts`. Unblocks barrel re-exports for the rest of the work. - **Phase 2 — Helpers.** Add `computeMaxSupplySharePrice` and `computeMinWithdrawSharePrice` in `src/helpers/slippage.ts`; `validateWithdrawAmount`, `validateWithdrawShares`, and the unified `validateNativeAsset` in `src/helpers/validate.ts`. Unit tests colocated. -- **Phase 3 — `computeVaultV1Reallocations` extension.** Add the `operation` discriminator; update the borrow caller to pass `"borrow"`; cover the withdraw branch with new tests. +- **Phase 3 — `computeReallocations` extension.** Add the `operation` discriminator; update the borrow caller to pass `"borrow"`; cover the withdraw branch with new tests. - **Phase 4 — Action builders.** `src/actions/marketV1/supply.ts` and `src/actions/marketV1/withdraw.ts` + colocated unit tests + barrel update. - **Phase 5 — Entity wiring.** Two new methods on `MorphoMarketV1` (`supply`, `withdraw`); generalize `getReallocations` to take `{ amount, operation }`. - **Phase 6 — Fork tests.** Anvil mainnet at the pinned block; reuse `CbbtcUsdcMarketV1`, `SteakhouseUsdcVaultV1`, `WbtcUsdcSourceMarket`, `WstethUsdcSourceMarket` from existing fixtures. Cover happy paths, modes, native, permit2, reallocation single/multi/fee, `InsufficientSharedLiquidityError`, missing `setAuthorization`. @@ -143,7 +143,7 @@ Messages follow the canonical `" . ."` sha ## Considered Alternatives -### Alternative 1: Fork `computeVaultV1Reallocations` into a withdraw-specific helper +### Alternative 1: Fork `computeReallocations` into a withdraw-specific helper Add `computeWithdrawReallocations` alongside the existing function. @@ -178,7 +178,7 @@ Bundle the native-unwrap path with `withdraw` to ship a complete native story. - **Slippage is bounded on both sides.** Supply uses `maxSharePriceE27 = (assets / shares) × (WAD + slippage)` (upper bound, RAY-scaled), so a malicious actor inflating the share price via a donation between transaction construction and execution cannot dilute the supplier. Withdraw uses `minSharePriceE27 = (assets / shares) × (WAD − slippage)` (lower bound), capping the loss to slippage tolerance. Both helpers cap at `MAX_ABSOLUTE_SHARE_PRICE` like the existing repay helper. - **Authorization is enforced.** `withdraw` requires `setAuthorization(generalAdapter1, true)` on Morpho. `getRequirements` returns the typed authorization tx so integrators send it before the bundle; if they don't, the bundle reverts on the Morpho-side auth check. -- **Reallocation fees are paid only when they can actually unblock the withdraw.** `computeVaultV1Reallocations` continues to throw `InsufficientSharedLiquidityError` when the aggregate reallocatable liquidity strictly under-covers the absolute shortfall — preventing the user from paying ETH fees to the PublicAllocator on a withdraw that would still revert. +- **Reallocation fees are paid only when they can actually unblock the withdraw.** `computeReallocations` continues to throw `InsufficientSharedLiquidityError` when the aggregate reallocatable liquidity strictly under-covers the absolute shortfall — preventing the user from paying ETH fees to the PublicAllocator on a withdraw that would still revert. - **`validateReallocations` is reused unchanged.** Strict-ascending market IDs, no withdrawal on the target market, non-empty withdrawals, non-negative fee. - **Input validation runs before any encoding.** Every error is a named class (`Error` subclass) that integrators can pattern-match on; messages never leak raw `Error` strings from upstream. @@ -194,7 +194,7 @@ Bundle the native-unwrap path with `withdraw` to ship a complete native story. - `packages/morpho-sdk/src/actions/marketV1/borrow.ts` — closest existing template (slippage + reallocation). - `packages/morpho-sdk/src/actions/marketV1/repay.ts` — assets/shares mode reference. - `packages/morpho-sdk/src/actions/marketV1/supplyCollateral.ts` — native wrap reference. -- `packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts` — extended in Phase 3. +- `packages/morpho-sdk/src/helpers/computeReallocations.ts` — extended in Phase 3. - `packages/morpho-sdk/src/helpers/slippage.ts` — extended in Phase 2. - [`Morpho.sol`](https://github.com/morpho-org/morpho-blue/blob/main/src/Morpho.sol) — `supply` / `withdraw` reference. - [`GeneralAdapter1.sol`](https://github.com/morpho-org/bundler3/blob/main/src/adapters/GeneralAdapter1.sol) — `morphoSupply` / `morphoWithdraw` reference. diff --git a/docs/tibs/TIB-2026-06-16-shared-liquidity-target-utilization-metric.md b/docs/tibs/TIB-2026-06-16-shared-liquidity-target-utilization-metric.md index 575ea837b..2c2ac12b4 100644 --- a/docs/tibs/TIB-2026-06-16-shared-liquidity-target-utilization-metric.md +++ b/docs/tibs/TIB-2026-06-16-shared-liquidity-target-utilization-metric.md @@ -13,34 +13,34 @@ Integrators (frontends, allocators, risk dashboards) repeatedly ask one question about a Morpho Blue market: **"how much can still be borrowed here before it gets unhealthy, counting liquidity the PublicAllocator could pull in from sibling markets?"** -`computeVaultV1Reallocations` already answers a *transactional* variant of this — given a concrete borrow/withdraw `amount`, it builds the `reallocateTo` calls and **throws** when liquidity is insufficient. That shape is wrong for a display metric: +`computeReallocations` already answers a *transactional* variant of this — given a concrete borrow/withdraw `amount`, it builds the `reallocateTo` calls and **throws** when liquidity is insufficient. That shape is wrong for a display metric: - It needs an `amount` the caller is trying to find in the first place. - It throws on insufficiency, so callers must wrap it in try/catch just to read a number. - It returns calldata, not a quantity. -This TIB freezes the design of two read-only metrics that answer the question directly, exposed as methods on the `VaultV1ReallocationData` entity the caller already holds. +This TIB freezes the design of two read-only metrics that answer the question directly, exposed as methods on the `ReallocationData` entity the caller already holds. ## Goals / Non-Goals **Goals** -- Add `VaultV1ReallocationData.getPublicReallocationLiquidity(marketId, options?)`: the total liquidity the PublicAllocator can reallocate **into** a market from sibling markets — a `bigint` that never throws on insufficiency (returns `0n`). It still throws `UnknownReallocationMarketError` when the target market is absent. -- Add `VaultV1ReallocationData.getAvailableLiquidityToTargetUtilization(marketId, targetUtilization?, options?)`: the liquidity available to bring a market to a target utilization — the max borrow keeping post-borrow utilization at or below the target on the **post-reallocation** supply (`getBorrowToUtilization({ supply + L, borrow }, targetUtilization)`) — also never throws on insufficiency (same absent-market exception). +- Add `ReallocationData.getPublicReallocationLiquidity(marketId, options?)`: the total liquidity the PublicAllocator can reallocate **into** a market from sibling markets — a `bigint` that never throws on insufficiency (returns `0n`). It still throws `UnknownReallocationMarketError` when the target market is absent. +- Add `ReallocationData.getAvailableLiquidityToTargetUtilization(marketId, targetUtilization?, options?)`: the liquidity available to bring a market to a target utilization — the max borrow keeping post-borrow utilization at or below the target on the **post-reallocation** supply (`getBorrowToUtilization({ supply + L, borrow }, targetUtilization)`) — also never throws on insufficiency (same absent-market exception). - Reuse the existing PublicAllocator discovery (`getMarketPublicReallocations`) — no fork of the reallocation algorithm. -- Share the supply-target-utilization resolution with `computeVaultV1Reallocations` instead of duplicating it. +- Share the supply-target-utilization resolution with `computeReallocations` instead of duplicating it. **Non-Goals** -- No calldata. These metrics never produce a transaction; `computeVaultV1Reallocations` remains the builder. -- No mutation of `computeVaultV1Reallocations`' behavior or its public options. (An earlier `maintainSupplyTargetUtilization` opt-in explored for this was dropped — see Considered Alternatives.) +- No calldata. These metrics never produce a transaction; `computeReallocations` remains the builder. +- No mutation of `computeReallocations`' behavior or its public options. (An earlier `maintainSupplyTargetUtilization` opt-in explored for this was dropped — see Considered Alternatives.) - No modeling of the borrowed amount as new supply. Borrowing raises `totalBorrowAssets` but not `totalSupplyAssets`, so the metric measures borrow `x` against the post-reallocation supply `S + L` (`(B + x) / (S + L) ≤ targetUtilization`) — see Assumptions. ## Proposed Solution -### Placement: methods on `VaultV1ReallocationData`, not standalone helpers +### Placement: methods on `ReallocationData`, not standalone helpers -Both metrics only read from a `VaultV1ReallocationData` instance (`getMarket`, `getMarketPublicReallocations`) and return a derived `bigint`. They live as **methods on the entity** the caller already obtains from `MorphoBlue.getReallocationData(...)`, next to the `getMarketPublicReallocations` they wrap. They stay pure (no I/O, no mutation), consistent with the entity layer's "compute derived values" role. They are intentionally **not** `MorphoBlue` methods (no chainId/fetch coupling) and **not** free helpers (they belong with the state they read). +Both metrics only read from a `ReallocationData` instance (`getMarket`, `getMarketPublicReallocations`) and return a derived `bigint`. They live as **methods on the entity** the caller already obtains from `MorphoBlue.getReallocationData(...)`, next to the `getMarketPublicReallocations` they wrap. They stay pure (no I/O, no mutation), consistent with the entity layer's "compute derived values" role. They are intentionally **not** `MorphoBlue` methods (no chainId/fetch coupling) and **not** free helpers (they belong with the state they read). ### `getPublicReallocationLiquidity` @@ -66,16 +66,16 @@ Two facts about the metric's meaning: 1. **Below the reallocation trigger, only own liquidity counts.** The PublicAllocator only reallocates once a market crosses its `supplyTargetUtilization`. If the caller asks for a target *below* that trigger, no reallocation would happen, so the answer is the market's own borrow headroom alone. 2. **Otherwise, borrow-to-target on the post-reallocation supply.** Reallocated supply `L` is added to the market's supply, so the borrowable amount is `getBorrowToUtilization({ supply + L, borrow }, targetUtilization)`. Below the target this equals own headroom + `targetUtilization · L` — `L` only contributes its scaled share, since it also raises the denominator. At or above the target, `zeroFloorSub` clamps to `0n` when `L` is too small to bring utilization back under the target; borrowing more would only push it further over. (The earlier `targetUtilization === market.utilization` special case is subsumed: there `ownHeadroom` is `0` and the formula returns `targetUtilization · L`.) -> Unlike the transactional `computeVaultV1Reallocations` fallback, this metric never relaxes the target market toward 100% and never force-drains source markets: it honours whatever source withdrawal cap the caller configures (friendly by default). +> Unlike the transactional `computeReallocations` fallback, this metric never relaxes the target market toward 100% and never force-drains source markets: it honours whatever source withdrawal cap the caller configures (friendly by default). ### Shared resolution helper -`computeVaultV1Reallocations` and `getAvailableLiquidityToTargetUtilization` both need the effective supply-target utilization for a market (per-market override → default override → `DEFAULT_SUPPLY_TARGET_UTILIZATION`). That resolution is factored into one helper, `getSupplyTargetUtilization(marketId, options)`, instead of being duplicated. +`computeReallocations` and `getAvailableLiquidityToTargetUtilization` both need the effective supply-target utilization for a market (per-market override → default override → `DEFAULT_SUPPLY_TARGET_UTILIZATION`). That resolution is factored into one helper, `getSupplyTargetUtilization(marketId, options)`, instead of being duplicated. ### Implementation ```ts -// VaultV1ReallocationData.getAvailableLiquidityToTargetUtilization +// ReallocationData.getAvailableLiquidityToTargetUtilization const market = this.getMarket(marketId).accrueInterest(options?.timestamp); const supplyTargetUtilization = getSupplyTargetUtilization(marketId, options); @@ -96,17 +96,17 @@ return MarketUtils.getBorrowToUtilization( // rul ## Considered Alternatives -### Alternative 1: Add an opt-in flag to `computeVaultV1Reallocations` +### Alternative 1: Add an opt-in flag to `computeReallocations` -The first iteration threaded a `maintainSupplyTargetUtilization` boolean through `ReallocationComputeOptions` and `computeVaultV1Reallocations` (holding the target market at its supply target instead of relaxing it to 100% in the aggressive phase). +The first iteration threaded a `maintainSupplyTargetUtilization` boolean through `ReallocationComputeOptions` and `computeReallocations` (holding the target market at its supply target instead of relaxing it to 100% in the aggressive phase). -**Why rejected:** The metric takes no borrow amount and never builds calldata, so it shares almost nothing with `computeVaultV1Reallocations`'s control flow. Bolting it on widened the builder's option surface and its phase-2 branch for a read-only concern. A read-only metric on `VaultV1ReallocationData` is smaller, purer, and easier to test. The flag was fully reverted. +**Why rejected:** The metric takes no borrow amount and never builds calldata, so it shares almost nothing with `computeReallocations`'s control flow. Bolting it on widened the builder's option surface and its phase-2 branch for a read-only concern. A read-only metric on `ReallocationData` is smaller, purer, and easier to test. The flag was fully reverted. ### Alternative 2: Standalone helpers in the `helpers/` layer The metrics were first shipped as free `compute*` helpers (`computeAvailableSharedLiquidity`, `computeAvailableLiquidityToTargetUtilization`) re-exported from the package root. -**Why rejected (review feedback):** they only operate on a `VaultV1ReallocationData` instance and wrap its `getMarketPublicReallocations`, so they read more naturally as methods on that class (next to the data they consume) than as helpers that take the entity as an argument. Moving them also keeps the public helper surface minimal. +**Why rejected (review feedback):** they only operate on a `ReallocationData` instance and wrap its `getMarketPublicReallocations`, so they read more naturally as methods on that class (next to the data they consume) than as helpers that take the entity as an argument. Moving them also keeps the public helper surface minimal. ### Alternative 3: Force-drain sources to 100% utilization @@ -123,14 +123,14 @@ Add the full reallocatable liquidity `L` to the own headroom without scaling. ## Assumptions & Constraints - **Exact for Morpho borrow semantics.** The return is `getBorrowToUtilization({ supply + L, borrow }, targetUtilization)` = `zeroFloorSub(wMulDown(supply + L, targetUtilization), borrow)`, the max borrow `x` keeping post-borrow utilization `(borrow + x) / (supply + L)` at or below the target. Borrowing raises `totalBorrowAssets` only, not `totalSupplyAssets`, so `x` is not in the denominator; an above-target market clamps to `0n` when reallocation cannot bring it back under. Below the target this equals own headroom + `targetUtilization · L` (to within 1 wei of fixed-point flooring). -- **Pass `options.timestamp` from the fetch block.** Accrual otherwise falls back to the target market's `lastUpdate`, which can diverge from the source rows' fetch block — same constraint as `computeVaultV1Reallocations`. -- Pure entity methods, no I/O, no mutation. Additive public surface (two new `VaultV1ReallocationData` methods + one internal helper). Semver: **minor**. +- **Pass `options.timestamp` from the fetch block.** Accrual otherwise falls back to the target market's `lastUpdate`, which can diverge from the source rows' fetch block — same constraint as `computeReallocations`. +- Pure entity methods, no I/O, no mutation. Additive public surface (two new `ReallocationData` methods + one internal helper). Semver: **minor**. - `viem` stays the only peer dep. No new runtime dependencies. ## References -- `packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts` — `getPublicReallocationLiquidity`, `getAvailableLiquidityToTargetUtilization`, and the `getMarketPublicReallocations` discovery they reuse. -- `packages/morpho-sdk/src/helpers/utilization.ts` — `getSupplyTargetUtilization`, shared with `computeVaultV1Reallocations`. -- `packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts` — the transactional counterpart (builds calldata, throws on insufficiency). +- `packages/morpho-sdk/src/entities/reallocationData.ts` — `getPublicReallocationLiquidity`, `getAvailableLiquidityToTargetUtilization`, and the `getMarketPublicReallocations` discovery they reuse. +- `packages/morpho-sdk/src/helpers/utilization.ts` — `getSupplyTargetUtilization`, shared with `computeReallocations`. +- `packages/morpho-sdk/src/helpers/computeReallocations.ts` — the transactional counterpart (builds calldata, throws on insufficiency). - `DEFAULT_SUPPLY_TARGET_UTILIZATION` (90.5%) / `DEFAULT_WITHDRAWAL_TARGET_UTILIZATION` (92%) in `src/helpers/constant.ts`. - Root [`AGENTS.md`](../../AGENTS.md) §1 (entity layer / purity), §3 (types), §5 (testing), §6 (JSDoc). diff --git a/packages/blue-sdk/AGENTS.md b/packages/blue-sdk/AGENTS.md index bd0aa56f4..c88389b1c 100644 --- a/packages/blue-sdk/AGENTS.md +++ b/packages/blue-sdk/AGENTS.md @@ -12,6 +12,7 @@ - Getters may throw typed `Unknown*Error`; nullable lookup paths should use `_try` or `tryGet*`-style helpers deliberately. - Vault V2 absolute/relative allocation-cap math is canonical in `VaultV2Utils.allocationHeadroom`; consumers such as `AccrualVaultV2.maxDeposit` and shared-liquidity simulation delegate to it. - Vault V2 BluePublicAllocator config interfaces are readonly identity-bearing projections: they include the explicit allocator and vault, plus the adapter and derived market-params id for pair-scoped state. +- `marketParamsAbi` is owned by `@morpho-org/morpho-ts/abis` and re-exported from `MarketParams.ts` for backward compatibility; do not define a second copy in this package. ## Continuous Improvement diff --git a/packages/blue-sdk/package.json b/packages/blue-sdk/package.json index 75b17b981..98469d224 100644 --- a/packages/blue-sdk/package.json +++ b/packages/blue-sdk/package.json @@ -33,7 +33,7 @@ "@noble/hashes": "^2.2.0" }, "peerDependencies": { - "@morpho-org/morpho-ts": "^2.7.0" + "@morpho-org/morpho-ts": "^2.9.0" }, "devDependencies": { "@morpho-org/morpho-ts": "workspace:^", diff --git a/packages/blue-sdk/src/market/MarketParams.test.ts b/packages/blue-sdk/src/market/MarketParams.test.ts index 0f738c907..c1e7fbdbd 100644 --- a/packages/blue-sdk/src/market/MarketParams.test.ts +++ b/packages/blue-sdk/src/market/MarketParams.test.ts @@ -1,3 +1,4 @@ +import { marketParamsAbi as canonicalMarketParamsAbi } from "@morpho-org/morpho-ts/abis"; import { encodeAbiParameters } from "viem"; import { describe, expect, test } from "vitest"; import { LOAN_TOKEN, marketParams } from "../__test__/fixtures.js"; @@ -9,6 +10,10 @@ import type { MarketId } from "../types.js"; import { MarketParams, marketParamsAbi } from "./MarketParams.js"; describe("MarketParams", () => { + test("re-exports the canonical market params ABI", () => { + expect(marketParamsAbi).toBe(canonicalMarketParamsAbi); + }); + test("get returns the cached params by id", () => { const params = new MarketParams(marketParams()); diff --git a/packages/blue-sdk/src/market/MarketParams.ts b/packages/blue-sdk/src/market/MarketParams.ts index 665a017c0..51f144e55 100644 --- a/packages/blue-sdk/src/market/MarketParams.ts +++ b/packages/blue-sdk/src/market/MarketParams.ts @@ -1,4 +1,5 @@ import { ZERO_ADDRESS } from "@morpho-org/morpho-ts"; +import { marketParamsAbi } from "@morpho-org/morpho-ts/abis"; import { decodeAbiParameters, type Hex } from "viem"; import { InvalidMarketParamsError, @@ -23,16 +24,7 @@ export type InputMarketParams = Pick< >; /** ABI tuple definition for Morpho Blue market params. */ -export const marketParamsAbi = { - type: "tuple", - components: [ - { type: "address", name: "loanToken" }, - { type: "address", name: "collateralToken" }, - { type: "address", name: "oracle" }, - { type: "address", name: "irm" }, - { type: "uint256", name: "lltv" }, - ], -} as const; +export { marketParamsAbi }; /** * Represents a market's configuration (also called market params). diff --git a/packages/morpho-ts/AGENTS.md b/packages/morpho-ts/AGENTS.md index a861ff506..03cf5a8c0 100644 --- a/packages/morpho-ts/AGENTS.md +++ b/packages/morpho-ts/AGENTS.md @@ -1,6 +1,7 @@ # morpho-ts Conventions - Keep this package framework-free and dependency-light; export generic helpers plus cross-protocol SDK primitives that Blue and Midnight both need, including shared math, typed errors, constants, address/hex/call descriptor types, shared ABI literals, and address/deployment registries. +- `marketParamsAbi` is canonically defined in this package's `abis` subpath; protocol packages may re-export it for compatibility but must not redefine it. - Preserve nullability through helpers, e.g. `transformValue(value, fn)` returns nullish input unchanged. - Helpers should preserve input type shape unless their name explicitly signals formatting or conversion. - Use type guards for filtering, e.g. `array.filter(isDefined)`. diff --git a/packages/morpho-ts/src/abis.ts b/packages/morpho-ts/src/abis.ts index 537742e60..943c1deac 100644 --- a/packages/morpho-ts/src/abis.ts +++ b/packages/morpho-ts/src/abis.ts @@ -4038,33 +4038,17 @@ export const metaMorphoAbi = [ }, ] as const; -const morphoBlueMarketParamsAbiComponents = [ - { - internalType: "address", - name: "loanToken", - type: "address", - }, - { - internalType: "address", - name: "collateralToken", - type: "address", - }, - { - internalType: "address", - name: "oracle", - type: "address", - }, - { - internalType: "address", - name: "irm", - type: "address", - }, - { - internalType: "uint256", - name: "lltv", - type: "uint256", - }, -] as const; +/** ABI tuple definition for Morpho Blue market params. */ +export const marketParamsAbi = { + type: "tuple", + components: [ + { type: "address", name: "loanToken" }, + { type: "address", name: "collateralToken" }, + { type: "address", name: "oracle" }, + { type: "address", name: "irm" }, + { type: "uint256", name: "lltv" }, + ], +} as const; /** PublicAllocator ABI used to read vault allocator configuration and flow caps. */ export const publicAllocatorAbi = [ @@ -4470,7 +4454,7 @@ export const publicAllocatorAbi = [ { components: [ { - components: morphoBlueMarketParamsAbiComponents, + components: marketParamsAbi.components, internalType: "struct MarketParams", name: "marketParams", type: "tuple", @@ -4486,7 +4470,7 @@ export const publicAllocatorAbi = [ type: "tuple[]", }, { - components: morphoBlueMarketParamsAbiComponents, + components: marketParamsAbi.components, internalType: "struct MarketParams", name: "supplyMarketParams", type: "tuple", @@ -4711,7 +4695,7 @@ export const vaultV2BluePublicAllocatorAbi = [ type: "address", }, { - components: morphoBlueMarketParamsAbiComponents, + components: marketParamsAbi.components, internalType: "struct MarketParams", name: "deallocateMarketParams", type: "tuple", @@ -4722,7 +4706,7 @@ export const vaultV2BluePublicAllocatorAbi = [ type: "address", }, { - components: morphoBlueMarketParamsAbiComponents, + components: marketParamsAbi.components, internalType: "struct MarketParams", name: "allocateMarketParams", type: "tuple", @@ -4751,7 +4735,7 @@ export const vaultV2BluePublicAllocatorAbi = [ type: "address", }, { - components: morphoBlueMarketParamsAbiComponents, + components: marketParamsAbi.components, internalType: "struct MarketParams", name: "marketParams", type: "tuple", From f3cc8b383147b64dba5dd2ba2abb7add82788c10 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 7 Aug 2026 11:12:19 +0200 Subject: [PATCH 09/41] feat: add Vault V2 liquidity loader --- .changeset/brave-vaults-reallocate.md | 5 +- packages/liquidity-sdk-viem/AGENTS.md | 5 +- packages/liquidity-sdk-viem/README.md | 63 ++--- packages/liquidity-sdk-viem/package.json | 6 +- packages/liquidity-sdk-viem/src/index.ts | 1 + .../src/vaultV2LiquidityLoader.test.ts | 219 ++++++++++++++++++ .../src/vaultV2LiquidityLoader.ts | 219 ++++++++++++++++++ .../src/index.ts | 3 + .../src/morpho-protocol-evm.test.ts | 37 ++- .../src/morpho-protocol-evm.ts | 6 +- 10 files changed, 523 insertions(+), 41 deletions(-) create mode 100644 packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts create mode 100644 packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 3f0e09b3e..cf567f0cd 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -3,7 +3,8 @@ "@morpho-org/blue-sdk": minor "@morpho-org/blue-sdk-viem": minor "@morpho-org/morpho-sdk": minor -"@morpho-org/liquidity-sdk-viem": patch +"@morpho-org/liquidity-sdk-viem": minor +"@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases and migrate `liquidity-sdk-viem` to the canonical V1 state name. +Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases, add an independent `VaultV2LiquidityLoader` alongside the existing Vault V1 loader, raise its `blue-sdk-viem` and `morpho-sdk` peer floors to the introducing minors, and allow the WDK borrow flow to accept the combined V1/V2 reallocation union. diff --git a/packages/liquidity-sdk-viem/AGENTS.md b/packages/liquidity-sdk-viem/AGENTS.md index be782a201..c2a367d42 100644 --- a/packages/liquidity-sdk-viem/AGENTS.md +++ b/packages/liquidity-sdk-viem/AGENTS.md @@ -2,14 +2,15 @@ - GraphQL queries live in `graphql/*.gql`; regenerate API types with this package's `codegen` script. - Do not hand-edit generated `src/api/sdk.ts`; update queries or `codegen.ts` instead. -- `loader.ts` is the package core; keep public liquidity planning behavior there. +- `loader.ts` owns Vault V1 planning and `vaultV2LiquidityLoader.ts` owns the independent Vault V2 planner. Keep their state fetching and simulation flows separate; do not use inheritance or a shared mutable state model across allocator versions. - Loader code batches by market ID through `DataLoader`. - Snapshot all onchain state at one block before simulation, e.g. pass `{ blockNumber: block.number }`. - Convert API maps through `fromEntries` and filter with `isDefined`. - Public liquidity options use WAD-scaled `bigint` thresholds. - `apiSdk` is a singleton `GraphQLClient` bound to `BLUE_API_GRAPHQL_URL`. - Batch expensive market requests by chunking IDs before paginating. -- Keep loader output deterministic: return `withdrawals`, `startState`, `endState`, and utilization. +- Keep loader output deterministic: return version-appropriate `withdrawals` or `reallocations`, `startState`, `endState`, and utilization. +- Vault V2 has no canonical BluePublicAllocator registry entry. Its loader takes the allocator and participating Vault V2 addresses explicitly and never infers them from chain configuration. ## Continuous Improvement diff --git a/packages/liquidity-sdk-viem/README.md b/packages/liquidity-sdk-viem/README.md index e4fe12e36..515514e4a 100644 --- a/packages/liquidity-sdk-viem/README.md +++ b/packages/liquidity-sdk-viem/README.md @@ -23,7 +23,7 @@ ## Overview -Viem-based package that provides utilities to build viem-based liquidity bots on Morpho and examples using Flashbots and Morpho's GraphQL API. +Viem-based loaders for computing shared liquidity from PublicAllocator V1 and the Vault V2 BluePublicAllocator. ## Installation @@ -37,45 +37,48 @@ yarn add @morpho-org/liquidity-sdk-viem ## Usage -### Fetch from API or RPC +### Vault V1 ```typescript +import type { MarketId } from "@morpho-org/blue-sdk"; import { LiquidityLoader } from "@morpho-org/liquidity-sdk-viem"; +import { createPublicClient, http } from "viem"; +import { mainnet } from "viem/chains"; -const loader = new LiquidityLoader( - client // viem client. -); - -const [withdrawals1, withdrawals2] = await Promise.all([ - loader.fetch( - "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc" as MarketId, - "api" - ), - loader.fetch( - "0xe475337d11be1db07f7c5a156e511f05d1844308e66e17d2ba5da0839d3b34d9" as MarketId, - "rpc" - ), -]); -``` +const client = createPublicClient({ chain: mainnet, transport: http() }); +const loader = new LiquidityLoader(client); +const marketId = + "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc" as MarketId; -### Fetch only from API +const { withdrawals, startState, endState, targetBorrowUtilization } = + await loader.fetch(marketId); +``` -```typescript -import { ChainId } from "@morpho-org/blue-sdk"; -import { LiquidityLoader } from "@morpho-org/liquidity-sdk-viem"; +`LiquidityLoader` discovers PublicAllocator V1 vaults through the Morpho API, snapshots their state through the viem client, and returns source-market withdrawals. -const loader = new LiquidityLoader({ chainId: ChainId.EthMainnet }); +### Vault V2 -const [withdrawals1, withdrawals2] = await Promise.all([ - loader.fetch( - "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc" as MarketId - ), - loader.fetch( - "0xe475337d11be1db07f7c5a156e511f05d1844308e66e17d2ba5da0839d3b34d9" as MarketId - ), -]); +```typescript +import type { MarketId } from "@morpho-org/blue-sdk"; +import { VaultV2LiquidityLoader } from "@morpho-org/liquidity-sdk-viem"; +import { createPublicClient, http } from "viem"; +import { mainnet } from "viem/chains"; + +const client = createPublicClient({ chain: mainnet, transport: http() }); +const loader = new VaultV2LiquidityLoader(client, { + allocator: "0x0000000000000000000000000000000000000001", + vaults: ["0x0000000000000000000000000000000000000002"], + maxNativePenalty: 1_000_000_000_000_000n, +}); +const marketId = + "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc" as MarketId; + +const { reallocations, startState, endState, targetBorrowUtilization } = + await loader.fetch(marketId); ``` +`VaultV2LiquidityLoader` is a separate RPC-only loader. The BluePublicAllocator and participating Vault V2 addresses are explicit because the protocol has no canonical allocator registry entry. Its `reallocations` can be passed directly to Morpho SDK Blue borrow and withdraw actions. + ## Development Contribute from the monorepo root. See [CONTRIBUTING.md](../../CONTRIBUTING.md) for setup, checks, and package workflow. Report vulnerabilities through [SECURITY.md](../../SECURITY.md). diff --git a/packages/liquidity-sdk-viem/package.json b/packages/liquidity-sdk-viem/package.json index e00521965..eb7065a66 100644 --- a/packages/liquidity-sdk-viem/package.json +++ b/packages/liquidity-sdk-viem/package.json @@ -1,6 +1,6 @@ { "name": "@morpho-org/liquidity-sdk-viem", - "description": "Viem-based package that helps seamlessly calculate the liquidity available through the PublicAllocator.", + "description": "Viem-based package that calculates shared liquidity through PublicAllocator V1 and the Vault V2 BluePublicAllocator.", "version": "4.1.1", "author": "Morpho Association ", "contributors": [ @@ -30,8 +30,8 @@ }, "peerDependencies": { "@morpho-org/blue-sdk": "^6.0.0", - "@morpho-org/blue-sdk-viem": "^5.0.0", - "@morpho-org/morpho-sdk": "^5.4.0", + "@morpho-org/blue-sdk-viem": "^5.3.0", + "@morpho-org/morpho-sdk": "^5.5.0", "@morpho-org/morpho-ts": "^2.7.0", "dataloader": "^2.2.3", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", diff --git a/packages/liquidity-sdk-viem/src/index.ts b/packages/liquidity-sdk-viem/src/index.ts index dd8245678..c5088a05e 100644 --- a/packages/liquidity-sdk-viem/src/index.ts +++ b/packages/liquidity-sdk-viem/src/index.ts @@ -1 +1,2 @@ export * from "./loader.js"; +export * from "./vaultV2LiquidityLoader.js"; diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts new file mode 100644 index 000000000..7d291dffb --- /dev/null +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts @@ -0,0 +1,219 @@ +import { + AccrualVaultV2, + AccrualVaultV2MorphoMarketV1AdapterV2, + Market, + MarketParams, + MathLib, +} from "@morpho-org/blue-sdk"; +import { createMockClient } from "@morpho-org/test/mock"; +import { type Address, type Hex, toHex, zeroAddress, zeroHash } from "viem"; +import { mainnet } from "viem/chains"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +const { + fetchAccrualVaultV2Mock, + fetchMarketMock, + fetchVaultV2PublicAllocatorDataMock, +} = vi.hoisted(() => ({ + fetchAccrualVaultV2Mock: vi.fn(), + fetchMarketMock: vi.fn(), + fetchVaultV2PublicAllocatorDataMock: vi.fn(), +})); + +vi.mock("@morpho-org/blue-sdk-viem", () => ({ + fetchAccrualVaultV2: fetchAccrualVaultV2Mock, + fetchMarket: fetchMarketMock, + fetchVaultV2PublicAllocatorData: fetchVaultV2PublicAllocatorDataMock, +})); + +const { VaultV2LiquidityLoader } = await import("./vaultV2LiquidityLoader.js"); + +const BLOCK_NUMBER = 10n; +const BLOCK_TIMESTAMP = 1_700_000_000n; +const ALLOCATOR: Address = "0x0000000000000000000000000000000000000001"; +const VAULT: Address = "0x0000000000000000000000000000000000000002"; +const ADAPTER: Address = "0x0000000000000000000000000000000000000003"; +const ASSET: Address = "0x0000000000000000000000000000000000000004"; +const IRM: Address = "0x0000000000000000000000000000000000000005"; + +const marketParams = new MarketParams({ + loanToken: ASSET, + collateralToken: "0x0000000000000000000000000000000000000006", + oracle: zeroAddress, + irm: IRM, + lltv: 860_000_000_000_000_000n, +}); +const market = new Market({ + params: marketParams, + totalSupplyAssets: 100n, + totalBorrowAssets: 95n, + totalSupplyShares: 100_000_000n, + totalBorrowShares: 95_000_000n, + lastUpdate: BLOCK_TIMESTAMP, + fee: 0n, +}); +const adapter = new AccrualVaultV2MorphoMarketV1AdapterV2( + { + address: ADAPTER, + parentVault: VAULT, + skimRecipient: zeroAddress, + marketIds: [market.id], + adaptiveCurveIrm: IRM, + supplyShares: { [market.id]: 0n }, + }, + [market], +); +const vault = new AccrualVaultV2( + { + address: VAULT, + asset: ASSET, + _totalAssets: 100n, + totalSupply: 100n, + virtualShares: 0n, + maxRate: 0n, + lastUpdate: BLOCK_TIMESTAMP, + liquidityAdapter: zeroAddress, + liquidityData: "0x", + liquidityAllocations: undefined, + performanceFee: 0n, + managementFee: 0n, + performanceFeeRecipient: zeroAddress, + managementFeeRecipient: zeroAddress, + }, + undefined, + [adapter], + 100n, + {}, +); +const ids = adapter.ids(market.params); +const allocatorData = { + publicAllocatorConfig: { + allocator: ALLOCATOR, + vault: VAULT, + canAllocateFromIdle: true, + nativePenalty: 12n, + }, + marketPublicAllocatorConfigs: { + [ids[2]]: { + allocator: ALLOCATOR, + vault: VAULT, + adapter: ADAPTER, + marketParamsId: ids[2], + absoluteCap: 1_000n, + canDeallocate: false, + isActiveAdapter: true, + }, + }, + allocations: Object.fromEntries( + ids.map((id) => [ + id, + { + id, + absoluteCap: 1_000n, + relativeCap: MathLib.WAD, + allocation: 0n, + }, + ]), + ), +}; + +const rpcBlock = () => ({ + baseFeePerGas: toHex(0n), + difficulty: toHex(0n), + extraData: "0x", + gasLimit: toHex(30_000_000n), + gasUsed: toHex(0n), + hash: zeroHash, + logsBloom: `0x${"00".repeat(256)}` as Hex, + miner: zeroAddress, + mixHash: zeroHash, + nonce: "0x0000000000000000", + number: toHex(BLOCK_NUMBER), + parentHash: zeroHash, + receiptsRoot: zeroHash, + sha3Uncles: zeroHash, + size: toHex(0n), + stateRoot: zeroHash, + timestamp: toHex(BLOCK_TIMESTAMP), + totalDifficulty: toHex(0n), + transactions: [], + transactionsRoot: zeroHash, + uncles: [], +}); + +const setup = () => { + const handle = createMockClient(mainnet); + handle.request.mockImplementation(async ({ method }) => { + if (method === "eth_getBlockByNumber") return rpcBlock(); + if (method === "eth_chainId") return toHex(mainnet.id); + throw new Error(`Unexpected RPC method: ${method}`); + }); + fetchMarketMock.mockResolvedValue(market); + fetchAccrualVaultV2Mock.mockResolvedValue(vault); + fetchVaultV2PublicAllocatorDataMock.mockResolvedValue(allocatorData); + return handle; +}; + +describe("VaultV2LiquidityLoader", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("default: returns action-ready Vault V2 reallocations", async () => { + const { client } = setup(); + const loader = new VaultV2LiquidityLoader(client, { + allocator: ALLOCATOR, + vaults: [VAULT], + }); + + const result = await loader.fetch(market.id); + + expect(result.reallocations).toStrictEqual([ + { + allocator: ALLOCATOR, + type: "bluePublicAllocator", + vault: VAULT, + from: { type: "idle" }, + to: { adapter: ADAPTER }, + assets: 100n, + nativePenalty: 12n, + }, + ]); + expect(result.endState.getMarket(market.id).totalSupplyAssets).toBe(200n); + expect(result.targetBorrowUtilization).toBe(900_000_000_000_000_000n); + expect(fetchMarketMock).toHaveBeenCalledWith(market.id, client, { + blockNumber: BLOCK_NUMBER, + chainId: mainnet.id, + }); + expect(fetchAccrualVaultV2Mock).toHaveBeenCalledWith(VAULT, client, { + blockNumber: BLOCK_NUMBER, + chainId: mainnet.id, + deployless: undefined, + }); + expect(fetchVaultV2PublicAllocatorDataMock).toHaveBeenCalledWith( + ALLOCATOR, + vault, + client, + { blockNumber: BLOCK_NUMBER, deployless: undefined }, + ); + }); + + test("behavior: filters vaults above the maximum native penalty", async () => { + const { client } = setup(); + const loader = new VaultV2LiquidityLoader(client, { + allocator: ALLOCATOR, + vaults: [VAULT], + maxNativePenalty: 11n, + deployless: "force", + }); + + await expect(loader.fetch(market.id)).resolves.toMatchObject({ + reallocations: [], + }); + expect(fetchAccrualVaultV2Mock).toHaveBeenCalledWith(VAULT, client, { + blockNumber: BLOCK_NUMBER, + chainId: mainnet.id, + deployless: "force", + }); + }); +}); diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts new file mode 100644 index 000000000..f8b8875e5 --- /dev/null +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts @@ -0,0 +1,219 @@ +import type { MarketId } from "@morpho-org/blue-sdk"; +import { + fetchAccrualVaultV2, + fetchMarket, + fetchVaultV2PublicAllocatorData, +} from "@morpho-org/blue-sdk-viem"; +import { + DEFAULT_SUPPLY_TARGET_UTILIZATION, + type VaultV2BlueReallocation, +} from "@morpho-org/morpho-sdk"; +import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; +import { fromEntries } from "@morpho-org/morpho-ts"; +import DataLoader from "dataloader"; +import type { Address, Chain, Client, Transport } from "viem"; +import { getBlock } from "viem/actions"; + +const REALLOCATION_SIMULATION_DELAY = 3_600n; + +/** Represents the configuration for fetching and simulating Vault V2 shared liquidity. */ +export interface VaultV2LiquidityParameters { + /** Explicit BluePublicAllocator contract used for every generated reallocation. */ + readonly allocator: Address; + + /** Vault V2 addresses whose reallocatable liquidity should be considered. */ + readonly vaults: readonly Address[]; + + /** Maximum native-token penalty accepted per BluePublicAllocator call. */ + readonly maxNativePenalty?: bigint; + + /** Deployless read mode forwarded to Vault V2 fetchers. Defaults to `true` with direct-read fallback. */ + readonly deployless?: boolean | "force"; +} + +/** Represents a Vault V2 shared-liquidity plan built from one consistent block snapshot. */ +export interface VaultV2LiquidityResult { + /** Vault V2 state before applying the computed reallocations. */ + readonly startState: VaultV2ReallocationData; + + /** Vault V2 state after applying the computed reallocations. */ + readonly endState: VaultV2ReallocationData; + + /** Flat action-ready BluePublicAllocator calls, in execution order. */ + readonly reallocations: readonly VaultV2BlueReallocation[]; + + /** Fixed target utilization used by the Vault V2 planner, scaled by WAD. */ + readonly targetBorrowUtilization: bigint; +} + +/** + * Represents a loader that fetches and simulates Vault V2 BluePublicAllocator shared liquidity. + * + * This class is independent from the Vault V1 `LiquidityLoader`: it + * discovers no allocator or vault addresses through the API and only consumes + * the explicit Vault V2 configuration supplied by the caller. + * + * @example + * ```ts + * import type { MarketId } from "@morpho-org/blue-sdk"; + * import { + * type VaultV2LiquidityResult, + * VaultV2LiquidityLoader, + * } from "@morpho-org/liquidity-sdk-viem"; + * import { type Address, createPublicClient, http } from "viem"; + * import { mainnet } from "viem/chains"; + * + * export async function loadVaultV2Liquidity( + * allocator: Address, + * vault: Address, + * marketId: MarketId, + * ): Promise { + * const client = createPublicClient({ chain: mainnet, transport: http() }); + * const loader = new VaultV2LiquidityLoader(client, { + * allocator, + * vaults: [vault], + * maxNativePenalty: 1_000_000_000_000_000n, + * }); + * return loader.fetch(marketId); + * } + * ``` + */ +export class VaultV2LiquidityLoader { + protected readonly dataLoader: DataLoader; + + /** + * Creates a Vault V2 shared-liquidity loader. + * + * @param client - Viem client used to snapshot onchain state. + * @param parameters - Explicit allocator, participating vaults, and optional fetch/planning limits. + */ + public constructor( + public readonly client: Client, + public readonly parameters: VaultV2LiquidityParameters, + ) { + this.dataLoader = new DataLoader( + async (marketIds) => { + const { client: loaderClient, parameters: loaderParameters } = this; + const block = await getBlock(loaderClient); + const fetchParameters = { + blockNumber: block.number, + deployless: loaderParameters.deployless, + } as const; + + const [markets, vaults] = await Promise.all([ + Promise.all( + marketIds.map((marketId) => + fetchMarket(marketId, loaderClient, { + blockNumber: block.number, + chainId: loaderClient.chain.id, + }), + ), + ), + Promise.all( + loaderParameters.vaults.map((vault) => + fetchAccrualVaultV2(vault, loaderClient, { + ...fetchParameters, + chainId: loaderClient.chain.id, + }), + ), + ), + ]); + const publicAllocatorData = await Promise.all( + vaults.map((vault) => + fetchVaultV2PublicAllocatorData( + loaderParameters.allocator, + vault, + loaderClient, + fetchParameters, + ), + ), + ); + const startState = new VaultV2ReallocationData({ + chainId: loaderClient.chain.id, + allocator: loaderParameters.allocator, + markets: fromEntries( + markets.map((market) => [market.id, market] as const), + ), + vaults: fromEntries( + vaults.map((vault) => [vault.address, vault] as const), + ), + allocations: fromEntries( + publicAllocatorData.map( + ({ publicAllocatorConfig, allocations }) => [ + publicAllocatorConfig.vault, + allocations, + ], + ), + ), + publicAllocatorConfigs: fromEntries( + publicAllocatorData.map(({ publicAllocatorConfig }) => [ + publicAllocatorConfig.vault, + publicAllocatorConfig, + ]), + ), + marketPublicAllocatorConfigs: fromEntries( + publicAllocatorData.map( + ({ publicAllocatorConfig, marketPublicAllocatorConfigs }) => [ + publicAllocatorConfig.vault, + marketPublicAllocatorConfigs, + ], + ), + ), + }); + + return markets.map((market) => { + const { data: endState, reallocations } = + startState.computeVaultV2Reallocations(market.id, { + timestamp: block.timestamp + REALLOCATION_SIMULATION_DELAY, + reallocatableVaults: loaderParameters.vaults, + maxNativePenalty: loaderParameters.maxNativePenalty, + }); + + return { + startState, + endState, + reallocations, + targetBorrowUtilization: DEFAULT_SUPPLY_TARGET_UTILIZATION, + }; + }); + }, + { cache: false }, + ); + } + + /** + * Fetches a Vault V2 shared-liquidity plan for a target Morpho Blue market. + * + * @param marketId - Target market id to plan reallocations for. + * @returns The start state, simulated end state, action-ready reallocations, and target utilization. + * @throws {UnknownFactory} when the configured chain has no Vault V2 factory. + * @throws {UnknownOfFactory} when a configured vault address is not a Vault V2 from the chain's factory. + * @throws {UnsupportedVaultV2AdapterError} when a configured vault contains an unsupported adapter. + * @throws {viem.BaseError} when a viem RPC read fails. + * @example + * ```ts + * import type { MarketId } from "@morpho-org/blue-sdk"; + * import { VaultV2LiquidityLoader } from "@morpho-org/liquidity-sdk-viem"; + * import { type Address, createPublicClient, http } from "viem"; + * import { mainnet } from "viem/chains"; + * + * async function fetchPlan( + * allocator: Address, + * vault: Address, + * marketId: MarketId, + * ) { + * const client = createPublicClient({ chain: mainnet, transport: http() }); + * const loader = new VaultV2LiquidityLoader(client, { + * allocator, + * vaults: [vault], + * }); + * const result = await loader.fetch(marketId); + * // result satisfies VaultV2LiquidityResult + * return result; + * } + * ``` + */ + public fetch(marketId: MarketId) { + return this.dataLoader.load(marketId); + } +} diff --git a/packages/wdk-protocol-lending-morpho-evm/src/index.ts b/packages/wdk-protocol-lending-morpho-evm/src/index.ts index 9d82ccb75..7ba5bcd1c 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/index.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/index.ts @@ -1,7 +1,10 @@ export type { InputMarketParams } from "@morpho-org/blue-sdk"; export type { + BlueReallocation, RequirementSignature, VaultReallocation, + VaultV1BlueReallocation, + VaultV2BlueReallocation, } from "@morpho-org/morpho-sdk"; export type { TransactionResult } from "@tetherto/wdk-wallet"; export type { diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index a4ee6d057..eee8ae41b 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -1,4 +1,7 @@ -import type { RequirementSignature } from "@morpho-org/morpho-sdk"; +import type { + RequirementSignature, + VaultV2BlueReallocation, +} from "@morpho-org/morpho-sdk"; import * as viem from "viem"; import { beforeEach, describe, expect, test, vi } from "vitest"; @@ -474,6 +477,38 @@ describe.sequential("MorphoProtocolEvm", () => { expect(result).toEqual({ hash: "dummy-borrow-hash", fee: 12_345n }); }); + test("should forward Vault V2 BluePublicAllocator reallocations", async () => { + const reallocation = { + allocator: "0x0000000000000000000000000000000000000010", + type: "bluePublicAllocator", + vault: VAULT, + from: { type: "idle" }, + to: { + adapter: "0x0000000000000000000000000000000000000020", + }, + assets: 50_000n, + nativePenalty: 1n, + } satisfies VaultV2BlueReallocation; + + account.sendTransaction = vi + .fn() + .mockResolvedValue({ hash: "dummy-v2-borrow-hash", fee: 12_345n }); + + await protocol.borrow({ + token: TOKEN, + amount: 100_000n, + reallocations: [reallocation], + }); + + expect(marketEntity.borrow).toHaveBeenCalledWith({ + amount: 100_000n, + userAddress: ADDRESS, + positionData, + slippageTolerance: undefined, + reallocations: [reallocation], + }); + }); + test("should fetch market params when only borrowMarketId is configured", async () => { // biome-ignore lint/suspicious/noShadow: test-local protocol shadowing the suite default const protocol = new MorphoProtocolEvm(account, { diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 909c5a40d..39a26e279 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -6,6 +6,7 @@ import { import { fetchMarket } from "@morpho-org/blue-sdk-viem"; import { type BlueAuthorizationAction, + type BlueReallocation, type ERC20ApprovalAction, type Metadata, type MorphoClientType, @@ -13,7 +14,6 @@ import { type Requirement, type RequirementSignature, type Transaction, - type VaultReallocation, } from "@morpho-org/morpho-sdk"; import type { BorrowResult, @@ -156,8 +156,8 @@ export interface MorphoBorrowOptions { amount: number | bigint; /** The address on behalf of which the borrow operation should be performed. Must match the wallet account address when set. */ onBehalfOf?: string; - /** Optional MetaMorpho Vault V1 reallocations to include in the borrow action. */ - reallocations?: readonly VaultReallocation[]; + /** Optional Vault V1 PublicAllocator or Vault V2 BluePublicAllocator reallocations to include in the borrow action. */ + reallocations?: readonly BlueReallocation[]; /** Signature returned by a Morpho SDK authorization requirement, folded into the bundle as `setAuthorizationWithSig`. */ requirementSignature?: RequirementSignature; /** Optional Morpho SDK slippage tolerance in WAD precision. */ From c2b6a5db43724ec7ad6a61c62c37bbf899ccdf39 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 7 Aug 2026 11:25:12 +0200 Subject: [PATCH 10/41] refactor: rename Vault V2 allocator options --- .changeset/brave-vaults-reallocate.md | 2 +- ...7-29-vault-v2-public-allocator-shared-liquidity.md | 2 +- packages/morpho-sdk/AGENTS.md | 2 +- .../src/entities/vaultV2ReallocationData.ts | 11 +++++------ .../src/helpers/computeVaultV2Reallocations.ts | 4 ++-- packages/morpho-sdk/src/types/AGENTS.md | 1 + packages/morpho-sdk/src/types/sharedLiquidity.ts | 5 +---- 7 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index cf567f0cd..522c72760 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -7,4 +7,4 @@ "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases, add an independent `VaultV2LiquidityLoader` alongside the existing Vault V1 loader, raise its `blue-sdk-viem` and `morpho-sdk` peer floors to the introducing minors, and allow the WDK borrow flow to accept the combined V1/V2 reallocation union. +Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases, add an independent `VaultV2LiquidityLoader` alongside the existing Vault V1 loader, raise its `blue-sdk-viem` and `morpho-sdk` peer floors to the introducing minors, and allow the WDK borrow flow to accept the combined V1/V2 reallocation union. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index ff6e01d1b..9cbcdc3d2 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -78,7 +78,7 @@ This TIB freezes that Vault V2 design. | V2 Bundler actions | `vaultV2BluePublicAllocatorReallocate`, `vaultV2BluePublicAllocatorAllocateFromIdle` | | V2 allocator ABI | `vaultV2BluePublicAllocatorAbi` | | Shared action union | `BlueReallocation` | -| V2 options | `PublicAllocatorOptionsVaultV2`, `ReallocationComputeOptionsVaultV2` | +| V2 options | `VaultV2BluePublicAllocatorOptions` | | V2 config | `VaultV2PublicAllocatorConfig`, `VaultV2MarketPublicAllocatorConfig` | | Fetchers | `fetchVaultV2PublicAllocatorConfig`, `fetchVaultV2MarketPublicAllocatorConfig`, `fetchVaultV2PublicAllocatorData` | diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index d0d058170..fb48612b8 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -29,7 +29,7 @@ Protocol terms used across this package's docs and JSDoc: - **PublicAllocator V1** — MetaMorpho allocator that moves liquidity from one or more sorted source markets into a target via `reallocateTo(...)`; each call pays one `fee`. - **BluePublicAllocator** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered; each call pays its own `nativePenalty`. Its canonical ABI export is `vaultV2BluePublicAllocatorAbi`. - **VaultExitBundlesV1** — standalone periphery for exiting an illiquid VaultV1 or single-adapter VaultV2 into idle underlying assets and/or Morpho Blue supply positions. -- **Shared-liquidity naming** — `VaultV1ReallocationData`, `InputVaultV1ReallocationData`, `computeVaultV1Reallocations`, and `VaultV1BlueReallocation` are canonical for PublicAllocator V1. Their published predecessors (`ReallocationData`, `InputReallocationData`, `computeReallocations`, `getMarketPublicReallocations`, and `VaultReallocation`) remain deprecated aliases. `VaultV2ReallocationData.computeVaultV2Reallocations` and `computeVaultV2Reallocations` produce flat, action-ready `VaultV2BlueReallocation` calls. +- **Shared-liquidity naming** — `VaultV1ReallocationData`, `InputVaultV1ReallocationData`, `computeVaultV1Reallocations`, and `VaultV1BlueReallocation` are canonical for PublicAllocator V1. Their published predecessors (`ReallocationData`, `InputReallocationData`, `computeReallocations`, `getMarketPublicReallocations`, and `VaultReallocation`) remain deprecated aliases. `VaultV2ReallocationData.computeVaultV2Reallocations` and `computeVaultV2Reallocations` accept `VaultV2BluePublicAllocatorOptions` and produce flat, action-ready `VaultV2BlueReallocation` calls. ### Bundler actions diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index 3abaf6263..1a9a3a702 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -18,8 +18,7 @@ import { DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, } from "../helpers/constant.js"; import type { - PublicAllocatorOptionsVaultV2, - ReallocationComputeOptionsVaultV2, + VaultV2BluePublicAllocatorOptions, VaultV2BlueReallocation, } from "../types/index.js"; import { @@ -391,7 +390,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { */ public computeVaultV2Reallocations( marketId: MarketId, - options: PublicAllocatorOptionsVaultV2 = {}, + options: VaultV2BluePublicAllocatorOptions = {}, ) { return this._computeVaultV2Reallocations({ marketId, @@ -416,7 +415,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { }: { readonly marketId: MarketId; readonly maxWithdrawalUtilization: bigint; - readonly options?: PublicAllocatorOptionsVaultV2; + readonly options?: VaultV2BluePublicAllocatorOptions; }): { readonly reallocations: readonly VaultV2BlueReallocation[]; readonly data: VaultV2ReallocationData; @@ -482,7 +481,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { */ public getPublicReallocationLiquidityVaultV2( marketId: MarketId, - options?: PublicAllocatorOptionsVaultV2, + options?: VaultV2BluePublicAllocatorOptions, ) { return this.computeVaultV2Reallocations( marketId, @@ -508,7 +507,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { public getAvailableLiquidityToUtilizationVaultV2( marketId: MarketId, utilization: bigint = DEFAULT_SUPPLY_TARGET_UTILIZATION, - options?: ReallocationComputeOptionsVaultV2, + options?: VaultV2BluePublicAllocatorOptions, ) { const market = this.getMarket(marketId).accrueInterest(options?.timestamp); if (DEFAULT_SUPPLY_TARGET_UTILIZATION > utilization) diff --git a/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts b/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts index 4d7ae6fce..211cb9108 100644 --- a/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts @@ -2,8 +2,8 @@ import { type MarketId, MarketUtils, MathLib } from "@morpho-org/blue-sdk"; import type { VaultV2ReallocationData } from "../entities/vaultV2ReallocationData.js"; import { InsufficientSharedLiquidityError, - type ReallocationComputeOptionsVaultV2, ReallocationWithdrawExceedsMarketSupplyError, + type VaultV2BluePublicAllocatorOptions, type VaultV2BlueReallocation, } from "../types/index.js"; import { DEFAULT_SUPPLY_TARGET_UTILIZATION } from "./constant.js"; @@ -49,7 +49,7 @@ export const computeVaultV2Reallocations = ({ readonly marketId: MarketId; readonly operation: "borrow" | "withdraw"; readonly amount: bigint; - readonly options?: ReallocationComputeOptionsVaultV2; + readonly options?: VaultV2BluePublicAllocatorOptions; }): readonly VaultV2BlueReallocation[] => { if (options?.enabled === false) return []; diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index 27f3a274a..eb62507f0 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -20,6 +20,7 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. - `VaultV1BlueReallocation` — legacy-untagged or explicitly `publicAllocatorV1` vault address + fee + sorted withdrawals; maps to `reallocateTo()`. `VaultReallocation` is its deprecated compatibility alias. - `VaultV2BlueReallocation` — tagged BluePublicAllocator/vault/source/target-adapter/assets/native-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. +- `VaultV2BluePublicAllocatorOptions` — canonical Vault V2 discovery and planner options for timestamp, enablement, vault allowlisting, and the maximum native penalty. - `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. ## Errors (`error.ts`) diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index f368ace98..5b762de9f 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -45,7 +45,7 @@ export interface PublicAllocatorOptions { } /** Options controlling Vault V2 BluePublicAllocator reallocation discovery. */ -export interface PublicAllocatorOptionsVaultV2 { +export interface VaultV2BluePublicAllocatorOptions { /** Whether Vault V2 public allocator discovery is enabled. */ readonly enabled?: boolean; @@ -187,6 +187,3 @@ export interface ReallocationComputeOptions extends PublicAllocatorOptions { */ readonly defaultSupplyTargetUtilization?: bigint; } - -/** Options for the Vault V2 borrow/withdraw reallocation planner. */ -export type ReallocationComputeOptionsVaultV2 = PublicAllocatorOptionsVaultV2; From 3abb520fb4b64358ddef860c7006a11e4c2ec903 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Tue, 11 Aug 2026 14:58:34 +0200 Subject: [PATCH 11/41] feat: use REST data for Vault V2 liquidity --- .changeset/brave-vaults-reallocate.md | 2 +- .../BluePublicAllocatorReadFixture.sol | 32 ++ .../VaultV2PublicAllocatorConfig.test.ts | 99 +++- .../BluePublicAllocatorReadFixture.ts | 200 ++++++++ packages/liquidity-sdk-viem/README.md | 2 +- packages/liquidity-sdk-viem/src/api/rest.ts | 280 +++++++++++ packages/liquidity-sdk-viem/src/errors.ts | 73 +++ packages/liquidity-sdk-viem/src/index.ts | 1 + .../src/vaultV2LiquidityLoader.test.ts | 439 ++++++++++++------ .../src/vaultV2LiquidityLoader.ts | 323 +++++++++++-- .../entities/vaultV2ReallocationData.test.ts | 17 +- .../helpers/computeVaultV2Reallocations.ts | 2 +- scripts/compile-solidity.js | 3 + 13 files changed, 1303 insertions(+), 170 deletions(-) create mode 100644 packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol create mode 100644 packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts create mode 100644 packages/liquidity-sdk-viem/src/api/rest.ts create mode 100644 packages/liquidity-sdk-viem/src/errors.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 522c72760..f0bed352d 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -7,4 +7,4 @@ "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases, add an independent `VaultV2LiquidityLoader` alongside the existing Vault V1 loader, raise its `blue-sdk-viem` and `morpho-sdk` peer floors to the introducing minors, and allow the WDK borrow flow to accept the combined V1/V2 reallocation union. +Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases, add an independent REST-backed `VaultV2LiquidityLoader` alongside the existing Vault V1 loader, raise its `blue-sdk-viem` and `morpho-sdk` peer floors to the introducing minors, and allow the WDK borrow flow to accept the combined V1/V2 reallocation union. diff --git a/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol b/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol new file mode 100644 index 000000000..2318fa65f --- /dev/null +++ b/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +struct VaultData { + bool canAllocateFromIdle; + uint120 nativePenalty; + uint120 accruedNativePenalty; +} + +/// @dev Stateful EVM fixture for exercising BluePublicAllocator read paths on an Anvil fork. +contract BluePublicAllocatorReadFixture { + mapping(address vault => mapping(bytes32 id => uint256)) public absoluteCap; + mapping(address vault => mapping(bytes32 id => bool)) public canDeallocate; + mapping(address vault => mapping(address adapter => bool)) public isActiveAdapter; + mapping(address vault => VaultData) public vaultData; + + function setAbsoluteCap(address vault, bytes32 id, uint256 value) external { + absoluteCap[vault][id] = value; + } + + function setCanDeallocate(address vault, bytes32 id, bool value) external { + canDeallocate[vault][id] = value; + } + + function setIsActiveAdapter(address vault, address adapter, bool value) external { + isActiveAdapter[vault][adapter] = value; + } + + function setVaultData(address vault, bool canAllocateFromIdle, uint120 nativePenalty) external { + vaultData[vault] = VaultData(canAllocateFromIdle, nativePenalty, 0); + } +} diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts index 9873d7ec3..4ebf8e6bd 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts @@ -9,13 +9,19 @@ import { createMockClient, mockRead } from "@morpho-org/test/mock"; import type { Address } from "viem"; import { zeroAddress } from "viem"; import { mainnet } from "viem/chains"; -import { describe, expect, test } from "vitest"; +import { assert, describe, expect, test } from "vitest"; +import { + abi as fixtureAbi, + code as fixtureCode, +} from "../../../test/fixtures/BluePublicAllocatorReadFixture.js"; +import { vaultV2Test } from "../../../test/setup.js"; import { mockDeploylessRead, mockDeploylessReads, } from "../../__test__/viem.js"; import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; import { abi as queryAbi } from "../../queries/vault-v2/GetVaultV2PublicAllocatorConfig.js"; +import { fetchAccrualVaultV2 } from "./VaultV2.js"; import { fetchVaultV2MarketPublicAllocatorConfig, fetchVaultV2PublicAllocatorConfig, @@ -214,3 +220,94 @@ describe("Vault V2 public allocator fetchers", () => { ).resolves.toStrictEqual(expected); }); }); + +describe("Vault V2 public allocator fetchers on fork", () => { + vaultV2Test( + "matches direct reads against the deployless query", + async ({ client }) => { + const forkVault = await fetchAccrualVaultV2( + "0x4C7b69b4a82e9E5D8ec60E96516f7A0E17CBC55C", + client, + ); + const forkAdapter = forkVault.accrualAdapters.find( + (candidate) => + candidate instanceof AccrualVaultV2MorphoMarketV1AdapterV2, + ); + assert(forkAdapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2); + + const forkMarket = forkAdapter.markets[0]; + assert(forkMarket != null); + + const deploymentHash = await client.deployContract({ + abi: fixtureAbi, + bytecode: fixtureCode, + }); + const { contractAddress: allocator } = + await client.waitForTransactionReceipt({ hash: deploymentHash }); + assert(allocator != null); + + const forkMarketParamsId = forkAdapter.ids(forkMarket.params)[2]; + await client.writeContract({ + address: allocator, + abi: fixtureAbi, + functionName: "setVaultData", + args: [forkVault.address, true, 12n], + }); + await client.writeContract({ + address: allocator, + abi: fixtureAbi, + functionName: "setAbsoluteCap", + args: [forkVault.address, forkMarketParamsId, 500n], + }); + await client.writeContract({ + address: allocator, + abi: fixtureAbi, + functionName: "setCanDeallocate", + args: [forkVault.address, forkMarketParamsId, true], + }); + await client.writeContract({ + address: allocator, + abi: fixtureAbi, + functionName: "setIsActiveAdapter", + args: [forkVault.address, forkAdapter.address, true], + }); + + const [deployless, direct] = await Promise.all([ + fetchVaultV2PublicAllocatorData(allocator, forkVault, client, { + deployless: "force", + }), + fetchVaultV2PublicAllocatorData(allocator, forkVault, client, { + deployless: false, + }), + ]); + + expect(deployless).toStrictEqual(direct); + expect(deployless.publicAllocatorConfig).toStrictEqual({ + allocator, + vault: forkVault.address, + canAllocateFromIdle: true, + nativePenalty: 12n, + }); + expect( + deployless.marketPublicAllocatorConfigs[forkMarketParamsId], + ).toStrictEqual({ + allocator, + vault: forkVault.address, + adapter: forkAdapter.address, + marketParamsId: forkMarketParamsId, + absoluteCap: 500n, + canDeallocate: true, + isActiveAdapter: true, + }); + expect( + Object.values(deployless.allocations).some( + (allocation) => + allocation != null && + (allocation.absoluteCap > 0n || + allocation.relativeCap > 0n || + allocation.allocation > 0n), + ), + ).toBe(true); + }, + ); +}); diff --git a/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts b/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts new file mode 100644 index 000000000..520be61e1 --- /dev/null +++ b/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts @@ -0,0 +1,200 @@ +/** @internal Deployless `BluePublicAllocatorReadFixture` query ABI. */ +export const abi = [ + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bytes32", + name: "id", + type: "bytes32", + }, + ], + name: "absoluteCap", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bytes32", + name: "id", + type: "bytes32", + }, + ], + name: "canDeallocate", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + ], + name: "isActiveAdapter", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bytes32", + name: "id", + type: "bytes32", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "setAbsoluteCap", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bytes32", + name: "id", + type: "bytes32", + }, + { + internalType: "bool", + name: "value", + type: "bool", + }, + ], + name: "setCanDeallocate", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + internalType: "bool", + name: "value", + type: "bool", + }, + ], + name: "setIsActiveAdapter", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bool", + name: "canAllocateFromIdle", + type: "bool", + }, + { + internalType: "uint120", + name: "nativePenalty", + type: "uint120", + }, + ], + name: "setVaultData", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + ], + name: "vaultData", + outputs: [ + { + internalType: "bool", + name: "canAllocateFromIdle", + type: "bool", + }, + { + internalType: "uint120", + name: "nativePenalty", + type: "uint120", + }, + { + internalType: "uint120", + name: "accruedNativePenalty", + type: "uint120", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +/** @internal Deployless `BluePublicAllocatorReadFixture` query bytecode. */ +export const code = + "0x60808060405234601557610410908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c908163065b6543146102af5750806308f804d81461026c578063391a1d161461022c5780634b12d3b2146101e357806366faa8391461018c5780638aeed1d114610129578063c4b956c3146100d25763d72ff79a14610074575f80fd5b346100ce5760203660031901126100ce576001600160a01b0361009561039f565b165f526003602052606060405f20546001600160781b036040519160ff811615158352818160081c16602084015260801c166040820152f35b5f80fd5b346100ce5760603660031901126100ce576101276100ee61039f565b6100f66103cb565b9060018060a01b03165f52600160205260405f206024355f5260205260405f209060ff801983541691151516179055565b005b346100ce5760603660031901126100ce5761012761014561039f565b61014d6103b5565b6101556103cb565b9160018060a01b03165f52600260205260405f209060018060a01b03165f5260205260405f209060ff801983541691151516179055565b346100ce5760403660031901126100ce576101a561039f565b6101ad6103b5565b9060018060a01b03165f52600260205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b346100ce5760403660031901126100ce576001600160a01b0361020461039f565b165f52600160205260405f206024355f52602052602060ff60405f2054166040519015158152f35b346100ce5760603660031901126100ce576001600160a01b0361024d61039f565b165f525f60205260405f206024355f5260205260443560405f20555f80f35b346100ce5760403660031901126100ce576001600160a01b0361028d61039f565b165f525f60205260405f206024355f52602052602060405f2054604051908152f35b346100ce5760603660031901126100ce576102c861039f565b6024358015158091036100ce57604435906001600160781b0382168092036100ce576060840184811067ffffffffffffffff82111761038b5760405283526020830190815260408301915f835260018060a01b03165f52600360205261034160405f2093511515849060ff801983541691151516179055565b5182549151610100600160f81b031990921660089190911b6fffffffffffffffffffffffffffffff00161760809190911b6effffffffffffffffffffffffffffff60801b16179055005b634e487b7160e01b5f52604160045260245ffd5b600435906001600160a01b03821682036100ce57565b602435906001600160a01b03821682036100ce57565b6044359081151582036100ce5756fea2646970667358221220dd8548f38071fa9b1f6ac957439322ff1f394071a4122a5efa4668a8d925d21564736f6c63430008240033"; diff --git a/packages/liquidity-sdk-viem/README.md b/packages/liquidity-sdk-viem/README.md index 515514e4a..add30ffde 100644 --- a/packages/liquidity-sdk-viem/README.md +++ b/packages/liquidity-sdk-viem/README.md @@ -77,7 +77,7 @@ const { reallocations, startState, endState, targetBorrowUtilization } = await loader.fetch(marketId); ``` -`VaultV2LiquidityLoader` is a separate RPC-only loader. The BluePublicAllocator and participating Vault V2 addresses are explicit because the protocol has no canonical allocator registry entry. Its `reallocations` can be passed directly to Morpho SDK Blue borrow and withdraw actions. +`VaultV2LiquidityLoader` is a separate REST-backed loader. It reads Vault V2 configuration, state, allocations, withdrawal penalties, Blue market state, adapter positions, oracle prices, and adaptive-curve IRM state from the Morpho REST APIs. BluePublicAllocator-only configuration remains an onchain read through the supplied viem client. The allocator and participating Vault V2 addresses are explicit because the protocol has no canonical allocator registry entry. Its `reallocations` can be passed directly to Morpho SDK Blue borrow and withdraw actions. ## Development diff --git a/packages/liquidity-sdk-viem/src/api/rest.ts b/packages/liquidity-sdk-viem/src/api/rest.ts new file mode 100644 index 000000000..1d863c5ef --- /dev/null +++ b/packages/liquidity-sdk-viem/src/api/rest.ts @@ -0,0 +1,280 @@ +import type { MarketId } from "@morpho-org/blue-sdk"; +import { BLUE_API_BASE_URL } from "@morpho-org/morpho-ts"; +import type { Address, Hash, Hex } from "viem"; +import { + MissingVaultV2LiquidityApiDataError, + VaultV2LiquidityApiError, +} from "../errors.js"; + +interface ApiEnvelope { + readonly data: Data; +} + +interface VaultV2AssetResponse { + readonly address: Address; + readonly decimals: number; + readonly name: string; + readonly symbol: string; +} + +interface VaultV2GatesResponse { + readonly send_shares: Address | null; + readonly receive_shares: Address | null; + readonly send_assets: Address | null; + readonly receive_assets: Address | null; +} + +interface VaultV2Response { + readonly chain_id: number; + readonly address: Address; + readonly last_indexed_block: string; + readonly version: string; + readonly name: string; + readonly symbol: string; + readonly asset: VaultV2AssetResponse; + readonly decimals_offset: number; + readonly factory_address: Address; + readonly creation_block_number: string; + readonly owner: Address; + readonly curator: Address; + readonly timelock_seconds: number; + readonly management_fee_wad: string | null; + readonly management_fee_recipient: Address | null; + readonly performance_fee_wad: string | null; + readonly performance_fee_recipient: Address | null; + readonly max_rate_per_second_wad: string; + readonly adapter_registry: Address; + readonly liquidity_adapter: Address; + readonly liquidity_data: Hex; + readonly gates: VaultV2GatesResponse; +} + +interface VaultV2StateResponse { + readonly chain_id: number; + readonly address: Address; + readonly last_indexed_block: string; + readonly last_accrual_timestamp: number; + readonly total_assets: string; + readonly total_supply: string; + readonly withdrawable_assets: string; + readonly allocated_assets: string; + readonly idle_assets: string; + readonly share_price_ray: string; +} + +interface VaultV2CapResponse { + readonly cap_id: Hash; + readonly cap_data: Hex; + readonly allocated_assets: string; + readonly absolute_cap: string; + readonly relative_cap_wad: string; + readonly cap_type: "adapter" | "collateral" | "market_v1"; + readonly market_id?: MarketId; + readonly collateral_address?: Address; +} + +interface VaultV2AdapterAllocationResponse { + readonly adapter_address: Address; + readonly adapter_kind: + | "morpho_market_v1" + | "morpho_market_v1_v2" + | "morpho_vault_v1" + | "morpho_vault_v2"; + readonly caps: readonly VaultV2CapResponse[]; +} + +interface VaultV2AllocationsResponse { + readonly chain_id: number; + readonly vault_address: Address; + readonly last_indexed_block: string; + readonly allocations: readonly VaultV2AdapterAllocationResponse[]; + readonly unscoped_caps: readonly VaultV2CapResponse[]; +} + +interface VaultV2AdapterPenaltyResponse { + readonly adapter_address: Address; + readonly adapter_kind: + | "blue_market_adapter" + | "vault_v1_adapter" + | "vault_v2_adapter" + | "unknown_adapter"; + readonly force_deallocatable_assets: string; + readonly penalty_rate_wad: string; +} + +interface VaultV2WithdrawalOptionsResponse { + readonly chain_id: number; + readonly vault_address: Address; + readonly liquidity_adapter_available_assets: string; + readonly idle_assets: string; + readonly adapter_penalties: readonly VaultV2AdapterPenaltyResponse[]; +} + +interface MarketResponse { + readonly chain_id: number; + readonly market_id: MarketId; + readonly loan_token: Address; + readonly collateral_token: Address; + readonly oracle_address: Address; + readonly irm_address: Address; + readonly lltv_wad: string; + readonly creation_block_number: string; +} + +interface MarketStateResponse { + readonly chain_id: number; + readonly market_id: MarketId; + readonly last_indexed_block: string; + readonly last_accrual_timestamp: number; + readonly total_supply_assets: string; + readonly total_supply_shares: string; + readonly total_borrow_assets: string; + readonly total_borrow_shares: string; + readonly fee_wad: string; +} + +interface MarketPositionResponse { + readonly chain_id: number; + readonly market_id: MarketId; + readonly user_address: Address; + readonly last_indexed_block: string; + readonly collateral_assets: string; + readonly supply_shares: string; + readonly borrow_shares: string; +} + +interface MarketPositionParameters { + readonly chainId: number; + readonly marketId: MarketId; + readonly user: Address; +} + +interface OracleStateResponse { + readonly chain_id: number; + readonly oracle_address: Address; + readonly last_indexed_block?: string; + readonly last_updated_at?: string | null; + readonly price?: string | null; +} + +interface MarketIrmResponse { + readonly chainId: number; + readonly marketId: MarketId; + readonly irmAddress: Address; + /** @deprecated The consumer API always returns the fixed 90% target. */ + readonly targetUtilization: number; + readonly utilization: number | null; + readonly apyAtTarget: number | null; + readonly rateAtTarget?: string | null; + readonly borrowToTarget: number | null; +} + +async function requestApi( + path: string, + responseKind: "envelope" | "root" = "envelope", +): Promise { + const url = new URL(path, BLUE_API_BASE_URL); + let response: Response; + try { + response = await globalThis.fetch(url, { + headers: { Accept: "application/json" }, + }); + } catch (error) { + throw new VaultV2LiquidityApiError({ + url: url.toString(), + cause: error, + }); + } + + if (!response.ok) + throw new VaultV2LiquidityApiError({ + url: url.toString(), + status: response.status, + }); + + let body: Data | ApiEnvelope; + try { + body = await response.json(); + } catch (error) { + throw new VaultV2LiquidityApiError({ + url: url.toString(), + status: response.status, + cause: error, + }); + } + + if (body == null) + throw new MissingVaultV2LiquidityApiDataError(url.toString()); + if (responseKind === "root") return body as Data; + + const data = (body as ApiEnvelope).data; + if (data == null) + throw new MissingVaultV2LiquidityApiDataError(url.toString()); + return data; +} + +const apiSelector = (chainId: number, identifier: string) => + `${chainId}:${encodeURIComponent(identifier)}`; + +/** @internal Fetches Vault V2 configuration from the Morpho REST API. */ +export const fetchRestVaultV2 = (chainId: number, address: Address) => + requestApi(`/v0/vaults-v2/${apiSelector(chainId, address)}`); + +/** @internal Fetches Vault V2 accounting state from the Morpho REST API. */ +export const fetchRestVaultV2State = (chainId: number, address: Address) => + requestApi( + `/v1/vaults-v2/${apiSelector(chainId, address)}/state`, + ); + +/** @internal Fetches Vault V2 adapter allocations and cap state from the Morpho REST API. */ +export const fetchRestVaultV2Allocations = ( + chainId: number, + address: Address, +) => + requestApi( + `/v0/vaults-v2/${apiSelector(chainId, address)}/allocations`, + ); + +/** @internal Fetches Vault V2 adapter force-deallocation penalties from the Morpho REST API. */ +export const fetchRestVaultV2WithdrawalOptions = ( + chainId: number, + address: Address, +) => + requestApi( + `/v0/vaults-v2/${apiSelector(chainId, address)}/withdrawal-options`, + ); + +/** @internal Fetches Morpho Blue market configuration from the REST API. */ +export const fetchRestMarket = (chainId: number, marketId: MarketId) => + requestApi( + `/v0/blue/markets/${apiSelector(chainId, marketId)}`, + ); + +/** @internal Fetches Morpho Blue market accounting state from the REST API. */ +export const fetchRestMarketState = (chainId: number, marketId: MarketId) => + requestApi( + `/v0/blue/markets/${apiSelector(chainId, marketId)}/state`, + ); + +/** @internal Fetches a Morpho Blue market position from the REST API. */ +export const fetchRestMarketPosition = ({ + chainId, + marketId, + user, +}: MarketPositionParameters) => + requestApi( + `/v0/blue/markets/${apiSelector(chainId, marketId)}/users/${encodeURIComponent(user)}/position`, + ); + +/** @internal Fetches a Morpho Blue oracle price from the REST API. */ +export const fetchRestOracleState = (chainId: number, address: Address) => + requestApi( + `/v0/oracles/${apiSelector(chainId, address)}/state`, + ); + +/** @internal Fetches a Morpho Blue market's adaptive-curve IRM state from the REST API. */ +export const fetchRestMarketIrm = (chainId: number, marketId: MarketId) => + requestApi( + `/consumer/chains/${chainId}/markets/${encodeURIComponent(marketId)}/irm`, + "root", + ); diff --git a/packages/liquidity-sdk-viem/src/errors.ts b/packages/liquidity-sdk-viem/src/errors.ts new file mode 100644 index 000000000..296c15456 --- /dev/null +++ b/packages/liquidity-sdk-viem/src/errors.ts @@ -0,0 +1,73 @@ +/** + * Thrown when the Morpho API cannot provide Vault V2 liquidity data. + * + * @example + * ```ts + * import { VaultV2LiquidityApiError } from "@morpho-org/liquidity-sdk-viem"; + * + * const error = new VaultV2LiquidityApiError({ + * url: "https://api.morpho.org/v0/vaults-v2/1:0x1234", + * status: 503, + * }); + * console.error(error.status, error.url); + * ``` + */ +export class VaultV2LiquidityApiError extends Error { + /** HTTP status returned by the Morpho API, when a response was received. */ + public readonly status?: number; + + /** API endpoint that failed. */ + public readonly url: string; + + /** + * Creates a typed Vault V2 API failure. + * + * @param parameters - Endpoint, optional HTTP status, and optional lower-level failure. + */ + public constructor(parameters: { + readonly url: string; + readonly status?: number; + readonly cause?: unknown; + }) { + super( + parameters.status === undefined + ? `Morpho API request to "${parameters.url}" failed before receiving an HTTP response. Retry the request or verify network connectivity.` + : `Morpho API request to "${parameters.url}" failed with HTTP status "${parameters.status}". Retry the request or verify the Vault V2 configuration.`, + parameters.cause === undefined ? undefined : { cause: parameters.cause }, + ); + this.name = "VaultV2LiquidityApiError"; + this.status = parameters.status; + this.url = parameters.url; + } +} + +/** + * Thrown when a successful Morpho API response omits data required for a Vault V2 simulation. + * + * @example + * ```ts + * import { MissingVaultV2LiquidityApiDataError } from "@morpho-org/liquidity-sdk-viem"; + * + * const error = new MissingVaultV2LiquidityApiDataError( + * "market 0x1234 rateAtTarget", + * ); + * console.error(error.resource); + * ``` + */ +export class MissingVaultV2LiquidityApiDataError extends Error { + /** Description of the missing API resource. */ + public readonly resource: string; + + /** + * Creates a typed missing API data failure. + * + * @param resource - Description of the missing API resource. + */ + public constructor(resource: string) { + super( + `Morpho API response omitted required Vault V2 liquidity data for "${resource}". Retry after the API indexer catches up.`, + ); + this.name = "MissingVaultV2LiquidityApiDataError"; + this.resource = resource; + } +} diff --git a/packages/liquidity-sdk-viem/src/index.ts b/packages/liquidity-sdk-viem/src/index.ts index c5088a05e..faf3d0d14 100644 --- a/packages/liquidity-sdk-viem/src/index.ts +++ b/packages/liquidity-sdk-viem/src/index.ts @@ -1,2 +1,3 @@ +export * from "./errors.js"; export * from "./loader.js"; export * from "./vaultV2LiquidityLoader.js"; diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts index 7d291dffb..b9411e76c 100644 --- a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts @@ -1,122 +1,49 @@ import { - AccrualVaultV2, - AccrualVaultV2MorphoMarketV1AdapterV2, - Market, + getChainAddresses, MarketParams, MathLib, + VaultV2MorphoMarketV1AdapterV2, } from "@morpho-org/blue-sdk"; -import { createMockClient } from "@morpho-org/test/mock"; +import { BLUE_API_BASE_URL } from "@morpho-org/morpho-ts"; +import { + vaultV2Abi, + vaultV2BluePublicAllocatorAbi, +} from "@morpho-org/morpho-ts/abis"; +import { createMockClient, mockRead } from "@morpho-org/test/mock"; +import nock from "nock"; import { type Address, type Hex, toHex, zeroAddress, zeroHash } from "viem"; import { mainnet } from "viem/chains"; -import { beforeEach, describe, expect, test, vi } from "vitest"; - -const { - fetchAccrualVaultV2Mock, - fetchMarketMock, - fetchVaultV2PublicAllocatorDataMock, -} = vi.hoisted(() => ({ - fetchAccrualVaultV2Mock: vi.fn(), - fetchMarketMock: vi.fn(), - fetchVaultV2PublicAllocatorDataMock: vi.fn(), -})); - -vi.mock("@morpho-org/blue-sdk-viem", () => ({ - fetchAccrualVaultV2: fetchAccrualVaultV2Mock, - fetchMarket: fetchMarketMock, - fetchVaultV2PublicAllocatorData: fetchVaultV2PublicAllocatorDataMock, -})); - -const { VaultV2LiquidityLoader } = await import("./vaultV2LiquidityLoader.js"); +import { beforeEach, describe, expect, test } from "vitest"; +import { fetchRestVaultV2 } from "./api/rest.js"; +import { + MissingVaultV2LiquidityApiDataError, + VaultV2LiquidityApiError, +} from "./errors.js"; +import { VaultV2LiquidityLoader } from "./vaultV2LiquidityLoader.js"; const BLOCK_NUMBER = 10n; const BLOCK_TIMESTAMP = 1_700_000_000n; +const ORACLE_PRICE = 10n ** 36n; const ALLOCATOR: Address = "0x0000000000000000000000000000000000000001"; const VAULT: Address = "0x0000000000000000000000000000000000000002"; const ADAPTER: Address = "0x0000000000000000000000000000000000000003"; const ASSET: Address = "0x0000000000000000000000000000000000000004"; -const IRM: Address = "0x0000000000000000000000000000000000000005"; +const COLLATERAL: Address = "0x0000000000000000000000000000000000000006"; +const ORACLE: Address = "0x0000000000000000000000000000000000000007"; +const IRM = getChainAddresses(mainnet.id).adaptiveCurveIrm; const marketParams = new MarketParams({ loanToken: ASSET, - collateralToken: "0x0000000000000000000000000000000000000006", - oracle: zeroAddress, + collateralToken: COLLATERAL, + oracle: ORACLE, irm: IRM, lltv: 860_000_000_000_000_000n, }); -const market = new Market({ - params: marketParams, - totalSupplyAssets: 100n, - totalBorrowAssets: 95n, - totalSupplyShares: 100_000_000n, - totalBorrowShares: 95_000_000n, - lastUpdate: BLOCK_TIMESTAMP, - fee: 0n, -}); -const adapter = new AccrualVaultV2MorphoMarketV1AdapterV2( - { - address: ADAPTER, - parentVault: VAULT, - skimRecipient: zeroAddress, - marketIds: [market.id], - adaptiveCurveIrm: IRM, - supplyShares: { [market.id]: 0n }, - }, - [market], -); -const vault = new AccrualVaultV2( - { - address: VAULT, - asset: ASSET, - _totalAssets: 100n, - totalSupply: 100n, - virtualShares: 0n, - maxRate: 0n, - lastUpdate: BLOCK_TIMESTAMP, - liquidityAdapter: zeroAddress, - liquidityData: "0x", - liquidityAllocations: undefined, - performanceFee: 0n, - managementFee: 0n, - performanceFeeRecipient: zeroAddress, - managementFeeRecipient: zeroAddress, - }, - undefined, - [adapter], - 100n, - {}, -); -const ids = adapter.ids(market.params); -const allocatorData = { - publicAllocatorConfig: { - allocator: ALLOCATOR, - vault: VAULT, - canAllocateFromIdle: true, - nativePenalty: 12n, - }, - marketPublicAllocatorConfigs: { - [ids[2]]: { - allocator: ALLOCATOR, - vault: VAULT, - adapter: ADAPTER, - marketParamsId: ids[2], - absoluteCap: 1_000n, - canDeallocate: false, - isActiveAdapter: true, - }, - }, - allocations: Object.fromEntries( - ids.map((id) => [ - id, - { - id, - absoluteCap: 1_000n, - relativeCap: MathLib.WAD, - allocation: 0n, - }, - ]), - ), -}; - +const ids = [ + VaultV2MorphoMarketV1AdapterV2.adapterId(ADAPTER), + VaultV2MorphoMarketV1AdapterV2.collateralId(COLLATERAL), + VaultV2MorphoMarketV1AdapterV2.marketParamsId(ADAPTER, marketParams), +] as const; const rpcBlock = () => ({ baseFeePerGas: toHex(0n), difficulty: toHex(0n), @@ -141,32 +68,238 @@ const rpcBlock = () => ({ uncles: [], }); -const setup = () => { +const vaultConfigResponse = { + data: { + chain_id: mainnet.id, + address: VAULT, + last_indexed_block: BLOCK_NUMBER.toString(), + version: "2.0", + name: "Vault V2", + symbol: "v2", + asset: { address: ASSET, decimals: 18, name: "Asset", symbol: "AST" }, + decimals_offset: 0, + factory_address: zeroAddress, + creation_block_number: "1", + owner: zeroAddress, + curator: zeroAddress, + timelock_seconds: 0, + management_fee_wad: null, + management_fee_recipient: null, + performance_fee_wad: null, + performance_fee_recipient: null, + max_rate_per_second_wad: "0", + adapter_registry: zeroAddress, + liquidity_adapter: zeroAddress, + liquidity_data: "0x" as Hex, + gates: { + send_shares: null, + receive_shares: null, + send_assets: null, + receive_assets: null, + }, + }, +}; + +const setupApi = (vaultStatus = 200, includePenalty = true) => { + const rest = nock(BLUE_API_BASE_URL); + rest + .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}`) + .reply(vaultStatus, vaultConfigResponse); + rest.get(`/v1/vaults-v2/${mainnet.id}:${VAULT}/state`).reply(200, { + data: { + chain_id: mainnet.id, + address: VAULT, + last_indexed_block: BLOCK_NUMBER.toString(), + last_accrual_timestamp: Number(BLOCK_TIMESTAMP), + total_assets: "100", + total_supply: "100", + withdrawable_assets: "100", + allocated_assets: "0", + idle_assets: "100", + share_price_ray: "1000000000000000000000000000", + }, + }); + rest.get(`/v0/vaults-v2/${mainnet.id}:${VAULT}/allocations`).reply(200, { + data: { + chain_id: mainnet.id, + vault_address: VAULT, + last_indexed_block: BLOCK_NUMBER.toString(), + allocations: [ + { + adapter_address: ADAPTER, + adapter_kind: "morpho_market_v1_v2", + caps: [ + { + cap_id: ids[2], + cap_data: "0x", + allocated_assets: "0", + absolute_cap: "1000", + relative_cap_wad: MathLib.WAD.toString(), + cap_type: "market_v1", + market_id: marketParams.id, + }, + ], + }, + ], + unscoped_caps: [], + }, + }); + rest + .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}/withdrawal-options`) + .reply(200, { + data: { + chain_id: mainnet.id, + vault_address: VAULT, + liquidity_adapter_available_assets: "0", + idle_assets: "100", + adapter_penalties: includePenalty + ? [ + { + adapter_address: ADAPTER, + adapter_kind: "blue_market_adapter", + force_deallocatable_assets: "0", + penalty_rate_wad: "0", + }, + ] + : [], + }, + }); + rest.get(`/v0/blue/markets/${mainnet.id}:${marketParams.id}`).reply(200, { + data: { + chain_id: mainnet.id, + market_id: marketParams.id, + loan_token: ASSET, + collateral_token: COLLATERAL, + oracle_address: ORACLE, + irm_address: IRM, + lltv_wad: marketParams.lltv.toString(), + creation_block_number: "1", + }, + }); + rest + .get(`/v0/blue/markets/${mainnet.id}:${marketParams.id}/state`) + .reply(200, { + data: { + chain_id: mainnet.id, + market_id: marketParams.id, + last_indexed_block: BLOCK_NUMBER.toString(), + last_accrual_timestamp: Number(BLOCK_TIMESTAMP), + total_supply_assets: "100", + total_supply_shares: "100000000", + total_borrow_assets: "95", + total_borrow_shares: "95000000", + fee_wad: "0", + }, + }); + + rest + .get( + `/v0/blue/markets/${mainnet.id}:${marketParams.id}/users/${ADAPTER}/position`, + ) + .reply(200, { + data: { + chain_id: mainnet.id, + market_id: marketParams.id, + user_address: ADAPTER, + last_indexed_block: BLOCK_NUMBER.toString(), + collateral_assets: "0", + supply_shares: "0", + borrow_shares: "0", + }, + }); + rest.get(`/v0/oracles/${mainnet.id}:${ORACLE}/state`).reply(200, { + data: { + chain_id: mainnet.id, + oracle_address: ORACLE, + last_indexed_block: BLOCK_NUMBER.toString(), + last_updated_at: BLOCK_TIMESTAMP.toString(), + price: ORACLE_PRICE.toString(), + }, + }); + rest + .get(`/consumer/chains/${mainnet.id}/markets/${marketParams.id}/irm`) + .reply(200, { + chainId: mainnet.id, + marketId: marketParams.id, + irmAddress: IRM, + targetUtilization: 0.9, + utilization: 0.95, + apyAtTarget: 0, + rateAtTarget: "0", + borrowToTarget: 0, + }); + + return rest; +}; + +const setupClient = () => { const handle = createMockClient(mainnet); - handle.request.mockImplementation(async ({ method }) => { + const defaultRequest = handle.request.getMockImplementation(); + handle.request.mockImplementation(async (call) => { + const { method } = call; if (method === "eth_getBlockByNumber") return rpcBlock(); - if (method === "eth_chainId") return toHex(mainnet.id); - throw new Error(`Unexpected RPC method: ${method}`); + return defaultRequest?.(call); + }); + mockRead(handle, { + address: ALLOCATOR, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "vaultData", + result: [true, 12n, 0n], + }); + mockRead(handle, { + address: ALLOCATOR, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "absoluteCap", + result: 1_000n, + }); + mockRead(handle, { + address: ALLOCATOR, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "canDeallocate", + result: false, + }); + mockRead(handle, { + address: ALLOCATOR, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "isActiveAdapter", + result: true, + }); + mockRead(handle, { + address: VAULT, + abi: vaultV2Abi, + functionName: "absoluteCap", + result: 1_000n, + }); + mockRead(handle, { + address: VAULT, + abi: vaultV2Abi, + functionName: "relativeCap", + result: MathLib.WAD, + }); + mockRead(handle, { + address: VAULT, + abi: vaultV2Abi, + functionName: "allocation", + result: 0n, }); - fetchMarketMock.mockResolvedValue(market); - fetchAccrualVaultV2Mock.mockResolvedValue(vault); - fetchVaultV2PublicAllocatorDataMock.mockResolvedValue(allocatorData); return handle; }; -describe("VaultV2LiquidityLoader", () => { +describe.sequential("VaultV2LiquidityLoader", () => { beforeEach(() => { - vi.clearAllMocks(); + nock.cleanAll(); }); - test("default: returns action-ready Vault V2 reallocations", async () => { - const { client } = setup(); + test("default: hydrates Vault V2 data from REST", async () => { + const api = setupApi(); + const { client } = setupClient(); const loader = new VaultV2LiquidityLoader(client, { allocator: ALLOCATOR, vaults: [VAULT], + deployless: false, }); - const result = await loader.fetch(market.id); + const result = await loader.fetch(marketParams.id); expect(result.reallocations).toStrictEqual([ { @@ -179,41 +312,77 @@ describe("VaultV2LiquidityLoader", () => { nativePenalty: 12n, }, ]); - expect(result.endState.getMarket(market.id).totalSupplyAssets).toBe(200n); - expect(result.targetBorrowUtilization).toBe(900_000_000_000_000_000n); - expect(fetchMarketMock).toHaveBeenCalledWith(market.id, client, { - blockNumber: BLOCK_NUMBER, - chainId: mainnet.id, - }); - expect(fetchAccrualVaultV2Mock).toHaveBeenCalledWith(VAULT, client, { - blockNumber: BLOCK_NUMBER, - chainId: mainnet.id, - deployless: undefined, - }); - expect(fetchVaultV2PublicAllocatorDataMock).toHaveBeenCalledWith( - ALLOCATOR, - vault, - client, - { blockNumber: BLOCK_NUMBER, deployless: undefined }, + expect(result.endState.getMarket(marketParams.id).totalSupplyAssets).toBe( + 200n, ); + expect(result.startState.getMarket(marketParams.id)).toMatchObject({ + price: ORACLE_PRICE, + rateAtTarget: 0n, + }); + expect( + result.startState.getAdapter(VAULT, ADAPTER).supplyShares[ + marketParams.id + ], + ).toBe(0n); + expect( + result.startState.getVault(VAULT).forceDeallocatePenalties[ADAPTER], + ).toBe(0n); + expect(result.targetBorrowUtilization).toBe(900_000_000_000_000_000n); + api.done(); }); test("behavior: filters vaults above the maximum native penalty", async () => { - const { client } = setup(); + const api = setupApi(); + const { client } = setupClient(); const loader = new VaultV2LiquidityLoader(client, { allocator: ALLOCATOR, vaults: [VAULT], maxNativePenalty: 11n, - deployless: "force", + deployless: false, }); - await expect(loader.fetch(market.id)).resolves.toMatchObject({ + await expect(loader.fetch(marketParams.id)).resolves.toMatchObject({ reallocations: [], }); - expect(fetchAccrualVaultV2Mock).toHaveBeenCalledWith(VAULT, client, { - blockNumber: BLOCK_NUMBER, - chainId: mainnet.id, - deployless: "force", + api.done(); + }); + + test("error: VaultV2LiquidityApiError", async () => { + setupApi(503); + const { client } = setupClient(); + const loader = new VaultV2LiquidityLoader(client, { + allocator: ALLOCATOR, + vaults: [VAULT], }); + + await expect(loader.fetch(marketParams.id)).rejects.toBeInstanceOf( + VaultV2LiquidityApiError, + ); + }); + + test("error: VaultV2LiquidityApiError wraps network failures", async () => { + const api = nock(BLUE_API_BASE_URL) + .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}`) + .replyWithError("network unavailable"); + + await expect(fetchRestVaultV2(mainnet.id, VAULT)).rejects.toMatchObject({ + name: "VaultV2LiquidityApiError", + status: undefined, + cause: expect.anything(), + }); + api.done(); + }); + + test("error: MissingVaultV2LiquidityApiDataError", async () => { + setupApi(200, false); + const { client } = setupClient(); + const loader = new VaultV2LiquidityLoader(client, { + allocator: ALLOCATOR, + vaults: [VAULT], + }); + + await expect(loader.fetch(marketParams.id)).rejects.toBeInstanceOf( + MissingVaultV2LiquidityApiDataError, + ); }); }); diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts index f8b8875e5..2a4eabc2f 100644 --- a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts @@ -1,7 +1,13 @@ -import type { MarketId } from "@morpho-org/blue-sdk"; +import { + AccrualVaultV2, + AccrualVaultV2MorphoMarketV1AdapterV2, + getChainAddresses, + Market, + type MarketId, + MarketParams, +} from "@morpho-org/blue-sdk"; import { fetchAccrualVaultV2, - fetchMarket, fetchVaultV2PublicAllocatorData, } from "@morpho-org/blue-sdk-viem"; import { @@ -11,8 +17,27 @@ import { import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; import { fromEntries } from "@morpho-org/morpho-ts"; import DataLoader from "dataloader"; -import type { Address, Chain, Client, Transport } from "viem"; +import { + type Address, + type Chain, + type Client, + isAddressEqual, + type Transport, + zeroAddress, +} from "viem"; import { getBlock } from "viem/actions"; +import { + fetchRestMarket, + fetchRestMarketIrm, + fetchRestMarketPosition, + fetchRestMarketState, + fetchRestOracleState, + fetchRestVaultV2, + fetchRestVaultV2Allocations, + fetchRestVaultV2State, + fetchRestVaultV2WithdrawalOptions, +} from "./api/rest.js"; +import { MissingVaultV2LiquidityApiDataError } from "./errors.js"; const REALLOCATION_SIMULATION_DELAY = 3_600n; @@ -27,11 +52,11 @@ export interface VaultV2LiquidityParameters { /** Maximum native-token penalty accepted per BluePublicAllocator call. */ readonly maxNativePenalty?: bigint; - /** Deployless read mode forwarded to Vault V2 fetchers. Defaults to `true` with direct-read fallback. */ + /** Deployless read mode forwarded to allocator reads and RPC fallbacks. Defaults to `true` with direct-read fallback. */ readonly deployless?: boolean | "force"; } -/** Represents a Vault V2 shared-liquidity plan built from one consistent block snapshot. */ +/** Represents a Vault V2 shared-liquidity plan built from the latest indexed API state. */ export interface VaultV2LiquidityResult { /** Vault V2 state before applying the computed reallocations. */ readonly startState: VaultV2ReallocationData; @@ -49,9 +74,10 @@ export interface VaultV2LiquidityResult { /** * Represents a loader that fetches and simulates Vault V2 BluePublicAllocator shared liquidity. * - * This class is independent from the Vault V1 `LiquidityLoader`: it - * discovers no allocator or vault addresses through the API and only consumes - * the explicit Vault V2 configuration supplied by the caller. + * This class is independent from the Vault V1 `LiquidityLoader`. It consumes + * explicit allocator and vault addresses, loads Vault V2 and market state from + * the Morpho REST API, and reads BluePublicAllocator-only configuration through + * the viem client. * * @example * ```ts @@ -84,7 +110,7 @@ export class VaultV2LiquidityLoader { /** * Creates a Vault V2 shared-liquidity loader. * - * @param client - Viem client used to snapshot onchain state. + * @param client - Viem client used for the current block and BluePublicAllocator-only state. * @param parameters - Explicit allocator, participating vaults, and optional fetch/planning limits. */ public constructor( @@ -94,30 +120,265 @@ export class VaultV2LiquidityLoader { this.dataLoader = new DataLoader( async (marketIds) => { const { client: loaderClient, parameters: loaderParameters } = this; - const block = await getBlock(loaderClient); + const chainId = loaderClient.chain.id; + const [block, restVaults] = await Promise.all([ + getBlock(loaderClient), + Promise.all( + loaderParameters.vaults.map(async (vault) => { + const [config, state, allocations] = await Promise.all([ + fetchRestVaultV2(chainId, vault), + fetchRestVaultV2State(chainId, vault), + fetchRestVaultV2Allocations(chainId, vault), + ]); + return { config, state, allocations }; + }), + ), + ]); const fetchParameters = { blockNumber: block.number, deployless: loaderParameters.deployless, } as const; - const [markets, vaults] = await Promise.all([ - Promise.all( - marketIds.map((marketId) => - fetchMarket(marketId, loaderClient, { - blockNumber: block.number, - chainId: loaderClient.chain.id, - }), + const restHydratedVaults = restVaults.filter( + ({ config, allocations }) => + allocations.allocations.every( + ({ adapter_kind }) => adapter_kind === "morpho_market_v1_v2", + ) && + !( + config.gates.receive_shares != null && + (BigInt(config.management_fee_wad ?? 0) > 0n || + BigInt(config.performance_fee_wad ?? 0) > 0n) ), + ); + const rpcHydratedVaults = restVaults.filter( + (vault) => !restHydratedVaults.includes(vault), + ); + + const rpcVaults = await Promise.all( + rpcHydratedVaults.map(({ config }) => + fetchAccrualVaultV2(config.address, loaderClient, { + ...fetchParameters, + chainId, + }), ), - Promise.all( - loaderParameters.vaults.map((vault) => - fetchAccrualVaultV2(vault, loaderClient, { - ...fetchParameters, - chainId: loaderClient.chain.id, + ); + + const restMarketIds = new Set(marketIds); + for (const { allocations } of restHydratedVaults) { + for (const adapter of allocations.allocations) { + for (const cap of adapter.caps) { + if (cap.market_id != null) restMarketIds.add(cap.market_id); + } + } + } + + const adapterMarketPairs = new Map< + string, + { readonly adapterAddress: Address; readonly marketId: MarketId } + >(); + for (const { allocations } of restHydratedVaults) { + for (const allocation of allocations.allocations) { + for (const { market_id } of allocation.caps) { + if (market_id == null) continue; + adapterMarketPairs.set( + `${allocation.adapter_address.toLowerCase()}:${market_id.toLowerCase()}`, + { + adapterAddress: allocation.adapter_address, + marketId: market_id, + }, + ); + } + } + } + const allRestMarketIds = [...restMarketIds]; + const { adaptiveCurveIrm } = getChainAddresses(chainId); + + const [restMarkets, withdrawalOptions, marketPositions] = + await Promise.all([ + Promise.all( + allRestMarketIds.map(async (marketId) => { + const config = await fetchRestMarket(chainId, marketId); + const [state, oracleState, marketIrm] = await Promise.all([ + fetchRestMarketState(chainId, marketId), + isAddressEqual(config.oracle_address, zeroAddress) + ? undefined + : fetchRestOracleState(chainId, config.oracle_address), + isAddressEqual(config.irm_address, adaptiveCurveIrm) + ? fetchRestMarketIrm(chainId, marketId) + : undefined, + ]); + if ( + isAddressEqual(config.irm_address, adaptiveCurveIrm) && + marketIrm?.rateAtTarget == null + ) + throw new MissingVaultV2LiquidityApiDataError( + `market ${config.market_id} rateAtTarget`, + ); + + return { + config, + state, + price: + oracleState?.price == null + ? undefined + : BigInt(oracleState.price), + rateAtTarget: + marketIrm?.rateAtTarget == null + ? undefined + : BigInt(marketIrm.rateAtTarget), + }; }), ), + Promise.all( + restHydratedVaults.map(async ({ config }) => ({ + vaultAddress: config.address, + data: await fetchRestVaultV2WithdrawalOptions( + chainId, + config.address, + ), + })), + ), + Promise.all( + [...adapterMarketPairs.values()].map( + ({ adapterAddress, marketId }) => + fetchRestMarketPosition({ + chainId, + marketId, + user: adapterAddress, + }), + ), + ), + ]); + + const forceDeallocatePenalties = new Map( + withdrawalOptions.flatMap(({ vaultAddress, data }) => + data.adapter_penalties.map( + ({ adapter_address, penalty_rate_wad }) => + [ + `${vaultAddress.toLowerCase()}:${adapter_address.toLowerCase()}`, + BigInt(penalty_rate_wad), + ] as const, + ), ), - ]); + ); + const positionSupplyShares = new Map( + marketPositions.map( + ({ user_address, market_id, supply_shares }) => + [ + `${user_address.toLowerCase()}:${market_id.toLowerCase()}`, + BigInt(supply_shares), + ] as const, + ), + ); + + const markets = restMarkets.map( + ({ config, state, price, rateAtTarget }) => + new Market({ + params: new MarketParams({ + loanToken: config.loan_token, + collateralToken: config.collateral_token, + oracle: config.oracle_address, + irm: config.irm_address, + lltv: BigInt(config.lltv_wad), + }), + totalSupplyAssets: BigInt(state.total_supply_assets), + totalSupplyShares: BigInt(state.total_supply_shares), + totalBorrowAssets: BigInt(state.total_borrow_assets), + totalBorrowShares: BigInt(state.total_borrow_shares), + lastUpdate: BigInt(state.last_accrual_timestamp), + fee: BigInt(state.fee_wad), + price, + rateAtTarget, + }), + ); + const marketById = new Map( + markets.map((market) => [market.id.toLowerCase(), market] as const), + ); + + const apiVaults = restHydratedVaults.map( + ({ config, state, allocations }) => { + const adapters = allocations.allocations.map((allocation) => { + const adapterMarkets = allocation.caps + .map(({ market_id }) => market_id) + .filter((marketId): marketId is MarketId => marketId != null) + .map((marketId) => { + const market = marketById.get(marketId.toLowerCase()); + if (market == null) + throw new MissingVaultV2LiquidityApiDataError( + `market ${marketId}`, + ); + return market; + }); + const penalty = forceDeallocatePenalties.get( + `${config.address.toLowerCase()}:${allocation.adapter_address.toLowerCase()}`, + ); + if (penalty == null) + throw new MissingVaultV2LiquidityApiDataError( + `vault ${config.address} adapter ${allocation.adapter_address} forceDeallocatePenalty`, + ); + + return { + adapter: new AccrualVaultV2MorphoMarketV1AdapterV2( + { + address: allocation.adapter_address, + parentVault: config.address, + skimRecipient: zeroAddress, + marketIds: adapterMarkets.map(({ id }) => id), + adaptiveCurveIrm, + supplyShares: fromEntries( + adapterMarkets.map((market) => [ + market.id, + positionSupplyShares.get( + `${allocation.adapter_address.toLowerCase()}:${market.id.toLowerCase()}`, + ) ?? 0n, + ]), + ), + }, + adapterMarkets, + ), + penalty, + }; + }); + const liquidityAdapter = adapters.find(({ adapter }) => + isAddressEqual(adapter.address, config.liquidity_adapter), + )?.adapter; + + return new AccrualVaultV2( + { + address: config.address, + name: config.name, + symbol: config.symbol, + decimals: config.asset.decimals + config.decimals_offset, + asset: config.asset.address, + _totalAssets: BigInt(state.total_assets), + totalSupply: BigInt(state.total_supply), + virtualShares: 10n ** BigInt(config.decimals_offset), + maxRate: BigInt(config.max_rate_per_second_wad), + lastUpdate: BigInt(state.last_accrual_timestamp), + liquidityAdapter: config.liquidity_adapter, + liquidityData: config.liquidity_data, + liquidityAllocations: undefined, + performanceFee: BigInt(config.performance_fee_wad ?? 0), + managementFee: BigInt(config.management_fee_wad ?? 0), + performanceFeeRecipient: + config.performance_fee_recipient ?? zeroAddress, + managementFeeRecipient: + config.management_fee_recipient ?? zeroAddress, + }, + liquidityAdapter, + adapters.map(({ adapter }) => adapter), + BigInt(state.idle_assets), + fromEntries( + adapters.map(({ adapter, penalty }) => [ + adapter.address, + penalty, + ]), + ), + ); + }, + ); + const vaults = [...apiVaults, ...rpcVaults]; + const publicAllocatorData = await Promise.all( vaults.map((vault) => fetchVaultV2PublicAllocatorData( @@ -129,7 +390,7 @@ export class VaultV2LiquidityLoader { ), ); const startState = new VaultV2ReallocationData({ - chainId: loaderClient.chain.id, + chainId, allocator: loaderParameters.allocator, markets: fromEntries( markets.map((market) => [market.id, market] as const), @@ -161,7 +422,12 @@ export class VaultV2LiquidityLoader { ), }); - return markets.map((market) => { + return marketIds.map((marketId) => { + const market = marketById.get(marketId.toLowerCase()); + if (market == null) + throw new MissingVaultV2LiquidityApiDataError( + `target market ${marketId}`, + ); const { data: endState, reallocations } = startState.computeVaultV2Reallocations(market.id, { timestamp: block.timestamp + REALLOCATION_SIMULATION_DELAY, @@ -186,10 +452,9 @@ export class VaultV2LiquidityLoader { * * @param marketId - Target market id to plan reallocations for. * @returns The start state, simulated end state, action-ready reallocations, and target utilization. - * @throws {UnknownFactory} when the configured chain has no Vault V2 factory. - * @throws {UnknownOfFactory} when a configured vault address is not a Vault V2 from the chain's factory. - * @throws {UnsupportedVaultV2AdapterError} when a configured vault contains an unsupported adapter. - * @throws {viem.BaseError} when a viem RPC read fails. + * @throws {VaultV2LiquidityApiError} when a REST API request fails. + * @throws {MissingVaultV2LiquidityApiDataError} when indexed REST data is incomplete. + * @throws {viem.BaseError} when a BluePublicAllocator read or RPC compatibility fallback fails. * @example * ```ts * import type { MarketId } from "@morpho-org/blue-sdk"; diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index 9ab53171d..e50df32c1 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -519,7 +519,20 @@ describe("computeVaultV2Reallocations", () => { }); expect(reallocations).toHaveLength(1); - expect(reallocations[0]?.assets).toBe(22n); + expect(reallocations[0]?.assets).toBe(23n); + }); + + test("behavior: rounds required supply up to the utilization target", () => { + const { data } = makeFixture({ targetSupply: 1n, targetBorrow: 0n }); + + const reallocations = computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 1n, + }); + + expect(reallocations[0]?.assets).toBe(1n); }); test("behavior: falls back to a 100% source-utilization ceiling", () => { @@ -607,7 +620,7 @@ describe("computeVaultV2Reallocations", () => { amount: 1n, options: { maxNativePenalty: 7n }, })[0]?.assets, - ).toBe(1n); + ).toBe(2n); }); test("error: InsufficientSharedLiquidityError rejects a partial plan", () => { diff --git a/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts b/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts index 211cb9108..cfbb5f9a4 100644 --- a/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts @@ -80,7 +80,7 @@ export const computeVaultV2Reallocations = ({ return []; let requiredAssets = - MathLib.wDivDown(newTotalBorrowAssets, DEFAULT_SUPPLY_TARGET_UTILIZATION) - + MathLib.wDivUp(newTotalBorrowAssets, DEFAULT_SUPPLY_TARGET_UTILIZATION) - newTotalSupplyAssets; const friendly = data.computeVaultV2Reallocations(marketId, options); diff --git a/scripts/compile-solidity.js b/scripts/compile-solidity.js index b9f0514df..fc96a8ee7 100644 --- a/scripts/compile-solidity.js +++ b/scripts/compile-solidity.js @@ -26,6 +26,9 @@ const packageConfigs = { if (sourceName.includes("/interfaces/")) return null; const parsed = parse(sourceName); + if (sourceName.includes("/fixtures/")) { + return join(packageDir, "test", "fixtures", `${parsed.name}.ts`); + } return join( packageDir, "src", From 6e52e15c0b984ab8606e05a60a5efefc3931c0db Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 14 Aug 2026 10:28:00 +0200 Subject: [PATCH 12/41] fix: update BluePublicAllocator contract integration --- .changeset/brave-vaults-reallocate.md | 2 +- ...lt-v2-public-allocator-shared-liquidity.md | 98 +++++---- .../GetVaultV2PublicAllocatorConfig.sol | 10 +- .../BluePublicAllocatorReadFixture.sol | 15 +- .../interfaces/IBluePublicAllocator.sol | 7 +- .../VaultV2PublicAllocatorConfig.test.ts | 24 +-- .../vault-v2/VaultV2PublicAllocatorConfig.ts | 18 +- .../blue-sdk-viem/src/queries/GetHolding.ts | 2 +- .../blue-sdk-viem/src/queries/GetMarket.ts | 2 +- .../blue-sdk-viem/src/queries/GetToken.ts | 2 +- .../blue-sdk-viem/src/queries/GetVault.ts | 2 +- .../blue-sdk-viem/src/queries/GetVaultUser.ts | 2 +- .../src/queries/vault-v2/GetAccrualVaultV2.ts | 2 +- .../src/queries/vault-v2/GetVaultV2.ts | 2 +- .../GetVaultV2MorphoMarketV1Adapter.ts | 2 +- .../GetVaultV2MorphoMarketV1AdapterV2.ts | 2 +- .../GetVaultV2MorphoVaultV1Adapter.ts | 2 +- .../GetVaultV2PublicAllocatorConfig.ts | 12 +- .../BluePublicAllocatorReadFixture.ts | 27 +-- .../vault/v2/VaultV2PublicAllocatorConfig.ts | 12 +- packages/liquidity-sdk-viem/README.md | 2 +- .../src/vaultV2LiquidityLoader.test.ts | 10 +- .../src/vaultV2LiquidityLoader.ts | 8 +- packages/morpho-sdk/AGENTS.md | 4 +- packages/morpho-sdk/BUNDLER3.md | 15 +- packages/morpho-sdk/README.md | 3 +- packages/morpho-sdk/src/actions/AGENTS.md | 2 +- .../morpho-sdk/src/actions/blue/AGENTS.md | 12 +- .../blue/borrow.bluePublicAllocator.test.ts | 96 +++++++-- .../morpho-sdk/src/actions/blue/borrow.ts | 17 +- .../actions/blue/buildReallocationActions.ts | 58 ++++-- .../morpho-sdk/src/actions/blue/refinance.ts | 16 +- .../actions/blue/supplyCollateralBorrow.ts | 19 +- .../morpho-sdk/src/actions/blue/withdraw.ts | 21 +- .../morpho-sdk/src/bundler/actions.test.ts | 120 ++++++++++- packages/morpho-sdk/src/bundler/actions.ts | 195 ++++++++++++------ packages/morpho-sdk/src/bundler/types.ts | 8 +- .../morpho-sdk/src/entities/blue/AGENTS.md | 3 +- ...ue.bluePublicAllocatorRequirements.test.ts | 94 +++++++++ packages/morpho-sdk/src/entities/blue/blue.ts | 177 ++++++++++------ .../entities/vaultV2ReallocationData.test.ts | 55 ++--- .../src/entities/vaultV2ReallocationData.ts | 49 +++-- packages/morpho-sdk/src/helpers/AGENTS.md | 2 +- .../src/helpers/bluePublicAllocator.test.ts | 57 +++++ .../src/helpers/bluePublicAllocator.ts | 56 +++++ .../helpers/computeVaultV2Reallocations.ts | 2 +- .../morpho-sdk/src/helpers/validate.test.ts | 43 +++- packages/morpho-sdk/src/helpers/validate.ts | 44 +++- packages/morpho-sdk/src/types/AGENTS.md | 8 +- packages/morpho-sdk/src/types/action.ts | 12 ++ packages/morpho-sdk/src/types/error.ts | 57 ++++- .../morpho-sdk/src/types/sharedLiquidity.ts | 11 +- packages/morpho-ts/src/abis.ts | 29 +-- .../src/morpho-protocol-evm.test.ts | 3 +- .../src/morpho-protocol-evm.ts | 16 +- 55 files changed, 1155 insertions(+), 414 deletions(-) create mode 100644 packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts create mode 100644 packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts create mode 100644 packages/morpho-sdk/src/helpers/bluePublicAllocator.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index f0bed352d..d2fde5fa3 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -7,4 +7,4 @@ "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and allocator config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum native-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases, add an independent REST-backed `VaultV2LiquidityLoader` alongside the existing Vault V1 loader, raise its `blue-sdk-viem` and `morpho-sdk` peer floors to the introducing minors, and allow the WDK borrow flow to accept the combined V1/V2 reallocation union. +Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, approve the allocator from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases, add an independent REST-backed `VaultV2LiquidityLoader` alongside the existing Vault V1 loader, raise its `blue-sdk-viem` and `morpho-sdk` peer floors to the introducing minors, and allow the WDK borrow flow to accept the combined V1/V2 reallocation union. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index 9cbcdc3d2..3e68c43a8 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -41,7 +41,7 @@ This TIB freezes that Vault V2 design. planning. - Return flat, action-ready `VaultV2BlueReallocation[]`; one entry is exactly one `reallocate(...)` or `allocateFromIdle(...)` call and pays one - `nativePenalty`. + proportional vault-asset penalty. - Keep `computeVaultV1Reallocations(...)` as the Vault V1 planner and make the versioned V1 discovery/type names canonical. - Simulate the allocator target cap, all three Vault V2 allocation caps, @@ -54,9 +54,9 @@ This TIB freezes that Vault V2 design. - No BluePublicAllocator address registry entry. The allocator contract is an explicit input to fetchers, state, and every returned call. -- No curator-facing setters such as `setAbsoluteCap`, `setCanDeallocate`, or - `setNativePenalty`. -- No penalty-efficiency optimizer beyond an explicit maximum native-penalty +- No curator-facing setters such as `setAbsoluteCap`, `setCanPullFromMarket`, + or `setPenalty`. +- No penalty-efficiency optimizer beyond an explicit maximum-penalty filter. Retained candidates are ranked by obtainable assets. ## Public API and naming @@ -105,14 +105,16 @@ export interface VaultV2BlueReallocation { readonly from: BluePublicAllocatorSource; readonly to: { readonly adapter: Address }; readonly assets: bigint; - readonly nativePenalty: bigint; + readonly penalty: bigint; } ``` The target market parameters come from the enclosing Blue action. Existing borrow, supply-collateral-borrow, loan-asset withdraw, and refinance builders expand each V2 entry into an existing Bundler3 allocator action. The bundle's -native value is the sum of V1 fees and every retained V2 call's penalty. +native value includes only V1 fees. V2 penalty assets are pulled once in the +target loan token through GeneralAdapter1, then approved and spent from +Bundler3 per allocator call. ## Contract model @@ -121,8 +123,14 @@ the fork fixture documented under Dependencies. The relevant read and write surface is: ```solidity +struct VaultData { + bool canPullFromIdle; + uint64 penalty; +} + +address public immutable vaultV2Factory; mapping(address vault => mapping(bytes32 id => uint256)) public absoluteCap; -mapping(address vault => mapping(bytes32 id => bool)) public canDeallocate; +mapping(address vault => mapping(bytes32 id => bool)) public canPullFromMarket; mapping(address vault => mapping(address adapter => bool)) public isActiveAdapter; mapping(address vault => VaultData) public vaultData; @@ -132,24 +140,36 @@ function reallocate( MarketParams calldata deallocateMarketParams, address allocateAdapter, MarketParams calldata allocateMarketParams, - uint128 assets -) external payable; + uint128 assets, + uint64 penalty +) external; function allocateFromIdle( address vault, address adapter, MarketParams calldata marketParams, - uint128 assets -) external payable; + uint128 assets, + uint64 penalty +) external; ``` +The write surface has two distinct authorization paths. The BluePublicAllocator +contract itself must be registered as a Vault V2 allocator so its downstream +`vault.deallocate(...)` and `vault.allocate(...)` calls are authorized. +`reallocate(...)` and `allocateFromIdle(...)` are otherwise permissionless to +their external caller and require the calldata `penalty` to equal the stored +rate. They pull `ceil(assets × penalty / WAD)` of the target loan token from +the caller directly to the vault before allocating. Configuration setters +require the external caller to satisfy `vault.isAllocator(msg.sender)` and +reject vaults not registered in the constructor-supplied Vault V2 factory. + One call has one source and one target. There is no V1-style withdrawal array, -ordering requirement, or multi-source fee refund. `nativePenalty` is charged -per call. +ordering requirement, or multi-source fee refund. The proportional penalty is +rounded up and charged per call. The allocator cap is a post-state ceiling on the target adapter's -`marketParamsId`, not a consumable flow budget. Source-side allocator state is -only `canDeallocate`. +`marketParamsId`, not a consumable flow budget. It must be non-zero before the +vault call. Source-side allocator state is only `canPullFromMarket`. ## Derived allocation IDs @@ -185,8 +205,8 @@ export interface InputVaultV2ReallocationData { ``` The readonly config projections are self-identifying. Vault-wide state carries -`allocator`, `vault`, `canAllocateFromIdle`, and `nativePenalty`. Pair state -also carries `adapter`, `marketParamsId`, `absoluteCap`, `canDeallocate`, and +`allocator`, `vault`, `canPullFromIdle`, and `penalty`. Pair state also carries +`adapter`, `marketParamsId`, `absoluteCap`, `canPullFromMarket`, and `isActiveAdapter`. ## Fetching @@ -253,8 +273,8 @@ A candidate exists only when: `adaptiveCurveIrm`; - target/source adapters are active, source deallocation is permitted, or idle allocation is permitted; -- the vault's configured `nativePenalty` does not exceed - `options.maxNativePenalty` when that threshold is provided; +- the vault's configured `penalty` does not exceed `options.maxPenalty` when + that threshold is provided; - all three target vault caps have a positive absolute cap; - all three source allocations are non-zero for market sources; - the source pair is not the exact target `(adapter, market)` pair. The same @@ -295,11 +315,15 @@ sources are applied in contract order: | target derived IDs | `+= targetUntracked + assets` | same | | source market/shares | withdraw first | unchanged | | target market/shares | supply second | supply | -| vault idle balance | receives and then spends the principal assets | spends principal | +| vault idle balance | `+= penaltyAssets`, then `+= assets`, then `-= assets` | `+= penaltyAssets`, then `-= assets` | | vault `_totalAssets` | unchanged | unchanged | -Shared IDs are updated twice in that order. Penalties are never refunded or -folded into vault accounting. +Shared IDs are updated twice in that order. Penalties remain as direct vault +asset donations. The planner records them in the cloned idle balance but does +not recycle newly donated assets as another shared-liquidity source; otherwise +round-up dust could create a self-replenishing idle candidate. Source untracked +interest changes only the derived allocation IDs; it never becomes idle token +balance. ## Planner @@ -313,10 +337,11 @@ If the post-operation utilization is at most the fixed 90% target, it returns no calls. Otherwise it discovers friendly sources using the fixed 90% source ceiling. If the operation would still have `borrow > supply`, it continues from the friendly post-state with an internal 100% source ceiling. Both phases -ignore vaults above the configured `maxNativePenalty` threshold. +ignore vaults above the configured `maxPenalty` threshold. The flat calls are capped in discovery order to the required amount. Every -retained call keeps its full `nativePenalty`. The planner throws: +retained call keeps its configured `penalty`; its asset cost is recomputed from +the final capped `assets` amount. The planner throws: - `ReallocationWithdrawExceedsMarketSupplyError` when a requested withdraw is impossible regardless of reallocations; @@ -326,8 +351,11 @@ retained call keeps its full `nativePenalty`. The planner throws: ## Validation and metrics The existing `validateReallocations` validates the combined `BlueReallocation` -union. A V2 market source is rejected only when both its adapter and market -match the target pair. The same market through another adapter is accepted. +union. V2 penalties must be between zero and WAD (and therefore fit the +contract's `uint64`), and every call for the same explicit allocator-vault pair +must use one consistent penalty. A V2 market source is rejected only when both +its adapter and market match the target pair. The same market through another +adapter is accepted. `VaultV2ReallocationData` exposes: @@ -388,8 +416,11 @@ source and target thresholds plus an internal 100% fallback. - A plan is a block-state simulation, not an execution guarantee. Allocator caps, shares, and market liquidity can be front-run. -- `msg.value` must cover each call's exact native penalty; a reverted call - still consumes gas. +- The caller must approve GeneralAdapter1 for the aggregate V2 penalty assets. + `getRequirements()` emits a classic loan-token approval when needed; V2 + allocator calls themselves are nonpayable. +- The calldata penalty protects against a curator changing the configured rate + between transaction signing and execution: a mismatch reverts. - Pass `options.timestamp` from the block used to fetch state so market and vault accrual share one reference point. - Relative-cap arithmetic rounds down. Overstating by one wei can cause an @@ -399,19 +430,16 @@ source and target thresholds plus an internal 100% fallback. ## Dependencies -- `morpho-org/vault-v2` commit - `4c7c110a9a3c3ce1ec545fff3b8a832f16cedfcc` for the pinned allocator fixture - and surrounding Vault V2 contracts. -- `BluePublicAllocator.sol` last-touch commit - `b41782590d3d33d8d836aedd233aaa72ac8b2aa2` for the allocator interface - described here. +- `morpho-org/vault-v2` `BluePublicAllocator.sol` last-touch commit + `a54e96c4cda93d5231df513f8e378653999c0e38` for the allocator interface and + behavior described here. - Existing `VaultV2MorphoMarketV1AdapterV2.ids`, `AccrualVaultV2`, Morpho Blue `Market`, and Bundler3 allocator encoders. - Existing Anvil fork harness from `@morpho-org/test`. ## References -- [`BluePublicAllocator.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/periphery/blue-public-allocator/BluePublicAllocator.sol) +- [`BluePublicAllocator.sol`](https://github.com/morpho-org/vault-v2/blob/a54e96c4cda93d5231df513f8e378653999c0e38/src/periphery/blue-public-allocator/BluePublicAllocator.sol) - [`VaultV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/VaultV2.sol) - [`MorphoMarketV1AdapterV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/adapters/MorphoMarketV1AdapterV2.sol) - [TIB-2026-06-16 shared-liquidity target-utilization metric](./TIB-2026-06-16-shared-liquidity-target-utilization-metric.md) diff --git a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol index 58c0d0691..69346666d 100644 --- a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol +++ b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol @@ -13,7 +13,7 @@ struct VaultV2MarketPublicAllocatorResponse { address adapter; bytes32 marketParamsId; uint256 absoluteCap; - bool canDeallocate; + bool canPullFromMarket; bool isActiveAdapter; } @@ -25,8 +25,8 @@ struct VaultV2AllocationResponse { } struct VaultV2PublicAllocatorResponse { - bool canAllocateFromIdle; - uint120 nativePenalty; + bool canPullFromIdle; + uint64 penalty; VaultV2MarketPublicAllocatorResponse[] marketConfigs; VaultV2AllocationResponse[] allocations; } @@ -38,7 +38,7 @@ contract GetVaultV2PublicAllocatorConfig { VaultV2MarketPublicAllocatorRequest[] calldata marketRequests, bytes32[] calldata allocationIds ) external view returns (VaultV2PublicAllocatorResponse memory res) { - (res.canAllocateFromIdle, res.nativePenalty,) = allocator.vaultData(address(vault)); + (res.canPullFromIdle, res.penalty) = allocator.vaultData(address(vault)); uint256 marketRequestsLength = marketRequests.length; res.marketConfigs = new VaultV2MarketPublicAllocatorResponse[](marketRequestsLength); @@ -48,7 +48,7 @@ contract GetVaultV2PublicAllocatorConfig { adapter: request.adapter, marketParamsId: request.marketParamsId, absoluteCap: allocator.absoluteCap(address(vault), request.marketParamsId), - canDeallocate: allocator.canDeallocate(address(vault), request.marketParamsId), + canPullFromMarket: allocator.canPullFromMarket(address(vault), request.marketParamsId), isActiveAdapter: allocator.isActiveAdapter(address(vault), request.adapter) }); } diff --git a/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol b/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol index 2318fa65f..a25a9e00b 100644 --- a/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol +++ b/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol @@ -2,15 +2,14 @@ pragma solidity ^0.8.0; struct VaultData { - bool canAllocateFromIdle; - uint120 nativePenalty; - uint120 accruedNativePenalty; + bool canPullFromIdle; + uint64 penalty; } /// @dev Stateful EVM fixture for exercising BluePublicAllocator read paths on an Anvil fork. contract BluePublicAllocatorReadFixture { mapping(address vault => mapping(bytes32 id => uint256)) public absoluteCap; - mapping(address vault => mapping(bytes32 id => bool)) public canDeallocate; + mapping(address vault => mapping(bytes32 id => bool)) public canPullFromMarket; mapping(address vault => mapping(address adapter => bool)) public isActiveAdapter; mapping(address vault => VaultData) public vaultData; @@ -18,15 +17,15 @@ contract BluePublicAllocatorReadFixture { absoluteCap[vault][id] = value; } - function setCanDeallocate(address vault, bytes32 id, bool value) external { - canDeallocate[vault][id] = value; + function setCanPullFromMarket(address vault, bytes32 id, bool value) external { + canPullFromMarket[vault][id] = value; } function setIsActiveAdapter(address vault, address adapter, bool value) external { isActiveAdapter[vault][adapter] = value; } - function setVaultData(address vault, bool canAllocateFromIdle, uint120 nativePenalty) external { - vaultData[vault] = VaultData(canAllocateFromIdle, nativePenalty, 0); + function setVaultData(address vault, bool canPullFromIdle, uint64 penalty) external { + vaultData[vault] = VaultData(canPullFromIdle, penalty); } } diff --git a/packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol b/packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol index 9f90c604e..b6b788239 100644 --- a/packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol +++ b/packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol @@ -4,10 +4,7 @@ pragma solidity ^0.8.0; interface IBluePublicAllocator { function absoluteCap(address vault, bytes32 id) external view returns (uint256); - function canDeallocate(address vault, bytes32 id) external view returns (bool); + function canPullFromMarket(address vault, bytes32 id) external view returns (bool); function isActiveAdapter(address vault, address adapter) external view returns (bool); - function vaultData(address vault) - external - view - returns (bool canAllocateFromIdle, uint120 nativePenalty, uint120 accruedNativePenalty); + function vaultData(address vault) external view returns (bool canPullFromIdle, uint64 penalty); } diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts index 4ebf8e6bd..f097c6433 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts @@ -90,8 +90,8 @@ const expected = { publicAllocatorConfig: { allocator: ALLOCATOR, vault: VAULT, - canAllocateFromIdle: true, - nativePenalty: 12n, + canPullFromIdle: true, + penalty: 12n, }, marketPublicAllocatorConfigs: { [marketParamsId]: { @@ -100,7 +100,7 @@ const expected = { adapter: ADAPTER, marketParamsId, absoluteCap: 500n, - canDeallocate: true, + canPullFromMarket: true, isActiveAdapter: true, }, }, @@ -122,7 +122,7 @@ const mockDirectReads = (handle: ReturnType) => { address: ALLOCATOR, abi: vaultV2BluePublicAllocatorAbi, functionName: "vaultData", - result: [true, 12n, 34n], + result: [true, 12n], }); mockRead(handle, { address: ALLOCATOR, @@ -133,7 +133,7 @@ const mockDirectReads = (handle: ReturnType) => { mockRead(handle, { address: ALLOCATOR, abi: vaultV2BluePublicAllocatorAbi, - functionName: "canDeallocate", + functionName: "canPullFromMarket", result: true, }); mockRead(handle, { @@ -186,14 +186,14 @@ describe("Vault V2 public allocator fetchers", () => { test("behavior: deployless batching returns all derived ids", async () => { const handle = createMockClient(mainnet); mockDeploylessRead(handle, queryAbi, "query", { - canAllocateFromIdle: true, - nativePenalty: 12n, + canPullFromIdle: true, + penalty: 12n, marketConfigs: [ { adapter: ADAPTER, marketParamsId, absoluteCap: 500n, - canDeallocate: true, + canPullFromMarket: true, isActiveAdapter: true, }, ], @@ -262,7 +262,7 @@ describe("Vault V2 public allocator fetchers on fork", () => { await client.writeContract({ address: allocator, abi: fixtureAbi, - functionName: "setCanDeallocate", + functionName: "setCanPullFromMarket", args: [forkVault.address, forkMarketParamsId, true], }); await client.writeContract({ @@ -285,8 +285,8 @@ describe("Vault V2 public allocator fetchers on fork", () => { expect(deployless.publicAllocatorConfig).toStrictEqual({ allocator, vault: forkVault.address, - canAllocateFromIdle: true, - nativePenalty: 12n, + canPullFromIdle: true, + penalty: 12n, }); expect( deployless.marketPublicAllocatorConfigs[forkMarketParamsId], @@ -296,7 +296,7 @@ describe("Vault V2 public allocator fetchers on fork", () => { adapter: forkAdapter.address, marketParamsId: forkMarketParamsId, absoluteCap: 500n, - canDeallocate: true, + canPullFromMarket: true, isActiveAdapter: true, }); expect( diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts index a308f2bda..9071ad0bf 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts @@ -27,7 +27,7 @@ import type { * @param parameters.blockNumber - Optional block number for historical reads. * @param parameters.blockTag - Optional block tag for historical reads. * @param parameters.stateOverride - Optional viem state override. - * @returns The vault's idle-allocation permission and per-call native penalty. + * @returns The vault's idle-pull permission and WAD-scaled vault-asset penalty. * @example * ```ts * import { fetchVaultV2PublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; @@ -42,7 +42,7 @@ export async function fetchVaultV2PublicAllocatorConfig( client: Client, parameters: FetchParameters = {}, ): Promise { - const [canAllocateFromIdle, nativePenalty] = await readContract(client, { + const [canPullFromIdle, penalty] = await readContract(client, { ...parameters, address: allocator, abi: vaultV2BluePublicAllocatorAbi, @@ -53,8 +53,8 @@ export async function fetchVaultV2PublicAllocatorConfig( return { allocator, vault, - canAllocateFromIdle, - nativePenalty, + canPullFromIdle, + penalty, }; } @@ -93,7 +93,7 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( client: Client, parameters: FetchParameters = {}, ): Promise { - const [absoluteCap, canDeallocate, isActiveAdapter] = await Promise.all([ + const [absoluteCap, canPullFromMarket, isActiveAdapter] = await Promise.all([ readContract(client, { ...parameters, address: allocator, @@ -105,7 +105,7 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( ...parameters, address: allocator, abi: vaultV2BluePublicAllocatorAbi, - functionName: "canDeallocate", + functionName: "canPullFromMarket", args: [vault, marketParamsId], }), readContract(client, { @@ -123,7 +123,7 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( adapter, marketParamsId, absoluteCap, - canDeallocate, + canPullFromMarket, isActiveAdapter, }; } @@ -212,8 +212,8 @@ export async function fetchVaultV2PublicAllocatorData( publicAllocatorConfig: { allocator, vault: vault.address, - canAllocateFromIdle: result.canAllocateFromIdle, - nativePenalty: result.nativePenalty, + canPullFromIdle: result.canPullFromIdle, + penalty: result.penalty, } satisfies VaultV2PublicAllocatorConfig, marketPublicAllocatorConfigs, allocations, diff --git a/packages/blue-sdk-viem/src/queries/GetHolding.ts b/packages/blue-sdk-viem/src/queries/GetHolding.ts index 1b42cb252..a9cda86b6 100644 --- a/packages/blue-sdk-viem/src/queries/GetHolding.ts +++ b/packages/blue-sdk-viem/src/queries/GetHolding.ts @@ -119,4 +119,4 @@ export const abi = [ /** @internal Deployless `GetHolding` query bytecode. */ export const code = - "0x60808060405234601557610794908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c634755ff3e14610024575f80fd5b346104735760e0366003190112610473576004356001600160a01b0381168103610473576024356001600160a01b038116810361047357604435906001600160a01b038216820361047357606435926001600160a01b038416840361047357608435906001600160a01b03821682036104735760a43580151581036104735760c4358015158103610473576101406040525f60805260a0966100c4610709565b885260c0946100d1610709565b86525f60e0818152610100829052610120919091526040516370a0823160e01b81526001600160a01b038981166004830152919991602090829060249082908b165afa9081156105d7575f91610685575b50608052604051636eb1769f60e11b81526001600160a01b0389811660048301529182166024820152838216151591602090829060449082908b165afa9081156105d7575f91610653575b50811561064a57604051636eb1769f60e11b81526001600160a01b03808b16600483015285166024820152602081806044810103816001600160a01b038c165afa80156105d7575f90610616575b6101f491505b604051636eb1769f60e11b81526001600160a01b03808d1660048301528616602482015291602090839081906044820190565b03816001600160a01b038d165afa9182156105d7575f926105e2575b506040519261021e846106cb565b8352602083015260408201528a5261050f575b5050604051623f675f60e91b81526001600160a01b0386811660048301526020908290602490829088165afa5f91816104db575b506104cd575b50604051624b894760e91b81526001600160a01b0386811660048301526020908290602490829088165afa5f91816104ac575b5061048957501561047f576102b760015b610120610752565b610351575b5065ffffffffffff91506040908180519560805187525180516020880152602081015182880152015160608601525160018060a01b0381511660808601528260208201511660a086015201511660c083015251151560e0820152608080015161010082015260a06080015190600382101561033d5761014091610120820152f35b634e487b7160e01b5f52602160045260245ffd5b5f6101205260405163650369bf60e01b815290602090829060049082906001600160a01b03165afa5f918161042d575b509065ffffffffffff936040939261039c575b5050906102bc565b8351633af32abf60e01b81526001600160a01b0391821660048201529160209183916024918391165afa5f91816103fc575b506103da575b80610394565b156103f2576103ec6002610120610752565b5f6103d4565b6103ec60016102af565b61041f91925060203d602011610426575b61041781836106e7565b81019061073a565b905f6103ce565b503d61040d565b909291506020813d602011610477575b8161044a602093836106e7565b810103126104735751916001600160a01b03831683036104735790919065ffffffffffff610381565b5f80fd5b3d915061043d565b6102b760026102af565b1590506104a25761049d6002610120610752565b6102b7565b61049d60016102af565b6104c691925060203d6020116104265761041781836106e7565b905f61029e565b60018752610100525f61026b565b9091506020813d602011610507575b816104f7602093836106e7565b810103126104735751905f610265565b3d91506104ea565b60405163927da10560e01b81526001600160a01b038881166004830152868116602483015291821660448201529160609183916064918391165afa9081156105d7575f91610562575b5084525f80610231565b90506060813d6060116105cf575b8161057d606093836106e7565b810103126104735760405190610592826106cb565b80516001600160a01b0381168103610473576105c49160409184526105b960208201610727565b602085015201610727565b60408201525f610558565b3d9150610570565b6040513d5f823e3d90fd5b9091506020813d60201161060e575b816105fe602093836106e7565b810103126104735751905f610210565b3d91506105f1565b506020813d602011610642575b81610630602093836106e7565b81010312610473576101f490516101bb565b3d9150610623565b6101f45f6101c1565b90506020813d60201161067d575b8161066e602093836106e7565b8101031261047357515f61016d565b3d9150610661565b90506020813d6020116106af575b816106a0602093836106e7565b8101031261047357515f610122565b3d9150610693565b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff8211176106b757604052565b90601f8019910116810190811067ffffffffffffffff8211176106b757604052565b60405190610716826106cb565b5f6040838281528260208201520152565b519065ffffffffffff8216820361047357565b90816020910312610473575180151581036104735790565b600382101561033d575256fea26469706673582212208fbc642a6a063550b533e25297d2425360449477f7c40e84e4179d222f687dac64736f6c63430008230033"; + "0x60808060405234601557610794908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c634755ff3e14610024575f80fd5b346104735760e0366003190112610473576004356001600160a01b0381168103610473576024356001600160a01b038116810361047357604435906001600160a01b038216820361047357606435926001600160a01b038416840361047357608435906001600160a01b03821682036104735760a43580151581036104735760c4358015158103610473576101406040525f60805260a0966100c4610709565b885260c0946100d1610709565b86525f60e0818152610100829052610120919091526040516370a0823160e01b81526001600160a01b038981166004830152919991602090829060249082908b165afa9081156105d7575f91610685575b50608052604051636eb1769f60e11b81526001600160a01b0389811660048301529182166024820152838216151591602090829060449082908b165afa9081156105d7575f91610653575b50811561064a57604051636eb1769f60e11b81526001600160a01b03808b16600483015285166024820152602081806044810103816001600160a01b038c165afa80156105d7575f90610616575b6101f491505b604051636eb1769f60e11b81526001600160a01b03808d1660048301528616602482015291602090839081906044820190565b03816001600160a01b038d165afa9182156105d7575f926105e2575b506040519261021e846106cb565b8352602083015260408201528a5261050f575b5050604051623f675f60e91b81526001600160a01b0386811660048301526020908290602490829088165afa5f91816104db575b506104cd575b50604051624b894760e91b81526001600160a01b0386811660048301526020908290602490829088165afa5f91816104ac575b5061048957501561047f576102b760015b610120610752565b610351575b5065ffffffffffff91506040908180519560805187525180516020880152602081015182880152015160608601525160018060a01b0381511660808601528260208201511660a086015201511660c083015251151560e0820152608080015161010082015260a06080015190600382101561033d5761014091610120820152f35b634e487b7160e01b5f52602160045260245ffd5b5f6101205260405163650369bf60e01b815290602090829060049082906001600160a01b03165afa5f918161042d575b509065ffffffffffff936040939261039c575b5050906102bc565b8351633af32abf60e01b81526001600160a01b0391821660048201529160209183916024918391165afa5f91816103fc575b506103da575b80610394565b156103f2576103ec6002610120610752565b5f6103d4565b6103ec60016102af565b61041f91925060203d602011610426575b61041781836106e7565b81019061073a565b905f6103ce565b503d61040d565b909291506020813d602011610477575b8161044a602093836106e7565b810103126104735751916001600160a01b03831683036104735790919065ffffffffffff610381565b5f80fd5b3d915061043d565b6102b760026102af565b1590506104a25761049d6002610120610752565b6102b7565b61049d60016102af565b6104c691925060203d6020116104265761041781836106e7565b905f61029e565b60018752610100525f61026b565b9091506020813d602011610507575b816104f7602093836106e7565b810103126104735751905f610265565b3d91506104ea565b60405163927da10560e01b81526001600160a01b038881166004830152868116602483015291821660448201529160609183916064918391165afa9081156105d7575f91610562575b5084525f80610231565b90506060813d6060116105cf575b8161057d606093836106e7565b810103126104735760405190610592826106cb565b80516001600160a01b0381168103610473576105c49160409184526105b960208201610727565b602085015201610727565b60408201525f610558565b3d9150610570565b6040513d5f823e3d90fd5b9091506020813d60201161060e575b816105fe602093836106e7565b810103126104735751905f610210565b3d91506105f1565b506020813d602011610642575b81610630602093836106e7565b81010312610473576101f490516101bb565b3d9150610623565b6101f45f6101c1565b90506020813d60201161067d575b8161066e602093836106e7565b8101031261047357515f61016d565b3d9150610661565b90506020813d6020116106af575b816106a0602093836106e7565b8101031261047357515f610122565b3d9150610693565b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff8211176106b757604052565b90601f8019910116810190811067ffffffffffffffff8211176106b757604052565b60405190610716826106cb565b5f6040838281528260208201520152565b519065ffffffffffff8216820361047357565b90816020910312610473575180151581036104735790565b600382101561033d575256fea264697066735822122095a40c118b49c33ea63eedac0f2c5b9f776b8e0143bd1d1ec1e7e427a022bf7c64736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/GetMarket.ts b/packages/blue-sdk-viem/src/queries/GetMarket.ts index e9fe8f163..d4779c9ee 100644 --- a/packages/blue-sdk-viem/src/queries/GetMarket.ts +++ b/packages/blue-sdk-viem/src/queries/GetMarket.ts @@ -119,4 +119,4 @@ export const abi = [ /** @internal Deployless `GetMarket` query bytecode. */ export const code = - "0x608080604052346015576104f8908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63d8f172c414610025575f80fd5b34610285576060366003190112610285576004356001600160a01b0381169190829003610285576044356001600160a01b0381169290602435908490036102855761006f8361042c565b60405161007b8161042c565b5f81525f60208201525f60408201525f60608201525f60808201528352602083016040516100a88161045c565b5f81525f60208201525f60408201525f60608201525f60808201525f60a0820152815260408401905f825260608501925f845260808601945f8652604051632c3c915760e01b815282600482015260a081602481855afa908115610291575f9161039f575b5060249160c091895260405192838092632e3071cd60e11b82528660048301525afa908115610291575f91610302575b5082528551604001516001600160a01b03168061029c575b508551606001516001600160a01b0316871461021e575b5060408051955180516001600160a01b0390811688526020808301518216818a015282840151821689850152606080840151909216828a015260809283015189840152935180516001600160801b0390811660a08b81019190915295820151811660c08b015293810151841660e08a0152908101518316610100890152908101518216610120880152909101511661014085015251151561016084015251610180830152516101a08201526101c09150f35b6020906024604051809981936301977b5760e01b835260048301525afa958615610291575f96610258575b509483526101c09460a061016c565b95506020863d602011610289575b8161027360209383610478565b810103126102855794519460a0610249565b5f80fd5b3d9150610266565b6040513d5f823e3d90fd5b60206004916040519283809263501ad8ff60e11b82525afa5f91816102ce575b5015610155576001845284525f610155565b9091506020813d6020116102fa575b816102ea60209383610478565b810103126102855751905f6102bc565b3d91506102dd565b905060c0813d60c011610397575b8161031d60c09383610478565b810103126102855761038c60a0604051926103378461045c565b610340816104ae565b845261034e602082016104ae565b602085015261035f604082016104ae565b6040850152610370606082016104ae565b6060850152610381608082016104ae565b6080850152016104ae565b60a08201525f61013d565b3d9150610310565b905060a0813d60a011610424575b816103ba60a09383610478565b810103126102855760249160c0916080604051916103d78361042c565b6103e08161049a565b83526103ee6020820161049a565b60208401526103ff6040820161049a565b60408401526104106060820161049a565b60608401520151608082015291509161010d565b3d91506103ad565b60a0810190811067ffffffffffffffff82111761044857604052565b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff82111761044857604052565b90601f8019910116810190811067ffffffffffffffff82111761044857604052565b51906001600160a01b038216820361028557565b51906001600160801b03821682036102855756fea2646970667358221220acbd98f027aaca3ed2f90675c4eece5d7bd1a9fbbbb62956dae5a7491ebc745564736f6c634300081b0033"; + "0x608080604052346015576104f8908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63d8f172c414610025575f80fd5b34610285576060366003190112610285576004356001600160a01b0381169190829003610285576044356001600160a01b0381169290602435908490036102855761006f8361042c565b60405161007b8161042c565b5f81525f60208201525f60408201525f60608201525f60808201528352602083016040516100a88161045c565b5f81525f60208201525f60408201525f60608201525f60808201525f60a0820152815260408401905f825260608501925f845260808601945f8652604051632c3c915760e01b815282600482015260a081602481855afa908115610291575f9161039f575b5060249160c091895260405192838092632e3071cd60e11b82528660048301525afa908115610291575f91610302575b5082528551604001516001600160a01b03168061029c575b508551606001516001600160a01b0316871461021e575b5060408051955180516001600160a01b0390811688526020808301518216818a015282840151821689850152606080840151909216828a015260809283015189840152935180516001600160801b0390811660a08b81019190915295820151811660c08b015293810151841660e08a0152908101518316610100890152908101518216610120880152909101511661014085015251151561016084015251610180830152516101a08201526101c09150f35b6020906024604051809981936301977b5760e01b835260048301525afa958615610291575f96610258575b509483526101c09460a061016c565b95506020863d602011610289575b8161027360209383610478565b810103126102855794519460a0610249565b5f80fd5b3d9150610266565b6040513d5f823e3d90fd5b60206004916040519283809263501ad8ff60e11b82525afa5f91816102ce575b5015610155576001845284525f610155565b9091506020813d6020116102fa575b816102ea60209383610478565b810103126102855751905f6102bc565b3d91506102dd565b905060c0813d60c011610397575b8161031d60c09383610478565b810103126102855761038c60a0604051926103378461045c565b610340816104ae565b845261034e602082016104ae565b602085015261035f604082016104ae565b6040850152610370606082016104ae565b6060850152610381608082016104ae565b6080850152016104ae565b60a08201525f61013d565b3d9150610310565b905060a0813d60a011610424575b816103ba60a09383610478565b810103126102855760249160c0916080604051916103d78361042c565b6103e08161049a565b83526103ee6020820161049a565b60208401526103ff6040820161049a565b60408401526104106060820161049a565b60608401520151608082015291509161010d565b3d91506103ad565b60a0810190811067ffffffffffffffff82111761044857604052565b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff82111761044857604052565b90601f8019910116810190811067ffffffffffffffff82111761044857604052565b51906001600160a01b038216820361028557565b51906001600160801b03821682036102855756fea2646970667358221220b6ade9a1ccfe49ca6800384146bc973387a5593355ce6b644f37a62beabd7be964736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/GetToken.ts b/packages/blue-sdk-viem/src/queries/GetToken.ts index 4730e6bad..05c028ef5 100644 --- a/packages/blue-sdk-viem/src/queries/GetToken.ts +++ b/packages/blue-sdk-viem/src/queries/GetToken.ts @@ -107,4 +107,4 @@ export const abi = [ /** @internal Deployless `GetToken` query bytecode. */ export const code = - "0x608080604052346015576108b5908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63287861f914610025575f80fd5b346102cd5760403660031901126102cd576004356001600160a01b03811691908290036102cd576024359182151583036102cd57610100820182811067ffffffffffffffff821117610307576040525f8252602082015f8152604083019160608352606084015f8152608085016060815260a08601945f865260c08701946100ab61037d565b865260e08801985f8a526100dd6040516306fdde0360e01b6020820152600481526100d760248261035b565b87610444565b906102fb575b506040516395d89b4160e01b602082015260048152610107906100d760248261035b565b906102ef575b5060405163313ce56760e01b6020820152600481526101379061013160248261035b565b87610579565b906102e4575b5061026d575b90610185916101546101a0966105cb565b90610261575b506040519860208a525160208a0152511515604089015251610100606089015261012088019061031b565b91511515608087015251858203601f190160a087015261031b565b915160c08401525192601f198383030160e084015260ff60f81b845116825260c06101ef6101dd602087015160e0602087015260e086019061031b565b6040870151858203604087015261031b565b946060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c08186039101526020808351958681520192015f945b8086106102495750508293505115156101008301520390f35b90926020806001928651815201940195019490610230565b60018b5287525f61015a565b6040516301afd7c160e11b815294602086600481845afa9586156102d9575f9661029b575b50948752610143565b955091906020863d6020116102d1575b816102b86020938361035b565b810103126102cd579451949091610154610292565b5f80fd5b3d91506102ab565b6040513d5f823e3d90fd5b60ff1689525f61013d565b6001835283525f61010d565b6001865284525f6100e3565b634e487b7160e01b5f52604160045260245ffd5b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b60e0810190811067ffffffffffffffff82111761030757604052565b90601f8019910116810190811067ffffffffffffffff82111761030757604052565b6040519061038a8261033f565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b67ffffffffffffffff811161030757601f01601f191660200190565b3d156103f9573d906103e0826103b3565b916103ee604051938461035b565b82523d5f602084013e565b606090565b81601f820112156102cd57805190610415826103b3565b92610423604051948561035b565b828452602083830101116102cd57815f9260208093018386015e8301015290565b5f918291602082519201905afa6104596103cf565b90156105285761046881610772565b61053f5760208151036105285760200151905f5b602081108061050c575b156104935760010161047c565b9161049d836103b3565b926104ab604051948561035b565b808452601f196104ba826103b3565b013660208601375f5b8181106104d35750505060019190565b60208110156104f85784518110156104f85780836001921a60208288010153016104c3565b634e487b7160e01b5f52603260045260245ffd5b156104f85782811a60f81b6001600160f81b0319161515610486565b505f9060405161053960208261035b565b5f815290565b80518101906020818303126102cd5760208101519167ffffffffffffffff83116102cd576105749260208092019201016103fe565b600191565b5f918291602082519201905afa61058e6103cf565b901580156105bf575b6105b857602001519060ff82116105b15760ff6001921690565b5f91508190565b505f905f90565b50602081511415610597565b5f806105d561037d565b9260405160208101906342580cb760e11b8252600481526105f760248261035b565b51915afa906106046103cf565b91158015610762575b61075b57508051810160e082602083019203126102cd5760208201516001600160f81b0319811692908390036102cd57604081015167ffffffffffffffff81116102cd57826020610660928401016103fe565b606082015167ffffffffffffffff81116102cd57836020610683928501016103fe565b608083015160a08401516001600160a01b03811693919291908490036102cd5760c08501519460e08101519067ffffffffffffffff82116102cd57019580603f880112156102cd5760208701519667ffffffffffffffff8811610307578760051b90604051986106f6602084018b61035b565b8952602080808b0193830101019283116102cd57604001905b82821061074b57505050604051966107268861033f565b8752602087015260408601526060850152608084015260a083015260c0820152600191565b815181526020918201910161070f565b5f92909150565b5061076c8261079f565b1561060d565b604081511061079a5760208101516020810361079457610791916107fd565b90565b50505f90565b505f90565b60e081511061079a57602081015160081b61079a5760a081015160a01c61079a57604081015160608201516107d960e08401519284610823565b156107f6576107e89083610823565b156107945761079191610832565b5050505f90565b80519061080c6020848461085b565b156107f657820160200151919003601f1901101590565b80519061080c60e0848461085b565b80519061084160e0848461085b565b156107f657820160200151919003601f190160051c101590565b909182108015610873575b61079457601f1901101590565b50601f8216151561086656fea2646970667358221220435fb6a1dbeb66dccc928b0ed9c2dec64283566ad336124d8fa31e5ecf03b89464736f6c634300081b0033"; + "0x608080604052346015576108b5908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63287861f914610025575f80fd5b346102cd5760403660031901126102cd576004356001600160a01b03811691908290036102cd576024359182151583036102cd57610100820182811067ffffffffffffffff821117610307576040525f8252602082015f8152604083019160608352606084015f8152608085016060815260a08601945f865260c08701946100ab61037d565b865260e08801985f8a526100dd6040516306fdde0360e01b6020820152600481526100d760248261035b565b87610444565b906102fb575b506040516395d89b4160e01b602082015260048152610107906100d760248261035b565b906102ef575b5060405163313ce56760e01b6020820152600481526101379061013160248261035b565b87610579565b906102e4575b5061026d575b90610185916101546101a0966105cb565b90610261575b506040519860208a525160208a0152511515604089015251610100606089015261012088019061031b565b91511515608087015251858203601f190160a087015261031b565b915160c08401525192601f198383030160e084015260ff60f81b845116825260c06101ef6101dd602087015160e0602087015260e086019061031b565b6040870151858203604087015261031b565b946060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c08186039101526020808351958681520192015f945b8086106102495750508293505115156101008301520390f35b90926020806001928651815201940195019490610230565b60018b5287525f61015a565b6040516301afd7c160e11b815294602086600481845afa9586156102d9575f9661029b575b50948752610143565b955091906020863d6020116102d1575b816102b86020938361035b565b810103126102cd579451949091610154610292565b5f80fd5b3d91506102ab565b6040513d5f823e3d90fd5b60ff1689525f61013d565b6001835283525f61010d565b6001865284525f6100e3565b634e487b7160e01b5f52604160045260245ffd5b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b60e0810190811067ffffffffffffffff82111761030757604052565b90601f8019910116810190811067ffffffffffffffff82111761030757604052565b6040519061038a8261033f565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b67ffffffffffffffff811161030757601f01601f191660200190565b3d156103f9573d906103e0826103b3565b916103ee604051938461035b565b82523d5f602084013e565b606090565b81601f820112156102cd57805190610415826103b3565b92610423604051948561035b565b828452602083830101116102cd57815f9260208093018386015e8301015290565b5f918291602082519201905afa6104596103cf565b90156105285761046881610772565b61053f5760208151036105285760200151905f5b602081108061050c575b156104935760010161047c565b9161049d836103b3565b926104ab604051948561035b565b808452601f196104ba826103b3565b013660208601375f5b8181106104d35750505060019190565b60208110156104f85784518110156104f85780836001921a60208288010153016104c3565b634e487b7160e01b5f52603260045260245ffd5b156104f85782811a60f81b6001600160f81b0319161515610486565b505f9060405161053960208261035b565b5f815290565b80518101906020818303126102cd5760208101519167ffffffffffffffff83116102cd576105749260208092019201016103fe565b600191565b5f918291602082519201905afa61058e6103cf565b901580156105bf575b6105b857602001519060ff82116105b15760ff6001921690565b5f91508190565b505f905f90565b50602081511415610597565b5f806105d561037d565b9260405160208101906342580cb760e11b8252600481526105f760248261035b565b51915afa906106046103cf565b91158015610762575b61075b57508051810160e082602083019203126102cd5760208201516001600160f81b0319811692908390036102cd57604081015167ffffffffffffffff81116102cd57826020610660928401016103fe565b606082015167ffffffffffffffff81116102cd57836020610683928501016103fe565b608083015160a08401516001600160a01b03811693919291908490036102cd5760c08501519460e08101519067ffffffffffffffff82116102cd57019580603f880112156102cd5760208701519667ffffffffffffffff8811610307578760051b90604051986106f6602084018b61035b565b8952602080808b0193830101019283116102cd57604001905b82821061074b57505050604051966107268861033f565b8752602087015260408601526060850152608084015260a083015260c0820152600191565b815181526020918201910161070f565b5f92909150565b5061076c8261079f565b1561060d565b604081511061079a5760208101516020810361079457610791916107fd565b90565b50505f90565b505f90565b60e081511061079a57602081015160081b61079a5760a081015160a01c61079a57604081015160608201516107d960e08401519284610823565b156107f6576107e89083610823565b156107945761079191610832565b5050505f90565b80519061080c6020848461085b565b156107f657820160200151919003601f1901101590565b80519061080c60e0848461085b565b80519061084160e0848461085b565b156107f657820160200151919003601f190160051c101590565b909182108015610873575b61079457601f1901101590565b50601f8216151561086656fea264697066735822122001e988100482fa1cba104f82ee731d109995554ba9278f9f8954f6cd9bd6d46f64736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/GetVault.ts b/packages/blue-sdk-viem/src/queries/GetVault.ts index 892d776f8..87846db97 100644 --- a/packages/blue-sdk-viem/src/queries/GetVault.ts +++ b/packages/blue-sdk-viem/src/queries/GetVault.ts @@ -256,4 +256,4 @@ export const abi = [ /** @internal Deployless `GetVault` query bytecode. */ export const code = - "0x60808060405234601557611638908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63c93eac5414610024575f80fd5b34610bb7576060366003190112610bb7576004356001600160a01b0381168103610bb7576024356001600160a01b0381168103610bb7576044356001600160a01b0381168103610bb7576102e0604052604051610080816113bd565b5f815260606020820152606060408201525f60608201525f60808201526100a561144a565b60a08201526080525f6020608001525f6040608001525f6060608001525f60808001526040516100d48161140e565b5f8082526020820152610120526040516100ed8161140e565b5f80825260208201819052610140919091526101608190526101808190526101a08190526101c08190526101e08190526102008190526102208190526102408190526102605260606102808190526102a05260405161014b816113d8565b5f81525f60208201525f604082015261024060800152604051630a6d4d4b60e21b815260018060a01b038416600482015260208160248160018060a01b0386165afa908115610bc3575f91611333575b5015611281575b506040516338d52e0f60e01b81526020816004816001600160a01b0387165afa908115610bc3575f91611247575b506040516395d89b4160e01b81525f816004816001600160a01b0388165afa908115610bc3575f9161122d575b506040516306fdde0360e01b81525f816004816001600160a01b0389165afa908115610bc3575f9161120b575b5060405163313ce56760e01b8152906020826004816001600160a01b038a165afa918215610bc3575f926111ea575b50604051632ba9c2b360e21b8152926020846004816001600160a01b038b165afa938415610bc3575f946111b9575b5061029161144a565b505f8060405160208101906342580cb760e11b8252600481526102b5602482611429565b51906001600160a01b038b165afa6102cb611562565b901561117e57805181019060e08160208401930312610bb75760208101516001600160f81b031981168103610bb75760408201516001600160401b038111610bb75783602061031c928501016114c7565b60608301516001600160401b038111610bb75784602061033e928601016114c7565b608084015160a0850151939092906001600160a01b0385168503610bb75760c08601519560e0810151976001600160401b038911610bb75780603f8a8401011215610bb757602089830101519161039483611591565b996103a26040519b8c611429565b838b5260208b01926040838301600587901b010111610bb757604081830101925b6040838301600587901b0101841061116e575050505050926103f69a98959260ff9a9794928b9996936040519d8e6113f3565b8a60f81b168d5260208d015260408c015260608b015260018060a01b031660808a015260a089015260c088015260405197610430896113bd565b60018060a01b031688526020880152604087015216606085015216608083015260a0820152608052604051638da5cb5b60e01b815260208160048160018060a01b0387165afa908115610bc3575f91611134575b506001600160a01b0390811660a05260405163e66f53b760e01b8152906020908290600490829087165afa908115610bc3575f916110fa575b506001600160a01b0390811660c052604051630229549960e51b8152906020908290600490829087165afa908115610bc3575f916110c0575b506001600160a01b0390811660e0526040516334cc866d60e21b8152906020908290600490829087165afa908115610bc3575f9161108e575b506101005260408051637cc4d9a160e01b815290816004816001600160a01b0387165afa908115610bc3575f9161102b575b506101205260408051633b1618dd60e11b815290816004816001600160a01b0387165afa908115610bc3575f91610fd2575b5061014052604051631c61872f60e31b81526020816004816001600160a01b0387165afa908115610bc3575f91610f98575b506001600160a01b039081166101605260405163ddca3f4360e01b8152906020908290600490829087165afa8015610bc3575f90610f4f575b6001600160601b0316610180525060405163011a412160e61b81526020816004816001600160a01b0387165afa908115610bc3575f91610f15575b506001600160a01b039081166101a05260405163388af5b560e01b8152906020908290600490829087165afa908115610bc3575f91610edb575b506001600160a01b039081166101c0526040516318160ddd60e01b8152906020908290600490829087165afa908115610bc3575f91610ea9575b506101e0526040516278744560e21b81526020816004816001600160a01b0387165afa908115610bc3575f91610e77575b506102005260405163568efc0760e01b81526020816004816001600160a01b0387165afa908115610bc3575f91610e45575b5061022052604051630872d2c560e21b60208201908152600482525f9182919061073b602482611429565b51906001600160a01b0386165afa610751611562565b9080610e39575b610e14575b50604051630a17b31360e41b81526020816004816001600160a01b0387165afa908115610bc3575f91610de2575b50610795816115a8565b610280525f5b818110610d615750506040516333f91ebb60e01b81526020816004816001600160a01b0387165afa908115610bc3575f91610d2f575b506107db816115a8565b6102a0525f5b818110610cae5750506001600160a01b038116151580610c3f575b610aaa575b505060405160208152806080516102e0602083015260018060a01b0381511661030083015260a061085e610846602084015160c06103208701526103c0860190611366565b60408401518582036102ff1901610340870152611366565b916060810151610360850152608081015161038085015201516102ff19838303016103a084015260ff60f81b815116825260c06108bf6108ad602084015160e0602087015260e0860190611366565b60408401518582036040870152611366565b916060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c0818303910152602080835192838152019201905f5b818110610a9157505050610a4b610a619160018060a01b0360206080015116604085015260018060a01b0360406080015116606085015260018060a01b03606060800151166080850152608080015160a08501526001600160401b03602060a06080015160018060c01b0381511660c088015201511660e08501526001600160401b03602060c06080015160018060a01b0381511661010088015201511661012085015260018060a01b0360e060800151166101408501526101006080015161016085015260018060a01b03610120608001511661018085015260018060a01b0361014060800151166101a0850152610160608001516101c0850152610180608001516101e08501526101a0608001516102008501526101c06080015115156102208501526101e06080015161024085015261020060800151601f198583030161026086015261138a565b6102a051838203601f190161028085015261138a565b6102c0805180516001600160a01b03166102a0850152602081015191840191909152604001516102e08301520390f35b8251845285945060209384019390920191600101610900565b604051630c7508df60e31b81526001600160a01b0380841660048301529092906020908490602490829086165afa928315610bc3575f93610c03575b50604051636fcca69b60e01b81526001600160a01b0380831660048301529091906020908390602490829087165afa918215610bc3575f92610bce575b506040516348d88a5960e11b81526001600160a01b0391821660048201529260209184916024918391165afa918215610bc3575f92610b8b575b5060405192610b6b846113d8565b6001600160a01b03168352602083015260408201526102c0525f80610801565b9091506020813d602011610bbb575b81610ba760209383611429565b81010312610bb75751905f610b5d565b5f80fd5b3d9150610b9a565b6040513d5f823e3d90fd5b9091506020813d602011610bfb575b81610bea60209383611429565b81010312610bb75751906020610b23565b3d9150610bdd565b9092506020813d602011610c37575b81610c1f60209383611429565b81010312610bb757610c3090611498565b915f610ae6565b3d9150610c12565b506040516326f6f90760e11b81526001600160a01b0382811660048301526020908290602490829087165afa908115610bc3575f91610c7f575b506107fc565b610ca1915060203d602011610ca7575b610c998183611429565b810190611480565b5f610c79565b503d610c8f565b6040516362518ddf60e01b815260048101829052906020826024816001600160a01b0389165afa8015610bc3575f90610cfd575b60019250610cf682610220608001516115da565b52016107e1565b506020823d8211610d27575b81610d1660209383611429565b81010312610bb75760019151610ce2565b3d9150610d09565b90506020813d602011610d59575b81610d4a60209383611429565b81010312610bb757515f6107d1565b3d9150610d3d565b60405163f7d1852160e01b815260048101829052906020826024816001600160a01b0389165afa8015610bc3575f90610db0575b60019250610da982610200608001516115da565b520161079b565b506020823d8211610dda575b81610dc960209383611429565b81010312610bb75760019151610d95565b3d9150610dbc565b90506020813d602011610e0c575b81610dfd60209383611429565b81010312610bb757515f61078b565b3d9150610df0565b60016102405280516020828101929182019190910312610bb75751610260525f61075d565b50602081511015610758565b90506020813d602011610e6f575b81610e6060209383611429565b81010312610bb757515f610710565b3d9150610e53565b90506020813d602011610ea1575b81610e9260209383611429565b81010312610bb757515f6106de565b3d9150610e85565b90506020813d602011610ed3575b81610ec460209383611429565b81010312610bb757515f6106ad565b3d9150610eb7565b90506020813d602011610f0d575b81610ef660209383611429565b81010312610bb757610f0790611498565b5f610673565b3d9150610ee9565b90506020813d602011610f47575b81610f3060209383611429565b81010312610bb757610f4190611498565b5f610639565b3d9150610f23565b506020813d602011610f90575b81610f6960209383611429565b81010312610bb757516001600160601b0381168103610bb7576001600160601b03906105fe565b3d9150610f5c565b90506020813d602011610fca575b81610fb360209383611429565b81010312610bb757610fc490611498565b5f6105c5565b3d9150610fa6565b90506040813d604011611023575b81610fed60409383611429565b81010312610bb7576110186020604051926110078461140e565b61101081611498565b84520161154e565b60208201525f610593565b3d9150610fe0565b90506040813d604011611086575b8161104660409383611429565b81010312610bb7576040519061105b8261140e565b80516001600160c01b0381168103610bb757825261107b9060200161154e565b60208201525f610561565b3d9150611039565b90506020813d6020116110b8575b816110a960209383611429565b81010312610bb757515f61052f565b3d915061109c565b90506020813d6020116110f2575b816110db60209383611429565b81010312610bb7576110ec90611498565b5f6104f6565b3d91506110ce565b90506020813d60201161112c575b8161111560209383611429565b81010312610bb75761112690611498565b5f6104bd565b3d9150611108565b90506020813d602011611166575b8161114f60209383611429565b81010312610bb75761116090611498565b5f610484565b3d9150611142565b83518152602093840193016103c3565b60405162461bcd60e51b8152602060048201526013602482015272195a5c0dcc4c911bdb585a5b8819985a5b1959606a1b6044820152606490fd5b6111dc91945060203d6020116111e3575b6111d48183611429565b810190611535565b925f610288565b503d6111ca565b61120491925060203d6020116111e3576111d48183611429565b905f610259565b61122791503d805f833e61121f8183611429565b81019061150d565b5f61022a565b61124191503d805f833e61121f8183611429565b5f6101fd565b90506020813d602011611279575b8161126260209383611429565b81010312610bb75761127390611498565b5f6101d0565b3d9150611255565b600146148015611328575b806112bb575b6101a25763634ba39d60e11b5f9081526001600160a01b03918216600452921660245250604490fd5b50604051630a6d4d4b60e21b81526001600160a01b038416600482015260208160248173a9c3d3a366466fa809d1ae982fb2c46e5fc411015afa908115610bc3575f91611309575b50611292565b611322915060203d602011610ca757610c998183611429565b5f611303565b50612105461461128c565b61134c915060203d602011610ca757610c998183611429565b5f61019b565b634e487b7160e01b5f52604160045260245ffd5b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106113a75750505090565b825184526020938401939092019160010161139a565b60c081019081106001600160401b0382111761135257604052565b606081019081106001600160401b0382111761135257604052565b60e081019081106001600160401b0382111761135257604052565b604081019081106001600160401b0382111761135257604052565b90601f801991011681019081106001600160401b0382111761135257604052565b60405190611457826113f3565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b90816020910312610bb757518015158103610bb75790565b51906001600160a01b0382168203610bb757565b6001600160401b03811161135257601f01601f191660200190565b81601f82011215610bb7578051906114de826114ac565b926114ec6040519485611429565b82845260208383010111610bb757815f9260208093018386015e8301015290565b90602082820312610bb75781516001600160401b038111610bb75761153292016114c7565b90565b90816020910312610bb7575160ff81168103610bb75790565b51906001600160401b0382168203610bb757565b3d1561158c573d90611573826114ac565b916115816040519384611429565b82523d5f602084013e565b606090565b6001600160401b0381116113525760051b60200190565b906115b282611591565b6115bf6040519182611429565b82815280926115d0601f1991611591565b0190602036910137565b80518210156115ee5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea26469706673582212208a840402e999073d07e228ab73197725911c16597958760b3632810f70e1ebd464736f6c63430008230033"; + "0x60808060405234601557611638908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63c93eac5414610024575f80fd5b34610bb7576060366003190112610bb7576004356001600160a01b0381168103610bb7576024356001600160a01b0381168103610bb7576044356001600160a01b0381168103610bb7576102e0604052604051610080816113bd565b5f815260606020820152606060408201525f60608201525f60808201526100a561144a565b60a08201526080525f6020608001525f6040608001525f6060608001525f60808001526040516100d48161140e565b5f8082526020820152610120526040516100ed8161140e565b5f80825260208201819052610140919091526101608190526101808190526101a08190526101c08190526101e08190526102008190526102208190526102408190526102605260606102808190526102a05260405161014b816113d8565b5f81525f60208201525f604082015261024060800152604051630a6d4d4b60e21b815260018060a01b038416600482015260208160248160018060a01b0386165afa908115610bc3575f91611333575b5015611281575b506040516338d52e0f60e01b81526020816004816001600160a01b0387165afa908115610bc3575f91611247575b506040516395d89b4160e01b81525f816004816001600160a01b0388165afa908115610bc3575f9161122d575b506040516306fdde0360e01b81525f816004816001600160a01b0389165afa908115610bc3575f9161120b575b5060405163313ce56760e01b8152906020826004816001600160a01b038a165afa918215610bc3575f926111ea575b50604051632ba9c2b360e21b8152926020846004816001600160a01b038b165afa938415610bc3575f946111b9575b5061029161144a565b505f8060405160208101906342580cb760e11b8252600481526102b5602482611429565b51906001600160a01b038b165afa6102cb611562565b901561117e57805181019060e08160208401930312610bb75760208101516001600160f81b031981168103610bb75760408201516001600160401b038111610bb75783602061031c928501016114c7565b60608301516001600160401b038111610bb75784602061033e928601016114c7565b608084015160a0850151939092906001600160a01b0385168503610bb75760c08601519560e0810151976001600160401b038911610bb75780603f8a8401011215610bb757602089830101519161039483611591565b996103a26040519b8c611429565b838b5260208b01926040838301600587901b010111610bb757604081830101925b6040838301600587901b0101841061116e575050505050926103f69a98959260ff9a9794928b9996936040519d8e6113f3565b8a60f81b168d5260208d015260408c015260608b015260018060a01b031660808a015260a089015260c088015260405197610430896113bd565b60018060a01b031688526020880152604087015216606085015216608083015260a0820152608052604051638da5cb5b60e01b815260208160048160018060a01b0387165afa908115610bc3575f91611134575b506001600160a01b0390811660a05260405163e66f53b760e01b8152906020908290600490829087165afa908115610bc3575f916110fa575b506001600160a01b0390811660c052604051630229549960e51b8152906020908290600490829087165afa908115610bc3575f916110c0575b506001600160a01b0390811660e0526040516334cc866d60e21b8152906020908290600490829087165afa908115610bc3575f9161108e575b506101005260408051637cc4d9a160e01b815290816004816001600160a01b0387165afa908115610bc3575f9161102b575b506101205260408051633b1618dd60e11b815290816004816001600160a01b0387165afa908115610bc3575f91610fd2575b5061014052604051631c61872f60e31b81526020816004816001600160a01b0387165afa908115610bc3575f91610f98575b506001600160a01b039081166101605260405163ddca3f4360e01b8152906020908290600490829087165afa8015610bc3575f90610f4f575b6001600160601b0316610180525060405163011a412160e61b81526020816004816001600160a01b0387165afa908115610bc3575f91610f15575b506001600160a01b039081166101a05260405163388af5b560e01b8152906020908290600490829087165afa908115610bc3575f91610edb575b506001600160a01b039081166101c0526040516318160ddd60e01b8152906020908290600490829087165afa908115610bc3575f91610ea9575b506101e0526040516278744560e21b81526020816004816001600160a01b0387165afa908115610bc3575f91610e77575b506102005260405163568efc0760e01b81526020816004816001600160a01b0387165afa908115610bc3575f91610e45575b5061022052604051630872d2c560e21b60208201908152600482525f9182919061073b602482611429565b51906001600160a01b0386165afa610751611562565b9080610e39575b610e14575b50604051630a17b31360e41b81526020816004816001600160a01b0387165afa908115610bc3575f91610de2575b50610795816115a8565b610280525f5b818110610d615750506040516333f91ebb60e01b81526020816004816001600160a01b0387165afa908115610bc3575f91610d2f575b506107db816115a8565b6102a0525f5b818110610cae5750506001600160a01b038116151580610c3f575b610aaa575b505060405160208152806080516102e0602083015260018060a01b0381511661030083015260a061085e610846602084015160c06103208701526103c0860190611366565b60408401518582036102ff1901610340870152611366565b916060810151610360850152608081015161038085015201516102ff19838303016103a084015260ff60f81b815116825260c06108bf6108ad602084015160e0602087015260e0860190611366565b60408401518582036040870152611366565b916060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c0818303910152602080835192838152019201905f5b818110610a9157505050610a4b610a619160018060a01b0360206080015116604085015260018060a01b0360406080015116606085015260018060a01b03606060800151166080850152608080015160a08501526001600160401b03602060a06080015160018060c01b0381511660c088015201511660e08501526001600160401b03602060c06080015160018060a01b0381511661010088015201511661012085015260018060a01b0360e060800151166101408501526101006080015161016085015260018060a01b03610120608001511661018085015260018060a01b0361014060800151166101a0850152610160608001516101c0850152610180608001516101e08501526101a0608001516102008501526101c06080015115156102208501526101e06080015161024085015261020060800151601f198583030161026086015261138a565b6102a051838203601f190161028085015261138a565b6102c0805180516001600160a01b03166102a0850152602081015191840191909152604001516102e08301520390f35b8251845285945060209384019390920191600101610900565b604051630c7508df60e31b81526001600160a01b0380841660048301529092906020908490602490829086165afa928315610bc3575f93610c03575b50604051636fcca69b60e01b81526001600160a01b0380831660048301529091906020908390602490829087165afa918215610bc3575f92610bce575b506040516348d88a5960e11b81526001600160a01b0391821660048201529260209184916024918391165afa918215610bc3575f92610b8b575b5060405192610b6b846113d8565b6001600160a01b03168352602083015260408201526102c0525f80610801565b9091506020813d602011610bbb575b81610ba760209383611429565b81010312610bb75751905f610b5d565b5f80fd5b3d9150610b9a565b6040513d5f823e3d90fd5b9091506020813d602011610bfb575b81610bea60209383611429565b81010312610bb75751906020610b23565b3d9150610bdd565b9092506020813d602011610c37575b81610c1f60209383611429565b81010312610bb757610c3090611498565b915f610ae6565b3d9150610c12565b506040516326f6f90760e11b81526001600160a01b0382811660048301526020908290602490829087165afa908115610bc3575f91610c7f575b506107fc565b610ca1915060203d602011610ca7575b610c998183611429565b810190611480565b5f610c79565b503d610c8f565b6040516362518ddf60e01b815260048101829052906020826024816001600160a01b0389165afa8015610bc3575f90610cfd575b60019250610cf682610220608001516115da565b52016107e1565b506020823d8211610d27575b81610d1660209383611429565b81010312610bb75760019151610ce2565b3d9150610d09565b90506020813d602011610d59575b81610d4a60209383611429565b81010312610bb757515f6107d1565b3d9150610d3d565b60405163f7d1852160e01b815260048101829052906020826024816001600160a01b0389165afa8015610bc3575f90610db0575b60019250610da982610200608001516115da565b520161079b565b506020823d8211610dda575b81610dc960209383611429565b81010312610bb75760019151610d95565b3d9150610dbc565b90506020813d602011610e0c575b81610dfd60209383611429565b81010312610bb757515f61078b565b3d9150610df0565b60016102405280516020828101929182019190910312610bb75751610260525f61075d565b50602081511015610758565b90506020813d602011610e6f575b81610e6060209383611429565b81010312610bb757515f610710565b3d9150610e53565b90506020813d602011610ea1575b81610e9260209383611429565b81010312610bb757515f6106de565b3d9150610e85565b90506020813d602011610ed3575b81610ec460209383611429565b81010312610bb757515f6106ad565b3d9150610eb7565b90506020813d602011610f0d575b81610ef660209383611429565b81010312610bb757610f0790611498565b5f610673565b3d9150610ee9565b90506020813d602011610f47575b81610f3060209383611429565b81010312610bb757610f4190611498565b5f610639565b3d9150610f23565b506020813d602011610f90575b81610f6960209383611429565b81010312610bb757516001600160601b0381168103610bb7576001600160601b03906105fe565b3d9150610f5c565b90506020813d602011610fca575b81610fb360209383611429565b81010312610bb757610fc490611498565b5f6105c5565b3d9150610fa6565b90506040813d604011611023575b81610fed60409383611429565b81010312610bb7576110186020604051926110078461140e565b61101081611498565b84520161154e565b60208201525f610593565b3d9150610fe0565b90506040813d604011611086575b8161104660409383611429565b81010312610bb7576040519061105b8261140e565b80516001600160c01b0381168103610bb757825261107b9060200161154e565b60208201525f610561565b3d9150611039565b90506020813d6020116110b8575b816110a960209383611429565b81010312610bb757515f61052f565b3d915061109c565b90506020813d6020116110f2575b816110db60209383611429565b81010312610bb7576110ec90611498565b5f6104f6565b3d91506110ce565b90506020813d60201161112c575b8161111560209383611429565b81010312610bb75761112690611498565b5f6104bd565b3d9150611108565b90506020813d602011611166575b8161114f60209383611429565b81010312610bb75761116090611498565b5f610484565b3d9150611142565b83518152602093840193016103c3565b60405162461bcd60e51b8152602060048201526013602482015272195a5c0dcc4c911bdb585a5b8819985a5b1959606a1b6044820152606490fd5b6111dc91945060203d6020116111e3575b6111d48183611429565b810190611535565b925f610288565b503d6111ca565b61120491925060203d6020116111e3576111d48183611429565b905f610259565b61122791503d805f833e61121f8183611429565b81019061150d565b5f61022a565b61124191503d805f833e61121f8183611429565b5f6101fd565b90506020813d602011611279575b8161126260209383611429565b81010312610bb75761127390611498565b5f6101d0565b3d9150611255565b600146148015611328575b806112bb575b6101a25763634ba39d60e11b5f9081526001600160a01b03918216600452921660245250604490fd5b50604051630a6d4d4b60e21b81526001600160a01b038416600482015260208160248173a9c3d3a366466fa809d1ae982fb2c46e5fc411015afa908115610bc3575f91611309575b50611292565b611322915060203d602011610ca757610c998183611429565b5f611303565b50612105461461128c565b61134c915060203d602011610ca757610c998183611429565b5f61019b565b634e487b7160e01b5f52604160045260245ffd5b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106113a75750505090565b825184526020938401939092019160010161139a565b60c081019081106001600160401b0382111761135257604052565b606081019081106001600160401b0382111761135257604052565b60e081019081106001600160401b0382111761135257604052565b604081019081106001600160401b0382111761135257604052565b90601f801991011681019081106001600160401b0382111761135257604052565b60405190611457826113f3565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b90816020910312610bb757518015158103610bb75790565b51906001600160a01b0382168203610bb757565b6001600160401b03811161135257601f01601f191660200190565b81601f82011215610bb7578051906114de826114ac565b926114ec6040519485611429565b82845260208383010111610bb757815f9260208093018386015e8301015290565b90602082820312610bb75781516001600160401b038111610bb75761153292016114c7565b90565b90816020910312610bb7575160ff81168103610bb75790565b51906001600160401b0382168203610bb757565b3d1561158c573d90611573826114ac565b916115816040519384611429565b82523d5f602084013e565b606090565b6001600160401b0381116113525760051b60200190565b906115b282611591565b6115bf6040519182611429565b82815280926115d0601f1991611591565b0190602036910137565b80518210156115ee5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea264697066735822122098628d5ddcc8838da003b780c240384c1be843b6d03b8205462eb3e49247d83864736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/GetVaultUser.ts b/packages/blue-sdk-viem/src/queries/GetVaultUser.ts index 9d4a01e32..5929bd92a 100644 --- a/packages/blue-sdk-viem/src/queries/GetVaultUser.ts +++ b/packages/blue-sdk-viem/src/queries/GetVaultUser.ts @@ -40,4 +40,4 @@ export const abi = [ /** @internal Deployless `GetVaultUser` query bytecode. */ export const code = - "0x6080806040523460155761025b908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610156576040366003190112610156576004356001600160a01b0381169190829003610156576024356001600160a01b0381169290839003610156576040820182811067ffffffffffffffff8211176101ef576040525f825260208201905f82526040516326f6f90760e11b8152846004820152602081602481855afa908115610162575f916101b4575b50151583526040516338d52e0f60e01b815293602085600481855afa948515610162575f9561016d575b509060446020926040519687938492636eb1769f60e11b84526004840152602483015260018060a01b03165afa8015610162575f9061012b575b6040935081528251915115158252516020820152f35b506020833d60201161015a575b8161014560209383610203565b810103126101565760409251610115565b5f80fd5b3d9150610138565b6040513d5f823e3d90fd5b9094506020813d6020116101ac575b8161018960209383610203565b810103126101565751906001600160a01b038216820361015657909360446100db565b3d915061017c565b90506020813d6020116101e7575b816101cf60209383610203565b8101031261015657518015158103610156575f6100b1565b3d91506101c2565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff8211176101ef5760405256fea2646970667358221220dab07071235db46cf4ce3e33469d528ea840db8713e312abcc6046560cb9b95d64736f6c634300081b0033"; + "0x6080806040523460155761025b908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610156576040366003190112610156576004356001600160a01b0381169190829003610156576024356001600160a01b0381169290839003610156576040820182811067ffffffffffffffff8211176101ef576040525f825260208201905f82526040516326f6f90760e11b8152846004820152602081602481855afa908115610162575f916101b4575b50151583526040516338d52e0f60e01b815293602085600481855afa948515610162575f9561016d575b509060446020926040519687938492636eb1769f60e11b84526004840152602483015260018060a01b03165afa8015610162575f9061012b575b6040935081528251915115158252516020820152f35b506020833d60201161015a575b8161014560209383610203565b810103126101565760409251610115565b5f80fd5b3d9150610138565b6040513d5f823e3d90fd5b9094506020813d6020116101ac575b8161018960209383610203565b810103126101565751906001600160a01b038216820361015657909360446100db565b3d915061017c565b90506020813d6020116101e7575b816101cf60209383610203565b8101031261015657518015158103610156575f6100b1565b3d91506101c2565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff8211176101ef5760405256fea2646970667358221220a5cf3b5661ba130f9f6beb24e7b3e584d9ba455572013775bb30810d2238ce5a64736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetAccrualVaultV2.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetAccrualVaultV2.ts index d23f06e5c..6cff6972a 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetAccrualVaultV2.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetAccrualVaultV2.ts @@ -1588,4 +1588,4 @@ export const abi = [ /** @internal Deployless `GetAccrualVaultV2` query bytecode. */ export const code = - "0x60808060405234601557613cde908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c630f0d54d814610024575f80fd5b34610afb57610100366003190112610afb576004356001600160a01b0381169003610afb576024356001600160a01b0381168103610afb576044356001600160a01b0381169003610afb576064356001600160a01b0381169003610afb576084356001600160a01b0381169003610afb5760a4356001600160a01b0381169003610afb5760c4356001600160a01b0381169003610afb5760e4356001600160a01b0381169003610afb576103206040526040516100e081611a96565b5f8082526060602083018190526040830181905280830182905260809290925260a081905260c081905260e08190526101008190526101208190526101408190526101608190526101808290526101a08190526101c0919091526101e08190526102008190526102208190526102408190526102608190526102808190526102a08190526102c052610170611cae565b6102e052606061030052604051635edec50d60e01b81526001600160a01b03600480358216908301526020908290602490829086165afa908115610b07575f9161145b575b5015611433576040516338d52e0f60e01b815260208160048181356001600160a01b03165afa908115610b07575f916113f9575b506040516395d89b4160e01b81525f8160048181356001600160a01b03165afa908115610b07575f916113df575b506040516306fdde0360e01b8152905f8260048181356001600160a01b03165afa918215610b07575f926113bb575b5060405163313ce56760e01b81529160208360048181356001600160a01b03165afa918215610b075760ff935f9361138a575b506040519461028786611a96565b60018060a01b03168552602085015260408401521660608201526080526040516338d52e0f60e01b815260208160048160018060a01b038235165afa908115610b07575f91611350575b506001600160a01b0390811660a05260405163ce04bebb60e01b815290602090829060049082908235165afa8015610b07575f90611310575b6001600160801b031660c052506040516318160ddd60e01b815260208160048181356001600160a01b03165afa908115610b07575f916112de575b5060e0526040516331c6651b60e21b815260208160048181356001600160a01b03165afa908115610b07575f916112ac575b506101005260405163ece1d6e560e01b815260208160048181356001600160a01b03165afa8015610b07575f9061126c575b6001600160401b0316610120525060405163c046371160e01b815260208160048181356001600160a01b03165afa8015610b07575f9061122c575b6001600160401b0316610140525060405163ad468d1160e01b815260208160048181356001600160a01b03165afa908115610b07575f916111f2575b506001600160a01b03908116610160526040516305c0524560e31b8152905f90829060049082908235165afa908115610b07575f916111a2575b50610180526040516343bc43c160e11b815260208160048181356001600160a01b03165afa8015610b07576001600160601b03915f91611183575b50166101e05260405163537bfaeb60e11b815260208160048181356001600160a01b03165afa8015610b07576001600160601b03915f91611154575b50166102005260405163ed27f7c960e01b815260208160048181356001600160a01b03165afa908115610b07575f9161111a575b506001600160a01b03908116610220526040516306d9a30160e41b815290602090829060049082908235165afa908115610b07575f916110e0575b506001600160a01b0316610240526101e0516001600160601b0316156110d957610220516040516326326d2760e21b81526001600160a01b039182166004808301919091529091602091839160249183919035165afa908115610b07575f9161109f575b505b151561026052610200516001600160601b03161561109857610240516040516326326d2760e21b81526001600160a01b039182166004808301919091529091602091839160249183919035165afa908115610b07575f9161105e575b505b15156102805260a0516040516370a0823160e01b81526001600160a01b0360048035821690830152909160209183916024918391165afa908115610b07575f9161102c575b506102a0526044356001600160a01b0316151580610fab575b6084356001600160a01b031615159081610f28575b8080610f1b575b610ef857808115610ef1575b15156101a05215610d38575060408051906106a58183611b38565b600182525f5b601f1982018110610cf55750506101406080015261073f60018060a01b0360e0608001511660405160208101916040835260046060830152637468697360e01b608083015260408201526080815261070460a082611b38565b5190206040519061071482611a96565b81525f60208201525f60408201525f6060820152610140608001519061073982611e49565b52611e49565b505b6101c051515f5b818110610b8f57610160516001600160a01b031680610b5b575b50604051630b54457960e31b815260208160048181356001600160a01b03165afa908115610b07575f91610b29575b5061079b81611e32565b6107a86040519182611b38565b818152601f196107b783611e32565b015f5b818110610b12575050610300525f5b818110610a425760405160208152806108b96080516102a0602084015260018060a01b038151166102c0840152606061082e610816602084015160806102e0880152610340870190611495565b60408401518682036102bf1901610300880152611495565b91015161032084015260a080516001600160a01b03908116604086015260c080516001600160801b0316606087015260e08051608088015261010080519488019490945261012080516001600160401b03908116938901939093526101405190921690870152610160519091169185019190915261018051848303601f190191850191909152611495565b6101a05115156101408301526101c051828203601f19016101608401528051808352602092830192909101905f5b818110610a085750505061099e906001600160601b0361016060800151166101808401526001600160601b0361018060800151166101a084015260018060a01b036101a060800151166101c084015260018060a01b036101c060800151166101e08401526101e06080015115156102008401526102006080015115156102208401526102206080015161024084015261024060800151151561026084015261026060800151601f19848303016102808501526115ac565b61030051601f19838303016102a084015280518083526020600582901b8401810193928101925f918101905b8383106109d75786860387f35b9193955091936020806109f6600193601f1986820301875289516115ac565b970193019301909286959492936109ca565b91935091602060806001926060875180518352848101518584015260408101516040840152015160608201520194019101918493926108e7565b604051906313bd406b60e21b825280600483015260208260248160018060a01b03600435165afa918215610b07575f92610abe575b50610ab781610aa260019460e4359060c4359060a43590608435906064359060443590600435612014565b6103005190610ab18383611e6a565b52611e6a565b50016107c9565b91506020823d8211610aff575b81610ad860209383611b38565b81010312610afb57610ab781610aa2610af2600195611d2b565b94505050610a77565b5f80fd5b3d9150610acb565b6040513d5f823e3d90fd5b602090610b1d611cae565b828286010152016107ba565b90506020813d602011610b53575b81610b4460209383611b38565b81010312610afb575181610791565b3d9150610b37565b60016102c052610b859060e4359060c4359060a43590608435906064359060443590600435612014565b6102e05280610762565b610b9f8161014060800151611e6a565b5190815160405190632f0374dd60e21b8252600482015260208160248160018060a01b03600435165afa908115610b07575f91610cc4575b50602083015281516040519063a68bafa360e01b8252600482015260208160248160018060a01b03600435165afa908115610b07575f91610c93575b5060408301528151916040519263c69507dd60e01b8452600484015260208360248160018060a01b03600435165afa928315610b07575f93610c5f575b50916060600193015201610748565b92506020833d8211610c8b575b81610c7960209383611b38565b81010312610afb579151916060610c50565b3d9150610c6c565b90506020813d8211610cbc575b81610cad60209383611b38565b81010312610afb575184610c13565b3d9150610ca0565b90506020813d8211610ced575b81610cde60209383611b38565b81010312610afb575184610bd7565b3d9150610cd1565b602090604051610d0481611a96565b5f81525f838201525f60408201525f6060820152828286010152016106ab565b634e487b7160e01b5f52604160045260245ffd5b1561074157610d566101006080015160208082518301019101611e7e565b6101605160405163cc3802bf60e01b8152915f91839160a49183916001600160a01b0390911690610d8b9060048401906114ec565b5afa908115610b07575f91610e61575b508051610da781611e32565b90610db56040519283611b38565b808252610dc4601f1991611e32565b015f5b818110610e325750506101c0525f5b8151811015610e2b5780610e2481610df060019486611e6a565b5160405190610dfe82611a96565b81525f60208201525f60408201525f60608201526101406080015190610ab18383611e6a565b5001610dd6565b5050610741565b602090604051610e4181611a96565b5f81525f838201525f60408201525f606082015282828601015201610dc7565b90503d805f833e610e728183611b38565b810190602081830312610afb578051906001600160401b038211610afb57019080601f83011215610afb578151610ea881611e32565b92610eb66040519485611b38565b81845260208085019260051b820101928311610afb57602001905b828210610ee15750505081610d9b565b8151815260209182019101610ed1565b508161068a565b61016051636364223f60e01b5f9081526001600160a01b03909116600452602490fd5b506101805151151561067e565b610160516040516335abafe560e21b81526001600160a01b03918216600482015291925060209082906024908290608435165afa908115610b07575f91610f71575b5090610677565b90506020813d602011610fa3575b81610f8c60209383611b38565b81010312610afb57610f9d90611d1e565b82610f6a565b3d9150610f7f565b5061016051604051632c77566560e01b81526001600160a01b0391821660048201529060209082906024908290604435165afa908115610b07575f91610ff2575b50610662565b90506020813d602011611024575b8161100d60209383611b38565b81010312610afb5761101e90611d1e565b81610fec565b3d9150611000565b90506020813d602011611056575b8161104760209383611b38565b81010312610afb575181610649565b3d915061103a565b90506020813d602011611090575b8161107960209383611b38565b81010312610afb5761108a90611d1e565b81610602565b3d915061106c565b6001610604565b90506020813d6020116110d1575b816110ba60209383611b38565b81010312610afb576110cb90611d1e565b816105a4565b3d91506110ad565b60016105a6565b90506020813d602011611112575b816110fb60209383611b38565b81010312610afb5761110c90611d2b565b81610540565b3d91506110ee565b90506020813d60201161114c575b8161113560209383611b38565b81010312610afb5761114690611d2b565b81610505565b3d9150611128565b611176915060203d60201161117c575b61116e8183611b38565b810190611e13565b826104d1565b503d611164565b61119c915060203d60201161117c5761116e8183611b38565b82610495565b90503d805f833e6111b38183611b38565b810190602081830312610afb578051906001600160401b038211610afb57019080601f83011215610afb5781516111ec92602001611d5a565b8161045a565b90506020813d602011611224575b8161120d60209383611b38565b81010312610afb5761121e90611d2b565b81610420565b3d9150611200565b506020813d602011611264575b8161124660209383611b38565b81010312610afb5761125f6001600160401b0391611dff565b6103e4565b3d9150611239565b506020813d6020116112a4575b8161128660209383611b38565b81010312610afb5761129f6001600160401b0391611dff565b6103a9565b3d9150611279565b90506020813d6020116112d6575b816112c760209383611b38565b81010312610afb575181610377565b3d91506112ba565b90506020813d602011611308575b816112f960209383611b38565b81010312610afb575181610345565b3d91506112ec565b506020813d602011611348575b8161132a60209383611b38565b81010312610afb576113436001600160801b0391611deb565b61030a565b3d915061131d565b90506020813d602011611382575b8161136b60209383611b38565b81010312610afb5761137c90611d2b565b816102d1565b3d915061135e565b6113ad91935060203d6020116113b4575b6113a58183611b38565b810190611dd2565b9185610279565b503d61139b565b6113d89192503d805f833e6113d08183611b38565b810190611dad565b9083610246565b6113f391503d805f833e6113d08183611b38565b82610217565b90506020813d60201161142b575b8161141460209383611b38565b81010312610afb5761142590611d2b565b816101e9565b3d9150611407565b63634ba39d60e11b5f9081526001600160a01b03918216600490815235909116602452604490fd5b90506020813d60201161148d575b8161147660209383611b38565b81010312610afb5761148790611d1e565b5f6101b5565b3d9150611469565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106114d65750505090565b82518452602093840193909201916001016114c9565b80516001600160a01b03908116835260208083015182169084015260408083015182169084015260608083015190911690830152608090810151910152565b60806101a09161153c8482516114ec565b6001600160801b0360a0602083015182815116828801528260208201511660c08801528260408201511660e0880152826060820151166101008801528285820151166101208801520151166101408501526040810151151561016085015260608101516101808501520151910152565b60018060a01b03815116825260ff602082015116602083015260018060a01b03604082015116604083015260018060a01b0360608201511660608301526080810151608083015260018060a01b0360a08201511660a083015260c08101519061018060c084015281516102e061018085015260018060a01b0381511661046085015260a061166661164e602084015160c0610480890152610520880190611495565b604084015187820361045f19016104a0890152611495565b9160608101516104c087015260808101516104e0870152015161045f198583030161050086015260ff60f81b815116825260c06116c76116b5602084015160e0602087015260e0860190611495565b60408401518582036040870152611495565b916060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c0818303910152602080835192838152019201905f5b818110611a80575050506020838101516001600160a01b039081166101a087015260408581015182166101c088015260608601519091166101e0870152608085015161020087015260a085015180516001600160c01b0316610220880152909101516001600160401b031661024086810191909152909161184c906118339060c087015180516001600160a01b039081166102608b01526020909101516001600160401b03166102808a015260e088015181166102a08a01526101008801516102c08a015261012088015181166102e08a0152610140880151166103008901526101608701516103208901526101808701516103408901526101a087015115156103608901526101c08701516103808901526101e087015161017f19898303016103a08a01526114b9565b61020086015187820361017f19016103c08901526114b9565b9361022081015115156103e0870152015160018060a01b038151166104008601526020810151610420860152015161044084015260e08101519183810360e0850152602080845192838152019301905f5b8181106119b65750505061010081015161010084015261012081015191838103610120850152602080845192838152019301905f5b818110611951575050506101609060018060a01b0361014082015116610140850152015191610160818303910152602080835192838152019201905f5b81811061191c5750505090565b9091926020610200600192611946604088518051845285810151868501520151604083019061152b565b01940192910161190f565b90919360206102c06001926119ab6040895161196e8482516114ec565b61199e8682015160a08601906001600160801b036040809280518552826020820151166020860152015116910152565b015161010083019061152b565b0195019291016118d2565b90919360206103006001926001600160801b0360e0895180518452858101511515868501526001600160401b036040820151166040850152611a1b606082015160608601906001600160401b036020809260018060c01b038151168552015116910152565b611a4c608082015160a08601906001600160801b036040809280518552826020820151166020860152015116910152565b611a5f60a082015161010086019061152b565b60c081015183166102c08501520151166102e082015201950192910161189d565b8251845260209384019390920191600101611708565b608081019081106001600160401b03821117610d2457604052565b60e081019081106001600160401b03821117610d2457604052565b604081019081106001600160401b03821117610d2457604052565b606081019081106001600160401b03821117610d2457604052565b60c081019081106001600160401b03821117610d2457604052565b60a081019081106001600160401b03821117610d2457604052565b90601f801991011681019081106001600160401b03821117610d2457604052565b60405190611b6682611ab1565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b60405190611b9c82611ae7565b5f6040838281528260208201520152565b6040519061026082018281106001600160401b03821117610d245760405281604051611bd881611b02565b5f815260606020820152606060408201525f60608201525f6080820152611bfd611b59565b60a082015281525f60208201525f60408201525f60608201525f6080820152604051611c2881611acc565b5f81525f602082015260a0820152604051611c4281611acc565b5f81525f602082015260c08201525f60e08201525f6101008201525f6101208201525f6101408201525f6101608201525f6101808201525f6101a08201525f6101c082015260606101e082015260606102008201525f610220820152610240611ca9611b8f565b910152565b6040519061018082018281106001600160401b03821117610d24576040526060610160835f81525f60208201525f60408201525f838201525f60808201525f60a0820152611cfa611bad565b60c08201528260e08201525f610100820152826101208201525f6101408201520152565b51908115158203610afb57565b51906001600160a01b0382168203610afb57565b6001600160401b038111610d2457601f01601f191660200190565b929192611d6682611d3f565b91611d746040519384611b38565b829481845281830111610afb578281602093845f96015e010152565b9080601f83011215610afb578151611daa92602001611d5a565b90565b90602082820312610afb5781516001600160401b038111610afb57611daa9201611d90565b90816020910312610afb575160ff81168103610afb5790565b51906001600160801b0382168203610afb57565b51906001600160401b0382168203610afb57565b90816020910312610afb57516001600160601b0381168103610afb5790565b6001600160401b038111610d245760051b60200190565b805115611e565760200190565b634e487b7160e01b5f52603260045260245ffd5b8051821015611e565760209160051b010190565b908160a0910312610afb57608060405191611e9883611b1d565b611ea181611d2b565b8352611eaf60208201611d2b565b6020840152611ec060408201611d2b565b6040840152611ed160608201611d2b565b60608401520151608082015290565b60405190611eed82611b1d565b5f6080838281528260208201528260408201528260608201520152565b60405190611f1782611b1d565b5f608083611f23611ee0565b8152604051611f3181611b02565b83815283602082015283604082015283606082015283838201528360a082015260208201528260408201528260608201520152565b90816060910312610afb57611f9d6040805192611f8284611ae7565b80518452611f9260208201611deb565b602085015201611deb565b604082015290565b6040519061010082018281106001600160401b03821117610d24576040525f60e083828152826020820152826040820152604051611fe281611acc565b8381528360208201526060820152611ff8611b8f565b6080820152612005611f0a565b60a08201528260c08201520152565b95939091979692612023611cae565b6001600160a01b038481168083526040516399e9918360e01b815260048101829052929b909990929160209183916024918391165afa908115610b07575f9161399d575b5060808b01526001600160a01b0316801515908161392e575b50156131dc57505050600160208701526040516307f1b29b60e11b8152602081600481885afa908115610b07575f916131a2575b506001600160a01b03166040878101919091525163388af5b560e01b8152602081600481885afa908115610b07575f91613168575b506001600160a01b0316606087015260405163e4baaddf60e01b815292602084600481885afa938415610b07575f9461312c575b506001600160a01b0390931660a08701818152939061213a611bad565b916040516338d52e0f60e01b8152602081600481865afa908115610b07575f916130f2575b506040516395d89b4160e01b81525f81600481875afa908115610b07575f916130d8575b506040516306fdde0360e01b81525f81600481885afa908115610b07575f916130be575b5060405163313ce56760e01b815290602082600481895afa918215610b07575f9261309d575b50604051632ba9c2b360e21b8152926020846004818a5afa938415610b07575f9461307c575b506121fc611b59565b505f8060405160208101906342580cb760e11b825260048152612220602482611b38565b51908a5afa61222d613c47565b901561304157805181019060e08160208401930312610afb5760208101516001600160f81b0319811690819003610afb5760408201516001600160401b038111610afb5783602061228092850101611d90565b60608301516001600160401b038111610afb578460206122a292860101611d90565b608084015160a08501516001600160a01b0381169491939190859003610afb5760c08601519560e0810151906001600160401b038211610afb57019680603f89011215610afb5760208801516122f781611e32565b986123056040519a8b611b38565b818a52602080808c019360051b83010101928311610afb57604001905b82821061303157505050926123489a98959260ff9a9794928b9996936040519d8e611ab1565b8d5260208d015260408c015260608b015260808a015260a089015260c08801526040519761237589611b02565b60018060a01b031688526020880152604087015216606085015216608083015260a08201528352604051638da5cb5b60e01b8152602081600481865afa908115610b07575f91612ff7575b506001600160a01b031660208481019190915260405163e66f53b760e01b81529081600481865afa908115610b07575f91612fbd575b506001600160a01b031660408481019190915251630229549960e51b8152602081600481865afa908115610b07575f91612f83575b506001600160a01b031660608401526040516334cc866d60e21b8152602081600481865afa908115610b07575f91612f51575b50608084015260408051637cc4d9a160e01b81529081600481865afa908115610b07575f91612f32575b5060a084015260408051633b1618dd60e11b81529081600481865afa908115610b07575f91612ed9575b5060c0840152604051631c61872f60e31b8152602081600481865afa908115610b07575f91612e9f575b506001600160a01b031660e084015260405163ddca3f4360e01b8152602081600481865afa8015610b07576001600160601b03915f91612e80575b501661010084015260405163011a412160e61b8152602081600481865afa908115610b07575f91612e46575b506001600160a01b031661012084015260405163388af5b560e01b8152602081600481865afa908115610b07575f91612e0c575b506001600160a01b03166101408401526040516318160ddd60e01b8152602081600481865afa908115610b07575f91612dda575b5061016084015260405163568efc0760e01b8152602081600481865afa908115610b07575f91612da8575b506101808401525f806040516020810190630872d2c560e21b825260048152612600602482611b38565b5190855afa61260d613c47565b9080612d9c575b612d73575b50604051630a17b31360e41b8152602081600481865afa908115610b07575f91612d41575b5061264881613c76565b6101e085019081525f5b828110612cce5750506040516333f91ebb60e01b8152949050602085600481865afa948515610b07575f95612c9a575b5061268c85613c76565b9461020085019586525f5b818110612c275750506001600160a01b0316801515949092908580612bbb575b612a61575b60c08b019485525151946126cf86611e32565b946126dd6040519687611b38565b868652601f196126ec88611e32565b015f5b818110612a4a57505060e08c019586525f5b87811061278757505096516040516370a0823160e01b8152600481019990995260209750889650602495508694506001600160a01b0316925050505afa908115610b07575f91612755575b50610100830152565b90506020813d60201161277f575b8161277060209383611b38565b81010312610afb57515f61274c565b3d9150612763565b6127978161020084510151611e6a565b51906127a1611fa5565b91604051636638c7bb60e11b81528160048201526060816024818a5afa908115610b07575f916129cd575b5080516001600160b81b031684526020808201511515908501526040908101516001600160401b031684820152805163518df2eb60e11b81526004810183905290816024818a5afa908115610b07575f9161299f575b506060848101919091526040516349e2903160e11b8152600481018390526001600160a01b03881660248201529081806044810103816001600160a01b038c165afa908115610b07575f91612971575b506080840152846128848c838a6139cf565b60a08501526128aa575b506128a3816001938a5190610ab18383611e6a565b5001612701565b9160405192639dbcd5b960e01b845286600485015260248401526040836044818b5afa928315610b07575f93612904575b5082516001600160801b0390811660c083015260209093015190921660e08301526128a361288e565b92506040833d8211612969575b8161291e60409383611b38565b81010312610afb57816128a3916001600160801b03602060019661295a826040519261294984611acc565b61295281611deb565b845201611deb565b828201529650505091506128db565b3d9150612911565b612992915060603d8111612998575b61298a8183611b38565b810190611f66565b5f612872565b503d612980565b6129c0915060403d81116129c6575b6129b88183611b38565b810190613c07565b5f612822565b503d6129ae565b90506060813d8211612a42575b816129e760609383611b38565b81010312610afb576040516129fb81611ae7565b8151906001600160b81b0382168203610afb57612a3760406001600160401b039481948452612a2c60208201611d1e565b602085015201611dff565b8282015291506127cc565b3d91506129da565b602090612a55611fa5565b82828b010152016126ef565b6001610220860152604051630c7508df60e31b815260048101839052602081602481885afa908115610b07575f91612b81575b50604051636fcca69b60e01b815260048101849052602081602481895afa908115610b07575f91612b4f575b506040516348d88a5960e11b815260048101859052906020826024818a5afa918215610b07575f92612b1b575b5060405192612afb84611ae7565b6001600160a01b03168352602083015260408201526102408601526126bc565b9091506020813d602011612b47575b81612b3760209383611b38565b81010312610afb5751905f612aed565b3d9150612b2a565b90506020813d602011612b79575b81612b6a60209383611b38565b81010312610afb57515f612ac0565b3d9150612b5d565b90506020813d602011612bb3575b81612b9c60209383611b38565b81010312610afb57612bad90611d2b565b5f612a94565b3d9150612b8f565b506040516326f6f90760e11b815260048101859052602081602481865afa908115610b07575f91612bed575b506126b7565b90506020813d602011612c1f575b81612c0860209383611b38565b81010312610afb57612c1990611d1e565b5f612be7565b3d9150612bfb565b6040516362518ddf60e01b81526004810182905290602082602481895afa8015610b07575f90612c68575b60019250612c61828a51611e6a565b5201612697565b506020823d8211612c92575b81612c8160209383611b38565b81010312610afb5760019151612c52565b3d9150612c74565b9094506020813d602011612cc6575b81612cb660209383611b38565b81010312610afb5751935f612682565b3d9150612ca9565b60405163f7d1852160e01b81526004810182905290602082602481895afa8015610b07575f90612d0f575b60019250612d08828551611e6a565b5201612652565b506020823d8211612d39575b81612d2860209383611b38565b81010312610afb5760019151612cf9565b3d9150612d1b565b90506020813d602011612d6b575b81612d5c60209383611b38565b81010312610afb57515f61263e565b3d9150612d4f565b60016101a085015260208151918180820193849201010312610afb57516101c08401525f612619565b50602081511015612614565b90506020813d602011612dd2575b81612dc360209383611b38565b81010312610afb57515f6125d6565b3d9150612db6565b90506020813d602011612e04575b81612df560209383611b38565b81010312610afb57515f6125ab565b3d9150612de8565b90506020813d602011612e3e575b81612e2760209383611b38565b81010312610afb57612e3890611d2b565b5f612577565b3d9150612e1a565b90506020813d602011612e78575b81612e6160209383611b38565b81010312610afb57612e7290611d2b565b5f612543565b3d9150612e54565b612e99915060203d60201161117c5761116e8183611b38565b5f612517565b90506020813d602011612ed1575b81612eba60209383611b38565b81010312610afb57612ecb90611d2b565b5f6124dc565b3d9150612ead565b90506040813d604011612f2a575b81612ef460409383611b38565b81010312610afb57612f1f602060405192612f0e84611acc565b612f1781611d2b565b845201611dff565b60208201525f6124b2565b3d9150612ee7565b612f4b915060403d6040116129c6576129b88183611b38565b5f612488565b90506020813d602011612f7b575b81612f6c60209383611b38565b81010312610afb57515f61245e565b3d9150612f5f565b90506020813d602011612fb5575b81612f9e60209383611b38565b81010312610afb57612faf90611d2b565b5f61242b565b3d9150612f91565b90506020813d602011612fef575b81612fd860209383611b38565b81010312610afb57612fe990611d2b565b5f6123f6565b3d9150612fcb565b90506020813d602011613029575b8161301260209383611b38565b81010312610afb5761302390611d2b565b5f6123c0565b3d9150613005565b8151815260209182019101612322565b60405162461bcd60e51b8152602060048201526013602482015272195a5c0dcc4c911bdb585a5b8819985a5b1959606a1b6044820152606490fd5b61309691945060203d6020116113b4576113a58183611b38565b925f6121f3565b6130b791925060203d6020116113b4576113a58183611b38565b905f6121cd565b6130d291503d805f833e6113d08183611b38565b5f6121a7565b6130ec91503d805f833e6113d08183611b38565b5f612183565b90506020813d602011613124575b8161310d60209383611b38565b81010312610afb5761311e90611d2b565b5f61215f565b3d9150613100565b9093506020813d602011613160575b8161314860209383611b38565b81010312610afb5761315990611d2b565b925f61211d565b3d915061313b565b90506020813d60201161319a575b8161318360209383611b38565b81010312610afb5761319490611d2b565b5f6120e9565b3d9150613176565b90506020813d6020116131d4575b816131bd60209383611b38565b81010312610afb576131ce90611d2b565b5f6120b4565b3d91506131b0565b939591949193919250906001600160a01b031680151590816138bf575b50156134f15750600260208701526040516307f1b29b60e11b8152602081600481865afa908115610b07575f916134b7575b506001600160a01b03166040878101919091525163388af5b560e01b8152602081600481865afa908115610b07575f9161347d575b506001600160a01b0316606087015260405163b045ff5b60e01b815292602084600481865afa938415610b07575f94613449575b5061329e84611e32565b926132ac6040519485611b38565b848452601f196132bb86611e32565b015f5b81811061340b57505061012088019384525f5b8581106132e15750505050505050565b604051631f1a892160e11b8152600481018290529060a082602481865afa918215610b07575f926133db575b50604051602081019061332082856114ec565b60a0815261332f60c082611b38565b5190209161333e828851611e6a565b51526040516349e2903160e11b8152600481018390526001600160a01b0385166024820152606081806044810103816001600160a01b038a165afa908115610b07576001936133a7928b925f916133bd575b50602061339e868c51611e6a565b510152876139cf565b60406133b4838951611e6a565b510152016132d1565b6133d5915060603d81116129985761298a8183611b38565b5f613390565b6133fd91925060a03d8111613404575b6133f58183611b38565b810190611e7e565b905f61330d565b503d6133eb565b60209060405161341a81611ae7565b613422611ee0565b815261342c611b8f565b83820152613438611f0a565b6040820152828289010152016132be565b9093506020813d602011613475575b8161346560209383611b38565b81010312610afb5751925f613294565b3d9150613458565b90506020813d6020116134af575b8161349860209383611b38565b81010312610afb576134a990611d2b565b5f613260565b3d915061348b565b90506020813d6020116134e9575b816134d260209383611b38565b81010312610afb576134e390611d2b565b5f61322b565b3d91506134c5565b91939250906001600160a01b03168015159081613850575b501561383d57600360208601526040516307f1b29b60e11b8152602081600481875afa908115610b07575f91613803575b506001600160a01b03166040868101919091525163388af5b560e01b8152602081600481875afa908115610b07575f916137c9575b506001600160a01b03166060860152604051630399e3a560e41b8152602081600481875afa908115610b07575f9161378f575b506001600160a01b031661014086015260405163ace48b4560e01b815291602083600481875afa928315610b07575f9361375b575b506135e183611e32565b916135ef6040519384611b38565b838352601f196135fe85611e32565b015f5b81811061372b57505061016087019283525f5b84811061362357505050505050565b60405163779a968360e01b815260048101829052906020826024818a5afa918215610b07575f926136f8575b508161365c828751611e6a565b5152604051630dd5aa9b60e31b815260048101839052916020836024818b5afa8015610b075785935f916136c0575b50926136aa9160019460206136a1868b51611e6a565b510152856139cf565b60406136b7838851611e6a565b51015201613614565b9350506020833d82116136f0575b816136db60209383611b38565b81010312610afb5791518492906136aa61368b565b3d91506136ce565b9091506020813d8211613723575b8161371360209383611b38565b81010312610afb5751905f61364f565b3d9150613706565b60209060405161373a81611ae7565b5f81525f8382015261374a611f0a565b604082015282828801015201613601565b9092506020813d602011613787575b8161377760209383611b38565b81010312610afb5751915f6135d7565b3d915061376a565b90506020813d6020116137c1575b816137aa60209383611b38565b81010312610afb576137bb90611d2b565b5f6135a2565b3d915061379d565b90506020813d6020116137fb575b816137e460209383611b38565b81010312610afb576137f590611d2b565b5f61356f565b3d91506137d7565b90506020813d602011613835575b8161381e60209383611b38565b81010312610afb5761382f90611d2b565b5f61353a565b3d9150613811565b82636364223f60e01b5f5260045260245ffd5b60249150602090604051928380926335abafe560e21b82528860048301525afa908115610b07575f91613885575b505f613509565b90506020813d6020116138b7575b816138a060209383611b38565b81010312610afb576138b190611d1e565b5f61387e565b3d9150613893565b602491506020906040519283809263230dbab560e01b82528860048301525afa908115610b07575f916138f4575b505f6131f9565b90506020813d602011613926575b8161390f60209383611b38565b81010312610afb5761392090611d1e565b5f6138ed565b3d9150613902565b6024915060209060405192838092632c77566560e01b82528c60048301525afa908115610b07575f91613963575b505f612080565b90506020813d602011613995575b8161397e60209383611b38565b81010312610afb5761398f90611d1e565b5f61395c565b3d9150613971565b90506020813d6020116139c7575b816139b860209383611b38565b81010312610afb57515f612067565b3d91506139ab565b9291906139da611f0a565b604051632c3c915760e01b81526004810183905290946001600160a01b03169060a081602481855afa918215610b075760249260c0925f91613be8575b50875260405192838092632e3071cd60e11b82528660048301525afa908115610b07575f91613b4b575b5060208501528351604001516001600160a01b031680613adf575b508351606001516001600160a01b0392831692168214613a7a575050565b6020906024604051809481936301977b5760e01b835260048301525afa908115610b07575f91613aad575b506080830152565b90506020813d602011613ad7575b81613ac860209383611b38565b81010312610afb57515f613aa5565b3d9150613abb565b60206004916040519283809263501ad8ff60e11b82525afa5f9181613b17575b5015613a5c576001604086015260608501525f613a5c565b9091506020813d602011613b43575b81613b3360209383611b38565b81010312610afb5751905f613aff565b3d9150613b26565b905060c0813d60c011613be0575b81613b6660c09383611b38565b81010312610afb57613bd560a060405192613b8084611b02565b613b8981611deb565b8452613b9760208201611deb565b6020850152613ba860408201611deb565b6040850152613bb960608201611deb565b6060850152613bca60808201611deb565b608085015201611deb565b60a08201525f613a41565b3d9150613b59565b613c01915060a03d60a011613404576133f58183611b38565b5f613a17565b90816040910312610afb5760405190613c1f82611acc565b80516001600160c01b0381168103610afb578252613c3f90602001611dff565b602082015290565b3d15613c71573d90613c5882611d3f565b91613c666040519384611b38565b82523d5f602084013e565b606090565b90613c8082611e32565b613c8d6040519182611b38565b8281528092613c9e601f1991611e32565b019060203691013756fea2646970667358221220a6e108e7d4b1ff604e7cc99616f34f83acd5fbda44325a0aefb522a2513439a864736f6c63430008230033"; + "0x60808060405234601557613cde908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c630f0d54d814610024575f80fd5b34610afb57610100366003190112610afb576004356001600160a01b0381169003610afb576024356001600160a01b0381168103610afb576044356001600160a01b0381169003610afb576064356001600160a01b0381169003610afb576084356001600160a01b0381169003610afb5760a4356001600160a01b0381169003610afb5760c4356001600160a01b0381169003610afb5760e4356001600160a01b0381169003610afb576103206040526040516100e081611a96565b5f8082526060602083018190526040830181905280830182905260809290925260a081905260c081905260e08190526101008190526101208190526101408190526101608190526101808290526101a08190526101c0919091526101e08190526102008190526102208190526102408190526102608190526102808190526102a08190526102c052610170611cae565b6102e052606061030052604051635edec50d60e01b81526001600160a01b03600480358216908301526020908290602490829086165afa908115610b07575f9161145b575b5015611433576040516338d52e0f60e01b815260208160048181356001600160a01b03165afa908115610b07575f916113f9575b506040516395d89b4160e01b81525f8160048181356001600160a01b03165afa908115610b07575f916113df575b506040516306fdde0360e01b8152905f8260048181356001600160a01b03165afa918215610b07575f926113bb575b5060405163313ce56760e01b81529160208360048181356001600160a01b03165afa918215610b075760ff935f9361138a575b506040519461028786611a96565b60018060a01b03168552602085015260408401521660608201526080526040516338d52e0f60e01b815260208160048160018060a01b038235165afa908115610b07575f91611350575b506001600160a01b0390811660a05260405163ce04bebb60e01b815290602090829060049082908235165afa8015610b07575f90611310575b6001600160801b031660c052506040516318160ddd60e01b815260208160048181356001600160a01b03165afa908115610b07575f916112de575b5060e0526040516331c6651b60e21b815260208160048181356001600160a01b03165afa908115610b07575f916112ac575b506101005260405163ece1d6e560e01b815260208160048181356001600160a01b03165afa8015610b07575f9061126c575b6001600160401b0316610120525060405163c046371160e01b815260208160048181356001600160a01b03165afa8015610b07575f9061122c575b6001600160401b0316610140525060405163ad468d1160e01b815260208160048181356001600160a01b03165afa908115610b07575f916111f2575b506001600160a01b03908116610160526040516305c0524560e31b8152905f90829060049082908235165afa908115610b07575f916111a2575b50610180526040516343bc43c160e11b815260208160048181356001600160a01b03165afa8015610b07576001600160601b03915f91611183575b50166101e05260405163537bfaeb60e11b815260208160048181356001600160a01b03165afa8015610b07576001600160601b03915f91611154575b50166102005260405163ed27f7c960e01b815260208160048181356001600160a01b03165afa908115610b07575f9161111a575b506001600160a01b03908116610220526040516306d9a30160e41b815290602090829060049082908235165afa908115610b07575f916110e0575b506001600160a01b0316610240526101e0516001600160601b0316156110d957610220516040516326326d2760e21b81526001600160a01b039182166004808301919091529091602091839160249183919035165afa908115610b07575f9161109f575b505b151561026052610200516001600160601b03161561109857610240516040516326326d2760e21b81526001600160a01b039182166004808301919091529091602091839160249183919035165afa908115610b07575f9161105e575b505b15156102805260a0516040516370a0823160e01b81526001600160a01b0360048035821690830152909160209183916024918391165afa908115610b07575f9161102c575b506102a0526044356001600160a01b0316151580610fab575b6084356001600160a01b031615159081610f28575b8080610f1b575b610ef857808115610ef1575b15156101a05215610d38575060408051906106a58183611b38565b600182525f5b601f1982018110610cf55750506101406080015261073f60018060a01b0360e0608001511660405160208101916040835260046060830152637468697360e01b608083015260408201526080815261070460a082611b38565b5190206040519061071482611a96565b81525f60208201525f60408201525f6060820152610140608001519061073982611e49565b52611e49565b505b6101c051515f5b818110610b8f57610160516001600160a01b031680610b5b575b50604051630b54457960e31b815260208160048181356001600160a01b03165afa908115610b07575f91610b29575b5061079b81611e32565b6107a86040519182611b38565b818152601f196107b783611e32565b015f5b818110610b12575050610300525f5b818110610a425760405160208152806108b96080516102a0602084015260018060a01b038151166102c0840152606061082e610816602084015160806102e0880152610340870190611495565b60408401518682036102bf1901610300880152611495565b91015161032084015260a080516001600160a01b03908116604086015260c080516001600160801b0316606087015260e08051608088015261010080519488019490945261012080516001600160401b03908116938901939093526101405190921690870152610160519091169185019190915261018051848303601f190191850191909152611495565b6101a05115156101408301526101c051828203601f19016101608401528051808352602092830192909101905f5b818110610a085750505061099e906001600160601b0361016060800151166101808401526001600160601b0361018060800151166101a084015260018060a01b036101a060800151166101c084015260018060a01b036101c060800151166101e08401526101e06080015115156102008401526102006080015115156102208401526102206080015161024084015261024060800151151561026084015261026060800151601f19848303016102808501526115ac565b61030051601f19838303016102a084015280518083526020600582901b8401810193928101925f918101905b8383106109d75786860387f35b9193955091936020806109f6600193601f1986820301875289516115ac565b970193019301909286959492936109ca565b91935091602060806001926060875180518352848101518584015260408101516040840152015160608201520194019101918493926108e7565b604051906313bd406b60e21b825280600483015260208260248160018060a01b03600435165afa918215610b07575f92610abe575b50610ab781610aa260019460e4359060c4359060a43590608435906064359060443590600435612014565b6103005190610ab18383611e6a565b52611e6a565b50016107c9565b91506020823d8211610aff575b81610ad860209383611b38565b81010312610afb57610ab781610aa2610af2600195611d2b565b94505050610a77565b5f80fd5b3d9150610acb565b6040513d5f823e3d90fd5b602090610b1d611cae565b828286010152016107ba565b90506020813d602011610b53575b81610b4460209383611b38565b81010312610afb575181610791565b3d9150610b37565b60016102c052610b859060e4359060c4359060a43590608435906064359060443590600435612014565b6102e05280610762565b610b9f8161014060800151611e6a565b5190815160405190632f0374dd60e21b8252600482015260208160248160018060a01b03600435165afa908115610b07575f91610cc4575b50602083015281516040519063a68bafa360e01b8252600482015260208160248160018060a01b03600435165afa908115610b07575f91610c93575b5060408301528151916040519263c69507dd60e01b8452600484015260208360248160018060a01b03600435165afa928315610b07575f93610c5f575b50916060600193015201610748565b92506020833d8211610c8b575b81610c7960209383611b38565b81010312610afb579151916060610c50565b3d9150610c6c565b90506020813d8211610cbc575b81610cad60209383611b38565b81010312610afb575184610c13565b3d9150610ca0565b90506020813d8211610ced575b81610cde60209383611b38565b81010312610afb575184610bd7565b3d9150610cd1565b602090604051610d0481611a96565b5f81525f838201525f60408201525f6060820152828286010152016106ab565b634e487b7160e01b5f52604160045260245ffd5b1561074157610d566101006080015160208082518301019101611e7e565b6101605160405163cc3802bf60e01b8152915f91839160a49183916001600160a01b0390911690610d8b9060048401906114ec565b5afa908115610b07575f91610e61575b508051610da781611e32565b90610db56040519283611b38565b808252610dc4601f1991611e32565b015f5b818110610e325750506101c0525f5b8151811015610e2b5780610e2481610df060019486611e6a565b5160405190610dfe82611a96565b81525f60208201525f60408201525f60608201526101406080015190610ab18383611e6a565b5001610dd6565b5050610741565b602090604051610e4181611a96565b5f81525f838201525f60408201525f606082015282828601015201610dc7565b90503d805f833e610e728183611b38565b810190602081830312610afb578051906001600160401b038211610afb57019080601f83011215610afb578151610ea881611e32565b92610eb66040519485611b38565b81845260208085019260051b820101928311610afb57602001905b828210610ee15750505081610d9b565b8151815260209182019101610ed1565b508161068a565b61016051636364223f60e01b5f9081526001600160a01b03909116600452602490fd5b506101805151151561067e565b610160516040516335abafe560e21b81526001600160a01b03918216600482015291925060209082906024908290608435165afa908115610b07575f91610f71575b5090610677565b90506020813d602011610fa3575b81610f8c60209383611b38565b81010312610afb57610f9d90611d1e565b82610f6a565b3d9150610f7f565b5061016051604051632c77566560e01b81526001600160a01b0391821660048201529060209082906024908290604435165afa908115610b07575f91610ff2575b50610662565b90506020813d602011611024575b8161100d60209383611b38565b81010312610afb5761101e90611d1e565b81610fec565b3d9150611000565b90506020813d602011611056575b8161104760209383611b38565b81010312610afb575181610649565b3d915061103a565b90506020813d602011611090575b8161107960209383611b38565b81010312610afb5761108a90611d1e565b81610602565b3d915061106c565b6001610604565b90506020813d6020116110d1575b816110ba60209383611b38565b81010312610afb576110cb90611d1e565b816105a4565b3d91506110ad565b60016105a6565b90506020813d602011611112575b816110fb60209383611b38565b81010312610afb5761110c90611d2b565b81610540565b3d91506110ee565b90506020813d60201161114c575b8161113560209383611b38565b81010312610afb5761114690611d2b565b81610505565b3d9150611128565b611176915060203d60201161117c575b61116e8183611b38565b810190611e13565b826104d1565b503d611164565b61119c915060203d60201161117c5761116e8183611b38565b82610495565b90503d805f833e6111b38183611b38565b810190602081830312610afb578051906001600160401b038211610afb57019080601f83011215610afb5781516111ec92602001611d5a565b8161045a565b90506020813d602011611224575b8161120d60209383611b38565b81010312610afb5761121e90611d2b565b81610420565b3d9150611200565b506020813d602011611264575b8161124660209383611b38565b81010312610afb5761125f6001600160401b0391611dff565b6103e4565b3d9150611239565b506020813d6020116112a4575b8161128660209383611b38565b81010312610afb5761129f6001600160401b0391611dff565b6103a9565b3d9150611279565b90506020813d6020116112d6575b816112c760209383611b38565b81010312610afb575181610377565b3d91506112ba565b90506020813d602011611308575b816112f960209383611b38565b81010312610afb575181610345565b3d91506112ec565b506020813d602011611348575b8161132a60209383611b38565b81010312610afb576113436001600160801b0391611deb565b61030a565b3d915061131d565b90506020813d602011611382575b8161136b60209383611b38565b81010312610afb5761137c90611d2b565b816102d1565b3d915061135e565b6113ad91935060203d6020116113b4575b6113a58183611b38565b810190611dd2565b9185610279565b503d61139b565b6113d89192503d805f833e6113d08183611b38565b810190611dad565b9083610246565b6113f391503d805f833e6113d08183611b38565b82610217565b90506020813d60201161142b575b8161141460209383611b38565b81010312610afb5761142590611d2b565b816101e9565b3d9150611407565b63634ba39d60e11b5f9081526001600160a01b03918216600490815235909116602452604490fd5b90506020813d60201161148d575b8161147660209383611b38565b81010312610afb5761148790611d1e565b5f6101b5565b3d9150611469565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106114d65750505090565b82518452602093840193909201916001016114c9565b80516001600160a01b03908116835260208083015182169084015260408083015182169084015260608083015190911690830152608090810151910152565b60806101a09161153c8482516114ec565b6001600160801b0360a0602083015182815116828801528260208201511660c08801528260408201511660e0880152826060820151166101008801528285820151166101208801520151166101408501526040810151151561016085015260608101516101808501520151910152565b60018060a01b03815116825260ff602082015116602083015260018060a01b03604082015116604083015260018060a01b0360608201511660608301526080810151608083015260018060a01b0360a08201511660a083015260c08101519061018060c084015281516102e061018085015260018060a01b0381511661046085015260a061166661164e602084015160c0610480890152610520880190611495565b604084015187820361045f19016104a0890152611495565b9160608101516104c087015260808101516104e0870152015161045f198583030161050086015260ff60f81b815116825260c06116c76116b5602084015160e0602087015260e0860190611495565b60408401518582036040870152611495565b916060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c0818303910152602080835192838152019201905f5b818110611a80575050506020838101516001600160a01b039081166101a087015260408581015182166101c088015260608601519091166101e0870152608085015161020087015260a085015180516001600160c01b0316610220880152909101516001600160401b031661024086810191909152909161184c906118339060c087015180516001600160a01b039081166102608b01526020909101516001600160401b03166102808a015260e088015181166102a08a01526101008801516102c08a015261012088015181166102e08a0152610140880151166103008901526101608701516103208901526101808701516103408901526101a087015115156103608901526101c08701516103808901526101e087015161017f19898303016103a08a01526114b9565b61020086015187820361017f19016103c08901526114b9565b9361022081015115156103e0870152015160018060a01b038151166104008601526020810151610420860152015161044084015260e08101519183810360e0850152602080845192838152019301905f5b8181106119b65750505061010081015161010084015261012081015191838103610120850152602080845192838152019301905f5b818110611951575050506101609060018060a01b0361014082015116610140850152015191610160818303910152602080835192838152019201905f5b81811061191c5750505090565b9091926020610200600192611946604088518051845285810151868501520151604083019061152b565b01940192910161190f565b90919360206102c06001926119ab6040895161196e8482516114ec565b61199e8682015160a08601906001600160801b036040809280518552826020820151166020860152015116910152565b015161010083019061152b565b0195019291016118d2565b90919360206103006001926001600160801b0360e0895180518452858101511515868501526001600160401b036040820151166040850152611a1b606082015160608601906001600160401b036020809260018060c01b038151168552015116910152565b611a4c608082015160a08601906001600160801b036040809280518552826020820151166020860152015116910152565b611a5f60a082015161010086019061152b565b60c081015183166102c08501520151166102e082015201950192910161189d565b8251845260209384019390920191600101611708565b608081019081106001600160401b03821117610d2457604052565b60e081019081106001600160401b03821117610d2457604052565b604081019081106001600160401b03821117610d2457604052565b606081019081106001600160401b03821117610d2457604052565b60c081019081106001600160401b03821117610d2457604052565b60a081019081106001600160401b03821117610d2457604052565b90601f801991011681019081106001600160401b03821117610d2457604052565b60405190611b6682611ab1565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b60405190611b9c82611ae7565b5f6040838281528260208201520152565b6040519061026082018281106001600160401b03821117610d245760405281604051611bd881611b02565b5f815260606020820152606060408201525f60608201525f6080820152611bfd611b59565b60a082015281525f60208201525f60408201525f60608201525f6080820152604051611c2881611acc565b5f81525f602082015260a0820152604051611c4281611acc565b5f81525f602082015260c08201525f60e08201525f6101008201525f6101208201525f6101408201525f6101608201525f6101808201525f6101a08201525f6101c082015260606101e082015260606102008201525f610220820152610240611ca9611b8f565b910152565b6040519061018082018281106001600160401b03821117610d24576040526060610160835f81525f60208201525f60408201525f838201525f60808201525f60a0820152611cfa611bad565b60c08201528260e08201525f610100820152826101208201525f6101408201520152565b51908115158203610afb57565b51906001600160a01b0382168203610afb57565b6001600160401b038111610d2457601f01601f191660200190565b929192611d6682611d3f565b91611d746040519384611b38565b829481845281830111610afb578281602093845f96015e010152565b9080601f83011215610afb578151611daa92602001611d5a565b90565b90602082820312610afb5781516001600160401b038111610afb57611daa9201611d90565b90816020910312610afb575160ff81168103610afb5790565b51906001600160801b0382168203610afb57565b51906001600160401b0382168203610afb57565b90816020910312610afb57516001600160601b0381168103610afb5790565b6001600160401b038111610d245760051b60200190565b805115611e565760200190565b634e487b7160e01b5f52603260045260245ffd5b8051821015611e565760209160051b010190565b908160a0910312610afb57608060405191611e9883611b1d565b611ea181611d2b565b8352611eaf60208201611d2b565b6020840152611ec060408201611d2b565b6040840152611ed160608201611d2b565b60608401520151608082015290565b60405190611eed82611b1d565b5f6080838281528260208201528260408201528260608201520152565b60405190611f1782611b1d565b5f608083611f23611ee0565b8152604051611f3181611b02565b83815283602082015283604082015283606082015283838201528360a082015260208201528260408201528260608201520152565b90816060910312610afb57611f9d6040805192611f8284611ae7565b80518452611f9260208201611deb565b602085015201611deb565b604082015290565b6040519061010082018281106001600160401b03821117610d24576040525f60e083828152826020820152826040820152604051611fe281611acc565b8381528360208201526060820152611ff8611b8f565b6080820152612005611f0a565b60a08201528260c08201520152565b95939091979692612023611cae565b6001600160a01b038481168083526040516399e9918360e01b815260048101829052929b909990929160209183916024918391165afa908115610b07575f9161399d575b5060808b01526001600160a01b0316801515908161392e575b50156131dc57505050600160208701526040516307f1b29b60e11b8152602081600481885afa908115610b07575f916131a2575b506001600160a01b03166040878101919091525163388af5b560e01b8152602081600481885afa908115610b07575f91613168575b506001600160a01b0316606087015260405163e4baaddf60e01b815292602084600481885afa938415610b07575f9461312c575b506001600160a01b0390931660a08701818152939061213a611bad565b916040516338d52e0f60e01b8152602081600481865afa908115610b07575f916130f2575b506040516395d89b4160e01b81525f81600481875afa908115610b07575f916130d8575b506040516306fdde0360e01b81525f81600481885afa908115610b07575f916130be575b5060405163313ce56760e01b815290602082600481895afa918215610b07575f9261309d575b50604051632ba9c2b360e21b8152926020846004818a5afa938415610b07575f9461307c575b506121fc611b59565b505f8060405160208101906342580cb760e11b825260048152612220602482611b38565b51908a5afa61222d613c47565b901561304157805181019060e08160208401930312610afb5760208101516001600160f81b0319811690819003610afb5760408201516001600160401b038111610afb5783602061228092850101611d90565b60608301516001600160401b038111610afb578460206122a292860101611d90565b608084015160a08501516001600160a01b0381169491939190859003610afb5760c08601519560e0810151906001600160401b038211610afb57019680603f89011215610afb5760208801516122f781611e32565b986123056040519a8b611b38565b818a52602080808c019360051b83010101928311610afb57604001905b82821061303157505050926123489a98959260ff9a9794928b9996936040519d8e611ab1565b8d5260208d015260408c015260608b015260808a015260a089015260c08801526040519761237589611b02565b60018060a01b031688526020880152604087015216606085015216608083015260a08201528352604051638da5cb5b60e01b8152602081600481865afa908115610b07575f91612ff7575b506001600160a01b031660208481019190915260405163e66f53b760e01b81529081600481865afa908115610b07575f91612fbd575b506001600160a01b031660408481019190915251630229549960e51b8152602081600481865afa908115610b07575f91612f83575b506001600160a01b031660608401526040516334cc866d60e21b8152602081600481865afa908115610b07575f91612f51575b50608084015260408051637cc4d9a160e01b81529081600481865afa908115610b07575f91612f32575b5060a084015260408051633b1618dd60e11b81529081600481865afa908115610b07575f91612ed9575b5060c0840152604051631c61872f60e31b8152602081600481865afa908115610b07575f91612e9f575b506001600160a01b031660e084015260405163ddca3f4360e01b8152602081600481865afa8015610b07576001600160601b03915f91612e80575b501661010084015260405163011a412160e61b8152602081600481865afa908115610b07575f91612e46575b506001600160a01b031661012084015260405163388af5b560e01b8152602081600481865afa908115610b07575f91612e0c575b506001600160a01b03166101408401526040516318160ddd60e01b8152602081600481865afa908115610b07575f91612dda575b5061016084015260405163568efc0760e01b8152602081600481865afa908115610b07575f91612da8575b506101808401525f806040516020810190630872d2c560e21b825260048152612600602482611b38565b5190855afa61260d613c47565b9080612d9c575b612d73575b50604051630a17b31360e41b8152602081600481865afa908115610b07575f91612d41575b5061264881613c76565b6101e085019081525f5b828110612cce5750506040516333f91ebb60e01b8152949050602085600481865afa948515610b07575f95612c9a575b5061268c85613c76565b9461020085019586525f5b818110612c275750506001600160a01b0316801515949092908580612bbb575b612a61575b60c08b019485525151946126cf86611e32565b946126dd6040519687611b38565b868652601f196126ec88611e32565b015f5b818110612a4a57505060e08c019586525f5b87811061278757505096516040516370a0823160e01b8152600481019990995260209750889650602495508694506001600160a01b0316925050505afa908115610b07575f91612755575b50610100830152565b90506020813d60201161277f575b8161277060209383611b38565b81010312610afb57515f61274c565b3d9150612763565b6127978161020084510151611e6a565b51906127a1611fa5565b91604051636638c7bb60e11b81528160048201526060816024818a5afa908115610b07575f916129cd575b5080516001600160b81b031684526020808201511515908501526040908101516001600160401b031684820152805163518df2eb60e11b81526004810183905290816024818a5afa908115610b07575f9161299f575b506060848101919091526040516349e2903160e11b8152600481018390526001600160a01b03881660248201529081806044810103816001600160a01b038c165afa908115610b07575f91612971575b506080840152846128848c838a6139cf565b60a08501526128aa575b506128a3816001938a5190610ab18383611e6a565b5001612701565b9160405192639dbcd5b960e01b845286600485015260248401526040836044818b5afa928315610b07575f93612904575b5082516001600160801b0390811660c083015260209093015190921660e08301526128a361288e565b92506040833d8211612969575b8161291e60409383611b38565b81010312610afb57816128a3916001600160801b03602060019661295a826040519261294984611acc565b61295281611deb565b845201611deb565b828201529650505091506128db565b3d9150612911565b612992915060603d8111612998575b61298a8183611b38565b810190611f66565b5f612872565b503d612980565b6129c0915060403d81116129c6575b6129b88183611b38565b810190613c07565b5f612822565b503d6129ae565b90506060813d8211612a42575b816129e760609383611b38565b81010312610afb576040516129fb81611ae7565b8151906001600160b81b0382168203610afb57612a3760406001600160401b039481948452612a2c60208201611d1e565b602085015201611dff565b8282015291506127cc565b3d91506129da565b602090612a55611fa5565b82828b010152016126ef565b6001610220860152604051630c7508df60e31b815260048101839052602081602481885afa908115610b07575f91612b81575b50604051636fcca69b60e01b815260048101849052602081602481895afa908115610b07575f91612b4f575b506040516348d88a5960e11b815260048101859052906020826024818a5afa918215610b07575f92612b1b575b5060405192612afb84611ae7565b6001600160a01b03168352602083015260408201526102408601526126bc565b9091506020813d602011612b47575b81612b3760209383611b38565b81010312610afb5751905f612aed565b3d9150612b2a565b90506020813d602011612b79575b81612b6a60209383611b38565b81010312610afb57515f612ac0565b3d9150612b5d565b90506020813d602011612bb3575b81612b9c60209383611b38565b81010312610afb57612bad90611d2b565b5f612a94565b3d9150612b8f565b506040516326f6f90760e11b815260048101859052602081602481865afa908115610b07575f91612bed575b506126b7565b90506020813d602011612c1f575b81612c0860209383611b38565b81010312610afb57612c1990611d1e565b5f612be7565b3d9150612bfb565b6040516362518ddf60e01b81526004810182905290602082602481895afa8015610b07575f90612c68575b60019250612c61828a51611e6a565b5201612697565b506020823d8211612c92575b81612c8160209383611b38565b81010312610afb5760019151612c52565b3d9150612c74565b9094506020813d602011612cc6575b81612cb660209383611b38565b81010312610afb5751935f612682565b3d9150612ca9565b60405163f7d1852160e01b81526004810182905290602082602481895afa8015610b07575f90612d0f575b60019250612d08828551611e6a565b5201612652565b506020823d8211612d39575b81612d2860209383611b38565b81010312610afb5760019151612cf9565b3d9150612d1b565b90506020813d602011612d6b575b81612d5c60209383611b38565b81010312610afb57515f61263e565b3d9150612d4f565b60016101a085015260208151918180820193849201010312610afb57516101c08401525f612619565b50602081511015612614565b90506020813d602011612dd2575b81612dc360209383611b38565b81010312610afb57515f6125d6565b3d9150612db6565b90506020813d602011612e04575b81612df560209383611b38565b81010312610afb57515f6125ab565b3d9150612de8565b90506020813d602011612e3e575b81612e2760209383611b38565b81010312610afb57612e3890611d2b565b5f612577565b3d9150612e1a565b90506020813d602011612e78575b81612e6160209383611b38565b81010312610afb57612e7290611d2b565b5f612543565b3d9150612e54565b612e99915060203d60201161117c5761116e8183611b38565b5f612517565b90506020813d602011612ed1575b81612eba60209383611b38565b81010312610afb57612ecb90611d2b565b5f6124dc565b3d9150612ead565b90506040813d604011612f2a575b81612ef460409383611b38565b81010312610afb57612f1f602060405192612f0e84611acc565b612f1781611d2b565b845201611dff565b60208201525f6124b2565b3d9150612ee7565b612f4b915060403d6040116129c6576129b88183611b38565b5f612488565b90506020813d602011612f7b575b81612f6c60209383611b38565b81010312610afb57515f61245e565b3d9150612f5f565b90506020813d602011612fb5575b81612f9e60209383611b38565b81010312610afb57612faf90611d2b565b5f61242b565b3d9150612f91565b90506020813d602011612fef575b81612fd860209383611b38565b81010312610afb57612fe990611d2b565b5f6123f6565b3d9150612fcb565b90506020813d602011613029575b8161301260209383611b38565b81010312610afb5761302390611d2b565b5f6123c0565b3d9150613005565b8151815260209182019101612322565b60405162461bcd60e51b8152602060048201526013602482015272195a5c0dcc4c911bdb585a5b8819985a5b1959606a1b6044820152606490fd5b61309691945060203d6020116113b4576113a58183611b38565b925f6121f3565b6130b791925060203d6020116113b4576113a58183611b38565b905f6121cd565b6130d291503d805f833e6113d08183611b38565b5f6121a7565b6130ec91503d805f833e6113d08183611b38565b5f612183565b90506020813d602011613124575b8161310d60209383611b38565b81010312610afb5761311e90611d2b565b5f61215f565b3d9150613100565b9093506020813d602011613160575b8161314860209383611b38565b81010312610afb5761315990611d2b565b925f61211d565b3d915061313b565b90506020813d60201161319a575b8161318360209383611b38565b81010312610afb5761319490611d2b565b5f6120e9565b3d9150613176565b90506020813d6020116131d4575b816131bd60209383611b38565b81010312610afb576131ce90611d2b565b5f6120b4565b3d91506131b0565b939591949193919250906001600160a01b031680151590816138bf575b50156134f15750600260208701526040516307f1b29b60e11b8152602081600481865afa908115610b07575f916134b7575b506001600160a01b03166040878101919091525163388af5b560e01b8152602081600481865afa908115610b07575f9161347d575b506001600160a01b0316606087015260405163b045ff5b60e01b815292602084600481865afa938415610b07575f94613449575b5061329e84611e32565b926132ac6040519485611b38565b848452601f196132bb86611e32565b015f5b81811061340b57505061012088019384525f5b8581106132e15750505050505050565b604051631f1a892160e11b8152600481018290529060a082602481865afa918215610b07575f926133db575b50604051602081019061332082856114ec565b60a0815261332f60c082611b38565b5190209161333e828851611e6a565b51526040516349e2903160e11b8152600481018390526001600160a01b0385166024820152606081806044810103816001600160a01b038a165afa908115610b07576001936133a7928b925f916133bd575b50602061339e868c51611e6a565b510152876139cf565b60406133b4838951611e6a565b510152016132d1565b6133d5915060603d81116129985761298a8183611b38565b5f613390565b6133fd91925060a03d8111613404575b6133f58183611b38565b810190611e7e565b905f61330d565b503d6133eb565b60209060405161341a81611ae7565b613422611ee0565b815261342c611b8f565b83820152613438611f0a565b6040820152828289010152016132be565b9093506020813d602011613475575b8161346560209383611b38565b81010312610afb5751925f613294565b3d9150613458565b90506020813d6020116134af575b8161349860209383611b38565b81010312610afb576134a990611d2b565b5f613260565b3d915061348b565b90506020813d6020116134e9575b816134d260209383611b38565b81010312610afb576134e390611d2b565b5f61322b565b3d91506134c5565b91939250906001600160a01b03168015159081613850575b501561383d57600360208601526040516307f1b29b60e11b8152602081600481875afa908115610b07575f91613803575b506001600160a01b03166040868101919091525163388af5b560e01b8152602081600481875afa908115610b07575f916137c9575b506001600160a01b03166060860152604051630399e3a560e41b8152602081600481875afa908115610b07575f9161378f575b506001600160a01b031661014086015260405163ace48b4560e01b815291602083600481875afa928315610b07575f9361375b575b506135e183611e32565b916135ef6040519384611b38565b838352601f196135fe85611e32565b015f5b81811061372b57505061016087019283525f5b84811061362357505050505050565b60405163779a968360e01b815260048101829052906020826024818a5afa918215610b07575f926136f8575b508161365c828751611e6a565b5152604051630dd5aa9b60e31b815260048101839052916020836024818b5afa8015610b075785935f916136c0575b50926136aa9160019460206136a1868b51611e6a565b510152856139cf565b60406136b7838851611e6a565b51015201613614565b9350506020833d82116136f0575b816136db60209383611b38565b81010312610afb5791518492906136aa61368b565b3d91506136ce565b9091506020813d8211613723575b8161371360209383611b38565b81010312610afb5751905f61364f565b3d9150613706565b60209060405161373a81611ae7565b5f81525f8382015261374a611f0a565b604082015282828801015201613601565b9092506020813d602011613787575b8161377760209383611b38565b81010312610afb5751915f6135d7565b3d915061376a565b90506020813d6020116137c1575b816137aa60209383611b38565b81010312610afb576137bb90611d2b565b5f6135a2565b3d915061379d565b90506020813d6020116137fb575b816137e460209383611b38565b81010312610afb576137f590611d2b565b5f61356f565b3d91506137d7565b90506020813d602011613835575b8161381e60209383611b38565b81010312610afb5761382f90611d2b565b5f61353a565b3d9150613811565b82636364223f60e01b5f5260045260245ffd5b60249150602090604051928380926335abafe560e21b82528860048301525afa908115610b07575f91613885575b505f613509565b90506020813d6020116138b7575b816138a060209383611b38565b81010312610afb576138b190611d1e565b5f61387e565b3d9150613893565b602491506020906040519283809263230dbab560e01b82528860048301525afa908115610b07575f916138f4575b505f6131f9565b90506020813d602011613926575b8161390f60209383611b38565b81010312610afb5761392090611d1e565b5f6138ed565b3d9150613902565b6024915060209060405192838092632c77566560e01b82528c60048301525afa908115610b07575f91613963575b505f612080565b90506020813d602011613995575b8161397e60209383611b38565b81010312610afb5761398f90611d1e565b5f61395c565b3d9150613971565b90506020813d6020116139c7575b816139b860209383611b38565b81010312610afb57515f612067565b3d91506139ab565b9291906139da611f0a565b604051632c3c915760e01b81526004810183905290946001600160a01b03169060a081602481855afa918215610b075760249260c0925f91613be8575b50875260405192838092632e3071cd60e11b82528660048301525afa908115610b07575f91613b4b575b5060208501528351604001516001600160a01b031680613adf575b508351606001516001600160a01b0392831692168214613a7a575050565b6020906024604051809481936301977b5760e01b835260048301525afa908115610b07575f91613aad575b506080830152565b90506020813d602011613ad7575b81613ac860209383611b38565b81010312610afb57515f613aa5565b3d9150613abb565b60206004916040519283809263501ad8ff60e11b82525afa5f9181613b17575b5015613a5c576001604086015260608501525f613a5c565b9091506020813d602011613b43575b81613b3360209383611b38565b81010312610afb5751905f613aff565b3d9150613b26565b905060c0813d60c011613be0575b81613b6660c09383611b38565b81010312610afb57613bd560a060405192613b8084611b02565b613b8981611deb565b8452613b9760208201611deb565b6020850152613ba860408201611deb565b6040850152613bb960608201611deb565b6060850152613bca60808201611deb565b608085015201611deb565b60a08201525f613a41565b3d9150613b59565b613c01915060a03d60a011613404576133f58183611b38565b5f613a17565b90816040910312610afb5760405190613c1f82611acc565b80516001600160c01b0381168103610afb578252613c3f90602001611dff565b602082015290565b3d15613c71573d90613c5882611d3f565b91613c666040519384611b38565b82523d5f602084013e565b606090565b90613c8082611e32565b613c8d6040519182611b38565b8281528092613c9e601f1991611e32565b019060203691013756fea26469706673582212203e675bf0e683983964ae8cc072b121082d3c6c0bb834d392a811927381e6b0e764736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2.ts index cdfc813ac..39b58972f 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2.ts @@ -201,4 +201,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2` query bytecode. */ export const code = - "0x6080806040523460155761147b908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63f12c3a9214610024575f80fd5b34610a02576080366003190112610a02576004356001600160a01b0381168103610a02576024356001600160a01b0381168103610a02576044356001600160a01b0381168103610a0257606435916001600160a01b0383168303610a02576102c0604052604051610094816112cf565b5f80825260606020808401829052604080850183905282850184905260809490945260a083905260c083905260e08390526101008390526101208390526101408390526101608290526101808390526101a08290526101c08390526101e0919091526102008290526102208290526102408290526102608290526102808290526102a0919091529051635edec50d60e01b81526001600160a01b0386811660048301529091908290602490829086165afa908115610a0e575f9161128c575b501561126757506040516338d52e0f60e01b81526020816004816001600160a01b0388165afa908115610a0e575f9161122d575b506040516395d89b4160e01b81525f816004816001600160a01b0389165afa908115610a0e575f91611213575b506040516306fdde0360e01b8152905f826004816001600160a01b038a165afa918215610a0e575f926111ef575b5060405163313ce56760e01b8152916020836004816001600160a01b038b165afa918215610a0e575f926111ae575b60ff935060405194610222866112cf565b60018060a01b03168552602085015260408401521660608201526080526040516338d52e0f60e01b815260208160048160018060a01b0388165afa908115610a0e575f91611174575b506001600160a01b0390811660a05260405163ce04bebb60e01b8152906020908290600490829088165afa8015610a0e575f9061112b575b6001600160801b031660c052506040516318160ddd60e01b81526020816004816001600160a01b0388165afa908115610a0e575f916110f9575b5060e0526040516331c6651b60e21b81526020816004816001600160a01b0388165afa908115610a0e575f916110c7575b506101005260405163ece1d6e560e01b81526020816004816001600160a01b0388165afa8015610a0e5767ffffffffffffffff915f916110a8575b50166101205260405163c046371160e01b81526020816004816001600160a01b0388165afa8015610a0e5767ffffffffffffffff915f91611079575b50166101405260405163ad468d1160e01b81526020816004816001600160a01b0388165afa908115610a0e575f9161103f575b506001600160a01b03908116610180526040516305c0524560e31b8152905f908290600490829088165afa908115610a0e575f91610fee575b506101a0526040516343bc43c160e11b81526020816004816001600160a01b0388165afa8015610a0e576001600160601b03915f91610fcf575b50166102005260405163537bfaeb60e11b81526020816004816001600160a01b0388165afa8015610a0e576001600160601b03915f91610fa0575b50166102205260405163ed27f7c960e01b81526020816004816001600160a01b0388165afa908115610a0e575f91610f66575b506001600160a01b03908116610240526040516306d9a30160e41b8152906020908290600490829088165afa908115610a0e575f91610f2c575b506001600160a01b031661026052610200516001600160601b031615610f2557610240516040516326326d2760e21b81526001600160a01b039182166004820152906020908290602490829088165afa908115610a0e575f91610f06575b505b151561028052610220516001600160601b031615610eff57610260516040516326326d2760e21b81526001600160a01b039182166004820152906020908290602490829088165afa908115610a0e575f91610ee0575b505b15156102a052604051630b54457960e31b81526020816004816001600160a01b0388165afa908115610a0e575f91610eae575b506105c8816113f8565b6105d560405191826112eb565b818152601f196105e4836113f8565b01366020830137610160525f5b818110610e1e5750506001600160a01b03811615159081610dba575b506001600160a01b03821615159182610d44575b508080610d37575b610d1457808115610d0d575b15156101c05215610abe5750604080519061065081836112eb565b60018252601f19015f5b818110610a7b575050610160608001526106eb60018060a01b03610100608001511660405160208101916040835260046060830152637468697360e01b60808301526040820152608081526106b060a0826112eb565b519020604051906106c0826112cf565b81525f60208201525f60408201525f606082015261016060800151906106e582611410565b52611410565b505b6101e051515f5b818110610907576040516020815280608051610240602083015260018060a01b03815116610260830152606061075661073e602084015160806102808701526102e08601906112ab565b604084015185820361025f19016102a08701526112ab565b9101516102c083015260a080516001600160a01b0316604084015260c080516001600160801b0316606085015260e0805160808601526101008051938601939093526101205167ffffffffffffffff90811692860192909252610140519091169084015261016051838303601f1901918401919091528051808352602092830192909101905f5b8181106108e5575050610180516001600160a01b0316610120840152506101a051828203601f190161014084015261081591906112ab565b6101c05115156101608301526101e051828203601f19016101808401528051808352602092830192909101905f5b8181106108ab57505061020080516001600160601b039081166101a086015261022080519091166101c086015261024080516001600160a01b039081166101e0880152610260511692860192909252610280511515908501526102a051151590840152500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610843565b82516001600160a01b03168452859450602093840193909201916001016107dd565b6109178161016060800151611431565b518051604051632f0374dd60e21b815260048101919091529091906020816024816001600160a01b0389165afa908115610a0e575f91610a4a575b50602083015281516040519063a68bafa360e01b8252600482015260208160248160018060a01b0389165afa908115610a0e575f91610a19575b5060408301528151916040519263c69507dd60e01b8452600484015260208360248160018060a01b0389165afa928315610a0e575f936109d6575b509160606001930152016106f4565b92506020833d8211610a06575b816109f0602093836112eb565b81010312610a025791519160606109c7565b5f80fd5b3d91506109e3565b6040513d5f823e3d90fd5b90506020813d8211610a42575b81610a33602093836112eb565b81010312610a0257515f61098c565b3d9150610a26565b90506020813d8211610a73575b81610a64602093836112eb565b81010312610a0257515f610952565b3d9150610a57565b602090604051610a8a816112cf565b5f81525f838201525f60408201525f60608201528282860101520161065a565b634e487b7160e01b5f52604160045260245ffd5b156106ed57610120608001519060a082805181010312610a02576040519160a083019083821067ffffffffffffffff831117610aaa5760a091604052610b0660208201611325565b8452610b1460408201611325565b6020850152610b2560608201611325565b6040850152610b3660808201611325565b6060850190815291015160808401908152610180516040805163cc3802bf60e01b815286516001600160a01b039081166004830152602088015181166024830152919096015181166044870152925183166064860152905160848501525f91849160a4918391165afa918215610a0e575f92610c7a575b508151610bb9816113f8565b90610bc760405192836112eb565b808252610bd6601f19916113f8565b015f5b818110610c4b5750506101e0525f5b8251811015610c435780610c3c81610c0260019487611431565b5160405190610c10826112cf565b81525f60208201525f60408201525f60608201526101606080015190610c368383611431565b52611431565b5001610be8565b5090506106ed565b602090604051610c5a816112cf565b5f81525f838201525f60408201525f606082015282828601015201610bd9565b9091503d805f833e610c8c81836112eb565b810190602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a02578151610cc3816113f8565b92610cd160405194856112eb565b81845260208085019260051b820101928311610a0257602001905b828210610cfd57505050905f610bad565b8151815260209182019101610cec565b5081610635565b61018051636364223f60e01b5f9081526001600160a01b03909116600452602490fd5b506101a051511515610629565b610180516040516335abafe560e21b81526001600160a01b03918216600482015292935060209183916024918391165afa908115610a0e575f91610d8b575b50905f610621565b610dad915060203d602011610db3575b610da581836112eb565b81019061130d565b5f610d83565b503d610d9b565b61018051604051632c77566560e01b81526001600160a01b039182166004820152925060209183916024918391165afa908115610a0e575f91610dff575b505f61060d565b610e18915060203d602011610db357610da581836112eb565b5f610df8565b6040516313bd406b60e21b815260048101829052906020826024816001600160a01b038a165afa8015610a0e575f90610e75575b60019250610e658260e060800151611431565b90838060a01b03169052016105f1565b506020823d8211610ea6575b81610e8e602093836112eb565b81010312610a0257610ea1600192611325565b610e52565b3d9150610e81565b90506020813d602011610ed8575b81610ec9602093836112eb565b81010312610a0257515f6105be565b3d9150610ebc565b610ef9915060203d602011610db357610da581836112eb565b5f610589565b600161058b565b610f1f915060203d602011610db357610da581836112eb565b5f610531565b6001610533565b90506020813d602011610f5e575b81610f47602093836112eb565b81010312610a0257610f5890611325565b5f6104d3565b3d9150610f3a565b90506020813d602011610f98575b81610f81602093836112eb565b81010312610a0257610f9290611325565b5f610499565b3d9150610f74565b610fc2915060203d602011610fc8575b610fba81836112eb565b8101906113d9565b5f610466565b503d610fb0565b610fe8915060203d602011610fc857610fba81836112eb565b5f61042b565b90503d805f833e610fff81836112eb565b810190602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a0257815161103992602001611339565b5f6103f1565b90506020813d602011611071575b8161105a602093836112eb565b81010312610a025761106b90611325565b5f6103b8565b3d915061104d565b61109b915060203d6020116110a1575b61109381836112eb565b8101906113b9565b5f610385565b503d611089565b6110c1915060203d6020116110a15761109381836112eb565b5f610349565b90506020813d6020116110f1575b816110e2602093836112eb565b81010312610a0257515f61030e565b3d91506110d5565b90506020813d602011611123575b81611114602093836112eb565b81010312610a0257515f6102dd565b3d9150611107565b506020813d60201161116c575b81611145602093836112eb565b81010312610a0257516001600160801b0381168103610a02576001600160801b03906102a3565b3d9150611138565b90506020813d6020116111a6575b8161118f602093836112eb565b81010312610a02576111a090611325565b5f61026b565b3d9150611182565b9150916020813d6020116111e7575b816111ca602093836112eb565b81010312610a0257519160ff83168303610a025760ff9291610211565b3d91506111bd565b61120c9192503d805f833e61120481836112eb565b81019061137f565b905f6101e2565b61122791503d805f833e61120481836112eb565b5f6101b4565b90506020813d60201161125f575b81611248602093836112eb565b81010312610a025761125990611325565b5f610187565b3d915061123b565b63634ba39d60e11b5f9081526001600160a01b03918216600452908416602452604490fd5b6112a5915060203d602011610db357610da581836112eb565b5f610153565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b6080810190811067ffffffffffffffff821117610aaa57604052565b90601f8019910116810190811067ffffffffffffffff821117610aaa57604052565b90816020910312610a0257518015158103610a025790565b51906001600160a01b0382168203610a0257565b92919267ffffffffffffffff8211610aaa5760405191611363601f8201601f1916602001846112eb565b829481845281830111610a02578281602093845f96015e010152565b602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a025781516113b692602001611339565b90565b90816020910312610a02575167ffffffffffffffff81168103610a025790565b90816020910312610a0257516001600160601b0381168103610a025790565b67ffffffffffffffff8111610aaa5760051b60200190565b80511561141d5760200190565b634e487b7160e01b5f52603260045260245ffd5b805182101561141d5760209160051b01019056fea26469706673582212204960061b86c1b207a8b92f2eadd53aaee751b9574d274bc613cea5449aaf759164736f6c63430008230033"; + "0x6080806040523460155761147b908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63f12c3a9214610024575f80fd5b34610a02576080366003190112610a02576004356001600160a01b0381168103610a02576024356001600160a01b0381168103610a02576044356001600160a01b0381168103610a0257606435916001600160a01b0383168303610a02576102c0604052604051610094816112cf565b5f80825260606020808401829052604080850183905282850184905260809490945260a083905260c083905260e08390526101008390526101208390526101408390526101608290526101808390526101a08290526101c08390526101e0919091526102008290526102208290526102408290526102608290526102808290526102a0919091529051635edec50d60e01b81526001600160a01b0386811660048301529091908290602490829086165afa908115610a0e575f9161128c575b501561126757506040516338d52e0f60e01b81526020816004816001600160a01b0388165afa908115610a0e575f9161122d575b506040516395d89b4160e01b81525f816004816001600160a01b0389165afa908115610a0e575f91611213575b506040516306fdde0360e01b8152905f826004816001600160a01b038a165afa918215610a0e575f926111ef575b5060405163313ce56760e01b8152916020836004816001600160a01b038b165afa918215610a0e575f926111ae575b60ff935060405194610222866112cf565b60018060a01b03168552602085015260408401521660608201526080526040516338d52e0f60e01b815260208160048160018060a01b0388165afa908115610a0e575f91611174575b506001600160a01b0390811660a05260405163ce04bebb60e01b8152906020908290600490829088165afa8015610a0e575f9061112b575b6001600160801b031660c052506040516318160ddd60e01b81526020816004816001600160a01b0388165afa908115610a0e575f916110f9575b5060e0526040516331c6651b60e21b81526020816004816001600160a01b0388165afa908115610a0e575f916110c7575b506101005260405163ece1d6e560e01b81526020816004816001600160a01b0388165afa8015610a0e5767ffffffffffffffff915f916110a8575b50166101205260405163c046371160e01b81526020816004816001600160a01b0388165afa8015610a0e5767ffffffffffffffff915f91611079575b50166101405260405163ad468d1160e01b81526020816004816001600160a01b0388165afa908115610a0e575f9161103f575b506001600160a01b03908116610180526040516305c0524560e31b8152905f908290600490829088165afa908115610a0e575f91610fee575b506101a0526040516343bc43c160e11b81526020816004816001600160a01b0388165afa8015610a0e576001600160601b03915f91610fcf575b50166102005260405163537bfaeb60e11b81526020816004816001600160a01b0388165afa8015610a0e576001600160601b03915f91610fa0575b50166102205260405163ed27f7c960e01b81526020816004816001600160a01b0388165afa908115610a0e575f91610f66575b506001600160a01b03908116610240526040516306d9a30160e41b8152906020908290600490829088165afa908115610a0e575f91610f2c575b506001600160a01b031661026052610200516001600160601b031615610f2557610240516040516326326d2760e21b81526001600160a01b039182166004820152906020908290602490829088165afa908115610a0e575f91610f06575b505b151561028052610220516001600160601b031615610eff57610260516040516326326d2760e21b81526001600160a01b039182166004820152906020908290602490829088165afa908115610a0e575f91610ee0575b505b15156102a052604051630b54457960e31b81526020816004816001600160a01b0388165afa908115610a0e575f91610eae575b506105c8816113f8565b6105d560405191826112eb565b818152601f196105e4836113f8565b01366020830137610160525f5b818110610e1e5750506001600160a01b03811615159081610dba575b506001600160a01b03821615159182610d44575b508080610d37575b610d1457808115610d0d575b15156101c05215610abe5750604080519061065081836112eb565b60018252601f19015f5b818110610a7b575050610160608001526106eb60018060a01b03610100608001511660405160208101916040835260046060830152637468697360e01b60808301526040820152608081526106b060a0826112eb565b519020604051906106c0826112cf565b81525f60208201525f60408201525f606082015261016060800151906106e582611410565b52611410565b505b6101e051515f5b818110610907576040516020815280608051610240602083015260018060a01b03815116610260830152606061075661073e602084015160806102808701526102e08601906112ab565b604084015185820361025f19016102a08701526112ab565b9101516102c083015260a080516001600160a01b0316604084015260c080516001600160801b0316606085015260e0805160808601526101008051938601939093526101205167ffffffffffffffff90811692860192909252610140519091169084015261016051838303601f1901918401919091528051808352602092830192909101905f5b8181106108e5575050610180516001600160a01b0316610120840152506101a051828203601f190161014084015261081591906112ab565b6101c05115156101608301526101e051828203601f19016101808401528051808352602092830192909101905f5b8181106108ab57505061020080516001600160601b039081166101a086015261022080519091166101c086015261024080516001600160a01b039081166101e0880152610260511692860192909252610280511515908501526102a051151590840152500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610843565b82516001600160a01b03168452859450602093840193909201916001016107dd565b6109178161016060800151611431565b518051604051632f0374dd60e21b815260048101919091529091906020816024816001600160a01b0389165afa908115610a0e575f91610a4a575b50602083015281516040519063a68bafa360e01b8252600482015260208160248160018060a01b0389165afa908115610a0e575f91610a19575b5060408301528151916040519263c69507dd60e01b8452600484015260208360248160018060a01b0389165afa928315610a0e575f936109d6575b509160606001930152016106f4565b92506020833d8211610a06575b816109f0602093836112eb565b81010312610a025791519160606109c7565b5f80fd5b3d91506109e3565b6040513d5f823e3d90fd5b90506020813d8211610a42575b81610a33602093836112eb565b81010312610a0257515f61098c565b3d9150610a26565b90506020813d8211610a73575b81610a64602093836112eb565b81010312610a0257515f610952565b3d9150610a57565b602090604051610a8a816112cf565b5f81525f838201525f60408201525f60608201528282860101520161065a565b634e487b7160e01b5f52604160045260245ffd5b156106ed57610120608001519060a082805181010312610a02576040519160a083019083821067ffffffffffffffff831117610aaa5760a091604052610b0660208201611325565b8452610b1460408201611325565b6020850152610b2560608201611325565b6040850152610b3660808201611325565b6060850190815291015160808401908152610180516040805163cc3802bf60e01b815286516001600160a01b039081166004830152602088015181166024830152919096015181166044870152925183166064860152905160848501525f91849160a4918391165afa918215610a0e575f92610c7a575b508151610bb9816113f8565b90610bc760405192836112eb565b808252610bd6601f19916113f8565b015f5b818110610c4b5750506101e0525f5b8251811015610c435780610c3c81610c0260019487611431565b5160405190610c10826112cf565b81525f60208201525f60408201525f60608201526101606080015190610c368383611431565b52611431565b5001610be8565b5090506106ed565b602090604051610c5a816112cf565b5f81525f838201525f60408201525f606082015282828601015201610bd9565b9091503d805f833e610c8c81836112eb565b810190602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a02578151610cc3816113f8565b92610cd160405194856112eb565b81845260208085019260051b820101928311610a0257602001905b828210610cfd57505050905f610bad565b8151815260209182019101610cec565b5081610635565b61018051636364223f60e01b5f9081526001600160a01b03909116600452602490fd5b506101a051511515610629565b610180516040516335abafe560e21b81526001600160a01b03918216600482015292935060209183916024918391165afa908115610a0e575f91610d8b575b50905f610621565b610dad915060203d602011610db3575b610da581836112eb565b81019061130d565b5f610d83565b503d610d9b565b61018051604051632c77566560e01b81526001600160a01b039182166004820152925060209183916024918391165afa908115610a0e575f91610dff575b505f61060d565b610e18915060203d602011610db357610da581836112eb565b5f610df8565b6040516313bd406b60e21b815260048101829052906020826024816001600160a01b038a165afa8015610a0e575f90610e75575b60019250610e658260e060800151611431565b90838060a01b03169052016105f1565b506020823d8211610ea6575b81610e8e602093836112eb565b81010312610a0257610ea1600192611325565b610e52565b3d9150610e81565b90506020813d602011610ed8575b81610ec9602093836112eb565b81010312610a0257515f6105be565b3d9150610ebc565b610ef9915060203d602011610db357610da581836112eb565b5f610589565b600161058b565b610f1f915060203d602011610db357610da581836112eb565b5f610531565b6001610533565b90506020813d602011610f5e575b81610f47602093836112eb565b81010312610a0257610f5890611325565b5f6104d3565b3d9150610f3a565b90506020813d602011610f98575b81610f81602093836112eb565b81010312610a0257610f9290611325565b5f610499565b3d9150610f74565b610fc2915060203d602011610fc8575b610fba81836112eb565b8101906113d9565b5f610466565b503d610fb0565b610fe8915060203d602011610fc857610fba81836112eb565b5f61042b565b90503d805f833e610fff81836112eb565b810190602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a0257815161103992602001611339565b5f6103f1565b90506020813d602011611071575b8161105a602093836112eb565b81010312610a025761106b90611325565b5f6103b8565b3d915061104d565b61109b915060203d6020116110a1575b61109381836112eb565b8101906113b9565b5f610385565b503d611089565b6110c1915060203d6020116110a15761109381836112eb565b5f610349565b90506020813d6020116110f1575b816110e2602093836112eb565b81010312610a0257515f61030e565b3d91506110d5565b90506020813d602011611123575b81611114602093836112eb565b81010312610a0257515f6102dd565b3d9150611107565b506020813d60201161116c575b81611145602093836112eb565b81010312610a0257516001600160801b0381168103610a02576001600160801b03906102a3565b3d9150611138565b90506020813d6020116111a6575b8161118f602093836112eb565b81010312610a02576111a090611325565b5f61026b565b3d9150611182565b9150916020813d6020116111e7575b816111ca602093836112eb565b81010312610a0257519160ff83168303610a025760ff9291610211565b3d91506111bd565b61120c9192503d805f833e61120481836112eb565b81019061137f565b905f6101e2565b61122791503d805f833e61120481836112eb565b5f6101b4565b90506020813d60201161125f575b81611248602093836112eb565b81010312610a025761125990611325565b5f610187565b3d915061123b565b63634ba39d60e11b5f9081526001600160a01b03918216600452908416602452604490fd5b6112a5915060203d602011610db357610da581836112eb565b5f610153565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b6080810190811067ffffffffffffffff821117610aaa57604052565b90601f8019910116810190811067ffffffffffffffff821117610aaa57604052565b90816020910312610a0257518015158103610a025790565b51906001600160a01b0382168203610a0257565b92919267ffffffffffffffff8211610aaa5760405191611363601f8201601f1916602001846112eb565b829481845281830111610a02578281602093845f96015e010152565b602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a025781516113b692602001611339565b90565b90816020910312610a02575167ffffffffffffffff81168103610a025790565b90816020910312610a0257516001600160601b0381168103610a025790565b67ffffffffffffffff8111610aaa5760051b60200190565b80511561141d5760200190565b634e487b7160e01b5f52603260045260245ffd5b805182101561141d5760209160051b01019056fea2646970667358221220e503bacf467e61444adc05af2f0e81ecb697388cff9d14d825058d01a57b9bf564736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1Adapter.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1Adapter.ts index faebd54d4..423c63f7b 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1Adapter.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1Adapter.ts @@ -88,4 +88,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2MorphoMarketV1Adapter` query bytecode. */ export const code = - "0x60808060405234601557610538908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610310576040366003190112610310576004356001600160a01b0381169190829003610310576024356001600160a01b03811690819003610310576060820182811067ffffffffffffffff82111761045c576040525f825260208201925f845260408301916060835260405163230dbab560e01b8152826004820152602081602481855afa90811561031c575f91610421575b501561040c57506040516307f1b29b60e11b8152602081600481855afa90811561031c575f916103d2575b506001600160a01b0316835260405163388af5b560e01b8152602081600481855afa90811561031c575f91610398575b506001600160a01b0316845260405163b045ff5b60e01b815290602082600481845afa91821561031c575f92610364575b5061015382959493956104c2565b610160604051918261048c565b828152601f1961016f846104c2565b015f5b81811061032757505085525f5b82811061023557505060408051602080825293516001600160a01b03908116858301529451909416908401525091516060808301528051608083018190529192839260a0840192909101905f5b8181106101da575050500390f35b825180516001600160a01b039081168652602082810151821681880152604080840151831690880152606080840151909216918701919091526080918201519186019190915286955060a090940193909201916001016101cc565b604051631f1a892160e11b815260048101829052949593949060a082602481865afa91821561031c575f9261028b575b506102808160019387519061027a83836104da565b526104da565b50019493929461017f565b915060a0823d8211610314575b816102a560a0938361048c565b8101031261031057610280816001936080604051916102c383610470565b6102cc816104ae565b83526102da602082016104ae565b60208401526102eb604082016104ae565b60408401526102fc606082016104ae565b606084015201516080820152935050610265565b5f80fd5b3d9150610298565b6040513d5f823e3d90fd5b6020906040989697985161033a81610470565b5f81525f838201525f60408201525f60608201525f60808201528282860101520196959496610172565b9091506020813d602011610390575b816103806020938361048c565b810103126103105751905f610145565b3d9150610373565b90506020813d6020116103ca575b816103b36020938361048c565b81010312610310576103c4906104ae565b5f610114565b3d91506103a6565b90506020813d602011610404575b816103ed6020938361048c565b81010312610310576103fe906104ae565b5f6100e4565b3d91506103e0565b63634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610454575b8161043c6020938361048c565b8101031261031057518015158103610310575f6100b9565b3d915061042f565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761045c57604052565b90601f8019910116810190811067ffffffffffffffff82111761045c57604052565b51906001600160a01b038216820361031057565b67ffffffffffffffff811161045c5760051b60200190565b80518210156104ee5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220dfb21c34432d601f765bfd2882d5cb1ad3536aa834246292ce550ba864b28ea664736f6c634300081b0033"; + "0x60808060405234601557610538908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610310576040366003190112610310576004356001600160a01b0381169190829003610310576024356001600160a01b03811690819003610310576060820182811067ffffffffffffffff82111761045c576040525f825260208201925f845260408301916060835260405163230dbab560e01b8152826004820152602081602481855afa90811561031c575f91610421575b501561040c57506040516307f1b29b60e11b8152602081600481855afa90811561031c575f916103d2575b506001600160a01b0316835260405163388af5b560e01b8152602081600481855afa90811561031c575f91610398575b506001600160a01b0316845260405163b045ff5b60e01b815290602082600481845afa91821561031c575f92610364575b5061015382959493956104c2565b610160604051918261048c565b828152601f1961016f846104c2565b015f5b81811061032757505085525f5b82811061023557505060408051602080825293516001600160a01b03908116858301529451909416908401525091516060808301528051608083018190529192839260a0840192909101905f5b8181106101da575050500390f35b825180516001600160a01b039081168652602082810151821681880152604080840151831690880152606080840151909216918701919091526080918201519186019190915286955060a090940193909201916001016101cc565b604051631f1a892160e11b815260048101829052949593949060a082602481865afa91821561031c575f9261028b575b506102808160019387519061027a83836104da565b526104da565b50019493929461017f565b915060a0823d8211610314575b816102a560a0938361048c565b8101031261031057610280816001936080604051916102c383610470565b6102cc816104ae565b83526102da602082016104ae565b60208401526102eb604082016104ae565b60408401526102fc606082016104ae565b606084015201516080820152935050610265565b5f80fd5b3d9150610298565b6040513d5f823e3d90fd5b6020906040989697985161033a81610470565b5f81525f838201525f60408201525f60608201525f60808201528282860101520196959496610172565b9091506020813d602011610390575b816103806020938361048c565b810103126103105751905f610145565b3d9150610373565b90506020813d6020116103ca575b816103b36020938361048c565b81010312610310576103c4906104ae565b5f610114565b3d91506103a6565b90506020813d602011610404575b816103ed6020938361048c565b81010312610310576103fe906104ae565b5f6100e4565b3d91506103e0565b63634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610454575b8161043c6020938361048c565b8101031261031057518015158103610310575f6100b9565b3d915061042f565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761045c57604052565b90601f8019910116810190811067ffffffffffffffff82111761045c57604052565b51906001600160a01b038216820361031057565b67ffffffffffffffff811161045c5760051b60200190565b80518210156104ee5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220ded6e791bc86d2589da25a4cd9fd0cc6554083b90f948b1bd94bde469481ee5464736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1AdapterV2.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1AdapterV2.ts index 47237c0b0..0b7635ba8 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1AdapterV2.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1AdapterV2.ts @@ -78,4 +78,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2MorphoMarketV1AdapterV2` query bytecode. */ export const code = - "0x60808060405234601557610552908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610305576040366003190112610305576004356001600160a01b0381169190829003610305576024356001600160a01b03811690819003610305576080820182811067ffffffffffffffff82111761046b576040525f825260208201925f845260408301905f82526060840192606084526040516335abafe560e21b8152826004820152602081602481855afa908115610311575f91610430575b501561041b57506040516307f1b29b60e11b8152602081600481855afa908115610311575f916103fc575b506001600160a01b0316845260405163388af5b560e01b8152602081600481855afa908115610311575f916103dd575b506001600160a01b03168552604051630399e3a560e41b8152602081600481855afa908115610311575f916103ae575b506001600160a01b0316825260405163ace48b4560e01b8152602081600481855afa908115610311575f9161037c575b5061018b8196959493966104dc565b610198604051918261049b565b818152601f196101a7836104dc565b015f5b81811061034f57505083525f5b81811061024157505060408051602080825294516001600160a01b039081168683015295518616918101919091529451909316606085015251608080850152805160a0850181905284935060c084019291909101905f5b81811061021c575050500390f35b825180518552602090810151818601528695506040909401939092019160010161020e565b60409693949596519063779a968360e01b8252806004830152602082602481875afa918215610311575f9261031c575b50604051630dd5aa9b60e31b81526004810183905291602083602481885afa928315610311575f936102da575b50816102ce91600194604051916102b48361047f565b825260208201528851906102c883836104f4565b526104f4565b500195949392956101b7565b92506020833d8211610309575b816102f46020938361049b565b81010312610305579151918161029e565b5f80fd5b3d91506102e7565b6040513d5f823e3d90fd5b9091506020813d8211610347575b816103376020938361049b565b810103126103055751905f610271565b3d915061032a565b60209060409996979899516103638161047f565b5f81525f838201528282860101520197969594976101aa565b90506020813d6020116103a6575b816103976020938361049b565b8101031261030557515f61017c565b3d915061038a565b6103d0915060203d6020116103d6575b6103c8818361049b565b8101906104bd565b5f61014c565b503d6103be565b6103f6915060203d6020116103d6576103c8818361049b565b5f61011c565b610415915060203d6020116103d6576103c8818361049b565b5f6100ec565b63634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610463575b8161044b6020938361049b565b8101031261030557518015158103610305575f6100c1565b3d915061043e565b634e487b7160e01b5f52604160045260245ffd5b6040810190811067ffffffffffffffff82111761046b57604052565b90601f8019910116810190811067ffffffffffffffff82111761046b57604052565b9081602091031261030557516001600160a01b03811681036103055790565b67ffffffffffffffff811161046b5760051b60200190565b80518210156105085760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea26469706673582212202b6841f6278647fbce4a7e04c1e286d66d533ceaccf4cbe6aa91bf714e1623cc64736f6c634300081b0033"; + "0x60808060405234601557610552908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610305576040366003190112610305576004356001600160a01b0381169190829003610305576024356001600160a01b03811690819003610305576080820182811067ffffffffffffffff82111761046b576040525f825260208201925f845260408301905f82526060840192606084526040516335abafe560e21b8152826004820152602081602481855afa908115610311575f91610430575b501561041b57506040516307f1b29b60e11b8152602081600481855afa908115610311575f916103fc575b506001600160a01b0316845260405163388af5b560e01b8152602081600481855afa908115610311575f916103dd575b506001600160a01b03168552604051630399e3a560e41b8152602081600481855afa908115610311575f916103ae575b506001600160a01b0316825260405163ace48b4560e01b8152602081600481855afa908115610311575f9161037c575b5061018b8196959493966104dc565b610198604051918261049b565b818152601f196101a7836104dc565b015f5b81811061034f57505083525f5b81811061024157505060408051602080825294516001600160a01b039081168683015295518616918101919091529451909316606085015251608080850152805160a0850181905284935060c084019291909101905f5b81811061021c575050500390f35b825180518552602090810151818601528695506040909401939092019160010161020e565b60409693949596519063779a968360e01b8252806004830152602082602481875afa918215610311575f9261031c575b50604051630dd5aa9b60e31b81526004810183905291602083602481885afa928315610311575f936102da575b50816102ce91600194604051916102b48361047f565b825260208201528851906102c883836104f4565b526104f4565b500195949392956101b7565b92506020833d8211610309575b816102f46020938361049b565b81010312610305579151918161029e565b5f80fd5b3d91506102e7565b6040513d5f823e3d90fd5b9091506020813d8211610347575b816103376020938361049b565b810103126103055751905f610271565b3d915061032a565b60209060409996979899516103638161047f565b5f81525f838201528282860101520197969594976101aa565b90506020813d6020116103a6575b816103976020938361049b565b8101031261030557515f61017c565b3d915061038a565b6103d0915060203d6020116103d6575b6103c8818361049b565b8101906104bd565b5f61014c565b503d6103be565b6103f6915060203d6020116103d6576103c8818361049b565b5f61011c565b610415915060203d6020116103d6576103c8818361049b565b5f6100ec565b63634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610463575b8161044b6020938361049b565b8101031261030557518015158103610305575f6100c1565b3d915061043e565b634e487b7160e01b5f52604160045260245ffd5b6040810190811067ffffffffffffffff82111761046b57604052565b90601f8019910116810190811067ffffffffffffffff82111761046b57604052565b9081602091031261030557516001600160a01b03811681036103055790565b67ffffffffffffffff811161046b5760051b60200190565b80518210156105085760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea26469706673582212208c72ce6097e17757df43c0f405ac807cbbe835f98f7df5d4f1d29179d2595f1d64736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoVaultV1Adapter.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoVaultV1Adapter.ts index 50a21f618..5e214a36a 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoVaultV1Adapter.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoVaultV1Adapter.ts @@ -61,4 +61,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2MorphoVaultV1Adapter` query bytecode. */ export const code = - "0x608080604052346015576102cb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610234576040366003190112610234576004356001600160a01b0381169190829003610234576024356001600160a01b03811690819003610234576060820182811067ffffffffffffffff821117610240576040525f8252602082015f815260408301915f8352604051632c77566560e01b8152856004820152602081602481855afa9081156101a3575f91610201575b50156101ea575060405163e4baaddf60e01b8152602081600481885afa9081156101a3575f916101cb575b506001600160a01b031683526040516307f1b29b60e11b8152602081600481885afa9485156101a3576004956020925f916101ae575b506001600160a01b0316835260405163388af5b560e01b815295869182905afa80156101a3576060945f91610174575b506001600160a01b03908116835260408051945182168552915181166020850152915190911690820152f35b610196915060203d60201161019c575b61018e8183610254565b810190610276565b5f610148565b503d610184565b6040513d5f823e3d90fd5b6101c59150833d851161019c5761018e8183610254565b5f610118565b6101e4915060203d60201161019c5761018e8183610254565b5f6100e2565b849063634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610238575b8161021c60209383610254565b8101031261023457518015158103610234575f6100b7565b5f80fd5b3d915061020f565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff82111761024057604052565b9081602091031261023457516001600160a01b0381168103610234579056fea264697066735822122085a2452553e2f4801b563c99b5b576d6f292803c08eb7cf6a8f6d9f4b42facd664736f6c634300081b0033"; + "0x608080604052346015576102cb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610234576040366003190112610234576004356001600160a01b0381169190829003610234576024356001600160a01b03811690819003610234576060820182811067ffffffffffffffff821117610240576040525f8252602082015f815260408301915f8352604051632c77566560e01b8152856004820152602081602481855afa9081156101a3575f91610201575b50156101ea575060405163e4baaddf60e01b8152602081600481885afa9081156101a3575f916101cb575b506001600160a01b031683526040516307f1b29b60e11b8152602081600481885afa9485156101a3576004956020925f916101ae575b506001600160a01b0316835260405163388af5b560e01b815295869182905afa80156101a3576060945f91610174575b506001600160a01b03908116835260408051945182168552915181166020850152915190911690820152f35b610196915060203d60201161019c575b61018e8183610254565b810190610276565b5f610148565b503d610184565b6040513d5f823e3d90fd5b6101c59150833d851161019c5761018e8183610254565b5f610118565b6101e4915060203d60201161019c5761018e8183610254565b5f6100e2565b849063634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610238575b8161021c60209383610254565b8101031261023457518015158103610234575f6100b7565b5f80fd5b3d915061020f565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff82111761024057604052565b9081602091031261023457516001600160a01b0381168103610234579056fea26469706673582212208a2b78d082f0a82f3ec4b99f59a2484bdb11271cefce4c01e4af4a1ddcb05a7f64736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts index 9b98b54c1..b9ed65011 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts @@ -41,13 +41,13 @@ export const abi = [ components: [ { internalType: "bool", - name: "canAllocateFromIdle", + name: "canPullFromIdle", type: "bool", }, { - internalType: "uint120", - name: "nativePenalty", - type: "uint120", + internalType: "uint64", + name: "penalty", + type: "uint64", }, { components: [ @@ -68,7 +68,7 @@ export const abi = [ }, { internalType: "bool", - name: "canDeallocate", + name: "canPullFromMarket", type: "bool", }, { @@ -121,4 +121,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2PublicAllocatorConfig` query bytecode. */ export const code = - "0x6080806040523460155761081c908161001a8239f35b5f80fdfe60a0806040526004361015610012575f80fd5b5f3560e01c635938912f14610025575f80fd5b34610309576080366003190112610309576004356001600160a01b038116608081905203610309576024356001600160a01b03811690819003610309576044359067ffffffffffffffff821161030957366023830112156103095781600401359167ffffffffffffffff8311610309573660248460061b83010111610309576064359367ffffffffffffffff851161030957366023860112156103095784600401359067ffffffffffffffff8211610309573660248360051b88010111610309576100ef81610703565b5f815260208101955f8752604082019660608852606083019260608452604051636b97fbcd60e11b81528760048201526060816024816080515afa8015610315575f915f916106a6575b506001600160781b031683521515815261015288610792565b61015f604051918261074f565b888152601f1961016e8a610792565b015f5b81811061067157505089525f5b88811015610398578060061b870190610199602483016107aa565b60405163011f009b60e31b81526001600160a01b038b166004820152604480850135602483018190529294919060209082908190810103816080515afa908115610315578c84915f93610361575b5060405163258969d960e11b81526001600160a01b03909116600482015260248101919091529160208380604481015b03816080515afa928315610315575f93610320575b50602461023991016107aa565b92604051936366faa83960e01b85528d600486015260018060a01b031660248501526020846044816080515afa938415610315575f946102c8575b50928492600196926102c1956040519461028d86610733565b898060a01b031685526020850152604084015215156060830152151560808201528d51906102bb83836107be565b526107be565b500161017e565b92959193506020833d821161030d575b816102e56020938361074f565b81010312610309576001956102c1946102fe8795610771565b955092965092610274565b5f80fd5b3d91506102d8565b6040513d5f823e3d90fd5b9092506020813d8211610359575b8161033b6020938361074f565b8101031261030957602461035161023992610771565b93915061022c565b3d915061032e565b925050506020813d8211610390575b8161037d6020938361074f565b810103126103095751828c6102176101e7565b3d9150610370565b50869550886103a686610792565b6103b3604051918261074f565b868152601f196103c288610792565b015f5b81811061064257505085525f5b868110156105405760248160051b860101359060405191632f0374dd60e21b83528060048401526020836024818d5afa928315610315575f9361050d575b5060405163a68bafa360e01b8152600481018290526020816024818e5afa8015610315575f906104db575b60405163c69507dd60e01b81526004810184905291506020826024818f5afa918215610315575f926104a5575b5091839161049e936001966040519361048085610703565b84526020840152604083015260608201528951906102bb83836107be565b50016103d2565b9150916020823d82116104d3575b816104c06020938361074f565b8101031261030957905190916001610468565b3d91506104b3565b506020813d8211610505575b816104f46020938361074f565b81010312610309576024905161043b565b3d91506104e7565b9092506020813d8211610538575b816105286020938361074f565b810103126103095751918a610410565b3d915061051b565b50604080516020808252935115158482015293516001600160781b0316908401525160806060840152805160a08401819052839260c084019287929101905f5b8181106105ef575050505190601f19838203016080840152602080835192838152019201905f5b8181106105b5575050500390f35b91935091602060806001926060875180518352848101518584015260408101516040840152015160608201520194019101918493926105a7565b825180516001600160a01b03168652602081810151818801526040808301519088015260608083015115159088015260809182015115159187019190915287965060a09095019490920191600101610580565b60209060405161065181610703565b5f81525f838201525f60408201525f6060820152828286010152016103c5565b60209060405161068081610733565b5f81525f838201525f60408201525f60608201525f608082015282828601015201610171565b9150506060813d6060116106fb575b816106c26060938361074f565b81010312610309576001600160781b036106db82610771565b6106f360406106ec6020860161077e565b940161077e565b509190610139565b3d91506106b5565b6080810190811067ffffffffffffffff82111761071f57604052565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761071f57604052565b90601f8019910116810190811067ffffffffffffffff82111761071f57604052565b5190811515820361030957565b51906001600160781b038216820361030957565b67ffffffffffffffff811161071f5760051b60200190565b356001600160a01b03811681036103095790565b80518210156107d25760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220047f1c0eede09b101d0fb8a8e9baf5e9bee81891a384b3c9f6d1cbccb91684ab64736f6c63430008240033"; + "0x60808060405234601557610808908161001a8239f35b5f80fdfe60a0806040526004361015610012575f80fd5b5f3560e01c635938912f14610025575f80fd5b3461030a57608036600319011261030a576004356001600160a01b03811660808190520361030a576024356001600160a01b0381169081900361030a576044359067ffffffffffffffff821161030a573660238301121561030a5781600401359167ffffffffffffffff831161030a573660248460061b8301011161030a576064359367ffffffffffffffff851161030a573660238601121561030a5784600401359067ffffffffffffffff821161030a573660248360051b8801011161030a576100ef81610703565b5f815260208101955f8752604082019660608852606083019260608452604051636b97fbcd60e11b81528760048201526040816024816080515afa8015610316575f915f916106a8575b5067ffffffffffffffff168352151581526101538861077e565b610160604051918261074f565b888152601f1961016f8a61077e565b015f5b81811061067357505089525f5b88811015610399578060061b87019061019a60248301610796565b60405163011f009b60e31b81526001600160a01b038b166004820152604480850135602483018190529294919060209082908190810103816080515afa908115610316578c84915f93610362575b506040516369f1e26b60e01b81526001600160a01b03909116600482015260248101919091529160208380604481015b03816080515afa928315610316575f93610321575b50602461023a9101610796565b92604051936366faa83960e01b85528d600486015260018060a01b031660248501526020846044816080515afa938415610316575f946102c9575b50928492600196926102c2956040519461028e86610733565b898060a01b031685526020850152604084015215156060830152151560808201528d51906102bc83836107aa565b526107aa565b500161017f565b92959193506020833d821161030e575b816102e66020938361074f565b8101031261030a576001956102c2946102ff8795610771565b955092965092610275565b5f80fd5b3d91506102d9565b6040513d5f823e3d90fd5b9092506020813d821161035a575b8161033c6020938361074f565b8101031261030a57602461035261023a92610771565b93915061022d565b3d915061032f565b925050506020813d8211610391575b8161037e6020938361074f565b8101031261030a5751828c6102186101e8565b3d9150610371565b50869550886103a78661077e565b6103b4604051918261074f565b868152601f196103c38861077e565b015f5b81811061064457505085525f5b868110156105415760248160051b860101359060405191632f0374dd60e21b83528060048401526020836024818d5afa928315610316575f9361050e575b5060405163a68bafa360e01b8152600481018290526020816024818e5afa8015610316575f906104dc575b60405163c69507dd60e01b81526004810184905291506020826024818f5afa918215610316575f926104a6575b5091839161049f936001966040519361048185610703565b84526020840152604083015260608201528951906102bc83836107aa565b50016103d3565b9150916020823d82116104d4575b816104c16020938361074f565b8101031261030a57905190916001610469565b3d91506104b4565b506020813d8211610506575b816104f56020938361074f565b8101031261030a576024905161043c565b3d91506104e8565b9092506020813d8211610539575b816105296020938361074f565b8101031261030a5751918a610411565b3d915061051c565b506040805160208082529351151584820152935167ffffffffffffffff16908401525160806060840152805160a08401819052839260c084019287929101905f5b8181106105f1575050505190601f19838203016080840152602080835192838152019201905f5b8181106105b7575050500390f35b91935091602060806001926060875180518352848101518584015260408101516040840152015160608201520194019101918493926105a9565b825180516001600160a01b03168652602081810151818801526040808301519088015260608083015115159088015260809182015115159187019190915287965060a09095019490920191600101610582565b60209060405161065381610703565b5f81525f838201525f60408201525f6060820152828286010152016103c6565b60209060405161068281610733565b5f81525f838201525f60408201525f60608201525f608082015282828601015201610172565b9150506040813d6040116106fb575b816106c46040938361074f565b8101031261030a5760206106d782610771565b9101519067ffffffffffffffff8216820361030a579067ffffffffffffffff610139565b3d91506106b7565b6080810190811067ffffffffffffffff82111761071f57604052565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761071f57604052565b90601f8019910116810190811067ffffffffffffffff82111761071f57604052565b5190811515820361030a57565b67ffffffffffffffff811161071f5760051b60200190565b356001600160a01b038116810361030a5790565b80518210156107be5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220a77aa844da5d389376ee1433d996e0bb253977abafbd38ee8232086c67b7d22164736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts b/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts index 520be61e1..3b9d68b0a 100644 --- a/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts +++ b/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts @@ -37,7 +37,7 @@ export const abi = [ type: "bytes32", }, ], - name: "canDeallocate", + name: "canPullFromMarket", outputs: [ { internalType: "bool", @@ -113,7 +113,7 @@ export const abi = [ type: "bool", }, ], - name: "setCanDeallocate", + name: "setCanPullFromMarket", outputs: [], stateMutability: "nonpayable", type: "function", @@ -150,13 +150,13 @@ export const abi = [ }, { internalType: "bool", - name: "canAllocateFromIdle", + name: "canPullFromIdle", type: "bool", }, { - internalType: "uint120", - name: "nativePenalty", - type: "uint120", + internalType: "uint64", + name: "penalty", + type: "uint64", }, ], name: "setVaultData", @@ -176,18 +176,13 @@ export const abi = [ outputs: [ { internalType: "bool", - name: "canAllocateFromIdle", + name: "canPullFromIdle", type: "bool", }, { - internalType: "uint120", - name: "nativePenalty", - type: "uint120", - }, - { - internalType: "uint120", - name: "accruedNativePenalty", - type: "uint120", + internalType: "uint64", + name: "penalty", + type: "uint64", }, ], stateMutability: "view", @@ -197,4 +192,4 @@ export const abi = [ /** @internal Deployless `BluePublicAllocatorReadFixture` query bytecode. */ export const code = - "0x60808060405234601557610410908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c908163065b6543146102af5750806308f804d81461026c578063391a1d161461022c5780634b12d3b2146101e357806366faa8391461018c5780638aeed1d114610129578063c4b956c3146100d25763d72ff79a14610074575f80fd5b346100ce5760203660031901126100ce576001600160a01b0361009561039f565b165f526003602052606060405f20546001600160781b036040519160ff811615158352818160081c16602084015260801c166040820152f35b5f80fd5b346100ce5760603660031901126100ce576101276100ee61039f565b6100f66103cb565b9060018060a01b03165f52600160205260405f206024355f5260205260405f209060ff801983541691151516179055565b005b346100ce5760603660031901126100ce5761012761014561039f565b61014d6103b5565b6101556103cb565b9160018060a01b03165f52600260205260405f209060018060a01b03165f5260205260405f209060ff801983541691151516179055565b346100ce5760403660031901126100ce576101a561039f565b6101ad6103b5565b9060018060a01b03165f52600260205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b346100ce5760403660031901126100ce576001600160a01b0361020461039f565b165f52600160205260405f206024355f52602052602060ff60405f2054166040519015158152f35b346100ce5760603660031901126100ce576001600160a01b0361024d61039f565b165f525f60205260405f206024355f5260205260443560405f20555f80f35b346100ce5760403660031901126100ce576001600160a01b0361028d61039f565b165f525f60205260405f206024355f52602052602060405f2054604051908152f35b346100ce5760603660031901126100ce576102c861039f565b6024358015158091036100ce57604435906001600160781b0382168092036100ce576060840184811067ffffffffffffffff82111761038b5760405283526020830190815260408301915f835260018060a01b03165f52600360205261034160405f2093511515849060ff801983541691151516179055565b5182549151610100600160f81b031990921660089190911b6fffffffffffffffffffffffffffffff00161760809190911b6effffffffffffffffffffffffffffff60801b16179055005b634e487b7160e01b5f52604160045260245ffd5b600435906001600160a01b03821682036100ce57565b602435906001600160a01b03821682036100ce57565b6044359081151582036100ce5756fea2646970667358221220dd8548f38071fa9b1f6ac957439322ff1f394071a4122a5efa4668a8d925d21564736f6c63430008240033"; + "0x608080604052346015576103d8908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c90816308f804d81461032957508063391a1d16146102e95780634d29c2d81461029457806366faa8391461023d57806369f1e26b146101f45780638aeed1d11461018f578063d72ff79a146101415763e156c1a814610074575f80fd5b3461013d57606036600319011261013d5761008d610367565b6024359081151580920361013d576044359067ffffffffffffffff821680920361013d57604051926040840184811067ffffffffffffffff8211176101295760405283526020830191825260018060a01b03165f52600360205261010460405f2092511515839060ff801983541691151516179055565b51815468ffffffffffffffff00191660089190911b68ffffffffffffffff0016179055005b634e487b7160e01b5f52604160045260245ffd5b5f80fd5b3461013d57602036600319011261013d576001600160a01b03610162610367565b165f5260036020526040805f205467ffffffffffffffff82519160ff81161515835260081c166020820152f35b3461013d57606036600319011261013d576101f26101ab610367565b6101b361037d565b6101bb610393565b9160018060a01b03165f52600260205260405f209060018060a01b03165f5260205260405f209060ff801983541691151516179055565b005b3461013d57604036600319011261013d576001600160a01b03610215610367565b165f52600160205260405f206024355f52602052602060ff60405f2054166040519015158152f35b3461013d57604036600319011261013d57610256610367565b61025e61037d565b9060018060a01b03165f52600260205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b3461013d57606036600319011261013d576101f26102b0610367565b6102b8610393565b9060018060a01b03165f52600160205260405f206024355f5260205260405f209060ff801983541691151516179055565b3461013d57606036600319011261013d576001600160a01b0361030a610367565b165f525f60205260405f206024355f5260205260443560405f20555f80f35b3461013d57604036600319011261013d576020906001600160a01b0361034d610367565b165f525f825260405f206024355f52825260405f20548152f35b600435906001600160a01b038216820361013d57565b602435906001600160a01b038216820361013d57565b60443590811515820361013d5756fea26469706673582212207e4c910413ca026005bc13c4f22d88556290c586ac09157584e6975b6ac52f5464736f6c63430008240033"; diff --git a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts index 23ee39e1b..93ccf0d89 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts @@ -6,10 +6,10 @@ export interface VaultV2PublicAllocatorConfig { readonly allocator: Address; /** Configured Vault V2 address. */ readonly vault: Address; - /** Whether the allocator may move the vault's idle assets into a Blue market. */ - readonly canAllocateFromIdle: boolean; - /** Native-token penalty charged for each allocator call. */ - readonly nativePenalty: bigint; + /** Whether the allocator may pull the vault's idle assets into a Blue market. */ + readonly canPullFromIdle: boolean; + /** Proportional vault-asset penalty charged for each call, scaled by WAD. */ + readonly penalty: bigint; } /** Public allocator permissions and cap for one Vault V2 adapter-market pair. */ @@ -24,8 +24,8 @@ export interface VaultV2MarketPublicAllocatorConfig { readonly marketParamsId: Hash; /** Maximum post-state allocation accepted by the allocator. */ readonly absoluteCap: bigint; - /** Whether the allocator may deallocate this adapter-market pair. */ - readonly canDeallocate: boolean; + /** Whether the allocator may pull assets from this adapter-market pair. */ + readonly canPullFromMarket: boolean; /** Whether the allocator currently recognizes the adapter. */ readonly isActiveAdapter: boolean; } diff --git a/packages/liquidity-sdk-viem/README.md b/packages/liquidity-sdk-viem/README.md index add30ffde..b07b75387 100644 --- a/packages/liquidity-sdk-viem/README.md +++ b/packages/liquidity-sdk-viem/README.md @@ -68,7 +68,7 @@ const client = createPublicClient({ chain: mainnet, transport: http() }); const loader = new VaultV2LiquidityLoader(client, { allocator: "0x0000000000000000000000000000000000000001", vaults: ["0x0000000000000000000000000000000000000002"], - maxNativePenalty: 1_000_000_000_000_000n, + maxPenalty: 1_000_000_000_000_000n, }); const marketId = "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc" as MarketId; diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts index b9411e76c..b94574364 100644 --- a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts @@ -244,7 +244,7 @@ const setupClient = () => { address: ALLOCATOR, abi: vaultV2BluePublicAllocatorAbi, functionName: "vaultData", - result: [true, 12n, 0n], + result: [true, 12n], }); mockRead(handle, { address: ALLOCATOR, @@ -255,7 +255,7 @@ const setupClient = () => { mockRead(handle, { address: ALLOCATOR, abi: vaultV2BluePublicAllocatorAbi, - functionName: "canDeallocate", + functionName: "canPullFromMarket", result: false, }); mockRead(handle, { @@ -309,7 +309,7 @@ describe.sequential("VaultV2LiquidityLoader", () => { from: { type: "idle" }, to: { adapter: ADAPTER }, assets: 100n, - nativePenalty: 12n, + penalty: 12n, }, ]); expect(result.endState.getMarket(marketParams.id).totalSupplyAssets).toBe( @@ -331,13 +331,13 @@ describe.sequential("VaultV2LiquidityLoader", () => { api.done(); }); - test("behavior: filters vaults above the maximum native penalty", async () => { + test("behavior: filters vaults above the maximum penalty", async () => { const api = setupApi(); const { client } = setupClient(); const loader = new VaultV2LiquidityLoader(client, { allocator: ALLOCATOR, vaults: [VAULT], - maxNativePenalty: 11n, + maxPenalty: 11n, deployless: false, }); diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts index 2a4eabc2f..53ecaac1d 100644 --- a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts @@ -49,8 +49,8 @@ export interface VaultV2LiquidityParameters { /** Vault V2 addresses whose reallocatable liquidity should be considered. */ readonly vaults: readonly Address[]; - /** Maximum native-token penalty accepted per BluePublicAllocator call. */ - readonly maxNativePenalty?: bigint; + /** Maximum WAD-scaled vault-asset penalty accepted per BluePublicAllocator call. */ + readonly maxPenalty?: bigint; /** Deployless read mode forwarded to allocator reads and RPC fallbacks. Defaults to `true` with direct-read fallback. */ readonly deployless?: boolean | "force"; @@ -98,7 +98,7 @@ export interface VaultV2LiquidityResult { * const loader = new VaultV2LiquidityLoader(client, { * allocator, * vaults: [vault], - * maxNativePenalty: 1_000_000_000_000_000n, + * maxPenalty: 1_000_000_000_000_000n, * }); * return loader.fetch(marketId); * } @@ -432,7 +432,7 @@ export class VaultV2LiquidityLoader { startState.computeVaultV2Reallocations(market.id, { timestamp: block.timestamp + REALLOCATION_SIMULATION_DELAY, reallocatableVaults: loaderParameters.vaults, - maxNativePenalty: loaderParameters.maxNativePenalty, + maxPenalty: loaderParameters.maxPenalty, }); return { diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index fb48612b8..5f024d54d 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -27,7 +27,7 @@ Protocol terms used across this package's docs and JSDoc: - **bundler3** — the bundler entry point; receives a sequence of adapter actions in one transaction. - **GeneralAdapter1** — the bundler-side adapter that holds approvals/auth and executes Morpho calls on the user's behalf. Required as the spender for ERC-20 approvals on every bundled path; required as authorized operator on Morpho for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, and `withdraw` (the supplier-side path). - **PublicAllocator V1** — MetaMorpho allocator that moves liquidity from one or more sorted source markets into a target via `reallocateTo(...)`; each call pays one `fee`. -- **BluePublicAllocator** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered; each call pays its own `nativePenalty`. Its canonical ABI export is `vaultV2BluePublicAllocatorAbi`. +- **BluePublicAllocator** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered. Each call passes the vault's configured WAD-scaled `uint64 penalty`; the allocator pulls `ceil(assets × penalty / WAD)` of the target loan token from Bundler3 and donates it directly to the vault. Its canonical ABI export is `vaultV2BluePublicAllocatorAbi`. - **VaultExitBundlesV1** — standalone periphery for exiting an illiquid VaultV1 or single-adapter VaultV2 into idle underlying assets and/or Morpho Blue supply positions. - **Shared-liquidity naming** — `VaultV1ReallocationData`, `InputVaultV1ReallocationData`, `computeVaultV1Reallocations`, and `VaultV1BlueReallocation` are canonical for PublicAllocator V1. Their published predecessors (`ReallocationData`, `InputReallocationData`, `computeReallocations`, `getMarketPublicReallocations`, and `VaultReallocation`) remain deprecated aliases. `VaultV2ReallocationData.computeVaultV2Reallocations` and `computeVaultV2Reallocations` accept `VaultV2BluePublicAllocatorOptions` and produce flat, action-ready `VaultV2BlueReallocation` calls. @@ -42,7 +42,7 @@ The action verbs an integrator sees in the bundle (`BundlerAction.encode...`): - **`nativeTransfer` + `wrapNative`** — pair that converts an attached native amount (`tx.value`) into the chain's wNative for a deposit/supply path. - **`forceDeallocate`** — VaultV2 multicall entry that pulls liquidity out of a specific adapter before withdraw/redeem. - **`reallocateTo`** — PublicAllocator V1 call that shifts liquidity from sorted source markets into the target market. -- **`vaultV2BluePublicAllocatorReallocate` / `vaultV2BluePublicAllocatorAllocateFromIdle`** — BluePublicAllocator calls that move one market source or vault idle liquidity into the enclosing Blue action's target market. Both target an explicit allocator address and carry one native penalty. +- **`vaultV2BluePublicAllocatorReallocate` / `vaultV2BluePublicAllocatorAllocateFromIdle`** — BluePublicAllocator calls that move one market source or vault idle liquidity into the enclosing Blue action's target market. Both target an explicit allocator address, approve the exact loan-token penalty from Bundler3, and carry the configured penalty rate in calldata. ### Constants and conventions diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index b745d0da1..f351e5f6d 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -41,6 +41,8 @@ Concretely, `blueSupplyCollateralBorrow` is not a new contract: it is simply the | Blue `supplyCollateral` | Bundler3 → GeneralAdapter1 | _(opt)_ `nativeTransfer` + `wrapNative` → `erc20TransferFrom` → `morphoSupplyCollateral` | | Blue `borrow` | Bundler3 → GeneralAdapter1 | _(opt)_ allocator reallocations → `morphoBorrow` _(requires `setAuthorization` for GA1 on Morpho)_ | | Blue `supplyCollateralBorrow` | Bundler3 → GeneralAdapter1 | `erc20TransferFrom` → `morphoSupplyCollateral` → _(opt)_ allocator reallocations → `morphoBorrow` | +| Blue `withdraw` | Bundler3 → GeneralAdapter1 | _(opt)_ allocator reallocations → `morphoWithdraw` | +| Blue `refinance` | Bundler3 → GeneralAdapter1 | _(opt)_ target allocator reallocations → `morphoSupplyCollateral` with the borrow/repay/withdraw callback | | Blue `repay` | Bundler3 → GeneralAdapter1 | `erc20TransferFrom` → `morphoRepay` (by `assets` or by `shares`) | | Blue `repayWithdrawCollateral` | Bundler3 → GeneralAdapter1 | `erc20TransferFrom` → `morphoRepay` → `morphoWithdrawCollateral` _(repay **before** withdraw, order is critical)_ | | VaultV1 `withdraw` / `redeem` | **Direct vault call** | _(no bundler, no adapter)_ | @@ -71,10 +73,13 @@ For every ERC-4626 deposit (VaultV1 / VaultV2), GeneralAdapter1 calls `erc4626De `BlueReallocation`s encode as Public Allocator V1 `reallocateTo` calls or Blue Public Allocator `reallocate`/`allocateFromIdle` calls. The same array may contain both, so Vault V1 and Vault V2 liquidity can be reallocated atomically in one Bundler3 transaction. They are **prepended to the -bundle** (borrow and withdraw) or **inserted between supply-collateral and borrow** -(`supplyCollateralBorrow`). `BundlerAction.encodeBundle` aggregates Public Allocator V1 fees and -Blue Public Allocator native penalties into `tx.value`. No extra off-chain machinery is required: -everything flows through the same bundler-action composition. +bundle** for borrow and loan-asset withdraw, **inserted between supply-collateral and borrow** for +`supplyCollateralBorrow`, and run **before the supply-collateral callback** for `blueRefinance`. +`BundlerAction.encodeBundle` includes Public Allocator V1 fees in `tx.value`. Blue Public Allocator +penalties are different: the bundle pulls the aggregate amount in the target loan token through +GeneralAdapter1, approves each exact per-call amount from Bundler3, and lets the allocator donate +it directly to the vault. The entity's `getRequirements()` returns the corresponding classic +loan-token approval when a V2 penalty is non-zero. ### 5. A single approval surface @@ -109,7 +114,7 @@ This is the main design caveat. For the following operations the SDK emits a **d - **Blue authorization for GA1 required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`.** A user who has never granted it will receive a requirement through [`getBlueAuthorizationRequirement`](src/actions/requirements/blue/getBlueAuthorizationRequirement.ts). Without signature support, this is a `setAuthorization` transaction to execute beforehand. With `supportSignature`, this is a signable requirement; pass the resulting `AuthorizationRequirementSignature` to `buildTx`, which folds it into the bundle as `setAuthorizationWithSig`. - **Critical order in `repayWithdrawCollateral`**: `morphoRepay` **must** precede `morphoWithdrawCollateral` in the bundle, otherwise the position is deemed unhealthy at withdraw time and the tx reverts. - **Builder must equal signer.** Bundler actions reference accounts in two different ways: some take an explicit `onBehalf` and act on `userAddress` (e.g. `morphoRepay`), others act implicitly on the **initiator** — the `msg.sender` of `bundler3.multicall`, i.e. the EOA signing the tx, not the adapter — (e.g. `erc20TransferFrom`, `morphoWithdrawCollateral`, the latter exposing no `onBehalf` parameter on GA1). `repayWithdrawCollateral` is the canonical example: the repay leg targets `userAddress` while the transfer-from and the withdraw target the initiator. If the address that built the tx (and filled `userAddress`) is not the address that signs/executes it, the bundle would repay one account's debt while pulling tokens from and withdrawing collateral against the signer. Transaction builders do not validate this at build time — callers MUST keep `userAddress` aligned with the signing account. The signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) take a `WalletClient` and enforce this at `sign()` time via `validateUserAddress` (throws `MissingClientPropertyError` / `AddressMismatchError`). -- **Tricky `tx.value`**: whenever a `nativeAmount`, Public Allocator V1 `reallocateTo` fee, or Blue Public Allocator native penalty is involved, `BundlerAction.encodeBundle` computes `tx.value`. Do not overwrite it on the caller side. +- **Tricky `tx.value`**: `BundlerAction.encodeBundle` computes native value for `nativeAmount` and Public Allocator V1 `reallocateTo` fees. Blue Public Allocator penalties are ERC-20 loan-token amounts and never contribute to `tx.value`. Do not overwrite the encoded value on the caller side. - **Chain-specific Bundler3 address**: always resolve through `getChainAddresses(chainId)` and validate that the viem client's `chainId` matches the params. ## Code references diff --git a/packages/morpho-sdk/README.md b/packages/morpho-sdk/README.md index 2ce2e8888..6440e26eb 100644 --- a/packages/morpho-sdk/README.md +++ b/packages/morpho-sdk/README.md @@ -269,7 +269,8 @@ graph LR style MM fill:#fff3e0,stroke:#ff9800 style V2C fill:#e3f2fd,stroke:#2196f3 style REQ fill:#f3e5f5,stroke:#9c27b0 - style PA fill:#fff9c4,stroke:#f9a825 + style PA1 fill:#fff9c4,stroke:#f9a825 + style BPA fill:#fff9c4,stroke:#f9a825 ``` ## Development diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index b85ac93da..633fb48a8 100644 --- a/packages/morpho-sdk/src/actions/AGENTS.md +++ b/packages/morpho-sdk/src/actions/AGENTS.md @@ -24,7 +24,7 @@ Only valid for assets/collateral configured as wNative. When `nativeAmount > 0`: ## Shared liquidity / reallocations (canonical statement) -`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultV1BlueReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. Tagged `VaultV2BlueReallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call pays its own `nativePenalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. `BundlerAction.encodeBundle` sums V1 fees and BluePublicAllocator penalties into `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level or BluePublicAllocator-source discriminators. +`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultV1BlueReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. Tagged `VaultV2BlueReallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call passes the vault's configured WAD-scaled `penalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. High-level builders pull the aggregate V2 penalty in the target loan token through GeneralAdapter1, then each low-level allocator action approves and spends its independently rounded `ceil(assets × penalty / WAD)` amount from Bundler3. Only V1 fees contribute to `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level or BluePublicAllocator-source discriminators, penalties above WAD, and inconsistent penalties for the same allocator-vault pair. ## Discriminated unions diff --git a/packages/morpho-sdk/src/actions/blue/AGENTS.md b/packages/morpho-sdk/src/actions/blue/AGENTS.md index febb29b49..0d73a1ef0 100644 --- a/packages/morpho-sdk/src/actions/blue/AGENTS.md +++ b/packages/morpho-sdk/src/actions/blue/AGENTS.md @@ -28,20 +28,22 @@ ERC-20 approval spender is **GeneralAdapter1** for any bundled path — never th | `supplyCollateral` (ERC-20) | `erc20TransferFrom → morphoSupplyCollateral` | | `supplyCollateral` (native) | `nativeTransfer → wrapNative → [erc20TransferFrom?] → morphoSupplyCollateral` | | `borrow` | `morphoBorrow` | -| `borrow` (with reallocations) | `[allocator reallocation × N] → morphoBorrow` | +| `borrow` (with reallocations) | `[V2 penalty transfer?] → [allocator reallocation × N] → morphoBorrow` | | `supplyCollateralBorrow` | `[nativeWrap?] → [erc20Transfer?] → morphoSupplyCollateral → morphoBorrow` | -| `supplyCollateralBorrow` (with reallocations) | `[nativeWrap?] → [erc20Transfer?] → morphoSupplyCollateral → [allocator reallocation × N] → morphoBorrow` | +| `supplyCollateralBorrow` (with reallocations) | `[nativeWrap?] → [collateral transfer?] → morphoSupplyCollateral → [V2 penalty transfer?] → [allocator reallocation × N] → morphoBorrow` | | `repay` (ERC-20) | `[erc20TransferFrom \| permit/permit2] → morphoRepay → [erc20Transfer skim (shares mode)]` | | `repay` (native) | `nativeTransfer → wrapNative → [erc20TransferFrom?] → morphoRepay → [skim (shares mode)]` | | `repayWithdrawCollateral` (ERC-20) | `[erc20TransferFrom \| permit/permit2] → morphoRepay → [skim (shares mode)] → morphoWithdrawCollateral` | | `repayWithdrawCollateral` (native) | `nativeTransfer → wrapNative → [erc20TransferFrom?] → morphoRepay → [skim (shares mode)] → morphoWithdrawCollateral` | | `withdraw` | `morphoWithdraw` | -| `withdraw` (with reallocations) | `[allocator reallocation × N] → morphoWithdraw` | +| `withdraw` (with reallocations) | `[V2 penalty transfer?] → [allocator reallocation × N] → morphoWithdraw` | An allocator reallocation is PublicAllocator V1 `reallocateTo` or BluePublicAllocator `reallocate`/`allocateFromIdle` according to the `BlueReallocation` discriminator. One bundle may -mix both allocator contracts. `BundlerAction.encodeBundle` derives `tx.value` from native wrapping -calls, PublicAllocator V1 fees, and BluePublicAllocator native penalties. +mix both allocator contracts. For non-zero V2 penalties, the builder adds one aggregate loan-token +`erc20TransferFrom` into Bundler3 and each allocator action expands to an exact token approval plus +the nonpayable allocator call. `BundlerAction.encodeBundle` derives `tx.value` only from native +wrapping calls and PublicAllocator V1 native fees. ## Mode and ordering rules diff --git a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts index 6a9e56664..f606a7c7f 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -1,6 +1,6 @@ -import { ChainId, MarketParams } from "@morpho-org/blue-sdk"; +import { ChainId, getChainAddresses, MarketParams } from "@morpho-org/blue-sdk"; import { vaultV2BluePublicAllocatorAbi as canonicalVaultV2BluePublicAllocatorAbi } from "@morpho-org/blue-sdk-viem"; -import { decodeFunctionData } from "viem"; +import { decodeFunctionData, erc20Abi } from "viem"; import { describe, expect, test } from "vitest"; import { bundler3Abi, @@ -8,7 +8,10 @@ import { publicAllocatorAbi, vaultV2BluePublicAllocatorAbi, } from "../../abis.js"; -import type { BlueReallocation } from "../../types/index.js"; +import { + type BlueReallocation, + InconsistentReallocationPenaltyError, +} from "../../types/index.js"; import { blueBorrow } from "./borrow.js"; const allocator = "0x0000000000000000000000000000000000000011"; @@ -36,6 +39,9 @@ const sourceMarket = new MarketParams({ describe("blueBorrow Blue Public Allocator", () => { test("default", () => { + const { + bundler3: { bundler3 }, + } = getChainAddresses(ChainId.EthMainnet); const reallocations: readonly BlueReallocation[] = [ { type: "publicAllocatorV1", @@ -54,7 +60,7 @@ describe("blueBorrow Blue Public Allocator", () => { }, to: { adapter: targetAdapter }, assets: 3n, - nativePenalty: 5n, + penalty: 5n, }, { type: "bluePublicAllocator", @@ -63,7 +69,7 @@ describe("blueBorrow Blue Public Allocator", () => { from: { type: "idle" }, to: { adapter: targetAdapter }, assets: 7n, - nativePenalty: 11n, + penalty: 5n, }, ]; @@ -77,35 +83,50 @@ describe("blueBorrow Blue Public Allocator", () => { }, }); - expect(tx.value).toBe(18n); - expect(tx.action.args.reallocationFee).toBe(18n); + expect(tx.value).toBe(2n); + expect(tx.action.args.reallocationFee).toBe(2n); + expect(tx.action.args.reallocationPenaltyAssets).toBe(2n); const bundle = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); const calls = bundle.args[0] ?? []; - expect(calls).toHaveLength(4); - expect(calls.slice(0, 3).map((call) => call.value)).toEqual([2n, 5n, 11n]); - expect(calls.slice(0, 3).map((call) => call.skipRevert)).toEqual([ - false, - false, - false, + expect(calls).toHaveLength(7); + expect(calls.map((call) => call.value)).toEqual([ + 0n, + 2n, + 0n, + 0n, + 0n, + 0n, + 0n, ]); + expect(calls.every((call) => call.skipRevert === false)).toBe(true); + + expect( + decodeFunctionData({ abi: generalAdapter1Abi, data: calls[0]!.data }), + ).toMatchObject({ + functionName: "erc20TransferFrom", + args: [targetMarket.loanToken, bundler3, 2n], + }); const publicAllocatorCall = decodeFunctionData({ abi: publicAllocatorAbi, - data: calls[0]!.data, + data: calls[1]!.data, }); expect(publicAllocatorCall.functionName).toBe("reallocateTo"); expect(publicAllocatorCall.args[0]).toBe(vaultV1); expect( decodeFunctionData({ abi: vaultV2BluePublicAllocatorAbi, - data: calls[1]!.data, + data: calls[3]!.data, }).functionName, ).toBe("reallocate"); + expect( + decodeFunctionData({ abi: erc20Abi, data: calls[2]!.data }), + ).toMatchObject({ functionName: "approve", args: [allocator, 1n] }); const idleCall = decodeFunctionData({ abi: vaultV2BluePublicAllocatorAbi, - data: calls[2]!.data, + data: calls[5]!.data, }); expect(idleCall.functionName).toBe("allocateFromIdle"); expect(idleCall.args[0]).toBe(vaultV2); @@ -118,12 +139,53 @@ describe("blueBorrow Blue Public Allocator", () => { lltv: targetMarket.lltv, }); expect(idleCall.args[3]).toBe(7n); + expect(idleCall.args[4]).toBe(5n); expect( - decodeFunctionData({ abi: generalAdapter1Abi, data: calls[3]!.data }) + decodeFunctionData({ abi: erc20Abi, data: calls[4]!.data }), + ).toMatchObject({ functionName: "approve", args: [allocator, 1n] }); + expect( + decodeFunctionData({ abi: generalAdapter1Abi, data: calls[6]!.data }) .functionName, ).toBe("morphoBorrow"); }); + test("error: InconsistentReallocationPenaltyError", () => { + expect(() => + blueBorrow({ + market: { chainId: ChainId.EthMainnet, marketParams: targetMarket }, + args: { + amount: 1n, + minSharePrice: 0n, + receiver, + reallocations: [ + { + type: "bluePublicAllocator", + allocator, + vault: vaultV2, + from: { + type: "market", + adapter: sourceAdapter, + marketParams: sourceMarket, + }, + to: { adapter: targetAdapter }, + assets: 3n, + penalty: 5n, + }, + { + type: "bluePublicAllocator", + allocator, + vault: vaultV2, + from: { type: "idle" }, + to: { adapter: targetAdapter }, + assets: 7n, + penalty: 11n, + }, + ], + }, + }), + ).toThrow(InconsistentReallocationPenaltyError); + }); + test("re-exports the canonical ABI", () => { expect(vaultV2BluePublicAllocatorAbi).toBe( canonicalVaultV2BluePublicAllocatorAbi, diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index 6051c1847..23cc93bc3 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -49,7 +49,8 @@ export interface BlueBorrowParams { * * When `reallocations` are provided, Public Allocator V1 entries encode `reallocateTo`, while V2 * market and idle entries encode `reallocate` and `allocateFromIdle`. The calls run before the - * borrow, and V1 fees plus V2 native penalties accumulate in `tx.value`. + * borrow. V1 fees accumulate in `tx.value`; V2 penalties are paid in the + * target loan token and donated directly to each vault. * * @param params.market.chainId - The chain the market lives on. * @param params.market.marketParams - Market params (loanToken, collateralToken, oracle, irm, lltv). @@ -65,10 +66,11 @@ export interface BlueBorrowParams { * typed `action` discriminator the simulation layer consumes. * @throws {NonPositiveInputError} when `amount <= 0n` or any reallocation withdrawal amount * is non-positive. - * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. - * @throws {NegativeInputError} when `minSharePrice < 0n`, a V1 fee, or a V2 native penalty is negative. + * @throws {NegativeInputError} when `minSharePrice < 0n`, a V1 fee, or a V2 penalty is negative. * @throws {EmptyReallocationWithdrawalsError} from `buildReallocationActions` when any * `reallocation.withdrawals` is empty. * @throws {ReallocationWithdrawalOnTargetMarketError} from `buildReallocationActions` when any @@ -111,15 +113,21 @@ export const blueBorrow = ({ const actions: Action[] = []; let reallocationFee = 0n; + let reallocationPenaltyAssets = 0n; if (authorizationSignature) { actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } if (reallocations && reallocations.length > 0) { - const result = buildReallocationActions(reallocations, marketParams); + const result = buildReallocationActions({ + chainId, + reallocations, + targetMarketParams: marketParams, + }); actions.push(...result.actions); reallocationFee = result.fee; + reallocationPenaltyAssets = result.penaltyAssets; } actions.push({ @@ -143,6 +151,7 @@ export const blueBorrow = ({ receiver, minSharePrice, reallocationFee, + reallocationPenaltyAssets, }, }, }); diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 4a707a94b..6dd36735f 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -1,37 +1,64 @@ -import type { MarketParams } from "@morpho-org/blue-sdk"; +import { getChainAddresses, type MarketParams } from "@morpho-org/blue-sdk"; import type { Action } from "../../bundler/index.js"; +import { computeVaultV2ReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; import { validateReallocations } from "../../helpers/index.js"; import type { BlueReallocation } from "../../types/index.js"; /** - * Builds Public Allocator V1 and Blue Public Allocator actions and computes their native cost. + * Builds Public Allocator V1 and Blue Public Allocator actions and their costs. * * PublicAllocator V1 entries preserve their `reallocateTo` ABI and validation. Each * BluePublicAllocator entry maps 1:1 to either `reallocate` for a market source or * `allocateFromIdle` for an idle source. The enclosing Blue action supplies the target market - * parameters. + * parameters. V2 penalties are pulled once in the target loan token to + * Bundler3; each allocator action then approves and spends its independently + * rounded share. * - * @param reallocations - PublicAllocator V1 and BluePublicAllocator reallocations in execution order. - * @param targetMarketParams - Target market params derived from the enclosing Blue action. - * @returns Encoded actions and the sum of PublicAllocator V1 fees plus BluePublicAllocator native penalties. - * @throws {NegativeInputError} when a PublicAllocator V1 fee or BluePublicAllocator native penalty is negative. + * @param params - Reallocation encoding inputs. + * @param params.chainId - Chain where the bundle will execute. + * @param params.reallocations - PublicAllocator V1 and BluePublicAllocator reallocations in execution order. + * @param params.targetMarketParams - Target market params derived from the enclosing Blue action. + * @returns Encoded actions, the native V1 fee, and the V2 loan-token penalty total. + * @throws {NegativeInputError} when a PublicAllocator V1 fee or BluePublicAllocator penalty is negative. * @throws {EmptyReallocationWithdrawalsError} when a PublicAllocator V1 reallocation has no withdrawals. * @throws {NonPositiveInputError} when a PublicAllocator V1 withdrawal or BluePublicAllocator asset amount is non-positive. - * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when PublicAllocator V1 withdrawals are not strictly market-id sorted. * @internal */ -export const buildReallocationActions = ( - reallocations: readonly BlueReallocation[], - targetMarketParams: MarketParams, -): { readonly actions: Action[]; readonly fee: bigint } => { +export const buildReallocationActions = ({ + chainId, + reallocations, + targetMarketParams, +}: { + readonly chainId: number; + readonly reallocations: readonly BlueReallocation[]; + readonly targetMarketParams: MarketParams; +}): { + readonly actions: Action[]; + readonly fee: bigint; + readonly penaltyAssets: bigint; +} => { + // Validate the action descriptors before encoding; the validator returns void. validateReallocations(reallocations, targetMarketParams.id); let fee = 0n; const actions: Action[] = []; + const penaltyAssets = computeVaultV2ReallocationPenaltyAssets(reallocations); + + if (penaltyAssets > 0n) { + const { + bundler3: { bundler3 }, + } = getChainAddresses(chainId); + actions.push({ + type: "erc20TransferFrom", + args: [targetMarketParams.loanToken, penaltyAssets, bundler3, false], + }); + } for (const reallocation of reallocations) { if (reallocation.type === "bluePublicAllocator") { @@ -46,7 +73,7 @@ export const buildReallocationActions = ( reallocation.to.adapter, targetMarketParams, reallocation.assets, - reallocation.nativePenalty, + reallocation.penalty, false, ], }); @@ -59,12 +86,11 @@ export const buildReallocationActions = ( reallocation.to.adapter, targetMarketParams, reallocation.assets, - reallocation.nativePenalty, + reallocation.penalty, false, ], }); } - fee += reallocation.nativePenalty; continue; } @@ -84,5 +110,5 @@ export const buildReallocationActions = ( fee += reallocation.fee; } - return { actions, fee }; + return { actions, fee, penaltyAssets }; }; diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index 72006bf76..224812e37 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -103,7 +103,7 @@ export interface BlueRefinanceParams { * @param params.args.minBorrowSharePrice - Minimum borrow share price (ray) on the target. * @param params.args.maxRepaySharePrice - Maximum repay share price (ray) on the source. * @param params.args.targetReallocations - Public Allocator V1 or V2 reallocations into the target, - * run before the supply leg. V1 fees and V2 native penalties add to `tx.value`. + * run before the supply leg. V1 fees add to `tx.value`; V2 penalties are paid in the target loan token. * @param params.args.authorizationSignature - Optional signed Morpho authorization; when present, * a `setAuthorizationWithSig` call is prepended to the bundle. * @param params.metadata - Optional analytics metadata appended to `tx.data`. @@ -112,11 +112,12 @@ export interface BlueRefinanceParams { * repay); in shares mode the entity passes both. Caller-facing mutual exclusivity is enforced at the entity layer. * @throws {NonPositiveInputError} when `collateralAmount <= 0n`, a repay leg has a non-positive * `maxRepaySharePrice`, or any reallocation withdrawal amount is non-positive. - * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {NegativeInputError} when `borrowAssets`, `borrowShares`, `minBorrowSharePrice`, - * `maxRepaySharePrice`, a V1 fee, or a V2 native penalty is negative. + * `maxRepaySharePrice`, a V1 fee, or a V2 penalty is negative. * @throws {RefinanceSameMarketError} when source and target market ids are equal. * @throws {RefinanceTokenMismatchError} when source and target do not share both tokens. * @throws {RefinanceSharesMissingBorrowAssetsError} when `borrowShares > 0n` but `borrowAssets` is omitted or non-positive. @@ -273,15 +274,21 @@ export const blueRefinance = ({ const actions: Action[] = []; let reallocationFee = 0n; + let reallocationPenaltyAssets = 0n; if (authorizationSignature) { actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } if (targetReallocations && targetReallocations.length > 0) { - const result = buildReallocationActions(targetReallocations, targetParams); + const result = buildReallocationActions({ + chainId, + reallocations: targetReallocations, + targetMarketParams: targetParams, + }); actions.push(...result.actions); reallocationFee = result.fee; + reallocationPenaltyAssets = result.penaltyAssets; } actions.push({ @@ -309,6 +316,7 @@ export const blueRefinance = ({ maxRepaySharePrice, user, reallocationFee, + reallocationPenaltyAssets, }, }, }); diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 7e34babcc..89f2242ed 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -53,8 +53,9 @@ export interface BlueSupplyCollateralBorrowParams { * Routed through bundler3: collateral transfer → `morphoSupplyCollateral` → optional Public * Allocator calls → `morphoBorrow`. V1 entries encode `reallocateTo`; V2 market and idle entries * encode `reallocate` and `allocateFromIdle`. When `nativeAmount > 0`, native ETH is wrapped via - * `GeneralAdapter1.wrapNative()` before the supply leg. V1 fees and V2 native penalties add to - * `tx.value`. + * `GeneralAdapter1.wrapNative()` before the supply leg. V1 fees add to + * `tx.value`; V2 penalties are paid in the target loan token and donated to + * the vaults. * * Prerequisite: `GeneralAdapter1` must be authorized on Morpho to borrow on behalf of the user. * Use `getRequirements()` on the entity to check and obtain the authorization transaction. @@ -82,10 +83,11 @@ export interface BlueSupplyCollateralBorrowParams { * @returns A deep-frozen `Transaction` with `to`, `value`, * `data`, and the typed `action` discriminator the simulation layer consumes. * @throws {NegativeInputError} when `amount`, `nativeAmount`, `minSharePrice`, a V1 fee, or a V2 - * native penalty is negative. + * penalty is negative. * @throws {NonPositiveInputError} when `borrowAmount <= 0n`, both collateral amounts resolve to * zero, or any reallocation withdrawal amount is non-positive. - * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {ChainWNativeMissingError} when `nativeAmount > 0n` but the chain has no configured wNative. @@ -181,11 +183,17 @@ export const blueSupplyCollateralBorrow = ({ }); let reallocationFee = 0n; + let reallocationPenaltyAssets = 0n; if (reallocations && reallocations.length > 0) { - const result = buildReallocationActions(reallocations, marketParams); + const result = buildReallocationActions({ + chainId, + reallocations, + targetMarketParams: marketParams, + }); actions.push(...result.actions); reallocationFee = result.fee; + reallocationPenaltyAssets = result.penaltyAssets; } actions.push({ @@ -212,6 +220,7 @@ export const blueSupplyCollateralBorrow = ({ receiver, nativeAmount, reallocationFee, + reallocationPenaltyAssets, }, }, }); diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index 3bc5fcf55..f777f4d92 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -57,9 +57,10 @@ export interface BlueWithdrawParams { * supplier position close; immune to interest accrual between tx construction and execution). * * When `reallocations` are provided, V1 entries encode `reallocateTo`, while V2 market and idle - * entries encode `reallocate` and `allocateFromIdle`. The calls run before the withdraw, and V1 - * fees plus V2 native penalties accumulate in `tx.value`. The on-chain `morphoWithdraw` sends - * the assets computed on-chain directly to `receiver`; no skim is required. + * entries encode `reallocate` and `allocateFromIdle`. The calls run before the withdraw. V1 + * fees accumulate in `tx.value`; V2 penalties are paid in the target loan + * token and donated to the vaults. The on-chain `morphoWithdraw` sends the + * assets computed on-chain directly to `receiver`; no skim is required. * * The withdraw is performed on behalf of the transaction initiator (signer) — there is no * separate `onBehalf` field; mirror `blueBorrow`. The entity layer keeps `receiver` aligned @@ -80,11 +81,12 @@ export interface BlueWithdrawParams { * @param params.metadata - Optional analytics metadata attached to the bundle. * @returns A deep-frozen `Transaction` with `to`, `value`, `data`, and * the typed `action` discriminator the simulation layer consumes. - * @throws {NegativeInputError} when `assets`, `shares`, `minSharePrice`, a V1 fee, or a V2 native + * @throws {NegativeInputError} when `assets`, `shares`, `minSharePrice`, a V1 fee, or a V2 * penalty is negative. * @throws {NonPositiveInputError} when both `assets` and `shares` are zero or any reallocation * withdrawal amount is non-positive. - * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {MutuallyExclusiveWithdrawAmountsError} when both `assets` and `shares` are non-zero. @@ -144,15 +146,21 @@ export const blueWithdraw = ({ const actions: Action[] = []; let reallocationFee = 0n; + let reallocationPenaltyAssets = 0n; if (authorizationSignature) { actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } if (reallocations && reallocations.length > 0) { - const result = buildReallocationActions(reallocations, marketParams); + const result = buildReallocationActions({ + chainId, + reallocations, + targetMarketParams: marketParams, + }); actions.push(...result.actions); reallocationFee = result.fee; + reallocationPenaltyAssets = result.penaltyAssets; } actions.push({ @@ -180,6 +188,7 @@ export const blueWithdraw = ({ receiver, minSharePrice, reallocationFee, + reallocationPenaltyAssets, }, }, }); diff --git a/packages/morpho-sdk/src/bundler/actions.test.ts b/packages/morpho-sdk/src/bundler/actions.test.ts index db6e75c2f..c2663414d 100644 --- a/packages/morpho-sdk/src/bundler/actions.test.ts +++ b/packages/morpho-sdk/src/bundler/actions.test.ts @@ -16,6 +16,7 @@ import { bytesToHex, decodeFunctionData, encodeAbiParameters, + erc20Abi, type Hex, isAddressEqual, keccak256, @@ -80,6 +81,10 @@ describe("BundlerAction", () => { .uint8Array({ minLength: 20, maxLength: 20 }) .map((bytes) => bytesToHex(bytes) as Address); const amountArbitrary = fc.bigInt({ min: 0n, max: 10n ** 24n }); + const penaltyArbitrary = fc.bigInt({ + min: 0n, + max: 1_000_000_000_000_000_000n, + }); const permitNumberArbitrary = fc.integer({ min: 0, max: 1_000_000 }); const skipRevertArbitrary = fc.boolean(); const marketArbitrary = fc.record({ @@ -355,7 +360,7 @@ describe("BundlerAction", () => { addressArbitrary, marketArbitrary, amountArbitrary, - amountArbitrary, + penaltyArbitrary, skipRevertArbitrary, ) .map( @@ -372,7 +377,7 @@ describe("BundlerAction", () => { addressArbitrary, marketArbitrary, amountArbitrary, - amountArbitrary, + penaltyArbitrary, skipRevertArbitrary, ) .map( @@ -610,7 +615,7 @@ describe("BundlerAction", () => { expect(calls[0]?.value).toBe(5n); }); - test("encodeBundle aggregates Blue Public Allocator native penalties", () => { + test("encodeBundle keeps Blue Public Allocator calls nonpayable", () => { const tx = BundlerAction.encodeBundle(chainId, [ { type: "vaultV2BluePublicAllocatorReallocate", @@ -632,11 +637,16 @@ describe("BundlerAction", () => { }, ]); - expect(tx.value).toBe(6n); + expect(tx.value).toBe(0n); const decoded = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); expect(decoded.functionName).toBe("multicall"); - expect((decoded.args[0] ?? []).map((call) => call.value)).toEqual([2n, 4n]); + expect((decoded.args[0] ?? []).map((call) => call.value)).toEqual([ + 0n, + 0n, + 0n, + 0n, + ]); }); test("encodeBundle includes callback action values in transaction value", () => { @@ -1559,6 +1569,52 @@ describe("BundlerAction", () => { }); test("vaultV2BluePublicAllocatorReallocate", () => { + const penalty = 1_000_000_000_000_000n; + const [approval, call] = BundlerAction.vaultV2BluePublicAllocatorReallocate( + allocator, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 1_000_000n, + penalty, + true, + ); + expect(approval).toBeDefined(); + expect(call).toBeDefined(); + expect( + decodeFunctionData({ abi: erc20Abi, data: approval!.data }), + ).toMatchObject({ + functionName: "approve", + args: [allocator, 1_000n], + }); + expect(approval).toMatchObject({ + to: market.loanToken, + value: 0n, + skipRevert: true, + }); + const decoded = decodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + data: call!.data, + }); + + expect(call!.to).toBe(allocator); + expect(call!.value).toBe(0n); + expect(call!.skipRevert).toBe(true); + expect(decoded.functionName).toBe("reallocate"); + expect(decoded.args).toEqual([ + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 1_000_000n, + penalty, + ]); + }); + + test("vaultV2BluePublicAllocatorReallocate with zero penalty", () => { const call = onlyCall( BundlerAction.vaultV2BluePublicAllocatorReallocate( allocator, @@ -1568,7 +1624,7 @@ describe("BundlerAction", () => { allocateAdapter, market, 1n, - 2n, + 0n, true, ), ); @@ -1578,7 +1634,7 @@ describe("BundlerAction", () => { }); expect(call.to).toBe(allocator); - expect(call.value).toBe(2n); + expect(call.value).toBe(0n); expect(call.skipRevert).toBe(true); expect(decoded.functionName).toBe("reallocate"); expect(decoded.args).toEqual([ @@ -1588,10 +1644,54 @@ describe("BundlerAction", () => { allocateAdapter, market, 1n, + 0n, ]); }); test("vaultV2BluePublicAllocatorAllocateFromIdle", () => { + const penalty = 1_000_000_000_000_000n; + const [approval, call] = + BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + allocator, + vault, + allocateAdapter, + market, + 1_000_000n, + penalty, + true, + ); + expect(approval).toBeDefined(); + expect(call).toBeDefined(); + expect( + decodeFunctionData({ abi: erc20Abi, data: approval!.data }), + ).toMatchObject({ + functionName: "approve", + args: [allocator, 1_000n], + }); + expect(approval).toMatchObject({ + to: market.loanToken, + value: 0n, + skipRevert: true, + }); + const decoded = decodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + data: call!.data, + }); + + expect(call!.to).toBe(allocator); + expect(call!.value).toBe(0n); + expect(call!.skipRevert).toBe(true); + expect(decoded.functionName).toBe("allocateFromIdle"); + expect(decoded.args).toEqual([ + vault, + allocateAdapter, + market, + 1_000_000n, + penalty, + ]); + }); + + test("vaultV2BluePublicAllocatorAllocateFromIdle with zero penalty", () => { const call = onlyCall( BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( allocator, @@ -1599,7 +1699,7 @@ describe("BundlerAction", () => { allocateAdapter, market, 1n, - 2n, + 0n, true, ), ); @@ -1609,10 +1709,10 @@ describe("BundlerAction", () => { }); expect(call.to).toBe(allocator); - expect(call.value).toBe(2n); + expect(call.value).toBe(0n); expect(call.skipRevert).toBe(true); expect(decoded.functionName).toBe("allocateFromIdle"); - expect(decoded.args).toEqual([vault, allocateAdapter, market, 1n]); + expect(decoded.args).toEqual([vault, allocateAdapter, market, 1n, 0n]); }); test("wrapNative", () => { diff --git a/packages/morpho-sdk/src/bundler/actions.ts b/packages/morpho-sdk/src/bundler/actions.ts index 56d7fb4b8..8e2f11426 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -13,6 +13,7 @@ import { type Address, encodeAbiParameters, encodeFunctionData, + erc20Abi, type Hex, isAddressEqual, keccak256, @@ -22,6 +23,7 @@ import { zeroHash, } from "viem"; import { bundler3Abi, coreAdapterAbi, generalAdapter1Abi } from "../abis.js"; +import { computeBluePublicAllocatorPenaltyAssets } from "../helpers/bluePublicAllocator.js"; import { BundlerErrors } from "../types/error.js"; import type { Action, @@ -1453,6 +1455,9 @@ export namespace BundlerAction { /** * Encodes a Vault V2 Blue Public Allocator market-to-market reallocation. * + * @remarks Bundler3 must already hold the computed penalty assets. The + * high-level Blue builders add the corresponding GeneralAdapter1 transfer. + * * @param allocator - Explicit Blue Public Allocator contract address. * @param vault - Vault whose liquidity is reallocated. * @param deallocateAdapter - Vault V2 adapter supplying the source market. @@ -1460,40 +1465,51 @@ export namespace BundlerAction { * @param allocateAdapter - Vault V2 adapter supplying the target market. * @param allocateMarket - Target Morpho Blue market parameters. * @param assets - Assets to reallocate, bounded by `uint128` by the high-level action. - * @param nativePenalty - Native penalty paid to the allocator. + * @param penalty - Vault-configured proportional penalty, scaled by WAD. * @param skipRevert - Whether Bundler3 should tolerate a revert. - * @returns One encoded call targeting the explicit allocator. + * @returns An exact token approval when needed, followed by the allocator call. * @example * ```ts + * import type { InputMarketParams } from "@morpho-org/blue-sdk"; * import { BundlerAction } from "@morpho-org/morpho-sdk/bundler"; - * - * const allocator = "0x0000000000000000000000000000000000000001"; - * const vault = "0x0000000000000000000000000000000000000002"; - * const sourceAdapter = "0x0000000000000000000000000000000000000003"; - * const targetAdapter = "0x0000000000000000000000000000000000000004"; + * import type { Address } from "viem"; + * + * const allocatorFixture = + * "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" satisfies Address; + * const keyrockUsdcVault = + * "0x04422053aDDbc9bB2759b248B574e3FCA76Bc145" satisfies Address; + * const sourceAdapterFixture = + * "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" satisfies Address; + * const targetAdapterFixture = + * "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" satisfies Address; + * const usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" satisfies Address; + * const weth = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" satisfies Address; + * const wbtc = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" satisfies Address; + * const ethUsdOracle = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" satisfies Address; + * const adaptiveCurveIrm = "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC" satisfies Address; * const sourceMarket = { - * loanToken: "0x0000000000000000000000000000000000000005", - * collateralToken: "0x0000000000000000000000000000000000000006", - * oracle: "0x0000000000000000000000000000000000000007", - * irm: "0x0000000000000000000000000000000000000008", + * loanToken: usdc, + * collateralToken: weth, + * oracle: ethUsdOracle, + * irm: adaptiveCurveIrm, * lltv: 860_000000000000000000n, - * }; + * } satisfies InputMarketParams; * const targetMarket = { * ...sourceMarket, - * collateralToken: "0x0000000000000000000000000000000000000009", - * }; + * collateralToken: wbtc, + * } satisfies InputMarketParams; * * const calls = BundlerAction.vaultV2BluePublicAllocatorReallocate( - * allocator, - * vault, - * sourceAdapter, + * allocatorFixture, + * keyrockUsdcVault, + * sourceAdapterFixture, * sourceMarket, - * targetAdapter, + * targetAdapterFixture, * targetMarket, * 1_000_000n, - * 10n, + * 1_000_000_000_000_000n, * ); - * // calls[0] targets `allocator` with `value: 10n` and `reallocate` calldata. + * // Bundler3 approves 1_000 USDC units, then calls `reallocate` with zero native value. * ``` */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call @@ -1505,66 +1521,99 @@ export namespace BundlerAction { allocateAdapter: Address, allocateMarket: InputMarketParams, assets: bigint, - nativePenalty: bigint, + penalty: bigint, skipRevert = false, ): BundlerCall[] { - return [ - { - to: allocator, + const calls: BundlerCall[] = []; + const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( + assets, + penalty, + ); + + if (penaltyAssets > 0n) { + calls.push({ + to: allocateMarket.loanToken, data: encodeFunctionData({ - abi: vaultV2BluePublicAllocatorAbi, - functionName: "reallocate", - args: [ - vault, - deallocateAdapter, - deallocateMarket, - allocateAdapter, - allocateMarket, - assets, - ], + abi: erc20Abi, + functionName: "approve", + args: [allocator, penaltyAssets], }), - value: nativePenalty, + value: 0n, skipRevert, callbackHash: zeroHash, - }, - ]; + }); + } + + calls.push({ + to: allocator, + data: encodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + functionName: "reallocate", + args: [ + vault, + deallocateAdapter, + deallocateMarket, + allocateAdapter, + allocateMarket, + assets, + penalty, + ], + }), + value: 0n, + skipRevert, + callbackHash: zeroHash, + }); + + return calls; } /** * Encodes a Vault V2 Blue Public Allocator allocation from vault idle liquidity. * + * @remarks Bundler3 must already hold the computed penalty assets. The + * high-level Blue builders add the corresponding GeneralAdapter1 transfer. + * * @param allocator - Explicit Blue Public Allocator contract address. * @param vault - Vault whose idle liquidity is allocated. * @param adapter - Vault V2 adapter supplying the target market. * @param market - Target Morpho Blue market parameters. * @param assets - Assets to allocate, bounded by `uint128` by the high-level action. - * @param nativePenalty - Native penalty paid to the allocator. + * @param penalty - Vault-configured proportional penalty, scaled by WAD. * @param skipRevert - Whether Bundler3 should tolerate a revert. - * @returns One encoded call targeting the explicit allocator. + * @returns An exact token approval when needed, followed by the allocator call. * @example * ```ts + * import type { InputMarketParams } from "@morpho-org/blue-sdk"; * import { BundlerAction } from "@morpho-org/morpho-sdk/bundler"; - * - * const allocator = "0x0000000000000000000000000000000000000001"; - * const vault = "0x0000000000000000000000000000000000000002"; - * const targetAdapter = "0x0000000000000000000000000000000000000003"; + * import type { Address } from "viem"; + * + * const allocatorFixture = + * "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" satisfies Address; + * const keyrockUsdcVault = + * "0x04422053aDDbc9bB2759b248B574e3FCA76Bc145" satisfies Address; + * const targetAdapterFixture = + * "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" satisfies Address; + * const usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" satisfies Address; + * const weth = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" satisfies Address; + * const ethUsdOracle = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" satisfies Address; + * const adaptiveCurveIrm = "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC" satisfies Address; * const targetMarket = { - * loanToken: "0x0000000000000000000000000000000000000004", - * collateralToken: "0x0000000000000000000000000000000000000005", - * oracle: "0x0000000000000000000000000000000000000006", - * irm: "0x0000000000000000000000000000000000000007", + * loanToken: usdc, + * collateralToken: weth, + * oracle: ethUsdOracle, + * irm: adaptiveCurveIrm, * lltv: 860_000000000000000000n, - * }; + * } satisfies InputMarketParams; * * const calls = BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( - * allocator, - * vault, - * targetAdapter, + * allocatorFixture, + * keyrockUsdcVault, + * targetAdapterFixture, * targetMarket, * 1_000_000n, - * 10n, + * 1_000_000_000_000_000n, * ); - * // calls[0] targets `allocator` with `value: 10n` and `allocateFromIdle` calldata. + * // Bundler3 approves 1_000 USDC units, then calls `allocateFromIdle` with zero native value. * ``` */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call @@ -1574,22 +1623,42 @@ export namespace BundlerAction { adapter: Address, market: InputMarketParams, assets: bigint, - nativePenalty: bigint, + penalty: bigint, skipRevert = false, ): BundlerCall[] { - return [ - { - to: allocator, + const calls: BundlerCall[] = []; + const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( + assets, + penalty, + ); + + if (penaltyAssets > 0n) { + calls.push({ + to: market.loanToken, data: encodeFunctionData({ - abi: vaultV2BluePublicAllocatorAbi, - functionName: "allocateFromIdle", - args: [vault, adapter, market, assets], + abi: erc20Abi, + functionName: "approve", + args: [allocator, penaltyAssets], }), - value: nativePenalty, + value: 0n, skipRevert, callbackHash: zeroHash, - }, - ]; + }); + } + + calls.push({ + to: allocator, + data: encodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + functionName: "allocateFromIdle", + args: [vault, adapter, market, assets, penalty], + }), + value: 0n, + skipRevert, + callbackHash: zeroHash, + }); + + return calls; } /** diff --git a/packages/morpho-sdk/src/bundler/types.ts b/packages/morpho-sdk/src/bundler/types.ts index 884ec0599..d39a01098 100644 --- a/packages/morpho-sdk/src/bundler/types.ts +++ b/packages/morpho-sdk/src/bundler/types.ts @@ -210,7 +210,7 @@ export interface ActionArgs { skipRevert?: boolean, ]; - /** Vault V2 Blue Public Allocator market-to-market reallocation with an explicit allocator address and native penalty. */ + /** Vault V2 Blue Public Allocator market-to-market reallocation with an explicit allocator address and WAD-scaled penalty. */ readonly vaultV2BluePublicAllocatorReallocate: [ allocator: Address, vault: Address, @@ -219,18 +219,18 @@ export interface ActionArgs { allocateAdapter: Address, allocateMarket: InputMarketParams, assets: bigint, - nativePenalty: bigint, + penalty: bigint, skipRevert?: boolean, ]; - /** Vault V2 Blue Public Allocator idle-to-market allocation with an explicit allocator address and native penalty. */ + /** Vault V2 Blue Public Allocator idle-to-market allocation with an explicit allocator address and WAD-scaled penalty. */ readonly vaultV2BluePublicAllocatorAllocateFromIdle: [ allocator: Address, vault: Address, adapter: Address, market: InputMarketParams, assets: bigint, - nativePenalty: bigint, + penalty: bigint, skipRevert?: boolean, ]; diff --git a/packages/morpho-sdk/src/entities/blue/AGENTS.md b/packages/morpho-sdk/src/entities/blue/AGENTS.md index 8281c48ae..c4aee8b1e 100644 --- a/packages/morpho-sdk/src/entities/blue/AGENTS.md +++ b/packages/morpho-sdk/src/entities/blue/AGENTS.md @@ -21,8 +21,9 @@ `getRequirements` returns: - ERC-20 approval for **GeneralAdapter1** on the collateral token (any path that supplies collateral) or the loan token (`supply`, `repay`, `repayWithdrawCollateral`). The approved amount is the **ERC-20 portion actually pulled**, not the total: for a native-funded repay it is `amount` (assets mode) or `max(0, toBorrowAssets(shares) − nativeAmount)` (shares mode — clamped at 0 so a `nativeAmount` that covers or exceeds the borrow assets pulls nothing). A fully-native repay pulls no ERC-20, so no approval requirement is emitted; in shares mode any wrapped native beyond the on-chain repay is skimmed back to the receiver. +- A classic ERC-20 approval for **GeneralAdapter1** on the loan token when `borrow`, `supplyCollateralBorrow`, `withdraw`, or `refinance` includes BluePublicAllocator reallocations with a non-zero penalty. The approved amount is the sum of each call's independently rounded `ceil(assets × penalty / WAD)` donation. This path deliberately does not return a permit signature, so it can coexist with a collateral-token permit in `supplyCollateralBorrow`. - `morpho.setAuthorization(generalAdapter1, true)` when authorization is not yet set on Morpho — read via `publicActions`. Required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, and `withdraw` (loan-asset). When `supportSignature` is enabled on the client, the authorization requirement is returned as a signable `Requirement` instead of a transaction; signing it produces an `AuthorizationRequirementSignature` that `buildTx` consumes and folds into the bundle as a `setAuthorizationWithSig` call, so no standalone authorization transaction is needed. `buildTx` accepts a `readonly RequirementSignature[]` and splits permit vs. authorization signatures via `isPermitSignature` / `isAuthorizationSignature`. -`withdrawCollateral` has no requirements. `repay` and `supply` need only loan-token approval (native wrapping requires the loan token to be the chain's wNative). Loan-asset `withdraw` needs only the Morpho authorization. +`withdrawCollateral` has no requirements. `repay` and `supply` need only loan-token approval (native wrapping requires the loan token to be the chain's wNative). Without V2 reallocations, loan-asset `withdraw` needs only the Morpho authorization. diff --git a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts new file mode 100644 index 000000000..e67c21100 --- /dev/null +++ b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts @@ -0,0 +1,94 @@ +import { + AccrualPosition, + getChainAddresses, + Market, + MarketParams, + ORACLE_PRICE_SCALE, +} from "@morpho-org/blue-sdk"; +import { blueAbi } from "@morpho-org/blue-sdk-viem"; +import { createMockClient, mockRead } from "@morpho-org/test/mock"; +import { erc20Abi } from "viem"; +import { mainnet } from "viem/chains"; +import { describe, expect, test } from "vitest"; +import { CbbtcUsdcBlue } from "../../../test/fixtures/blue.js"; +import { morphoViemExtension } from "../../client/index.js"; +import { isRequirementApproval } from "../../types/index.js"; + +const USER = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; +const marketParams = new MarketParams(CbbtcUsdcBlue); + +describe("MorphoBlue BluePublicAllocator requirements", () => { + test("default: includes the classic loan-token approval for V2 penalties", async () => { + const handle = createMockClient(mainnet); + const { + morpho, + bundler3: { generalAdapter1 }, + } = getChainAddresses(mainnet.id); + mockRead(handle, { + address: morpho, + abi: blueAbi, + functionName: "isAuthorized", + result: true, + }); + mockRead(handle, { + address: morpho, + abi: blueAbi, + functionName: "nonce", + result: 0n, + }); + mockRead(handle, { + address: marketParams.loanToken, + abi: erc20Abi, + functionName: "allowance", + result: 0n, + }); + + const positionData = new AccrualPosition( + { + user: USER, + supplyShares: 0n, + borrowShares: 10n ** 18n, + collateral: 10n ** 24n, + }, + new Market({ + params: marketParams, + totalSupplyAssets: 10n ** 24n, + totalBorrowAssets: 10n ** 24n / 2n, + totalSupplyShares: 10n ** 24n, + totalBorrowShares: 10n ** 24n / 2n, + lastUpdate: 1_700_000_000n, + fee: 0n, + price: ORACLE_PRICE_SCALE, + }), + ); + const market = handle.client + .extend(morphoViemExtension({ supportSignature: true })) + .morpho.blue(CbbtcUsdcBlue, mainnet.id); + + const requirements = await market + .borrow({ + amount: 1n, + userAddress: USER, + positionData, + reallocations: [ + { + type: "bluePublicAllocator", + allocator: CbbtcUsdcBlue.irm, + vault: CbbtcUsdcBlue.oracle, + from: { type: "idle" }, + to: { adapter: CbbtcUsdcBlue.collateralToken }, + assets: 1n, + penalty: 1n, + }, + ], + }) + .getRequirements(); + + const approval = requirements.find(isRequirementApproval); + expect(approval?.to).toBe(marketParams.loanToken); + expect(approval?.action.args).toStrictEqual({ + spender: generalAdapter1, + amount: 1n, + }); + }); +}); diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index d622c3653..d9fe6c89d 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -31,6 +31,7 @@ import { getBlueAuthorizationRequirement, getGeneralAdapterRequirements, } from "../../actions/index.js"; +import { computeVaultV2ReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; import { computeMaxRepaySharePrice, computeMaxSupplySharePrice, @@ -189,18 +190,19 @@ export interface BlueActions { * Computes `minSharePrice` from market supply state and `slippageTolerance`. * * When `reallocations` is provided, V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` - * actions are prepended to move liquidity before withdrawing. V1 fees and V2 native penalties - * add to the transaction value. + * actions are prepended to move liquidity before withdrawing. V1 fees add + * to the transaction value; V2 penalties are paid in the loan token. * - * `getRequirements` returns `morpho.setAuthorization(generalAdapter1, true)` if GA1 is not - * yet authorized on Morpho (returns `[]` when already authorized), since the bundler calls - * `withdraw(...,onBehalf=user,...)`. + * `getRequirements` returns the loan-token approval needed for V2 penalties + * and `morpho.setAuthorization(generalAdapter1, true)` when GA1 is not yet + * authorized on Morpho. * * **Stale `positionData` may cause unexpected supply share calculations.** * * @param params - Withdraw parameters including pre-fetched `positionData`. * @returns Object with `buildTx` and `getRequirements`. - * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ @@ -217,7 +219,11 @@ export interface BlueActions { signatures?: readonly RequirementSignature[], ) => Readonly>; getRequirements: () => Promise< - (Readonly> | Requirement)[] + ( + | Readonly> + | Readonly> + | Requirement + )[] >; }; @@ -229,17 +235,18 @@ export interface BlueActions { * Computes `minSharePrice` from market borrow state and `slippageTolerance`. * * When `reallocations` is provided, V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` - * actions are prepended before borrowing. V1 fees and V2 native penalties add to the - * transaction value. + * actions are prepended before borrowing. V1 fees add to the transaction + * value; V2 penalties are paid in the loan token. * - * `getRequirements` returns `morpho.setAuthorization(generalAdapter1, true)` if not yet authorized, - * since borrowing through bundler3 requires GeneralAdapter1 authorization on Morpho. + * `getRequirements` returns the loan-token approval needed for V2 penalties + * and Morpho authorization for GeneralAdapter1 when needed. * * **Stale `positionData` may cause unexpected health.** * * @param params - Borrow parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. - * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ @@ -254,7 +261,11 @@ export interface BlueActions { signatures?: readonly RequirementSignature[], ) => Readonly>; getRequirements: () => Promise< - (Readonly> | Requirement)[] + ( + | Readonly> + | Readonly> + | Requirement + )[] >; }; @@ -374,18 +385,20 @@ export interface BlueActions { * to prevent instant liquidation on new positions near the LLTV threshold. * * When `reallocations` is provided, V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` - * actions run between the collateral supply and `morphoBorrow`. V1 fees and V2 native penalties - * add to the transaction value. + * actions run between the collateral supply and `morphoBorrow`. V1 fees add + * to the transaction value; V2 penalties are paid in the loan token. * * `getRequirements` returns in parallel: * - ERC20 approval or permit for collateral token (to GeneralAdapter1). + * - Classic ERC20 approval for any V2 loan-token penalties. * - `morpho.setAuthorization(generalAdapter1, true)` if adapter is not yet authorized. * * **Stale `positionData` may cause unexpected health.** * * @param params - Combined parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. - * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ @@ -426,10 +439,11 @@ export interface BlueActions { * markets are forward-accrued to `now`; in shares mode the target borrow is overshot by * `slippageTolerance` and the callback sweeps the residual. * Target reallocations run first as V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` - * actions; V1 fees and V2 native penalties add to the transaction value. + * actions; V1 fees add to the transaction value and V2 penalties are paid + * in the loan token. * - * `getRequirements` returns `morpho.setAuthorization(generalAdapter1, true)` when GA1 is not yet - * authorized (a single global authorization covers both markets). + * `getRequirements` returns the loan-token approval needed for V2 penalties + * and Morpho authorization for GeneralAdapter1 when needed. * * @param params.userAddress - Position owner on both markets. * @param params.positionData - Pre-fetched source-market accrual position. @@ -441,7 +455,8 @@ export interface BlueActions { * @param params.slippageTolerance - WAD slippage tolerance. Defaults to `DEFAULT_SLIPPAGE_TOLERANCE`. * @param params.targetReallocations - Public Allocator V1 or V2 reallocations into the target market. * @returns Object with `buildTx` and `getRequirements`. - * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ @@ -462,7 +477,11 @@ export interface BlueActions { signatures?: readonly RequirementSignature[], ) => Readonly>; getRequirements: () => Promise< - (Readonly> | Requirement)[] + ( + | Readonly> + | Readonly> + | Requirement + )[] >; }; @@ -548,6 +567,22 @@ export class MorphoBlue implements BlueActions { private readonly chainId: number, ) {} + private getReallocationPenaltyRequirements( + userAddress: Address, + reallocations: readonly BlueReallocation[] | undefined, + ) { + const amount = computeVaultV2ReallocationPenaltyAssets(reallocations ?? []); + + // Penalty funding always uses the classic GeneralAdapter1 allowance so a + // collateral permit and a loan-token penalty can coexist in one bundle. + return getGeneralAdapterRequirements(this.client.viemClient, { + address: this.marketParams.loanToken, + chainId: this.chainId, + supportSignature: false, + args: { amount, from: userAddress }, + }); + } + async getMarketData(parameters?: FetchParameters): Promise { validateChainId(this.client.viemClient.chain?.id, this.chainId); @@ -696,6 +731,7 @@ export class MorphoBlue implements BlueActions { validateSlippageTolerance(slippageTolerance); if (reallocations) { + // Validate caller-supplied descriptors before reading state; the helper returns void. validateReallocations(reallocations, this.marketParams.id); } @@ -732,13 +768,16 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { - const authTx = await getBlueAuthorizationRequirement({ - viemClient: this.client.viemClient, - chainId: this.chainId, - userAddress, - supportSignature: this.client.options.supportSignature, - }); - return authTx ? [authTx] : []; + const [penaltyRequirements, authTx] = await Promise.all([ + this.getReallocationPenaltyRequirements(userAddress, reallocations), + getBlueAuthorizationRequirement({ + viemClient: this.client.viemClient, + chainId: this.chainId, + userAddress, + supportSignature: this.client.options.supportSignature, + }), + ]); + return [...penaltyRequirements, ...(authTx ? [authTx] : [])]; }, buildTx: (signatures?: readonly RequirementSignature[]) => { @@ -839,6 +878,7 @@ export class MorphoBlue implements BlueActions { validateSlippageTolerance(slippageTolerance); if (reallocations) { + // Validate caller-supplied descriptors before reading state; the helper returns void. validateReallocations(reallocations, this.marketParams.id); } @@ -867,13 +907,16 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { - const authTx = await getBlueAuthorizationRequirement({ - viemClient: this.client.viemClient, - chainId: this.chainId, - userAddress, - supportSignature: this.client.options.supportSignature, - }); - return authTx ? [authTx] : []; + const [penaltyRequirements, authTx] = await Promise.all([ + this.getReallocationPenaltyRequirements(userAddress, reallocations), + getBlueAuthorizationRequirement({ + viemClient: this.client.viemClient, + chainId: this.chainId, + userAddress, + supportSignature: this.client.options.supportSignature, + }), + ]); + return [...penaltyRequirements, ...(authTx ? [authTx] : [])]; }, buildTx: (signatures?: readonly RequirementSignature[]) => { @@ -1346,6 +1389,7 @@ export class MorphoBlue implements BlueActions { validateSlippageTolerance(slippageTolerance); if (reallocations) { + // Validate caller-supplied descriptors before reading state; the helper returns void. validateReallocations(reallocations, this.marketParams.id); } @@ -1378,24 +1422,30 @@ export class MorphoBlue implements BlueActions { }); return { getRequirements: async (params?: { useSimplePermit?: boolean }) => { - const [erc20Requirements, authTx] = await Promise.all([ - getGeneralAdapterRequirements(this.client.viemClient, { - address: this.marketParams.collateralToken, - chainId: this.chainId, - supportSignature: this.client.options.supportSignature, - supportDeployless: this.client.options.supportDeployless, - useSimplePermit: params?.useSimplePermit, - args: { amount, from: userAddress }, - }), - getBlueAuthorizationRequirement({ - viemClient: this.client.viemClient, - chainId: this.chainId, - userAddress, - supportSignature: this.client.options.supportSignature, - }), - ]); - - return [...erc20Requirements, ...(authTx ? [authTx] : [])]; + const [erc20Requirements, penaltyRequirements, authTx] = + await Promise.all([ + getGeneralAdapterRequirements(this.client.viemClient, { + address: this.marketParams.collateralToken, + chainId: this.chainId, + supportSignature: this.client.options.supportSignature, + supportDeployless: this.client.options.supportDeployless, + useSimplePermit: params?.useSimplePermit, + args: { amount, from: userAddress }, + }), + this.getReallocationPenaltyRequirements(userAddress, reallocations), + getBlueAuthorizationRequirement({ + viemClient: this.client.viemClient, + chainId: this.chainId, + userAddress, + supportSignature: this.client.options.supportSignature, + }), + ]); + + return [ + ...erc20Requirements, + ...penaltyRequirements, + ...(authTx ? [authTx] : []), + ]; }, buildTx: (signatures?: readonly RequirementSignature[]) => { @@ -1467,6 +1517,7 @@ export class MorphoBlue implements BlueActions { throw new BorrowAmountAndSharesExclusiveError(this.marketParams.id); } if (targetReallocations) { + // Validate caller-supplied descriptors before reading state; the helper returns void. validateReallocations(targetReallocations, target.marketParams.id); } @@ -1624,13 +1675,19 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { - const authTx = await getBlueAuthorizationRequirement({ - viemClient: this.client.viemClient, - chainId: this.chainId, - userAddress, - supportSignature: this.client.options.supportSignature, - }); - return authTx ? [authTx] : []; + const [penaltyRequirements, authTx] = await Promise.all([ + this.getReallocationPenaltyRequirements( + userAddress, + targetReallocations, + ), + getBlueAuthorizationRequirement({ + viemClient: this.client.viemClient, + chainId: this.chainId, + userAddress, + supportSignature: this.client.options.supportSignature, + }), + ]); + return [...penaltyRequirements, ...(authTx ? [authTx] : [])]; }, buildTx: (signatures?: readonly RequirementSignature[]) => { diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index e50df32c1..9a7070aa4 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -79,9 +79,9 @@ interface FixtureOptions { readonly allocatorTargetCap?: bigint; readonly firstTotalAssets?: bigint; readonly idle?: bigint; - readonly canAllocateFromIdle?: boolean; - readonly canDeallocate?: boolean; - readonly nativePenalty?: bigint; + readonly canPullFromIdle?: boolean; + readonly canPullFromMarket?: boolean; + readonly penalty?: bigint; } const makeFixture = ({ @@ -102,9 +102,9 @@ const makeFixture = ({ allocatorTargetCap = 10_000n, firstTotalAssets, idle = 0n, - canAllocateFromIdle = true, - canDeallocate = true, - nativePenalty = 7n, + canPullFromIdle = true, + canPullFromMarket = true, + penalty = 7n, }: FixtureOptions = {}) => { const sameMarket = sourceMarketParams.id === targetParams.id; const targetMarket = makeMarket({ @@ -255,8 +255,8 @@ const makeFixture = ({ [VAULT]: { allocator: ALLOCATOR, vault: VAULT, - canAllocateFromIdle, - nativePenalty, + canPullFromIdle, + penalty, }, }, marketPublicAllocatorConfigs: { @@ -267,7 +267,7 @@ const makeFixture = ({ adapter: TARGET_ADAPTER, marketParamsId: targetIds[2], absoluteCap: allocatorTargetCap, - canDeallocate: false, + canPullFromMarket: false, isActiveAdapter: true, }, [sourceIds[2]]: { @@ -276,7 +276,7 @@ const makeFixture = ({ adapter: sourceAdapterAddress, marketParamsId: sourceIds[2], absoluteCap: 0n, - canDeallocate, + canPullFromMarket, isActiveAdapter: true, }, }, @@ -307,7 +307,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }, to: { adapter: TARGET_ADAPTER }, assets: sourceExpectedAssets, - nativePenalty: 7n, + penalty: 7n, }, ]); expect(result.data).not.toBe(data); @@ -326,16 +326,16 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { const result = data.computeVaultV2Reallocations(targetParams.id); expect( - result.reallocations.map(({ from, assets, nativePenalty }) => ({ + result.reallocations.map(({ from, assets, penalty }) => ({ from: from.type, assets, - nativePenalty, + penalty, })), ).toStrictEqual([ - { from: "market", assets: sourceExpectedAssets, nativePenalty: 7n }, - { from: "idle", assets: 300n, nativePenalty: 7n }, + { from: "market", assets: sourceExpectedAssets, penalty: 7n }, + { from: "idle", assets: 300n, penalty: 7n }, ]); - expect(result.data.getVault(VAULT).assetBalance).toBe(0n); + expect(result.data.getVault(VAULT).assetBalance).toBe(2n); }); test("behavior: permits the target market through a different adapter", () => { @@ -362,7 +362,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { const result = data.computeVaultV2Reallocations(targetParams.id); expect(result.reallocations[0]?.assets).toBe(sourceExpectedAssets); - expect(result.data.getVault(VAULT).assetBalance).toBe(0n); + expect(result.data.getVault(VAULT).assetBalance).toBe(1n); }); test("behavior: target untracked interest consumes allocator headroom", () => { @@ -480,21 +480,21 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ).toStrictEqual([]); }); - test("behavior: ignores vault liquidity above the native penalty threshold", () => { + test("behavior: ignores vault liquidity above the penalty threshold", () => { const { data, sourceExpectedAssets } = makeFixture({ idle: 300n, - nativePenalty: 8n, + penalty: 8n, }); expect( data.computeVaultV2Reallocations(targetParams.id, { - maxNativePenalty: 7n, + maxPenalty: 7n, }).reallocations, ).toStrictEqual([]); expect( data .computeVaultV2Reallocations(targetParams.id, { - maxNativePenalty: 8n, + maxPenalty: 8n, }) .reallocations.map(({ from, assets }) => ({ from: from.type, @@ -566,7 +566,7 @@ describe("computeVaultV2Reallocations", () => { expect(reallocations[0]?.assets).toBe(10n); }); - test("behavior: charges nativePenalty for every retained flat call", () => { + test("behavior: preserves the configured penalty for every retained flat call", () => { const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 100n, @@ -593,14 +593,15 @@ describe("computeVaultV2Reallocations", () => { }); expect(reallocations).toHaveLength(2); - expect(tx.value).toBe(14n); + expect(tx.value).toBe(0n); + expect(tx.action.args.reallocationPenaltyAssets).toBe(2n); }); - test("behavior: excludes reallocations above the native penalty threshold", () => { + test("behavior: excludes reallocations above the penalty threshold", () => { const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 90n, - nativePenalty: 7n, + penalty: 7n, }); expect( @@ -609,7 +610,7 @@ describe("computeVaultV2Reallocations", () => { marketId: targetParams.id, operation: "borrow", amount: 1n, - options: { maxNativePenalty: 6n }, + options: { maxPenalty: 6n }, }), ).toStrictEqual([]); expect( @@ -618,7 +619,7 @@ describe("computeVaultV2Reallocations", () => { marketId: targetParams.id, operation: "borrow", amount: 1n, - options: { maxNativePenalty: 7n }, + options: { maxPenalty: 7n }, })[0]?.assets, ).toBe(2n); }); diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index 1a9a3a702..579976b52 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -13,6 +13,7 @@ import { } from "@morpho-org/blue-sdk"; import { _try, bigIntComparator } from "@morpho-org/morpho-ts"; import { type Address, type Hash, isAddressEqual } from "viem"; +import { computeBluePublicAllocatorPenaltyAssets } from "../helpers/bluePublicAllocator.js"; import { DEFAULT_SUPPLY_TARGET_UTILIZATION, DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, @@ -134,6 +135,8 @@ const cloneVault = (vault: AccrualVaultV2) => { * ``` */ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { + /** Penalty donations created by this simulation, excluded as fresh shared-liquidity sources. */ + private readonly donatedPenaltyAssets: Record; /** Chain id associated with this snapshot. */ public readonly chainId: number; /** Explicit BluePublicAllocator address used in returned calls. */ @@ -171,6 +174,10 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { this.allocations = {}; this.publicAllocatorConfigs = {}; this.marketPublicAllocatorConfigs = {}; + this.donatedPenaltyAssets = + input instanceof VaultV2ReallocationData + ? { ...input.donatedPenaltyAssets } + : {}; for (const [marketId, market] of Object.entries(input.markets ?? {}) as [ MarketId, @@ -372,12 +379,12 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * * The algorithm ranks action-ready calls by obtainable assets, includes idle * liquidity, applies each winner to cloned state, and stops when every - * candidate is exhausted. Vaults whose configured native penalty exceeds - * `options.maxNativePenalty` are ignored. Source markets are held below the + * candidate is exhausted. Vaults whose configured penalty exceeds + * `options.maxPenalty` are ignored. Source markets are held below the * SDK's default withdrawal-utilization ceiling. * * @param marketId - Target Blue market id. - * @param options - Optional timestamp, enable flag, vault allowlist, and maximum native penalty. + * @param options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. * @returns Flat action-ready reallocations and their post-simulation state. * @throws {@link UnknownReallocationMarketError} when the target market is absent. * @example @@ -404,7 +411,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * * @param marketId - Target market id. * @param maxWithdrawalUtilization - Source-market utilization ceiling. - * @param options - Discovery options, including the maximum native penalty. + * @param options - Discovery options, including the maximum penalty. * @returns Flat action-ready reallocations and post-simulation state. * @internal */ @@ -446,7 +453,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { vaultAddress: vault, marketId, maxWithdrawalUtilization, - maxNativePenalty: options.maxNativePenalty, + maxPenalty: options.maxPenalty, }), ) .filter( @@ -471,7 +478,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * Sums friendly Vault V2 shared liquidity available to a target market. * * @param marketId - Target Blue market id. - * @param options - Optional timestamp, enable flag, vault allowlist, and maximum native penalty. + * @param options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. * @returns Reallocatable market and idle assets, or `0n` when none are available. * @throws {@link UnknownReallocationMarketError} when the target market is absent. * @example @@ -495,7 +502,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * * @param marketId - Target Blue market id. * @param utilization - Desired utilization, scaled by WAD. Defaults to 90%. - * @param options - Optional timestamp, enable flag, vault allowlist, and maximum native penalty. + * @param options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. * @returns Borrowable assets while remaining at or below `utilization`. * @throws {@link UnknownReallocationMarketError} when the target market is absent. * @example @@ -560,12 +567,12 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { vaultAddress, marketId, maxWithdrawalUtilization, - maxNativePenalty, + maxPenalty, }: { readonly vaultAddress: Address; readonly marketId: MarketId; readonly maxWithdrawalUtilization: bigint; - readonly maxNativePenalty?: bigint; + readonly maxPenalty?: bigint; }) { return _try(() => { const vault = this.getVault(vaultAddress); @@ -573,8 +580,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { if ( !isAddressEqual(publicAllocatorConfig.allocator, this.allocator) || !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) || - (maxNativePenalty != null && - publicAllocatorConfig.nativePenalty > maxNativePenalty) + (maxPenalty != null && publicAllocatorConfig.penalty > maxPenalty) ) return; @@ -682,7 +688,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { return headroom; }; - if (publicAllocatorConfig.canAllocateFromIdle) { + if (publicAllocatorConfig.canPullFromIdle) { const targetHeadroom = getTargetCapHeadroom(new Set(), 0n); if (targetHeadroom != null) { const assets = MathLib.min( @@ -690,7 +696,10 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { targetSupplyHeadroom, allocatorHeadroom, targetHeadroom, - vault.assetBalance, + MathLib.zeroFloorSub( + vault.assetBalance, + this.donatedPenaltyAssets[vaultAddress] ?? 0n, + ), ); if (assets > 0n) { candidates.push({ @@ -700,7 +709,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { from: { type: "idle" }, to: { adapter: targetContext.adapter.address }, assets, - nativePenalty: publicAllocatorConfig.nativePenalty, + penalty: publicAllocatorConfig.penalty, }); } } @@ -742,7 +751,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { !isAddressEqual(sourceConfig.vault, vaultAddress) || !isAddressEqual(sourceConfig.adapter, sourceAdapter.address) || !sourceConfig.isActiveAdapter || - !sourceConfig.canDeallocate + !sourceConfig.canPullFromMarket ) return; @@ -788,7 +797,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { }, to: { adapter: targetContext.adapter.address }, assets, - nativePenalty: publicAllocatorConfig.nativePenalty, + penalty: publicAllocatorConfig.penalty, } satisfies VaultV2BlueReallocation; }, UnknownDataError); if (candidate != null) candidates.push(candidate); @@ -820,6 +829,14 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { const targetMarket = data.getMarket(targetMarketId); const targetIds = targetAdapter.ids(targetMarket.params); + const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( + reallocation.assets, + reallocation.penalty, + ); + vault.assetBalance += penaltyAssets; + data.donatedPenaltyAssets[reallocation.vault] = + (data.donatedPenaltyAssets[reallocation.vault] ?? 0n) + penaltyAssets; + if (reallocation.from.type === "market") { const sourceAdapter = data.getAdapter( reallocation.vault, diff --git a/packages/morpho-sdk/src/helpers/AGENTS.md b/packages/morpho-sdk/src/helpers/AGENTS.md index 1ee86af92..b66ad941a 100644 --- a/packages/morpho-sdk/src/helpers/AGENTS.md +++ b/packages/morpho-sdk/src/helpers/AGENTS.md @@ -9,7 +9,7 @@ Per-function contracts (arguments, return shapes, behavior) live as JSDoc on eac - **Encoders** (ABI encoding plus input validation, no I/O) — e.g. `encodeForceDeallocateCall(deallocation, onBehalf)`. ABI-encodes a single `VaultV2.forceDeallocate` calldata entry and throws `NonPositiveInputError` on a non-positive `amount`. The `data` field carries ABI-encoded `MarketParams` for the Morpho Market V1 adapter, or empty bytes otherwise. Internal sub-helpers (e.g. `encodeDeallocateData`) are not exported. - **Validators** (pure, throw typed errors) — `validateReallocations(...)`, `validateSlippageTolerance(...)`, `validatePositionHealth(...)`. Each enforces a public-API invariant: see the `error.ts` exports for the full list of error classes a caller may pattern-match on. - **Math / share-price helpers** — `computeMaxRepaySharePrice`, `computeMinBorrowSharePrice`, etc. Use `MAX_SLIPPAGE_TOLERANCE` and cap at `MAX_ABSOLUTE_SHARE_PRICE`. -- **Shared-liquidity** — `computeVaultV1Reallocations` builds PublicAllocator V1 reallocations for a borrow/withdraw; `computeReallocations` remains its deprecated compatibility alias. `computeVaultV2Reallocations` plans BluePublicAllocator reallocations and applies the configured native-penalty threshold in both discovery phases. `getSupplyTargetUtilization(marketId, options)` resolves the per-market → default → `DEFAULT_SUPPLY_TARGET_UTILIZATION` supply target for V1. Read-only liquidity metrics live on the corresponding versioned reallocation-data entity, not in this layer. +- **Shared-liquidity** — `computeVaultV1Reallocations` builds PublicAllocator V1 reallocations for a borrow/withdraw; `computeReallocations` remains its deprecated compatibility alias. `computeVaultV2Reallocations` plans BluePublicAllocator reallocations and applies the configured WAD-scaled penalty threshold in both discovery phases. `getSupplyTargetUtilization(marketId, options)` resolves the per-market → default → `DEFAULT_SUPPLY_TARGET_UTILIZATION` supply target for V1. Read-only liquidity metrics live on the corresponding versioned reallocation-data entity, not in this layer. - **Metadata** — `addTransactionMetadata(tx, metadata)` appends hex-encoded analytics bytes to `tx.data`: an optional 4-byte unix timestamp followed by a 4-byte origin (timestamp is omitted when `metadata.timestamp` is falsy). Callers gate on `metadata` being provided; the helper itself is a no-op when `tx.data` is empty. ## Constants diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts new file mode 100644 index 000000000..d4c00ae7a --- /dev/null +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts @@ -0,0 +1,57 @@ +import { MarketParams } from "@morpho-org/blue-sdk"; +import { describe, expect, test } from "vitest"; +import { CbbtcUsdcBlue } from "../../test/fixtures/blue.js"; +import type { BlueReallocation } from "../types/index.js"; +import { + computeBluePublicAllocatorPenaltyAssets, + computeVaultV2ReallocationPenaltyAssets, +} from "./bluePublicAllocator.js"; + +const marketParams = new MarketParams(CbbtcUsdcBlue); + +describe("computeBluePublicAllocatorPenaltyAssets", () => { + test("default", () => { + expect( + computeBluePublicAllocatorPenaltyAssets( + 1_000_000n, + 1_000_000_000_000_000n, + ), + ).toBe(1_000n); + }); + + test("behavior: rounds each positive fractional penalty up", () => { + expect(computeBluePublicAllocatorPenaltyAssets(1n, 1n)).toBe(1n); + }); +}); + +describe("computeVaultV2ReallocationPenaltyAssets", () => { + test("default", () => { + const reallocations: BlueReallocation[] = [ + { + vault: CbbtcUsdcBlue.oracle, + fee: 7n, + withdrawals: [{ marketParams, amount: 1n }], + }, + { + type: "bluePublicAllocator", + allocator: CbbtcUsdcBlue.irm, + vault: CbbtcUsdcBlue.oracle, + from: { type: "idle" }, + to: { adapter: CbbtcUsdcBlue.collateralToken }, + assets: 1n, + penalty: 1n, + }, + { + type: "bluePublicAllocator", + allocator: CbbtcUsdcBlue.irm, + vault: CbbtcUsdcBlue.oracle, + from: { type: "idle" }, + to: { adapter: CbbtcUsdcBlue.collateralToken }, + assets: 1n, + penalty: 1n, + }, + ]; + + expect(computeVaultV2ReallocationPenaltyAssets(reallocations)).toBe(2n); + }); +}); diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts new file mode 100644 index 000000000..61e926679 --- /dev/null +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts @@ -0,0 +1,56 @@ +import { MathLib } from "@morpho-org/blue-sdk"; +import type { BlueReallocation } from "../types/index.js"; + +/** + * Computes the vault-asset penalty charged for one BluePublicAllocator call. + * + * Mirrors the contract's upward-rounded `assets * penalty / WAD` calculation. + * Callers must validate that `assets` and `penalty` are within the contract's + * accepted ranges before encoding a transaction. + * + * @param assets - Assets moved by the allocator call. + * @param penalty - Vault-configured proportional penalty, scaled by WAD. + * @returns Vault assets transferred by the caller directly to the vault. + * @example + * ```ts + * const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( + * 1_000_000n, + * 1_000_000_000_000_000n, + * ); + * // penaltyAssets === 1_000n + * ``` + * @internal + */ +export const computeBluePublicAllocatorPenaltyAssets = ( + assets: bigint, + penalty: bigint, +) => MathLib.wMulUp(assets, penalty); + +/** + * Sums the independently rounded vault-asset penalties in a mixed V1/V2 plan. + * + * PublicAllocator V1 entries are ignored because their fees are paid in native + * token. Each V2 call is rounded independently, matching contract execution. + * + * @param reallocations - Mixed PublicAllocator V1 and BluePublicAllocator plan. + * @returns Total target loan-token assets needed for V2 penalties. + * @example + * ```ts + * const penaltyAssets = computeVaultV2ReallocationPenaltyAssets(reallocations); + * ``` + * @internal + */ +export const computeVaultV2ReallocationPenaltyAssets = ( + reallocations: readonly BlueReallocation[], +) => + reallocations.reduce( + (total, reallocation) => + reallocation.type === "bluePublicAllocator" + ? total + + computeBluePublicAllocatorPenaltyAssets( + reallocation.assets, + reallocation.penalty, + ) + : total, + 0n, + ); diff --git a/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts b/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts index cfbb5f9a4..39fb333d3 100644 --- a/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts @@ -21,7 +21,7 @@ import { DEFAULT_SUPPLY_TARGET_UTILIZATION } from "./constant.js"; * @param params.marketId - Target Blue market id. * @param params.operation - Operation driving the reallocation. * @param params.amount - Borrow or withdraw amount. - * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum native penalty. + * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. * @throws {@link InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. * @throws {@link ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index 4f8621a13..a7b635291 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -24,6 +24,7 @@ import { ChainWNativeMissingError, EmptyReallocationWithdrawalsError, ExcessiveSlippageToleranceError, + InconsistentReallocationPenaltyError, InputExceedsMaxError, InvalidReallocationSourceTypeError, InvalidReallocationTypeError, @@ -566,7 +567,7 @@ describe("validateReallocations", () => { from: { type: "idle" }, to: { adapter: USER_A }, assets: 1n, - nativePenalty: 0n, + penalty: 0n, }; test("should pass with valid reallocations", () => { @@ -595,13 +596,21 @@ describe("validateReallocations", () => { test.each([ { - name: "negative native penalty", + name: "negative penalty", reallocation: { ...validBluePublicAllocatorReallocation, - nativePenalty: -1n, + penalty: -1n, }, ErrorClass: NegativeInputError, }, + { + name: "penalty above WAD", + reallocation: { + ...validBluePublicAllocatorReallocation, + penalty: MathLib.WAD + 1n, + }, + ErrorClass: InputExceedsMaxError, + }, { name: "zero assets", reallocation: { ...validBluePublicAllocatorReallocation, assets: 0n }, @@ -624,6 +633,34 @@ describe("validateReallocations", () => { }, ); + test("error: InconsistentReallocationPenaltyError for one allocator-vault pair", () => { + expect(() => + validateReallocations( + [ + { ...validBluePublicAllocatorReallocation, penalty: 5n }, + { ...validBluePublicAllocatorReallocation, penalty: 11n }, + ], + targetMarketId, + ), + ).toThrow(InconsistentReallocationPenaltyError); + }); + + test("behavior: allows different penalties for different allocator-vault pairs", () => { + expect(() => + validateReallocations( + [ + { ...validBluePublicAllocatorReallocation, penalty: 5n }, + { + ...validBluePublicAllocatorReallocation, + allocator: USER_B, + penalty: 11n, + }, + ], + targetMarketId, + ), + ).not.toThrow(); + }); + test("error: ReallocationWithdrawalOnTargetMarketError for a Blue Public Allocator target-market source", () => { expect(() => validateReallocations( diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index 27c95ff14..6feeed17c 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -17,6 +17,7 @@ import { ChainWNativeMissingError, EmptyReallocationWithdrawalsError, ExcessiveSlippageToleranceError, + InconsistentReallocationPenaltyError, InputExceedsMaxError, InvalidReallocationSourceTypeError, InvalidReallocationTypeError, @@ -335,20 +336,22 @@ export const validateRepayShares = (params: { * - No withdrawal may target `targetMarketId`. * - Withdrawal market IDs must be strictly ascending. * - * BluePublicAllocator entries enforce non-negative `nativePenalty`, positive `uint128`-bounded - * `assets`, and a market source distinct from `targetMarketId`. Idle sources have no market or - * sorting rule. + * BluePublicAllocator entries enforce a WAD-bounded `penalty`, one consistent + * penalty per allocator-vault pair, positive `uint128`-bounded `assets`, and a + * market source distinct from the target adapter-market pair. Idle sources + * have no market or sorting rule. * * @param reallocations - The reallocations to validate. - * @param targetMarketId - The ID of the operation's target market. No withdrawal may reference this market. + * @param targetMarketId - The operation's target market ID. V1 withdrawals cannot reference it; V2 sources cannot reference it through their target adapter. * @returns Nothing when every reallocation is valid. * @throws {NegativeInputError} when a reallocation fee is negative. * @throws {EmptyReallocationWithdrawalsError} when a reallocation has no withdrawals. * @throws {NonPositiveInputError} when a withdrawal or BluePublicAllocator asset amount is non-positive. - * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128`. + * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128` or its penalty exceeds WAD. + * @throws {InconsistentReallocationPenaltyError} when entries for one allocator-vault pair use different penalties. * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source discriminator is unknown. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. - * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. + * @throws {ReallocationWithdrawalOnTargetMarketError} when a V1 source references the target market or a V2 source references its target adapter-market pair. * @throws {UnsortedReallocationWithdrawalsError} when withdrawals are not strictly market-id sorted. * @example * ```ts @@ -363,17 +366,23 @@ export const validateReallocations = ( reallocations: readonly BlueReallocation[], targetMarketId: MarketId, ): void => { + const penaltyByAllocatorVault = new Map(); + for (const r of reallocations) { if (r.type === "bluePublicAllocator") { const sourceType: string = r.from.type; if (sourceType !== "market" && sourceType !== "idle") { throw new InvalidReallocationSourceTypeError(sourceType); } - if (r.nativePenalty < 0n) { - throw new NegativeInputError( - "reallocation.nativePenalty", - r.nativePenalty, - ); + if (r.penalty < 0n) { + throw new NegativeInputError("reallocation.penalty", r.penalty); + } + if (r.penalty > MathLib.WAD) { + throw new InputExceedsMaxError({ + field: "reallocation.penalty", + value: r.penalty, + max: MathLib.WAD, + }); } if (r.assets <= 0n) { throw new NonPositiveInputError("reallocation.assets", r.assets); @@ -385,6 +394,19 @@ export const validateReallocations = ( max: maxUint128, }); } + + const penaltyKey = `${r.allocator.toLowerCase()}:${r.vault.toLowerCase()}`; + const expectedPenalty = penaltyByAllocatorVault.get(penaltyKey); + if (expectedPenalty !== undefined && expectedPenalty !== r.penalty) { + throw new InconsistentReallocationPenaltyError({ + allocator: r.allocator, + vault: r.vault, + expected: expectedPenalty, + actual: r.penalty, + }); + } + penaltyByAllocatorVault.set(penaltyKey, r.penalty); + if ( r.from.type === "market" && r.from.marketParams.id === targetMarketId && diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index eb62507f0..be6fc5d4b 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -19,17 +19,17 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. ## Shared liquidity (`sharedLiquidity.ts`) - `VaultV1BlueReallocation` — legacy-untagged or explicitly `publicAllocatorV1` vault address + fee + sorted withdrawals; maps to `reallocateTo()`. `VaultReallocation` is its deprecated compatibility alias. -- `VaultV2BlueReallocation` — tagged BluePublicAllocator/vault/source/target-adapter/assets/native-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. -- `VaultV2BluePublicAllocatorOptions` — canonical Vault V2 discovery and planner options for timestamp, enablement, vault allowlisting, and the maximum native penalty. +- `VaultV2BlueReallocation` — tagged BluePublicAllocator/vault/source/target-adapter/assets/WAD-scaled-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. +- `VaultV2BluePublicAllocatorOptions` — canonical Vault V2 discovery and planner options for timestamp, enablement, vault allowlisting, and the maximum proportional penalty. - `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. ## Errors (`error.ts`) One class per error case. Never throw a generic `Error` from SDK source. -- **Generic input bounds:** `NegativeInputError` for values that must be non-negative, `NonPositiveInputError` for values that must be positive, and `InputExceedsMaxError` for protocol-width upper bounds such as BluePublicAllocator's `uint128` assets. All expose the invalid `field` and `value`; reuse them across Vault, Blue, and Midnight instead of adding operation-specific scalar-bound errors. +- **Generic input bounds:** `NegativeInputError` for values that must be non-negative, `NonPositiveInputError` for values that must be positive, and `InputExceedsMaxError` for protocol upper bounds such as BluePublicAllocator's `uint128` assets and WAD-scaled `uint64` penalty. All expose the invalid `field` and `value`; reuse them across Vault, Blue, and Midnight instead of adding operation-specific scalar-bound errors. - **Market-specific:** `BorrowExceedsSafeLtvError`, `MissingMarketPriceError`, `NativeAmountOnNonWNativeAssetError`, `MutuallyExclusiveWithdrawAmountsError`, `WithdrawExceedsSupplyError`, `WithdrawSharesExceedSupplyError`. -- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationTypeError` for an unknown top-level Public Allocator variant, `InvalidReallocationSourceTypeError` for an unknown BluePublicAllocator source, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. +- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationTypeError` for an unknown top-level Public Allocator variant, `InvalidReallocationSourceTypeError` for an unknown BluePublicAllocator source, `InconsistentReallocationPenaltyError` for conflicting penalties on one allocator-vault pair, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. ## Adding a new operation diff --git a/packages/morpho-sdk/src/types/action.ts b/packages/morpho-sdk/src/types/action.ts index 8894a7afa..43142b337 100644 --- a/packages/morpho-sdk/src/types/action.ts +++ b/packages/morpho-sdk/src/types/action.ts @@ -168,7 +168,10 @@ export interface BlueWithdrawAction shares: bigint; receiver: Address; minSharePrice: bigint; + /** Native-token fees paid to PublicAllocator V1. */ reallocationFee: bigint; + /** Loan-token assets donated as BluePublicAllocator V2 penalties. */ + reallocationPenaltyAssets: bigint; } > {} @@ -191,7 +194,10 @@ export interface BlueBorrowAction amount: bigint; receiver: Address; minSharePrice: bigint; + /** Native-token fees paid to PublicAllocator V1. */ reallocationFee: bigint; + /** Loan-token assets donated as BluePublicAllocator V2 penalties. */ + reallocationPenaltyAssets: bigint; } > {} @@ -206,7 +212,10 @@ export interface BlueSupplyCollateralBorrowAction onBehalf: Address; receiver: Address; nativeAmount?: bigint; + /** Native-token fees paid to PublicAllocator V1. */ reallocationFee: bigint; + /** Loan-token assets donated as BluePublicAllocator V2 penalties. */ + reallocationPenaltyAssets: bigint; } > {} @@ -266,7 +275,10 @@ export interface BlueRefinanceAction readonly minBorrowSharePrice: bigint; readonly maxRepaySharePrice: bigint; readonly user: Address; + /** Native-token fees paid to PublicAllocator V1. */ readonly reallocationFee: bigint; + /** Loan-token assets donated as BluePublicAllocator V2 penalties. */ + readonly reallocationPenaltyAssets: bigint; } > {} diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 6f933f56d..43d2fc992 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -789,7 +789,7 @@ export class EmptyReallocationWithdrawalsError extends Error { } } -/** Thrown when a reallocation withdrawal references the operation's target market (which would be a no-op or self-deal). */ +/** Thrown when a V1 withdrawal references the target market or a V2 source references its exact target adapter-market pair. */ export class ReallocationWithdrawalOnTargetMarketError extends Error { constructor(vault: string, marketId: string) { super( @@ -845,6 +845,61 @@ export class InvalidReallocationSourceTypeError extends Error { } } +/** + * Thrown when one bundle assigns different penalty rates to the same Blue + * Public Allocator and Vault V2 pair. + * + * @example + * ```ts + * import { InconsistentReallocationPenaltyError } from "@morpho-org/morpho-sdk"; + * import type { Address } from "viem"; + * + * const allocatorFixture = + * "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" satisfies Address; + * const vaultFixture = + * "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" satisfies Address; + * const error = new InconsistentReallocationPenaltyError({ + * allocator: allocatorFixture, + * vault: vaultFixture, + * expected: 5n, + * actual: 11n, + * }); + * ``` + */ +export class InconsistentReallocationPenaltyError extends Error { + /** Blue Public Allocator contract address. */ + public readonly allocator: Address; + /** Vault whose configured penalty must be reused. */ + public readonly vault: Address; + /** Penalty rate established by the first matching bundle entry. */ + public readonly expected: bigint; + /** Conflicting penalty rate supplied by a later bundle entry. */ + public readonly actual: bigint; + + /** + * @param params - Conflicting allocator-vault penalty details. + * @param params.allocator - Blue Public Allocator contract address. + * @param params.vault - Vault whose configured penalty applies. + * @param params.expected - Penalty rate established by the first matching entry. + * @param params.actual - Conflicting penalty rate supplied by a later entry. + */ + public constructor(params: { + readonly allocator: Address; + readonly vault: Address; + readonly expected: bigint; + readonly actual: bigint; + }) { + super( + `Penalty for Blue Public Allocator "${params.allocator}" and vault "${params.vault}" must remain "${params.expected}" across the bundle, got "${params.actual}". Use the vault's configured penalty for every call.`, + ); + this.allocator = params.allocator; + this.vault = params.vault; + this.expected = params.expected; + this.actual = params.actual; + this.name = "InconsistentReallocationPenaltyError"; + } +} + /** Thrown when reallocation withdrawals within a vault are not strictly sorted by market id. */ export class UnsortedReallocationWithdrawalsError extends Error { constructor(vault: string, marketId: string) { diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 5b762de9f..1cb7e29a7 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -56,10 +56,11 @@ export interface VaultV2BluePublicAllocatorOptions { readonly reallocatableVaults?: readonly Address[]; /** - * Maximum native-token penalty accepted for each BluePublicAllocator call. - * Vaults with a higher configured penalty are ignored. Defaults to no limit. + * Maximum proportional vault-asset penalty accepted for each + * BluePublicAllocator call, scaled by WAD. Vaults with a higher configured + * penalty are ignored. Defaults to no limit. */ - readonly maxNativePenalty?: bigint; + readonly maxPenalty?: bigint; } /** @@ -134,8 +135,8 @@ export interface VaultV2BlueReallocation { readonly to: { readonly adapter: Address }; /** Asset amount, which must fit in `uint128`. */ readonly assets: bigint; - /** Native penalty paid for this individual allocator call. */ - readonly nativePenalty: bigint; + /** Vault-configured WAD-scaled penalty rate passed to the allocator. */ + readonly penalty: bigint; } /** diff --git a/packages/morpho-ts/src/abis.ts b/packages/morpho-ts/src/abis.ts index 943c1deac..040f0453a 100644 --- a/packages/morpho-ts/src/abis.ts +++ b/packages/morpho-ts/src/abis.ts @@ -4618,7 +4618,7 @@ export const vaultV2BluePublicAllocatorAbi = [ type: "bytes32", }, ], - name: "canDeallocate", + name: "canPullFromMarket", outputs: [ { internalType: "bool", @@ -4665,18 +4665,13 @@ export const vaultV2BluePublicAllocatorAbi = [ outputs: [ { internalType: "bool", - name: "canAllocateFromIdle", + name: "canPullFromIdle", type: "bool", }, { - internalType: "uint120", - name: "nativePenalty", - type: "uint120", - }, - { - internalType: "uint120", - name: "accruedNativePenalty", - type: "uint120", + internalType: "uint64", + name: "penalty", + type: "uint64", }, ], stateMutability: "view", @@ -4716,10 +4711,15 @@ export const vaultV2BluePublicAllocatorAbi = [ name: "assets", type: "uint128", }, + { + internalType: "uint64", + name: "penalty", + type: "uint64", + }, ], name: "reallocate", outputs: [], - stateMutability: "payable", + stateMutability: "nonpayable", type: "function", }, { @@ -4745,10 +4745,15 @@ export const vaultV2BluePublicAllocatorAbi = [ name: "assets", type: "uint128", }, + { + internalType: "uint64", + name: "penalty", + type: "uint64", + }, ], name: "allocateFromIdle", outputs: [], - stateMutability: "payable", + stateMutability: "nonpayable", type: "function", }, ] as const; diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index eee8ae41b..acc322cf0 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -487,7 +487,7 @@ describe.sequential("MorphoProtocolEvm", () => { adapter: "0x0000000000000000000000000000000000000020", }, assets: 50_000n, - nativePenalty: 1n, + penalty: 1n, } satisfies VaultV2BlueReallocation; account.sendTransaction = vi @@ -748,7 +748,6 @@ describe.sequential("MorphoProtocolEvm", () => { chainId: 1, provider: "https://dummy-rpc-url.com", bundlerUrl: "https://dummy-bundler-url.com", - entryPointAddress: "0x0000000000000000000000000000000000000007", safeModulesVersion: "0.3.0", isSponsored: false, useNativeCoins: true, diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 39a26e279..0f84f2325 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -667,14 +667,22 @@ export default class MorphoProtocolEvm extends LendingProtocol { * Returns Morpho SDK requirements for a borrow. * * @param options - The borrow options. - * @returns Authorization requirements. When offchain signatures are enabled - * (`supportSignature: true`), the authorization may instead be returned as a - * signable `RequirementSignatureRequest` to fold into the bundle via + * @returns Token-approval and authorization requirements. Vault V2 public + * allocator reallocations can require a loan-token approval for their + * penalty donation. When offchain signatures are enabled + * (`supportSignature: true`), the authorization may instead be returned as + * a signable `RequirementSignatureRequest` to fold into the bundle via * `setAuthorizationWithSig`. */ async getBorrowRequirements( options: MorphoBorrowOptions, - ): Promise<(RequirementAuthorization | RequirementSignatureRequest)[]> { + ): Promise< + ( + | RequirementApproval + | RequirementAuthorization + | RequirementSignatureRequest + )[] + > { const action = await this._getBorrowAction(options); return await action.getRequirements(); From a9433e9513297d280e717f0e8f13f69571a5e2f3 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 14 Aug 2026 16:45:14 +0200 Subject: [PATCH 13/41] fix: correct Vault V2 public allocator planning --- .changeset/brave-vaults-reallocate.md | 12 +- ...lt-v2-public-allocator-shared-liquidity.md | 56 ++- ...2PublicAllocatorConfig.integration.test.ts | 100 ++++ .../VaultV2PublicAllocatorConfig.test.ts | 99 +--- .../vault-v2/VaultV2PublicAllocatorConfig.ts | 56 ++- packages/blue-sdk/src/types.ts | 5 + .../vault/v2/VaultV2PublicAllocatorConfig.ts | 2 +- packages/liquidity-sdk-viem/README.md | 31 +- packages/liquidity-sdk-viem/package.json | 4 +- .../liquidity-sdk-viem/src/api/rest.test.ts | 57 +++ packages/liquidity-sdk-viem/src/api/rest.ts | 313 +++++++++++- packages/liquidity-sdk-viem/src/errors.ts | 75 ++- .../src/vaultV2LiquidityLoader.test.ts | 153 +++++- .../src/vaultV2LiquidityLoader.ts | 126 ++++- packages/morpho-sdk/BUNDLER3.md | 6 +- .../BluePublicAllocatorWriteFixture.sol | 123 +++++ packages/morpho-sdk/package.json | 1 + packages/morpho-sdk/src/actions/AGENTS.md | 2 +- .../blue/borrow.bluePublicAllocator.test.ts | 2 +- .../morpho-sdk/src/actions/blue/borrow.ts | 3 +- .../actions/blue/buildReallocationActions.ts | 41 +- .../src/actions/blue/refinance.test.ts | 107 ++++- .../morpho-sdk/src/actions/blue/refinance.ts | 3 +- .../blue/supplyCollateralBorrow.test.ts | 105 +++- .../actions/blue/supplyCollateralBorrow.ts | 55 ++- .../vaultV2Reallocations.integration.test.ts | 365 ++++++++++++++ .../blue/withdraw.bluePublicAllocator.test.ts | 110 +++++ .../morpho-sdk/src/actions/blue/withdraw.ts | 3 +- .../morpho-sdk/src/bundler/actions.test.ts | 44 +- packages/morpho-sdk/src/bundler/actions.ts | 26 +- ...ue.bluePublicAllocatorRequirements.test.ts | 177 ++++++- packages/morpho-sdk/src/entities/blue/blue.ts | 36 +- packages/morpho-sdk/src/entities/index.ts | 1 + .../entities/vaultV2ReallocationData.test.ts | 112 ++++- .../src/entities/vaultV2ReallocationData.ts | 452 +++++++++++++----- packages/morpho-sdk/src/helpers/AGENTS.md | 2 +- .../helpers/computeVaultV2Reallocations.ts | 138 ------ packages/morpho-sdk/src/helpers/index.ts | 1 - .../morpho-sdk/src/helpers/validate.test.ts | 94 +++- packages/morpho-sdk/src/helpers/validate.ts | 60 ++- packages/morpho-sdk/src/index.test.ts | 8 + packages/morpho-sdk/src/index.ts | 1 + packages/morpho-sdk/src/types/AGENTS.md | 2 +- packages/morpho-sdk/src/types/action.ts | 6 +- packages/morpho-sdk/src/types/error.ts | 81 +++- packages/morpho-sdk/src/utils.ts | 2 +- .../BluePublicAllocatorWriteFixture.ts | 429 +++++++++++++++++ packages/morpho-sdk/test/helpers/vaultV2.ts | 86 +++- .../wdk-protocol-lending-morpho-evm/README.md | 29 +- .../src/index.ts | 1 + .../src/morpho-protocol-evm.test.ts | 50 +- .../src/morpho-protocol-evm.ts | 46 +- scripts/compile-solidity.js | 12 + 53 files changed, 3322 insertions(+), 589 deletions(-) create mode 100644 packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts create mode 100644 packages/liquidity-sdk-viem/src/api/rest.test.ts create mode 100644 packages/morpho-sdk/contracts/fixtures/BluePublicAllocatorWriteFixture.sol create mode 100644 packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts create mode 100644 packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts delete mode 100644 packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts create mode 100644 packages/morpho-sdk/src/index.test.ts create mode 100644 packages/morpho-sdk/test/fixtures/BluePublicAllocatorWriteFixture.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index d2fde5fa3..2ffec153c 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -7,4 +7,14 @@ "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, approve the allocator from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases, add an independent REST-backed `VaultV2LiquidityLoader` alongside the existing Vault V1 loader, raise its `blue-sdk-viem` and `morpho-sdk` peer floors to the introducing minors, and allow the WDK borrow flow to accept the combined V1/V2 reallocation union. +Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. + +V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual, and target allocation; rejects same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. + +Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases. + +Compatibility note: `VaultV2MorphoMarketV1AdapterV2.ids()` now declares its existing three-element result as `readonly [Hash, Hash, Hash]`. The runtime values and ordering are unchanged, and derived allocation identifiers are immutable descriptors. We intentionally accept this TypeScript assignability tightening in the minor release; callers that explicitly require a mutable `Hash[]` can copy the tuple with `[...adapter.ids(params)]`. + +Add an independent REST-backed `VaultV2LiquidityLoader` alongside the existing Vault V1 loader. It validates successful API payloads at runtime, pins REST and RPC hydration to one indexed block, anchors live REST market totals to that block's timestamp to prevent double accrual, and fails explicitly on incomplete positions instead of treating missing state as zero. Raise its `blue-sdk`, `blue-sdk-viem`, `morpho-sdk`, and `morpho-ts` peer floors to the introducing versions. + +Add an explicit `MorphoBorrowWithV2ReallocationsOptions` WDK opt-in for the combined V1/V2 reallocation union and its possible approval requirement while preserving the legacy `MorphoBorrowOptions` input and authorization-only requirement result type. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index 3e68c43a8..13a318609 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -244,9 +244,11 @@ It returns both the capacity and the binding `CapacityLimitReason`. ## Accrual and untracked interest -Each considered vault is accrued once at the supplied timestamp. The accrued -vault's `_totalAssets` becomes the plan's frozen `firstTotalAssets` -denominator. Reallocation legs never change it. +Markets are first accrued to the supplied timestamp. For each vault, the first +simulated allocator call then follows contract order: transfer the penalty, +deallocate the source when present, and let `VaultV2.allocate()` perform the +vault's first accrual. The resulting `_totalAssets` becomes the plan's frozen +`firstTotalAssets` denominator. Later reallocation legs never change it. For adapter `a` and market `m`: @@ -277,8 +279,8 @@ A candidate exists only when: that threshold is provided; - all three target vault caps have a positive absolute cap; - all three source allocations are non-zero for market sources; -- the source pair is not the exact target `(adapter, market)` pair. The same - Blue market through another adapter is valid. +- the source Blue market is not the target market. Moving liquidity between + adapters of the same market creates no net market liquidity and is ignored. For each allocation ID shared by the source and target, feasibility is checked without principal cancellation: @@ -289,13 +291,16 @@ allocation[id] + sourceUntracked + targetUntracked ``` For non-shared target IDs, principal is bounded by cap headroom after target -untracked interest. The final obtainable amount is the minimum of: +untracked interest. A monotonic binary search applies each candidate amount to +a clone, then checks the exact post-accrual allocations against +`VaultV2Utils.allocationHeadroom({ ...allocation, allocation: 0n }, +firstTotalAssets)`. This is necessary because the first penalty donation can +change `firstTotalAssets` as the candidate amount changes. The initial search +ceiling is the minimum of: - `MathLib.MAX_UINT_128`; -- target Morpho market `uint128` supply headroom (unless a same-market source - deallocation creates the headroom in the same call); +- target Morpho market `uint128` supply headroom; - allocator target-cap headroom; -- each non-shared target Vault V2 cap headroom; - source expected supply assets; - source Blue withdrawal capacity to the configured utilization ceiling; or - the vault idle balance for an idle source. @@ -316,7 +321,7 @@ sources are applied in contract order: | source market/shares | withdraw first | unchanged | | target market/shares | supply second | supply | | vault idle balance | `+= penaltyAssets`, then `+= assets`, then `-= assets` | `+= penaltyAssets`, then `-= assets` | -| vault `_totalAssets` | unchanged | unchanged | +| vault `_totalAssets` | first call accrues after penalty + deallocation; then frozen | first call accrues after penalty; then frozen | Shared IDs are updated twice in that order. Penalties remain as direct vault asset donations. The planner records them in the cloned idle balance but does @@ -353,9 +358,8 @@ the final capped `assets` amount. The planner throws: The existing `validateReallocations` validates the combined `BlueReallocation` union. V2 penalties must be between zero and WAD (and therefore fit the contract's `uint64`), and every call for the same explicit allocator-vault pair -must use one consistent penalty. A V2 market source is rejected only when both -its adapter and market match the target pair. The same market through another -adapter is accepted. +must use one consistent penalty. A V2 market source is rejected whenever its +Blue market matches the target, regardless of adapter. `VaultV2ReallocationData` exposes: @@ -405,24 +409,29 @@ source and target thresholds plus an internal 100% fallback. deprecated. - `BluePublicAllocatorReallocation` receives no alias because it was not part of the published surface relative to `origin/main`. -- `liquidity-sdk-viem` migrates its public state declarations to - `VaultV1ReallocationData`; this is type-compatible with the deprecated class - alias and ships as a patch. -- The feature is minor for `morpho-ts`, `blue-sdk`, `blue-sdk-viem`, and - `morpho-sdk`. +- The compatible `liquidity-sdk-viem` V1 type-name migration would be a patch + in isolation. The new public Vault V2 loader makes this package a minor. +- The feature is minor for `morpho-ts`, `blue-sdk`, `blue-sdk-viem`, + `morpho-sdk`, `liquidity-sdk-viem`, and + `wdk-protocol-lending-morpho-evm`. - `blue-sdk-viem` raises its `blue-sdk` peer range to the new minor. +- `liquidity-sdk-viem` raises its `blue-sdk`, `blue-sdk-viem`, `morpho-sdk`, + and `morpho-ts` peer floors to the versions that introduce the V2 loader's + runtime imports. ## Security and operational constraints - A plan is a block-state simulation, not an execution guarantee. Allocator caps, shares, and market liquidity can be front-run. -- The caller must approve GeneralAdapter1 for the aggregate V2 penalty assets. - `getRequirements()` emits a classic loan-token approval when needed; V2 - allocator calls themselves are nonpayable. +- The user approves GeneralAdapter1 for the aggregate V2 penalty assets. + `getRequirements()` emits a classic loan-token approval when needed; + Bundler3 then grants the BluePublicAllocator an exact, non-skippable per-call + allowance before each nonpayable allocator call. - The calldata penalty protects against a curator changing the configured rate between transaction signing and execution: a mismatch reverts. -- Pass `options.timestamp` from the block used to fetch state so market and - vault accrual share one reference point. +- The planner defaults to the latest `lastUpdate` in its snapshot. Loaders + should pass the intended execution timestamp explicitly when simulating + beyond that snapshot so every market and vault shares one reference point. - Relative-cap arithmetic rounds down. Overstating by one wei can cause an on-chain revert. - The upstream ABI and fork fixture must stay pinned to the same Vault V2 @@ -446,6 +455,5 @@ source and target thresholds plus an internal 100% fallback. - `packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts` - `packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts` - `packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts` -- `packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts` - `packages/blue-sdk/src/vault/v2/VaultV2Utils.ts` - `packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts` diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts new file mode 100644 index 000000000..01d12de20 --- /dev/null +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts @@ -0,0 +1,100 @@ +import { AccrualVaultV2MorphoMarketV1AdapterV2 } from "@morpho-org/blue-sdk"; +import { assert, describe, expect } from "vitest"; +import { + abi as fixtureAbi, + code as fixtureCode, +} from "../../../test/fixtures/BluePublicAllocatorReadFixture.js"; +import { vaultV2Test } from "../../../test/setup.js"; +import { fetchAccrualVaultV2 } from "./VaultV2.js"; +import { fetchVaultV2PublicAllocatorData } from "./VaultV2PublicAllocatorConfig.js"; + +describe("Vault V2 public allocator fetchers on fork", () => { + vaultV2Test( + "default: matches direct reads against the deployless query", + async ({ client }) => { + const forkVault = await fetchAccrualVaultV2( + "0x4C7b69b4a82e9E5D8ec60E96516f7A0E17CBC55C", + client, + ); + const forkAdapter = forkVault.accrualAdapters.find( + (candidate) => + candidate instanceof AccrualVaultV2MorphoMarketV1AdapterV2, + ); + assert(forkAdapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2); + + const forkMarket = forkAdapter.markets[0]; + assert(forkMarket != null); + + const deploymentHash = await client.deployContract({ + abi: fixtureAbi, + bytecode: fixtureCode, + }); + const { contractAddress: allocator } = + await client.waitForTransactionReceipt({ hash: deploymentHash }); + assert(allocator != null); + + const forkMarketParamsId = forkAdapter.ids(forkMarket.params)[2]; + await client.writeContract({ + address: allocator, + abi: fixtureAbi, + functionName: "setVaultData", + args: [forkVault.address, true, 12n], + }); + await client.writeContract({ + address: allocator, + abi: fixtureAbi, + functionName: "setAbsoluteCap", + args: [forkVault.address, forkMarketParamsId, 500n], + }); + await client.writeContract({ + address: allocator, + abi: fixtureAbi, + functionName: "setCanPullFromMarket", + args: [forkVault.address, forkMarketParamsId, true], + }); + await client.writeContract({ + address: allocator, + abi: fixtureAbi, + functionName: "setIsActiveAdapter", + args: [forkVault.address, forkAdapter.address, true], + }); + + const [deployless, direct] = await Promise.all([ + fetchVaultV2PublicAllocatorData(allocator, forkVault, client, { + deployless: "force", + }), + fetchVaultV2PublicAllocatorData(allocator, forkVault, client, { + deployless: false, + }), + ]); + + expect(deployless).toStrictEqual(direct); + expect(deployless.publicAllocatorConfig).toStrictEqual({ + allocator, + vault: forkVault.address, + canPullFromIdle: true, + penalty: 12n, + }); + expect( + deployless.marketPublicAllocatorConfigs[forkMarketParamsId], + ).toStrictEqual({ + allocator, + vault: forkVault.address, + adapter: forkAdapter.address, + marketParamsId: forkMarketParamsId, + absoluteCap: 500n, + canPullFromMarket: true, + isActiveAdapter: true, + }); + expect( + Object.values(deployless.allocations).some( + (allocation) => + allocation != null && + (allocation.absoluteCap > 0n || + allocation.relativeCap > 0n || + allocation.allocation > 0n), + ), + ).toBe(true); + }, + ); +}); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts index f097c6433..1f1f89e92 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts @@ -9,19 +9,13 @@ import { createMockClient, mockRead } from "@morpho-org/test/mock"; import type { Address } from "viem"; import { zeroAddress } from "viem"; import { mainnet } from "viem/chains"; -import { assert, describe, expect, test } from "vitest"; -import { - abi as fixtureAbi, - code as fixtureCode, -} from "../../../test/fixtures/BluePublicAllocatorReadFixture.js"; -import { vaultV2Test } from "../../../test/setup.js"; +import { describe, expect, test } from "vitest"; import { mockDeploylessRead, mockDeploylessReads, } from "../../__test__/viem.js"; import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; import { abi as queryAbi } from "../../queries/vault-v2/GetVaultV2PublicAllocatorConfig.js"; -import { fetchAccrualVaultV2 } from "./VaultV2.js"; import { fetchVaultV2MarketPublicAllocatorConfig, fetchVaultV2PublicAllocatorConfig, @@ -220,94 +214,3 @@ describe("Vault V2 public allocator fetchers", () => { ).resolves.toStrictEqual(expected); }); }); - -describe("Vault V2 public allocator fetchers on fork", () => { - vaultV2Test( - "matches direct reads against the deployless query", - async ({ client }) => { - const forkVault = await fetchAccrualVaultV2( - "0x4C7b69b4a82e9E5D8ec60E96516f7A0E17CBC55C", - client, - ); - const forkAdapter = forkVault.accrualAdapters.find( - (candidate) => - candidate instanceof AccrualVaultV2MorphoMarketV1AdapterV2, - ); - assert(forkAdapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2); - - const forkMarket = forkAdapter.markets[0]; - assert(forkMarket != null); - - const deploymentHash = await client.deployContract({ - abi: fixtureAbi, - bytecode: fixtureCode, - }); - const { contractAddress: allocator } = - await client.waitForTransactionReceipt({ hash: deploymentHash }); - assert(allocator != null); - - const forkMarketParamsId = forkAdapter.ids(forkMarket.params)[2]; - await client.writeContract({ - address: allocator, - abi: fixtureAbi, - functionName: "setVaultData", - args: [forkVault.address, true, 12n], - }); - await client.writeContract({ - address: allocator, - abi: fixtureAbi, - functionName: "setAbsoluteCap", - args: [forkVault.address, forkMarketParamsId, 500n], - }); - await client.writeContract({ - address: allocator, - abi: fixtureAbi, - functionName: "setCanPullFromMarket", - args: [forkVault.address, forkMarketParamsId, true], - }); - await client.writeContract({ - address: allocator, - abi: fixtureAbi, - functionName: "setIsActiveAdapter", - args: [forkVault.address, forkAdapter.address, true], - }); - - const [deployless, direct] = await Promise.all([ - fetchVaultV2PublicAllocatorData(allocator, forkVault, client, { - deployless: "force", - }), - fetchVaultV2PublicAllocatorData(allocator, forkVault, client, { - deployless: false, - }), - ]); - - expect(deployless).toStrictEqual(direct); - expect(deployless.publicAllocatorConfig).toStrictEqual({ - allocator, - vault: forkVault.address, - canPullFromIdle: true, - penalty: 12n, - }); - expect( - deployless.marketPublicAllocatorConfigs[forkMarketParamsId], - ).toStrictEqual({ - allocator, - vault: forkVault.address, - adapter: forkAdapter.address, - marketParamsId: forkMarketParamsId, - absoluteCap: 500n, - canPullFromMarket: true, - isActiveAdapter: true, - }); - expect( - Object.values(deployless.allocations).some( - (allocation) => - allocation != null && - (allocation.absoluteCap > 0n || - allocation.relativeCap > 0n || - allocation.allocation > 0n), - ), - ).toBe(true); - }, - ); -}); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts index 9071ad0bf..b70af358a 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts @@ -28,11 +28,21 @@ import type { * @param parameters.blockTag - Optional block tag for historical reads. * @param parameters.stateOverride - Optional viem state override. * @returns The vault's idle-pull permission and WAD-scaled vault-asset penalty. + * @throws {viem.BaseError} when the contract read fails. * @example * ```ts + * import type { VaultV2PublicAllocatorConfig } from "@morpho-org/blue-sdk"; * import { fetchVaultV2PublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; + * import { type Address, createPublicClient, http } from "viem"; + * import { mainnet } from "viem/chains"; * - * const config = await fetchVaultV2PublicAllocatorConfig(allocator, vault, client); + * const client = createPublicClient({ chain: mainnet, transport: http() }); + * export async function fetchAllocatorConfig( + * allocator: Address, + * vault: Address, + * ): Promise { + * return fetchVaultV2PublicAllocatorConfig(allocator, vault, client); + * } * ``` */ // biome-ignore lint/complexity/useMaxParams: identity fields mirror the allocator's mapping keys @@ -71,17 +81,29 @@ export async function fetchVaultV2PublicAllocatorConfig( * @param parameters.blockTag - Optional block tag for historical reads. * @param parameters.stateOverride - Optional viem state override. * @returns The allocator cap and permissions for the adapter-market pair. + * @throws {viem.BaseError} when one of the contract reads fails. * @example * ```ts + * import type { VaultV2MarketPublicAllocatorConfig } from "@morpho-org/blue-sdk"; * import { fetchVaultV2MarketPublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; + * import { type Address, createPublicClient, type Hash, http } from "viem"; + * import { mainnet } from "viem/chains"; * - * const config = await fetchVaultV2MarketPublicAllocatorConfig( - * allocator, - * vault, - * adapter, - * marketParamsId, - * client, - * ); + * const client = createPublicClient({ chain: mainnet, transport: http() }); + * export async function fetchMarketAllocatorConfig( + * allocator: Address, + * vault: Address, + * adapter: Address, + * marketParamsId: Hash, + * ): Promise { + * return fetchVaultV2MarketPublicAllocatorConfig( + * allocator, + * vault, + * adapter, + * marketParamsId, + * client, + * ); + * } * ``` */ // biome-ignore lint/complexity/useMaxParams: identity fields mirror the allocator's mapping keys @@ -146,11 +168,27 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( * @param parameters.stateOverride - Optional viem state override. * @param parameters.deployless - Deployless mode; defaults to `true`, with direct-read fallback. * @returns Vault-wide config, adapter-market configs keyed by `marketParamsId`, and allocations keyed by derived id. + * @throws {viem.BaseError} when deployless mode is forced and fails, or when a direct contract read fails. * @example * ```ts + * import type { AccrualVaultV2 } from "@morpho-org/blue-sdk"; * import { fetchVaultV2PublicAllocatorData } from "@morpho-org/blue-sdk-viem"; + * import { type Address, createPublicClient, http } from "viem"; + * import { mainnet } from "viem/chains"; * - * const data = await fetchVaultV2PublicAllocatorData(allocator, vault, client); + * const client = createPublicClient({ chain: mainnet, transport: http() }); + * export async function fetchAllocatorData( + * allocator: Address, + * vault: AccrualVaultV2, + * ) { + * const data = await fetchVaultV2PublicAllocatorData( + * allocator, + * vault, + * client, + * ); + * // data contains publicAllocatorConfig, marketPublicAllocatorConfigs, and allocations. + * return data; + * } * ``` */ // biome-ignore lint/complexity/useMaxParams: follows the package's address/entity/client/options fetcher convention diff --git a/packages/blue-sdk/src/types.ts b/packages/blue-sdk/src/types.ts index 826449b77..69a9c30ca 100644 --- a/packages/blue-sdk/src/types.ts +++ b/packages/blue-sdk/src/types.ts @@ -5,6 +5,11 @@ import type { BigIntish as SharedBigIntish } from "@morpho-org/morpho-ts"; */ export type Address = `0x${string}`; +/** + * A 0x-prefixed hash value used for protocol identifiers. + */ +export type Hash = `0x${string}`; + /** * The id of a market used on the Blue contract */ diff --git a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts index 93ccf0d89..fa55be003 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts @@ -1,4 +1,4 @@ -import type { Address, Hash } from "viem"; +import type { Address, Hash } from "../../types.js"; /** Public allocator configuration for one Vault V2. */ export interface VaultV2PublicAllocatorConfig { diff --git a/packages/liquidity-sdk-viem/README.md b/packages/liquidity-sdk-viem/README.md index b07b75387..fbce70c31 100644 --- a/packages/liquidity-sdk-viem/README.md +++ b/packages/liquidity-sdk-viem/README.md @@ -60,21 +60,26 @@ const { withdrawals, startState, endState, targetBorrowUtilization } = ```typescript import type { MarketId } from "@morpho-org/blue-sdk"; -import { VaultV2LiquidityLoader } from "@morpho-org/liquidity-sdk-viem"; -import { createPublicClient, http } from "viem"; +import { + type VaultV2LiquidityResult, + VaultV2LiquidityLoader, +} from "@morpho-org/liquidity-sdk-viem"; +import { type Address, createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; -const client = createPublicClient({ chain: mainnet, transport: http() }); -const loader = new VaultV2LiquidityLoader(client, { - allocator: "0x0000000000000000000000000000000000000001", - vaults: ["0x0000000000000000000000000000000000000002"], - maxPenalty: 1_000_000_000_000_000n, -}); -const marketId = - "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc" as MarketId; - -const { reallocations, startState, endState, targetBorrowUtilization } = - await loader.fetch(marketId); +export async function loadVaultV2Liquidity( + allocator: Address, + vault: Address, + marketId: MarketId, +): Promise { + const client = createPublicClient({ chain: mainnet, transport: http() }); + const loader = new VaultV2LiquidityLoader(client, { + allocator, + vaults: [vault], + maxPenalty: 1_000_000_000_000_000n, + }); + return loader.fetch(marketId); +} ``` `VaultV2LiquidityLoader` is a separate REST-backed loader. It reads Vault V2 configuration, state, allocations, withdrawal penalties, Blue market state, adapter positions, oracle prices, and adaptive-curve IRM state from the Morpho REST APIs. BluePublicAllocator-only configuration remains an onchain read through the supplied viem client. The allocator and participating Vault V2 addresses are explicit because the protocol has no canonical allocator registry entry. Its `reallocations` can be passed directly to Morpho SDK Blue borrow and withdraw actions. diff --git a/packages/liquidity-sdk-viem/package.json b/packages/liquidity-sdk-viem/package.json index eb7065a66..1fde2376c 100644 --- a/packages/liquidity-sdk-viem/package.json +++ b/packages/liquidity-sdk-viem/package.json @@ -29,10 +29,10 @@ "codegen": "graphql-codegen --config codegen.ts" }, "peerDependencies": { - "@morpho-org/blue-sdk": "^6.0.0", + "@morpho-org/blue-sdk": "^6.5.0", "@morpho-org/blue-sdk-viem": "^5.3.0", "@morpho-org/morpho-sdk": "^5.5.0", - "@morpho-org/morpho-ts": "^2.7.0", + "@morpho-org/morpho-ts": "^2.9.0", "dataloader": "^2.2.3", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", "graphql-request": "^6.1.0", diff --git a/packages/liquidity-sdk-viem/src/api/rest.test.ts b/packages/liquidity-sdk-viem/src/api/rest.test.ts new file mode 100644 index 000000000..414cf3d8f --- /dev/null +++ b/packages/liquidity-sdk-viem/src/api/rest.test.ts @@ -0,0 +1,57 @@ +import { BLUE_API_BASE_URL } from "@morpho-org/morpho-ts"; +import nock from "nock"; +import { type Address, zeroAddress, zeroHash } from "viem"; +import { mainnet } from "viem/chains"; +import { afterEach, describe, expect, test } from "vitest"; +import { InvalidVaultV2LiquidityApiResponseError } from "../errors.js"; +import { fetchRestVaultV2Allocations } from "./rest.js"; + +const VAULT: Address = "0x0000000000000000000000000000000000000001"; + +const cap = { + cap_id: zeroHash, + cap_data: "0x", + allocated_assets: "0", + absolute_cap: "1000", + relative_cap_wad: "1000000000000000000", +} as const; + +describe.sequential("fetchRestVaultV2Allocations", () => { + afterEach(() => { + nock.cleanAll(); + }); + + test.each([ + ["market_v1 cap without market_id", { ...cap, cap_type: "market_v1" }], + [ + "collateral cap without collateral_address", + { ...cap, cap_type: "collateral" }, + ], + ] as const)( + "error: InvalidVaultV2LiquidityApiResponseError for %s", + async (_case, malformedCap) => { + const api = nock(BLUE_API_BASE_URL) + .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}/allocations`) + .reply(200, { + data: { + chain_id: mainnet.id, + vault_address: VAULT, + last_indexed_block: "1", + allocations: [ + { + adapter_address: zeroAddress, + adapter_kind: "morpho_market_v1_v2", + caps: [malformedCap], + }, + ], + unscoped_caps: [], + }, + }); + + await expect( + fetchRestVaultV2Allocations(mainnet.id, VAULT), + ).rejects.toBeInstanceOf(InvalidVaultV2LiquidityApiResponseError); + api.done(); + }, + ); +}); diff --git a/packages/liquidity-sdk-viem/src/api/rest.ts b/packages/liquidity-sdk-viem/src/api/rest.ts index 1d863c5ef..98898a749 100644 --- a/packages/liquidity-sdk-viem/src/api/rest.ts +++ b/packages/liquidity-sdk-viem/src/api/rest.ts @@ -1,15 +1,20 @@ import type { MarketId } from "@morpho-org/blue-sdk"; import { BLUE_API_BASE_URL } from "@morpho-org/morpho-ts"; -import type { Address, Hash, Hex } from "viem"; import { + type Address, + type Hash, + type Hex, + isAddress, + isAddressEqual, + isHex, + size, +} from "viem"; +import { + InvalidVaultV2LiquidityApiResponseError, MissingVaultV2LiquidityApiDataError, VaultV2LiquidityApiError, } from "../errors.js"; -interface ApiEnvelope { - readonly data: Data; -} - interface VaultV2AssetResponse { readonly address: Address; readonly decimals: number; @@ -62,17 +67,33 @@ interface VaultV2StateResponse { readonly share_price_ray: string; } -interface VaultV2CapResponse { +interface VaultV2CapResponseBase { readonly cap_id: Hash; readonly cap_data: Hex; readonly allocated_assets: string; readonly absolute_cap: string; readonly relative_cap_wad: string; - readonly cap_type: "adapter" | "collateral" | "market_v1"; - readonly market_id?: MarketId; - readonly collateral_address?: Address; } +type VaultV2CapResponse = VaultV2CapResponseBase & + ( + | { + readonly cap_type: "adapter"; + readonly market_id?: MarketId; + readonly collateral_address?: Address; + } + | { + readonly cap_type: "collateral"; + readonly market_id?: MarketId; + readonly collateral_address: Address; + } + | { + readonly cap_type: "market_v1"; + readonly market_id: MarketId; + readonly collateral_address?: Address; + } + ); + interface VaultV2AdapterAllocationResponse { readonly adapter_address: Address; readonly adapter_kind: @@ -152,7 +173,7 @@ interface MarketPositionParameters { interface OracleStateResponse { readonly chain_id: number; readonly oracle_address: Address; - readonly last_indexed_block?: string; + readonly last_indexed_block: string; readonly last_updated_at?: string | null; readonly price?: string | null; } @@ -169,9 +190,209 @@ interface MarketIrmResponse { readonly borrowToTarget: number | null; } +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); +const isInteger = (value: unknown): value is number => + typeof value === "number" && Number.isSafeInteger(value); +const isFiniteNumber = (value: unknown): value is number => + typeof value === "number" && Number.isFinite(value); +const isDecimalString = (value: unknown): value is string => + typeof value === "string" && /^\d+$/.test(value); +const isAddressValue = (value: unknown): value is Address => + typeof value === "string" && isAddress(value); +const isNullableAddress = (value: unknown): value is Address | null => + value === null || isAddressValue(value); +const isHexValue = (value: unknown): value is Hex => + typeof value === "string" && isHex(value, { strict: true }); +const isHashValue = (value: unknown): value is Hash => + isHexValue(value) && size(value) === 32; +const isNullableDecimalString = (value: unknown): value is string | null => + value === null || isDecimalString(value); + +const responseValidators = { + vault: (value: unknown): value is VaultV2Response => { + if (!isRecord(value) || !isRecord(value.asset) || !isRecord(value.gates)) + return false; + const { asset, gates } = value; + return ( + isInteger(value.chain_id) && + isAddressValue(value.address) && + isDecimalString(value.last_indexed_block) && + typeof value.version === "string" && + typeof value.name === "string" && + typeof value.symbol === "string" && + isAddressValue(asset.address) && + isInteger(asset.decimals) && + typeof asset.name === "string" && + typeof asset.symbol === "string" && + isInteger(value.decimals_offset) && + isAddressValue(value.factory_address) && + isDecimalString(value.creation_block_number) && + isAddressValue(value.owner) && + isAddressValue(value.curator) && + isInteger(value.timelock_seconds) && + isNullableDecimalString(value.management_fee_wad) && + isNullableAddress(value.management_fee_recipient) && + isNullableDecimalString(value.performance_fee_wad) && + isNullableAddress(value.performance_fee_recipient) && + isDecimalString(value.max_rate_per_second_wad) && + isAddressValue(value.adapter_registry) && + isAddressValue(value.liquidity_adapter) && + isHexValue(value.liquidity_data) && + isNullableAddress(gates.send_shares) && + isNullableAddress(gates.receive_shares) && + isNullableAddress(gates.send_assets) && + isNullableAddress(gates.receive_assets) + ); + }, + vaultState: (value: unknown): value is VaultV2StateResponse => + isRecord(value) && + isInteger(value.chain_id) && + isAddressValue(value.address) && + isDecimalString(value.last_indexed_block) && + isInteger(value.last_accrual_timestamp) && + isDecimalString(value.total_assets) && + isDecimalString(value.total_supply) && + isDecimalString(value.withdrawable_assets) && + isDecimalString(value.allocated_assets) && + isDecimalString(value.idle_assets) && + isDecimalString(value.share_price_ray), + vaultAllocations: (value: unknown): value is VaultV2AllocationsResponse => { + if ( + !isRecord(value) || + !isInteger(value.chain_id) || + !isAddressValue(value.vault_address) || + !isDecimalString(value.last_indexed_block) || + !Array.isArray(value.allocations) || + !Array.isArray(value.unscoped_caps) + ) + return false; + + const caps = [...value.unscoped_caps]; + for (const adapter of value.allocations) { + if ( + !isRecord(adapter) || + !isAddressValue(adapter.adapter_address) || + (adapter.adapter_kind !== "morpho_market_v1" && + adapter.adapter_kind !== "morpho_market_v1_v2" && + adapter.adapter_kind !== "morpho_vault_v1" && + adapter.adapter_kind !== "morpho_vault_v2") || + !Array.isArray(adapter.caps) + ) + return false; + caps.push(...adapter.caps); + } + + return caps.every((cap) => { + if ( + !isRecord(cap) || + !isHashValue(cap.cap_id) || + !isHexValue(cap.cap_data) || + !isDecimalString(cap.allocated_assets) || + !isDecimalString(cap.absolute_cap) || + !isDecimalString(cap.relative_cap_wad) || + (cap.market_id !== undefined && !isHashValue(cap.market_id)) || + (cap.collateral_address !== undefined && + !isAddressValue(cap.collateral_address)) + ) + return false; + + switch (cap.cap_type) { + case "adapter": + return true; + case "collateral": + return isAddressValue(cap.collateral_address); + case "market_v1": + return isHashValue(cap.market_id); + default: + return false; + } + }); + }, + withdrawalOptions: ( + value: unknown, + ): value is VaultV2WithdrawalOptionsResponse => + isRecord(value) && + isInteger(value.chain_id) && + isAddressValue(value.vault_address) && + isDecimalString(value.liquidity_adapter_available_assets) && + isDecimalString(value.idle_assets) && + Array.isArray(value.adapter_penalties) && + value.adapter_penalties.every( + (penalty) => + isRecord(penalty) && + isAddressValue(penalty.adapter_address) && + (penalty.adapter_kind === "blue_market_adapter" || + penalty.adapter_kind === "vault_v1_adapter" || + penalty.adapter_kind === "vault_v2_adapter" || + penalty.adapter_kind === "unknown_adapter") && + isDecimalString(penalty.force_deallocatable_assets) && + isDecimalString(penalty.penalty_rate_wad), + ), + market: (value: unknown): value is MarketResponse => + isRecord(value) && + isInteger(value.chain_id) && + isHashValue(value.market_id) && + isAddressValue(value.loan_token) && + isAddressValue(value.collateral_token) && + isAddressValue(value.oracle_address) && + isAddressValue(value.irm_address) && + isDecimalString(value.lltv_wad) && + isDecimalString(value.creation_block_number), + marketState: (value: unknown): value is MarketStateResponse => + isRecord(value) && + isInteger(value.chain_id) && + isHashValue(value.market_id) && + isDecimalString(value.last_indexed_block) && + isInteger(value.last_accrual_timestamp) && + isDecimalString(value.total_supply_assets) && + isDecimalString(value.total_supply_shares) && + isDecimalString(value.total_borrow_assets) && + isDecimalString(value.total_borrow_shares) && + isDecimalString(value.fee_wad), + marketPosition: (value: unknown): value is MarketPositionResponse => + isRecord(value) && + isInteger(value.chain_id) && + isHashValue(value.market_id) && + isAddressValue(value.user_address) && + isDecimalString(value.last_indexed_block) && + isDecimalString(value.collateral_assets) && + isDecimalString(value.supply_shares) && + isDecimalString(value.borrow_shares), + oracleState: (value: unknown): value is OracleStateResponse => + isRecord(value) && + isInteger(value.chain_id) && + isAddressValue(value.oracle_address) && + isDecimalString(value.last_indexed_block) && + (value.last_updated_at === undefined || + value.last_updated_at === null || + isDecimalString(value.last_updated_at)) && + (value.price === undefined || + value.price === null || + isDecimalString(value.price)), + marketIrm: (value: unknown): value is MarketIrmResponse => + isRecord(value) && + isInteger(value.chainId) && + isHashValue(value.marketId) && + isAddressValue(value.irmAddress) && + isFiniteNumber(value.targetUtilization) && + (value.utilization === null || isFiniteNumber(value.utilization)) && + (value.apyAtTarget === null || isFiniteNumber(value.apyAtTarget)) && + (value.rateAtTarget === undefined || + value.rateAtTarget === null || + isDecimalString(value.rateAtTarget)) && + (value.borrowToTarget === null || isFiniteNumber(value.borrowToTarget)), +}; + async function requestApi( path: string, - responseKind: "envelope" | "root" = "envelope", + { + validator, + responseKind = "envelope", + }: { + readonly validator: (value: unknown) => value is Data; + readonly responseKind?: "envelope" | "root"; + }, ): Promise { const url = new URL(path, BLUE_API_BASE_URL); let response: Response; @@ -192,7 +413,7 @@ async function requestApi( status: response.status, }); - let body: Data | ApiEnvelope; + let body: unknown; try { body = await response.json(); } catch (error) { @@ -205,11 +426,12 @@ async function requestApi( if (body == null) throw new MissingVaultV2LiquidityApiDataError(url.toString()); - if (responseKind === "root") return body as Data; - - const data = (body as ApiEnvelope).data; + const data = + responseKind === "root" ? body : isRecord(body) ? body.data : null; if (data == null) throw new MissingVaultV2LiquidityApiDataError(url.toString()); + if (!validator(data)) + throw new InvalidVaultV2LiquidityApiResponseError(url.toString()); return data; } @@ -218,12 +440,26 @@ const apiSelector = (chainId: number, identifier: string) => /** @internal Fetches Vault V2 configuration from the Morpho REST API. */ export const fetchRestVaultV2 = (chainId: number, address: Address) => - requestApi(`/v0/vaults-v2/${apiSelector(chainId, address)}`); + requestApi( + `/v0/vaults-v2/${apiSelector(chainId, address)}`, + { + validator: (value): value is VaultV2Response => + responseValidators.vault(value) && + value.chain_id === chainId && + isAddressEqual(value.address, address), + }, + ); /** @internal Fetches Vault V2 accounting state from the Morpho REST API. */ export const fetchRestVaultV2State = (chainId: number, address: Address) => requestApi( `/v1/vaults-v2/${apiSelector(chainId, address)}/state`, + { + validator: (value): value is VaultV2StateResponse => + responseValidators.vaultState(value) && + value.chain_id === chainId && + isAddressEqual(value.address, address), + }, ); /** @internal Fetches Vault V2 adapter allocations and cap state from the Morpho REST API. */ @@ -233,6 +469,12 @@ export const fetchRestVaultV2Allocations = ( ) => requestApi( `/v0/vaults-v2/${apiSelector(chainId, address)}/allocations`, + { + validator: (value): value is VaultV2AllocationsResponse => + responseValidators.vaultAllocations(value) && + value.chain_id === chainId && + isAddressEqual(value.vault_address, address), + }, ); /** @internal Fetches Vault V2 adapter force-deallocation penalties from the Morpho REST API. */ @@ -242,18 +484,36 @@ export const fetchRestVaultV2WithdrawalOptions = ( ) => requestApi( `/v0/vaults-v2/${apiSelector(chainId, address)}/withdrawal-options`, + { + validator: (value): value is VaultV2WithdrawalOptionsResponse => + responseValidators.withdrawalOptions(value) && + value.chain_id === chainId && + isAddressEqual(value.vault_address, address), + }, ); /** @internal Fetches Morpho Blue market configuration from the REST API. */ export const fetchRestMarket = (chainId: number, marketId: MarketId) => requestApi( `/v0/blue/markets/${apiSelector(chainId, marketId)}`, + { + validator: (value): value is MarketResponse => + responseValidators.market(value) && + value.chain_id === chainId && + value.market_id.toLowerCase() === marketId.toLowerCase(), + }, ); /** @internal Fetches Morpho Blue market accounting state from the REST API. */ export const fetchRestMarketState = (chainId: number, marketId: MarketId) => requestApi( `/v0/blue/markets/${apiSelector(chainId, marketId)}/state`, + { + validator: (value): value is MarketStateResponse => + responseValidators.marketState(value) && + value.chain_id === chainId && + value.market_id.toLowerCase() === marketId.toLowerCase(), + }, ); /** @internal Fetches a Morpho Blue market position from the REST API. */ @@ -264,17 +524,36 @@ export const fetchRestMarketPosition = ({ }: MarketPositionParameters) => requestApi( `/v0/blue/markets/${apiSelector(chainId, marketId)}/users/${encodeURIComponent(user)}/position`, + { + validator: (value): value is MarketPositionResponse => + responseValidators.marketPosition(value) && + value.chain_id === chainId && + value.market_id.toLowerCase() === marketId.toLowerCase() && + isAddressEqual(value.user_address, user), + }, ); /** @internal Fetches a Morpho Blue oracle price from the REST API. */ export const fetchRestOracleState = (chainId: number, address: Address) => requestApi( `/v0/oracles/${apiSelector(chainId, address)}/state`, + { + validator: (value): value is OracleStateResponse => + responseValidators.oracleState(value) && + value.chain_id === chainId && + isAddressEqual(value.oracle_address, address), + }, ); /** @internal Fetches a Morpho Blue market's adaptive-curve IRM state from the REST API. */ export const fetchRestMarketIrm = (chainId: number, marketId: MarketId) => requestApi( `/consumer/chains/${chainId}/markets/${encodeURIComponent(marketId)}/irm`, - "root", + { + validator: (value): value is MarketIrmResponse => + responseValidators.marketIrm(value) && + value.chainId === chainId && + value.marketId.toLowerCase() === marketId.toLowerCase(), + responseKind: "root", + }, ); diff --git a/packages/liquidity-sdk-viem/src/errors.ts b/packages/liquidity-sdk-viem/src/errors.ts index 296c15456..f12e1be53 100644 --- a/packages/liquidity-sdk-viem/src/errors.ts +++ b/packages/liquidity-sdk-viem/src/errors.ts @@ -6,7 +6,7 @@ * import { VaultV2LiquidityApiError } from "@morpho-org/liquidity-sdk-viem"; * * const error = new VaultV2LiquidityApiError({ - * url: "https://api.morpho.org/v0/vaults-v2/1:0x1234", + * url: "https://api.morpho.org/v0/vaults-v2", * status: 503, * }); * console.error(error.status, error.url); @@ -49,7 +49,7 @@ export class VaultV2LiquidityApiError extends Error { * import { MissingVaultV2LiquidityApiDataError } from "@morpho-org/liquidity-sdk-viem"; * * const error = new MissingVaultV2LiquidityApiDataError( - * "market 0x1234 rateAtTarget", + * "adaptive-curve IRM rateAtTarget", * ); * console.error(error.resource); * ``` @@ -71,3 +71,74 @@ export class MissingVaultV2LiquidityApiDataError extends Error { this.resource = resource; } } + +/** + * Thrown when a successful Morpho API response has an invalid runtime shape. + * + * @example + * ```ts + * import { InvalidVaultV2LiquidityApiResponseError } from "@morpho-org/liquidity-sdk-viem"; + * + * const error = new InvalidVaultV2LiquidityApiResponseError( + * "https://api.morpho.org/v1/vaults-v2", + * ); + * console.error(error.url); + * ``` + */ +export class InvalidVaultV2LiquidityApiResponseError extends Error { + /** API endpoint that returned malformed JSON data. */ + public readonly url: string; + + /** + * @param url - API endpoint whose successful response failed validation. + */ + public constructor(url: string) { + super( + `Morpho API response from "${url}" is not valid Vault V2 liquidity data. Retry after the API indexer recovers.`, + ); + this.name = "InvalidVaultV2LiquidityApiResponseError"; + this.url = url; + } +} + +/** + * Thrown when REST resources required for one liquidity plan were indexed at + * different blocks. + * + * @example + * ```ts + * import { InconsistentVaultV2LiquiditySnapshotError } from "@morpho-org/liquidity-sdk-viem"; + * + * const error = new InconsistentVaultV2LiquiditySnapshotError({ + * resource: "market state", + * expectedBlock: 20_000_000n, + * actualBlock: 20_000_001n, + * }); + * console.error(error.resource, error.expectedBlock, error.actualBlock); + * ``` + */ +export class InconsistentVaultV2LiquiditySnapshotError extends Error { + /** REST resource whose indexed block differs. */ + public readonly resource: string; + /** Indexed block selected for the plan. */ + public readonly expectedBlock: bigint; + /** Indexed block reported by the inconsistent resource. */ + public readonly actualBlock: bigint; + + /** + * @param parameters - Resource name and conflicting indexed blocks. + */ + public constructor(parameters: { + readonly resource: string; + readonly expectedBlock: bigint; + readonly actualBlock: bigint; + }) { + super( + `Vault V2 liquidity snapshot requires indexed block "${parameters.expectedBlock}", but "${parameters.resource}" reports "${parameters.actualBlock}". Retry after the API indexer converges.`, + ); + this.name = "InconsistentVaultV2LiquiditySnapshotError"; + this.resource = parameters.resource; + this.expectedBlock = parameters.expectedBlock; + this.actualBlock = parameters.actualBlock; + } +} diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts index b94574364..e09c82970 100644 --- a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts @@ -1,5 +1,7 @@ import { + AdaptiveCurveIrmLib, getChainAddresses, + Market, MarketParams, MathLib, VaultV2MorphoMarketV1AdapterV2, @@ -16,6 +18,8 @@ import { mainnet } from "viem/chains"; import { beforeEach, describe, expect, test } from "vitest"; import { fetchRestVaultV2 } from "./api/rest.js"; import { + InconsistentVaultV2LiquiditySnapshotError, + InvalidVaultV2LiquidityApiResponseError, MissingVaultV2LiquidityApiDataError, VaultV2LiquidityApiError, } from "./errors.js"; @@ -100,7 +104,33 @@ const vaultConfigResponse = { }, }; -const setupApi = (vaultStatus = 200, includePenalty = true) => { +const defaultMarketState = { + lastAccrualTimestamp: BLOCK_TIMESTAMP, + totalSupplyAssets: 100n, + totalSupplyShares: 100_000_000n, + totalBorrowAssets: 95n, + totalBorrowShares: 95_000_000n, + fee: 0n, +}; + +const setupApi = ({ + vaultStatus = 200, + includePenalty = true, + marketStateBlock = BLOCK_NUMBER, + marketState = {}, + positionUser = ADAPTER, + positionSupplyShares = 0n, + rateAtTarget = 0n, +}: { + readonly vaultStatus?: number; + readonly includePenalty?: boolean; + readonly marketStateBlock?: bigint; + readonly marketState?: Partial; + readonly positionUser?: Address; + readonly positionSupplyShares?: bigint; + readonly rateAtTarget?: bigint; +} = {}) => { + const resolvedMarketState = { ...defaultMarketState, ...marketState }; const rest = nock(BLUE_API_BASE_URL); rest .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}`) @@ -182,13 +212,15 @@ const setupApi = (vaultStatus = 200, includePenalty = true) => { data: { chain_id: mainnet.id, market_id: marketParams.id, - last_indexed_block: BLOCK_NUMBER.toString(), - last_accrual_timestamp: Number(BLOCK_TIMESTAMP), - total_supply_assets: "100", - total_supply_shares: "100000000", - total_borrow_assets: "95", - total_borrow_shares: "95000000", - fee_wad: "0", + last_indexed_block: marketStateBlock.toString(), + last_accrual_timestamp: Number( + resolvedMarketState.lastAccrualTimestamp, + ), + total_supply_assets: resolvedMarketState.totalSupplyAssets.toString(), + total_supply_shares: resolvedMarketState.totalSupplyShares.toString(), + total_borrow_assets: resolvedMarketState.totalBorrowAssets.toString(), + total_borrow_shares: resolvedMarketState.totalBorrowShares.toString(), + fee_wad: resolvedMarketState.fee.toString(), }, }); @@ -200,10 +232,10 @@ const setupApi = (vaultStatus = 200, includePenalty = true) => { data: { chain_id: mainnet.id, market_id: marketParams.id, - user_address: ADAPTER, + user_address: positionUser, last_indexed_block: BLOCK_NUMBER.toString(), collateral_assets: "0", - supply_shares: "0", + supply_shares: positionSupplyShares.toString(), borrow_shares: "0", }, }); @@ -225,7 +257,7 @@ const setupApi = (vaultStatus = 200, includePenalty = true) => { targetUtilization: 0.9, utilization: 0.95, apyAtTarget: 0, - rateAtTarget: "0", + rateAtTarget: rateAtTarget.toString(), borrowToTarget: 0, }); @@ -331,6 +363,62 @@ describe.sequential("VaultV2LiquidityLoader", () => { api.done(); }); + test("behavior: accrues REST-projected market totals only after the indexed block", async () => { + const storedTimestamp = BLOCK_TIMESTAMP - 3_600n; + const positionSupplyShares = 500_000_000_000_000_000_000_000_000n; + // Mirrors the raw market tuple and stored IRM value returned by pinned RPC. + const rawRpcMarket = new Market({ + params: marketParams, + totalSupplyAssets: 1_000_000_000_000_000_000_000n, + totalSupplyShares: 1_000_000_000_000_000_000_000_000_000n, + totalBorrowAssets: 950_000_000_000_000_000_000n, + totalBorrowShares: 950_000_000_000_000_000_000_000_000n, + lastUpdate: storedTimestamp, + fee: 0n, + price: ORACLE_PRICE, + rateAtTarget: AdaptiveCurveIrmLib.INITIAL_RATE_AT_TARGET, + }); + const indexedMarket = rawRpcMarket.accrueInterest(BLOCK_TIMESTAMP); + expect(indexedMarket.totalBorrowAssets).toBeGreaterThan( + rawRpcMarket.totalBorrowAssets, + ); + + const api = setupApi({ + marketState: { + lastAccrualTimestamp: storedTimestamp, + totalSupplyAssets: indexedMarket.totalSupplyAssets, + totalSupplyShares: indexedMarket.totalSupplyShares, + totalBorrowAssets: indexedMarket.totalBorrowAssets, + totalBorrowShares: indexedMarket.totalBorrowShares, + fee: indexedMarket.fee, + }, + positionSupplyShares, + rateAtTarget: indexedMarket.rateAtTarget, + }); + const { client } = setupClient(); + const loader = new VaultV2LiquidityLoader(client, { + allocator: ALLOCATOR, + vaults: [VAULT], + deployless: false, + }); + + const result = await loader.fetch(marketParams.id); + const executionTimestamp = BLOCK_TIMESTAMP + 3_600n; + const expectedMarket = indexedMarket.accrueInterest(executionTimestamp); + const hydratedMarket = result.startState.getMarket(marketParams.id); + + expect(hydratedMarket.lastUpdate).toBe(BLOCK_TIMESTAMP); + expect(hydratedMarket.accrueInterest(executionTimestamp)).toStrictEqual( + expectedMarket, + ); + expect( + result.startState + .getAdapter(VAULT, ADAPTER) + .realAssets(executionTimestamp), + ).toBe(expectedMarket.toSupplyAssets(positionSupplyShares)); + api.done(); + }); + test("behavior: filters vaults above the maximum penalty", async () => { const api = setupApi(); const { client } = setupClient(); @@ -348,7 +436,7 @@ describe.sequential("VaultV2LiquidityLoader", () => { }); test("error: VaultV2LiquidityApiError", async () => { - setupApi(503); + setupApi({ vaultStatus: 503 }); const { client } = setupClient(); const loader = new VaultV2LiquidityLoader(client, { allocator: ALLOCATOR, @@ -374,7 +462,7 @@ describe.sequential("VaultV2LiquidityLoader", () => { }); test("error: MissingVaultV2LiquidityApiDataError", async () => { - setupApi(200, false); + setupApi({ includePenalty: false }); const { client } = setupClient(); const loader = new VaultV2LiquidityLoader(client, { allocator: ALLOCATOR, @@ -385,4 +473,43 @@ describe.sequential("VaultV2LiquidityLoader", () => { MissingVaultV2LiquidityApiDataError, ); }); + + test("error: InvalidVaultV2LiquidityApiResponseError", async () => { + const api = nock(BLUE_API_BASE_URL) + .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}`) + .reply(200, { data: { chain_id: mainnet.id, address: VAULT } }); + + await expect(fetchRestVaultV2(mainnet.id, VAULT)).rejects.toBeInstanceOf( + InvalidVaultV2LiquidityApiResponseError, + ); + api.done(); + }); + + test("error: InconsistentVaultV2LiquiditySnapshotError", async () => { + setupApi({ marketStateBlock: BLOCK_NUMBER + 1n }); + const { client } = setupClient(); + const loader = new VaultV2LiquidityLoader(client, { + allocator: ALLOCATOR, + vaults: [VAULT], + }); + + await expect(loader.fetch(marketParams.id)).rejects.toBeInstanceOf( + InconsistentVaultV2LiquiditySnapshotError, + ); + }); + + test("error: mismatched adapter-market position is rejected", async () => { + setupApi({ + positionUser: "0x0000000000000000000000000000000000000005", + }); + const { client } = setupClient(); + const loader = new VaultV2LiquidityLoader(client, { + allocator: ALLOCATOR, + vaults: [VAULT], + }); + + await expect(loader.fetch(marketParams.id)).rejects.toBeInstanceOf( + InvalidVaultV2LiquidityApiResponseError, + ); + }); }); diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts index 53ecaac1d..aa4867143 100644 --- a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts @@ -37,10 +37,32 @@ import { fetchRestVaultV2State, fetchRestVaultV2WithdrawalOptions, } from "./api/rest.js"; -import { MissingVaultV2LiquidityApiDataError } from "./errors.js"; +import { + InconsistentVaultV2LiquiditySnapshotError, + MissingVaultV2LiquidityApiDataError, +} from "./errors.js"; const REALLOCATION_SIMULATION_DELAY = 3_600n; +const assertSnapshotBlock = ({ + expectedBlock, + indexedBlock, + resource, +}: { + readonly expectedBlock: bigint; + readonly indexedBlock: string; + readonly resource: string; +}) => { + const actualBlock = BigInt(indexedBlock); + if (actualBlock !== expectedBlock) { + throw new InconsistentVaultV2LiquiditySnapshotError({ + resource, + expectedBlock, + actualBlock, + }); + } +}; + /** Represents the configuration for fetching and simulating Vault V2 shared liquidity. */ export interface VaultV2LiquidityParameters { /** Explicit BluePublicAllocator contract used for every generated reallocation. */ @@ -121,21 +143,46 @@ export class VaultV2LiquidityLoader { async (marketIds) => { const { client: loaderClient, parameters: loaderParameters } = this; const chainId = loaderClient.chain.id; - const [block, restVaults] = await Promise.all([ - getBlock(loaderClient), - Promise.all( - loaderParameters.vaults.map(async (vault) => { - const [config, state, allocations] = await Promise.all([ - fetchRestVaultV2(chainId, vault), - fetchRestVaultV2State(chainId, vault), - fetchRestVaultV2Allocations(chainId, vault), - ]); - return { config, state, allocations }; - }), - ), - ]); + const restVaults = await Promise.all( + loaderParameters.vaults.map(async (vault) => { + const [config, state, allocations] = await Promise.all([ + fetchRestVaultV2(chainId, vault), + fetchRestVaultV2State(chainId, vault), + fetchRestVaultV2Allocations(chainId, vault), + ]); + return { config, state, allocations }; + }), + ); + const prefetchedMarketState = + restVaults.length === 0 + ? await fetchRestMarketState(chainId, marketIds[0]!) + : undefined; + const indexedBlockNumber = BigInt( + restVaults[0]?.config.last_indexed_block ?? + prefetchedMarketState!.last_indexed_block, + ); + for (const { config, state, allocations } of restVaults) { + assertSnapshotBlock({ + expectedBlock: indexedBlockNumber, + indexedBlock: config.last_indexed_block, + resource: `vault ${config.address} config`, + }); + assertSnapshotBlock({ + expectedBlock: indexedBlockNumber, + indexedBlock: state.last_indexed_block, + resource: `vault ${config.address} state`, + }); + assertSnapshotBlock({ + expectedBlock: indexedBlockNumber, + indexedBlock: allocations.last_indexed_block, + resource: `vault ${config.address} allocations`, + }); + } + const block = await getBlock(loaderClient, { + blockNumber: indexedBlockNumber, + }); const fetchParameters = { - blockNumber: block.number, + blockNumber: indexedBlockNumber, deployless: loaderParameters.deployless, } as const; @@ -199,7 +246,10 @@ export class VaultV2LiquidityLoader { allRestMarketIds.map(async (marketId) => { const config = await fetchRestMarket(chainId, marketId); const [state, oracleState, marketIrm] = await Promise.all([ - fetchRestMarketState(chainId, marketId), + prefetchedMarketState != null && + marketId.toLowerCase() === marketIds[0]!.toLowerCase() + ? prefetchedMarketState + : fetchRestMarketState(chainId, marketId), isAddressEqual(config.oracle_address, zeroAddress) ? undefined : fetchRestOracleState(chainId, config.oracle_address), @@ -218,6 +268,7 @@ export class VaultV2LiquidityLoader { return { config, state, + oracleState, price: oracleState?.price == null ? undefined @@ -250,6 +301,28 @@ export class VaultV2LiquidityLoader { ), ]); + for (const { config, state, oracleState } of restMarkets) { + assertSnapshotBlock({ + expectedBlock: indexedBlockNumber, + indexedBlock: state.last_indexed_block, + resource: `market ${config.market_id} state`, + }); + if (oracleState != null) { + assertSnapshotBlock({ + expectedBlock: indexedBlockNumber, + indexedBlock: oracleState.last_indexed_block, + resource: `oracle ${oracleState.oracle_address} state`, + }); + } + } + for (const position of marketPositions) { + assertSnapshotBlock({ + expectedBlock: indexedBlockNumber, + indexedBlock: position.last_indexed_block, + resource: `market ${position.market_id} position ${position.user_address}`, + }); + } + const forceDeallocatePenalties = new Map( withdrawalOptions.flatMap(({ vaultAddress, data }) => data.adapter_penalties.map( @@ -285,7 +358,9 @@ export class VaultV2LiquidityLoader { totalSupplyShares: BigInt(state.total_supply_shares), totalBorrowAssets: BigInt(state.total_borrow_assets), totalBorrowShares: BigInt(state.total_borrow_shares), - lastUpdate: BigInt(state.last_accrual_timestamp), + // REST totals and IRM state are projected to the indexed block; + // last_accrual_timestamp remains the older onchain storage value. + lastUpdate: block.timestamp, fee: BigInt(state.fee_wad), price, rateAtTarget, @@ -326,12 +401,17 @@ export class VaultV2LiquidityLoader { marketIds: adapterMarkets.map(({ id }) => id), adaptiveCurveIrm, supplyShares: fromEntries( - adapterMarkets.map((market) => [ - market.id, - positionSupplyShares.get( + adapterMarkets.map((market) => { + const supplyShares = positionSupplyShares.get( `${allocation.adapter_address.toLowerCase()}:${market.id.toLowerCase()}`, - ) ?? 0n, - ]), + ); + if (supplyShares == null) { + throw new MissingVaultV2LiquidityApiDataError( + `adapter ${allocation.adapter_address} market ${market.id} position`, + ); + } + return [market.id, supplyShares] as const; + }), ), }, adapterMarkets, @@ -453,7 +533,9 @@ export class VaultV2LiquidityLoader { * @param marketId - Target market id to plan reallocations for. * @returns The start state, simulated end state, action-ready reallocations, and target utilization. * @throws {VaultV2LiquidityApiError} when a REST API request fails. + * @throws {InvalidVaultV2LiquidityApiResponseError} when a successful REST response is malformed. * @throws {MissingVaultV2LiquidityApiDataError} when indexed REST data is incomplete. + * @throws {InconsistentVaultV2LiquiditySnapshotError} when REST resources report different indexed blocks. * @throws {viem.BaseError} when a BluePublicAllocator read or RPC compatibility fallback fails. * @example * ```ts diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index f351e5f6d..ea756f972 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -20,7 +20,7 @@ Instead of exposing the user directly to target contracts (ERC-4626 vault, Morph Bundler3 also calls allocator contracts directly for shared liquidity: `reallocateTo` on Public Allocator V1 and `reallocate` or `allocateFromIdle` on Blue Public Allocator. -The **spender** of every approval / permit / permit2 is therefore **always** `generalAdapter1`, never the vault or Morpho directly. See [src/actions/requirements/getRequirements.ts](src/actions/requirements/getRequirements.ts) and the "Requirements System" section of [ARCHITECTURE.md](ARCHITECTURE.md#requirements-system). +The spender of every **user-supplied** approval / permit / permit2 is `generalAdapter1`, never the vault or Morpho directly. Blue Public Allocator penalties add a separate internal allowance: Bundler3 approves the allocator for each exact penalty amount immediately before the non-skippable allocator call. See [`getGeneralAdapterRequirements`](src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.ts) and the "Requirements System" section of [ARCHITECTURE.md](ARCHITECTURE.md#requirements-system). ## Composability & modularity @@ -81,9 +81,9 @@ GeneralAdapter1, approves each exact per-call amount from Bundler3, and lets the it directly to the vault. The entity's `getRequirements()` returns the corresponding classic loan-token approval when a V2 penalty is non-zero. -### 5. A single approval surface +### 5. A single user approval surface -Whether it's a V1 deposit, a V2 deposit, a `supplyCollateral`, a `repay`, or a `supplyCollateralBorrow`: the spender is **always** `generalAdapter1`. A user who has already approved GA1 for a given token transparently reuses that approval. The approval / permit / permit2 decision is centralized in [`getRequirements`](src/actions/requirements/getRequirements.ts). +Whether it's a V1 deposit, a V2 deposit, a `supplyCollateral`, a `repay`, or a `supplyCollateralBorrow`, the spender presented to the user is **always** `generalAdapter1`. A user who has already approved GA1 for a given token transparently reuses that approval. The approval / permit / permit2 decision is centralized in [`getGeneralAdapterRequirements`](src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.ts). For Blue Public Allocator penalties, Bundler3 separately grants the allocator an exact per-call allowance; that approval and allocator call cannot be made independently skippable. ## Dangers & limits diff --git a/packages/morpho-sdk/contracts/fixtures/BluePublicAllocatorWriteFixture.sol b/packages/morpho-sdk/contracts/fixtures/BluePublicAllocatorWriteFixture.sol new file mode 100644 index 000000000..87bb3244c --- /dev/null +++ b/packages/morpho-sdk/contracts/fixtures/BluePublicAllocatorWriteFixture.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity ^0.8.0; + +struct MarketParams { + address loanToken; + address collateralToken; + address oracle; + address irm; + uint256 lltv; +} + +struct VaultData { + bool canPullFromIdle; + uint64 penalty; +} + +interface IERC20 { + function transferFrom(address from, address to, uint256 assets) external returns (bool); +} + +interface IVaultV2 { + function isAllocator(address account) external view returns (bool); + function allocation(bytes32 id) external view returns (uint256); + function allocate(address adapter, bytes memory data, uint256 assets) external; + function deallocate(address adapter, bytes memory data, uint256 assets) external; +} + +/// @dev Test-only fixture pinned to the BluePublicAllocator write ordering and checks. +contract BluePublicAllocatorWriteFixture { + uint256 internal constant WAD = 1e18; + + mapping(address vault => mapping(bytes32 id => uint256)) public absoluteCap; + mapping(address vault => mapping(bytes32 id => bool)) public canPullFromMarket; + mapping(address vault => mapping(address adapter => bool)) public isActiveAdapter; + mapping(address vault => VaultData) public vaultData; + + function setIsActiveAdapter(address vault, address adapter, bool value) external { + require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); + isActiveAdapter[vault][adapter] = value; + } + + function setAbsoluteCap(address vault, address adapter, MarketParams calldata marketParams, uint256 value) + external + { + require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); + absoluteCap[vault][vaultBlueId(adapter, marketParams)] = value; + } + + function setCanPullFromMarket(address vault, address adapter, MarketParams calldata marketParams, bool value) + external + { + require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); + canPullFromMarket[vault][vaultBlueId(adapter, marketParams)] = value; + } + + function setCanPullFromIdle(address vault, bool value) external { + require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); + vaultData[vault].canPullFromIdle = value; + } + + function setPenalty(address vault, uint64 value) external { + require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); + require(value <= WAD, "penalty too high"); + vaultData[vault].penalty = value; + } + + function reallocate( + address vault, + address deallocateAdapter, + MarketParams calldata deallocateMarketParams, + address allocateAdapter, + MarketParams calldata allocateMarketParams, + uint128 assets, + uint64 penalty + ) external { + require(vaultData[vault].penalty == penalty, "incorrect penalty"); + transferPenalty(allocateMarketParams.loanToken, msg.sender, vault, assets, penalty); + require(isActiveAdapter[vault][deallocateAdapter], "inactive source adapter"); + require(isActiveAdapter[vault][allocateAdapter], "inactive target adapter"); + + bytes32 deallocateId = vaultBlueId(deallocateAdapter, deallocateMarketParams); + require(canPullFromMarket[vault][deallocateId], "cannot pull from market"); + bytes32 allocateId = vaultBlueId(allocateAdapter, allocateMarketParams); + require(absoluteCap[vault][allocateId] > 0, "zero absolute cap"); + + IVaultV2(vault).deallocate(deallocateAdapter, abi.encode(deallocateMarketParams), assets); + IVaultV2(vault).allocate(allocateAdapter, abi.encode(allocateMarketParams), assets); + + require(IVaultV2(vault).allocation(allocateId) <= absoluteCap[vault][allocateId], "absolute cap exceeded"); + } + + function allocateFromIdle( + address vault, + address adapter, + MarketParams calldata marketParams, + uint128 assets, + uint64 penalty + ) external { + require(vaultData[vault].penalty == penalty, "incorrect penalty"); + transferPenalty(marketParams.loanToken, msg.sender, vault, assets, penalty); + require(isActiveAdapter[vault][adapter], "inactive adapter"); + require(vaultData[vault].canPullFromIdle, "cannot pull from idle"); + + bytes32 allocateId = vaultBlueId(adapter, marketParams); + require(absoluteCap[vault][allocateId] > 0, "zero absolute cap"); + IVaultV2(vault).allocate(adapter, abi.encode(marketParams), assets); + require(IVaultV2(vault).allocation(allocateId) <= absoluteCap[vault][allocateId], "absolute cap exceeded"); + } + + function transferPenalty(address token, address from, address vault, uint256 assets, uint256 penalty) internal { + uint256 penaltyAssets = assets * penalty == 0 ? 0 : (assets * penalty - 1) / WAD + 1; + if (penaltyAssets == 0) return; + + (bool success, bytes memory returnData) = + token.call(abi.encodeCall(IERC20.transferFrom, (from, vault, penaltyAssets))); + require(success && (returnData.length == 0 || abi.decode(returnData, (bool))), "transfer failed"); + } + + function vaultBlueId(address adapter, MarketParams calldata marketParams) internal pure returns (bytes32) { + return keccak256(abi.encode("this/marketParams", adapter, marketParams)); + } +} diff --git a/packages/morpho-sdk/package.json b/packages/morpho-sdk/package.json index c6fe79c2d..07869589a 100644 --- a/packages/morpho-sdk/package.json +++ b/packages/morpho-sdk/package.json @@ -92,6 +92,7 @@ "build": "tsc --noEmit && $npm_execpath build:cjs && $npm_execpath build:esm", "build:cjs": "tsc --build tsconfig.build.cjs.json && echo '{\"type\":\"commonjs\"}' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.build.esm.json && echo '{\"type\":\"module\"}' > lib/esm/package.json", + "compile": "node ../../scripts/compile-solidity.js morpho-sdk", "test": "vitest --root ../.. --project morpho-sdk" }, "dependencies": { diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index 633fb48a8..d186b511b 100644 --- a/packages/morpho-sdk/src/actions/AGENTS.md +++ b/packages/morpho-sdk/src/actions/AGENTS.md @@ -24,7 +24,7 @@ Only valid for assets/collateral configured as wNative. When `nativeAmount > 0`: ## Shared liquidity / reallocations (canonical statement) -`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultV1BlueReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. Tagged `VaultV2BlueReallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call passes the vault's configured WAD-scaled `penalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. High-level builders pull the aggregate V2 penalty in the target loan token through GeneralAdapter1, then each low-level allocator action approves and spends its independently rounded `ceil(assets × penalty / WAD)` amount from Bundler3. Only V1 fees contribute to `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level or BluePublicAllocator-source discriminators, penalties above WAD, and inconsistent penalties for the same allocator-vault pair. +`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultV1BlueReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. Tagged `VaultV2BlueReallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call passes the vault's configured WAD-scaled `penalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. High-level builders pull the aggregate V2 penalty in the target loan token through GeneralAdapter1, then each low-level allocator action approves and spends its independently rounded `ceil(assets × penalty / WAD)` amount from Bundler3. Only V1 fees contribute to `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level variants; malformed allocator, vault, and adapter addresses; absent, incomplete, or unknown BluePublicAllocator sources; penalties above WAD; and inconsistent penalties for the same allocator-vault pair. ## Discriminated unions diff --git a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts index f606a7c7f..7a0186399 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -26,7 +26,7 @@ const targetMarket = new MarketParams({ collateralToken: "0x0000000000000000000000000000000000000022", oracle: "0x0000000000000000000000000000000000000023", irm: "0x0000000000000000000000000000000000000024", - lltv: 860_000000000000000000n, + lltv: 860_000_000_000_000_000n, }); const sourceMarket = new MarketParams({ diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index 23cc93bc3..aaa7d20f4 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -68,7 +68,8 @@ export interface BlueBorrowParams { * is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {NegativeInputError} when `minSharePrice < 0n`, a V1 fee, or a V2 penalty is negative. * @throws {EmptyReallocationWithdrawalsError} from `buildReallocationActions` when any diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 6dd36735f..cd8cb7f7d 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -10,21 +10,24 @@ import type { BlueReallocation } from "../../types/index.js"; * PublicAllocator V1 entries preserve their `reallocateTo` ABI and validation. Each * BluePublicAllocator entry maps 1:1 to either `reallocate` for a market source or * `allocateFromIdle` for an idle source. The enclosing Blue action supplies the target market - * parameters. V2 penalties are pulled once in the target loan token to - * Bundler3; each allocator action then approves and spends its independently - * rounded share. + * parameters. V2 penalties are moved once in the target loan token to Bundler3; each allocator + * action then approves and spends its independently rounded share. * * @param params - Reallocation encoding inputs. * @param params.chainId - Chain where the bundle will execute. * @param params.reallocations - PublicAllocator V1 and BluePublicAllocator reallocations in execution order. * @param params.targetMarketParams - Target market params derived from the enclosing Blue action. + * @param params.penaltyFundingSource - Account that already holds the aggregate V2 penalty. Uses + * the transaction initiator by default; same-token collateral funding can pre-fund + * `GeneralAdapter1` instead. * @returns Encoded actions, the native V1 fee, and the V2 loan-token penalty total. * @throws {NegativeInputError} when a PublicAllocator V1 fee or BluePublicAllocator penalty is negative. * @throws {EmptyReallocationWithdrawalsError} when a PublicAllocator V1 reallocation has no withdrawals. * @throws {NonPositiveInputError} when a PublicAllocator V1 withdrawal or BluePublicAllocator asset amount is non-positive. * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a BluePublicAllocator identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when PublicAllocator V1 withdrawals are not strictly market-id sorted. @@ -34,10 +37,12 @@ export const buildReallocationActions = ({ chainId, reallocations, targetMarketParams, + penaltyFundingSource = "initiator", }: { readonly chainId: number; readonly reallocations: readonly BlueReallocation[]; readonly targetMarketParams: MarketParams; + readonly penaltyFundingSource?: "initiator" | "generalAdapter1"; }): { readonly actions: Action[]; readonly fee: bigint; @@ -52,12 +57,30 @@ export const buildReallocationActions = ({ if (penaltyAssets > 0n) { const { - bundler3: { bundler3 }, + bundler3: { bundler3, generalAdapter1 }, } = getChainAddresses(chainId); - actions.push({ - type: "erc20TransferFrom", - args: [targetMarketParams.loanToken, penaltyAssets, bundler3, false], - }); + actions.push( + penaltyFundingSource === "generalAdapter1" + ? { + type: "erc20Transfer", + args: [ + targetMarketParams.loanToken, + bundler3, + penaltyAssets, + generalAdapter1, + false, + ], + } + : { + type: "erc20TransferFrom", + args: [ + targetMarketParams.loanToken, + penaltyAssets, + bundler3, + false, + ], + }, + ); } for (const reallocation of reallocations) { diff --git a/packages/morpho-sdk/src/actions/blue/refinance.test.ts b/packages/morpho-sdk/src/actions/blue/refinance.test.ts index 779c09c44..fb54f9b50 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.test.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.test.ts @@ -1,8 +1,21 @@ import { getChainAddresses, MarketParams } from "@morpho-org/blue-sdk"; -import { type Address, maxUint256, parseUnits, toFunctionSelector } from "viem"; +import { + type Address, + decodeFunctionData, + erc20Abi, + maxUint256, + parseUnits, + toFunctionSelector, +} from "viem"; import { mainnet } from "viem/chains"; import { describe, expect, test } from "vitest"; import { + bundler3Abi, + generalAdapter1Abi, + vaultV2BluePublicAllocatorAbi, +} from "../../abis.js"; +import { + type BlueReallocation, NegativeInputError, NonPositiveInputError, ReallocationWithdrawalOnTargetMarketError, @@ -381,6 +394,12 @@ describe("blueRefinance", () => { }); const VAULT: Address = "0xBEEf5aFE88eF73337e5070aB2855d37dBF5493A4"; const REALLOC_FEE = parseUnits("0.01", 18); + const V2_ALLOCATOR: Address = "0x0000000000000000000000000000000000000011"; + const V2_VAULT: Address = "0x0000000000000000000000000000000000000012"; + const SOURCE_ADAPTER: Address = + "0x0000000000000000000000000000000000000013"; + const TARGET_ADAPTER: Address = + "0x0000000000000000000000000000000000000014"; const makeReallocations = (): readonly VaultReallocation[] => [ { @@ -462,6 +481,92 @@ describe("blueRefinance", () => { expect(reallocVaultIdx).toBeLessThan(supplyIdx); }); + test("behavior: V2 market and idle reallocations fund penalties before the target supply", () => { + const { + bundler3: { bundler3 }, + } = getChainAddresses(mainnet.id); + const targetReallocations: readonly BlueReallocation[] = [ + { + type: "bluePublicAllocator", + allocator: V2_ALLOCATOR, + vault: V2_VAULT, + from: { + type: "market", + adapter: SOURCE_ADAPTER, + marketParams: reallocSource, + }, + to: { adapter: TARGET_ADAPTER }, + assets: 10n, + penalty: 500_000_000_000_000_000n, + }, + { + type: "bluePublicAllocator", + allocator: V2_ALLOCATOR, + vault: V2_VAULT, + from: { type: "idle" }, + to: { adapter: TARGET_ADAPTER }, + assets: 6n, + penalty: 500_000_000_000_000_000n, + }, + ]; + + const tx = blueRefinance({ + source: { chainId: mainnet.id, marketParams: source }, + target: { marketParams: target }, + args: { + ...baseArgs, + borrowAssets: parseUnits("1000", 6), + targetReallocations, + }, + metadata: { origin: "a1b2c3d4" }, + }); + + expect(tx.value).toBe(0n); + expect(tx.action.args.reallocationFee).toBe(0n); + expect(tx.action.args.reallocationPenaltyAssets).toBe(8n); + expect(tx.data).toContain("a1b2c3d4"); + + const bundle = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); + const calls = bundle.args[0] ?? []; + expect(calls).toHaveLength(6); + expect( + decodeFunctionData({ abi: generalAdapter1Abi, data: calls[0]!.data }), + ).toMatchObject({ + functionName: "erc20TransferFrom", + args: [target.loanToken, bundler3, 8n], + }); + expect( + decodeFunctionData({ abi: erc20Abi, data: calls[1]!.data }), + ).toMatchObject({ + functionName: "approve", + args: [V2_ALLOCATOR, 5n], + }); + expect( + decodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + data: calls[2]!.data, + }).functionName, + ).toBe("reallocate"); + expect( + decodeFunctionData({ abi: erc20Abi, data: calls[3]!.data }), + ).toMatchObject({ + functionName: "approve", + args: [V2_ALLOCATOR, 3n], + }); + expect( + decodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + data: calls[4]!.data, + }).functionName, + ).toBe("allocateFromIdle"); + expect( + decodeFunctionData({ + abi: generalAdapter1Abi, + data: calls[5]!.data, + }).functionName, + ).toBe("morphoSupplyCollateral"); + }); + test("behavior: collat-only refinance accepts reallocations", () => { const tx = blueRefinance({ source: { chainId: mainnet.id, marketParams: source }, diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index 224812e37..962e6b316 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -114,7 +114,8 @@ export interface BlueRefinanceParams { * `maxRepaySharePrice`, or any reallocation withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {NegativeInputError} when `borrowAssets`, `borrowShares`, `minBorrowSharePrice`, * `maxRepaySharePrice`, a V1 fee, or a V2 penalty is negative. diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts index abe40676e..d2b311aec 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts @@ -1,5 +1,9 @@ -import { addressesRegistry, MarketParams } from "@morpho-org/blue-sdk"; -import { parseUnits } from "viem"; +import { + addressesRegistry, + getChainAddresses, + MarketParams, +} from "@morpho-org/blue-sdk"; +import { decodeFunctionData, type Hex, parseUnits } from "viem"; import { mainnet } from "viem/chains"; import { afterEach, describe, expect, vi } from "vitest"; import { @@ -8,13 +12,17 @@ import { WethUsdsBlue, } from "../../../test/fixtures/blue.js"; import { SteakhouseUsdcVaultV1 } from "../../../test/fixtures/vaultV1.js"; +import { makePermit } from "../../../test/helpers/permit.js"; import { test } from "../../../test/setup.js"; +import { bundler3Abi, generalAdapter1Abi } from "../../abis.js"; import { + type BlueReallocation, isRequirementApproval, isRequirementSignature, NativeAmountOnNonWNativeAssetError, NegativeInputError, NonPositiveInputError, + type PermitRequirementSignature, type VaultReallocation, } from "../../types/index.js"; import { getGeneralAdapterRequirements } from "../requirements/index.js"; @@ -208,6 +216,99 @@ describe("blueSupplyCollateralBorrow unit tests", () => { expect(tx.action.type).toBe("blueSupplyCollateralBorrow"); }); + test("behavior: shared-token permits fund collateral and V2 penalty with one pull", async ({ + client, + }) => { + const { + bundler3: { bundler3, generalAdapter1 }, + } = getChainAddresses(mainnet.id); + const sharedTokenMarket = new MarketParams({ + ...WethUsdsBlue, + loanToken: WethUsdsBlue.collateralToken, + }); + const reallocations: readonly BlueReallocation[] = [ + { + type: "bluePublicAllocator", + allocator: WethUsdsBlue.irm, + vault: WethUsdsBlue.oracle, + from: { type: "idle" }, + to: { adapter: WethUsdsBlue.collateralToken }, + assets: 10n, + penalty: 500_000_000_000_000_000n, + }, + ]; + const combinedAmount = 105n; + const signature = `0x${"11".repeat(64)}1b` as Hex; + const requirementSignatures = [ + makePermit({ + owner: client.account.address, + asset: sharedTokenMarket.loanToken, + amount: combinedAmount, + }), + { + args: { + owner: client.account.address, + asset: sharedTokenMarket.loanToken, + amount: combinedAmount, + nonce: 0n, + deadline: 1_900_000_000n, + expiration: 1_900_000_000n, + signature, + }, + action: { + type: "permit2", + args: { + spender: generalAdapter1, + amount: combinedAmount, + deadline: 1_900_000_000n, + expiration: 1_900_000_000n, + }, + }, + }, + ] satisfies readonly PermitRequirementSignature[]; + + for (const requirementSignature of requirementSignatures) { + const tx = blueSupplyCollateralBorrow({ + market: { chainId: mainnet.id, marketParams: sharedTokenMarket }, + args: { + amount: 100n, + borrowAmount: 1n, + onBehalf: client.account.address, + receiver: client.account.address, + minSharePrice: 0n, + requirementSignature, + reallocations, + }, + }); + + expect(tx.action.args.reallocationPenaltyAssets).toBe(5n); + const bundle = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); + const calls = bundle.args[0] ?? []; + expect(calls).toHaveLength(7); + expect( + decodeFunctionData({ + abi: generalAdapter1Abi, + data: calls[1]!.data, + }), + ).toMatchObject({ + functionName: + requirementSignature.action.type === "permit2" + ? "permit2TransferFrom" + : "erc20TransferFrom", + args: [sharedTokenMarket.loanToken, generalAdapter1, combinedAmount], + }); + expect( + decodeFunctionData({ + abi: generalAdapter1Abi, + data: calls[3]!.data, + }), + ).toMatchObject({ + functionName: "erc20Transfer", + args: [sharedTokenMarket.loanToken, bundler3, 5n], + }); + } + }); + test("should throw NegativeInputError when amount is negative", async ({ client, }) => { diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 89f2242ed..506ac6aea 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -1,6 +1,6 @@ import type { MarketParams } from "@morpho-org/blue-sdk"; import { deepFreeze } from "@morpho-org/morpho-ts"; -import type { Address } from "viem"; +import { type Address, isAddressEqual } from "viem"; import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; import { @@ -50,12 +50,13 @@ export interface BlueSupplyCollateralBorrowParams { /** * Prepares an atomic supply-collateral-and-borrow transaction for a Morpho Blue market. * - * Routed through bundler3: collateral transfer → `morphoSupplyCollateral` → optional Public + * Routed through bundler3: collateral funding → `morphoSupplyCollateral` → optional Public * Allocator calls → `morphoBorrow`. V1 entries encode `reallocateTo`; V2 market and idle entries * encode `reallocate` and `allocateFromIdle`. When `nativeAmount > 0`, native ETH is wrapped via * `GeneralAdapter1.wrapNative()` before the supply leg. V1 fees add to - * `tx.value`; V2 penalties are paid in the target loan token and donated to - * the vaults. + * `tx.value`; V2 penalties are paid in the target loan token and donated to the vaults. When the + * collateral and loan tokens match, one combined pull funds both collateral and penalties through + * `GeneralAdapter1`. * * Prerequisite: `GeneralAdapter1` must be authorized on Morpho to borrow on behalf of the user. * Use `getRequirements()` on the entity to check and obtain the authorization transaction. @@ -72,7 +73,7 @@ export interface BlueSupplyCollateralBorrowParams { * @param params.args.receiver - Address that receives the borrowed assets. * @param params.args.minSharePrice - Minimum borrow share price (in ray). Slippage protection. * @param params.args.requirementSignature - Optional pre-signed permit/permit2 approval for the - * collateral transfer. + * collateral funding. When collateral and loan tokens match, its amount includes V2 penalties. * @param params.args.nativeAmount - Optional amount of native token to wrap into wNative for the * collateral supply. Requires the collateral token to be the chain's wNative. * @param params.args.reallocations - Optional Public Allocator V1 or V2 reallocations to execute @@ -88,7 +89,8 @@ export interface BlueSupplyCollateralBorrowParams { * zero, or any reallocation withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {ChainWNativeMissingError} when `nativeAmount > 0n` but the chain has no configured wNative. * @throws {NativeAmountOnNonWNativeAssetError} when `nativeAmount > 0n` but the collateral @@ -96,7 +98,7 @@ export interface BlueSupplyCollateralBorrowParams { * @throws {DepositAssetMismatchError} from `getTokenRequirementActions` when `requirementSignature` * is provided and the signed asset differs from `marketParams.collateralToken`. * @throws {DepositAmountMismatchError} from `getTokenRequirementActions` when `requirementSignature` - * is provided and the signed amount differs from `args.amount`. + * is provided and the signed amount differs from the total ERC-20 funding amount. * @throws {Permit2ExpirationMissingError} from `getTokenRequirementActions` when a Permit2 requirement * signature is missing its expiration. * @throws {EmptyReallocationWithdrawalsError} from `buildReallocationActions` when any @@ -161,6 +163,24 @@ export const blueSupplyCollateralBorrow = ({ throw new NonPositiveInputError("totalCollateral", totalCollateral); } + const usesSharedFundingToken = isAddressEqual( + marketParams.collateralToken, + marketParams.loanToken, + ); + const reallocationResult = + reallocations && reallocations.length > 0 + ? buildReallocationActions({ + chainId, + reallocations, + targetMarketParams: marketParams, + penaltyFundingSource: usesSharedFundingToken + ? "generalAdapter1" + : "initiator", + }) + : { actions: [], fee: 0n, penaltyAssets: 0n }; + const erc20FundingAmount = + amount + (usesSharedFundingToken ? reallocationResult.penaltyAssets : 0n); + const actions: Action[] = []; if (authorizationSignature) { @@ -171,7 +191,7 @@ export const blueSupplyCollateralBorrow = ({ ...buildAssetFundingActions({ chainId, asset: marketParams.collateralToken, - erc20Amount: amount, + erc20Amount: erc20FundingAmount, nativeAmount: nativeAmount ?? 0n, requirementSignature, }), @@ -181,20 +201,7 @@ export const blueSupplyCollateralBorrow = ({ type: "morphoSupplyCollateral", args: [marketParams, totalCollateral, onBehalf, [], false], }); - - let reallocationFee = 0n; - let reallocationPenaltyAssets = 0n; - - if (reallocations && reallocations.length > 0) { - const result = buildReallocationActions({ - chainId, - reallocations, - targetMarketParams: marketParams, - }); - actions.push(...result.actions); - reallocationFee = result.fee; - reallocationPenaltyAssets = result.penaltyAssets; - } + actions.push(...reallocationResult.actions); actions.push({ type: "morphoBorrow", @@ -219,8 +226,8 @@ export const blueSupplyCollateralBorrow = ({ onBehalf, receiver, nativeAmount, - reallocationFee, - reallocationPenaltyAssets, + reallocationFee: reallocationResult.fee, + reallocationPenaltyAssets: reallocationResult.penaltyAssets, }, }, }); diff --git a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts new file mode 100644 index 000000000..8a2c469b4 --- /dev/null +++ b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts @@ -0,0 +1,365 @@ +import { + getChainAddresses, + MarketParams, + MathLib, + marketParamsAbi, +} from "@morpho-org/blue-sdk"; +import { + blueAbi, + readContractRestructured, + vaultV2Abi, +} from "@morpho-org/blue-sdk-viem"; +import type { AnvilTestClient } from "@morpho-org/test"; +import { createViemTest } from "@morpho-org/test/vitest"; +import { + encodeAbiParameters, + encodeFunctionData, + erc20Abi, + maxUint128, + parseUnits, +} from "viem"; +import { base } from "viem/chains"; +import { assert, describe, expect } from "vitest"; +import { + abi as allocatorAbi, + code as allocatorCode, +} from "../../../test/fixtures/BluePublicAllocatorWriteFixture.js"; +import { supplyCollateral } from "../../../test/helpers/blue.js"; +import { + deployMorphoMarketV1AdapterV2, + deployVaultV2, + submitAndAcceptVaultV2Call, +} from "../../../test/helpers/vaultV2.js"; +import { + isRequirementApproval, + isRequirementBlueAuthorization, + morphoViemExtension, +} from "../../index.js"; +import type { VaultV2BlueReallocation } from "../../types/index.js"; + +const test = createViemTest(base, { + forkUrl: process.env.BASE_RPC_URL, + forkBlockNumber: 41_290_768n, + stepsTracing: false, +}); + +const sourceMarket = new MarketParams({ + loanToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + collateralToken: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + oracle: "0x663BECd10daE6C4A3Dcd89F1d76c1174199639B9", + irm: "0x46415998764C29aB2a25CbeA6254146D50D22687", + lltv: 860_000_000_000_000_000n, +}); + +const targetMarket = new MarketParams({ + loanToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + collateralToken: "0x4200000000000000000000000000000000000006", + oracle: "0xFEa2D58cEfCb9fcb597723c6bAE66fFE4193aFE4", + irm: "0x46415998764C29aB2a25CbeA6254146D50D22687", + lltv: 860_000_000_000_000_000n, +}); + +describe("Blue actions with Vault V2 reallocations", () => { + test("executes market and idle reallocations through live Base contracts", async ({ + client, + }) => { + const anvilClient = client as AnvilTestClient; + const { morpho, bundler3 } = getChainAddresses(base.id); + const sourceAssets = parseUnits("20", 6); + const idleAssets = parseUnits("10", 6); + const sourceDeposit = parseUnits("100", 6); + const initialIdleAssets = parseUnits("20", 6); + const penalty = parseUnits("0.01", 18); + const borrowAmount = parseUnits("1", 6); + const collateralAmount = parseUnits("1", 18); + const totalPenaltyAssets = + MathLib.wMulUp(sourceAssets, penalty) + + MathLib.wMulUp(idleAssets, penalty); + + for (const marketParams of [sourceMarket, targetMarket]) { + const marketState = await readContractRestructured(client, { + address: morpho, + abi: blueAbi, + functionName: "market", + args: [marketParams.id], + }); + if (marketState.lastUpdate === 0n) { + await client.writeContract({ + address: morpho, + abi: blueAbi, + functionName: "createMarket", + args: [marketParams], + }); + } + } + + const vault = await deployVaultV2(anvilClient, targetMarket.loanToken); + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "setIsAllocator", + args: [client.account.address, true], + }), + }); + + const sourceAdapter = await deployMorphoMarketV1AdapterV2( + anvilClient, + vault, + ); + const targetAdapter = sourceAdapter; + + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "addAdapter", + args: [sourceAdapter], + }), + }); + + const idsData = new Set( + [sourceMarket, targetMarket].flatMap((marketParams) => [ + encodeAbiParameters( + [{ type: "string" }, { type: "address" }], + ["this", sourceAdapter], + ), + encodeAbiParameters( + [{ type: "string" }, { type: "address" }], + ["collateralToken", marketParams.collateralToken], + ), + encodeAbiParameters( + [{ type: "string" }, { type: "address" }, marketParamsAbi], + ["this/marketParams", sourceAdapter, marketParams], + ), + ]), + ); + + for (const idData of idsData) { + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "increaseAbsoluteCap", + args: [idData, maxUint128], + }), + }); + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "increaseRelativeCap", + args: [idData, MathLib.WAD], + }), + }); + } + + await client.writeContract({ + address: vault, + abi: vaultV2Abi, + functionName: "setLiquidityAdapterAndData", + args: [ + sourceAdapter, + encodeAbiParameters([marketParamsAbi], [sourceMarket]), + ], + }); + await client.deal({ + account: client.account.address, + erc20: targetMarket.loanToken, + amount: sourceDeposit, + }); + await client.approve({ + address: targetMarket.loanToken, + args: [vault, sourceDeposit], + }); + await client.writeContract({ + address: vault, + abi: vaultV2Abi, + functionName: "deposit", + args: [sourceDeposit, client.account.address], + }); + await client.deal({ + account: vault, + erc20: targetMarket.loanToken, + amount: initialIdleAssets, + }); + + const deploymentHash = await client.deployContract({ + abi: allocatorAbi, + bytecode: allocatorCode, + }); + const deploymentReceipt = await client.waitForTransactionReceipt({ + hash: deploymentHash, + }); + const allocator = deploymentReceipt.contractAddress; + assert(allocator != null); + + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "setIsAllocator", + args: [allocator, true], + }), + }); + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "setIsActiveAdapter", + args: [vault, sourceAdapter, true], + }); + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "setAbsoluteCap", + args: [vault, targetAdapter, targetMarket, maxUint128], + }); + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "setCanPullFromMarket", + args: [vault, sourceAdapter, sourceMarket, true], + }); + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "setCanPullFromIdle", + args: [vault, true], + }); + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "setPenalty", + args: [vault, penalty], + }); + + await supplyCollateral({ + client: anvilClient, + chainId: base.id, + market: targetMarket, + collateralAmount, + }); + await client.deal({ + account: client.account.address, + erc20: targetMarket.loanToken, + amount: totalPenaltyAssets, + }); + + const reallocations: readonly VaultV2BlueReallocation[] = [ + { + allocator, + type: "bluePublicAllocator", + vault, + from: { + type: "market", + adapter: sourceAdapter, + marketParams: sourceMarket, + }, + to: { adapter: targetAdapter }, + assets: sourceAssets, + penalty, + }, + { + allocator, + type: "bluePublicAllocator", + vault, + from: { type: "idle" }, + to: { adapter: targetAdapter }, + assets: idleAssets, + penalty, + }, + ]; + + const morphoClient = client.extend(morphoViemExtension()).morpho; + const market = morphoClient.blue(targetMarket, base.id); + const positionData = await market.getPositionData(client.account.address); + const borrow = market.borrow({ + userAddress: client.account.address, + amount: borrowAmount, + positionData, + reallocations, + }); + const requirements = await borrow.getRequirements(); + const approval = requirements.find(isRequirementApproval); + const authorization = requirements.find(isRequirementBlueAuthorization); + assert(approval != null); + assert(authorization != null); + await client.sendTransaction(approval); + await client.sendTransaction(authorization); + + const [sourcePositionBefore, targetPositionBefore, vaultBalanceBefore] = + await Promise.all([ + readContractRestructured(client, { + address: morpho, + abi: blueAbi, + functionName: "position", + args: [sourceMarket.id, sourceAdapter], + }), + readContractRestructured(client, { + address: morpho, + abi: blueAbi, + functionName: "position", + args: [targetMarket.id, targetAdapter], + }), + client.readContract({ + address: targetMarket.loanToken, + abi: erc20Abi, + functionName: "balanceOf", + args: [vault], + }), + ]); + + await client.sendTransaction(borrow.buildTx()); + + const [ + sourcePositionAfter, + targetPositionAfter, + vaultBalanceAfter, + bundlerBalanceAfter, + allocatorAllowanceAfter, + ] = await Promise.all([ + readContractRestructured(client, { + address: morpho, + abi: blueAbi, + functionName: "position", + args: [sourceMarket.id, sourceAdapter], + }), + readContractRestructured(client, { + address: morpho, + abi: blueAbi, + functionName: "position", + args: [targetMarket.id, targetAdapter], + }), + client.readContract({ + address: targetMarket.loanToken, + abi: erc20Abi, + functionName: "balanceOf", + args: [vault], + }), + client.readContract({ + address: targetMarket.loanToken, + abi: erc20Abi, + functionName: "balanceOf", + args: [bundler3.bundler3], + }), + client.readContract({ + address: targetMarket.loanToken, + abi: erc20Abi, + functionName: "allowance", + args: [bundler3.bundler3, allocator], + }), + ]); + + expect(sourcePositionAfter.supplyShares).toBeLessThan( + sourcePositionBefore.supplyShares, + ); + expect(targetPositionBefore.supplyShares).toBe(0n); + expect(targetPositionAfter.supplyShares).toBeGreaterThan(0n); + expect(vaultBalanceBefore).toBe(initialIdleAssets); + expect(vaultBalanceAfter).toBe( + vaultBalanceBefore + totalPenaltyAssets - idleAssets, + ); + expect(bundlerBalanceAfter).toBe(0n); + expect(allocatorAllowanceAfter).toBe(0n); + }); +}); diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts new file mode 100644 index 000000000..cdb7c061b --- /dev/null +++ b/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts @@ -0,0 +1,110 @@ +import { getChainAddresses } from "@morpho-org/blue-sdk"; +import { type Address, decodeFunctionData, erc20Abi } from "viem"; +import { mainnet } from "viem/chains"; +import { describe, expect, test } from "vitest"; +import { + CbbtcUsdcBlue, + WbtcUsdcSourceMarket, +} from "../../../test/fixtures/blue.js"; +import { + bundler3Abi, + generalAdapter1Abi, + vaultV2BluePublicAllocatorAbi, +} from "../../abis.js"; +import type { BlueReallocation } from "../../types/index.js"; +import { blueWithdraw } from "./withdraw.js"; + +const allocator: Address = "0x0000000000000000000000000000000000000011"; +const vault: Address = "0x0000000000000000000000000000000000000012"; +const sourceAdapter: Address = "0x0000000000000000000000000000000000000013"; +const targetAdapter: Address = "0x0000000000000000000000000000000000000014"; +const receiver: Address = "0x0000000000000000000000000000000000000015"; + +describe("blueWithdraw Blue Public Allocator", () => { + test("market and idle reallocations fund penalties before morphoWithdraw", () => { + const { + bundler3: { bundler3 }, + } = getChainAddresses(mainnet.id); + const reallocations: readonly BlueReallocation[] = [ + { + type: "bluePublicAllocator", + allocator, + vault, + from: { + type: "market", + adapter: sourceAdapter, + marketParams: WbtcUsdcSourceMarket, + }, + to: { adapter: targetAdapter }, + assets: 10n, + penalty: 500_000_000_000_000_000n, + }, + { + type: "bluePublicAllocator", + allocator, + vault, + from: { type: "idle" }, + to: { adapter: targetAdapter }, + assets: 6n, + penalty: 500_000_000_000_000_000n, + }, + ]; + + const tx = blueWithdraw({ + market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, + args: { + assets: 100n, + shares: 0n, + receiver, + minSharePrice: 0n, + reallocations, + }, + metadata: { origin: "a1b2c3d4" }, + }); + + expect(tx.value).toBe(0n); + expect(tx.action.args.reallocationFee).toBe(0n); + expect(tx.action.args.reallocationPenaltyAssets).toBe(8n); + expect(tx.data).toContain("a1b2c3d4"); + + const bundle = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); + const calls = bundle.args[0] ?? []; + expect(calls).toHaveLength(6); + expect( + decodeFunctionData({ abi: generalAdapter1Abi, data: calls[0]!.data }), + ).toMatchObject({ + functionName: "erc20TransferFrom", + args: [CbbtcUsdcBlue.loanToken, bundler3, 8n], + }); + expect( + decodeFunctionData({ abi: erc20Abi, data: calls[1]!.data }), + ).toMatchObject({ + functionName: "approve", + args: [allocator, 5n], + }); + expect( + decodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + data: calls[2]!.data, + }).functionName, + ).toBe("reallocate"); + expect( + decodeFunctionData({ abi: erc20Abi, data: calls[3]!.data }), + ).toMatchObject({ + functionName: "approve", + args: [allocator, 3n], + }); + expect( + decodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + data: calls[4]!.data, + }).functionName, + ).toBe("allocateFromIdle"); + expect( + decodeFunctionData({ + abi: generalAdapter1Abi, + data: calls[5]!.data, + }).functionName, + ).toBe("morphoWithdraw"); + }); +}); diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index f777f4d92..6bb36b47a 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -87,7 +87,8 @@ export interface BlueWithdrawParams { * withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. * @throws {MutuallyExclusiveWithdrawAmountsError} when both `assets` and `shares` are non-zero. * @throws {EmptyReallocationWithdrawalsError} when any reallocation has no withdrawals. diff --git a/packages/morpho-sdk/src/bundler/actions.test.ts b/packages/morpho-sdk/src/bundler/actions.test.ts index c2663414d..1445c6666 100644 --- a/packages/morpho-sdk/src/bundler/actions.test.ts +++ b/packages/morpho-sdk/src/bundler/actions.test.ts @@ -361,7 +361,7 @@ describe("BundlerAction", () => { marketArbitrary, amountArbitrary, penaltyArbitrary, - skipRevertArbitrary, + fc.constant(false), ) .map( (args) => @@ -378,7 +378,7 @@ describe("BundlerAction", () => { marketArbitrary, amountArbitrary, penaltyArbitrary, - skipRevertArbitrary, + fc.constant(false), ) .map( (args) => @@ -1579,7 +1579,6 @@ describe("BundlerAction", () => { market, 1_000_000n, penalty, - true, ); expect(approval).toBeDefined(); expect(call).toBeDefined(); @@ -1592,7 +1591,7 @@ describe("BundlerAction", () => { expect(approval).toMatchObject({ to: market.loanToken, value: 0n, - skipRevert: true, + skipRevert: false, }); const decoded = decodeFunctionData({ abi: vaultV2BluePublicAllocatorAbi, @@ -1601,7 +1600,7 @@ describe("BundlerAction", () => { expect(call!.to).toBe(allocator); expect(call!.value).toBe(0n); - expect(call!.skipRevert).toBe(true); + expect(call!.skipRevert).toBe(false); expect(decoded.functionName).toBe("reallocate"); expect(decoded.args).toEqual([ vault, @@ -1648,6 +1647,22 @@ describe("BundlerAction", () => { ]); }); + test("vaultV2BluePublicAllocatorReallocate rejects a skippable penalty approval", () => { + expect(() => + BundlerAction.vaultV2BluePublicAllocatorReallocate( + allocator, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 1_000_000n, + 1_000_000_000_000_000n, + true, + ), + ).toThrow(BundlerErrors.SkippableAllocatorPenalty); + }); + test("vaultV2BluePublicAllocatorAllocateFromIdle", () => { const penalty = 1_000_000_000_000_000n; const [approval, call] = @@ -1658,7 +1673,6 @@ describe("BundlerAction", () => { market, 1_000_000n, penalty, - true, ); expect(approval).toBeDefined(); expect(call).toBeDefined(); @@ -1671,7 +1685,7 @@ describe("BundlerAction", () => { expect(approval).toMatchObject({ to: market.loanToken, value: 0n, - skipRevert: true, + skipRevert: false, }); const decoded = decodeFunctionData({ abi: vaultV2BluePublicAllocatorAbi, @@ -1680,7 +1694,7 @@ describe("BundlerAction", () => { expect(call!.to).toBe(allocator); expect(call!.value).toBe(0n); - expect(call!.skipRevert).toBe(true); + expect(call!.skipRevert).toBe(false); expect(decoded.functionName).toBe("allocateFromIdle"); expect(decoded.args).toEqual([ vault, @@ -1715,6 +1729,20 @@ describe("BundlerAction", () => { expect(decoded.args).toEqual([vault, allocateAdapter, market, 1n, 0n]); }); + test("vaultV2BluePublicAllocatorAllocateFromIdle rejects a skippable penalty approval", () => { + expect(() => + BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + allocator, + vault, + allocateAdapter, + market, + 1_000_000n, + 1_000_000_000_000_000n, + true, + ), + ).toThrow(BundlerErrors.SkippableAllocatorPenalty); + }); + test("wrapNative", () => { const call = onlyCall( BundlerAction.wrapNative(chainId, 1n, recipient, true), diff --git a/packages/morpho-sdk/src/bundler/actions.ts b/packages/morpho-sdk/src/bundler/actions.ts index 8e2f11426..9801eeff7 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -1468,10 +1468,14 @@ export namespace BundlerAction { * @param penalty - Vault-configured proportional penalty, scaled by WAD. * @param skipRevert - Whether Bundler3 should tolerate a revert. * @returns An exact token approval when needed, followed by the allocator call. + * @throws {BundlerErrors.SkippableAllocatorPenalty} when `skipRevert` is true and a token approval is required. * @example * ```ts * import type { InputMarketParams } from "@morpho-org/blue-sdk"; - * import { BundlerAction } from "@morpho-org/morpho-sdk/bundler"; + * import { + * BundlerAction, + * type BundlerCall, + * } from "@morpho-org/morpho-sdk/bundler"; * import type { Address } from "viem"; * * const allocatorFixture = @@ -1492,14 +1496,14 @@ export namespace BundlerAction { * collateralToken: weth, * oracle: ethUsdOracle, * irm: adaptiveCurveIrm, - * lltv: 860_000000000000000000n, + * lltv: 860_000_000_000_000_000n, * } satisfies InputMarketParams; * const targetMarket = { * ...sourceMarket, * collateralToken: wbtc, * } satisfies InputMarketParams; * - * const calls = BundlerAction.vaultV2BluePublicAllocatorReallocate( + * const calls: BundlerCall[] = BundlerAction.vaultV2BluePublicAllocatorReallocate( * allocatorFixture, * keyrockUsdcVault, * sourceAdapterFixture, @@ -1529,6 +1533,9 @@ export namespace BundlerAction { assets, penalty, ); + if (skipRevert && penaltyAssets > 0n) { + throw new BundlerErrors.SkippableAllocatorPenalty(penaltyAssets); + } if (penaltyAssets > 0n) { calls.push({ @@ -1581,10 +1588,14 @@ export namespace BundlerAction { * @param penalty - Vault-configured proportional penalty, scaled by WAD. * @param skipRevert - Whether Bundler3 should tolerate a revert. * @returns An exact token approval when needed, followed by the allocator call. + * @throws {BundlerErrors.SkippableAllocatorPenalty} when `skipRevert` is true and a token approval is required. * @example * ```ts * import type { InputMarketParams } from "@morpho-org/blue-sdk"; - * import { BundlerAction } from "@morpho-org/morpho-sdk/bundler"; + * import { + * BundlerAction, + * type BundlerCall, + * } from "@morpho-org/morpho-sdk/bundler"; * import type { Address } from "viem"; * * const allocatorFixture = @@ -1602,10 +1613,10 @@ export namespace BundlerAction { * collateralToken: weth, * oracle: ethUsdOracle, * irm: adaptiveCurveIrm, - * lltv: 860_000000000000000000n, + * lltv: 860_000_000_000_000_000n, * } satisfies InputMarketParams; * - * const calls = BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + * const calls: BundlerCall[] = BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( * allocatorFixture, * keyrockUsdcVault, * targetAdapterFixture, @@ -1631,6 +1642,9 @@ export namespace BundlerAction { assets, penalty, ); + if (skipRevert && penaltyAssets > 0n) { + throw new BundlerErrors.SkippableAllocatorPenalty(penaltyAssets); + } if (penaltyAssets > 0n) { calls.push({ diff --git a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts index e67c21100..78bffff87 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts @@ -5,14 +5,23 @@ import { MarketParams, ORACLE_PRICE_SCALE, } from "@morpho-org/blue-sdk"; -import { blueAbi } from "@morpho-org/blue-sdk-viem"; +import { blueAbi, erc2612Abi, fetchToken } from "@morpho-org/blue-sdk-viem"; import { createMockClient, mockRead } from "@morpho-org/test/mock"; import { erc20Abi } from "viem"; import { mainnet } from "viem/chains"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { CbbtcUsdcBlue } from "../../../test/fixtures/blue.js"; import { morphoViemExtension } from "../../client/index.js"; -import { isRequirementApproval } from "../../types/index.js"; +import { + isRequirementApproval, + isRequirementSignature, +} from "../../types/index.js"; + +vi.mock("@morpho-org/blue-sdk-viem", async (importOriginal) => { + const original = + await importOriginal(); + return { ...original, fetchToken: vi.fn() }; +}); const USER = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; const marketParams = new MarketParams(CbbtcUsdcBlue); @@ -91,4 +100,166 @@ describe("MorphoBlue BluePublicAllocator requirements", () => { amount: 1n, }); }); + + test("behavior: aggregates collateral and penalty into one shared-token approval", async () => { + const sharedTokenParams = new MarketParams({ + ...CbbtcUsdcBlue, + collateralToken: CbbtcUsdcBlue.loanToken, + }); + const handle = createMockClient(mainnet); + const { + morpho, + bundler3: { generalAdapter1 }, + } = getChainAddresses(mainnet.id); + mockRead(handle, { + address: morpho, + abi: blueAbi, + functionName: "isAuthorized", + result: true, + }); + mockRead(handle, { + address: morpho, + abi: blueAbi, + functionName: "nonce", + result: 0n, + }); + mockRead(handle, { + address: sharedTokenParams.loanToken, + abi: erc20Abi, + functionName: "allowance", + result: 0n, + }); + const positionData = new AccrualPosition( + { + user: USER, + supplyShares: 0n, + borrowShares: 1n, + collateral: 1_000_000n, + }, + new Market({ + params: sharedTokenParams, + totalSupplyAssets: 1_000_000n, + totalBorrowAssets: 1n, + totalSupplyShares: 1_000_000n, + totalBorrowShares: 1n, + lastUpdate: 1_700_000_000n, + fee: 0n, + price: ORACLE_PRICE_SCALE, + }), + ); + const market = handle.client + .extend(morphoViemExtension({ supportSignature: false })) + .morpho.blue(sharedTokenParams, mainnet.id); + + const requirements = await market + .supplyCollateralBorrow({ + amount: 100n, + borrowAmount: 1n, + userAddress: USER, + positionData, + reallocations: [ + { + type: "bluePublicAllocator", + allocator: CbbtcUsdcBlue.irm, + vault: CbbtcUsdcBlue.oracle, + from: { type: "idle" }, + to: { adapter: CbbtcUsdcBlue.collateralToken }, + assets: 10n, + penalty: 500_000_000_000_000_000n, + }, + ], + }) + .getRequirements(); + + const approvals = requirements.filter(isRequirementApproval); + expect(approvals).toHaveLength(1); + expect(approvals[0]?.to).toBe(sharedTokenParams.loanToken); + expect(approvals[0]?.action.args).toStrictEqual({ + spender: generalAdapter1, + amount: 105n, + }); + }); + + test("behavior: aggregates collateral and penalty into one shared-token simple permit", async () => { + const sharedTokenParams = new MarketParams({ + ...CbbtcUsdcBlue, + collateralToken: CbbtcUsdcBlue.loanToken, + }); + const handle = createMockClient(mainnet); + const { morpho } = getChainAddresses(mainnet.id); + mockRead(handle, { + address: morpho, + abi: blueAbi, + functionName: "isAuthorized", + result: true, + }); + mockRead(handle, { + address: morpho, + abi: blueAbi, + functionName: "nonce", + result: 0n, + }); + mockRead(handle, { + address: sharedTokenParams.loanToken, + abi: erc2612Abi, + functionName: "nonces", + result: 0n, + }); + vi.mocked(fetchToken).mockResolvedValue({ + address: sharedTokenParams.loanToken, + decimals: 6, + symbol: "USDC", + name: "USD Coin", + fromUsd: () => 0n, + toUsd: () => 0n, + }); + const positionData = new AccrualPosition( + { + user: USER, + supplyShares: 0n, + borrowShares: 1n, + collateral: 1_000_000n, + }, + new Market({ + params: sharedTokenParams, + totalSupplyAssets: 1_000_000n, + totalBorrowAssets: 1n, + totalSupplyShares: 1_000_000n, + totalBorrowShares: 1n, + lastUpdate: 1_700_000_000n, + fee: 0n, + price: ORACLE_PRICE_SCALE, + }), + ); + const market = handle.client + .extend(morphoViemExtension({ supportSignature: true })) + .morpho.blue(sharedTokenParams, mainnet.id); + + const requirements = await market + .supplyCollateralBorrow({ + amount: 100n, + borrowAmount: 1n, + userAddress: USER, + positionData, + reallocations: [ + { + type: "bluePublicAllocator", + allocator: CbbtcUsdcBlue.irm, + vault: CbbtcUsdcBlue.oracle, + from: { type: "idle" }, + to: { adapter: CbbtcUsdcBlue.collateralToken }, + assets: 10n, + penalty: 500_000_000_000_000_000n, + }, + ], + }) + .getRequirements({ useSimplePermit: true }); + + const permits = requirements.filter(isRequirementSignature); + expect(permits).toHaveLength(1); + expect(permits[0]?.action).toMatchObject({ + type: "permit", + args: { amount: 105n }, + }); + }); }); diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index d9fe6c89d..e890a0da4 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -203,7 +203,8 @@ export interface BlueActions { * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ withdraw: ( @@ -247,7 +248,8 @@ export interface BlueActions { * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ borrow: (params: { @@ -399,7 +401,8 @@ export interface BlueActions { * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ supplyCollateralBorrow: ( @@ -457,7 +460,8 @@ export interface BlueActions { * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a V2 source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ refinance: (params: { @@ -573,8 +577,9 @@ export class MorphoBlue implements BlueActions { ) { const amount = computeVaultV2ReallocationPenaltyAssets(reallocations ?? []); - // Penalty funding always uses the classic GeneralAdapter1 allowance so a - // collateral permit and a loan-token penalty can coexist in one bundle. + // Separate-token penalty funding uses a classic GeneralAdapter1 allowance so a collateral + // permit and a loan-token penalty can coexist in one bundle. The shared-token path aggregates + // both amounts into the collateral requirement instead. return getGeneralAdapterRequirements(this.client.viemClient, { address: this.marketParams.loanToken, chainId: this.chainId, @@ -1422,6 +1427,13 @@ export class MorphoBlue implements BlueActions { }); return { getRequirements: async (params?: { useSimplePermit?: boolean }) => { + const penaltyAssets = computeVaultV2ReallocationPenaltyAssets( + reallocations ?? [], + ); + const usesSharedFundingToken = isAddressEqual( + this.marketParams.collateralToken, + this.marketParams.loanToken, + ); const [erc20Requirements, penaltyRequirements, authTx] = await Promise.all([ getGeneralAdapterRequirements(this.client.viemClient, { @@ -1430,9 +1442,17 @@ export class MorphoBlue implements BlueActions { supportSignature: this.client.options.supportSignature, supportDeployless: this.client.options.supportDeployless, useSimplePermit: params?.useSimplePermit, - args: { amount, from: userAddress }, + args: { + amount: amount + (usesSharedFundingToken ? penaltyAssets : 0n), + from: userAddress, + }, }), - this.getReallocationPenaltyRequirements(userAddress, reallocations), + usesSharedFundingToken + ? Promise.resolve([]) + : this.getReallocationPenaltyRequirements( + userAddress, + reallocations, + ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, diff --git a/packages/morpho-sdk/src/entities/index.ts b/packages/morpho-sdk/src/entities/index.ts index 4d58432ac..6520960fd 100644 --- a/packages/morpho-sdk/src/entities/index.ts +++ b/packages/morpho-sdk/src/entities/index.ts @@ -76,6 +76,7 @@ export { } from "./vaultV1ReallocationData.js"; export { MorphoVaultV2 } from "./vaultV2/index.js"; export { + computeVaultV2Reallocations, type InputVaultV2ReallocationData, VaultV2ReallocationData, } from "./vaultV2ReallocationData.js"; diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index 9a7070aa4..d1ba3ce8a 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -11,12 +11,15 @@ import type { Address, Hash } from "viem"; import { zeroAddress } from "viem"; import { describe, expect, test } from "vitest"; import { blueBorrow } from "../actions/index.js"; -import { computeVaultV2Reallocations } from "../helpers/index.js"; import { InsufficientSharedLiquidityError, ReallocationWithdrawExceedsMarketSupplyError, + UnknownReallocationMarketError, } from "../types/index.js"; -import { VaultV2ReallocationData } from "./vaultV2ReallocationData.js"; +import { + computeVaultV2Reallocations, + VaultV2ReallocationData, +} from "./vaultV2ReallocationData.js"; const TIMESTAMP = 1_700_000_000n; const ALLOCATOR = "0x0000000000000000000000000000000000000001"; @@ -46,10 +49,12 @@ const makeMarket = ({ params, supply, borrow, + lastUpdate = TIMESTAMP, }: { readonly params: MarketParams; readonly supply: bigint; readonly borrow: bigint; + readonly lastUpdate?: bigint; }) => new Market({ params, @@ -57,7 +62,7 @@ const makeMarket = ({ totalBorrowAssets: borrow, totalSupplyShares: supply * 1_000_000n, totalBorrowShares: borrow * 1_000_000n, - lastUpdate: TIMESTAMP, + lastUpdate, fee: 0n, }); @@ -82,6 +87,10 @@ interface FixtureOptions { readonly canPullFromIdle?: boolean; readonly canPullFromMarket?: boolean; readonly penalty?: bigint; + readonly sourceLastUpdate?: bigint; + readonly targetLastUpdate?: bigint; + readonly vaultLastUpdate?: bigint; + readonly maxRate?: bigint; } const makeFixture = ({ @@ -105,12 +114,17 @@ const makeFixture = ({ canPullFromIdle = true, canPullFromMarket = true, penalty = 7n, + sourceLastUpdate = TIMESTAMP, + targetLastUpdate = TIMESTAMP, + vaultLastUpdate = TIMESTAMP, + maxRate = 0n, }: FixtureOptions = {}) => { const sameMarket = sourceMarketParams.id === targetParams.id; const targetMarket = makeMarket({ params: targetParams, supply: sameMarket ? sourceSupply : targetSupply, borrow: sameMarket ? sourceBorrow : targetBorrow, + lastUpdate: targetLastUpdate, }); const sourceMarket = sameMarket ? targetMarket @@ -118,6 +132,7 @@ const makeFixture = ({ params: sourceMarketParams, supply: sourceSupply, borrow: sourceBorrow, + lastUpdate: sourceLastUpdate, }); const targetSupplyShares = targetMarket.toSupplyShares( targetPositionAssets, @@ -225,8 +240,8 @@ const makeFixture = ({ _totalAssets: totalAssets, totalSupply: totalAssets, virtualShares: 0n, - maxRate: 0n, - lastUpdate: TIMESTAMP, + maxRate, + lastUpdate: vaultLastUpdate, liquidityAdapter: zeroAddress, liquidityData: "0x", liquidityAllocations: undefined, @@ -338,20 +353,14 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { expect(result.data.getVault(VAULT).assetBalance).toBe(2n); }); - test("behavior: permits the target market through a different adapter", () => { - const { data, sourceExpectedAssets } = makeFixture({ + test("behavior: excludes the target market through a different adapter", () => { + const { data } = makeFixture({ sourceMarketParams: targetParams, }); expect( data.computeVaultV2Reallocations(targetParams.id).reallocations, - ).toMatchObject([ - { - from: { type: "market", adapter: SOURCE_ADAPTER }, - to: { adapter: TARGET_ADAPTER }, - assets: sourceExpectedAssets, - }, - ]); + ).toStrictEqual([]); }); test("behavior: allows deallocation assets to exceed stored allocation", () => { @@ -452,7 +461,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ).toStrictEqual([]); }); - test("behavior: same-market deallocation creates target supply headroom", () => { + test("behavior: same-market deallocation is not counted as target liquidity", () => { const { data } = makeFixture({ sourceMarketParams: targetParams, sourceSupply: MathLib.MAX_UINT_128, @@ -466,9 +475,41 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); expect( - data.computeVaultV2Reallocations(targetParams.id).reallocations[0] - ?.assets, - ).toBe(MathLib.MAX_UINT_128); + data.computeVaultV2Reallocations(targetParams.id).reallocations, + ).toStrictEqual([]); + }); + + test("behavior: defaults to the latest market or vault update timestamp", () => { + const { data } = makeFixture({ + sourceLastUpdate: TIMESTAMP + 1n, + vaultLastUpdate: TIMESTAMP + 2n, + }); + + expect(() => + data.computeVaultV2Reallocations(targetParams.id), + ).not.toThrow(); + }); + + test("behavior: freezes firstTotalAssets after the first penalty donation", () => { + const { data } = makeFixture({ + sourceSupply: 500n, + targetSupply: 0n, + firstTotalAssets: 500n, + maxRate: MathLib.WAD, + vaultLastUpdate: TIMESTAMP - 1n, + penalty: MathLib.WAD, + allocatorTargetCap: 10_000n, + targetCaps: [ + { absoluteCap: 10_000n, relativeCap: MathLib.WAD / 2n }, + { absoluteCap: 10_000n, relativeCap: MathLib.WAD / 2n }, + { absoluteCap: 10_000n, relativeCap: MathLib.WAD / 2n }, + ], + }); + + const result = data.computeVaultV2Reallocations(targetParams.id); + + expect(result.reallocations[0]?.assets).toBe(500n); + expect(result.data.getVault(VAULT)._totalAssets).toBe(1_000n); }); test("behavior: disabled discovery returns no calls", () => { @@ -480,6 +521,17 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ).toStrictEqual([]); }); + test("error: UnknownReallocationMarketError with an explicit timestamp", () => { + const { data } = makeFixture(); + data.markets[targetParams.id] = undefined; + + expect(() => + data.computeVaultV2Reallocations(targetParams.id, { + timestamp: TIMESTAMP, + }), + ).toThrow(UnknownReallocationMarketError); + }); + test("behavior: ignores vault liquidity above the penalty threshold", () => { const { data, sourceExpectedAssets } = makeFixture({ idle: 300n, @@ -535,6 +587,30 @@ describe("computeVaultV2Reallocations", () => { expect(reallocations[0]?.assets).toBe(1n); }); + test("behavior: plans at the latest snapshot timestamp by default", () => { + const { data } = makeFixture({ + targetSupply: 100n, + targetBorrow: 90n, + sourceLastUpdate: TIMESTAMP + 2n, + }); + + const defaultReallocations = computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 20n, + }); + const explicitReallocations = computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 20n, + options: { timestamp: TIMESTAMP + 2n }, + }); + + expect(defaultReallocations).toStrictEqual(explicitReallocations); + }); + test("behavior: falls back to a 100% source-utilization ceiling", () => { const { data } = makeFixture({ targetSupply: 100n, diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index 579976b52..403c01bc2 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -23,8 +23,10 @@ import type { VaultV2BlueReallocation, } from "../types/index.js"; import { + InsufficientSharedLiquidityError, ReallocationAdapterSupplySharesUnderflowError, ReallocationAllocationUnderflowError, + ReallocationWithdrawExceedsMarketSupplyError, UnknownReallocationAdapterError, UnknownReallocationAllocationError, UnknownReallocationMarketError, @@ -124,8 +126,9 @@ const cloneVault = (vault: AccrualVaultV2) => { * Immutable-by-convention state container for Vault V2 BluePublicAllocator simulations. * * Constructor inputs are cloned. Every simulated reallocation returns a new - * instance, while `firstTotalAssets` is represented by each accrued vault's - * frozen `_totalAssets` value for the duration of a plan. + * instance. The first allocation for each vault accrues it in contract order + * after the penalty donation and any source deallocation, then freezes that + * `_totalAssets` value as `firstTotalAssets` for the rest of the plan. * * @example * ```ts @@ -263,7 +266,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * * @param marketId - Market id to read. * @returns The market state. - * @throws {@link UnknownReallocationMarketError} when the market is absent. + * @throws {UnknownReallocationMarketError} when the market is absent. * @example * ```ts * const market = data.getMarket(marketId); @@ -280,7 +283,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * * @param vault - Vault V2 address. * @returns The accrued Vault V2 state. - * @throws {@link UnknownReallocationVaultError} when the vault is absent. + * @throws {UnknownReallocationVaultError} when the vault is absent. * @example * ```ts * const vault = data.getVault(vaultAddress); @@ -298,7 +301,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @param vault - Vault V2 address. * @param id - Derived allocation id. * @returns The allocation and cap state. - * @throws {@link UnknownReallocationAllocationError} when the record is absent. + * @throws {UnknownReallocationAllocationError} when the record is absent. * @example * ```ts * const allocation = data.getAllocation(vaultAddress, allocationId); @@ -316,7 +319,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * * @param vault - Vault V2 address. * @returns The vault-wide allocator configuration. - * @throws {@link UnknownReallocationPublicAllocatorConfigError} when it is absent. + * @throws {UnknownReallocationPublicAllocatorConfigError} when it is absent. * @example * ```ts * const config = data.getPublicAllocatorConfig(vaultAddress); @@ -335,7 +338,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @param vault - Vault V2 address. * @param marketParamsId - Adapter-scoped market-parameters id. * @returns The allocator cap and permissions. - * @throws {@link UnknownReallocationMarketPublicAllocatorConfigError} when it is absent. + * @throws {UnknownReallocationMarketPublicAllocatorConfigError} when it is absent. * @example * ```ts * const config = data.getMarketPublicAllocatorConfig(vaultAddress, marketParamsId); @@ -357,7 +360,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @param vault - Vault V2 address. * @param adapter - Adapter address. * @returns The accrued adapter state. - * @throws {@link UnknownReallocationAdapterError} when it is absent or unsupported. + * @throws {UnknownReallocationAdapterError} when it is absent or unsupported. * @example * ```ts * const adapter = data.getAdapter(vaultAddress, adapterAddress); @@ -386,7 +389,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @param marketId - Target Blue market id. * @param options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. * @returns Flat action-ready reallocations and their post-simulation state. - * @throws {@link UnknownReallocationMarketError} when the target market is absent. + * @throws {UnknownReallocationMarketError} when the target market is absent. * @example * ```ts * import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; @@ -399,7 +402,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { marketId: MarketId, options: VaultV2BluePublicAllocatorOptions = {}, ) { - return this._computeVaultV2Reallocations({ + return this.computeVaultV2ReallocationsAtUtilization({ marketId, maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, options, @@ -407,15 +410,138 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { } /** - * Computes Vault V2 reallocations using an explicit internal source-utilization ceiling. + * Computes the action-ready Vault V2 reallocations required by a Blue borrow + * or loan-asset withdrawal. * - * @param marketId - Target market id. - * @param maxWithdrawalUtilization - Source-market utilization ceiling. - * @param options - Discovery options, including the maximum penalty. - * @returns Flat action-ready reallocations and post-simulation state. - * @internal + * Friendly liquidity is considered first. When it cannot cover the absolute + * liquidity shortfall, the planner continues from that post-state up to 100% + * source utilization. Fee-bearing partial plans are rejected. + * + * @param params - Operation and discovery parameters. + * @param params.marketId - Target Blue market id. + * @param params.operation - Operation driving the reallocation. + * @param params.amount - Borrow or withdraw amount. + * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. + * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. + * @throws {UnknownReallocationMarketError} when the target market is absent. + * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. + * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. + * @example + * ```ts + * const reallocations = data.computeVaultV2ReallocationsForOperation({ + * marketId: targetMarketId, + * operation: "borrow", + * amount: 1_000_000n, + * options: { timestamp }, + * }); + * ``` */ - public _computeVaultV2Reallocations({ + public computeVaultV2ReallocationsForOperation({ + marketId, + operation, + amount, + options, + }: { + readonly marketId: MarketId; + readonly operation: "borrow" | "withdraw"; + readonly amount: bigint; + readonly options?: VaultV2BluePublicAllocatorOptions; + }): readonly VaultV2BlueReallocation[] { + if (options?.enabled === false) return []; + + const timestamp = + options?.timestamp == null + ? this.getLatestSnapshotTimestamp() + : BigInt(options.timestamp); + const normalizedOptions = { ...options, timestamp }; + const market = this.getMarket(marketId).accrueInterest(timestamp); + if (operation === "withdraw" && amount > market.totalSupplyAssets) { + throw new ReallocationWithdrawExceedsMarketSupplyError({ + marketId, + withdrawAmount: amount, + totalSupplyAssets: market.totalSupplyAssets, + }); + } + + const newTotalBorrowAssets = + operation === "borrow" + ? market.totalBorrowAssets + amount + : market.totalBorrowAssets; + const newTotalSupplyAssets = + operation === "withdraw" + ? market.totalSupplyAssets - amount + : market.totalSupplyAssets; + + if ( + MarketUtils.getUtilization({ + totalSupplyAssets: newTotalSupplyAssets, + totalBorrowAssets: newTotalBorrowAssets, + }) <= DEFAULT_SUPPLY_TARGET_UTILIZATION + ) + return []; + + let requiredAssets = + MathLib.wDivUp(newTotalBorrowAssets, DEFAULT_SUPPLY_TARGET_UTILIZATION) - + newTotalSupplyAssets; + + const friendly = this.computeVaultV2Reallocations( + marketId, + normalizedOptions, + ); + const discovered = [...friendly.reallocations]; + const friendlyMarket = friendly.data.getMarket(marketId); + const friendlyBorrow = + operation === "borrow" + ? friendlyMarket.totalBorrowAssets + amount + : friendlyMarket.totalBorrowAssets; + const friendlySupply = + operation === "withdraw" + ? friendlyMarket.totalSupplyAssets - amount + : friendlyMarket.totalSupplyAssets; + + if (friendlyBorrow > friendlySupply) { + requiredAssets = newTotalBorrowAssets - newTotalSupplyAssets; + discovered.push( + ...friendly.data.computeVaultV2ReallocationsAtUtilization({ + marketId, + maxWithdrawalUtilization: MathLib.WAD, + options: normalizedOptions, + }).reallocations, + ); + } + + if (requiredAssets <= 0n) return []; + + const absoluteShortfall = + newTotalBorrowAssets > newTotalSupplyAssets + ? newTotalBorrowAssets - newTotalSupplyAssets + : 0n; + const reallocations: VaultV2BlueReallocation[] = []; + let remainingRequiredAssets = requiredAssets; + let totalReallocated = 0n; + + for (const reallocation of discovered) { + const assets = MathLib.min(reallocation.assets, remainingRequiredAssets); + if (assets <= 0n) continue; + + reallocations.push({ ...reallocation, assets }); + remainingRequiredAssets -= assets; + totalReallocated += assets; + if (remainingRequiredAssets === 0n) break; + } + + if (totalReallocated < absoluteShortfall) { + throw new InsufficientSharedLiquidityError({ + marketId, + shortfall: absoluteShortfall, + available: totalReallocated, + }); + } + + return reallocations; + } + + private computeVaultV2ReallocationsAtUtilization({ marketId, maxWithdrawalUtilization, options = {}, @@ -429,10 +555,12 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { } { if (options.enabled === false) return { reallocations: [], data: this }; - const timestamp = BigInt( - options.timestamp ?? this.getMarket(marketId).lastUpdate, - ); - let data = this.accrue(timestamp); + this.getMarket(marketId); + const timestamp = + options.timestamp == null + ? this.getLatestSnapshotTimestamp() + : BigInt(options.timestamp); + let data = this.accrueMarkets(timestamp); const reallocations: VaultV2BlueReallocation[] = []; const configuredVaults = Object.keys(data.vaults) as Address[]; const vaultKeyByLower = new Map( @@ -480,7 +608,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @param marketId - Target Blue market id. * @param options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. * @returns Reallocatable market and idle assets, or `0n` when none are available. - * @throws {@link UnknownReallocationMarketError} when the target market is absent. + * @throws {UnknownReallocationMarketError} when the target market is absent. * @example * ```ts * const liquidity = data.getPublicReallocationLiquidityVaultV2(targetMarketId); @@ -504,7 +632,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @param utilization - Desired utilization, scaled by WAD. Defaults to 90%. * @param options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. * @returns Borrowable assets while remaining at or below `utilization`. - * @throws {@link UnknownReallocationMarketError} when the target market is absent. + * @throws {UnknownReallocationMarketError} when the target market is absent. * @example * ```ts * const liquidity = data.getAvailableLiquidityToUtilizationVaultV2(targetMarketId); @@ -516,13 +644,17 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { utilization: bigint = DEFAULT_SUPPLY_TARGET_UTILIZATION, options?: VaultV2BluePublicAllocatorOptions, ) { - const market = this.getMarket(marketId).accrueInterest(options?.timestamp); + const timestamp = + options?.timestamp == null + ? this.getLatestSnapshotTimestamp() + : BigInt(options.timestamp); + const market = this.getMarket(marketId).accrueInterest(timestamp); if (DEFAULT_SUPPLY_TARGET_UTILIZATION > utilization) return market.getBorrowToUtilization(utilization); const availableLiquidity = this.getPublicReallocationLiquidityVaultV2( marketId, - options, + { ...options, timestamp }, ); return MarketUtils.getBorrowToUtilization( { @@ -533,7 +665,18 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { ); } - private accrue(timestamp: bigint) { + private getLatestSnapshotTimestamp() { + let timestamp = 0n; + for (const market of Object.values(this.markets)) { + if (market != null) timestamp = MathLib.max(timestamp, market.lastUpdate); + } + for (const vault of Object.values(this.vaults)) { + if (vault != null) timestamp = MathLib.max(timestamp, vault.lastUpdate); + } + return timestamp; + } + + private accrueMarkets(timestamp: bigint) { const data = this.clone(); for (const [marketId, market] of Object.entries(data.markets) as [ @@ -549,15 +692,14 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { AccrualVaultV2 | undefined, ][]) { if (vault == null) continue; - const accruedVault = vault.accrueInterest(timestamp).vault; - for (const adapter of accruedVault.accrualAdapters) { + for (const adapter of vault.accrualAdapters) { if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) continue; adapter.markets = adapter.markets.map((market) => data.getMarket(market.id), ); } - data.vaults[address] = accruedVault; + data.vaults[address] = vault; } return data; @@ -574,6 +716,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { readonly maxWithdrawalUtilization: bigint; readonly maxPenalty?: bigint; }) { + const targetMarket = this.getMarket(marketId); return _try(() => { const vault = this.getVault(vaultAddress); const publicAllocatorConfig = this.getPublicAllocatorConfig(vaultAddress); @@ -584,7 +727,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { ) return; - const targetMarket = this.getMarket(marketId); const targetSupplyHeadroom = MathLib.zeroFloorSub( MathLib.MAX_UINT_128, targetMarket.totalSupplyAssets, @@ -652,66 +794,32 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { targetMarketParamsAllocation.allocation + targetContext.untracked, ); - const getTargetCapHeadroom = ( - sourceIds: ReadonlySet, - sourceUntracked: bigint, - ) => { - let headroom = MathLib.MAX_UINT_256; - for (const [ - index, - allocation, - ] of targetContext.allocations.entries()) { - const id = targetContext.ids[index]!; - if (sourceIds.has(id)) { - const postAllocation = - allocation.allocation + - sourceUntracked + - targetContext.untracked; - const capacity = VaultV2Utils.allocationHeadroom( - allocation, - vault._totalAssets, - ); - if (postAllocation > allocation.allocation + capacity.value) - return; - continue; - } - - headroom = MathLib.min( - headroom, - MathLib.zeroFloorSub( - VaultV2Utils.allocationHeadroom(allocation, vault._totalAssets) - .value, - targetContext.untracked, - ), - ); - } - return headroom; - }; - if (publicAllocatorConfig.canPullFromIdle) { - const targetHeadroom = getTargetCapHeadroom(new Set(), 0n); - if (targetHeadroom != null) { - const assets = MathLib.min( - MathLib.MAX_UINT_128, - targetSupplyHeadroom, - allocatorHeadroom, - targetHeadroom, - MathLib.zeroFloorSub( - vault.assetBalance, - this.donatedPenaltyAssets[vaultAddress] ?? 0n, - ), - ); - if (assets > 0n) { - candidates.push({ - allocator: this.allocator, - type: "bluePublicAllocator", - vault: vaultAddress, - from: { type: "idle" }, - to: { adapter: targetContext.adapter.address }, - assets, - penalty: publicAllocatorConfig.penalty, - }); - } + const maxAssets = MathLib.min( + MathLib.MAX_UINT_128, + targetSupplyHeadroom, + allocatorHeadroom, + MathLib.zeroFloorSub( + vault.assetBalance, + this.donatedPenaltyAssets[vaultAddress] ?? 0n, + ), + ); + const reallocation = { + allocator: this.allocator, + type: "bluePublicAllocator", + vault: vaultAddress, + from: { type: "idle" }, + to: { adapter: targetContext.adapter.address }, + assets: maxAssets, + penalty: publicAllocatorConfig.penalty, + } satisfies VaultV2BlueReallocation; + const assets = this.getMaxCapCompatibleAssets({ + reallocation, + targetMarketId: marketId, + timestamp: targetMarket.lastUpdate, + }); + if (assets > 0n) { + candidates.push({ ...reallocation, assets }); } } @@ -731,14 +839,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { ) ) continue; - if ( - sameMarketId(sourceMarket.id, marketId) && - isAddressEqual( - sourceAdapter.address, - targetContext.adapter.address, - ) - ) - continue; + if (sameMarketId(sourceMarket.id, marketId)) continue; const candidate = _try(() => { const sourceIds = sourceAdapter.ids(sourceMarket.params); @@ -764,29 +865,14 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { const expectedSupplyAssets = sourceMarket.toSupplyAssets( sourceAdapter.supplyShares[sourceMarket.id] ?? 0n, ); - const sourceUntracked = MathLib.zeroFloorSub( - expectedSupplyAssets, - sourceAllocations[2]!.allocation, - ); - const targetHeadroom = getTargetCapHeadroom( - new Set(sourceIds), - sourceUntracked, - ); - if (targetHeadroom == null) return; - - const assets = MathLib.min( + const maxAssets = MathLib.min( MathLib.MAX_UINT_128, - sameMarketId(sourceMarket.id, marketId) - ? MathLib.MAX_UINT_128 - : targetSupplyHeadroom, + targetSupplyHeadroom, allocatorHeadroom, - targetHeadroom, expectedSupplyAssets, sourceMarket.getWithdrawToUtilization(maxWithdrawalUtilization), ); - if (assets <= 0n) return; - - return { + const reallocation = { allocator: this.allocator, type: "bluePublicAllocator", vault: vaultAddress, @@ -796,9 +882,17 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { marketParams: sourceMarket.params, }, to: { adapter: targetContext.adapter.address }, - assets, + assets: maxAssets, penalty: publicAllocatorConfig.penalty, } satisfies VaultV2BlueReallocation; + const assets = this.getMaxCapCompatibleAssets({ + reallocation, + targetMarketId: marketId, + timestamp: targetMarket.lastUpdate, + }); + if (assets <= 0n) return; + + return { ...reallocation, assets }; }, UnknownDataError); if (candidate != null) candidates.push(candidate); } @@ -811,6 +905,47 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { }, UnknownDataError); } + private getMaxCapCompatibleAssets({ + reallocation, + targetMarketId, + timestamp, + }: { + readonly reallocation: VaultV2BlueReallocation; + readonly targetMarketId: MarketId; + readonly timestamp: bigint; + }) { + let lower = 0n; + let upper = reallocation.assets; + + while (lower < upper) { + const assets = (lower + upper + 1n) / 2n; + const postState = this.applyPublicReallocation({ + reallocation: { ...reallocation, assets }, + targetMarketId, + timestamp, + }); + const vault = postState.getVault(reallocation.vault); + const adapter = postState.getAdapter( + reallocation.vault, + reallocation.to.adapter, + ); + const targetIds = adapter.ids(postState.getMarket(targetMarketId).params); + const withinCaps = targetIds.every((id) => { + const allocation = postState.getAllocation(reallocation.vault, id); + const capacity = VaultV2Utils.allocationHeadroom( + { ...allocation, allocation: 0n }, + vault._totalAssets, + ).value; + return allocation.absoluteCap > 0n && allocation.allocation <= capacity; + }); + + if (withinCaps) lower = assets; + else upper = assets - 1n; + } + + return lower; + } + private applyPublicReallocation({ reallocation, targetMarketId, @@ -821,13 +956,8 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { readonly timestamp: bigint; }) { const data = this.clone(); - const vault = data.getVault(reallocation.vault); - const targetAdapter = data.getAdapter( - reallocation.vault, - reallocation.to.adapter, - ); + let vault = data.getVault(reallocation.vault); const targetMarket = data.getMarket(targetMarketId); - const targetIds = targetAdapter.ids(targetMarket.params); const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( reallocation.assets, @@ -878,6 +1008,15 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { vault.assetBalance += reallocation.assets; } + vault = vault.accrueInterest(timestamp).vault; + data.vaults[reallocation.vault] = vault; + + const targetAdapter = data.getAdapter( + reallocation.vault, + reallocation.to.adapter, + ); + const targetIds = targetAdapter.ids(targetMarket.params); + const currentTargetMarket = data.getMarket(targetMarket.id); const oldTargetAllocation = data.getAllocation( reallocation.vault, @@ -943,3 +1082,72 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { if (index >= 0) adapter.markets[index] = market; } } + +/** + * Computes action-ready Vault V2 BluePublicAllocator reallocations for a Blue + * borrow or loan-asset withdraw. + * + * @param params.reallocationData - Vault V2 reallocation state fetched at one block. + * @param params.marketId - Target Blue market id. + * @param params.operation - Operation driving the reallocation. + * @param params.amount - Borrow or withdraw amount. + * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. + * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. + * @throws {UnknownReallocationMarketError} when the target market is absent. + * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. + * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. + * @example + * ```ts + * import { Market, MarketParams } from "@morpho-org/blue-sdk"; + * import { + * computeVaultV2Reallocations, + * type VaultV2BlueReallocation, + * } from "@morpho-org/morpho-sdk"; + * import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; + * + * const timestamp = 1_700_000_000n; + * const marketParams = new MarketParams({ + * loanToken: "0x0000000000000000000000000000000000000001", + * collateralToken: "0x0000000000000000000000000000000000000002", + * oracle: "0x0000000000000000000000000000000000000003", + * irm: "0x0000000000000000000000000000000000000004", + * lltv: 860_000_000_000_000_000n, + * }); + * const market = new Market({ + * params: marketParams, + * totalSupplyAssets: 1_000_000n, + * totalBorrowAssets: 500_000n, + * totalSupplyShares: 1_000_000n, + * totalBorrowShares: 500_000n, + * lastUpdate: timestamp, + * fee: 0n, + * }); + * const reallocationData = new VaultV2ReallocationData({ + * chainId: 1, + * allocator: "0x0000000000000000000000000000000000000005", + * markets: { [marketParams.id]: market }, + * }); + * + * const reallocations: readonly VaultV2BlueReallocation[] = + * computeVaultV2Reallocations({ + * reallocationData, + * marketId: marketParams.id, + * operation: "borrow", + * amount: 100_000n, + * options: { timestamp }, + * }); + * + * console.log(reallocations); // [] — projected utilization remains below 90%. + * ``` + */ +export const computeVaultV2Reallocations = ({ + reallocationData, + ...params +}: { + readonly reallocationData: VaultV2ReallocationData; + readonly marketId: MarketId; + readonly operation: "borrow" | "withdraw"; + readonly amount: bigint; + readonly options?: VaultV2BluePublicAllocatorOptions; +}): readonly VaultV2BlueReallocation[] => + reallocationData.computeVaultV2ReallocationsForOperation(params); diff --git a/packages/morpho-sdk/src/helpers/AGENTS.md b/packages/morpho-sdk/src/helpers/AGENTS.md index b66ad941a..56d4056e3 100644 --- a/packages/morpho-sdk/src/helpers/AGENTS.md +++ b/packages/morpho-sdk/src/helpers/AGENTS.md @@ -9,7 +9,7 @@ Per-function contracts (arguments, return shapes, behavior) live as JSDoc on eac - **Encoders** (ABI encoding plus input validation, no I/O) — e.g. `encodeForceDeallocateCall(deallocation, onBehalf)`. ABI-encodes a single `VaultV2.forceDeallocate` calldata entry and throws `NonPositiveInputError` on a non-positive `amount`. The `data` field carries ABI-encoded `MarketParams` for the Morpho Market V1 adapter, or empty bytes otherwise. Internal sub-helpers (e.g. `encodeDeallocateData`) are not exported. - **Validators** (pure, throw typed errors) — `validateReallocations(...)`, `validateSlippageTolerance(...)`, `validatePositionHealth(...)`. Each enforces a public-API invariant: see the `error.ts` exports for the full list of error classes a caller may pattern-match on. - **Math / share-price helpers** — `computeMaxRepaySharePrice`, `computeMinBorrowSharePrice`, etc. Use `MAX_SLIPPAGE_TOLERANCE` and cap at `MAX_ABSOLUTE_SHARE_PRICE`. -- **Shared-liquidity** — `computeVaultV1Reallocations` builds PublicAllocator V1 reallocations for a borrow/withdraw; `computeReallocations` remains its deprecated compatibility alias. `computeVaultV2Reallocations` plans BluePublicAllocator reallocations and applies the configured WAD-scaled penalty threshold in both discovery phases. `getSupplyTargetUtilization(marketId, options)` resolves the per-market → default → `DEFAULT_SUPPLY_TARGET_UTILIZATION` supply target for V1. Read-only liquidity metrics live on the corresponding versioned reallocation-data entity, not in this layer. +- **Shared-liquidity** — `computeVaultV1Reallocations` builds PublicAllocator V1 reallocations for a borrow/withdraw; `computeReallocations` remains its deprecated compatibility alias. Vault V2 planning and state transitions live on `VaultV2ReallocationData`; the standalone `computeVaultV2Reallocations` export is a compatibility wrapper from the entity module, not a helper-layer dependency. `getSupplyTargetUtilization(marketId, options)` resolves the per-market → default → `DEFAULT_SUPPLY_TARGET_UTILIZATION` supply target for V1. Read-only liquidity metrics live on the corresponding versioned reallocation-data entity, not in this layer. - **Metadata** — `addTransactionMetadata(tx, metadata)` appends hex-encoded analytics bytes to `tx.data`: an optional 4-byte unix timestamp followed by a 4-byte origin (timestamp is omitted when `metadata.timestamp` is falsy). Callers gate on `metadata` being provided; the helper itself is a no-op when `tx.data` is empty. ## Constants diff --git a/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts b/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts deleted file mode 100644 index 39fb333d3..000000000 --- a/packages/morpho-sdk/src/helpers/computeVaultV2Reallocations.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { type MarketId, MarketUtils, MathLib } from "@morpho-org/blue-sdk"; -import type { VaultV2ReallocationData } from "../entities/vaultV2ReallocationData.js"; -import { - InsufficientSharedLiquidityError, - ReallocationWithdrawExceedsMarketSupplyError, - type VaultV2BluePublicAllocatorOptions, - type VaultV2BlueReallocation, -} from "../types/index.js"; -import { DEFAULT_SUPPLY_TARGET_UTILIZATION } from "./constant.js"; - -/** - * Computes action-ready Vault V2 BluePublicAllocator reallocations for a Blue - * borrow or loan-asset withdraw. - * - * The planner first uses the friendly 90% source-utilization ceiling, then - * retries from the friendly post-state with a 100% ceiling when the operation - * would otherwise remain illiquid. It refuses fee-bearing partial plans that - * cannot cover the operation's absolute shortfall. - * - * @param params.reallocationData - Vault V2 reallocation state fetched at one block. - * @param params.marketId - Target Blue market id. - * @param params.operation - Operation driving the reallocation. - * @param params.amount - Borrow or withdraw amount. - * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. - * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. - * @throws {@link InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. - * @throws {@link ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. - * @example - * ```ts - * import { computeVaultV2Reallocations } from "@morpho-org/morpho-sdk"; - * - * const reallocations = computeVaultV2Reallocations({ - * reallocationData, - * marketId, - * operation: "borrow", - * amount: 1_000_000n, - * options: { timestamp }, - * }); - * ``` - */ -export const computeVaultV2Reallocations = ({ - reallocationData: data, - marketId, - operation, - amount, - options, -}: { - readonly reallocationData: VaultV2ReallocationData; - readonly marketId: MarketId; - readonly operation: "borrow" | "withdraw"; - readonly amount: bigint; - readonly options?: VaultV2BluePublicAllocatorOptions; -}): readonly VaultV2BlueReallocation[] => { - if (options?.enabled === false) return []; - - const market = data.getMarket(marketId).accrueInterest(options?.timestamp); - if (operation === "withdraw" && amount > market.totalSupplyAssets) { - throw new ReallocationWithdrawExceedsMarketSupplyError({ - marketId, - withdrawAmount: amount, - totalSupplyAssets: market.totalSupplyAssets, - }); - } - - const newTotalBorrowAssets = - operation === "borrow" - ? market.totalBorrowAssets + amount - : market.totalBorrowAssets; - const newTotalSupplyAssets = - operation === "withdraw" - ? market.totalSupplyAssets - amount - : market.totalSupplyAssets; - - if ( - MarketUtils.getUtilization({ - totalSupplyAssets: newTotalSupplyAssets, - totalBorrowAssets: newTotalBorrowAssets, - }) <= DEFAULT_SUPPLY_TARGET_UTILIZATION - ) - return []; - - let requiredAssets = - MathLib.wDivUp(newTotalBorrowAssets, DEFAULT_SUPPLY_TARGET_UTILIZATION) - - newTotalSupplyAssets; - - const friendly = data.computeVaultV2Reallocations(marketId, options); - const discovered = [...friendly.reallocations]; - const friendlyMarket = friendly.data.getMarket(marketId); - const friendlyBorrow = - operation === "borrow" - ? friendlyMarket.totalBorrowAssets + amount - : friendlyMarket.totalBorrowAssets; - const friendlySupply = - operation === "withdraw" - ? friendlyMarket.totalSupplyAssets - amount - : friendlyMarket.totalSupplyAssets; - - if (friendlyBorrow > friendlySupply) { - requiredAssets = newTotalBorrowAssets - newTotalSupplyAssets; - discovered.push( - ...friendly.data._computeVaultV2Reallocations({ - marketId, - maxWithdrawalUtilization: MathLib.WAD, - options, - }).reallocations, - ); - } - - if (requiredAssets <= 0n) return []; - - const absoluteShortfall = - newTotalBorrowAssets > newTotalSupplyAssets - ? newTotalBorrowAssets - newTotalSupplyAssets - : 0n; - const reallocations: VaultV2BlueReallocation[] = []; - let remainingRequiredAssets = requiredAssets; - let totalReallocated = 0n; - - for (const reallocation of discovered) { - const assets = MathLib.min(reallocation.assets, remainingRequiredAssets); - if (assets <= 0n) continue; - - reallocations.push({ ...reallocation, assets }); - remainingRequiredAssets -= assets; - totalReallocated += assets; - if (remainingRequiredAssets === 0n) break; - } - - if (totalReallocated < absoluteShortfall) { - throw new InsufficientSharedLiquidityError({ - marketId, - shortfall: absoluteShortfall, - available: totalReallocated, - }); - } - - return reallocations; -}; diff --git a/packages/morpho-sdk/src/helpers/index.ts b/packages/morpho-sdk/src/helpers/index.ts index 07001fbda..668e8aa44 100644 --- a/packages/morpho-sdk/src/helpers/index.ts +++ b/packages/morpho-sdk/src/helpers/index.ts @@ -2,7 +2,6 @@ export { computeReallocations, computeVaultV1Reallocations, } from "./computeVaultV1Reallocations.js"; -export { computeVaultV2Reallocations } from "./computeVaultV2Reallocations.js"; export { APPROVE_ONLY_ONCE_TOKENS, DEFAULT_LLTV_BUFFER, diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index a7b635291..642b1848b 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -26,6 +26,7 @@ import { ExcessiveSlippageToleranceError, InconsistentReallocationPenaltyError, InputExceedsMaxError, + InvalidReallocationAddressError, InvalidReallocationSourceTypeError, InvalidReallocationTypeError, MarketIdMismatchError, @@ -616,6 +617,11 @@ describe("validateReallocations", () => { reallocation: { ...validBluePublicAllocatorReallocation, assets: 0n }, ErrorClass: NonPositiveInputError, }, + { + name: "negative assets", + reallocation: { ...validBluePublicAllocatorReallocation, assets: -1n }, + ErrorClass: NonPositiveInputError, + }, { name: "uint128 asset overflow", reallocation: { @@ -645,6 +651,20 @@ describe("validateReallocations", () => { ).toThrow(InconsistentReallocationPenaltyError); }); + test("behavior: accepts the maximum uint128 asset amount", () => { + expect(() => + validateReallocations( + [ + { + ...validBluePublicAllocatorReallocation, + assets: maxUint128, + }, + ], + targetMarketId, + ), + ).not.toThrow(); + }); + test("behavior: allows different penalties for different allocator-vault pairs", () => { expect(() => validateReallocations( @@ -679,7 +699,7 @@ describe("validateReallocations", () => { ).toThrow(ReallocationWithdrawalOnTargetMarketError); }); - test("behavior: allows the target market through a different Vault V2 adapter", () => { + test("error: target market through a different Vault V2 adapter", () => { expect(() => validateReallocations( [ @@ -694,7 +714,48 @@ describe("validateReallocations", () => { ], targetMarketId, ), - ).not.toThrow(); + ).toThrow(ReallocationWithdrawalOnTargetMarketError); + }); + + test("error: target market supplied as plain market params", () => { + const plainMarketParams = { + loanToken: marketParams.loanToken, + collateralToken: marketParams.collateralToken, + oracle: marketParams.oracle, + irm: marketParams.irm, + lltv: marketParams.lltv, + }; + const reallocation = { + ...validBluePublicAllocatorReallocation, + from: { + type: "market", + adapter: USER_A, + marketParams: plainMarketParams, + }, + } as unknown as BlueReallocation; + + expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( + ReallocationWithdrawalOnTargetMarketError, + ); + }); + + test.each([ + { name: "missing allocator", overrides: { allocator: undefined } }, + { name: "invalid vault", overrides: { vault: "not-an-address" } }, + { name: "missing target", overrides: { to: undefined } }, + { + name: "invalid target adapter", + overrides: { to: { adapter: "not-an-address" } }, + }, + ])("error: InvalidReallocationAddressError for $name", ({ overrides }) => { + const reallocation = { + ...validBluePublicAllocatorReallocation, + ...overrides, + } as unknown as BlueReallocation; + + expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( + InvalidReallocationAddressError, + ); }); test("error: InvalidReallocationSourceTypeError", () => { @@ -708,6 +769,35 @@ describe("validateReallocations", () => { ); }); + test.each([ + { name: "missing source", from: undefined }, + { name: "null source", from: null }, + { + name: "missing market params", + from: { type: "market", adapter: USER_A }, + }, + ])("error: InvalidReallocationSourceTypeError for $name", ({ from }) => { + const reallocation = { + ...validBluePublicAllocatorReallocation, + from, + } as unknown as BlueReallocation; + + expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( + InvalidReallocationSourceTypeError, + ); + }); + + test("error: InvalidReallocationAddressError for missing source adapter", () => { + const reallocation = { + ...validBluePublicAllocatorReallocation, + from: { type: "market", marketParams: sourceMarketA }, + } as unknown as BlueReallocation; + + expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( + InvalidReallocationAddressError, + ); + }); + test.each([ { name: "unknown top-level discriminator", diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index 6feeed17c..302b14f30 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -2,12 +2,13 @@ import { type AccrualPosition, getChainAddresses, type MarketId, + MarketUtils, MathLib, ORACLE_PRICE_SCALE, } from "@morpho-org/blue-sdk"; import type { MarketInput as MidnightMarketInput } from "@morpho-org/midnight-sdk"; import { isDefined } from "@morpho-org/morpho-ts"; -import { type Address, isAddressEqual, maxUint128 } from "viem"; +import { type Address, isAddress, isAddressEqual, maxUint128 } from "viem"; import { AccrualPositionUserMismatchError, AddressMismatchError, @@ -19,6 +20,7 @@ import { ExcessiveSlippageToleranceError, InconsistentReallocationPenaltyError, InputExceedsMaxError, + InvalidReallocationAddressError, InvalidReallocationSourceTypeError, InvalidReallocationTypeError, MarketIdMismatchError, @@ -338,20 +340,21 @@ export const validateRepayShares = (params: { * * BluePublicAllocator entries enforce a WAD-bounded `penalty`, one consistent * penalty per allocator-vault pair, positive `uint128`-bounded `assets`, and a - * market source distinct from the target adapter-market pair. Idle sources + * market source distinct from the target market. Idle sources * have no market or sorting rule. * * @param reallocations - The reallocations to validate. - * @param targetMarketId - The operation's target market ID. V1 withdrawals cannot reference it; V2 sources cannot reference it through their target adapter. + * @param targetMarketId - The operation's target market ID. Neither V1 nor V2 sources can reference it. * @returns Nothing when every reallocation is valid. * @throws {NegativeInputError} when a reallocation fee is negative. * @throws {EmptyReallocationWithdrawalsError} when a reallocation has no withdrawals. * @throws {NonPositiveInputError} when a withdrawal or BluePublicAllocator asset amount is non-positive. * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source discriminator is unknown. + * @throws {InvalidReallocationAddressError} when a BluePublicAllocator identity or adapter address is malformed. + * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. - * @throws {ReallocationWithdrawalOnTargetMarketError} when a V1 source references the target market or a V2 source references its target adapter-market pair. + * @throws {ReallocationWithdrawalOnTargetMarketError} when a V1 or V2 source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when withdrawals are not strictly market-id sorted. * @example * ```ts @@ -370,10 +373,48 @@ export const validateReallocations = ( for (const r of reallocations) { if (r.type === "bluePublicAllocator") { - const sourceType: string = r.from.type; + if (typeof r.allocator !== "string" || !isAddress(r.allocator)) { + throw new InvalidReallocationAddressError("allocator"); + } + if (typeof r.vault !== "string" || !isAddress(r.vault)) { + throw new InvalidReallocationAddressError("vault"); + } + if ( + r.to == null || + typeof r.to.adapter !== "string" || + !isAddress(r.to.adapter) + ) { + throw new InvalidReallocationAddressError("to.adapter"); + } + + const source = r.from; + if (source == null) { + throw new InvalidReallocationSourceTypeError(undefined); + } + const sourceType: string | undefined = source.type; if (sourceType !== "market" && sourceType !== "idle") { throw new InvalidReallocationSourceTypeError(sourceType); } + let sourceMarketId: MarketId | undefined; + if (source.type === "market") { + if (typeof source.adapter !== "string" || !isAddress(source.adapter)) { + throw new InvalidReallocationAddressError("from.adapter"); + } + if ( + source.marketParams == null || + !isAddress(source.marketParams.loanToken) || + !isAddress(source.marketParams.collateralToken) || + !isAddress(source.marketParams.oracle) || + !isAddress(source.marketParams.irm) || + typeof source.marketParams.lltv !== "bigint" + ) { + throw new InvalidReallocationSourceTypeError( + "market", + "marketParams", + ); + } + sourceMarketId = MarketUtils.getMarketId(source.marketParams); + } if (r.penalty < 0n) { throw new NegativeInputError("reallocation.penalty", r.penalty); } @@ -408,13 +449,12 @@ export const validateReallocations = ( penaltyByAllocatorVault.set(penaltyKey, r.penalty); if ( - r.from.type === "market" && - r.from.marketParams.id === targetMarketId && - isAddressEqual(r.from.adapter, r.to.adapter) + sourceMarketId !== undefined && + compareMarketIds(sourceMarketId, targetMarketId) === 0 ) { throw new ReallocationWithdrawalOnTargetMarketError( r.vault, - r.from.marketParams.id, + sourceMarketId, ); } continue; diff --git a/packages/morpho-sdk/src/index.test.ts b/packages/morpho-sdk/src/index.test.ts new file mode 100644 index 000000000..dda7444f3 --- /dev/null +++ b/packages/morpho-sdk/src/index.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, test } from "vitest"; +import { computeVaultV2Reallocations } from "./index.js"; + +describe("package root exports", () => { + test("exports computeVaultV2Reallocations", () => { + expect(computeVaultV2Reallocations).toBeTypeOf("function"); + }); +}); diff --git a/packages/morpho-sdk/src/index.ts b/packages/morpho-sdk/src/index.ts index 4630402d8..2f0e88d40 100644 --- a/packages/morpho-sdk/src/index.ts +++ b/packages/morpho-sdk/src/index.ts @@ -1,4 +1,5 @@ export * from "./actions/index.js"; export * from "./client/index.js"; +export { computeVaultV2Reallocations } from "./entities/vaultV2ReallocationData.js"; export * from "./helpers/index.js"; export * from "./types/index.js"; diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index be6fc5d4b..398118b46 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -29,7 +29,7 @@ One class per error case. Never throw a generic `Error` from SDK source. - **Generic input bounds:** `NegativeInputError` for values that must be non-negative, `NonPositiveInputError` for values that must be positive, and `InputExceedsMaxError` for protocol upper bounds such as BluePublicAllocator's `uint128` assets and WAD-scaled `uint64` penalty. All expose the invalid `field` and `value`; reuse them across Vault, Blue, and Midnight instead of adding operation-specific scalar-bound errors. - **Market-specific:** `BorrowExceedsSafeLtvError`, `MissingMarketPriceError`, `NativeAmountOnNonWNativeAssetError`, `MutuallyExclusiveWithdrawAmountsError`, `WithdrawExceedsSupplyError`, `WithdrawSharesExceedSupplyError`. -- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationTypeError` for an unknown top-level Public Allocator variant, `InvalidReallocationSourceTypeError` for an unknown BluePublicAllocator source, `InconsistentReallocationPenaltyError` for conflicting penalties on one allocator-vault pair, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. +- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationTypeError` for an unknown top-level Public Allocator variant, `InvalidReallocationAddressError` for malformed BluePublicAllocator identity or adapter addresses, `InvalidReallocationSourceTypeError` for an absent, incomplete, or unknown BluePublicAllocator source, `InconsistentReallocationPenaltyError` for conflicting penalties on one allocator-vault pair, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. ## Adding a new operation diff --git a/packages/morpho-sdk/src/types/action.ts b/packages/morpho-sdk/src/types/action.ts index 43142b337..ae120746f 100644 --- a/packages/morpho-sdk/src/types/action.ts +++ b/packages/morpho-sdk/src/types/action.ts @@ -171,7 +171,7 @@ export interface BlueWithdrawAction /** Native-token fees paid to PublicAllocator V1. */ reallocationFee: bigint; /** Loan-token assets donated as BluePublicAllocator V2 penalties. */ - reallocationPenaltyAssets: bigint; + readonly reallocationPenaltyAssets: bigint; } > {} @@ -197,7 +197,7 @@ export interface BlueBorrowAction /** Native-token fees paid to PublicAllocator V1. */ reallocationFee: bigint; /** Loan-token assets donated as BluePublicAllocator V2 penalties. */ - reallocationPenaltyAssets: bigint; + readonly reallocationPenaltyAssets: bigint; } > {} @@ -215,7 +215,7 @@ export interface BlueSupplyCollateralBorrowAction /** Native-token fees paid to PublicAllocator V1. */ reallocationFee: bigint; /** Loan-token assets donated as BluePublicAllocator V2 penalties. */ - reallocationPenaltyAssets: bigint; + readonly reallocationPenaltyAssets: bigint; } > {} diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 43d2fc992..7d7327d22 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -494,6 +494,31 @@ export namespace BundlerErrors { super(`unexpected signature authorizing "${authorized}"`); } } + + /** + * Thrown when a skippable Blue Public Allocator call would leave a usable + * token allowance behind after the allocator call reverts. + * + * @example + * ```ts + * import { BundlerErrors } from "@morpho-org/morpho-sdk"; + * + * if (error instanceof BundlerErrors.SkippableAllocatorPenalty) { + * // Rebuild the allocator call with skipRevert set to false. + * } + * ``` + */ + export class SkippableAllocatorPenalty extends Error { + /** + * @param penaltyAssets - Exact token amount approved to the allocator. + */ + public constructor(public readonly penaltyAssets: bigint) { + super( + `Blue Public Allocator calls with penalty assets cannot skip reverts. Rebuild with skipRevert false for penalty amount "${penaltyAssets}".`, + ); + this.name = "SkippableAllocatorPenalty"; + } + } } /** Requirement signature kind accepted by action-output transaction builders. */ @@ -789,7 +814,7 @@ export class EmptyReallocationWithdrawalsError extends Error { } } -/** Thrown when a V1 withdrawal references the target market or a V2 source references its exact target adapter-market pair. */ +/** Thrown when a Public Allocator source references the target Blue market. */ export class ReallocationWithdrawalOnTargetMarketError extends Error { constructor(vault: string, marketId: string) { super( @@ -824,7 +849,38 @@ export class InvalidReallocationTypeError extends Error { } /** - * Thrown when a Blue Public Allocator source has an unknown discriminator. + * Thrown when a Blue Public Allocator reallocation contains a malformed + * identity or adapter address. + * + * @example + * ```ts + * import { InvalidReallocationAddressError } from "@morpho-org/morpho-sdk"; + * + * const error = new InvalidReallocationAddressError("to.adapter"); + * if (error instanceof InvalidReallocationAddressError) { + * console.error(error.field); + * } + * ``` + */ +export class InvalidReallocationAddressError extends Error { + /** + * @param field - Reallocation address field that is absent or malformed. + */ + public constructor( + public readonly field: + | "allocator" + | "vault" + | "from.adapter" + | "to.adapter", + ) { + super(`Reallocation "${field}" must be a valid address.`); + this.name = "InvalidReallocationAddressError"; + } +} + +/** + * Thrown when a Blue Public Allocator source is absent, incomplete, or has an + * unknown discriminator. * * @example * ```ts @@ -835,11 +891,20 @@ export class InvalidReallocationTypeError extends Error { */ export class InvalidReallocationSourceTypeError extends Error { /** - * @param sourceType - Invalid runtime value received for `reallocation.from.type`. + * @param sourceType - Invalid runtime value received for `reallocation.from.type`, + * or `undefined` when the source or discriminator is absent. + * @param missingField - Required market-source field that is absent or malformed. */ - public constructor(public readonly sourceType: string) { + public constructor( + public readonly sourceType: string | undefined, + public readonly missingField?: "adapter" | "marketParams", + ) { super( - `Reallocation source type must be "market" or "idle", got "${sourceType}".`, + missingField == null + ? sourceType === undefined + ? 'Reallocation source must specify type "market" or "idle".' + : `Reallocation source type must be "market" or "idle", got "${sourceType}".` + : `Reallocation market source must include a valid "${missingField}".`, ); this.name = "InvalidReallocationSourceTypeError"; } @@ -1021,8 +1086,8 @@ export class DisabledReallocationMarketError extends Error { } /** - * Thrown when shared liquidity selected by `computeVaultV1Reallocations` cannot cover - * the operation's absolute shortfall on the target market — the resulting + * Thrown when shared liquidity selected by a Vault V1 or Vault V2 reallocation planner cannot + * cover the operation's absolute shortfall on the target market — the resulting * `morphoBorrow` or `morphoWithdraw` would still revert onchain. * * Pattern-match on the class and inspect `params` to surface the gap to users. @@ -1416,7 +1481,7 @@ export class WithdrawSharesExceedSupplyError extends Error { } /** - * Thrown when `computeVaultV1Reallocations` is called with a withdraw `amount` greater + * Thrown when a Vault V1 or Vault V2 reallocation planner receives a withdraw `amount` greater * than the target market's current `totalSupplyAssets` — the post-withdraw * supply would be negative, making the on-chain `morphoWithdraw` revert * regardless of any reallocation. Caught here so callers do not pay diff --git a/packages/morpho-sdk/src/utils.ts b/packages/morpho-sdk/src/utils.ts index 53bc46a56..4f38bdfb6 100644 --- a/packages/morpho-sdk/src/utils.ts +++ b/packages/morpho-sdk/src/utils.ts @@ -64,11 +64,11 @@ export { transformValue, values, } from "@morpho-org/morpho-ts"; +export { computeVaultV2Reallocations } from "./entities/vaultV2ReallocationData.js"; export { computeReallocations, computeVaultV1Reallocations, } from "./helpers/computeVaultV1Reallocations.js"; -export { computeVaultV2Reallocations } from "./helpers/computeVaultV2Reallocations.js"; export { addTransactionMetadata } from "./helpers/metadata.js"; export { computeMaxRepaySharePrice, diff --git a/packages/morpho-sdk/test/fixtures/BluePublicAllocatorWriteFixture.ts b/packages/morpho-sdk/test/fixtures/BluePublicAllocatorWriteFixture.ts new file mode 100644 index 000000000..e378834ac --- /dev/null +++ b/packages/morpho-sdk/test/fixtures/BluePublicAllocatorWriteFixture.ts @@ -0,0 +1,429 @@ +/** @internal Test-only `BluePublicAllocatorWriteFixture` contract ABI. */ +export const abi = [ + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bytes32", + name: "id", + type: "bytes32", + }, + ], + name: "absoluteCap", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + components: [ + { + internalType: "address", + name: "loanToken", + type: "address", + }, + { + internalType: "address", + name: "collateralToken", + type: "address", + }, + { + internalType: "address", + name: "oracle", + type: "address", + }, + { + internalType: "address", + name: "irm", + type: "address", + }, + { + internalType: "uint256", + name: "lltv", + type: "uint256", + }, + ], + internalType: "struct MarketParams", + name: "marketParams", + type: "tuple", + }, + { + internalType: "uint128", + name: "assets", + type: "uint128", + }, + { + internalType: "uint64", + name: "penalty", + type: "uint64", + }, + ], + name: "allocateFromIdle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bytes32", + name: "id", + type: "bytes32", + }, + ], + name: "canPullFromMarket", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + ], + name: "isActiveAdapter", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "deallocateAdapter", + type: "address", + }, + { + components: [ + { + internalType: "address", + name: "loanToken", + type: "address", + }, + { + internalType: "address", + name: "collateralToken", + type: "address", + }, + { + internalType: "address", + name: "oracle", + type: "address", + }, + { + internalType: "address", + name: "irm", + type: "address", + }, + { + internalType: "uint256", + name: "lltv", + type: "uint256", + }, + ], + internalType: "struct MarketParams", + name: "deallocateMarketParams", + type: "tuple", + }, + { + internalType: "address", + name: "allocateAdapter", + type: "address", + }, + { + components: [ + { + internalType: "address", + name: "loanToken", + type: "address", + }, + { + internalType: "address", + name: "collateralToken", + type: "address", + }, + { + internalType: "address", + name: "oracle", + type: "address", + }, + { + internalType: "address", + name: "irm", + type: "address", + }, + { + internalType: "uint256", + name: "lltv", + type: "uint256", + }, + ], + internalType: "struct MarketParams", + name: "allocateMarketParams", + type: "tuple", + }, + { + internalType: "uint128", + name: "assets", + type: "uint128", + }, + { + internalType: "uint64", + name: "penalty", + type: "uint64", + }, + ], + name: "reallocate", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + components: [ + { + internalType: "address", + name: "loanToken", + type: "address", + }, + { + internalType: "address", + name: "collateralToken", + type: "address", + }, + { + internalType: "address", + name: "oracle", + type: "address", + }, + { + internalType: "address", + name: "irm", + type: "address", + }, + { + internalType: "uint256", + name: "lltv", + type: "uint256", + }, + ], + internalType: "struct MarketParams", + name: "marketParams", + type: "tuple", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "setAbsoluteCap", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bool", + name: "value", + type: "bool", + }, + ], + name: "setCanPullFromIdle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + components: [ + { + internalType: "address", + name: "loanToken", + type: "address", + }, + { + internalType: "address", + name: "collateralToken", + type: "address", + }, + { + internalType: "address", + name: "oracle", + type: "address", + }, + { + internalType: "address", + name: "irm", + type: "address", + }, + { + internalType: "uint256", + name: "lltv", + type: "uint256", + }, + ], + internalType: "struct MarketParams", + name: "marketParams", + type: "tuple", + }, + { + internalType: "bool", + name: "value", + type: "bool", + }, + ], + name: "setCanPullFromMarket", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + internalType: "bool", + name: "value", + type: "bool", + }, + ], + name: "setIsActiveAdapter", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "uint64", + name: "value", + type: "uint64", + }, + ], + name: "setPenalty", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + ], + name: "vaultData", + outputs: [ + { + internalType: "bool", + name: "canPullFromIdle", + type: "bool", + }, + { + internalType: "uint64", + name: "penalty", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +/** @internal Test-only `BluePublicAllocatorWriteFixture` contract bytecode. */ +export const code = + "0x6080806040523460155761104e908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f905f3560e01c90816308f804d814610b4a575080635e0deb5414610a9e57806366faa83914610a4757806369f1e26b146109fe57806377b0aab1146109165780638aeed1d1146108565780638fdaa1a7146104d85780639a8a6795146104465780639a8b594414610391578063d72ff79a146103415763df31d68814610097575f80fd5b3461033e5761012036600319011261033e576100b1610b88565b6100b9610b9e565b9060a03660431901126102ac5760e4356001600160801b0381169182820361033c576101043567ffffffffffffffff8116809103610338576001600160a01b038216808752600360205260408720549094906101239060081c67ffffffffffffffff168314610c51565b604435926001600160a01b038416840361033457610142933390610ec6565b81845260026020526040842060018060a01b0384165f5260205260ff60405f205416156102fc57818452600360205260ff604085205416156102bf57839061018984610e3e565b9383835282602052604083208584526020526101aa60408420541515610c91565b6040516101b960208201610cd1565b60a081526101c860c082610bc8565b843b156102bb5783916101ef6040519485938493635c9ce04d60e01b855260048501610dae565b038183875af180156102b057610297575b505060405163c69507dd60e01b81526004810183905291602083602481855afa91821561028c578492610252575b61024f93508452836020526040842090845260205260408320541015610dfa565b80f35b91506020833d602011610284575b8161026d60209383610bc8565b810103126102805761024f92519161022e565b5f80fd5b3d9150610260565b6040513d86823e3d90fd5b816102a191610bc8565b6102ac57825f610200565b8280fd5b6040513d84823e3d90fd5b8380fd5b60405162461bcd60e51b815260206004820152601560248201527463616e6e6f742070756c6c2066726f6d2069646c6560581b6044820152606490fd5b60405162461bcd60e51b815260206004820152601060248201526f34b730b1ba34bb329030b230b83a32b960811b6044820152606490fd5b8780fd5b8580fd5b845b80fd5b503461033e57602036600319011261033e5760409081906001600160a01b03610368610b88565b1681526003602052205467ffffffffffffffff82519160ff81161515835260081c166020820152f35b503461033e57604036600319011261033e576103ab610b88565b6024359081151582036102ac576040516326f6f90760e11b815233600482015291906001600160a01b0316602083602481845afa92831561028c5761024f936103fb918691610417575b50610c16565b83526003602052604083209060ff801983541691151516179055565b610439915060203d60201161043f575b6104318183610bc8565b810190610bfe565b5f6103f5565b503d610427565b503461033e5761010036600319011261033e57610461610b88565b610469610b9e565b9060a03660431901126102ac576040516326f6f90760e11b81523360048201526001600160a01b039190911690602081602481855afa801561028c576104b59185916104175750610c16565b8252816020526104c86040832091610e3e565b825260205260e435604082205580f35b5034610280576101e0366003190112610280576104f3610b88565b6104fb610b9e565b9060a03660431901126102805760e4356001600160a01b038116908181036102805760a036610103190112610280576101a435916001600160801b038316808403610280576101c4359167ffffffffffffffff83168093036102805760018060a01b03861695865f5260036020526105858467ffffffffffffffff60405f205460081c1614610c51565b61010435936001600160a01b03851693848603610280576105a7923387610ec6565b855f52600260205260405f2060018060a01b0388165f5260205260ff60405f2054161561081157855f52600260205260405f20815f5260205260ff60405f205416156107cc576105f687610e3e565b865f52600160205260405f20905f5260205260ff60405f2054161561078757604051602081019160e08352601161010083015270746869732f6d61726b6574506172616d7360781b610120830152604082015261065860608201610104610d40565b610120815261066961014082610bc8565b51902095855f525f60205260405f20875f5260205261068d60405f20541515610c91565b6040519061069d60208301610cd1565b60a082526106ac60c083610bc8565b863b1561028057604051632590ce8b60e11b8152915f91839182916106d6918a9160048501610dae565b0381838a5af1801561077c57610761575b5060405160208101919091528693929150610124356001600160a01b0381169081900361033c576040820152610144356001600160a01b0381169081900361033c576060820152610164356001600160a01b0381169081900361033c5760808201526101843560a082015260a081526101c860c082610bc8565b6107719194939297505f90610bc8565b5f959091925f6106e7565b6040513d5f823e3d90fd5b60405162461bcd60e51b815260206004820152601760248201527f63616e6e6f742070756c6c2066726f6d206d61726b65740000000000000000006044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f696e6163746976652074617267657420616461707465720000000000000000006044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f696e61637469766520736f7572636520616461707465720000000000000000006044820152606490fd5b346102805760603660031901126102805761086f610b88565b610877610b9e565b90604435908115158203610280576040516326f6f90760e11b815233600482015292906001600160a01b0316602084602481845afa93841561077c576108f5946108c7915f916108f75750610c16565b5f52600260205260405f209060018060a01b03165f5260205260405f209060ff801983541691151516179055565b005b610910915060203d60201161043f576104318183610bc8565b866103f5565b346102805760403660031901126102805761092f610b88565b6024359067ffffffffffffffff821690818303610280576040516326f6f90760e11b81523360048201526001600160a01b039190911691602082602481865afa91821561077c57670de0b6b3a764000092610990915f916108f75750610c16565b116109c6575f52600360205260405f209068ffffffffffffffff0082549160081b169068ffffffffffffffff0019161790555f80f35b60405162461bcd60e51b815260206004820152601060248201526f0e0cadcc2d8e8f240e8dede40d0d2ced60831b6044820152606490fd5b34610280576040366003190112610280576001600160a01b03610a1f610b88565b165f52600160205260405f206024355f52602052602060ff60405f2054166040519015158152f35b3461028057604036600319011261028057610a60610b88565b610a68610b9e565b9060018060a01b03165f52600260205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b346102805761010036600319011261028057610ab8610b88565b610ac0610b9e565b9060a03660431901126102805760e435908115158203610280576040516326f6f90760e11b815233600482015292906001600160a01b0316602084602481845afa93841561077c576108f594610b1c915f916108f75750610c16565b5f526001602052610b3060405f2091610e3e565b5f5260205260405f209060ff801983541691151516179055565b34610280576040366003190112610280576020906001600160a01b03610b6e610b88565b165f525f825260405f206024355f52825260405f20548152f35b600435906001600160a01b038216820361028057565b602435906001600160a01b038216820361028057565b35906001600160a01b038216820361028057565b90601f8019910116810190811067ffffffffffffffff821117610bea57604052565b634e487b7160e01b5f52604160045260245ffd5b90816020910312610280575180151581036102805790565b15610c1d57565b60405162461bcd60e51b815260206004820152600c60248201526b1d5b985d5d1a1bdc9a5e995960a21b6044820152606490fd5b15610c5857565b60405162461bcd60e51b8152602060048201526011602482015270696e636f72726563742070656e616c747960781b6044820152606490fd5b15610c9857565b60405162461bcd60e51b815260206004820152601160248201527007a65726f206162736f6c7574652063617607c1b6044820152606490fd5b6044356001600160a01b038116908190036102805781526064356001600160a01b038116908190036102805760208201526084356001600160a01b0381169081900361028057604082015260a4356001600160a01b03811690819003610280576060820152608060c435910152565b60809081906001600160a01b03610d5682610bb4565b1684526001600160a01b03610d6d60208301610bb4565b1660208501526001600160a01b03610d8760408301610bb4565b1660408501526001600160a01b03610da160608301610bb4565b1660608501520135910152565b91608060206001600160801b039260409497969760018060a01b031686526060828701528051918291826060890152018387015e5f828287010152601f80199101168401019416910152565b15610e0157565b60405162461bcd60e51b815260206004820152601560248201527418589cdbdb1d5d194818d85c08195e18d959591959605a1b6044820152606490fd5b604051602081019160e08352601161010083015270746869732f6d61726b6574506172616d7360781b61012083015260018060a01b03166040820152610e88606082016044610d40565b6101208152610e9961014082610bc8565b51902090565b81810292918115918404141715610eb257565b634e487b7160e01b5f52601160045260245ffd5b91929093610ed48183610e9f565b610fe85750505f925b8315610fe2576040516323b872dd60e01b602082019081526001600160a01b0392831660248301529190931660448401526064808401949094529282525f9283928390610f2b608482610bc8565b51925af13d15610fdb573d67ffffffffffffffff8111610bea5760405190610f5d601f8201601f191660200183610bc8565b81523d5f602083013e5b81610fac575b5015610f7557565b60405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606490fd5b8051801592508215610fc1575b50505f610f6d565b610fd49250602080918301019101610bfe565b5f80610fb9565b6060610f67565b50505050565b610ff191610e9f565b5f198101908111610eb257670de0b6b3a7640000900460018101809111610eb25792610edd56fea264697066735822122070f851fe2a72e5c001f93dd70c5380fb09d85e34c5bf026e9cbd8e46d85451e864736f6c63430008240033"; diff --git a/packages/morpho-sdk/test/helpers/vaultV2.ts b/packages/morpho-sdk/test/helpers/vaultV2.ts index e4f8ba14c..60551f090 100644 --- a/packages/morpho-sdk/test/helpers/vaultV2.ts +++ b/packages/morpho-sdk/test/helpers/vaultV2.ts @@ -1,7 +1,18 @@ import { getChainAddresses } from "@morpho-org/blue-sdk"; -import { vaultV2FactoryAbi } from "@morpho-org/blue-sdk-viem"; +import { + morphoMarketV1AdapterV2FactoryAbi, + vaultV2Abi, + vaultV2FactoryAbi, +} from "@morpho-org/blue-sdk-viem"; import type { AnvilTestClient } from "@morpho-org/test"; -import { type Address, decodeEventLog, parseEventLogs, toHex } from "viem"; +import { + type Address, + decodeEventLog, + type Hex, + parseEther, + parseEventLogs, + toHex, +} from "viem"; export async function createVaultV2(params: { client: AnvilTestClient; @@ -46,3 +57,74 @@ export async function createVaultV2(params: { return { address: vaultAddress }; } + +export const submitAndAcceptVaultV2Call = async ( + client: AnvilTestClient, + params: { readonly vault: Address; readonly data: Hex }, +) => { + const { vault, data } = params; + await client.writeContract({ + address: vault, + abi: vaultV2Abi, + functionName: "submit", + args: [data], + }); + const hash = await client.sendTransaction({ to: vault, data }); + await client.waitForTransactionReceipt({ hash }); +}; + +export const deployVaultV2 = async ( + client: AnvilTestClient, + asset: Address, +) => { + await client.deal({ amount: parseEther("1") }); + const { address: vault } = await createVaultV2({ + client, + asset, + chainId: client.chain.id, + }); + await client.writeContract({ + address: vault, + abi: vaultV2Abi, + functionName: "setCurator", + args: [client.account.address], + }); + + return vault; +}; + +export const deployMorphoMarketV1AdapterV2 = async ( + client: AnvilTestClient, + vault: Address, +) => { + const { morphoMarketV1AdapterV2Factory } = getChainAddresses(client.chain.id); + const hash = await client.writeContract({ + address: morphoMarketV1AdapterV2Factory!, + abi: morphoMarketV1AdapterV2FactoryAbi, + functionName: "createMorphoMarketV1AdapterV2", + args: [vault], + }); + const receipt = await client.waitForTransactionReceipt({ hash }); + const event = receipt.logs + .map((log) => { + try { + return decodeEventLog({ + abi: morphoMarketV1AdapterV2FactoryAbi, + data: log.data, + topics: log.topics, + }); + } catch { + return undefined; + } + }) + .find( + (candidate) => + candidate?.eventName === "CreateMorphoMarketV1AdapterV2" && + "morphoMarketV1AdapterV2" in candidate.args, + ); + if (event?.eventName !== "CreateMorphoMarketV1AdapterV2") { + throw new Error("No CreateMorphoMarketV1AdapterV2 event found."); + } + + return event.args.morphoMarketV1AdapterV2; +}; diff --git a/packages/wdk-protocol-lending-morpho-evm/README.md b/packages/wdk-protocol-lending-morpho-evm/README.md index 425336244..4f0ff8455 100644 --- a/packages/wdk-protocol-lending-morpho-evm/README.md +++ b/packages/wdk-protocol-lending-morpho-evm/README.md @@ -14,6 +14,7 @@ This module follows Wallet Development Kit lending protocol conventions and acce - Withdraw from Morpho Vaults V2. - Supply and withdraw collateral in Morpho Blue market. - Borrow and repay from a configured Morpho Blue market. +- Opt into Vault V2 BluePublicAllocator reallocations for borrow liquidity. - Expose Morpho SDK approval/signature/authorization requirements. - Quote costs before sending. - Works with standard EVM accounts and ERC-4337 smart accounts. @@ -93,7 +94,7 @@ For vault deposits and collateral supply, pass either `amount`, `nativeAmount`, | `getSupplyCollateralRequirements(options)` | Return SDK requirements for collateral supply | | `quoteSupplyCollateral(options, config?)` | Quote collateral supply | | `borrow(options, config?)` | Borrow from the configured market | -| `getBorrowRequirements(options)` | Return SDK authorization requirements for borrow | +| `getBorrowRequirements(options)` | Return SDK authorization requirements, plus a penalty-token approval for the Vault V2 opt-in type | | `quoteBorrow(options, config?)` | Quote borrow | | `repay(options, config?)` | Repay by assets, or pass `amount: 'max'` to repay current borrow shares | | `getRepayRequirements(options)` | Return SDK requirements for repay | @@ -138,6 +139,32 @@ Requirement entries are one of: Morpho SDK enforces a builder/executor invariant for bundled actions. For that reason, `onBehalfOf` and vault/collateral withdrawal `to` must equal the connected wallet address in this WDK adapter. +Existing `MorphoBorrowOptions` callers keep the Vault V1 reallocation input and +an authorization-only `getBorrowRequirements` result type. To include Vault V2 +BluePublicAllocator calls, type the options as +`MorphoBorrowWithV2ReallocationsOptions`; this explicitly widens the result to +include the loan-token approval used for proportional penalty donations: + +```typescript +import type { MorphoBorrowWithV2ReallocationsOptions } from '@morpho-org/wdk-protocol-lending-morpho-evm' + +const options = { + token: usdc, + amount: 1_000_000n, + reallocations: [{ + allocator, + type: 'bluePublicAllocator', + vault, + from: { type: 'idle' }, + to: { adapter }, + assets: 1_000_000n, + penalty: 1_000_000_000_000_000n + }] +} satisfies MorphoBorrowWithV2ReallocationsOptions + +const requirements = await morpho.getBorrowRequirements(options) +``` + ## Fork E2E Test The regular unit test suite is fully mocked and runs as part of the workspace's `pnpm test` command. The Anvil-fork integration suite under `tests/integration/` is gated on `MAINNET_RPC_URL` being set; the corresponding tests are skipped otherwise. To execute the real vault deposit path against a mainnet fork: diff --git a/packages/wdk-protocol-lending-morpho-evm/src/index.ts b/packages/wdk-protocol-lending-morpho-evm/src/index.ts index 7ba5bcd1c..0f89b5f38 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/index.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/index.ts @@ -32,6 +32,7 @@ export type { Erc4337TransactionConfig, MarketPosition, MorphoBorrowOptions, + MorphoBorrowWithV2ReallocationsOptions, MorphoErc20SupplyOptions, MorphoEvmAccount, MorphoNativeSupplyOptions, diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index acc322cf0..278e56cd1 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -3,7 +3,14 @@ import type { VaultV2BlueReallocation, } from "@morpho-org/morpho-sdk"; import * as viem from "viem"; -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest"; +import type { + MorphoBorrowOptions, + MorphoBorrowWithV2ReallocationsOptions, + RequirementApproval, + RequirementAuthorization, + RequirementSignatureRequest, +} from "./morpho-protocol-evm.js"; const SEED = "cook voyage document eight skate token alien guide drink uncle term abuse"; @@ -551,15 +558,52 @@ describe.sequential("MorphoProtocolEvm", () => { }); test("should return borrow requirements from morpho-sdk", async () => { - const requirements = await protocol.getBorrowRequirements({ + const options = { token: TOKEN, amount: 100_000n, - }); + } satisfies MorphoBorrowOptions; + const promise = protocol.getBorrowRequirements(options); + expectTypeOf(promise).toEqualTypeOf< + Promise<(RequirementAuthorization | RequirementSignatureRequest)[]> + >(); + const requirements = await promise; expect(requirements).toEqual([{ action: { type: "blueAuthorization" } }]); expect(borrowAction.getRequirements).toHaveBeenCalled(); }); + test("types: Vault V2 borrow requirements opt into approval results", async () => { + const options = { + token: TOKEN, + amount: 100_000n, + reallocations: [ + { + allocator: "0x0000000000000000000000000000000000000010", + type: "bluePublicAllocator", + vault: VAULT, + from: { type: "idle" }, + to: { + adapter: "0x0000000000000000000000000000000000000020", + }, + assets: 50_000n, + penalty: 1n, + }, + ], + } satisfies MorphoBorrowWithV2ReallocationsOptions; + + const promise = protocol.getBorrowRequirements(options); + expectTypeOf(promise).toEqualTypeOf< + Promise< + ( + | RequirementApproval + | RequirementAuthorization + | RequirementSignatureRequest + )[] + > + >(); + await promise; + }); + test("should build the borrow without signatures by default", async () => { account.sendTransaction = vi .fn() diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 0f84f2325..298ec91ea 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -14,6 +14,7 @@ import { type Requirement, type RequirementSignature, type Transaction, + type VaultReallocation, } from "@morpho-org/morpho-sdk"; import type { BorrowResult, @@ -156,14 +157,33 @@ export interface MorphoBorrowOptions { amount: number | bigint; /** The address on behalf of which the borrow operation should be performed. Must match the wallet account address when set. */ onBehalfOf?: string; - /** Optional Vault V1 PublicAllocator or Vault V2 BluePublicAllocator reallocations to include in the borrow action. */ - reallocations?: readonly BlueReallocation[]; + /** Optional Vault V1 PublicAllocator reallocations to include in the borrow action. */ + reallocations?: readonly VaultReallocation[]; /** Signature returned by a Morpho SDK authorization requirement, folded into the bundle as `setAuthorizationWithSig`. */ requirementSignature?: RequirementSignature; /** Optional Morpho SDK slippage tolerance in WAD precision. */ slippageTolerance?: bigint; } +/** + * Borrow options that opt into Vault V2 BluePublicAllocator reallocations. + * + * Passing this type widens {@link MorphoProtocolEvm.getBorrowRequirements} to + * include the loan-token approval that a Vault V2 penalty may require. Legacy + * {@link MorphoBorrowOptions} callers retain the authorization-only result. + */ +export type MorphoBorrowWithV2ReallocationsOptions = Omit< + MorphoBorrowOptions, + "reallocations" +> & { + /** Vault V1 and Vault V2 reallocations to include in the borrow action. */ + readonly reallocations: readonly BlueReallocation[]; +}; + +type MorphoBorrowInput = + | MorphoBorrowOptions + | MorphoBorrowWithV2ReallocationsOptions; + export interface MorphoRepayOptions { /** The address of the token to repay. */ token: string; @@ -653,7 +673,7 @@ export default class MorphoProtocolEvm extends LendingProtocol { * @throws {Error} If the options are invalid, GeneralAdapter1 is not authorized, or the transaction fails. */ async borrow( - options: MorphoBorrowOptions, + options: MorphoBorrowInput, config?: Erc4337TransactionConfig, ): Promise { this._assertWritable("borrow(options)"); @@ -674,8 +694,20 @@ export default class MorphoProtocolEvm extends LendingProtocol { * a signable `RequirementSignatureRequest` to fold into the bundle via * `setAuthorizationWithSig`. */ - async getBorrowRequirements( + public getBorrowRequirements( options: MorphoBorrowOptions, + ): Promise<(RequirementAuthorization | RequirementSignatureRequest)[]>; + public getBorrowRequirements( + options: MorphoBorrowWithV2ReallocationsOptions, + ): Promise< + ( + | RequirementApproval + | RequirementAuthorization + | RequirementSignatureRequest + )[] + >; + public async getBorrowRequirements( + options: MorphoBorrowInput, ): Promise< ( | RequirementApproval @@ -696,7 +728,7 @@ export default class MorphoProtocolEvm extends LendingProtocol { * @returns The fee quote. */ async quoteBorrow( - options: MorphoBorrowOptions, + options: MorphoBorrowInput, config?: Erc4337TransactionConfig, ): Promise> { const tx = await this._getBorrowTransaction(options); @@ -710,7 +742,7 @@ export default class MorphoProtocolEvm extends LendingProtocol { onBehalfOf, slippageTolerance, reallocations, - }: MorphoBorrowOptions) { + }: MorphoBorrowInput) { const normalizedAmount = normalizeAmount(amount); this._assertAddress("token", token); this._assertOptionalAddress("onBehalfOf", onBehalfOf); @@ -736,7 +768,7 @@ export default class MorphoProtocolEvm extends LendingProtocol { } private async _getBorrowTransaction( - options: MorphoBorrowOptions, + options: MorphoBorrowInput, ): Promise { const action = await this._getBorrowAction(options); diff --git a/scripts/compile-solidity.js b/scripts/compile-solidity.js index fc96a8ee7..274a39db9 100644 --- a/scripts/compile-solidity.js +++ b/scripts/compile-solidity.js @@ -54,6 +54,18 @@ const packageConfigs = { ); }, }, + "morpho-sdk": { + bytecodeExportName: "code", + describeArtifact(contractName) { + return `Test-only \`${contractName}\` contract`; + }, + resolveOutputPath(sourceName) { + if (!sourceName.includes("/fixtures/")) return null; + + const parsed = parse(sourceName); + return join(packageDir, "test", "fixtures", `${parsed.name}.ts`); + }, + }, }; const config = packageConfigs[packageName]; From fdce6c42d69daec5904bc7aea95d2be1a43b8c83 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 14 Aug 2026 17:32:56 +0200 Subject: [PATCH 14/41] fix: harden Vault V2 reallocation planning --- .changeset/brave-vaults-reallocate.md | 2 +- .../vaultV2Reallocations.integration.test.ts | 223 ++++++++++++++++++ .../entities/vaultV2ReallocationData.test.ts | 80 ++++++- .../src/entities/vaultV2ReallocationData.ts | 37 ++- 4 files changed, 337 insertions(+), 5 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 2ffec153c..e50f90121 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -9,7 +9,7 @@ Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. -V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual, and target allocation; rejects same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. +V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases. diff --git a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts index 8a2c469b4..46061054b 100644 --- a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts +++ b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts @@ -6,6 +6,9 @@ import { } from "@morpho-org/blue-sdk"; import { blueAbi, + fetchAccrualVaultV2, + fetchMarket, + fetchVaultV2PublicAllocatorData, readContractRestructured, vaultV2Abi, } from "@morpho-org/blue-sdk-viem"; @@ -15,6 +18,7 @@ import { encodeAbiParameters, encodeFunctionData, erc20Abi, + keccak256, maxUint128, parseUnits, } from "viem"; @@ -30,6 +34,7 @@ import { deployVaultV2, submitAndAcceptVaultV2Call, } from "../../../test/helpers/vaultV2.js"; +import { VaultV2ReallocationData } from "../../entities/vaultV2ReallocationData.js"; import { isRequirementApproval, isRequirementBlueAuthorization, @@ -362,4 +367,222 @@ describe("Blue actions with Vault V2 reallocations", () => { expect(bundlerBalanceAfter).toBe(0n); expect(allocatorAllowanceAfter).toBe(0n); }); + + test("executes the simulated zero-elapsed relative-cap maximum", async ({ + client, + }) => { + const anvilClient = client as AnvilTestClient; + const { morpho } = getChainAddresses(base.id); + const depositAssets = parseUnits("100", 6); + const seedAssets = parseUnits("1", 6); + const postLossIdleAssets = parseUnits("89", 6); + const relativeCap = MathLib.WAD / 2n; + + const marketState = await readContractRestructured(client, { + address: morpho, + abi: blueAbi, + functionName: "market", + args: [targetMarket.id], + }); + if (marketState.lastUpdate === 0n) { + await client.writeContract({ + address: morpho, + abi: blueAbi, + functionName: "createMarket", + args: [targetMarket], + }); + } + + const vault = await deployVaultV2(anvilClient, targetMarket.loanToken); + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "setIsAllocator", + args: [client.account.address, true], + }), + }); + const targetAdapter = await deployMorphoMarketV1AdapterV2( + anvilClient, + vault, + ); + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "addAdapter", + args: [targetAdapter], + }), + }); + + const targetIdData = [ + encodeAbiParameters( + [{ type: "string" }, { type: "address" }], + ["this", targetAdapter], + ), + encodeAbiParameters( + [{ type: "string" }, { type: "address" }], + ["collateralToken", targetMarket.collateralToken], + ), + encodeAbiParameters( + [{ type: "string" }, { type: "address" }, marketParamsAbi], + ["this/marketParams", targetAdapter, targetMarket], + ), + ] as const; + for (const idData of targetIdData) { + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "increaseAbsoluteCap", + args: [idData, maxUint128], + }), + }); + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "increaseRelativeCap", + args: [idData, relativeCap], + }), + }); + } + + const deploymentHash = await client.deployContract({ + abi: allocatorAbi, + bytecode: allocatorCode, + }); + const deploymentReceipt = await client.waitForTransactionReceipt({ + hash: deploymentHash, + }); + const allocator = deploymentReceipt.contractAddress; + assert(allocator != null); + + await submitAndAcceptVaultV2Call(anvilClient, { + vault, + data: encodeFunctionData({ + abi: vaultV2Abi, + functionName: "setIsAllocator", + args: [allocator, true], + }), + }); + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "setIsActiveAdapter", + args: [vault, targetAdapter, true], + }); + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "setAbsoluteCap", + args: [vault, targetAdapter, targetMarket, maxUint128], + }); + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "setCanPullFromIdle", + args: [vault, true], + }); + + await client.deal({ + account: client.account.address, + erc20: targetMarket.loanToken, + amount: depositAssets, + }); + await client.approve({ + address: targetMarket.loanToken, + args: [vault, depositAssets], + }); + await client.writeContract({ + address: vault, + abi: vaultV2Abi, + functionName: "deposit", + args: [depositAssets, client.account.address], + }); + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "allocateFromIdle", + args: [vault, targetAdapter, targetMarket, seedAssets, 0n], + }); + + // Change the token balance without mining so the snapshot timestamp still + // equals lastUpdate while real vault assets are below stored _totalAssets. + await client.deal({ + account: vault, + erc20: targetMarket.loanToken, + amount: postLossIdleAssets, + }); + + const [vaultData, targetMarketData, block] = await Promise.all([ + fetchAccrualVaultV2(vault, client), + fetchMarket(targetMarket.id, client), + client.getBlock(), + ]); + const allocatorData = await fetchVaultV2PublicAllocatorData( + allocator, + vaultData, + client, + ); + const targetMarketParamsId = keccak256(targetIdData[2]); + const targetAllocation = allocatorData.allocations[targetMarketParamsId]; + assert(targetAllocation != null); + const realTotalAssets = vaultData.accrualAdapters.reduce( + (assets, adapter) => assets + adapter.realAssets(block.timestamp), + vaultData.assetBalance, + ); + const expectedMaximum = + MathLib.wMulDown(realTotalAssets, relativeCap) - + targetAllocation.allocation; + const reallocationData = new VaultV2ReallocationData({ + chainId: base.id, + allocator, + markets: { [targetMarket.id]: targetMarketData }, + vaults: { [vault]: vaultData }, + allocations: { [vault]: allocatorData.allocations }, + publicAllocatorConfigs: { + [vault]: allocatorData.publicAllocatorConfig, + }, + marketPublicAllocatorConfigs: { + [vault]: allocatorData.marketPublicAllocatorConfigs, + }, + }); + + expect(block.timestamp).toBe(vaultData.lastUpdate); + expect(realTotalAssets).toBeLessThan(vaultData._totalAssets); + const result = reallocationData.computeVaultV2Reallocations( + targetMarket.id, + { timestamp: block.timestamp }, + ); + expect(result.reallocations).toHaveLength(1); + expect(result.reallocations[0]?.assets).toBe(expectedMaximum); + + await client.writeContract({ + address: allocator, + abi: allocatorAbi, + functionName: "allocateFromIdle", + args: [ + vault, + targetAdapter, + targetMarket, + result.reallocations[0]!.assets, + 0n, + ], + }); + + const [allocationAfter, vaultAfter] = await Promise.all([ + client.readContract({ + address: vault, + abi: vaultV2Abi, + functionName: "allocation", + args: [targetMarketParamsId], + }), + fetchAccrualVaultV2(vault, client), + ]); + expect(allocationAfter).toBeGreaterThan(targetAllocation.allocation); + expect(allocationAfter).toBeLessThanOrEqual( + MathLib.wMulDown(vaultAfter._totalAssets, relativeCap), + ); + }); }); diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index d1ba3ce8a..791e7e194 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -13,6 +13,7 @@ import { describe, expect, test } from "vitest"; import { blueBorrow } from "../actions/index.js"; import { InsufficientSharedLiquidityError, + NonPositiveInputError, ReallocationWithdrawExceedsMarketSupplyError, UnknownReallocationMarketError, } from "../types/index.js"; @@ -424,6 +425,60 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { expect(result.data.getVault(VAULT)._totalAssets).toBe(1_000n); }); + test("behavior: recognizes zero-elapsed losses at the one-unit relative-cap boundary", () => { + const { data, targetIds } = makeFixture({ + sourceSupply: 0n, + targetSupply: 0n, + firstTotalAssets: 1_000n, + idle: 900n, + canPullFromMarket: false, + penalty: 0n, + targetCaps: [ + { absoluteCap: 10_000n, relativeCap: MathLib.WAD / 2n }, + { absoluteCap: 10_000n, relativeCap: MathLib.WAD / 2n }, + { absoluteCap: 10_000n, relativeCap: MathLib.WAD / 2n }, + ], + }); + + const result = data.computeVaultV2Reallocations(targetParams.id); + + expect(result.reallocations).toHaveLength(1); + expect(result.reallocations[0]?.assets).toBe(450n); + expect(result.data.getAllocation(VAULT, targetIds[2]).allocation).toBe( + 450n, + ); + expect(result.data.getVault(VAULT)._totalAssets).toBe(900n); + }); + + test("behavior: reuses firstTotalAssets across two reallocations for one vault", () => { + const relativeCap = (MathLib.WAD * 3n) / 4n; + const { data } = makeFixture({ + sourceSupply: 500n, + targetSupply: 0n, + firstTotalAssets: 1_000n, + idle: 400n, + penalty: 0n, + targetCaps: [ + { absoluteCap: 10_000n, relativeCap }, + { absoluteCap: 10_000n, relativeCap }, + { absoluteCap: 10_000n, relativeCap }, + ], + }); + + const result = data.computeVaultV2Reallocations(targetParams.id); + + expect( + result.reallocations.map(({ from, assets }) => ({ + from: from.type, + assets, + })), + ).toStrictEqual([ + { from: "market", assets: 500n }, + { from: "idle", assets: 175n }, + ]); + expect(result.data.getVault(VAULT)._totalAssets).toBe(900n); + }); + test("behavior: caps each call at uint128", () => { const sourceSupply = MathLib.MAX_UINT_128 + 10n; const { data } = makeFixture({ @@ -730,6 +785,29 @@ describe("computeVaultV2Reallocations", () => { ).toThrow(ReallocationWithdrawExceedsMarketSupplyError); }); + test.each([ + { operation: "borrow", amount: 0n }, + { operation: "borrow", amount: -1n }, + { operation: "withdraw", amount: 0n }, + { operation: "withdraw", amount: -1n }, + ] as const)( + "error: NonPositiveInputError for $operation amount $amount", + ({ operation, amount }) => { + const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 95n }); + const initialData = data.clone(); + + expect(() => + computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation, + amount, + }), + ).toThrow(NonPositiveInputError); + expect(data).toStrictEqual(initialData); + }, + ); + test("behavior: disabled planning returns no calls", () => { const { data } = makeFixture(); @@ -738,7 +816,7 @@ describe("computeVaultV2Reallocations", () => { reallocationData: data, marketId: targetParams.id, operation: "borrow", - amount: 1_000n, + amount: 0n, options: { enabled: false }, }), ).toStrictEqual([]); diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index 403c01bc2..a3db6b680 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -24,6 +24,7 @@ import type { } from "../types/index.js"; import { InsufficientSharedLiquidityError, + NonPositiveInputError, ReallocationAdapterSupplySharesUnderflowError, ReallocationAllocationUnderflowError, ReallocationWithdrawExceedsMarketSupplyError, @@ -140,6 +141,8 @@ const cloneVault = (vault: AccrualVaultV2) => { export class VaultV2ReallocationData implements InputVaultV2ReallocationData { /** Penalty donations created by this simulation, excluded as fresh shared-liquidity sources. */ private readonly donatedPenaltyAssets: Record; + /** Transaction-frozen cap denominator for each vault touched by this plan. */ + private readonly firstTotalAssets: Record; /** Chain id associated with this snapshot. */ public readonly chainId: number; /** Explicit BluePublicAllocator address used in returned calls. */ @@ -181,6 +184,10 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { input instanceof VaultV2ReallocationData ? { ...input.donatedPenaltyAssets } : {}; + this.firstTotalAssets = + input instanceof VaultV2ReallocationData + ? { ...input.firstTotalAssets } + : {}; for (const [marketId, market] of Object.entries(input.markets ?? {}) as [ MarketId, @@ -423,6 +430,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @param params.amount - Borrow or withdraw amount. * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. + * @throws {NonPositiveInputError} when `amount <= 0n` and planning is enabled. * @throws {UnknownReallocationMarketError} when the target market is absent. * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. @@ -448,6 +456,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { readonly options?: VaultV2BluePublicAllocatorOptions; }): readonly VaultV2BlueReallocation[] { if (options?.enabled === false) return []; + if (amount <= 0n) throw new NonPositiveInputError("amount", amount); const timestamp = options?.timestamp == null @@ -930,11 +939,15 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { reallocation.to.adapter, ); const targetIds = adapter.ids(postState.getMarket(targetMarketId).params); + // Vault V2 checks relative caps against the transient firstTotalAssets, + // which stays fixed after the vault's first allocation in a transaction. + const firstTotalAssets = + postState.firstTotalAssets[reallocation.vault] ?? vault._totalAssets; const withinCaps = targetIds.every((id) => { const allocation = postState.getAllocation(reallocation.vault, id); const capacity = VaultV2Utils.allocationHeadroom( { ...allocation, allocation: 0n }, - vault._totalAssets, + firstTotalAssets, ).value; return allocation.absoluteCap > 0n && allocation.allocation <= capacity; }); @@ -1008,8 +1021,25 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { vault.assetBalance += reallocation.assets; } - vault = vault.accrueInterest(timestamp).vault; - data.vaults[reallocation.vault] = vault; + if (data.firstTotalAssets[reallocation.vault] == null) { + // Vault V2's transient firstTotalAssets tracks the first allocation in a + // transaction independently from elapsed time. Later allocations must not + // recompute the denominator, even if their simulated balances have changed. + if (timestamp === vault.lastUpdate) { + // AccrualVaultV2 skips zero-elapsed accruals, but the contract's first + // touch still reads real adapter assets. With zero elapsed time, its + // growth clamp leaves only existing losses to recognize. + const realAssets = vault.accrualAdapters.reduce( + (assets, adapter) => assets + adapter.realAssets(timestamp), + vault.assetBalance, + ); + vault._totalAssets = MathLib.min(realAssets, vault._totalAssets); + } else { + vault = vault.accrueInterest(timestamp).vault; + } + data.vaults[reallocation.vault] = vault; + data.firstTotalAssets[reallocation.vault] = vault._totalAssets; + } const targetAdapter = data.getAdapter( reallocation.vault, @@ -1093,6 +1123,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @param params.amount - Borrow or withdraw amount. * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. + * @throws {NonPositiveInputError} when `amount <= 0n` and planning is enabled. * @throws {UnknownReallocationMarketError} when the target market is absent. * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. From c2eddaac3398aa3ab7e12a8d18b222028589f901 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 14 Aug 2026 18:01:18 +0200 Subject: [PATCH 15/41] fix: keep Vault V2 simulation state coherent --- .changeset/brave-vaults-reallocate.md | 2 +- packages/liquidity-sdk-viem/src/api/rest.ts | 379 ++++----- .../entities/vaultV2ReallocationData.test.ts | 301 +++++++ .../src/entities/vaultV2ReallocationData.ts | 783 ++++++++++-------- 4 files changed, 898 insertions(+), 567 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index e50f90121..10c8e5d0f 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -9,7 +9,7 @@ Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. -V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. +V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases. diff --git a/packages/liquidity-sdk-viem/src/api/rest.ts b/packages/liquidity-sdk-viem/src/api/rest.ts index 98898a749..5a5cfeb5e 100644 --- a/packages/liquidity-sdk-viem/src/api/rest.ts +++ b/packages/liquidity-sdk-viem/src/api/rest.ts @@ -206,183 +206,6 @@ const isHexValue = (value: unknown): value is Hex => typeof value === "string" && isHex(value, { strict: true }); const isHashValue = (value: unknown): value is Hash => isHexValue(value) && size(value) === 32; -const isNullableDecimalString = (value: unknown): value is string | null => - value === null || isDecimalString(value); - -const responseValidators = { - vault: (value: unknown): value is VaultV2Response => { - if (!isRecord(value) || !isRecord(value.asset) || !isRecord(value.gates)) - return false; - const { asset, gates } = value; - return ( - isInteger(value.chain_id) && - isAddressValue(value.address) && - isDecimalString(value.last_indexed_block) && - typeof value.version === "string" && - typeof value.name === "string" && - typeof value.symbol === "string" && - isAddressValue(asset.address) && - isInteger(asset.decimals) && - typeof asset.name === "string" && - typeof asset.symbol === "string" && - isInteger(value.decimals_offset) && - isAddressValue(value.factory_address) && - isDecimalString(value.creation_block_number) && - isAddressValue(value.owner) && - isAddressValue(value.curator) && - isInteger(value.timelock_seconds) && - isNullableDecimalString(value.management_fee_wad) && - isNullableAddress(value.management_fee_recipient) && - isNullableDecimalString(value.performance_fee_wad) && - isNullableAddress(value.performance_fee_recipient) && - isDecimalString(value.max_rate_per_second_wad) && - isAddressValue(value.adapter_registry) && - isAddressValue(value.liquidity_adapter) && - isHexValue(value.liquidity_data) && - isNullableAddress(gates.send_shares) && - isNullableAddress(gates.receive_shares) && - isNullableAddress(gates.send_assets) && - isNullableAddress(gates.receive_assets) - ); - }, - vaultState: (value: unknown): value is VaultV2StateResponse => - isRecord(value) && - isInteger(value.chain_id) && - isAddressValue(value.address) && - isDecimalString(value.last_indexed_block) && - isInteger(value.last_accrual_timestamp) && - isDecimalString(value.total_assets) && - isDecimalString(value.total_supply) && - isDecimalString(value.withdrawable_assets) && - isDecimalString(value.allocated_assets) && - isDecimalString(value.idle_assets) && - isDecimalString(value.share_price_ray), - vaultAllocations: (value: unknown): value is VaultV2AllocationsResponse => { - if ( - !isRecord(value) || - !isInteger(value.chain_id) || - !isAddressValue(value.vault_address) || - !isDecimalString(value.last_indexed_block) || - !Array.isArray(value.allocations) || - !Array.isArray(value.unscoped_caps) - ) - return false; - - const caps = [...value.unscoped_caps]; - for (const adapter of value.allocations) { - if ( - !isRecord(adapter) || - !isAddressValue(adapter.adapter_address) || - (adapter.adapter_kind !== "morpho_market_v1" && - adapter.adapter_kind !== "morpho_market_v1_v2" && - adapter.adapter_kind !== "morpho_vault_v1" && - adapter.adapter_kind !== "morpho_vault_v2") || - !Array.isArray(adapter.caps) - ) - return false; - caps.push(...adapter.caps); - } - - return caps.every((cap) => { - if ( - !isRecord(cap) || - !isHashValue(cap.cap_id) || - !isHexValue(cap.cap_data) || - !isDecimalString(cap.allocated_assets) || - !isDecimalString(cap.absolute_cap) || - !isDecimalString(cap.relative_cap_wad) || - (cap.market_id !== undefined && !isHashValue(cap.market_id)) || - (cap.collateral_address !== undefined && - !isAddressValue(cap.collateral_address)) - ) - return false; - - switch (cap.cap_type) { - case "adapter": - return true; - case "collateral": - return isAddressValue(cap.collateral_address); - case "market_v1": - return isHashValue(cap.market_id); - default: - return false; - } - }); - }, - withdrawalOptions: ( - value: unknown, - ): value is VaultV2WithdrawalOptionsResponse => - isRecord(value) && - isInteger(value.chain_id) && - isAddressValue(value.vault_address) && - isDecimalString(value.liquidity_adapter_available_assets) && - isDecimalString(value.idle_assets) && - Array.isArray(value.adapter_penalties) && - value.adapter_penalties.every( - (penalty) => - isRecord(penalty) && - isAddressValue(penalty.adapter_address) && - (penalty.adapter_kind === "blue_market_adapter" || - penalty.adapter_kind === "vault_v1_adapter" || - penalty.adapter_kind === "vault_v2_adapter" || - penalty.adapter_kind === "unknown_adapter") && - isDecimalString(penalty.force_deallocatable_assets) && - isDecimalString(penalty.penalty_rate_wad), - ), - market: (value: unknown): value is MarketResponse => - isRecord(value) && - isInteger(value.chain_id) && - isHashValue(value.market_id) && - isAddressValue(value.loan_token) && - isAddressValue(value.collateral_token) && - isAddressValue(value.oracle_address) && - isAddressValue(value.irm_address) && - isDecimalString(value.lltv_wad) && - isDecimalString(value.creation_block_number), - marketState: (value: unknown): value is MarketStateResponse => - isRecord(value) && - isInteger(value.chain_id) && - isHashValue(value.market_id) && - isDecimalString(value.last_indexed_block) && - isInteger(value.last_accrual_timestamp) && - isDecimalString(value.total_supply_assets) && - isDecimalString(value.total_supply_shares) && - isDecimalString(value.total_borrow_assets) && - isDecimalString(value.total_borrow_shares) && - isDecimalString(value.fee_wad), - marketPosition: (value: unknown): value is MarketPositionResponse => - isRecord(value) && - isInteger(value.chain_id) && - isHashValue(value.market_id) && - isAddressValue(value.user_address) && - isDecimalString(value.last_indexed_block) && - isDecimalString(value.collateral_assets) && - isDecimalString(value.supply_shares) && - isDecimalString(value.borrow_shares), - oracleState: (value: unknown): value is OracleStateResponse => - isRecord(value) && - isInteger(value.chain_id) && - isAddressValue(value.oracle_address) && - isDecimalString(value.last_indexed_block) && - (value.last_updated_at === undefined || - value.last_updated_at === null || - isDecimalString(value.last_updated_at)) && - (value.price === undefined || - value.price === null || - isDecimalString(value.price)), - marketIrm: (value: unknown): value is MarketIrmResponse => - isRecord(value) && - isInteger(value.chainId) && - isHashValue(value.marketId) && - isAddressValue(value.irmAddress) && - isFiniteNumber(value.targetUtilization) && - (value.utilization === null || isFiniteNumber(value.utilization)) && - (value.apyAtTarget === null || isFiniteNumber(value.apyAtTarget)) && - (value.rateAtTarget === undefined || - value.rateAtTarget === null || - isDecimalString(value.rateAtTarget)) && - (value.borrowToTarget === null || isFiniteNumber(value.borrowToTarget)), -}; async function requestApi( path: string, @@ -443,10 +266,49 @@ export const fetchRestVaultV2 = (chainId: number, address: Address) => requestApi( `/v0/vaults-v2/${apiSelector(chainId, address)}`, { - validator: (value): value is VaultV2Response => - responseValidators.vault(value) && - value.chain_id === chainId && - isAddressEqual(value.address, address), + validator: (value): value is VaultV2Response => { + if ( + !isRecord(value) || + !isRecord(value.asset) || + !isRecord(value.gates) + ) + return false; + const { asset, gates } = value; + return ( + isInteger(value.chain_id) && + value.chain_id === chainId && + isAddressValue(value.address) && + isAddressEqual(value.address, address) && + isDecimalString(value.last_indexed_block) && + typeof value.version === "string" && + typeof value.name === "string" && + typeof value.symbol === "string" && + isAddressValue(asset.address) && + isInteger(asset.decimals) && + typeof asset.name === "string" && + typeof asset.symbol === "string" && + isInteger(value.decimals_offset) && + isAddressValue(value.factory_address) && + isDecimalString(value.creation_block_number) && + isAddressValue(value.owner) && + isAddressValue(value.curator) && + isInteger(value.timelock_seconds) && + (value.management_fee_wad === null || + isDecimalString(value.management_fee_wad)) && + isNullableAddress(value.management_fee_recipient) && + (value.performance_fee_wad === null || + isDecimalString(value.performance_fee_wad)) && + isNullableAddress(value.performance_fee_recipient) && + isDecimalString(value.max_rate_per_second_wad) && + isAddressValue(value.adapter_registry) && + isAddressValue(value.liquidity_adapter) && + isHexValue(value.liquidity_data) && + isNullableAddress(gates.send_shares) && + isNullableAddress(gates.receive_shares) && + isNullableAddress(gates.send_assets) && + isNullableAddress(gates.receive_assets) + ); + }, }, ); @@ -456,9 +318,19 @@ export const fetchRestVaultV2State = (chainId: number, address: Address) => `/v1/vaults-v2/${apiSelector(chainId, address)}/state`, { validator: (value): value is VaultV2StateResponse => - responseValidators.vaultState(value) && + isRecord(value) && + isInteger(value.chain_id) && value.chain_id === chainId && - isAddressEqual(value.address, address), + isAddressValue(value.address) && + isAddressEqual(value.address, address) && + isDecimalString(value.last_indexed_block) && + isInteger(value.last_accrual_timestamp) && + isDecimalString(value.total_assets) && + isDecimalString(value.total_supply) && + isDecimalString(value.withdrawable_assets) && + isDecimalString(value.allocated_assets) && + isDecimalString(value.idle_assets) && + isDecimalString(value.share_price_ray), }, ); @@ -470,10 +342,60 @@ export const fetchRestVaultV2Allocations = ( requestApi( `/v0/vaults-v2/${apiSelector(chainId, address)}/allocations`, { - validator: (value): value is VaultV2AllocationsResponse => - responseValidators.vaultAllocations(value) && - value.chain_id === chainId && - isAddressEqual(value.vault_address, address), + validator: (value): value is VaultV2AllocationsResponse => { + if ( + !isRecord(value) || + !isInteger(value.chain_id) || + value.chain_id !== chainId || + !isAddressValue(value.vault_address) || + !isAddressEqual(value.vault_address, address) || + !isDecimalString(value.last_indexed_block) || + !Array.isArray(value.allocations) || + !Array.isArray(value.unscoped_caps) + ) + return false; + + const caps = [...value.unscoped_caps]; + for (const adapter of value.allocations) { + if ( + !isRecord(adapter) || + !isAddressValue(adapter.adapter_address) || + (adapter.adapter_kind !== "morpho_market_v1" && + adapter.adapter_kind !== "morpho_market_v1_v2" && + adapter.adapter_kind !== "morpho_vault_v1" && + adapter.adapter_kind !== "morpho_vault_v2") || + !Array.isArray(adapter.caps) + ) + return false; + caps.push(...adapter.caps); + } + + return caps.every((cap) => { + if ( + !isRecord(cap) || + !isHashValue(cap.cap_id) || + !isHexValue(cap.cap_data) || + !isDecimalString(cap.allocated_assets) || + !isDecimalString(cap.absolute_cap) || + !isDecimalString(cap.relative_cap_wad) || + (cap.market_id !== undefined && !isHashValue(cap.market_id)) || + (cap.collateral_address !== undefined && + !isAddressValue(cap.collateral_address)) + ) + return false; + + switch (cap.cap_type) { + case "adapter": + return true; + case "collateral": + return isAddressValue(cap.collateral_address); + case "market_v1": + return isHashValue(cap.market_id); + default: + return false; + } + }); + }, }, ); @@ -486,9 +408,25 @@ export const fetchRestVaultV2WithdrawalOptions = ( `/v0/vaults-v2/${apiSelector(chainId, address)}/withdrawal-options`, { validator: (value): value is VaultV2WithdrawalOptionsResponse => - responseValidators.withdrawalOptions(value) && + isRecord(value) && + isInteger(value.chain_id) && value.chain_id === chainId && - isAddressEqual(value.vault_address, address), + isAddressValue(value.vault_address) && + isAddressEqual(value.vault_address, address) && + isDecimalString(value.liquidity_adapter_available_assets) && + isDecimalString(value.idle_assets) && + Array.isArray(value.adapter_penalties) && + value.adapter_penalties.every( + (penalty) => + isRecord(penalty) && + isAddressValue(penalty.adapter_address) && + (penalty.adapter_kind === "blue_market_adapter" || + penalty.adapter_kind === "vault_v1_adapter" || + penalty.adapter_kind === "vault_v2_adapter" || + penalty.adapter_kind === "unknown_adapter") && + isDecimalString(penalty.force_deallocatable_assets) && + isDecimalString(penalty.penalty_rate_wad), + ), }, ); @@ -498,9 +436,17 @@ export const fetchRestMarket = (chainId: number, marketId: MarketId) => `/v0/blue/markets/${apiSelector(chainId, marketId)}`, { validator: (value): value is MarketResponse => - responseValidators.market(value) && + isRecord(value) && + isInteger(value.chain_id) && value.chain_id === chainId && - value.market_id.toLowerCase() === marketId.toLowerCase(), + isHashValue(value.market_id) && + value.market_id.toLowerCase() === marketId.toLowerCase() && + isAddressValue(value.loan_token) && + isAddressValue(value.collateral_token) && + isAddressValue(value.oracle_address) && + isAddressValue(value.irm_address) && + isDecimalString(value.lltv_wad) && + isDecimalString(value.creation_block_number), }, ); @@ -510,9 +456,18 @@ export const fetchRestMarketState = (chainId: number, marketId: MarketId) => `/v0/blue/markets/${apiSelector(chainId, marketId)}/state`, { validator: (value): value is MarketStateResponse => - responseValidators.marketState(value) && + isRecord(value) && + isInteger(value.chain_id) && value.chain_id === chainId && - value.market_id.toLowerCase() === marketId.toLowerCase(), + isHashValue(value.market_id) && + value.market_id.toLowerCase() === marketId.toLowerCase() && + isDecimalString(value.last_indexed_block) && + isInteger(value.last_accrual_timestamp) && + isDecimalString(value.total_supply_assets) && + isDecimalString(value.total_supply_shares) && + isDecimalString(value.total_borrow_assets) && + isDecimalString(value.total_borrow_shares) && + isDecimalString(value.fee_wad), }, ); @@ -526,10 +481,17 @@ export const fetchRestMarketPosition = ({ `/v0/blue/markets/${apiSelector(chainId, marketId)}/users/${encodeURIComponent(user)}/position`, { validator: (value): value is MarketPositionResponse => - responseValidators.marketPosition(value) && + isRecord(value) && + isInteger(value.chain_id) && value.chain_id === chainId && + isHashValue(value.market_id) && value.market_id.toLowerCase() === marketId.toLowerCase() && - isAddressEqual(value.user_address, user), + isAddressValue(value.user_address) && + isAddressEqual(value.user_address, user) && + isDecimalString(value.last_indexed_block) && + isDecimalString(value.collateral_assets) && + isDecimalString(value.supply_shares) && + isDecimalString(value.borrow_shares), }, ); @@ -539,9 +501,18 @@ export const fetchRestOracleState = (chainId: number, address: Address) => `/v0/oracles/${apiSelector(chainId, address)}/state`, { validator: (value): value is OracleStateResponse => - responseValidators.oracleState(value) && + isRecord(value) && + isInteger(value.chain_id) && value.chain_id === chainId && - isAddressEqual(value.oracle_address, address), + isAddressValue(value.oracle_address) && + isAddressEqual(value.oracle_address, address) && + isDecimalString(value.last_indexed_block) && + (value.last_updated_at === undefined || + value.last_updated_at === null || + isDecimalString(value.last_updated_at)) && + (value.price === undefined || + value.price === null || + isDecimalString(value.price)), }, ); @@ -551,9 +522,19 @@ export const fetchRestMarketIrm = (chainId: number, marketId: MarketId) => `/consumer/chains/${chainId}/markets/${encodeURIComponent(marketId)}/irm`, { validator: (value): value is MarketIrmResponse => - responseValidators.marketIrm(value) && + isRecord(value) && + isInteger(value.chainId) && value.chainId === chainId && - value.marketId.toLowerCase() === marketId.toLowerCase(), + isHashValue(value.marketId) && + value.marketId.toLowerCase() === marketId.toLowerCase() && + isAddressValue(value.irmAddress) && + isFiniteNumber(value.targetUtilization) && + (value.utilization === null || isFiniteNumber(value.utilization)) && + (value.apyAtTarget === null || isFiniteNumber(value.apyAtTarget)) && + (value.rateAtTarget === undefined || + value.rateAtTarget === null || + isDecimalString(value.rateAtTarget)) && + (value.borrowToTarget === null || isFiniteNumber(value.borrowToTarget)), responseKind: "root", }, ); diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index 791e7e194..3328050c8 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -1,6 +1,10 @@ import { + AccrualPosition, + AccrualVault, AccrualVaultV2, + AccrualVaultV2MorphoMarketV1Adapter, AccrualVaultV2MorphoMarketV1AdapterV2, + AccrualVaultV2MorphoVaultV1Adapter, ChainId, type IVaultV2Allocation, Market, @@ -29,6 +33,11 @@ const TARGET_ADAPTER = "0x0000000000000000000000000000000000000003"; const SOURCE_ADAPTER = "0x0000000000000000000000000000000000000004"; const LOAN_TOKEN = "0x0000000000000000000000000000000000000005"; const IRM = "0x0000000000000000000000000000000000000006"; +const SECOND_VAULT = "0x000000000000000000000000000000000000000b"; +const SECOND_TARGET_ADAPTER = "0x000000000000000000000000000000000000000C"; +const LEGACY_MARKET_ADAPTER = "0x000000000000000000000000000000000000000d"; +const VAULT_V1_ADAPTER = "0x000000000000000000000000000000000000000E"; +const NESTED_VAULT = "0x000000000000000000000000000000000000000F"; const targetParams = new MarketParams({ loanToken: LOAN_TOKEN, @@ -336,6 +345,298 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ); }); + test("behavior: keeps two vault adapters on one canonical market", () => { + const { data } = makeFixture({ + sourceSupply: 0n, + targetPositionAssets: 50n, + idle: 500n, + canPullFromMarket: false, + penalty: 0n, + }); + const targetMarket = data.getMarket(targetParams.id); + const secondTargetShares = targetMarket.toSupplyShares(50n, "Down"); + const secondTargetAdapter = new AccrualVaultV2MorphoMarketV1AdapterV2( + { + address: SECOND_TARGET_ADAPTER, + parentVault: SECOND_VAULT, + skimRecipient: zeroAddress, + marketIds: [targetMarket.id], + adaptiveCurveIrm: IRM, + supplyShares: { [targetMarket.id]: secondTargetShares }, + }, + [targetMarket], + ); + const secondTargetIds = secondTargetAdapter.ids(targetParams); + const secondAllocations: Record = {}; + for (const id of secondTargetIds) { + secondAllocations[id] = { + id, + absoluteCap: 10_000n, + relativeCap: MathLib.WAD, + allocation: 50n, + }; + } + const firstVault = data.getVault(VAULT); + const secondVault = new AccrualVaultV2( + { + ...firstVault, + address: SECOND_VAULT, + _totalAssets: 550n, + totalSupply: 550n, + }, + undefined, + [secondTargetAdapter], + 500n, + {}, + ); + const sharedData = new VaultV2ReallocationData({ + chainId: data.chainId, + allocator: data.allocator, + markets: data.markets, + vaults: { + [VAULT]: firstVault, + [SECOND_VAULT]: secondVault, + }, + allocations: { + [VAULT]: data.allocations[VAULT], + [SECOND_VAULT]: secondAllocations, + }, + publicAllocatorConfigs: { + [VAULT]: data.publicAllocatorConfigs[VAULT], + [SECOND_VAULT]: { + allocator: ALLOCATOR, + vault: SECOND_VAULT, + canPullFromIdle: true, + penalty: 0n, + }, + }, + marketPublicAllocatorConfigs: { + [VAULT]: data.marketPublicAllocatorConfigs[VAULT], + [SECOND_VAULT]: { + [secondTargetIds[2]]: { + allocator: ALLOCATOR, + vault: SECOND_VAULT, + adapter: SECOND_TARGET_ADAPTER, + marketParamsId: secondTargetIds[2], + absoluteCap: 10_000n, + canPullFromMarket: false, + isActiveAdapter: true, + }, + }, + }, + }); + + const initialCanonicalMarket = sharedData.getMarket(targetParams.id); + expect(sharedData.getAdapter(VAULT, TARGET_ADAPTER).markets[0]).toBe( + initialCanonicalMarket, + ); + expect( + sharedData.getAdapter(SECOND_VAULT, SECOND_TARGET_ADAPTER).markets[0], + ).toBe(initialCanonicalMarket); + + const result = sharedData.computeVaultV2Reallocations(targetParams.id); + const finalCanonicalMarket = result.data.getMarket(targetParams.id); + + expect(result.reallocations).toHaveLength(2); + expect(result.data.getAdapter(VAULT, TARGET_ADAPTER).markets[0]).toBe( + finalCanonicalMarket, + ); + expect( + result.data.getAdapter(SECOND_VAULT, SECOND_TARGET_ADAPTER).markets[0], + ).toBe(finalCanonicalMarket); + }); + + test("behavior: deep-clones legacy and nested accrued adapters", () => { + const { data } = makeFixture(); + const targetMarket = data.getMarket(targetParams.id); + const legacyPosition = new AccrualPosition( + { + user: LEGACY_MARKET_ADAPTER, + supplyShares: targetMarket.toSupplyShares(25n, "Down"), + borrowShares: 0n, + collateral: 0n, + }, + targetMarket, + ); + const legacyAdapter = new AccrualVaultV2MorphoMarketV1Adapter( + { + address: LEGACY_MARKET_ADAPTER, + parentVault: VAULT, + skimRecipient: zeroAddress, + marketParamsList: [targetParams], + }, + [legacyPosition], + ); + const nestedPosition = new AccrualPosition( + { + user: NESTED_VAULT, + supplyShares: targetMarket.toSupplyShares(30n, "Down"), + borrowShares: 0n, + collateral: 0n, + }, + targetMarket, + ); + const nestedVault = new AccrualVault( + { + address: NESTED_VAULT, + name: "Nested Vault", + symbol: "nv", + asset: LOAN_TOKEN, + decimalsOffset: 0n, + curator: VAULT, + owner: VAULT, + guardian: VAULT, + fee: 0n, + feeRecipient: VAULT, + skimRecipient: VAULT, + pendingTimelock: { value: 1n, validAt: TIMESTAMP + 1n }, + pendingGuardian: { value: VAULT, validAt: TIMESTAMP + 2n }, + pendingOwner: VAULT, + timelock: 0n, + supplyQueue: [targetMarket.id], + totalSupply: 30n, + lastTotalAssets: 30n, + publicAllocatorConfig: { + admin: VAULT, + fee: 1n, + accruedFee: 2n, + }, + }, + [ + { + config: { + vault: NESTED_VAULT, + marketId: targetMarket.id, + cap: 1_000n, + pendingCap: { value: 2_000n, validAt: TIMESTAMP + 3n }, + removableAt: 0n, + enabled: true, + publicAllocatorConfig: { + vault: NESTED_VAULT, + marketId: targetMarket.id, + maxIn: 100n, + maxOut: 200n, + }, + }, + position: nestedPosition, + }, + ], + ); + const nestedAdapter = new AccrualVaultV2MorphoVaultV1Adapter( + { + address: VAULT_V1_ADAPTER, + parentVault: VAULT, + skimRecipient: zeroAddress, + morphoVaultV1: NESTED_VAULT, + }, + nestedVault, + 30n, + ); + const fixtureVault = data.getVault(VAULT); + const inputVault = new AccrualVaultV2( + fixtureVault, + fixtureVault.accrualLiquidityAdapter, + [...fixtureVault.accrualAdapters, legacyAdapter, nestedAdapter], + fixtureVault.assetBalance, + fixtureVault.forceDeallocatePenalties, + ); + const input = new VaultV2ReallocationData({ + chainId: data.chainId, + allocator: data.allocator, + markets: data.markets, + vaults: { [VAULT]: inputVault }, + allocations: data.allocations, + publicAllocatorConfigs: data.publicAllocatorConfigs, + marketPublicAllocatorConfigs: data.marketPublicAllocatorConfigs, + }); + + const cloned = input.clone(); + const inputLegacy = input + .getVault(VAULT) + .accrualAdapters.find( + (adapter) => adapter instanceof AccrualVaultV2MorphoMarketV1Adapter, + ); + const clonedLegacy = cloned + .getVault(VAULT) + .accrualAdapters.find( + (adapter) => adapter instanceof AccrualVaultV2MorphoMarketV1Adapter, + ); + const inputNested = input + .getVault(VAULT) + .accrualAdapters.find( + (adapter) => adapter instanceof AccrualVaultV2MorphoVaultV1Adapter, + ); + const clonedNested = cloned + .getVault(VAULT) + .accrualAdapters.find( + (adapter) => adapter instanceof AccrualVaultV2MorphoVaultV1Adapter, + ); + + expect(clonedLegacy).not.toBe(inputLegacy); + expect(clonedLegacy?.positions[0]).not.toBe(inputLegacy?.positions[0]); + expect(clonedLegacy?.positions[0]?.market).not.toBe( + inputLegacy?.positions[0]?.market, + ); + expect(clonedNested).not.toBe(inputNested); + expect(clonedNested?.accrualVaultV1).not.toBe(inputNested?.accrualVaultV1); + expect(clonedNested?.accrualVaultV1.allocations).not.toBe( + inputNested?.accrualVaultV1.allocations, + ); + + const simulated = input.computeVaultV2Reallocations(targetMarket.id).data; + const simulatedLegacy = simulated + .getVault(VAULT) + .accrualAdapters.find( + (adapter) => adapter instanceof AccrualVaultV2MorphoMarketV1Adapter, + ); + const simulatedNested = simulated + .getVault(VAULT) + .accrualAdapters.find( + (adapter) => adapter instanceof AccrualVaultV2MorphoVaultV1Adapter, + ); + const simulatedTargetMarket = simulated.getMarket(targetMarket.id); + expect(simulatedLegacy?.positions[0]?.market.totalSupplyAssets).toBe( + simulatedTargetMarket.totalSupplyAssets, + ); + expect( + simulatedNested?.accrualVaultV1.allocations.get(targetMarket.id)?.position + .market.totalSupplyAssets, + ).toBe(simulatedTargetMarket.totalSupplyAssets); + + clonedLegacy!.positions[0]!.supplyShares += 1n; + clonedNested!.accrualVaultV1.pendingTimelock.value = 99n; + const clonedNestedAllocation = clonedNested!.accrualVaultV1.allocations.get( + targetMarket.id, + )!; + clonedNestedAllocation.config.pendingCap.value = 88n; + clonedNestedAllocation.position.supplyShares += 2n; + + expect(inputLegacy?.positions[0]?.supplyShares).toBe( + legacyPosition.supplyShares, + ); + expect(inputNested?.accrualVaultV1.pendingTimelock.value).toBe(1n); + expect( + inputNested?.accrualVaultV1.allocations.get(targetMarket.id)?.config + .pendingCap.value, + ).toBe(2_000n); + expect( + inputNested?.accrualVaultV1.allocations.get(targetMarket.id)?.position + .supplyShares, + ).toBe(nestedPosition.supplyShares); + }); + + test("behavior: repeated cap probes are deterministic and isolated", () => { + const { data } = makeFixture({ idle: 300n }); + const initialData = data.clone(); + + const first = data.computeVaultV2Reallocations(targetParams.id); + const second = data.computeVaultV2Reallocations(targetParams.id); + + expect(second.reallocations).toStrictEqual(first.reallocations); + expect(second.data).toStrictEqual(first.data); + expect(data).toStrictEqual(initialData); + }); + test("behavior: ranks market liquidity before idle and depletes both sources", () => { const { data, sourceExpectedAssets } = makeFixture({ idle: 300n }); diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index a3db6b680..2d8b13f1b 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -1,6 +1,11 @@ import { + AccrualPosition, + AccrualVault, AccrualVaultV2, + AccrualVaultV2MorphoMarketV1Adapter, AccrualVaultV2MorphoMarketV1AdapterV2, + AccrualVaultV2MorphoVaultV1Adapter, + type IAccrualVaultV2Adapter, type IVaultV2Allocation, Market, type MarketId, @@ -67,37 +72,92 @@ export interface InputVaultV2ReallocationData { >; } -type TargetContext = { - readonly adapter: AccrualVaultV2MorphoMarketV1AdapterV2; - readonly ids: readonly [Hash, Hash, Hash]; - readonly allocations: readonly IVaultV2Allocation[]; - readonly marketPublicAllocatorConfig: VaultV2MarketPublicAllocatorConfig; - readonly untracked: bigint; -}; - -const sameMarketId = (left: MarketId, right: MarketId) => - left.toLowerCase() === right.toLowerCase(); - const cloneMarket = (market: Market) => new Market({ ...market }); -const cloneAdapter = (adapter: AccrualVaultV2MorphoMarketV1AdapterV2) => - new AccrualVaultV2MorphoMarketV1AdapterV2( +const getCanonicalMarket = ( + markets: Record, + market: Market, +) => (markets[market.id] ??= cloneMarket(market)); + +const clonePosition = ( + position: AccrualPosition, + markets: Record, +) => + new AccrualPosition(position, getCanonicalMarket(markets, position.market)); + +const cloneAccrualVault = ( + vault: AccrualVault, + markets: Record, +) => + new AccrualVault( { - address: adapter.address, - parentVault: adapter.parentVault, - skimRecipient: adapter.skimRecipient, - marketIds: [...adapter.marketIds], - adaptiveCurveIrm: adapter.adaptiveCurveIrm, - supplyShares: { ...adapter.supplyShares }, + ...vault, + pendingTimelock: { ...vault.pendingTimelock }, + pendingGuardian: { ...vault.pendingGuardian }, + supplyQueue: [...vault.supplyQueue], + publicAllocatorConfig: + vault.publicAllocatorConfig == null + ? undefined + : { ...vault.publicAllocatorConfig }, }, - adapter.markets.map(cloneMarket), + [...vault.allocations.values()].map(({ config, position }) => ({ + config: { + ...config, + pendingCap: { ...config.pendingCap }, + publicAllocatorConfig: + config.publicAllocatorConfig == null + ? undefined + : { ...config.publicAllocatorConfig }, + }, + position: clonePosition(position, markets), + })), ); -const cloneVault = (vault: AccrualVaultV2) => { +const cloneAdapter = ( + adapter: IAccrualVaultV2Adapter, + markets: Record, +) => { + const base = { + address: adapter.address, + parentVault: adapter.parentVault, + skimRecipient: adapter.skimRecipient, + }; + + if (adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2) + return new AccrualVaultV2MorphoMarketV1AdapterV2( + { + ...base, + marketIds: [...adapter.marketIds], + adaptiveCurveIrm: adapter.adaptiveCurveIrm, + supplyShares: { ...adapter.supplyShares }, + }, + // V2 adapters can retain the canonical instance directly, so every + // adapter observes the same global Morpho market state. + adapter.markets.map((market) => getCanonicalMarket(markets, market)), + ); + + if (adapter instanceof AccrualVaultV2MorphoMarketV1Adapter) + return new AccrualVaultV2MorphoMarketV1Adapter( + { ...base, marketParamsList: [...adapter.marketParamsList] }, + adapter.positions.map((position) => clonePosition(position, markets)), + ); + + if (adapter instanceof AccrualVaultV2MorphoVaultV1Adapter) + return new AccrualVaultV2MorphoVaultV1Adapter( + { ...base, morphoVaultV1: adapter.morphoVaultV1 }, + cloneAccrualVault(adapter.accrualVaultV1, markets), + adapter.shares, + ); + + return adapter; +}; + +const cloneVault = ( + vault: AccrualVaultV2, + markets: Record, +) => { const adapters = vault.accrualAdapters.map((adapter) => - adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2 - ? cloneAdapter(adapter) - : adapter, + cloneAdapter(adapter, markets), ); const liquidityAdapter = vault.accrualLiquidityAdapter == null @@ -107,7 +167,7 @@ const cloneVault = (vault: AccrualVaultV2) => { adapter.address, vault.accrualLiquidityAdapter!.address, ), - ) ?? vault.accrualLiquidityAdapter); + ) ?? cloneAdapter(vault.accrualLiquidityAdapter, markets)); return new AccrualVaultV2( { @@ -200,16 +260,9 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { Address, AccrualVaultV2 | undefined, ][]) { - const clonedVault = vault == null ? undefined : cloneVault(vault); + const clonedVault = + vault == null ? undefined : cloneVault(vault, this.markets); this.vaults[address] = clonedVault; - - for (const adapter of clonedVault?.accrualAdapters ?? []) { - if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) - continue; - for (const market of adapter.markets) { - this.markets[market.id] ??= cloneMarket(market); - } - } } for (const [vault, allocations] of Object.entries( @@ -569,7 +622,10 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { options.timestamp == null ? this.getLatestSnapshotTimestamp() : BigInt(options.timestamp); - let data = this.accrueMarkets(timestamp); + let data = this.clone(); + for (const market of Object.values(data.markets)) { + if (market != null) data.setMarket(market.accrueInterest(timestamp)); + } const reallocations: VaultV2BlueReallocation[] = []; const configuredVaults = Object.keys(data.vaults) as Address[]; const vaultKeyByLower = new Map( @@ -585,14 +641,268 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { while (true) { const candidates = vaults - .map((vault) => - data.getLargestVaultReallocation({ - vaultAddress: vault, - marketId, - maxWithdrawalUtilization, - maxPenalty: options.maxPenalty, - }), - ) + .map((vaultAddress) => { + const targetMarket = data.getMarket(marketId); + return _try(() => { + const vault = data.getVault(vaultAddress); + const publicAllocatorConfig = + data.getPublicAllocatorConfig(vaultAddress); + if ( + !isAddressEqual( + publicAllocatorConfig.allocator, + data.allocator, + ) || + !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) || + (options.maxPenalty != null && + publicAllocatorConfig.penalty > options.maxPenalty) + ) + return; + + const targetSupplyHeadroom = MathLib.zeroFloorSub( + MathLib.MAX_UINT_128, + targetMarket.totalSupplyAssets, + ); + const rawCandidates: VaultV2BlueReallocation[] = []; + + for (const adapter of vault.accrualAdapters) { + if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) + continue; + if (!isAddressEqual(adapter.parentVault, vaultAddress)) continue; + if ( + !isAddressEqual(targetMarket.params.loanToken, vault.asset) || + !isAddressEqual( + targetMarket.params.irm, + adapter.adaptiveCurveIrm, + ) + ) + continue; + if ( + !adapter.markets.some( + (market) => + market.id.toLowerCase() === marketId.toLowerCase(), + ) + ) + continue; + + const targetContext = _try(() => { + const ids = adapter.ids(targetMarket.params); + const marketPublicAllocatorConfig = + data.getMarketPublicAllocatorConfig(vaultAddress, ids[2]); + if ( + !isAddressEqual( + marketPublicAllocatorConfig.allocator, + data.allocator, + ) || + !isAddressEqual( + marketPublicAllocatorConfig.vault, + vaultAddress, + ) || + !isAddressEqual( + marketPublicAllocatorConfig.adapter, + adapter.address, + ) || + !marketPublicAllocatorConfig.isActiveAdapter + ) + return; + + const allocations = ids.map((id) => + data.getAllocation(vaultAddress, id), + ); + if (allocations.some(({ absoluteCap }) => absoluteCap === 0n)) + return; + + const expectedSupplyAssets = targetMarket.toSupplyAssets( + adapter.supplyShares[marketId] ?? 0n, + ); + const untracked = MathLib.zeroFloorSub( + expectedSupplyAssets, + allocations[2]!.allocation, + ); + + return { + adapter, + allocations, + marketPublicAllocatorConfig, + untracked, + }; + }, UnknownDataError); + if (targetContext == null) continue; + + const targetMarketParamsAllocation = + targetContext.allocations[2]!; + const allocatorHeadroom = MathLib.zeroFloorSub( + targetContext.marketPublicAllocatorConfig.absoluteCap, + targetMarketParamsAllocation.allocation + + targetContext.untracked, + ); + + if (publicAllocatorConfig.canPullFromIdle) { + const assets = MathLib.min( + MathLib.MAX_UINT_128, + targetSupplyHeadroom, + allocatorHeadroom, + MathLib.zeroFloorSub( + vault.assetBalance, + data.donatedPenaltyAssets[vaultAddress] ?? 0n, + ), + ); + if (assets > 0n) { + rawCandidates.push({ + allocator: data.allocator, + type: "bluePublicAllocator", + vault: vaultAddress, + from: { type: "idle" }, + to: { adapter: targetContext.adapter.address }, + assets, + penalty: publicAllocatorConfig.penalty, + }); + } + } + + for (const sourceAdapter of vault.accrualAdapters) { + if ( + !( + sourceAdapter instanceof + AccrualVaultV2MorphoMarketV1AdapterV2 + ) + ) + continue; + if (!isAddressEqual(sourceAdapter.parentVault, vaultAddress)) + continue; + + for (const sourceMarketReference of sourceAdapter.markets) { + const sourceMarket = data.getMarket(sourceMarketReference.id); + if ( + !isAddressEqual( + sourceMarket.params.loanToken, + vault.asset, + ) || + !isAddressEqual( + sourceMarket.params.irm, + sourceAdapter.adaptiveCurveIrm, + ) + ) + continue; + if (sourceMarket.id.toLowerCase() === marketId.toLowerCase()) + continue; + + const candidate = _try(() => { + const sourceIds = sourceAdapter.ids(sourceMarket.params); + const sourceConfig = data.getMarketPublicAllocatorConfig( + vaultAddress, + sourceIds[2], + ); + if ( + !isAddressEqual(sourceConfig.allocator, data.allocator) || + !isAddressEqual(sourceConfig.vault, vaultAddress) || + !isAddressEqual( + sourceConfig.adapter, + sourceAdapter.address, + ) || + !sourceConfig.isActiveAdapter || + !sourceConfig.canPullFromMarket + ) + return; + + const sourceAllocations = sourceIds.map((id) => + data.getAllocation(vaultAddress, id), + ); + if ( + sourceAllocations.some( + ({ allocation }) => allocation === 0n, + ) + ) + return; + + const expectedSupplyAssets = sourceMarket.toSupplyAssets( + sourceAdapter.supplyShares[sourceMarket.id] ?? 0n, + ); + const assets = MathLib.min( + MathLib.MAX_UINT_128, + targetSupplyHeadroom, + allocatorHeadroom, + expectedSupplyAssets, + sourceMarket.getWithdrawToUtilization( + maxWithdrawalUtilization, + ), + ); + if (assets <= 0n) return; + + return { + allocator: data.allocator, + type: "bluePublicAllocator", + vault: vaultAddress, + from: { + type: "market", + adapter: sourceAdapter.address, + marketParams: sourceMarket.params, + }, + to: { adapter: targetContext.adapter.address }, + assets, + penalty: publicAllocatorConfig.penalty, + } satisfies VaultV2BlueReallocation; + }, UnknownDataError); + if (candidate != null) rawCandidates.push(candidate); + } + } + } + + const capCompatibleCandidates: VaultV2BlueReallocation[] = []; + for (const reallocation of rawCandidates) { + let lower = 0n; + let upper = reallocation.assets; + + while (lower < upper) { + const assets = (lower + upper + 1n) / 2n; + const postState = data.cloneWithPublicReallocation({ + reallocation: { ...reallocation, assets }, + targetMarketId: marketId, + timestamp: targetMarket.lastUpdate, + }); + const postVault = postState.getVault(reallocation.vault); + const postAdapter = postState.getAdapter( + reallocation.vault, + reallocation.to.adapter, + ); + const targetIds = postAdapter.ids( + postState.getMarket(marketId).params, + ); + // Vault V2 checks relative caps against the transient firstTotalAssets, + // which stays fixed after the vault's first allocation in a transaction. + const firstTotalAssets = + postState.firstTotalAssets[reallocation.vault] ?? + postVault._totalAssets; + const withinCaps = targetIds.every((id) => { + const allocation = postState.getAllocation( + reallocation.vault, + id, + ); + const capacity = VaultV2Utils.allocationHeadroom( + { ...allocation, allocation: 0n }, + firstTotalAssets, + ).value; + return ( + allocation.absoluteCap > 0n && + allocation.allocation <= capacity + ); + }); + + if (withinCaps) lower = assets; + else upper = assets - 1n; + } + + if (lower > 0n) + capCompatibleCandidates.push({ + ...reallocation, + assets: lower, + }); + } + + return capCompatibleCandidates.sort( + bigIntComparator(({ assets }) => assets, "desc"), + )[0]; + }, UnknownDataError); + }) .filter( (candidate): candidate is VaultV2BlueReallocation => candidate != null, @@ -603,7 +913,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { if (largest == null) return { reallocations, data }; reallocations.push(largest); - data = data.applyPublicReallocation({ + data = data.cloneWithPublicReallocation({ reallocation: largest, targetMarketId: marketId, timestamp, @@ -627,10 +937,11 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { marketId: MarketId, options?: VaultV2BluePublicAllocatorOptions, ) { - return this.computeVaultV2Reallocations( + return this.computeVaultV2ReallocationsAtUtilization({ marketId, + maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, options, - ).reallocations.reduce((total, { assets }) => total + assets, 0n); + }).reallocations.reduce((total, { assets }) => total + assets, 0n); } /** @@ -685,281 +996,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { return timestamp; } - private accrueMarkets(timestamp: bigint) { - const data = this.clone(); - - for (const [marketId, market] of Object.entries(data.markets) as [ - MarketId, - Market | undefined, - ][]) { - if (market != null) - data.markets[marketId] = market.accrueInterest(timestamp); - } - - for (const [address, vault] of Object.entries(data.vaults) as [ - Address, - AccrualVaultV2 | undefined, - ][]) { - if (vault == null) continue; - for (const adapter of vault.accrualAdapters) { - if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) - continue; - adapter.markets = adapter.markets.map((market) => - data.getMarket(market.id), - ); - } - data.vaults[address] = vault; - } - - return data; - } - - private getLargestVaultReallocation({ - vaultAddress, - marketId, - maxWithdrawalUtilization, - maxPenalty, - }: { - readonly vaultAddress: Address; - readonly marketId: MarketId; - readonly maxWithdrawalUtilization: bigint; - readonly maxPenalty?: bigint; - }) { - const targetMarket = this.getMarket(marketId); - return _try(() => { - const vault = this.getVault(vaultAddress); - const publicAllocatorConfig = this.getPublicAllocatorConfig(vaultAddress); - if ( - !isAddressEqual(publicAllocatorConfig.allocator, this.allocator) || - !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) || - (maxPenalty != null && publicAllocatorConfig.penalty > maxPenalty) - ) - return; - - const targetSupplyHeadroom = MathLib.zeroFloorSub( - MathLib.MAX_UINT_128, - targetMarket.totalSupplyAssets, - ); - const candidates: VaultV2BlueReallocation[] = []; - - for (const adapter of vault.accrualAdapters) { - if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) - continue; - if (!isAddressEqual(adapter.parentVault, vaultAddress)) continue; - if ( - !isAddressEqual(targetMarket.params.loanToken, vault.asset) || - !isAddressEqual(targetMarket.params.irm, adapter.adaptiveCurveIrm) - ) - continue; - if ( - !adapter.markets.some((market) => sameMarketId(market.id, marketId)) - ) - continue; - - const targetContext = _try((): TargetContext | undefined => { - const ids = adapter.ids(targetMarket.params); - const marketPublicAllocatorConfig = - this.getMarketPublicAllocatorConfig(vaultAddress, ids[2]); - if ( - !isAddressEqual( - marketPublicAllocatorConfig.allocator, - this.allocator, - ) || - !isAddressEqual(marketPublicAllocatorConfig.vault, vaultAddress) || - !isAddressEqual( - marketPublicAllocatorConfig.adapter, - adapter.address, - ) || - !marketPublicAllocatorConfig.isActiveAdapter - ) - return; - - const allocations = ids.map((id) => - this.getAllocation(vaultAddress, id), - ); - if (allocations.some(({ absoluteCap }) => absoluteCap === 0n)) return; - - const expectedSupplyAssets = targetMarket.toSupplyAssets( - adapter.supplyShares[marketId] ?? 0n, - ); - const untracked = MathLib.zeroFloorSub( - expectedSupplyAssets, - allocations[2]!.allocation, - ); - - return { - adapter, - ids, - allocations, - marketPublicAllocatorConfig, - untracked, - }; - }, UnknownDataError); - if (targetContext == null) continue; - - const targetMarketParamsAllocation = targetContext.allocations[2]!; - const allocatorHeadroom = MathLib.zeroFloorSub( - targetContext.marketPublicAllocatorConfig.absoluteCap, - targetMarketParamsAllocation.allocation + targetContext.untracked, - ); - - if (publicAllocatorConfig.canPullFromIdle) { - const maxAssets = MathLib.min( - MathLib.MAX_UINT_128, - targetSupplyHeadroom, - allocatorHeadroom, - MathLib.zeroFloorSub( - vault.assetBalance, - this.donatedPenaltyAssets[vaultAddress] ?? 0n, - ), - ); - const reallocation = { - allocator: this.allocator, - type: "bluePublicAllocator", - vault: vaultAddress, - from: { type: "idle" }, - to: { adapter: targetContext.adapter.address }, - assets: maxAssets, - penalty: publicAllocatorConfig.penalty, - } satisfies VaultV2BlueReallocation; - const assets = this.getMaxCapCompatibleAssets({ - reallocation, - targetMarketId: marketId, - timestamp: targetMarket.lastUpdate, - }); - if (assets > 0n) { - candidates.push({ ...reallocation, assets }); - } - } - - for (const sourceAdapter of vault.accrualAdapters) { - if (!(sourceAdapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) - continue; - if (!isAddressEqual(sourceAdapter.parentVault, vaultAddress)) - continue; - - for (const sourceMarketReference of sourceAdapter.markets) { - const sourceMarket = this.getMarket(sourceMarketReference.id); - if ( - !isAddressEqual(sourceMarket.params.loanToken, vault.asset) || - !isAddressEqual( - sourceMarket.params.irm, - sourceAdapter.adaptiveCurveIrm, - ) - ) - continue; - if (sameMarketId(sourceMarket.id, marketId)) continue; - - const candidate = _try(() => { - const sourceIds = sourceAdapter.ids(sourceMarket.params); - const sourceConfig = this.getMarketPublicAllocatorConfig( - vaultAddress, - sourceIds[2], - ); - if ( - !isAddressEqual(sourceConfig.allocator, this.allocator) || - !isAddressEqual(sourceConfig.vault, vaultAddress) || - !isAddressEqual(sourceConfig.adapter, sourceAdapter.address) || - !sourceConfig.isActiveAdapter || - !sourceConfig.canPullFromMarket - ) - return; - - const sourceAllocations = sourceIds.map((id) => - this.getAllocation(vaultAddress, id), - ); - if (sourceAllocations.some(({ allocation }) => allocation === 0n)) - return; - - const expectedSupplyAssets = sourceMarket.toSupplyAssets( - sourceAdapter.supplyShares[sourceMarket.id] ?? 0n, - ); - const maxAssets = MathLib.min( - MathLib.MAX_UINT_128, - targetSupplyHeadroom, - allocatorHeadroom, - expectedSupplyAssets, - sourceMarket.getWithdrawToUtilization(maxWithdrawalUtilization), - ); - const reallocation = { - allocator: this.allocator, - type: "bluePublicAllocator", - vault: vaultAddress, - from: { - type: "market", - adapter: sourceAdapter.address, - marketParams: sourceMarket.params, - }, - to: { adapter: targetContext.adapter.address }, - assets: maxAssets, - penalty: publicAllocatorConfig.penalty, - } satisfies VaultV2BlueReallocation; - const assets = this.getMaxCapCompatibleAssets({ - reallocation, - targetMarketId: marketId, - timestamp: targetMarket.lastUpdate, - }); - if (assets <= 0n) return; - - return { ...reallocation, assets }; - }, UnknownDataError); - if (candidate != null) candidates.push(candidate); - } - } - } - - return candidates.sort( - bigIntComparator(({ assets }) => assets, "desc"), - )[0]; - }, UnknownDataError); - } - - private getMaxCapCompatibleAssets({ - reallocation, - targetMarketId, - timestamp, - }: { - readonly reallocation: VaultV2BlueReallocation; - readonly targetMarketId: MarketId; - readonly timestamp: bigint; - }) { - let lower = 0n; - let upper = reallocation.assets; - - while (lower < upper) { - const assets = (lower + upper + 1n) / 2n; - const postState = this.applyPublicReallocation({ - reallocation: { ...reallocation, assets }, - targetMarketId, - timestamp, - }); - const vault = postState.getVault(reallocation.vault); - const adapter = postState.getAdapter( - reallocation.vault, - reallocation.to.adapter, - ); - const targetIds = adapter.ids(postState.getMarket(targetMarketId).params); - // Vault V2 checks relative caps against the transient firstTotalAssets, - // which stays fixed after the vault's first allocation in a transaction. - const firstTotalAssets = - postState.firstTotalAssets[reallocation.vault] ?? vault._totalAssets; - const withinCaps = targetIds.every((id) => { - const allocation = postState.getAllocation(reallocation.vault, id); - const capacity = VaultV2Utils.allocationHeadroom( - { ...allocation, allocation: 0n }, - firstTotalAssets, - ).value; - return allocation.absoluteCap > 0n && allocation.allocation <= capacity; - }); - - if (withinCaps) lower = assets; - else upper = assets - 1n; - } - - return lower; - } - - private applyPublicReallocation({ + private cloneWithPublicReallocation({ reallocation, targetMarketId, timestamp, @@ -1005,18 +1042,26 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { } sourceAdapter.supplyShares[sourceMarket.id] = currentSupplyShares - withdrawal.shares; - data.markets[sourceMarket.id] = withdrawal.market; - data.setAdapterMarket(sourceAdapter, withdrawal.market); + data.setMarket(withdrawal.market); const sourceChange = withdrawal.market.toSupplyAssets( sourceAdapter.supplyShares[sourceMarket.id] ?? 0n, ) - data.getAllocation(reallocation.vault, sourceIds[2]).allocation; for (const id of sourceIds) { - data.addAllocationChange({ - vault: reallocation.vault, - id, - change: sourceChange, - }); + const allocation = data.getAllocation(reallocation.vault, id); + const nextAllocation = allocation.allocation + sourceChange; + if (nextAllocation < 0n) { + throw new ReallocationAllocationUnderflowError({ + vault: reallocation.vault, + id, + allocation: allocation.allocation, + change: sourceChange, + }); + } + data.allocations[reallocation.vault]![id] = { + ...allocation, + allocation: nextAllocation, + }; } vault.assetBalance += reallocation.assets; } @@ -1060,56 +1105,60 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { const targetSupplyShares = (targetAdapter.supplyShares[targetMarket.id] ?? 0n) + supply.shares; targetAdapter.supplyShares[targetMarket.id] = targetSupplyShares; - data.markets[targetMarket.id] = supply.market; - data.setAdapterMarket(targetAdapter, supply.market); + data.setMarket(supply.market); const targetChange = supply.market.toSupplyAssets(targetSupplyShares) - oldTargetAllocation; for (const id of targetIds) { - data.addAllocationChange({ - vault: reallocation.vault, - id, - change: targetChange, - }); + const allocation = data.getAllocation(reallocation.vault, id); + const nextAllocation = allocation.allocation + targetChange; + if (nextAllocation < 0n) { + throw new ReallocationAllocationUnderflowError({ + vault: reallocation.vault, + id, + allocation: allocation.allocation, + change: targetChange, + }); + } + data.allocations[reallocation.vault]![id] = { + ...allocation, + allocation: nextAllocation, + }; } vault.assetBalance -= reallocation.assets; return data; } - private addAllocationChange({ - vault, - id, - change, - }: { - readonly vault: Address; - readonly id: Hash; - readonly change: bigint; - }) { - const allocation = this.getAllocation(vault, id); - const nextAllocation = allocation.allocation + change; - if (nextAllocation < 0n) { - throw new ReallocationAllocationUnderflowError({ - vault, - id, - allocation: allocation.allocation, - change, - }); - } - this.allocations[vault]![id] = { - ...allocation, - allocation: nextAllocation, - }; - } + private setMarket(market: Market) { + this.markets[market.id] = market; - private setAdapterMarket( - adapter: AccrualVaultV2MorphoMarketV1AdapterV2, - market: Market, - ) { - const index = adapter.markets.findIndex((candidate) => - sameMarketId(candidate.id, market.id), - ); - if (index >= 0) adapter.markets[index] = market; + // A Morpho market is global state shared by every vault position. Legacy + // AccrualPosition constructors copy their Market, so rebuild those adapter + // views as well as repointing V2 adapters whenever the canonical state moves. + for (const vault of Object.values(this.vaults)) { + if (vault == null) continue; + const adapters = new Set(vault.accrualAdapters); + if (vault.accrualLiquidityAdapter != null) + adapters.add(vault.accrualLiquidityAdapter); + + for (const adapter of adapters) { + if (adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2) { + adapter.markets = adapter.markets.map((adapterMarket) => + getCanonicalMarket(this.markets, adapterMarket), + ); + } else if (adapter instanceof AccrualVaultV2MorphoMarketV1Adapter) { + adapter.positions = adapter.positions.map((position) => + clonePosition(position, this.markets), + ); + } else if (adapter instanceof AccrualVaultV2MorphoVaultV1Adapter) { + adapter.accrualVaultV1 = cloneAccrualVault( + adapter.accrualVaultV1, + this.markets, + ); + } + } + } } } From 6649e1c6dcb24ac4e43f90d58007125dd7544ce9 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Mon, 17 Aug 2026 13:57:59 +0200 Subject: [PATCH 16/41] refactor: split Vault V2 liquidity loader --- .changeset/brave-vaults-reallocate.md | 3 - ...lt-v2-public-allocator-shared-liquidity.md | 10 +- packages/liquidity-sdk-viem/AGENTS.md | 5 +- packages/liquidity-sdk-viem/README.md | 68 +-- packages/liquidity-sdk-viem/package.json | 10 +- .../liquidity-sdk-viem/src/api/rest.test.ts | 57 -- packages/liquidity-sdk-viem/src/api/rest.ts | 540 ----------------- packages/liquidity-sdk-viem/src/errors.ts | 144 ----- packages/liquidity-sdk-viem/src/index.ts | 2 - packages/liquidity-sdk-viem/src/loader.ts | 12 +- .../src/vaultV2LiquidityLoader.test.ts | 515 ---------------- .../src/vaultV2LiquidityLoader.ts | 566 ------------------ 12 files changed, 45 insertions(+), 1887 deletions(-) delete mode 100644 packages/liquidity-sdk-viem/src/api/rest.test.ts delete mode 100644 packages/liquidity-sdk-viem/src/api/rest.ts delete mode 100644 packages/liquidity-sdk-viem/src/errors.ts delete mode 100644 packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts delete mode 100644 packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 10c8e5d0f..808e99ace 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -3,7 +3,6 @@ "@morpho-org/blue-sdk": minor "@morpho-org/blue-sdk-viem": minor "@morpho-org/morpho-sdk": minor -"@morpho-org/liquidity-sdk-viem": minor "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- @@ -15,6 +14,4 @@ Use coherent versioned names across the V1 and V2 reallocation APIs, including ` Compatibility note: `VaultV2MorphoMarketV1AdapterV2.ids()` now declares its existing three-element result as `readonly [Hash, Hash, Hash]`. The runtime values and ordering are unchanged, and derived allocation identifiers are immutable descriptors. We intentionally accept this TypeScript assignability tightening in the minor release; callers that explicitly require a mutable `Hash[]` can copy the tuple with `[...adapter.ids(params)]`. -Add an independent REST-backed `VaultV2LiquidityLoader` alongside the existing Vault V1 loader. It validates successful API payloads at runtime, pins REST and RPC hydration to one indexed block, anchors live REST market totals to that block's timestamp to prevent double accrual, and fails explicitly on incomplete positions instead of treating missing state as zero. Raise its `blue-sdk`, `blue-sdk-viem`, `morpho-sdk`, and `morpho-ts` peer floors to the introducing versions. - Add an explicit `MorphoBorrowWithV2ReallocationsOptions` WDK opt-in for the combined V1/V2 reallocation union and its possible approval requirement while preserving the legacy `MorphoBorrowOptions` input and authorization-only requirement result type. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index 13a318609..b150e5658 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -5,7 +5,7 @@ | **Status** | Accepted | | **Date** | 2026-07-29 | | **Author** | @foulques | -| **Scope** | `morpho-sdk`, `liquidity-sdk-viem`, `blue-sdk-viem`, `blue-sdk`, and `morpho-ts` | +| **Scope** | `morpho-sdk`, `blue-sdk-viem`, `blue-sdk`, and `morpho-ts` | ## Context @@ -409,15 +409,9 @@ source and target thresholds plus an internal 100% fallback. deprecated. - `BluePublicAllocatorReallocation` receives no alias because it was not part of the published surface relative to `origin/main`. -- The compatible `liquidity-sdk-viem` V1 type-name migration would be a patch - in isolation. The new public Vault V2 loader makes this package a minor. - The feature is minor for `morpho-ts`, `blue-sdk`, `blue-sdk-viem`, - `morpho-sdk`, `liquidity-sdk-viem`, and - `wdk-protocol-lending-morpho-evm`. + `morpho-sdk`, and `wdk-protocol-lending-morpho-evm`. - `blue-sdk-viem` raises its `blue-sdk` peer range to the new minor. -- `liquidity-sdk-viem` raises its `blue-sdk`, `blue-sdk-viem`, `morpho-sdk`, - and `morpho-ts` peer floors to the versions that introduce the V2 loader's - runtime imports. ## Security and operational constraints diff --git a/packages/liquidity-sdk-viem/AGENTS.md b/packages/liquidity-sdk-viem/AGENTS.md index c2a367d42..be782a201 100644 --- a/packages/liquidity-sdk-viem/AGENTS.md +++ b/packages/liquidity-sdk-viem/AGENTS.md @@ -2,15 +2,14 @@ - GraphQL queries live in `graphql/*.gql`; regenerate API types with this package's `codegen` script. - Do not hand-edit generated `src/api/sdk.ts`; update queries or `codegen.ts` instead. -- `loader.ts` owns Vault V1 planning and `vaultV2LiquidityLoader.ts` owns the independent Vault V2 planner. Keep their state fetching and simulation flows separate; do not use inheritance or a shared mutable state model across allocator versions. +- `loader.ts` is the package core; keep public liquidity planning behavior there. - Loader code batches by market ID through `DataLoader`. - Snapshot all onchain state at one block before simulation, e.g. pass `{ blockNumber: block.number }`. - Convert API maps through `fromEntries` and filter with `isDefined`. - Public liquidity options use WAD-scaled `bigint` thresholds. - `apiSdk` is a singleton `GraphQLClient` bound to `BLUE_API_GRAPHQL_URL`. - Batch expensive market requests by chunking IDs before paginating. -- Keep loader output deterministic: return version-appropriate `withdrawals` or `reallocations`, `startState`, `endState`, and utilization. -- Vault V2 has no canonical BluePublicAllocator registry entry. Its loader takes the allocator and participating Vault V2 addresses explicitly and never infers them from chain configuration. +- Keep loader output deterministic: return `withdrawals`, `startState`, `endState`, and utilization. ## Continuous Improvement diff --git a/packages/liquidity-sdk-viem/README.md b/packages/liquidity-sdk-viem/README.md index fbce70c31..e4fe12e36 100644 --- a/packages/liquidity-sdk-viem/README.md +++ b/packages/liquidity-sdk-viem/README.md @@ -23,7 +23,7 @@ ## Overview -Viem-based loaders for computing shared liquidity from PublicAllocator V1 and the Vault V2 BluePublicAllocator. +Viem-based package that provides utilities to build viem-based liquidity bots on Morpho and examples using Flashbots and Morpho's GraphQL API. ## Installation @@ -37,52 +37,44 @@ yarn add @morpho-org/liquidity-sdk-viem ## Usage -### Vault V1 +### Fetch from API or RPC ```typescript -import type { MarketId } from "@morpho-org/blue-sdk"; import { LiquidityLoader } from "@morpho-org/liquidity-sdk-viem"; -import { createPublicClient, http } from "viem"; -import { mainnet } from "viem/chains"; -const client = createPublicClient({ chain: mainnet, transport: http() }); -const loader = new LiquidityLoader(client); -const marketId = - "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc" as MarketId; - -const { withdrawals, startState, endState, targetBorrowUtilization } = - await loader.fetch(marketId); +const loader = new LiquidityLoader( + client // viem client. +); + +const [withdrawals1, withdrawals2] = await Promise.all([ + loader.fetch( + "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc" as MarketId, + "api" + ), + loader.fetch( + "0xe475337d11be1db07f7c5a156e511f05d1844308e66e17d2ba5da0839d3b34d9" as MarketId, + "rpc" + ), +]); ``` -`LiquidityLoader` discovers PublicAllocator V1 vaults through the Morpho API, snapshots their state through the viem client, and returns source-market withdrawals. - -### Vault V2 +### Fetch only from API ```typescript -import type { MarketId } from "@morpho-org/blue-sdk"; -import { - type VaultV2LiquidityResult, - VaultV2LiquidityLoader, -} from "@morpho-org/liquidity-sdk-viem"; -import { type Address, createPublicClient, http } from "viem"; -import { mainnet } from "viem/chains"; - -export async function loadVaultV2Liquidity( - allocator: Address, - vault: Address, - marketId: MarketId, -): Promise { - const client = createPublicClient({ chain: mainnet, transport: http() }); - const loader = new VaultV2LiquidityLoader(client, { - allocator, - vaults: [vault], - maxPenalty: 1_000_000_000_000_000n, - }); - return loader.fetch(marketId); -} -``` +import { ChainId } from "@morpho-org/blue-sdk"; +import { LiquidityLoader } from "@morpho-org/liquidity-sdk-viem"; + +const loader = new LiquidityLoader({ chainId: ChainId.EthMainnet }); -`VaultV2LiquidityLoader` is a separate REST-backed loader. It reads Vault V2 configuration, state, allocations, withdrawal penalties, Blue market state, adapter positions, oracle prices, and adaptive-curve IRM state from the Morpho REST APIs. BluePublicAllocator-only configuration remains an onchain read through the supplied viem client. The allocator and participating Vault V2 addresses are explicit because the protocol has no canonical allocator registry entry. Its `reallocations` can be passed directly to Morpho SDK Blue borrow and withdraw actions. +const [withdrawals1, withdrawals2] = await Promise.all([ + loader.fetch( + "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc" as MarketId + ), + loader.fetch( + "0xe475337d11be1db07f7c5a156e511f05d1844308e66e17d2ba5da0839d3b34d9" as MarketId + ), +]); +``` ## Development diff --git a/packages/liquidity-sdk-viem/package.json b/packages/liquidity-sdk-viem/package.json index 1fde2376c..e00521965 100644 --- a/packages/liquidity-sdk-viem/package.json +++ b/packages/liquidity-sdk-viem/package.json @@ -1,6 +1,6 @@ { "name": "@morpho-org/liquidity-sdk-viem", - "description": "Viem-based package that calculates shared liquidity through PublicAllocator V1 and the Vault V2 BluePublicAllocator.", + "description": "Viem-based package that helps seamlessly calculate the liquidity available through the PublicAllocator.", "version": "4.1.1", "author": "Morpho Association ", "contributors": [ @@ -29,10 +29,10 @@ "codegen": "graphql-codegen --config codegen.ts" }, "peerDependencies": { - "@morpho-org/blue-sdk": "^6.5.0", - "@morpho-org/blue-sdk-viem": "^5.3.0", - "@morpho-org/morpho-sdk": "^5.5.0", - "@morpho-org/morpho-ts": "^2.9.0", + "@morpho-org/blue-sdk": "^6.0.0", + "@morpho-org/blue-sdk-viem": "^5.0.0", + "@morpho-org/morpho-sdk": "^5.4.0", + "@morpho-org/morpho-ts": "^2.7.0", "dataloader": "^2.2.3", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", "graphql-request": "^6.1.0", diff --git a/packages/liquidity-sdk-viem/src/api/rest.test.ts b/packages/liquidity-sdk-viem/src/api/rest.test.ts deleted file mode 100644 index 414cf3d8f..000000000 --- a/packages/liquidity-sdk-viem/src/api/rest.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { BLUE_API_BASE_URL } from "@morpho-org/morpho-ts"; -import nock from "nock"; -import { type Address, zeroAddress, zeroHash } from "viem"; -import { mainnet } from "viem/chains"; -import { afterEach, describe, expect, test } from "vitest"; -import { InvalidVaultV2LiquidityApiResponseError } from "../errors.js"; -import { fetchRestVaultV2Allocations } from "./rest.js"; - -const VAULT: Address = "0x0000000000000000000000000000000000000001"; - -const cap = { - cap_id: zeroHash, - cap_data: "0x", - allocated_assets: "0", - absolute_cap: "1000", - relative_cap_wad: "1000000000000000000", -} as const; - -describe.sequential("fetchRestVaultV2Allocations", () => { - afterEach(() => { - nock.cleanAll(); - }); - - test.each([ - ["market_v1 cap without market_id", { ...cap, cap_type: "market_v1" }], - [ - "collateral cap without collateral_address", - { ...cap, cap_type: "collateral" }, - ], - ] as const)( - "error: InvalidVaultV2LiquidityApiResponseError for %s", - async (_case, malformedCap) => { - const api = nock(BLUE_API_BASE_URL) - .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}/allocations`) - .reply(200, { - data: { - chain_id: mainnet.id, - vault_address: VAULT, - last_indexed_block: "1", - allocations: [ - { - adapter_address: zeroAddress, - adapter_kind: "morpho_market_v1_v2", - caps: [malformedCap], - }, - ], - unscoped_caps: [], - }, - }); - - await expect( - fetchRestVaultV2Allocations(mainnet.id, VAULT), - ).rejects.toBeInstanceOf(InvalidVaultV2LiquidityApiResponseError); - api.done(); - }, - ); -}); diff --git a/packages/liquidity-sdk-viem/src/api/rest.ts b/packages/liquidity-sdk-viem/src/api/rest.ts deleted file mode 100644 index 5a5cfeb5e..000000000 --- a/packages/liquidity-sdk-viem/src/api/rest.ts +++ /dev/null @@ -1,540 +0,0 @@ -import type { MarketId } from "@morpho-org/blue-sdk"; -import { BLUE_API_BASE_URL } from "@morpho-org/morpho-ts"; -import { - type Address, - type Hash, - type Hex, - isAddress, - isAddressEqual, - isHex, - size, -} from "viem"; -import { - InvalidVaultV2LiquidityApiResponseError, - MissingVaultV2LiquidityApiDataError, - VaultV2LiquidityApiError, -} from "../errors.js"; - -interface VaultV2AssetResponse { - readonly address: Address; - readonly decimals: number; - readonly name: string; - readonly symbol: string; -} - -interface VaultV2GatesResponse { - readonly send_shares: Address | null; - readonly receive_shares: Address | null; - readonly send_assets: Address | null; - readonly receive_assets: Address | null; -} - -interface VaultV2Response { - readonly chain_id: number; - readonly address: Address; - readonly last_indexed_block: string; - readonly version: string; - readonly name: string; - readonly symbol: string; - readonly asset: VaultV2AssetResponse; - readonly decimals_offset: number; - readonly factory_address: Address; - readonly creation_block_number: string; - readonly owner: Address; - readonly curator: Address; - readonly timelock_seconds: number; - readonly management_fee_wad: string | null; - readonly management_fee_recipient: Address | null; - readonly performance_fee_wad: string | null; - readonly performance_fee_recipient: Address | null; - readonly max_rate_per_second_wad: string; - readonly adapter_registry: Address; - readonly liquidity_adapter: Address; - readonly liquidity_data: Hex; - readonly gates: VaultV2GatesResponse; -} - -interface VaultV2StateResponse { - readonly chain_id: number; - readonly address: Address; - readonly last_indexed_block: string; - readonly last_accrual_timestamp: number; - readonly total_assets: string; - readonly total_supply: string; - readonly withdrawable_assets: string; - readonly allocated_assets: string; - readonly idle_assets: string; - readonly share_price_ray: string; -} - -interface VaultV2CapResponseBase { - readonly cap_id: Hash; - readonly cap_data: Hex; - readonly allocated_assets: string; - readonly absolute_cap: string; - readonly relative_cap_wad: string; -} - -type VaultV2CapResponse = VaultV2CapResponseBase & - ( - | { - readonly cap_type: "adapter"; - readonly market_id?: MarketId; - readonly collateral_address?: Address; - } - | { - readonly cap_type: "collateral"; - readonly market_id?: MarketId; - readonly collateral_address: Address; - } - | { - readonly cap_type: "market_v1"; - readonly market_id: MarketId; - readonly collateral_address?: Address; - } - ); - -interface VaultV2AdapterAllocationResponse { - readonly adapter_address: Address; - readonly adapter_kind: - | "morpho_market_v1" - | "morpho_market_v1_v2" - | "morpho_vault_v1" - | "morpho_vault_v2"; - readonly caps: readonly VaultV2CapResponse[]; -} - -interface VaultV2AllocationsResponse { - readonly chain_id: number; - readonly vault_address: Address; - readonly last_indexed_block: string; - readonly allocations: readonly VaultV2AdapterAllocationResponse[]; - readonly unscoped_caps: readonly VaultV2CapResponse[]; -} - -interface VaultV2AdapterPenaltyResponse { - readonly adapter_address: Address; - readonly adapter_kind: - | "blue_market_adapter" - | "vault_v1_adapter" - | "vault_v2_adapter" - | "unknown_adapter"; - readonly force_deallocatable_assets: string; - readonly penalty_rate_wad: string; -} - -interface VaultV2WithdrawalOptionsResponse { - readonly chain_id: number; - readonly vault_address: Address; - readonly liquidity_adapter_available_assets: string; - readonly idle_assets: string; - readonly adapter_penalties: readonly VaultV2AdapterPenaltyResponse[]; -} - -interface MarketResponse { - readonly chain_id: number; - readonly market_id: MarketId; - readonly loan_token: Address; - readonly collateral_token: Address; - readonly oracle_address: Address; - readonly irm_address: Address; - readonly lltv_wad: string; - readonly creation_block_number: string; -} - -interface MarketStateResponse { - readonly chain_id: number; - readonly market_id: MarketId; - readonly last_indexed_block: string; - readonly last_accrual_timestamp: number; - readonly total_supply_assets: string; - readonly total_supply_shares: string; - readonly total_borrow_assets: string; - readonly total_borrow_shares: string; - readonly fee_wad: string; -} - -interface MarketPositionResponse { - readonly chain_id: number; - readonly market_id: MarketId; - readonly user_address: Address; - readonly last_indexed_block: string; - readonly collateral_assets: string; - readonly supply_shares: string; - readonly borrow_shares: string; -} - -interface MarketPositionParameters { - readonly chainId: number; - readonly marketId: MarketId; - readonly user: Address; -} - -interface OracleStateResponse { - readonly chain_id: number; - readonly oracle_address: Address; - readonly last_indexed_block: string; - readonly last_updated_at?: string | null; - readonly price?: string | null; -} - -interface MarketIrmResponse { - readonly chainId: number; - readonly marketId: MarketId; - readonly irmAddress: Address; - /** @deprecated The consumer API always returns the fixed 90% target. */ - readonly targetUtilization: number; - readonly utilization: number | null; - readonly apyAtTarget: number | null; - readonly rateAtTarget?: string | null; - readonly borrowToTarget: number | null; -} - -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value); -const isInteger = (value: unknown): value is number => - typeof value === "number" && Number.isSafeInteger(value); -const isFiniteNumber = (value: unknown): value is number => - typeof value === "number" && Number.isFinite(value); -const isDecimalString = (value: unknown): value is string => - typeof value === "string" && /^\d+$/.test(value); -const isAddressValue = (value: unknown): value is Address => - typeof value === "string" && isAddress(value); -const isNullableAddress = (value: unknown): value is Address | null => - value === null || isAddressValue(value); -const isHexValue = (value: unknown): value is Hex => - typeof value === "string" && isHex(value, { strict: true }); -const isHashValue = (value: unknown): value is Hash => - isHexValue(value) && size(value) === 32; - -async function requestApi( - path: string, - { - validator, - responseKind = "envelope", - }: { - readonly validator: (value: unknown) => value is Data; - readonly responseKind?: "envelope" | "root"; - }, -): Promise { - const url = new URL(path, BLUE_API_BASE_URL); - let response: Response; - try { - response = await globalThis.fetch(url, { - headers: { Accept: "application/json" }, - }); - } catch (error) { - throw new VaultV2LiquidityApiError({ - url: url.toString(), - cause: error, - }); - } - - if (!response.ok) - throw new VaultV2LiquidityApiError({ - url: url.toString(), - status: response.status, - }); - - let body: unknown; - try { - body = await response.json(); - } catch (error) { - throw new VaultV2LiquidityApiError({ - url: url.toString(), - status: response.status, - cause: error, - }); - } - - if (body == null) - throw new MissingVaultV2LiquidityApiDataError(url.toString()); - const data = - responseKind === "root" ? body : isRecord(body) ? body.data : null; - if (data == null) - throw new MissingVaultV2LiquidityApiDataError(url.toString()); - if (!validator(data)) - throw new InvalidVaultV2LiquidityApiResponseError(url.toString()); - return data; -} - -const apiSelector = (chainId: number, identifier: string) => - `${chainId}:${encodeURIComponent(identifier)}`; - -/** @internal Fetches Vault V2 configuration from the Morpho REST API. */ -export const fetchRestVaultV2 = (chainId: number, address: Address) => - requestApi( - `/v0/vaults-v2/${apiSelector(chainId, address)}`, - { - validator: (value): value is VaultV2Response => { - if ( - !isRecord(value) || - !isRecord(value.asset) || - !isRecord(value.gates) - ) - return false; - const { asset, gates } = value; - return ( - isInteger(value.chain_id) && - value.chain_id === chainId && - isAddressValue(value.address) && - isAddressEqual(value.address, address) && - isDecimalString(value.last_indexed_block) && - typeof value.version === "string" && - typeof value.name === "string" && - typeof value.symbol === "string" && - isAddressValue(asset.address) && - isInteger(asset.decimals) && - typeof asset.name === "string" && - typeof asset.symbol === "string" && - isInteger(value.decimals_offset) && - isAddressValue(value.factory_address) && - isDecimalString(value.creation_block_number) && - isAddressValue(value.owner) && - isAddressValue(value.curator) && - isInteger(value.timelock_seconds) && - (value.management_fee_wad === null || - isDecimalString(value.management_fee_wad)) && - isNullableAddress(value.management_fee_recipient) && - (value.performance_fee_wad === null || - isDecimalString(value.performance_fee_wad)) && - isNullableAddress(value.performance_fee_recipient) && - isDecimalString(value.max_rate_per_second_wad) && - isAddressValue(value.adapter_registry) && - isAddressValue(value.liquidity_adapter) && - isHexValue(value.liquidity_data) && - isNullableAddress(gates.send_shares) && - isNullableAddress(gates.receive_shares) && - isNullableAddress(gates.send_assets) && - isNullableAddress(gates.receive_assets) - ); - }, - }, - ); - -/** @internal Fetches Vault V2 accounting state from the Morpho REST API. */ -export const fetchRestVaultV2State = (chainId: number, address: Address) => - requestApi( - `/v1/vaults-v2/${apiSelector(chainId, address)}/state`, - { - validator: (value): value is VaultV2StateResponse => - isRecord(value) && - isInteger(value.chain_id) && - value.chain_id === chainId && - isAddressValue(value.address) && - isAddressEqual(value.address, address) && - isDecimalString(value.last_indexed_block) && - isInteger(value.last_accrual_timestamp) && - isDecimalString(value.total_assets) && - isDecimalString(value.total_supply) && - isDecimalString(value.withdrawable_assets) && - isDecimalString(value.allocated_assets) && - isDecimalString(value.idle_assets) && - isDecimalString(value.share_price_ray), - }, - ); - -/** @internal Fetches Vault V2 adapter allocations and cap state from the Morpho REST API. */ -export const fetchRestVaultV2Allocations = ( - chainId: number, - address: Address, -) => - requestApi( - `/v0/vaults-v2/${apiSelector(chainId, address)}/allocations`, - { - validator: (value): value is VaultV2AllocationsResponse => { - if ( - !isRecord(value) || - !isInteger(value.chain_id) || - value.chain_id !== chainId || - !isAddressValue(value.vault_address) || - !isAddressEqual(value.vault_address, address) || - !isDecimalString(value.last_indexed_block) || - !Array.isArray(value.allocations) || - !Array.isArray(value.unscoped_caps) - ) - return false; - - const caps = [...value.unscoped_caps]; - for (const adapter of value.allocations) { - if ( - !isRecord(adapter) || - !isAddressValue(adapter.adapter_address) || - (adapter.adapter_kind !== "morpho_market_v1" && - adapter.adapter_kind !== "morpho_market_v1_v2" && - adapter.adapter_kind !== "morpho_vault_v1" && - adapter.adapter_kind !== "morpho_vault_v2") || - !Array.isArray(adapter.caps) - ) - return false; - caps.push(...adapter.caps); - } - - return caps.every((cap) => { - if ( - !isRecord(cap) || - !isHashValue(cap.cap_id) || - !isHexValue(cap.cap_data) || - !isDecimalString(cap.allocated_assets) || - !isDecimalString(cap.absolute_cap) || - !isDecimalString(cap.relative_cap_wad) || - (cap.market_id !== undefined && !isHashValue(cap.market_id)) || - (cap.collateral_address !== undefined && - !isAddressValue(cap.collateral_address)) - ) - return false; - - switch (cap.cap_type) { - case "adapter": - return true; - case "collateral": - return isAddressValue(cap.collateral_address); - case "market_v1": - return isHashValue(cap.market_id); - default: - return false; - } - }); - }, - }, - ); - -/** @internal Fetches Vault V2 adapter force-deallocation penalties from the Morpho REST API. */ -export const fetchRestVaultV2WithdrawalOptions = ( - chainId: number, - address: Address, -) => - requestApi( - `/v0/vaults-v2/${apiSelector(chainId, address)}/withdrawal-options`, - { - validator: (value): value is VaultV2WithdrawalOptionsResponse => - isRecord(value) && - isInteger(value.chain_id) && - value.chain_id === chainId && - isAddressValue(value.vault_address) && - isAddressEqual(value.vault_address, address) && - isDecimalString(value.liquidity_adapter_available_assets) && - isDecimalString(value.idle_assets) && - Array.isArray(value.adapter_penalties) && - value.adapter_penalties.every( - (penalty) => - isRecord(penalty) && - isAddressValue(penalty.adapter_address) && - (penalty.adapter_kind === "blue_market_adapter" || - penalty.adapter_kind === "vault_v1_adapter" || - penalty.adapter_kind === "vault_v2_adapter" || - penalty.adapter_kind === "unknown_adapter") && - isDecimalString(penalty.force_deallocatable_assets) && - isDecimalString(penalty.penalty_rate_wad), - ), - }, - ); - -/** @internal Fetches Morpho Blue market configuration from the REST API. */ -export const fetchRestMarket = (chainId: number, marketId: MarketId) => - requestApi( - `/v0/blue/markets/${apiSelector(chainId, marketId)}`, - { - validator: (value): value is MarketResponse => - isRecord(value) && - isInteger(value.chain_id) && - value.chain_id === chainId && - isHashValue(value.market_id) && - value.market_id.toLowerCase() === marketId.toLowerCase() && - isAddressValue(value.loan_token) && - isAddressValue(value.collateral_token) && - isAddressValue(value.oracle_address) && - isAddressValue(value.irm_address) && - isDecimalString(value.lltv_wad) && - isDecimalString(value.creation_block_number), - }, - ); - -/** @internal Fetches Morpho Blue market accounting state from the REST API. */ -export const fetchRestMarketState = (chainId: number, marketId: MarketId) => - requestApi( - `/v0/blue/markets/${apiSelector(chainId, marketId)}/state`, - { - validator: (value): value is MarketStateResponse => - isRecord(value) && - isInteger(value.chain_id) && - value.chain_id === chainId && - isHashValue(value.market_id) && - value.market_id.toLowerCase() === marketId.toLowerCase() && - isDecimalString(value.last_indexed_block) && - isInteger(value.last_accrual_timestamp) && - isDecimalString(value.total_supply_assets) && - isDecimalString(value.total_supply_shares) && - isDecimalString(value.total_borrow_assets) && - isDecimalString(value.total_borrow_shares) && - isDecimalString(value.fee_wad), - }, - ); - -/** @internal Fetches a Morpho Blue market position from the REST API. */ -export const fetchRestMarketPosition = ({ - chainId, - marketId, - user, -}: MarketPositionParameters) => - requestApi( - `/v0/blue/markets/${apiSelector(chainId, marketId)}/users/${encodeURIComponent(user)}/position`, - { - validator: (value): value is MarketPositionResponse => - isRecord(value) && - isInteger(value.chain_id) && - value.chain_id === chainId && - isHashValue(value.market_id) && - value.market_id.toLowerCase() === marketId.toLowerCase() && - isAddressValue(value.user_address) && - isAddressEqual(value.user_address, user) && - isDecimalString(value.last_indexed_block) && - isDecimalString(value.collateral_assets) && - isDecimalString(value.supply_shares) && - isDecimalString(value.borrow_shares), - }, - ); - -/** @internal Fetches a Morpho Blue oracle price from the REST API. */ -export const fetchRestOracleState = (chainId: number, address: Address) => - requestApi( - `/v0/oracles/${apiSelector(chainId, address)}/state`, - { - validator: (value): value is OracleStateResponse => - isRecord(value) && - isInteger(value.chain_id) && - value.chain_id === chainId && - isAddressValue(value.oracle_address) && - isAddressEqual(value.oracle_address, address) && - isDecimalString(value.last_indexed_block) && - (value.last_updated_at === undefined || - value.last_updated_at === null || - isDecimalString(value.last_updated_at)) && - (value.price === undefined || - value.price === null || - isDecimalString(value.price)), - }, - ); - -/** @internal Fetches a Morpho Blue market's adaptive-curve IRM state from the REST API. */ -export const fetchRestMarketIrm = (chainId: number, marketId: MarketId) => - requestApi( - `/consumer/chains/${chainId}/markets/${encodeURIComponent(marketId)}/irm`, - { - validator: (value): value is MarketIrmResponse => - isRecord(value) && - isInteger(value.chainId) && - value.chainId === chainId && - isHashValue(value.marketId) && - value.marketId.toLowerCase() === marketId.toLowerCase() && - isAddressValue(value.irmAddress) && - isFiniteNumber(value.targetUtilization) && - (value.utilization === null || isFiniteNumber(value.utilization)) && - (value.apyAtTarget === null || isFiniteNumber(value.apyAtTarget)) && - (value.rateAtTarget === undefined || - value.rateAtTarget === null || - isDecimalString(value.rateAtTarget)) && - (value.borrowToTarget === null || isFiniteNumber(value.borrowToTarget)), - responseKind: "root", - }, - ); diff --git a/packages/liquidity-sdk-viem/src/errors.ts b/packages/liquidity-sdk-viem/src/errors.ts deleted file mode 100644 index f12e1be53..000000000 --- a/packages/liquidity-sdk-viem/src/errors.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Thrown when the Morpho API cannot provide Vault V2 liquidity data. - * - * @example - * ```ts - * import { VaultV2LiquidityApiError } from "@morpho-org/liquidity-sdk-viem"; - * - * const error = new VaultV2LiquidityApiError({ - * url: "https://api.morpho.org/v0/vaults-v2", - * status: 503, - * }); - * console.error(error.status, error.url); - * ``` - */ -export class VaultV2LiquidityApiError extends Error { - /** HTTP status returned by the Morpho API, when a response was received. */ - public readonly status?: number; - - /** API endpoint that failed. */ - public readonly url: string; - - /** - * Creates a typed Vault V2 API failure. - * - * @param parameters - Endpoint, optional HTTP status, and optional lower-level failure. - */ - public constructor(parameters: { - readonly url: string; - readonly status?: number; - readonly cause?: unknown; - }) { - super( - parameters.status === undefined - ? `Morpho API request to "${parameters.url}" failed before receiving an HTTP response. Retry the request or verify network connectivity.` - : `Morpho API request to "${parameters.url}" failed with HTTP status "${parameters.status}". Retry the request or verify the Vault V2 configuration.`, - parameters.cause === undefined ? undefined : { cause: parameters.cause }, - ); - this.name = "VaultV2LiquidityApiError"; - this.status = parameters.status; - this.url = parameters.url; - } -} - -/** - * Thrown when a successful Morpho API response omits data required for a Vault V2 simulation. - * - * @example - * ```ts - * import { MissingVaultV2LiquidityApiDataError } from "@morpho-org/liquidity-sdk-viem"; - * - * const error = new MissingVaultV2LiquidityApiDataError( - * "adaptive-curve IRM rateAtTarget", - * ); - * console.error(error.resource); - * ``` - */ -export class MissingVaultV2LiquidityApiDataError extends Error { - /** Description of the missing API resource. */ - public readonly resource: string; - - /** - * Creates a typed missing API data failure. - * - * @param resource - Description of the missing API resource. - */ - public constructor(resource: string) { - super( - `Morpho API response omitted required Vault V2 liquidity data for "${resource}". Retry after the API indexer catches up.`, - ); - this.name = "MissingVaultV2LiquidityApiDataError"; - this.resource = resource; - } -} - -/** - * Thrown when a successful Morpho API response has an invalid runtime shape. - * - * @example - * ```ts - * import { InvalidVaultV2LiquidityApiResponseError } from "@morpho-org/liquidity-sdk-viem"; - * - * const error = new InvalidVaultV2LiquidityApiResponseError( - * "https://api.morpho.org/v1/vaults-v2", - * ); - * console.error(error.url); - * ``` - */ -export class InvalidVaultV2LiquidityApiResponseError extends Error { - /** API endpoint that returned malformed JSON data. */ - public readonly url: string; - - /** - * @param url - API endpoint whose successful response failed validation. - */ - public constructor(url: string) { - super( - `Morpho API response from "${url}" is not valid Vault V2 liquidity data. Retry after the API indexer recovers.`, - ); - this.name = "InvalidVaultV2LiquidityApiResponseError"; - this.url = url; - } -} - -/** - * Thrown when REST resources required for one liquidity plan were indexed at - * different blocks. - * - * @example - * ```ts - * import { InconsistentVaultV2LiquiditySnapshotError } from "@morpho-org/liquidity-sdk-viem"; - * - * const error = new InconsistentVaultV2LiquiditySnapshotError({ - * resource: "market state", - * expectedBlock: 20_000_000n, - * actualBlock: 20_000_001n, - * }); - * console.error(error.resource, error.expectedBlock, error.actualBlock); - * ``` - */ -export class InconsistentVaultV2LiquiditySnapshotError extends Error { - /** REST resource whose indexed block differs. */ - public readonly resource: string; - /** Indexed block selected for the plan. */ - public readonly expectedBlock: bigint; - /** Indexed block reported by the inconsistent resource. */ - public readonly actualBlock: bigint; - - /** - * @param parameters - Resource name and conflicting indexed blocks. - */ - public constructor(parameters: { - readonly resource: string; - readonly expectedBlock: bigint; - readonly actualBlock: bigint; - }) { - super( - `Vault V2 liquidity snapshot requires indexed block "${parameters.expectedBlock}", but "${parameters.resource}" reports "${parameters.actualBlock}". Retry after the API indexer converges.`, - ); - this.name = "InconsistentVaultV2LiquiditySnapshotError"; - this.resource = parameters.resource; - this.expectedBlock = parameters.expectedBlock; - this.actualBlock = parameters.actualBlock; - } -} diff --git a/packages/liquidity-sdk-viem/src/index.ts b/packages/liquidity-sdk-viem/src/index.ts index faf3d0d14..dd8245678 100644 --- a/packages/liquidity-sdk-viem/src/index.ts +++ b/packages/liquidity-sdk-viem/src/index.ts @@ -1,3 +1 @@ -export * from "./errors.js"; export * from "./loader.js"; -export * from "./vaultV2LiquidityLoader.js"; diff --git a/packages/liquidity-sdk-viem/src/loader.ts b/packages/liquidity-sdk-viem/src/loader.ts index af1530271..b824cafbf 100644 --- a/packages/liquidity-sdk-viem/src/loader.ts +++ b/packages/liquidity-sdk-viem/src/loader.ts @@ -6,7 +6,7 @@ import { fetchVaultMarketConfig, } from "@morpho-org/blue-sdk-viem"; import type { PublicReallocation } from "@morpho-org/morpho-sdk"; -import { VaultV1ReallocationData } from "@morpho-org/morpho-sdk/entities"; +import { ReallocationData } from "@morpho-org/morpho-sdk/entities"; import { entries, fromEntries, isDefined } from "@morpho-org/morpho-ts"; import DataLoader from "dataloader"; import type { Chain, Client, Transport } from "viem"; @@ -42,8 +42,8 @@ export class LiquidityLoader { protected readonly dataLoader: DataLoader< MarketId, { - startState: VaultV1ReallocationData; - endState: VaultV1ReallocationData; + startState: ReallocationData; + endState: ReallocationData; withdrawals: readonly PublicReallocation[]; targetBorrowUtilization: bigint; } @@ -157,7 +157,7 @@ export class LiquidityLoader { ), ]); - const startState = new VaultV1ReallocationData({ + const startState = new ReallocationData({ chainId, markets: fromEntries(markets.map((market) => [market.id, market])), vaults: fromEntries(vaults.map((vault) => [vault.address, vault])), @@ -222,7 +222,7 @@ export class LiquidityLoader { * @param marketId - Target market id to plan withdrawals for. * @returns The start state, simulated end state, computed withdrawals, and target borrow utilization. * - * @remarks The returned `endState` is produced by `VaultV1ReallocationData.getMarketPublicReallocations` + * @remarks The returned `endState` is produced by `ReallocationData.getMarketPublicReallocations` * from onchain inputs fetched at one block, with reallocation headroom evaluated one hour after * that block timestamp. * @@ -244,7 +244,7 @@ export class LiquidityLoader { * const { withdrawals, endState } = await loader.fetch(marketId); * * // withdrawals: readonly PublicReallocation[] - * // endState: VaultV1ReallocationData + * // endState: ReallocationData * ``` */ public fetch(marketId: MarketId) { diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts deleted file mode 100644 index e09c82970..000000000 --- a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts +++ /dev/null @@ -1,515 +0,0 @@ -import { - AdaptiveCurveIrmLib, - getChainAddresses, - Market, - MarketParams, - MathLib, - VaultV2MorphoMarketV1AdapterV2, -} from "@morpho-org/blue-sdk"; -import { BLUE_API_BASE_URL } from "@morpho-org/morpho-ts"; -import { - vaultV2Abi, - vaultV2BluePublicAllocatorAbi, -} from "@morpho-org/morpho-ts/abis"; -import { createMockClient, mockRead } from "@morpho-org/test/mock"; -import nock from "nock"; -import { type Address, type Hex, toHex, zeroAddress, zeroHash } from "viem"; -import { mainnet } from "viem/chains"; -import { beforeEach, describe, expect, test } from "vitest"; -import { fetchRestVaultV2 } from "./api/rest.js"; -import { - InconsistentVaultV2LiquiditySnapshotError, - InvalidVaultV2LiquidityApiResponseError, - MissingVaultV2LiquidityApiDataError, - VaultV2LiquidityApiError, -} from "./errors.js"; -import { VaultV2LiquidityLoader } from "./vaultV2LiquidityLoader.js"; - -const BLOCK_NUMBER = 10n; -const BLOCK_TIMESTAMP = 1_700_000_000n; -const ORACLE_PRICE = 10n ** 36n; -const ALLOCATOR: Address = "0x0000000000000000000000000000000000000001"; -const VAULT: Address = "0x0000000000000000000000000000000000000002"; -const ADAPTER: Address = "0x0000000000000000000000000000000000000003"; -const ASSET: Address = "0x0000000000000000000000000000000000000004"; -const COLLATERAL: Address = "0x0000000000000000000000000000000000000006"; -const ORACLE: Address = "0x0000000000000000000000000000000000000007"; -const IRM = getChainAddresses(mainnet.id).adaptiveCurveIrm; - -const marketParams = new MarketParams({ - loanToken: ASSET, - collateralToken: COLLATERAL, - oracle: ORACLE, - irm: IRM, - lltv: 860_000_000_000_000_000n, -}); -const ids = [ - VaultV2MorphoMarketV1AdapterV2.adapterId(ADAPTER), - VaultV2MorphoMarketV1AdapterV2.collateralId(COLLATERAL), - VaultV2MorphoMarketV1AdapterV2.marketParamsId(ADAPTER, marketParams), -] as const; -const rpcBlock = () => ({ - baseFeePerGas: toHex(0n), - difficulty: toHex(0n), - extraData: "0x", - gasLimit: toHex(30_000_000n), - gasUsed: toHex(0n), - hash: zeroHash, - logsBloom: `0x${"00".repeat(256)}` as Hex, - miner: zeroAddress, - mixHash: zeroHash, - nonce: "0x0000000000000000", - number: toHex(BLOCK_NUMBER), - parentHash: zeroHash, - receiptsRoot: zeroHash, - sha3Uncles: zeroHash, - size: toHex(0n), - stateRoot: zeroHash, - timestamp: toHex(BLOCK_TIMESTAMP), - totalDifficulty: toHex(0n), - transactions: [], - transactionsRoot: zeroHash, - uncles: [], -}); - -const vaultConfigResponse = { - data: { - chain_id: mainnet.id, - address: VAULT, - last_indexed_block: BLOCK_NUMBER.toString(), - version: "2.0", - name: "Vault V2", - symbol: "v2", - asset: { address: ASSET, decimals: 18, name: "Asset", symbol: "AST" }, - decimals_offset: 0, - factory_address: zeroAddress, - creation_block_number: "1", - owner: zeroAddress, - curator: zeroAddress, - timelock_seconds: 0, - management_fee_wad: null, - management_fee_recipient: null, - performance_fee_wad: null, - performance_fee_recipient: null, - max_rate_per_second_wad: "0", - adapter_registry: zeroAddress, - liquidity_adapter: zeroAddress, - liquidity_data: "0x" as Hex, - gates: { - send_shares: null, - receive_shares: null, - send_assets: null, - receive_assets: null, - }, - }, -}; - -const defaultMarketState = { - lastAccrualTimestamp: BLOCK_TIMESTAMP, - totalSupplyAssets: 100n, - totalSupplyShares: 100_000_000n, - totalBorrowAssets: 95n, - totalBorrowShares: 95_000_000n, - fee: 0n, -}; - -const setupApi = ({ - vaultStatus = 200, - includePenalty = true, - marketStateBlock = BLOCK_NUMBER, - marketState = {}, - positionUser = ADAPTER, - positionSupplyShares = 0n, - rateAtTarget = 0n, -}: { - readonly vaultStatus?: number; - readonly includePenalty?: boolean; - readonly marketStateBlock?: bigint; - readonly marketState?: Partial; - readonly positionUser?: Address; - readonly positionSupplyShares?: bigint; - readonly rateAtTarget?: bigint; -} = {}) => { - const resolvedMarketState = { ...defaultMarketState, ...marketState }; - const rest = nock(BLUE_API_BASE_URL); - rest - .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}`) - .reply(vaultStatus, vaultConfigResponse); - rest.get(`/v1/vaults-v2/${mainnet.id}:${VAULT}/state`).reply(200, { - data: { - chain_id: mainnet.id, - address: VAULT, - last_indexed_block: BLOCK_NUMBER.toString(), - last_accrual_timestamp: Number(BLOCK_TIMESTAMP), - total_assets: "100", - total_supply: "100", - withdrawable_assets: "100", - allocated_assets: "0", - idle_assets: "100", - share_price_ray: "1000000000000000000000000000", - }, - }); - rest.get(`/v0/vaults-v2/${mainnet.id}:${VAULT}/allocations`).reply(200, { - data: { - chain_id: mainnet.id, - vault_address: VAULT, - last_indexed_block: BLOCK_NUMBER.toString(), - allocations: [ - { - adapter_address: ADAPTER, - adapter_kind: "morpho_market_v1_v2", - caps: [ - { - cap_id: ids[2], - cap_data: "0x", - allocated_assets: "0", - absolute_cap: "1000", - relative_cap_wad: MathLib.WAD.toString(), - cap_type: "market_v1", - market_id: marketParams.id, - }, - ], - }, - ], - unscoped_caps: [], - }, - }); - rest - .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}/withdrawal-options`) - .reply(200, { - data: { - chain_id: mainnet.id, - vault_address: VAULT, - liquidity_adapter_available_assets: "0", - idle_assets: "100", - adapter_penalties: includePenalty - ? [ - { - adapter_address: ADAPTER, - adapter_kind: "blue_market_adapter", - force_deallocatable_assets: "0", - penalty_rate_wad: "0", - }, - ] - : [], - }, - }); - rest.get(`/v0/blue/markets/${mainnet.id}:${marketParams.id}`).reply(200, { - data: { - chain_id: mainnet.id, - market_id: marketParams.id, - loan_token: ASSET, - collateral_token: COLLATERAL, - oracle_address: ORACLE, - irm_address: IRM, - lltv_wad: marketParams.lltv.toString(), - creation_block_number: "1", - }, - }); - rest - .get(`/v0/blue/markets/${mainnet.id}:${marketParams.id}/state`) - .reply(200, { - data: { - chain_id: mainnet.id, - market_id: marketParams.id, - last_indexed_block: marketStateBlock.toString(), - last_accrual_timestamp: Number( - resolvedMarketState.lastAccrualTimestamp, - ), - total_supply_assets: resolvedMarketState.totalSupplyAssets.toString(), - total_supply_shares: resolvedMarketState.totalSupplyShares.toString(), - total_borrow_assets: resolvedMarketState.totalBorrowAssets.toString(), - total_borrow_shares: resolvedMarketState.totalBorrowShares.toString(), - fee_wad: resolvedMarketState.fee.toString(), - }, - }); - - rest - .get( - `/v0/blue/markets/${mainnet.id}:${marketParams.id}/users/${ADAPTER}/position`, - ) - .reply(200, { - data: { - chain_id: mainnet.id, - market_id: marketParams.id, - user_address: positionUser, - last_indexed_block: BLOCK_NUMBER.toString(), - collateral_assets: "0", - supply_shares: positionSupplyShares.toString(), - borrow_shares: "0", - }, - }); - rest.get(`/v0/oracles/${mainnet.id}:${ORACLE}/state`).reply(200, { - data: { - chain_id: mainnet.id, - oracle_address: ORACLE, - last_indexed_block: BLOCK_NUMBER.toString(), - last_updated_at: BLOCK_TIMESTAMP.toString(), - price: ORACLE_PRICE.toString(), - }, - }); - rest - .get(`/consumer/chains/${mainnet.id}/markets/${marketParams.id}/irm`) - .reply(200, { - chainId: mainnet.id, - marketId: marketParams.id, - irmAddress: IRM, - targetUtilization: 0.9, - utilization: 0.95, - apyAtTarget: 0, - rateAtTarget: rateAtTarget.toString(), - borrowToTarget: 0, - }); - - return rest; -}; - -const setupClient = () => { - const handle = createMockClient(mainnet); - const defaultRequest = handle.request.getMockImplementation(); - handle.request.mockImplementation(async (call) => { - const { method } = call; - if (method === "eth_getBlockByNumber") return rpcBlock(); - return defaultRequest?.(call); - }); - mockRead(handle, { - address: ALLOCATOR, - abi: vaultV2BluePublicAllocatorAbi, - functionName: "vaultData", - result: [true, 12n], - }); - mockRead(handle, { - address: ALLOCATOR, - abi: vaultV2BluePublicAllocatorAbi, - functionName: "absoluteCap", - result: 1_000n, - }); - mockRead(handle, { - address: ALLOCATOR, - abi: vaultV2BluePublicAllocatorAbi, - functionName: "canPullFromMarket", - result: false, - }); - mockRead(handle, { - address: ALLOCATOR, - abi: vaultV2BluePublicAllocatorAbi, - functionName: "isActiveAdapter", - result: true, - }); - mockRead(handle, { - address: VAULT, - abi: vaultV2Abi, - functionName: "absoluteCap", - result: 1_000n, - }); - mockRead(handle, { - address: VAULT, - abi: vaultV2Abi, - functionName: "relativeCap", - result: MathLib.WAD, - }); - mockRead(handle, { - address: VAULT, - abi: vaultV2Abi, - functionName: "allocation", - result: 0n, - }); - return handle; -}; - -describe.sequential("VaultV2LiquidityLoader", () => { - beforeEach(() => { - nock.cleanAll(); - }); - - test("default: hydrates Vault V2 data from REST", async () => { - const api = setupApi(); - const { client } = setupClient(); - const loader = new VaultV2LiquidityLoader(client, { - allocator: ALLOCATOR, - vaults: [VAULT], - deployless: false, - }); - - const result = await loader.fetch(marketParams.id); - - expect(result.reallocations).toStrictEqual([ - { - allocator: ALLOCATOR, - type: "bluePublicAllocator", - vault: VAULT, - from: { type: "idle" }, - to: { adapter: ADAPTER }, - assets: 100n, - penalty: 12n, - }, - ]); - expect(result.endState.getMarket(marketParams.id).totalSupplyAssets).toBe( - 200n, - ); - expect(result.startState.getMarket(marketParams.id)).toMatchObject({ - price: ORACLE_PRICE, - rateAtTarget: 0n, - }); - expect( - result.startState.getAdapter(VAULT, ADAPTER).supplyShares[ - marketParams.id - ], - ).toBe(0n); - expect( - result.startState.getVault(VAULT).forceDeallocatePenalties[ADAPTER], - ).toBe(0n); - expect(result.targetBorrowUtilization).toBe(900_000_000_000_000_000n); - api.done(); - }); - - test("behavior: accrues REST-projected market totals only after the indexed block", async () => { - const storedTimestamp = BLOCK_TIMESTAMP - 3_600n; - const positionSupplyShares = 500_000_000_000_000_000_000_000_000n; - // Mirrors the raw market tuple and stored IRM value returned by pinned RPC. - const rawRpcMarket = new Market({ - params: marketParams, - totalSupplyAssets: 1_000_000_000_000_000_000_000n, - totalSupplyShares: 1_000_000_000_000_000_000_000_000_000n, - totalBorrowAssets: 950_000_000_000_000_000_000n, - totalBorrowShares: 950_000_000_000_000_000_000_000_000n, - lastUpdate: storedTimestamp, - fee: 0n, - price: ORACLE_PRICE, - rateAtTarget: AdaptiveCurveIrmLib.INITIAL_RATE_AT_TARGET, - }); - const indexedMarket = rawRpcMarket.accrueInterest(BLOCK_TIMESTAMP); - expect(indexedMarket.totalBorrowAssets).toBeGreaterThan( - rawRpcMarket.totalBorrowAssets, - ); - - const api = setupApi({ - marketState: { - lastAccrualTimestamp: storedTimestamp, - totalSupplyAssets: indexedMarket.totalSupplyAssets, - totalSupplyShares: indexedMarket.totalSupplyShares, - totalBorrowAssets: indexedMarket.totalBorrowAssets, - totalBorrowShares: indexedMarket.totalBorrowShares, - fee: indexedMarket.fee, - }, - positionSupplyShares, - rateAtTarget: indexedMarket.rateAtTarget, - }); - const { client } = setupClient(); - const loader = new VaultV2LiquidityLoader(client, { - allocator: ALLOCATOR, - vaults: [VAULT], - deployless: false, - }); - - const result = await loader.fetch(marketParams.id); - const executionTimestamp = BLOCK_TIMESTAMP + 3_600n; - const expectedMarket = indexedMarket.accrueInterest(executionTimestamp); - const hydratedMarket = result.startState.getMarket(marketParams.id); - - expect(hydratedMarket.lastUpdate).toBe(BLOCK_TIMESTAMP); - expect(hydratedMarket.accrueInterest(executionTimestamp)).toStrictEqual( - expectedMarket, - ); - expect( - result.startState - .getAdapter(VAULT, ADAPTER) - .realAssets(executionTimestamp), - ).toBe(expectedMarket.toSupplyAssets(positionSupplyShares)); - api.done(); - }); - - test("behavior: filters vaults above the maximum penalty", async () => { - const api = setupApi(); - const { client } = setupClient(); - const loader = new VaultV2LiquidityLoader(client, { - allocator: ALLOCATOR, - vaults: [VAULT], - maxPenalty: 11n, - deployless: false, - }); - - await expect(loader.fetch(marketParams.id)).resolves.toMatchObject({ - reallocations: [], - }); - api.done(); - }); - - test("error: VaultV2LiquidityApiError", async () => { - setupApi({ vaultStatus: 503 }); - const { client } = setupClient(); - const loader = new VaultV2LiquidityLoader(client, { - allocator: ALLOCATOR, - vaults: [VAULT], - }); - - await expect(loader.fetch(marketParams.id)).rejects.toBeInstanceOf( - VaultV2LiquidityApiError, - ); - }); - - test("error: VaultV2LiquidityApiError wraps network failures", async () => { - const api = nock(BLUE_API_BASE_URL) - .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}`) - .replyWithError("network unavailable"); - - await expect(fetchRestVaultV2(mainnet.id, VAULT)).rejects.toMatchObject({ - name: "VaultV2LiquidityApiError", - status: undefined, - cause: expect.anything(), - }); - api.done(); - }); - - test("error: MissingVaultV2LiquidityApiDataError", async () => { - setupApi({ includePenalty: false }); - const { client } = setupClient(); - const loader = new VaultV2LiquidityLoader(client, { - allocator: ALLOCATOR, - vaults: [VAULT], - }); - - await expect(loader.fetch(marketParams.id)).rejects.toBeInstanceOf( - MissingVaultV2LiquidityApiDataError, - ); - }); - - test("error: InvalidVaultV2LiquidityApiResponseError", async () => { - const api = nock(BLUE_API_BASE_URL) - .get(`/v0/vaults-v2/${mainnet.id}:${VAULT}`) - .reply(200, { data: { chain_id: mainnet.id, address: VAULT } }); - - await expect(fetchRestVaultV2(mainnet.id, VAULT)).rejects.toBeInstanceOf( - InvalidVaultV2LiquidityApiResponseError, - ); - api.done(); - }); - - test("error: InconsistentVaultV2LiquiditySnapshotError", async () => { - setupApi({ marketStateBlock: BLOCK_NUMBER + 1n }); - const { client } = setupClient(); - const loader = new VaultV2LiquidityLoader(client, { - allocator: ALLOCATOR, - vaults: [VAULT], - }); - - await expect(loader.fetch(marketParams.id)).rejects.toBeInstanceOf( - InconsistentVaultV2LiquiditySnapshotError, - ); - }); - - test("error: mismatched adapter-market position is rejected", async () => { - setupApi({ - positionUser: "0x0000000000000000000000000000000000000005", - }); - const { client } = setupClient(); - const loader = new VaultV2LiquidityLoader(client, { - allocator: ALLOCATOR, - vaults: [VAULT], - }); - - await expect(loader.fetch(marketParams.id)).rejects.toBeInstanceOf( - InvalidVaultV2LiquidityApiResponseError, - ); - }); -}); diff --git a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts deleted file mode 100644 index aa4867143..000000000 --- a/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts +++ /dev/null @@ -1,566 +0,0 @@ -import { - AccrualVaultV2, - AccrualVaultV2MorphoMarketV1AdapterV2, - getChainAddresses, - Market, - type MarketId, - MarketParams, -} from "@morpho-org/blue-sdk"; -import { - fetchAccrualVaultV2, - fetchVaultV2PublicAllocatorData, -} from "@morpho-org/blue-sdk-viem"; -import { - DEFAULT_SUPPLY_TARGET_UTILIZATION, - type VaultV2BlueReallocation, -} from "@morpho-org/morpho-sdk"; -import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; -import { fromEntries } from "@morpho-org/morpho-ts"; -import DataLoader from "dataloader"; -import { - type Address, - type Chain, - type Client, - isAddressEqual, - type Transport, - zeroAddress, -} from "viem"; -import { getBlock } from "viem/actions"; -import { - fetchRestMarket, - fetchRestMarketIrm, - fetchRestMarketPosition, - fetchRestMarketState, - fetchRestOracleState, - fetchRestVaultV2, - fetchRestVaultV2Allocations, - fetchRestVaultV2State, - fetchRestVaultV2WithdrawalOptions, -} from "./api/rest.js"; -import { - InconsistentVaultV2LiquiditySnapshotError, - MissingVaultV2LiquidityApiDataError, -} from "./errors.js"; - -const REALLOCATION_SIMULATION_DELAY = 3_600n; - -const assertSnapshotBlock = ({ - expectedBlock, - indexedBlock, - resource, -}: { - readonly expectedBlock: bigint; - readonly indexedBlock: string; - readonly resource: string; -}) => { - const actualBlock = BigInt(indexedBlock); - if (actualBlock !== expectedBlock) { - throw new InconsistentVaultV2LiquiditySnapshotError({ - resource, - expectedBlock, - actualBlock, - }); - } -}; - -/** Represents the configuration for fetching and simulating Vault V2 shared liquidity. */ -export interface VaultV2LiquidityParameters { - /** Explicit BluePublicAllocator contract used for every generated reallocation. */ - readonly allocator: Address; - - /** Vault V2 addresses whose reallocatable liquidity should be considered. */ - readonly vaults: readonly Address[]; - - /** Maximum WAD-scaled vault-asset penalty accepted per BluePublicAllocator call. */ - readonly maxPenalty?: bigint; - - /** Deployless read mode forwarded to allocator reads and RPC fallbacks. Defaults to `true` with direct-read fallback. */ - readonly deployless?: boolean | "force"; -} - -/** Represents a Vault V2 shared-liquidity plan built from the latest indexed API state. */ -export interface VaultV2LiquidityResult { - /** Vault V2 state before applying the computed reallocations. */ - readonly startState: VaultV2ReallocationData; - - /** Vault V2 state after applying the computed reallocations. */ - readonly endState: VaultV2ReallocationData; - - /** Flat action-ready BluePublicAllocator calls, in execution order. */ - readonly reallocations: readonly VaultV2BlueReallocation[]; - - /** Fixed target utilization used by the Vault V2 planner, scaled by WAD. */ - readonly targetBorrowUtilization: bigint; -} - -/** - * Represents a loader that fetches and simulates Vault V2 BluePublicAllocator shared liquidity. - * - * This class is independent from the Vault V1 `LiquidityLoader`. It consumes - * explicit allocator and vault addresses, loads Vault V2 and market state from - * the Morpho REST API, and reads BluePublicAllocator-only configuration through - * the viem client. - * - * @example - * ```ts - * import type { MarketId } from "@morpho-org/blue-sdk"; - * import { - * type VaultV2LiquidityResult, - * VaultV2LiquidityLoader, - * } from "@morpho-org/liquidity-sdk-viem"; - * import { type Address, createPublicClient, http } from "viem"; - * import { mainnet } from "viem/chains"; - * - * export async function loadVaultV2Liquidity( - * allocator: Address, - * vault: Address, - * marketId: MarketId, - * ): Promise { - * const client = createPublicClient({ chain: mainnet, transport: http() }); - * const loader = new VaultV2LiquidityLoader(client, { - * allocator, - * vaults: [vault], - * maxPenalty: 1_000_000_000_000_000n, - * }); - * return loader.fetch(marketId); - * } - * ``` - */ -export class VaultV2LiquidityLoader { - protected readonly dataLoader: DataLoader; - - /** - * Creates a Vault V2 shared-liquidity loader. - * - * @param client - Viem client used for the current block and BluePublicAllocator-only state. - * @param parameters - Explicit allocator, participating vaults, and optional fetch/planning limits. - */ - public constructor( - public readonly client: Client, - public readonly parameters: VaultV2LiquidityParameters, - ) { - this.dataLoader = new DataLoader( - async (marketIds) => { - const { client: loaderClient, parameters: loaderParameters } = this; - const chainId = loaderClient.chain.id; - const restVaults = await Promise.all( - loaderParameters.vaults.map(async (vault) => { - const [config, state, allocations] = await Promise.all([ - fetchRestVaultV2(chainId, vault), - fetchRestVaultV2State(chainId, vault), - fetchRestVaultV2Allocations(chainId, vault), - ]); - return { config, state, allocations }; - }), - ); - const prefetchedMarketState = - restVaults.length === 0 - ? await fetchRestMarketState(chainId, marketIds[0]!) - : undefined; - const indexedBlockNumber = BigInt( - restVaults[0]?.config.last_indexed_block ?? - prefetchedMarketState!.last_indexed_block, - ); - for (const { config, state, allocations } of restVaults) { - assertSnapshotBlock({ - expectedBlock: indexedBlockNumber, - indexedBlock: config.last_indexed_block, - resource: `vault ${config.address} config`, - }); - assertSnapshotBlock({ - expectedBlock: indexedBlockNumber, - indexedBlock: state.last_indexed_block, - resource: `vault ${config.address} state`, - }); - assertSnapshotBlock({ - expectedBlock: indexedBlockNumber, - indexedBlock: allocations.last_indexed_block, - resource: `vault ${config.address} allocations`, - }); - } - const block = await getBlock(loaderClient, { - blockNumber: indexedBlockNumber, - }); - const fetchParameters = { - blockNumber: indexedBlockNumber, - deployless: loaderParameters.deployless, - } as const; - - const restHydratedVaults = restVaults.filter( - ({ config, allocations }) => - allocations.allocations.every( - ({ adapter_kind }) => adapter_kind === "morpho_market_v1_v2", - ) && - !( - config.gates.receive_shares != null && - (BigInt(config.management_fee_wad ?? 0) > 0n || - BigInt(config.performance_fee_wad ?? 0) > 0n) - ), - ); - const rpcHydratedVaults = restVaults.filter( - (vault) => !restHydratedVaults.includes(vault), - ); - - const rpcVaults = await Promise.all( - rpcHydratedVaults.map(({ config }) => - fetchAccrualVaultV2(config.address, loaderClient, { - ...fetchParameters, - chainId, - }), - ), - ); - - const restMarketIds = new Set(marketIds); - for (const { allocations } of restHydratedVaults) { - for (const adapter of allocations.allocations) { - for (const cap of adapter.caps) { - if (cap.market_id != null) restMarketIds.add(cap.market_id); - } - } - } - - const adapterMarketPairs = new Map< - string, - { readonly adapterAddress: Address; readonly marketId: MarketId } - >(); - for (const { allocations } of restHydratedVaults) { - for (const allocation of allocations.allocations) { - for (const { market_id } of allocation.caps) { - if (market_id == null) continue; - adapterMarketPairs.set( - `${allocation.adapter_address.toLowerCase()}:${market_id.toLowerCase()}`, - { - adapterAddress: allocation.adapter_address, - marketId: market_id, - }, - ); - } - } - } - const allRestMarketIds = [...restMarketIds]; - const { adaptiveCurveIrm } = getChainAddresses(chainId); - - const [restMarkets, withdrawalOptions, marketPositions] = - await Promise.all([ - Promise.all( - allRestMarketIds.map(async (marketId) => { - const config = await fetchRestMarket(chainId, marketId); - const [state, oracleState, marketIrm] = await Promise.all([ - prefetchedMarketState != null && - marketId.toLowerCase() === marketIds[0]!.toLowerCase() - ? prefetchedMarketState - : fetchRestMarketState(chainId, marketId), - isAddressEqual(config.oracle_address, zeroAddress) - ? undefined - : fetchRestOracleState(chainId, config.oracle_address), - isAddressEqual(config.irm_address, adaptiveCurveIrm) - ? fetchRestMarketIrm(chainId, marketId) - : undefined, - ]); - if ( - isAddressEqual(config.irm_address, adaptiveCurveIrm) && - marketIrm?.rateAtTarget == null - ) - throw new MissingVaultV2LiquidityApiDataError( - `market ${config.market_id} rateAtTarget`, - ); - - return { - config, - state, - oracleState, - price: - oracleState?.price == null - ? undefined - : BigInt(oracleState.price), - rateAtTarget: - marketIrm?.rateAtTarget == null - ? undefined - : BigInt(marketIrm.rateAtTarget), - }; - }), - ), - Promise.all( - restHydratedVaults.map(async ({ config }) => ({ - vaultAddress: config.address, - data: await fetchRestVaultV2WithdrawalOptions( - chainId, - config.address, - ), - })), - ), - Promise.all( - [...adapterMarketPairs.values()].map( - ({ adapterAddress, marketId }) => - fetchRestMarketPosition({ - chainId, - marketId, - user: adapterAddress, - }), - ), - ), - ]); - - for (const { config, state, oracleState } of restMarkets) { - assertSnapshotBlock({ - expectedBlock: indexedBlockNumber, - indexedBlock: state.last_indexed_block, - resource: `market ${config.market_id} state`, - }); - if (oracleState != null) { - assertSnapshotBlock({ - expectedBlock: indexedBlockNumber, - indexedBlock: oracleState.last_indexed_block, - resource: `oracle ${oracleState.oracle_address} state`, - }); - } - } - for (const position of marketPositions) { - assertSnapshotBlock({ - expectedBlock: indexedBlockNumber, - indexedBlock: position.last_indexed_block, - resource: `market ${position.market_id} position ${position.user_address}`, - }); - } - - const forceDeallocatePenalties = new Map( - withdrawalOptions.flatMap(({ vaultAddress, data }) => - data.adapter_penalties.map( - ({ adapter_address, penalty_rate_wad }) => - [ - `${vaultAddress.toLowerCase()}:${adapter_address.toLowerCase()}`, - BigInt(penalty_rate_wad), - ] as const, - ), - ), - ); - const positionSupplyShares = new Map( - marketPositions.map( - ({ user_address, market_id, supply_shares }) => - [ - `${user_address.toLowerCase()}:${market_id.toLowerCase()}`, - BigInt(supply_shares), - ] as const, - ), - ); - - const markets = restMarkets.map( - ({ config, state, price, rateAtTarget }) => - new Market({ - params: new MarketParams({ - loanToken: config.loan_token, - collateralToken: config.collateral_token, - oracle: config.oracle_address, - irm: config.irm_address, - lltv: BigInt(config.lltv_wad), - }), - totalSupplyAssets: BigInt(state.total_supply_assets), - totalSupplyShares: BigInt(state.total_supply_shares), - totalBorrowAssets: BigInt(state.total_borrow_assets), - totalBorrowShares: BigInt(state.total_borrow_shares), - // REST totals and IRM state are projected to the indexed block; - // last_accrual_timestamp remains the older onchain storage value. - lastUpdate: block.timestamp, - fee: BigInt(state.fee_wad), - price, - rateAtTarget, - }), - ); - const marketById = new Map( - markets.map((market) => [market.id.toLowerCase(), market] as const), - ); - - const apiVaults = restHydratedVaults.map( - ({ config, state, allocations }) => { - const adapters = allocations.allocations.map((allocation) => { - const adapterMarkets = allocation.caps - .map(({ market_id }) => market_id) - .filter((marketId): marketId is MarketId => marketId != null) - .map((marketId) => { - const market = marketById.get(marketId.toLowerCase()); - if (market == null) - throw new MissingVaultV2LiquidityApiDataError( - `market ${marketId}`, - ); - return market; - }); - const penalty = forceDeallocatePenalties.get( - `${config.address.toLowerCase()}:${allocation.adapter_address.toLowerCase()}`, - ); - if (penalty == null) - throw new MissingVaultV2LiquidityApiDataError( - `vault ${config.address} adapter ${allocation.adapter_address} forceDeallocatePenalty`, - ); - - return { - adapter: new AccrualVaultV2MorphoMarketV1AdapterV2( - { - address: allocation.adapter_address, - parentVault: config.address, - skimRecipient: zeroAddress, - marketIds: adapterMarkets.map(({ id }) => id), - adaptiveCurveIrm, - supplyShares: fromEntries( - adapterMarkets.map((market) => { - const supplyShares = positionSupplyShares.get( - `${allocation.adapter_address.toLowerCase()}:${market.id.toLowerCase()}`, - ); - if (supplyShares == null) { - throw new MissingVaultV2LiquidityApiDataError( - `adapter ${allocation.adapter_address} market ${market.id} position`, - ); - } - return [market.id, supplyShares] as const; - }), - ), - }, - adapterMarkets, - ), - penalty, - }; - }); - const liquidityAdapter = adapters.find(({ adapter }) => - isAddressEqual(adapter.address, config.liquidity_adapter), - )?.adapter; - - return new AccrualVaultV2( - { - address: config.address, - name: config.name, - symbol: config.symbol, - decimals: config.asset.decimals + config.decimals_offset, - asset: config.asset.address, - _totalAssets: BigInt(state.total_assets), - totalSupply: BigInt(state.total_supply), - virtualShares: 10n ** BigInt(config.decimals_offset), - maxRate: BigInt(config.max_rate_per_second_wad), - lastUpdate: BigInt(state.last_accrual_timestamp), - liquidityAdapter: config.liquidity_adapter, - liquidityData: config.liquidity_data, - liquidityAllocations: undefined, - performanceFee: BigInt(config.performance_fee_wad ?? 0), - managementFee: BigInt(config.management_fee_wad ?? 0), - performanceFeeRecipient: - config.performance_fee_recipient ?? zeroAddress, - managementFeeRecipient: - config.management_fee_recipient ?? zeroAddress, - }, - liquidityAdapter, - adapters.map(({ adapter }) => adapter), - BigInt(state.idle_assets), - fromEntries( - adapters.map(({ adapter, penalty }) => [ - adapter.address, - penalty, - ]), - ), - ); - }, - ); - const vaults = [...apiVaults, ...rpcVaults]; - - const publicAllocatorData = await Promise.all( - vaults.map((vault) => - fetchVaultV2PublicAllocatorData( - loaderParameters.allocator, - vault, - loaderClient, - fetchParameters, - ), - ), - ); - const startState = new VaultV2ReallocationData({ - chainId, - allocator: loaderParameters.allocator, - markets: fromEntries( - markets.map((market) => [market.id, market] as const), - ), - vaults: fromEntries( - vaults.map((vault) => [vault.address, vault] as const), - ), - allocations: fromEntries( - publicAllocatorData.map( - ({ publicAllocatorConfig, allocations }) => [ - publicAllocatorConfig.vault, - allocations, - ], - ), - ), - publicAllocatorConfigs: fromEntries( - publicAllocatorData.map(({ publicAllocatorConfig }) => [ - publicAllocatorConfig.vault, - publicAllocatorConfig, - ]), - ), - marketPublicAllocatorConfigs: fromEntries( - publicAllocatorData.map( - ({ publicAllocatorConfig, marketPublicAllocatorConfigs }) => [ - publicAllocatorConfig.vault, - marketPublicAllocatorConfigs, - ], - ), - ), - }); - - return marketIds.map((marketId) => { - const market = marketById.get(marketId.toLowerCase()); - if (market == null) - throw new MissingVaultV2LiquidityApiDataError( - `target market ${marketId}`, - ); - const { data: endState, reallocations } = - startState.computeVaultV2Reallocations(market.id, { - timestamp: block.timestamp + REALLOCATION_SIMULATION_DELAY, - reallocatableVaults: loaderParameters.vaults, - maxPenalty: loaderParameters.maxPenalty, - }); - - return { - startState, - endState, - reallocations, - targetBorrowUtilization: DEFAULT_SUPPLY_TARGET_UTILIZATION, - }; - }); - }, - { cache: false }, - ); - } - - /** - * Fetches a Vault V2 shared-liquidity plan for a target Morpho Blue market. - * - * @param marketId - Target market id to plan reallocations for. - * @returns The start state, simulated end state, action-ready reallocations, and target utilization. - * @throws {VaultV2LiquidityApiError} when a REST API request fails. - * @throws {InvalidVaultV2LiquidityApiResponseError} when a successful REST response is malformed. - * @throws {MissingVaultV2LiquidityApiDataError} when indexed REST data is incomplete. - * @throws {InconsistentVaultV2LiquiditySnapshotError} when REST resources report different indexed blocks. - * @throws {viem.BaseError} when a BluePublicAllocator read or RPC compatibility fallback fails. - * @example - * ```ts - * import type { MarketId } from "@morpho-org/blue-sdk"; - * import { VaultV2LiquidityLoader } from "@morpho-org/liquidity-sdk-viem"; - * import { type Address, createPublicClient, http } from "viem"; - * import { mainnet } from "viem/chains"; - * - * async function fetchPlan( - * allocator: Address, - * vault: Address, - * marketId: MarketId, - * ) { - * const client = createPublicClient({ chain: mainnet, transport: http() }); - * const loader = new VaultV2LiquidityLoader(client, { - * allocator, - * vaults: [vault], - * }); - * const result = await loader.fetch(marketId); - * // result satisfies VaultV2LiquidityResult - * return result; - * } - * ``` - */ - public fetch(marketId: MarketId) { - return this.dataLoader.load(marketId); - } -} From bcbe0fcce51498d909e20d3da95317475dc54a34 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Mon, 17 Aug 2026 14:12:58 +0200 Subject: [PATCH 17/41] refactor: normalize Vault V2 active adapters --- .changeset/brave-vaults-reallocate.md | 2 +- ...lt-v2-public-allocator-shared-liquidity.md | 18 ++- packages/blue-sdk-viem/AGENTS.md | 2 +- .../GetVaultV2PublicAllocatorConfig.sol | 12 +- ...2PublicAllocatorConfig.integration.test.ts | 4 +- .../VaultV2PublicAllocatorConfig.test.ts | 25 ++- .../vault-v2/VaultV2PublicAllocatorConfig.ts | 142 ++++++++++-------- .../GetVaultV2PublicAllocatorConfig.ts | 17 ++- packages/blue-sdk/AGENTS.md | 2 +- .../vault/v2/VaultV2PublicAllocatorConfig.ts | 4 +- .../vaultV2Reallocations.integration.test.ts | 1 + .../entities/vaultV2ReallocationData.test.ts | 32 +++- .../src/entities/vaultV2ReallocationData.ts | 23 ++- 13 files changed, 192 insertions(+), 92 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 808e99ace..763c59c7c 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -6,7 +6,7 @@ "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. +Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, normalize active adapters as a vault-keyed address set, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index b150e5658..856cae2cb 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -195,6 +195,9 @@ export interface InputVaultV2ReallocationData { readonly publicAllocatorConfigs?: Readonly< Record >; + readonly activeAdapters?: Readonly< + Record | undefined> + >; readonly marketPublicAllocatorConfigs?: Readonly< Record< Address, @@ -205,9 +208,9 @@ export interface InputVaultV2ReallocationData { ``` The readonly config projections are self-identifying. Vault-wide state carries -`allocator`, `vault`, `canPullFromIdle`, and `penalty`. Pair state also carries -`adapter`, `marketParamsId`, `absoluteCap`, `canPullFromMarket`, and -`isActiveAdapter`. +`allocator`, `vault`, `canPullFromIdle`, and `penalty`. Adapter activation is +normalized as a vault-keyed set of adapter addresses. Market state carries +`adapter`, `marketParamsId`, `absoluteCap`, and `canPullFromMarket`. ## Fetching @@ -220,10 +223,11 @@ The readonly config projections are self-identifying. Vault-wide state carries - `fetchVaultV2PublicAllocatorData(allocator, hydratedVault, client, parameters?)`. -The batched fetcher derives every supported adapter/market request and every -unique allocation ID from the hydrated `AccrualVaultV2`. It defaults to one -deployless read and falls back to equivalent direct reads unless deployless -mode is forced. No chain-address lookup occurs. +The batched fetcher derives every supported adapter, adapter/market request, +and unique allocation ID from the hydrated `AccrualVaultV2`. It returns active +adapters as a `Set
`, defaults to one deployless read, and falls back to +equivalent direct reads unless deployless mode is forced. No chain-address +lookup occurs. Only `AccrualVaultV2MorphoMarketV1AdapterV2` adapters participate. Other adapter classes are ignored even if an allocator reports them as active. diff --git a/packages/blue-sdk-viem/AGENTS.md b/packages/blue-sdk-viem/AGENTS.md index b37a87a46..80bcea623 100644 --- a/packages/blue-sdk-viem/AGENTS.md +++ b/packages/blue-sdk-viem/AGENTS.md @@ -12,7 +12,7 @@ - Normalize unsafe user addresses with `safeGetAddress`, not lowercasing alone. - Typed-data helpers return `TypedDataDefinition`, e.g. `getPermitTypedData(...)`. - Re-export ABI literals from `@morpho-org/morpho-ts` when they exist there; keep local ABI declarations only for Blue-specific viem surfaces absent from `morpho-ts`. -- Vault V2 BluePublicAllocator fetchers always accept the allocator address explicitly; there is no chain-address registry entry. The hydrated-vault batch fetch derives supported adapter/market/allocation ids, defaults to one deployless read, and falls back to direct reads. +- Vault V2 BluePublicAllocator fetchers always accept the allocator address explicitly; there is no chain-address registry entry. The hydrated-vault batch fetch derives supported adapter/market/allocation ids, returns active adapters as a set separate from market configs, defaults to one deployless read, and falls back to direct reads. ## Continuous Improvement diff --git a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol index 69346666d..95f171303 100644 --- a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol +++ b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol @@ -14,7 +14,6 @@ struct VaultV2MarketPublicAllocatorResponse { bytes32 marketParamsId; uint256 absoluteCap; bool canPullFromMarket; - bool isActiveAdapter; } struct VaultV2AllocationResponse { @@ -27,6 +26,7 @@ struct VaultV2AllocationResponse { struct VaultV2PublicAllocatorResponse { bool canPullFromIdle; uint64 penalty; + bool[] isActiveAdapters; VaultV2MarketPublicAllocatorResponse[] marketConfigs; VaultV2AllocationResponse[] allocations; } @@ -35,11 +35,18 @@ contract GetVaultV2PublicAllocatorConfig { function query( IBluePublicAllocator allocator, IVaultV2 vault, + address[] calldata adapters, VaultV2MarketPublicAllocatorRequest[] calldata marketRequests, bytes32[] calldata allocationIds ) external view returns (VaultV2PublicAllocatorResponse memory res) { (res.canPullFromIdle, res.penalty) = allocator.vaultData(address(vault)); + uint256 adaptersLength = adapters.length; + res.isActiveAdapters = new bool[](adaptersLength); + for (uint256 i; i < adaptersLength; ++i) { + res.isActiveAdapters[i] = allocator.isActiveAdapter(address(vault), adapters[i]); + } + uint256 marketRequestsLength = marketRequests.length; res.marketConfigs = new VaultV2MarketPublicAllocatorResponse[](marketRequestsLength); for (uint256 i; i < marketRequestsLength; ++i) { @@ -48,8 +55,7 @@ contract GetVaultV2PublicAllocatorConfig { adapter: request.adapter, marketParamsId: request.marketParamsId, absoluteCap: allocator.absoluteCap(address(vault), request.marketParamsId), - canPullFromMarket: allocator.canPullFromMarket(address(vault), request.marketParamsId), - isActiveAdapter: allocator.isActiveAdapter(address(vault), request.adapter) + canPullFromMarket: allocator.canPullFromMarket(address(vault), request.marketParamsId) }); } diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts index 01d12de20..b34ca2731 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts @@ -75,6 +75,9 @@ describe("Vault V2 public allocator fetchers on fork", () => { canPullFromIdle: true, penalty: 12n, }); + expect(deployless.activeAdapters).toStrictEqual( + new Set([forkAdapter.address]), + ); expect( deployless.marketPublicAllocatorConfigs[forkMarketParamsId], ).toStrictEqual({ @@ -84,7 +87,6 @@ describe("Vault V2 public allocator fetchers on fork", () => { marketParamsId: forkMarketParamsId, absoluteCap: 500n, canPullFromMarket: true, - isActiveAdapter: true, }); expect( Object.values(deployless.allocations).some( diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts index 1f1f89e92..d6bcd4063 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts @@ -87,6 +87,7 @@ const expected = { canPullFromIdle: true, penalty: 12n, }, + activeAdapters: new Set([ADAPTER]), marketPublicAllocatorConfigs: { [marketParamsId]: { allocator: ALLOCATOR, @@ -95,7 +96,6 @@ const expected = { marketParamsId, absoluteCap: 500n, canPullFromMarket: true, - isActiveAdapter: true, }, }, allocations: Object.fromEntries( @@ -111,7 +111,10 @@ const expected = { ), }; -const mockDirectReads = (handle: ReturnType) => { +const mockDirectReads = ( + handle: ReturnType, + isActiveAdapter = true, +) => { mockRead(handle, { address: ALLOCATOR, abi: vaultV2BluePublicAllocatorAbi, @@ -134,7 +137,7 @@ const mockDirectReads = (handle: ReturnType) => { address: ALLOCATOR, abi: vaultV2BluePublicAllocatorAbi, functionName: "isActiveAdapter", - result: true, + result: isActiveAdapter, }); mockRead(handle, { address: VAULT, @@ -182,13 +185,13 @@ describe("Vault V2 public allocator fetchers", () => { mockDeploylessRead(handle, queryAbi, "query", { canPullFromIdle: true, penalty: 12n, + isActiveAdapters: [true], marketConfigs: [ { adapter: ADAPTER, marketParamsId, absoluteCap: 500n, canPullFromMarket: true, - isActiveAdapter: true, }, ], allocations: ids.map((id) => ({ @@ -213,4 +216,18 @@ describe("Vault V2 public allocator fetchers", () => { fetchVaultV2PublicAllocatorData(ALLOCATOR, vault, handle.client), ).resolves.toStrictEqual(expected); }); + + test("behavior: omits inactive adapters from the registry", async () => { + const handle = createMockClient(mainnet); + mockDeploylessReads(handle, [new Error("deployless unavailable")]); + mockDirectReads(handle, false); + + const result = await fetchVaultV2PublicAllocatorData( + ALLOCATOR, + vault, + handle.client, + ); + + expect(result.activeAdapters).toStrictEqual(new Set()); + }); }); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts index b70af358a..387273f0b 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts @@ -69,7 +69,7 @@ export async function fetchVaultV2PublicAllocatorConfig( } /** - * Fetches BluePublicAllocator permissions and cap state for one Vault V2 adapter-market pair. + * Fetches BluePublicAllocator permission and cap state for one Vault V2 adapter-market pair. * * @param allocator - Explicit BluePublicAllocator contract address. * @param vault - Vault V2 address. @@ -80,7 +80,7 @@ export async function fetchVaultV2PublicAllocatorConfig( * @param parameters.blockNumber - Optional block number for historical reads. * @param parameters.blockTag - Optional block tag for historical reads. * @param parameters.stateOverride - Optional viem state override. - * @returns The allocator cap and permissions for the adapter-market pair. + * @returns The allocator cap and pull permission for the adapter-market pair. * @throws {viem.BaseError} when one of the contract reads fails. * @example * ```ts @@ -115,7 +115,7 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( client: Client, parameters: FetchParameters = {}, ): Promise { - const [absoluteCap, canPullFromMarket, isActiveAdapter] = await Promise.all([ + const [absoluteCap, canPullFromMarket] = await Promise.all([ readContract(client, { ...parameters, address: allocator, @@ -130,13 +130,6 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( functionName: "canPullFromMarket", args: [vault, marketParamsId], }), - readContract(client, { - ...parameters, - address: allocator, - abi: vaultV2BluePublicAllocatorAbi, - functionName: "isActiveAdapter", - args: [vault, adapter], - }), ]); return { @@ -146,7 +139,6 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( marketParamsId, absoluteCap, canPullFromMarket, - isActiveAdapter, }; } @@ -167,7 +159,7 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( * @param parameters.blockTag - Optional block tag for historical reads. * @param parameters.stateOverride - Optional viem state override. * @param parameters.deployless - Deployless mode; defaults to `true`, with direct-read fallback. - * @returns Vault-wide config, adapter-market configs keyed by `marketParamsId`, and allocations keyed by derived id. + * @returns Vault-wide config, active-adapter set, adapter-market configs keyed by `marketParamsId`, and allocations keyed by derived id. * @throws {viem.BaseError} when deployless mode is forced and fails, or when a direct contract read fails. * @example * ```ts @@ -186,7 +178,7 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( * vault, * client, * ); - * // data contains publicAllocatorConfig, marketPublicAllocatorConfigs, and allocations. + * // data contains publicAllocatorConfig, activeAdapters, marketPublicAllocatorConfigs, and allocations. * return data; * } * ``` @@ -202,10 +194,12 @@ export async function fetchVaultV2PublicAllocatorData( readonly adapter: Address; readonly marketParamsId: Hash; }[] = []; + const adapters = new Set
(); const allocationIds = new Set(); for (const adapter of vault.accrualAdapters) { if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) continue; + adapters.add(adapter.address); for (const market of adapter.markets) { const ids = adapter.ids(market.params); @@ -217,6 +211,7 @@ export async function fetchVaultV2PublicAllocatorData( } } + const adapterList = [...adapters]; const allocationIdList = [...allocationIds]; if (deployless) { @@ -226,7 +221,13 @@ export async function fetchVaultV2PublicAllocatorData( abi, code, functionName: "query", - args: [allocator, vault.address, marketRequests, allocationIdList], + args: [ + allocator, + vault.address, + adapterList, + marketRequests, + allocationIdList, + ], }); const marketPublicAllocatorConfigs: Record< @@ -253,6 +254,9 @@ export async function fetchVaultV2PublicAllocatorData( canPullFromIdle: result.canPullFromIdle, penalty: result.penalty, } satisfies VaultV2PublicAllocatorConfig, + activeAdapters: new Set( + adapterList.filter((_, index) => result.isActiveAdapters[index]), + ), marketPublicAllocatorConfigs, allocations, }; @@ -262,56 +266,71 @@ export async function fetchVaultV2PublicAllocatorData( } } - const [publicAllocatorConfig, marketConfigs, allocationValues] = - await Promise.all([ - fetchVaultV2PublicAllocatorConfig( - allocator, - vault.address, - client, - parameters, + const [ + publicAllocatorConfig, + isActiveAdapters, + marketConfigs, + allocationValues, + ] = await Promise.all([ + fetchVaultV2PublicAllocatorConfig( + allocator, + vault.address, + client, + parameters, + ), + Promise.all( + adapterList.map((adapter) => + readContract(client, { + ...parameters, + address: allocator, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "isActiveAdapter", + args: [vault.address, adapter], + }), ), - Promise.all( - marketRequests.map(({ adapter, marketParamsId }) => - fetchVaultV2MarketPublicAllocatorConfig( - allocator, - vault.address, - adapter, - marketParamsId, - client, - parameters, - ), + ), + Promise.all( + marketRequests.map(({ adapter, marketParamsId }) => + fetchVaultV2MarketPublicAllocatorConfig( + allocator, + vault.address, + adapter, + marketParamsId, + client, + parameters, ), ), - Promise.all( - allocationIdList.map(async (id) => { - const [absoluteCap, relativeCap, allocation] = await Promise.all([ - readContract(client, { - ...parameters, - address: vault.address, - abi: vaultV2Abi, - functionName: "absoluteCap", - args: [id], - }), - readContract(client, { - ...parameters, - address: vault.address, - abi: vaultV2Abi, - functionName: "relativeCap", - args: [id], - }), - readContract(client, { - ...parameters, - address: vault.address, - abi: vaultV2Abi, - functionName: "allocation", - args: [id], - }), - ]); + ), + Promise.all( + allocationIdList.map(async (id) => { + const [absoluteCap, relativeCap, allocation] = await Promise.all([ + readContract(client, { + ...parameters, + address: vault.address, + abi: vaultV2Abi, + functionName: "absoluteCap", + args: [id], + }), + readContract(client, { + ...parameters, + address: vault.address, + abi: vaultV2Abi, + functionName: "relativeCap", + args: [id], + }), + readContract(client, { + ...parameters, + address: vault.address, + abi: vaultV2Abi, + functionName: "allocation", + args: [id], + }), + ]); - return { id, absoluteCap, relativeCap, allocation }; - }), - ), - ]); + return { id, absoluteCap, relativeCap, allocation }; + }), + ), + ]); const marketPublicAllocatorConfigs: Record< Hash, @@ -328,6 +347,9 @@ export async function fetchVaultV2PublicAllocatorData( return { publicAllocatorConfig, + activeAdapters: new Set( + adapterList.filter((_, index) => isActiveAdapters[index]), + ), marketPublicAllocatorConfigs, allocations, }; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts index b9ed65011..7df732017 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts @@ -12,6 +12,11 @@ export const abi = [ name: "vault", type: "address", }, + { + internalType: "address[]", + name: "adapters", + type: "address[]", + }, { components: [ { @@ -49,6 +54,11 @@ export const abi = [ name: "penalty", type: "uint64", }, + { + internalType: "bool[]", + name: "isActiveAdapters", + type: "bool[]", + }, { components: [ { @@ -71,11 +81,6 @@ export const abi = [ name: "canPullFromMarket", type: "bool", }, - { - internalType: "bool", - name: "isActiveAdapter", - type: "bool", - }, ], internalType: "struct VaultV2MarketPublicAllocatorResponse[]", name: "marketConfigs", @@ -121,4 +126,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2PublicAllocatorConfig` query bytecode. */ export const code = - "0x60808060405234601557610808908161001a8239f35b5f80fdfe60a0806040526004361015610012575f80fd5b5f3560e01c635938912f14610025575f80fd5b3461030a57608036600319011261030a576004356001600160a01b03811660808190520361030a576024356001600160a01b0381169081900361030a576044359067ffffffffffffffff821161030a573660238301121561030a5781600401359167ffffffffffffffff831161030a573660248460061b8301011161030a576064359367ffffffffffffffff851161030a573660238601121561030a5784600401359067ffffffffffffffff821161030a573660248360051b8801011161030a576100ef81610703565b5f815260208101955f8752604082019660608852606083019260608452604051636b97fbcd60e11b81528760048201526040816024816080515afa8015610316575f915f916106a8575b5067ffffffffffffffff168352151581526101538861077e565b610160604051918261074f565b888152601f1961016f8a61077e565b015f5b81811061067357505089525f5b88811015610399578060061b87019061019a60248301610796565b60405163011f009b60e31b81526001600160a01b038b166004820152604480850135602483018190529294919060209082908190810103816080515afa908115610316578c84915f93610362575b506040516369f1e26b60e01b81526001600160a01b03909116600482015260248101919091529160208380604481015b03816080515afa928315610316575f93610321575b50602461023a9101610796565b92604051936366faa83960e01b85528d600486015260018060a01b031660248501526020846044816080515afa938415610316575f946102c9575b50928492600196926102c2956040519461028e86610733565b898060a01b031685526020850152604084015215156060830152151560808201528d51906102bc83836107aa565b526107aa565b500161017f565b92959193506020833d821161030e575b816102e66020938361074f565b8101031261030a576001956102c2946102ff8795610771565b955092965092610275565b5f80fd5b3d91506102d9565b6040513d5f823e3d90fd5b9092506020813d821161035a575b8161033c6020938361074f565b8101031261030a57602461035261023a92610771565b93915061022d565b3d915061032f565b925050506020813d8211610391575b8161037e6020938361074f565b8101031261030a5751828c6102186101e8565b3d9150610371565b50869550886103a78661077e565b6103b4604051918261074f565b868152601f196103c38861077e565b015f5b81811061064457505085525f5b868110156105415760248160051b860101359060405191632f0374dd60e21b83528060048401526020836024818d5afa928315610316575f9361050e575b5060405163a68bafa360e01b8152600481018290526020816024818e5afa8015610316575f906104dc575b60405163c69507dd60e01b81526004810184905291506020826024818f5afa918215610316575f926104a6575b5091839161049f936001966040519361048185610703565b84526020840152604083015260608201528951906102bc83836107aa565b50016103d3565b9150916020823d82116104d4575b816104c16020938361074f565b8101031261030a57905190916001610469565b3d91506104b4565b506020813d8211610506575b816104f56020938361074f565b8101031261030a576024905161043c565b3d91506104e8565b9092506020813d8211610539575b816105296020938361074f565b8101031261030a5751918a610411565b3d915061051c565b506040805160208082529351151584820152935167ffffffffffffffff16908401525160806060840152805160a08401819052839260c084019287929101905f5b8181106105f1575050505190601f19838203016080840152602080835192838152019201905f5b8181106105b7575050500390f35b91935091602060806001926060875180518352848101518584015260408101516040840152015160608201520194019101918493926105a9565b825180516001600160a01b03168652602081810151818801526040808301519088015260608083015115159088015260809182015115159187019190915287965060a09095019490920191600101610582565b60209060405161065381610703565b5f81525f838201525f60408201525f6060820152828286010152016103c6565b60209060405161068281610733565b5f81525f838201525f60408201525f60608201525f608082015282828601015201610172565b9150506040813d6040116106fb575b816106c46040938361074f565b8101031261030a5760206106d782610771565b9101519067ffffffffffffffff8216820361030a579067ffffffffffffffff610139565b3d91506106b7565b6080810190811067ffffffffffffffff82111761071f57604052565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761071f57604052565b90601f8019910116810190811067ffffffffffffffff82111761071f57604052565b5190811515820361030a57565b67ffffffffffffffff811161071f5760051b60200190565b356001600160a01b038116810361030a5790565b80518210156107be5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220a77aa844da5d389376ee1433d996e0bb253977abafbd38ee8232086c67b7d22164736f6c63430008240033"; + "0x608080604052346015576108bb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c6352ae457214610025575f80fd5b3461030e5760a036600319011261030e576004356001600160a01b038116919082900361030e576024356001600160a01b0381169081900361030e576044356001600160401b03811161030e57610080903690600401610785565b606492919235906001600160401b03821161030e573660238301121561030e578160040135946001600160401b03861161030e573660248760061b8501011161030e576084356001600160401b03811161030e576100e2903690600401610785565b60a083949294018381106001600160401b03821117610771576040525f835260208301935f8552604084019160608352606085019860608a526080860194606086528c60408b6024825180948193636b97fbcd60e11b835260048301525afa801561031a575f915f91610718575b506001600160401b0316895215158752610169816107fe565b61017660405191826107d0565b818152601f19610185836107fe565b0136602083013785528c5f5b82811061067257505050506101a58a6107fe565b6101b260405191826107d0565b8a8152601f196101c18c6107fe565b015f5b81811061065b57505089525f5b8a81101561035e578b908060061b890161022d60208c60446101f560248601610839565b60405163011f009b60e31b81526001600160a01b03909316600484015294013560248201819052959092839190829081906044820190565b03915afa801561031a578f8d86935f93610325575b506040516369f1e26b60e01b81526001600160a01b039190911660048201526024810193909352602090839060449082905afa91821561031a575f926102cf575b509183916102c8936001966040519361029b856107b5565b888060a01b0316845260208401526040830152151560608201528d51906102c2838361084d565b5261084d565b50016101d1565b9150916020823d8211610312575b816102ea602093836107d0565b8101031261030e576001946102c89361030386946107f1565b935091935094610283565b5f80fd5b3d91506102dd565b6040513d5f823e3d90fd5b93505050506020813d8211610356575b81610342602093836107d0565b8101031261030e575183908f8d6020610242565b3d9150610335565b50889291889161036d816107fe565b61037a60405191826107d0565b818152601f19610389836107fe565b015f5b81811061064457505086525f5b8181106104dc576001600160401b0389898989896040519586956020875260c0870195511515602088015251166040860152519260a060608601528351809152602060e086019401905f5b8181106104c1575050505191601f19848203016080850152602080845192838152019301905f5b81811061047a575050505190601f198382030160a0840152602080835192838152019201905f5b818110610440575050500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610432565b825180516001600160a01b0316865260208181015181880152604080830151908801526060918201511515918701919091528796506080909501949092019160010161040b565b825115158652879650602095860195909201916001016103e4565b6104e7818385610815565b359060405191632f0374dd60e21b8352806004840152602083602481895afa92831561031a575f93610611575b5060405163a68bafa360e01b8152600481018290526020816024818a5afa90811561031a575f916105e0575b5060405163c69507dd60e01b815260048101839052906020826024818b5afa91821561031a575f926105aa575b509183916105a39360019660405193610585856107b5565b84526020840152604083015260608201528a51906102c2838361084d565b5001610399565b9150916020823d82116105d8575b816105c5602093836107d0565b8101031261030e5790519091600161056d565b3d91506105b8565b90506020813d8211610609575b816105fa602093836107d0565b8101031261030e57518c610540565b3d91506105ed565b9092506020813d821161063c575b8161062c602093836107d0565b8101031261030e5751918b610514565b3d915061061f565b60209061064f610861565b8282860101520161038c565b602090610666610861565b828286010152016101c4565b60208c604461068a61068585888a610815565b610839565b6040516366faa83960e01b815260048101939093526001600160a01b03166024830152909384919082905afa801561031a575f906106df575b600192506106d282895161084d565b9015159052018d90610191565b506020823d8211610710575b816106f8602093836107d0565b8101031261030e5761070b6001926107f1565b6106c3565b3d91506106eb565b9150506040813d604011610769575b81610734604093836107d0565b8101031261030e576020610747826107f1565b910151906001600160401b038216820361030e57906001600160401b03610150565b3d9150610727565b634e487b7160e01b5f52604160045260245ffd5b9181601f8401121561030e578235916001600160401b03831161030e576020808501948460051b01011161030e57565b608081019081106001600160401b0382111761077157604052565b90601f801991011681019081106001600160401b0382111761077157604052565b5190811515820361030e57565b6001600160401b0381116107715760051b60200190565b91908110156108255760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b038116810361030e5790565b80518210156108255760209160051b010190565b6040519061086e826107b5565b5f606083828152826020820152826040820152015256fea264697066735822122000c10ec804ce24c308983455a410ac3fd542112e1d058ca4af402aeebd1bdf3164736f6c63430008240033"; diff --git a/packages/blue-sdk/AGENTS.md b/packages/blue-sdk/AGENTS.md index c88389b1c..12b92fab6 100644 --- a/packages/blue-sdk/AGENTS.md +++ b/packages/blue-sdk/AGENTS.md @@ -11,7 +11,7 @@ - Protocol entity folders (`market/`, `vault/`, `token/`, `position/`, `holding/`, `user/`) own their classes and folder barrels. - Getters may throw typed `Unknown*Error`; nullable lookup paths should use `_try` or `tryGet*`-style helpers deliberately. - Vault V2 absolute/relative allocation-cap math is canonical in `VaultV2Utils.allocationHeadroom`; consumers such as `AccrualVaultV2.maxDeposit` and shared-liquidity simulation delegate to it. -- Vault V2 BluePublicAllocator config interfaces are readonly identity-bearing projections: they include the explicit allocator and vault, plus the adapter and derived market-params id for pair-scoped state. +- Vault V2 BluePublicAllocator config interfaces are readonly identity-bearing projections: they include the explicit allocator and vault, plus the adapter and derived market-params id for market-scoped state. Adapter activation is normalized separately as a vault-keyed set of adapter addresses. - `marketParamsAbi` is owned by `@morpho-org/morpho-ts/abis` and re-exported from `MarketParams.ts` for backward compatibility; do not define a second copy in this package. ## Continuous Improvement diff --git a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts index fa55be003..c8d2181a0 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts @@ -12,7 +12,7 @@ export interface VaultV2PublicAllocatorConfig { readonly penalty: bigint; } -/** Public allocator permissions and cap for one Vault V2 adapter-market pair. */ +/** Public allocator permission and cap for one Vault V2 adapter-market pair. */ export interface VaultV2MarketPublicAllocatorConfig { /** BluePublicAllocator contract from which the configuration was read. */ readonly allocator: Address; @@ -26,6 +26,4 @@ export interface VaultV2MarketPublicAllocatorConfig { readonly absoluteCap: bigint; /** Whether the allocator may pull assets from this adapter-market pair. */ readonly canPullFromMarket: boolean; - /** Whether the allocator currently recognizes the adapter. */ - readonly isActiveAdapter: boolean; } diff --git a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts index 46061054b..7cd8dff52 100644 --- a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts +++ b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts @@ -544,6 +544,7 @@ describe("Blue actions with Vault V2 reallocations", () => { publicAllocatorConfigs: { [vault]: allocatorData.publicAllocatorConfig, }, + activeAdapters: { [vault]: allocatorData.activeAdapters }, marketPublicAllocatorConfigs: { [vault]: allocatorData.marketPublicAllocatorConfigs, }, diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index 3328050c8..9f6f1d949 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -96,6 +96,7 @@ interface FixtureOptions { readonly idle?: bigint; readonly canPullFromIdle?: boolean; readonly canPullFromMarket?: boolean; + readonly allocatorActiveAdapters?: ReadonlySet
; readonly penalty?: bigint; readonly sourceLastUpdate?: bigint; readonly targetLastUpdate?: bigint; @@ -123,6 +124,7 @@ const makeFixture = ({ idle = 0n, canPullFromIdle = true, canPullFromMarket = true, + allocatorActiveAdapters, penalty = 7n, sourceLastUpdate = TIMESTAMP, targetLastUpdate = TIMESTAMP, @@ -284,6 +286,11 @@ const makeFixture = ({ penalty, }, }, + activeAdapters: { + [VAULT]: + allocatorActiveAdapters ?? + new Set(adapters.map((adapter) => adapter.address)), + }, marketPublicAllocatorConfigs: { [VAULT]: { [targetIds[2]]: { @@ -293,7 +300,6 @@ const makeFixture = ({ marketParamsId: targetIds[2], absoluteCap: allocatorTargetCap, canPullFromMarket: false, - isActiveAdapter: true, }, [sourceIds[2]]: { allocator: ALLOCATOR, @@ -302,7 +308,6 @@ const makeFixture = ({ marketParamsId: sourceIds[2], absoluteCap: 0n, canPullFromMarket, - isActiveAdapter: true, }, }, }, @@ -345,6 +350,19 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ); }); + test("behavior: ignores inactive source and target adapters", () => { + for (const allocatorActiveAdapters of [ + new Set
([TARGET_ADAPTER]), + new Set
([SOURCE_ADAPTER]), + ]) { + const { data } = makeFixture({ allocatorActiveAdapters }); + + expect( + data.computeVaultV2Reallocations(targetParams.id).reallocations, + ).toStrictEqual([]); + } + }); + test("behavior: keeps two vault adapters on one canonical market", () => { const { data } = makeFixture({ sourceSupply: 0n, @@ -410,6 +428,10 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { penalty: 0n, }, }, + activeAdapters: { + [VAULT]: data.activeAdapters[VAULT], + [SECOND_VAULT]: new Set([SECOND_TARGET_ADAPTER]), + }, marketPublicAllocatorConfigs: { [VAULT]: data.marketPublicAllocatorConfigs[VAULT], [SECOND_VAULT]: { @@ -420,7 +442,6 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { marketParamsId: secondTargetIds[2], absoluteCap: 10_000n, canPullFromMarket: false, - isActiveAdapter: true, }, }, }, @@ -547,10 +568,15 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { vaults: { [VAULT]: inputVault }, allocations: data.allocations, publicAllocatorConfigs: data.publicAllocatorConfigs, + activeAdapters: data.activeAdapters, marketPublicAllocatorConfigs: data.marketPublicAllocatorConfigs, }); const cloned = input.clone(); + expect(cloned.activeAdapters[VAULT]).toStrictEqual( + input.activeAdapters[VAULT], + ); + expect(cloned.activeAdapters[VAULT]).not.toBe(input.activeAdapters[VAULT]); const inputLegacy = input .getVault(VAULT) .accrualAdapters.find( diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index 2d8b13f1b..1f7f6b0c8 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -62,6 +62,10 @@ export interface InputVaultV2ReallocationData { readonly publicAllocatorConfigs?: Readonly< Record >; + /** BluePublicAllocator-active adapters indexed by vault address. */ + readonly activeAdapters?: Readonly< + Record | undefined> + >; /** Adapter-market BluePublicAllocator configuration indexed by vault and `marketParamsId`. */ readonly marketPublicAllocatorConfigs?: Readonly< Record< @@ -221,6 +225,11 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { Address, VaultV2PublicAllocatorConfig | undefined >; + /** BluePublicAllocator-active adapters indexed by vault address. */ + public readonly activeAdapters: Record< + Address, + ReadonlySet
| undefined + >; /** Adapter-market allocator configuration indexed by vault and market-params id. */ public readonly marketPublicAllocatorConfigs: Record< Address, @@ -239,6 +248,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { this.vaults = {}; this.allocations = {}; this.publicAllocatorConfigs = {}; + this.activeAdapters = {}; this.marketPublicAllocatorConfigs = {}; this.donatedPenaltyAssets = input instanceof VaultV2ReallocationData @@ -288,6 +298,13 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { config == null ? undefined : { ...config }; } + for (const [vault, adapters] of Object.entries( + input.activeAdapters ?? {}, + ) as [Address, ReadonlySet
| undefined][]) { + this.activeAdapters[vault] = + adapters == null ? undefined : new Set(adapters); + } + for (const [vault, configs] of Object.entries( input.marketPublicAllocatorConfigs ?? {}, ) as [ @@ -657,6 +674,8 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { publicAllocatorConfig.penalty > options.maxPenalty) ) return; + const activeAdapters = data.activeAdapters[vaultAddress]; + if (activeAdapters == null) return; const targetSupplyHeadroom = MathLib.zeroFloorSub( MathLib.MAX_UINT_128, @@ -701,7 +720,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { marketPublicAllocatorConfig.adapter, adapter.address, ) || - !marketPublicAllocatorConfig.isActiveAdapter + !activeAdapters.has(adapter.address) ) return; @@ -799,7 +818,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { sourceConfig.adapter, sourceAdapter.address, ) || - !sourceConfig.isActiveAdapter || + !activeAdapters.has(sourceAdapter.address) || !sourceConfig.canPullFromMarket ) return; From 141f00a08c3267a5e56c9627ed51fee0eb0c7c6c Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Mon, 17 Aug 2026 15:42:39 +0200 Subject: [PATCH 18/41] refactor: simplify public allocator implementation --- .../morpho-sdk/src/actions/blue/borrow.ts | 22 +++++++-------- .../actions/blue/buildReallocationActions.ts | 6 ++--- .../morpho-sdk/src/actions/blue/refinance.ts | 22 +++++++-------- .../actions/blue/supplyCollateralBorrow.ts | 19 ++++++------- .../morpho-sdk/src/actions/blue/withdraw.ts | 27 ++++++++----------- .../src/entities/vaultV2ReallocationData.ts | 7 +++-- 6 files changed, 45 insertions(+), 58 deletions(-) diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index aaa7d20f4..e1382e07d 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -113,23 +113,21 @@ export const blueBorrow = ({ } const actions: Action[] = []; - let reallocationFee = 0n; - let reallocationPenaltyAssets = 0n; if (authorizationSignature) { actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } - if (reallocations && reallocations.length > 0) { - const result = buildReallocationActions({ - chainId, - reallocations, - targetMarketParams: marketParams, - }); - actions.push(...result.actions); - reallocationFee = result.fee; - reallocationPenaltyAssets = result.penaltyAssets; - } + const { + actions: reallocationActions, + fee: reallocationFee, + penaltyAssets: reallocationPenaltyAssets, + } = buildReallocationActions({ + chainId, + reallocations, + targetMarketParams: marketParams, + }); + actions.push(...reallocationActions); actions.push({ type: "morphoBorrow", diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index cd8cb7f7d..e0bb8fbc8 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -15,7 +15,7 @@ import type { BlueReallocation } from "../../types/index.js"; * * @param params - Reallocation encoding inputs. * @param params.chainId - Chain where the bundle will execute. - * @param params.reallocations - PublicAllocator V1 and BluePublicAllocator reallocations in execution order. + * @param params.reallocations - Optional PublicAllocator V1 and BluePublicAllocator reallocations in execution order. * @param params.targetMarketParams - Target market params derived from the enclosing Blue action. * @param params.penaltyFundingSource - Account that already holds the aggregate V2 penalty. Uses * the transaction initiator by default; same-token collateral funding can pre-fund @@ -35,12 +35,12 @@ import type { BlueReallocation } from "../../types/index.js"; */ export const buildReallocationActions = ({ chainId, - reallocations, + reallocations = [], targetMarketParams, penaltyFundingSource = "initiator", }: { readonly chainId: number; - readonly reallocations: readonly BlueReallocation[]; + readonly reallocations?: readonly BlueReallocation[]; readonly targetMarketParams: MarketParams; readonly penaltyFundingSource?: "initiator" | "generalAdapter1"; }): { diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index 962e6b316..93f2cf531 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -274,23 +274,21 @@ export const blueRefinance = ({ }); const actions: Action[] = []; - let reallocationFee = 0n; - let reallocationPenaltyAssets = 0n; if (authorizationSignature) { actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } - if (targetReallocations && targetReallocations.length > 0) { - const result = buildReallocationActions({ - chainId, - reallocations: targetReallocations, - targetMarketParams: targetParams, - }); - actions.push(...result.actions); - reallocationFee = result.fee; - reallocationPenaltyAssets = result.penaltyAssets; - } + const { + actions: reallocationActions, + fee: reallocationFee, + penaltyAssets: reallocationPenaltyAssets, + } = buildReallocationActions({ + chainId, + reallocations: targetReallocations, + targetMarketParams: targetParams, + }); + actions.push(...reallocationActions); actions.push({ type: "morphoSupplyCollateral", diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 506ac6aea..125a476f8 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -167,17 +167,14 @@ export const blueSupplyCollateralBorrow = ({ marketParams.collateralToken, marketParams.loanToken, ); - const reallocationResult = - reallocations && reallocations.length > 0 - ? buildReallocationActions({ - chainId, - reallocations, - targetMarketParams: marketParams, - penaltyFundingSource: usesSharedFundingToken - ? "generalAdapter1" - : "initiator", - }) - : { actions: [], fee: 0n, penaltyAssets: 0n }; + const reallocationResult = buildReallocationActions({ + chainId, + reallocations, + targetMarketParams: marketParams, + penaltyFundingSource: usesSharedFundingToken + ? "generalAdapter1" + : "initiator", + }); const erc20FundingAmount = amount + (usesSharedFundingToken ? reallocationResult.penaltyAssets : 0n); diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index 6bb36b47a..fb6f68e79 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -146,33 +146,28 @@ export const blueWithdraw = ({ } const actions: Action[] = []; - let reallocationFee = 0n; - let reallocationPenaltyAssets = 0n; if (authorizationSignature) { actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } - if (reallocations && reallocations.length > 0) { - const result = buildReallocationActions({ - chainId, - reallocations, - targetMarketParams: marketParams, - }); - actions.push(...result.actions); - reallocationFee = result.fee; - reallocationPenaltyAssets = result.penaltyAssets; - } + const { + actions: reallocationActions, + fee: reallocationFee, + penaltyAssets: reallocationPenaltyAssets, + } = buildReallocationActions({ + chainId, + reallocations, + targetMarketParams: marketParams, + }); + actions.push(...reallocationActions); actions.push({ type: "morphoWithdraw", args: [marketParams, assets, shares, minSharePrice, receiver, false], }); - let tx = { - ...BundlerAction.encodeBundle(chainId, actions), - value: reallocationFee, - }; + let tx = BundlerAction.encodeBundle(chainId, actions); if (metadata) { tx = addTransactionMetadata(tx, metadata); diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index 1f7f6b0c8..519eef9bf 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -597,7 +597,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { : 0n; const reallocations: VaultV2BlueReallocation[] = []; let remainingRequiredAssets = requiredAssets; - let totalReallocated = 0n; for (const reallocation of discovered) { const assets = MathLib.min(reallocation.assets, remainingRequiredAssets); @@ -605,15 +604,15 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { reallocations.push({ ...reallocation, assets }); remainingRequiredAssets -= assets; - totalReallocated += assets; if (remainingRequiredAssets === 0n) break; } - if (totalReallocated < absoluteShortfall) { + const reallocatedAssets = requiredAssets - remainingRequiredAssets; + if (reallocatedAssets < absoluteShortfall) { throw new InsufficientSharedLiquidityError({ marketId, shortfall: absoluteShortfall, - available: totalReallocated, + available: reallocatedAssets, }); } From db7906ae11bb90606dcdd089735e3ed57d2ff629 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Mon, 17 Aug 2026 16:21:23 +0200 Subject: [PATCH 19/41] refactor: accept iterable reallocation inputs --- .changeset/brave-vaults-reallocate.md | 2 +- ...lt-v2-public-allocator-shared-liquidity.md | 14 ++-- .../blue/borrow.bluePublicAllocator.test.ts | 2 +- .../morpho-sdk/src/actions/blue/borrow.ts | 2 +- .../actions/blue/buildReallocationActions.ts | 10 +-- .../morpho-sdk/src/actions/blue/refinance.ts | 2 +- .../actions/blue/supplyCollateralBorrow.ts | 2 +- .../morpho-sdk/src/actions/blue/withdraw.ts | 2 +- packages/morpho-sdk/src/entities/AGENTS.md | 2 +- .../blue/blue.inputValidation.test.ts | 8 +-- packages/morpho-sdk/src/entities/blue/blue.ts | 65 +++++++++++-------- .../entities/vaultV1ReallocationData.test.ts | 2 +- .../src/entities/vaultV1ReallocationData.ts | 2 +- .../entities/vaultV2ReallocationData.test.ts | 15 +++-- .../src/entities/vaultV2ReallocationData.ts | 20 ++++-- .../src/helpers/bluePublicAllocator.ts | 25 ++++--- .../helpers/computeVaultV1Reallocations.ts | 17 +++-- packages/morpho-sdk/src/helpers/validate.ts | 2 +- .../morpho-sdk/src/types/sharedLiquidity.ts | 10 ++- .../src/morpho-protocol-evm.ts | 4 +- 20 files changed, 125 insertions(+), 83 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 763c59c7c..8c62c3532 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -6,7 +6,7 @@ "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, normalize active adapters as a vault-keyed address set, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. +Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while normalizing them before repeated use, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index 856cae2cb..738c2626a 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -116,6 +116,11 @@ native value includes only V1 fees. V2 penalty assets are pulled once in the target loan token through GeneralAdapter1, then approved and spent from Bundler3 per allocator call. +Consumer-supplied reallocation plans and vault allowlists accept any iterable, +including arrays, readonly arrays, sets, and generators. Entry points normalize +them before validation or repeated lazy use; ordered output descriptors remain +readonly arrays. + ## Contract model The ABI is pinned from `morpho-org/vault-v2` at the same upstream revision as @@ -196,7 +201,7 @@ export interface InputVaultV2ReallocationData { Record >; readonly activeAdapters?: Readonly< - Record | undefined> + Record | undefined> >; readonly marketPublicAllocatorConfigs?: Readonly< Record< @@ -208,9 +213,10 @@ export interface InputVaultV2ReallocationData { ``` The readonly config projections are self-identifying. Vault-wide state carries -`allocator`, `vault`, `canPullFromIdle`, and `penalty`. Adapter activation is -normalized as a vault-keyed set of adapter addresses. Market state carries -`adapter`, `marketParamsId`, `absoluteCap`, and `canPullFromMarket`. +`allocator`, `vault`, `canPullFromIdle`, and `penalty`. Adapter activation input +accepts arrays, readonly arrays, sets, or any other iterable, and is normalized +as a vault-keyed set of adapter addresses. Market state carries `adapter`, +`marketParamsId`, `absoluteCap`, and `canPullFromMarket`. ## Fetching diff --git a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts index 7a0186399..d92dc74af 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -79,7 +79,7 @@ describe("blueBorrow Blue Public Allocator", () => { amount: 1n, minSharePrice: 0n, receiver, - reallocations, + reallocations: reallocations.values(), }, }); diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index e1382e07d..9ee08753f 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -29,7 +29,7 @@ export interface BlueBorrowParams { /** Minimum borrow share price (in ray). Protects against share price manipulation. */ minSharePrice: bigint; /** Public Allocator V1 or V2 reallocations to execute before borrowing. */ - reallocations?: readonly BlueReallocation[]; + reallocations?: Iterable; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index e0bb8fbc8..f70e08384 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -40,7 +40,7 @@ export const buildReallocationActions = ({ penaltyFundingSource = "initiator", }: { readonly chainId: number; - readonly reallocations?: readonly BlueReallocation[]; + readonly reallocations?: Iterable; readonly targetMarketParams: MarketParams; readonly penaltyFundingSource?: "initiator" | "generalAdapter1"; }): { @@ -48,12 +48,14 @@ export const buildReallocationActions = ({ readonly fee: bigint; readonly penaltyAssets: bigint; } => { + const reallocationList = [...reallocations]; // Validate the action descriptors before encoding; the validator returns void. - validateReallocations(reallocations, targetMarketParams.id); + validateReallocations(reallocationList, targetMarketParams.id); let fee = 0n; const actions: Action[] = []; - const penaltyAssets = computeVaultV2ReallocationPenaltyAssets(reallocations); + const penaltyAssets = + computeVaultV2ReallocationPenaltyAssets(reallocationList); if (penaltyAssets > 0n) { const { @@ -83,7 +85,7 @@ export const buildReallocationActions = ({ ); } - for (const reallocation of reallocations) { + for (const reallocation of reallocationList) { if (reallocation.type === "bluePublicAllocator") { if (reallocation.from.type === "market") { actions.push({ diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index 93f2cf531..84607c178 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -45,7 +45,7 @@ export interface BlueRefinanceParams { /** Maximum repay share price on the source market (in ray); must be > 0 when a repay leg exists. */ maxRepaySharePrice: bigint; /** Public Allocator V1 or V2 reallocations into the target market, run before the supply leg. */ - targetReallocations?: readonly BlueReallocation[]; + targetReallocations?: Iterable; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 125a476f8..14b8fc110 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -36,7 +36,7 @@ export interface BlueSupplyCollateralBorrowParams { /** Optional pre-signed permit/permit2 approval for the collateral transfer. */ requirementSignature?: PermitRequirementSignature; /** Public Allocator V1 or V2 reallocations to execute before borrowing. */ - reallocations?: readonly BlueReallocation[]; + reallocations?: Iterable; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index fb6f68e79..affd16487 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -36,7 +36,7 @@ export interface BlueWithdrawParams { * computed via `MorphoBlue.getReallocations({ operation: "withdraw", amount })` or directly * via `computeVaultV1Reallocations({ operation: "withdraw", amount, ... })`. */ - reallocations?: readonly BlueReallocation[]; + reallocations?: Iterable; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a diff --git a/packages/morpho-sdk/src/entities/AGENTS.md b/packages/morpho-sdk/src/entities/AGENTS.md index c0c58805c..e9c7cfaf5 100644 --- a/packages/morpho-sdk/src/entities/AGENTS.md +++ b/packages/morpho-sdk/src/entities/AGENTS.md @@ -15,6 +15,6 @@ See [`packages/morpho-sdk/AGENTS.md`](../../AGENTS.md) routing summary. ## Shared liquidity -`MorphoBlue.borrow()`, `supplyCollateralBorrow()`, `withdraw()`, and `refinance()` accept optional reallocations. The entity validates their state-independent shape before returning requirements, and the pure action repeats the same validation before encoding. `getReallocationData` may fetch the inputs needed to compute reallocations, but action encoding stays outside the entity fetch path. +`MorphoBlue.borrow()`, `supplyCollateralBorrow()`, `withdraw()`, and `refinance()` accept optional reallocations. Consumer-supplied reallocation plans and vault allowlists accept any iterable and are normalized once before lazy or repeated use; ordered outputs remain readonly arrays. The entity validates their state-independent shape before returning requirements, and the pure action repeats the same validation before encoding. `getReallocationData` may fetch the inputs needed to compute reallocations, but action encoding stays outside the entity fetch path. `VaultV1ReallocationData` is the entity-level state container for PublicAllocator V1 simulations; `ReallocationData` remains its deprecated compatibility alias. `VaultV2ReallocationData` owns the separate BluePublicAllocator state model. Their public maps are readable snapshots for inspection; state transitions stay on their methods and return cloned instances of the same versioned class. diff --git a/packages/morpho-sdk/src/entities/blue/blue.inputValidation.test.ts b/packages/morpho-sdk/src/entities/blue/blue.inputValidation.test.ts index 5c14ff9b7..7d1e06844 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.inputValidation.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.inputValidation.test.ts @@ -35,7 +35,7 @@ describe("MorphoBlue reallocation input validation", () => { assets: 1n, userAddress: USER, positionData: undefined as never, - reallocations: INVALID_REALLOCATIONS, + reallocations: INVALID_REALLOCATIONS.values(), }), ).toThrow(NegativeInputError); }); @@ -48,7 +48,7 @@ describe("MorphoBlue reallocation input validation", () => { amount: 1n, userAddress: USER, positionData: undefined as never, - reallocations: INVALID_REALLOCATIONS, + reallocations: INVALID_REALLOCATIONS.values(), }), ).toThrow(NegativeInputError); }); @@ -62,7 +62,7 @@ describe("MorphoBlue reallocation input validation", () => { borrowAmount: 1n, userAddress: USER, positionData: undefined as never, - reallocations: INVALID_REALLOCATIONS, + reallocations: INVALID_REALLOCATIONS.values(), }), ).toThrow(NegativeInputError); }); @@ -79,7 +79,7 @@ describe("MorphoBlue reallocation input validation", () => { positionData: undefined as never, }, collateralAmount: 1n, - targetReallocations: INVALID_REALLOCATIONS, + targetReallocations: INVALID_REALLOCATIONS.values(), }), ).toThrow(NegativeInputError); }); diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index e890a0da4..d8564f619 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -213,7 +213,7 @@ export interface BlueActions { receiver?: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly BlueReallocation[]; + reallocations?: Iterable; } & AssetsOrSharesArgs, ) => { buildTx: ( @@ -257,7 +257,7 @@ export interface BlueActions { amount: bigint; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly BlueReallocation[]; + reallocations?: Iterable; }) => { buildTx: ( signatures?: readonly RequirementSignature[], @@ -411,7 +411,7 @@ export interface BlueActions { positionData: AccrualPosition; borrowAmount: bigint; slippageTolerance?: bigint; - reallocations?: readonly BlueReallocation[]; + reallocations?: Iterable; } & DepositAmountArgs, ) => { buildTx: ( @@ -475,7 +475,7 @@ export interface BlueActions { borrowAssets?: bigint; borrowShares?: bigint; slippageTolerance?: bigint; - targetReallocations?: readonly BlueReallocation[]; + targetReallocations?: Iterable; }) => { buildTx: ( signatures?: readonly RequirementSignature[], @@ -573,7 +573,7 @@ export class MorphoBlue implements BlueActions { private getReallocationPenaltyRequirements( userAddress: Address, - reallocations: readonly BlueReallocation[] | undefined, + reallocations: Iterable | undefined, ) { const amount = computeVaultV2ReallocationPenaltyAssets(reallocations ?? []); @@ -699,7 +699,7 @@ export class MorphoBlue implements BlueActions { receiver?: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly BlueReallocation[]; + reallocations?: Iterable; } & AssetsOrSharesArgs, ) { validateChainId(this.client.viemClient.chain?.id, this.chainId); @@ -711,6 +711,7 @@ export class MorphoBlue implements BlueActions { slippageTolerance = DEFAULT_SLIPPAGE_TOLERANCE, reallocations, } = params; + const reallocationList = [...(reallocations ?? [])]; // Mode normalization: a missing or undefined `assets`/`shares` key collapses to `0n` // so the mutual-exclusion and positivity checks below are pure value comparisons. @@ -735,9 +736,9 @@ export class MorphoBlue implements BlueActions { } validateSlippageTolerance(slippageTolerance); - if (reallocations) { + if (reallocationList.length > 0) { // Validate caller-supplied descriptors before reading state; the helper returns void. - validateReallocations(reallocations, this.marketParams.id); + validateReallocations(reallocationList, this.marketParams.id); } if (!positionData) { @@ -774,7 +775,10 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { const [penaltyRequirements, authTx] = await Promise.all([ - this.getReallocationPenaltyRequirements(userAddress, reallocations), + this.getReallocationPenaltyRequirements( + userAddress, + reallocationList, + ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -797,7 +801,7 @@ export class MorphoBlue implements BlueActions { shares, receiver, minSharePrice, - reallocations, + reallocations: reallocationList, authorizationSignature: authorization, }, metadata: this.client.options.metadata, @@ -873,18 +877,19 @@ export class MorphoBlue implements BlueActions { userAddress: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly BlueReallocation[]; + reallocations?: Iterable; }) { validateChainId(this.client.viemClient.chain?.id, this.chainId); + const reallocationList = [...(reallocations ?? [])]; if (amount <= 0n) { throw new NonPositiveInputError("amount", amount); } validateSlippageTolerance(slippageTolerance); - if (reallocations) { + if (reallocationList.length > 0) { // Validate caller-supplied descriptors before reading state; the helper returns void. - validateReallocations(reallocations, this.marketParams.id); + validateReallocations(reallocationList, this.marketParams.id); } if (!positionData) { @@ -913,7 +918,10 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { const [penaltyRequirements, authTx] = await Promise.all([ - this.getReallocationPenaltyRequirements(userAddress, reallocations), + this.getReallocationPenaltyRequirements( + userAddress, + reallocationList, + ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -938,7 +946,7 @@ export class MorphoBlue implements BlueActions { amount, receiver: userAddress, minSharePrice, - reallocations, + reallocations: reallocationList, authorizationSignature: authorization, }, metadata: this.client.options.metadata, @@ -1371,9 +1379,10 @@ export class MorphoBlue implements BlueActions { positionData: AccrualPosition; borrowAmount: bigint; slippageTolerance?: bigint; - reallocations?: readonly BlueReallocation[]; + reallocations?: Iterable; } & DepositAmountArgs) { validateChainId(this.client.viemClient.chain?.id, this.chainId); + const reallocationList = [...(reallocations ?? [])]; if (amount < 0n) { throw new NegativeInputError("amount", amount); @@ -1393,9 +1402,9 @@ export class MorphoBlue implements BlueActions { } validateSlippageTolerance(slippageTolerance); - if (reallocations) { + if (reallocationList.length > 0) { // Validate caller-supplied descriptors before reading state; the helper returns void. - validateReallocations(reallocations, this.marketParams.id); + validateReallocations(reallocationList, this.marketParams.id); } if (!positionData) { @@ -1427,9 +1436,8 @@ export class MorphoBlue implements BlueActions { }); return { getRequirements: async (params?: { useSimplePermit?: boolean }) => { - const penaltyAssets = computeVaultV2ReallocationPenaltyAssets( - reallocations ?? [], - ); + const penaltyAssets = + computeVaultV2ReallocationPenaltyAssets(reallocationList); const usesSharedFundingToken = isAddressEqual( this.marketParams.collateralToken, this.marketParams.loanToken, @@ -1451,7 +1459,7 @@ export class MorphoBlue implements BlueActions { ? Promise.resolve([]) : this.getReallocationPenaltyRequirements( userAddress, - reallocations, + reallocationList, ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, @@ -1488,7 +1496,7 @@ export class MorphoBlue implements BlueActions { minSharePrice, requirementSignature: permit, authorizationSignature: authorization, - reallocations, + reallocations: reallocationList, }, metadata: this.client.options.metadata, }); @@ -1516,10 +1524,11 @@ export class MorphoBlue implements BlueActions { borrowAssets?: bigint; borrowShares?: bigint; slippageTolerance?: bigint; - targetReallocations?: readonly BlueReallocation[]; + targetReallocations?: Iterable; }) { validateChainId(this.client.viemClient.chain?.id, this.chainId); validateSlippageTolerance(slippageTolerance); + const targetReallocationList = [...(targetReallocations ?? [])]; if (collateralAmount <= 0n) { throw new NonPositiveInputError("collateralAmount", collateralAmount); @@ -1536,9 +1545,9 @@ export class MorphoBlue implements BlueActions { if (requestedAssets > 0n && requestedShares > 0n) { throw new BorrowAmountAndSharesExclusiveError(this.marketParams.id); } - if (targetReallocations) { + if (targetReallocationList.length > 0) { // Validate caller-supplied descriptors before reading state; the helper returns void. - validateReallocations(targetReallocations, target.marketParams.id); + validateReallocations(targetReallocationList, target.marketParams.id); } if (!positionData) { @@ -1698,7 +1707,7 @@ export class MorphoBlue implements BlueActions { const [penaltyRequirements, authTx] = await Promise.all([ this.getReallocationPenaltyRequirements( userAddress, - targetReallocations, + targetReallocationList, ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, @@ -1728,7 +1737,7 @@ export class MorphoBlue implements BlueActions { borrowShares: requestedShares, minBorrowSharePrice, maxRepaySharePrice, - targetReallocations, + targetReallocations: targetReallocationList, authorizationSignature: authorization, }, metadata: this.client.options.metadata, diff --git a/packages/morpho-sdk/src/entities/vaultV1ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.test.ts index 5723315ec..ae168e027 100644 --- a/packages/morpho-sdk/src/entities/vaultV1ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.test.ts @@ -513,7 +513,7 @@ describe("VaultV1ReallocationData unit coverage", () => { ).toEqual([]); expect( data.getMarketPublicReallocations(targetParams.id, { - reallocatableVaults: [zeroAddress], + reallocatableVaults: new Set([zeroAddress]), }).withdrawals, ).toEqual([]); diff --git a/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts index 2797a8e96..e5d5db4ad 100644 --- a/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts @@ -400,7 +400,7 @@ export class VaultV1ReallocationData implements InputVaultV1ReallocationData { ); const vaults = Array.from( new Set( - (reallocatableVaults ?? configuredVaults) + [...(reallocatableVaults ?? configuredVaults)] .map((vault) => vaultKeyByLower.get(vault.toLowerCase())) .filter((vault): vault is Address => vault != null), ), diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index 9f6f1d949..d05e4d798 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -96,7 +96,7 @@ interface FixtureOptions { readonly idle?: bigint; readonly canPullFromIdle?: boolean; readonly canPullFromMarket?: boolean; - readonly allocatorActiveAdapters?: ReadonlySet
; + readonly allocatorActiveAdapters?: Iterable
; readonly penalty?: bigint; readonly sourceLastUpdate?: bigint; readonly targetLastUpdate?: bigint; @@ -288,8 +288,7 @@ const makeFixture = ({ }, activeAdapters: { [VAULT]: - allocatorActiveAdapters ?? - new Set(adapters.map((adapter) => adapter.address)), + allocatorActiveAdapters ?? adapters.map((adapter) => adapter.address), }, marketPublicAllocatorConfigs: { [VAULT]: { @@ -323,6 +322,9 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { test("default: returns an action-ready market reallocation and cloned post-state", () => { const { data, sourceExpectedAssets, sourceIds, targetIds } = makeFixture(); + expect(data.activeAdapters[VAULT]).toStrictEqual( + new Set([TARGET_ADAPTER, SOURCE_ADAPTER]), + ); const result = data.computeVaultV2Reallocations(targetParams.id); expect(result.reallocations).toStrictEqual([ @@ -352,9 +354,9 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { test("behavior: ignores inactive source and target adapters", () => { for (const allocatorActiveAdapters of [ - new Set
([TARGET_ADAPTER]), - new Set
([SOURCE_ADAPTER]), - ]) { + [TARGET_ADAPTER], + [SOURCE_ADAPTER], + ] as const) { const { data } = makeFixture({ allocatorActiveAdapters }); expect( @@ -1006,6 +1008,7 @@ describe("computeVaultV2Reallocations", () => { marketId: targetParams.id, operation: "borrow", amount: 40n, + options: { reallocatableVaults: [VAULT as Address].values() }, }); expect(reallocations[0]?.assets).toBe(40n); diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index 519eef9bf..aee175a36 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -62,9 +62,12 @@ export interface InputVaultV2ReallocationData { readonly publicAllocatorConfigs?: Readonly< Record >; - /** BluePublicAllocator-active adapters indexed by vault address. */ + /** + * BluePublicAllocator-active adapters indexed by vault address. + * Arrays, readonly arrays, sets, and other iterables are accepted and normalized to sets. + */ readonly activeAdapters?: Readonly< - Record | undefined> + Record | undefined> >; /** Adapter-market BluePublicAllocator configuration indexed by vault and `marketParamsId`. */ readonly marketPublicAllocatorConfigs?: Readonly< @@ -300,7 +303,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { for (const [vault, adapters] of Object.entries( input.activeAdapters ?? {}, - ) as [Address, ReadonlySet
| undefined][]) { + ) as [Address, Iterable
| undefined][]) { this.activeAdapters[vault] = adapters == null ? undefined : new Set(adapters); } @@ -532,7 +535,14 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { options?.timestamp == null ? this.getLatestSnapshotTimestamp() : BigInt(options.timestamp); - const normalizedOptions = { ...options, timestamp }; + const normalizedOptions = { + ...options, + timestamp, + reallocatableVaults: + options?.reallocatableVaults == null + ? undefined + : [...options.reallocatableVaults], + }; const market = this.getMarket(marketId).accrueInterest(timestamp); if (operation === "withdraw" && amount > market.totalSupplyAssets) { throw new ReallocationWithdrawExceedsMarketSupplyError({ @@ -649,7 +659,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { ); const vaults = Array.from( new Set( - (options.reallocatableVaults ?? configuredVaults) + [...(options.reallocatableVaults ?? configuredVaults)] .map((vault) => vaultKeyByLower.get(vault.toLowerCase())) .filter((vault): vault is Address => vault != null), ), diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts index 61e926679..470b64b25 100644 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts @@ -41,16 +41,15 @@ export const computeBluePublicAllocatorPenaltyAssets = ( * @internal */ export const computeVaultV2ReallocationPenaltyAssets = ( - reallocations: readonly BlueReallocation[], -) => - reallocations.reduce( - (total, reallocation) => - reallocation.type === "bluePublicAllocator" - ? total + - computeBluePublicAllocatorPenaltyAssets( - reallocation.assets, - reallocation.penalty, - ) - : total, - 0n, - ); + reallocations: Iterable, +) => { + let total = 0n; + for (const reallocation of reallocations) { + if (reallocation.type === "bluePublicAllocator") + total += computeBluePublicAllocatorPenaltyAssets( + reallocation.assets, + reallocation.penalty, + ); + } + return total; +}; diff --git a/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts index 543c4d335..13253c8c2 100644 --- a/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts @@ -162,10 +162,19 @@ export const computeVaultV1Reallocations = ({ readonly options?: ReallocationComputeOptions; }): readonly VaultV1BlueReallocation[] => { if (options?.enabled === false) return []; + const normalizedOptions = { + ...options, + reallocatableVaults: + options?.reallocatableVaults == null + ? undefined + : [...options.reallocatableVaults], + }; // VaultV1ReallocationData does not retain the fetch block; pass that block timestamp // to compute against the same accrued state, otherwise Market defaults to lastUpdate. - const market = data.getMarket(marketId).accrueInterest(options?.timestamp); + const market = data + .getMarket(marketId) + .accrueInterest(normalizedOptions.timestamp); // Reject unreachable withdraws before any utilization math: a negative // post-supply yields a negative utilization that short-circuits the @@ -190,7 +199,7 @@ export const computeVaultV1Reallocations = ({ const supplyTargetUtilization = getSupplyTargetUtilization( market.params.id, - options, + normalizedOptions, ); if ( @@ -210,7 +219,7 @@ export const computeVaultV1Reallocations = ({ // Phase 1: "friendly" reallocations respecting withdrawal utilization targets. const { withdrawals: friendlyWithdrawals, data: friendlyReallocationData } = - data.computeVaultV1Reallocations(market.id, options); + data.computeVaultV1Reallocations(market.id, normalizedOptions); const withdrawals = [...friendlyWithdrawals]; @@ -233,7 +242,7 @@ export const computeVaultV1Reallocations = ({ requiredAssets = newTotalBorrowAssets - newTotalSupplyAssets; withdrawals.push( ...friendlyReallocationData.computeVaultV1Reallocations(market.id, { - ...options, + ...normalizedOptions, defaultMaxWithdrawalUtilization: MathLib.WAD, maxWithdrawalUtilization: {}, }).withdrawals, diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index 302b14f30..c0e3c6d6b 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -366,7 +366,7 @@ export const validateRepayShares = (params: { * ``` */ export const validateReallocations = ( - reallocations: readonly BlueReallocation[], + reallocations: Iterable, targetMarketId: MarketId, ): void => { const penaltyByAllocatorVault = new Map(); diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 1cb7e29a7..206fee2fd 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -16,9 +16,10 @@ export interface PublicAllocatorOptions { /** * Vaults to consider for reallocation. They must have enabled the PublicAllocator. + * Arrays, readonly arrays, sets, and other iterables are accepted. * Defaults to all vaults present in the reallocation data. */ - readonly reallocatableVaults?: readonly Address[]; + readonly reallocatableVaults?: Iterable
; /** * The maximum utilization each source market may reach when withdrawing @@ -52,8 +53,11 @@ export interface VaultV2BluePublicAllocatorOptions { /** Timestamp at which market and Vault V2 interest is evaluated. */ readonly timestamp?: BigIntish; - /** Vault V2 addresses to consider. Defaults to every vault in the reallocation data. */ - readonly reallocatableVaults?: readonly Address[]; + /** + * Vault V2 addresses to consider. Arrays, readonly arrays, sets, and other + * iterables are accepted. Defaults to every vault in the reallocation data. + */ + readonly reallocatableVaults?: Iterable
; /** * Maximum proportional vault-asset penalty accepted for each diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 298ec91ea..6931bb35a 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -158,7 +158,7 @@ export interface MorphoBorrowOptions { /** The address on behalf of which the borrow operation should be performed. Must match the wallet account address when set. */ onBehalfOf?: string; /** Optional Vault V1 PublicAllocator reallocations to include in the borrow action. */ - reallocations?: readonly VaultReallocation[]; + reallocations?: Iterable; /** Signature returned by a Morpho SDK authorization requirement, folded into the bundle as `setAuthorizationWithSig`. */ requirementSignature?: RequirementSignature; /** Optional Morpho SDK slippage tolerance in WAD precision. */ @@ -177,7 +177,7 @@ export type MorphoBorrowWithV2ReallocationsOptions = Omit< "reallocations" > & { /** Vault V1 and Vault V2 reallocations to include in the borrow action. */ - readonly reallocations: readonly BlueReallocation[]; + readonly reallocations: Iterable; }; type MorphoBorrowInput = From 4f1a1969a0f1367806e65f49888d1836e24e71e0 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Mon, 17 Aug 2026 17:43:37 +0200 Subject: [PATCH 20/41] feat: register Vault V2 public allocators by chain --- .changeset/brave-vaults-reallocate.md | 2 +- ...lt-v2-public-allocator-shared-liquidity.md | 39 ++++--- packages/blue-sdk-viem/AGENTS.md | 2 +- ...2PublicAllocatorConfig.integration.test.ts | 23 ++-- .../VaultV2PublicAllocatorConfig.test.ts | 20 ++-- .../vault-v2/VaultV2PublicAllocatorConfig.ts | 61 +++++----- packages/blue-sdk/AGENTS.md | 2 +- .../vault/v2/VaultV2PublicAllocatorConfig.ts | 4 - packages/morpho-sdk/AGENTS.md | 4 +- packages/morpho-sdk/src/actions/AGENTS.md | 2 +- .../morpho-sdk/src/actions/blue/AGENTS.md | 2 +- .../blue/borrow.bluePublicAllocator.test.ts | 11 +- .../morpho-sdk/src/actions/blue/borrow.ts | 6 +- .../actions/blue/buildReallocationActions.ts | 10 +- .../src/actions/blue/refinance.test.ts | 6 +- .../morpho-sdk/src/actions/blue/refinance.ts | 6 +- .../blue/supplyCollateralBorrow.test.ts | 2 - .../actions/blue/supplyCollateralBorrow.ts | 6 +- .../vaultV2Reallocations.integration.test.ts | 32 ++++-- .../blue/withdraw.bluePublicAllocator.test.ts | 6 +- .../morpho-sdk/src/actions/blue/withdraw.ts | 6 +- .../morpho-sdk/src/bundler/actions.test.ts | 50 ++++++--- packages/morpho-sdk/src/bundler/actions.ts | 42 ++++--- packages/morpho-sdk/src/bundler/types.ts | 6 +- ...ue.bluePublicAllocatorRequirements.test.ts | 6 - packages/morpho-sdk/src/entities/blue/blue.ts | 24 ++-- .../entities/vaultV2ReallocationData.test.ts | 11 -- .../src/entities/vaultV2ReallocationData.ts | 19 ---- .../src/helpers/bluePublicAllocator.test.ts | 4 - .../src/helpers/bluePublicAllocator.ts | 2 +- .../morpho-sdk/src/helpers/validate.test.ts | 39 +++---- packages/morpho-sdk/src/helpers/validate.ts | 38 +++---- packages/morpho-sdk/src/types/AGENTS.md | 6 +- packages/morpho-sdk/src/types/error.ts | 106 ++++++++---------- .../morpho-sdk/src/types/sharedLiquidity.ts | 11 +- packages/morpho-ts/src/addresses.test.ts | 26 +++++ packages/morpho-ts/src/addresses.ts | 15 +++ .../wdk-protocol-lending-morpho-evm/README.md | 2 - .../src/morpho-protocol-evm.test.ts | 4 - 39 files changed, 314 insertions(+), 349 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 8c62c3532..0cee2e7c9 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -6,7 +6,7 @@ "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add the canonical `vaultV2BluePublicAllocatorAbi` to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while normalizing them before repeated use, add explicit-allocator deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. +Add the canonical `vaultV2BluePublicAllocatorAbi` and per-chain `bluePublicAllocator` deployments to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while normalizing them before repeated use, add chain-registry-backed deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index 738c2626a..f71e59c48 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -49,11 +49,11 @@ This TIB freezes that Vault V2 design. interest, adapter permissions, idle liquidity, and `uint128` bounds. - Reuse one combined `validateReallocations(...)` for the action-ready V1/V2 union. +- Resolve the single canonical BluePublicAllocator deployment from each + chain's address registry entry. ## Non-goals -- No BluePublicAllocator address registry entry. The allocator contract is an - explicit input to fetchers, state, and every returned call. - No curator-facing setters such as `setAbsoluteCap`, `setCanPullFromMarket`, or `setPenalty`. - No penalty-efficiency optimizer beyond an explicit maximum-penalty @@ -77,6 +77,7 @@ This TIB freezes that Vault V2 design. | V2 action input | `VaultV2BlueReallocation` | | V2 Bundler actions | `vaultV2BluePublicAllocatorReallocate`, `vaultV2BluePublicAllocatorAllocateFromIdle` | | V2 allocator ABI | `vaultV2BluePublicAllocatorAbi` | +| V2 allocator address | `ChainAddresses.bluePublicAllocator` | | Shared action union | `BlueReallocation` | | V2 options | `VaultV2BluePublicAllocatorOptions` | | V2 config | `VaultV2PublicAllocatorConfig`, `VaultV2MarketPublicAllocatorConfig` | @@ -99,8 +100,6 @@ export type BluePublicAllocatorSource = | { readonly type: "idle" }; export interface VaultV2BlueReallocation { - readonly allocator: Address; - readonly type: "bluePublicAllocator"; readonly vault: Address; readonly from: BluePublicAllocatorSource; readonly to: { readonly adapter: Address }; @@ -191,7 +190,6 @@ State is therefore keyed by `(vault, derivedId)`, not by a projected ```ts export interface InputVaultV2ReallocationData { readonly chainId: number; - readonly allocator: Address; readonly markets?: Readonly>; readonly vaults?: Readonly>; readonly allocations?: Readonly< @@ -213,7 +211,7 @@ export interface InputVaultV2ReallocationData { ``` The readonly config projections are self-identifying. Vault-wide state carries -`allocator`, `vault`, `canPullFromIdle`, and `penalty`. Adapter activation input +`vault`, `canPullFromIdle`, and `penalty`. Adapter activation input accepts arrays, readonly arrays, sets, or any other iterable, and is normalized as a vault-keyed set of adapter addresses. Market state carries `adapter`, `marketParamsId`, `absoluteCap`, and `canPullFromMarket`. @@ -221,19 +219,19 @@ as a vault-keyed set of adapter addresses. Market state carries `adapter`, ## Fetching `vaultV2BluePublicAllocatorAbi` includes the three allocator mapping reads and -`vaultData`. Fetchers always take the allocator address explicitly: +`vaultData`. Fetchers resolve `bluePublicAllocator` from `parameters.chainId`, +defaulting to the client chain id: -- `fetchVaultV2PublicAllocatorConfig(allocator, vault, client, parameters?)`; -- `fetchVaultV2MarketPublicAllocatorConfig(allocator, vault, adapter, +- `fetchVaultV2PublicAllocatorConfig(vault, client, parameters?)`; +- `fetchVaultV2MarketPublicAllocatorConfig(vault, adapter, marketParamsId, client, parameters?)`; -- `fetchVaultV2PublicAllocatorData(allocator, hydratedVault, client, +- `fetchVaultV2PublicAllocatorData(hydratedVault, client, parameters?)`. The batched fetcher derives every supported adapter, adapter/market request, and unique allocation ID from the hydrated `AccrualVaultV2`. It returns active adapters as a `Set
`, defaults to one deployless read, and falls back to -equivalent direct reads unless deployless mode is forced. No chain-address -lookup occurs. +equivalent direct reads unless deployless mode is forced. Only `AccrualVaultV2MorphoMarketV1AdapterV2` adapters participate. Other adapter classes are ignored even if an allocator reports them as active. @@ -367,8 +365,8 @@ the final capped `assets` amount. The planner throws: The existing `validateReallocations` validates the combined `BlueReallocation` union. V2 penalties must be between zero and WAD (and therefore fit the -contract's `uint64`), and every call for the same explicit allocator-vault pair -must use one consistent penalty. A V2 market source is rejected whenever its +contract's `uint64`), and every call for the same vault must use one consistent +penalty. A V2 market source is rejected whenever its Blue market matches the target, regardless of adapter. `VaultV2ReallocationData` exposes: @@ -392,14 +390,14 @@ is already accepted by the branch's Blue action builders. ### Add a V2 validator -Rejected because the action layer already consumes a discriminated V1/V2 -union. One validator is the single source of truth for amount bounds, source -tags, and target-pair exclusion. +Rejected because the action layer already consumes one structural V1/V2 union: +V1 has `withdrawals`, while V2 has `from`. One validator is the single source +of truth for amount bounds, source tags, and target-pair exclusion. -### Register a canonical allocator address +### Accept allocator addresses from callers -Rejected because there is no canonical per-chain deployment to register. -Identity is explicit in config data and action inputs. +Rejected because BluePublicAllocator has one canonical deployment per chain. +The chain registry is the single source of truth for fetches and transactions. ### Copy V1's deprecated utilization options @@ -455,6 +453,7 @@ source and target thresholds plus an internal 100% fallback. - [`BluePublicAllocator.sol`](https://github.com/morpho-org/vault-v2/blob/a54e96c4cda93d5231df513f8e378653999c0e38/src/periphery/blue-public-allocator/BluePublicAllocator.sol) - [`VaultV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/VaultV2.sol) - [`MorphoMarketV1AdapterV2.sol`](https://github.com/morpho-org/vault-v2/blob/main/src/adapters/MorphoMarketV1AdapterV2.sol) +- [BluePublicAllocator deployments](https://github.com/morpho-org/deployments/pull/233) - [TIB-2026-06-16 shared-liquidity target-utilization metric](./TIB-2026-06-16-shared-liquidity-target-utilization-metric.md) - `packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts` - `packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts` diff --git a/packages/blue-sdk-viem/AGENTS.md b/packages/blue-sdk-viem/AGENTS.md index 80bcea623..7ec59df63 100644 --- a/packages/blue-sdk-viem/AGENTS.md +++ b/packages/blue-sdk-viem/AGENTS.md @@ -12,7 +12,7 @@ - Normalize unsafe user addresses with `safeGetAddress`, not lowercasing alone. - Typed-data helpers return `TypedDataDefinition`, e.g. `getPermitTypedData(...)`. - Re-export ABI literals from `@morpho-org/morpho-ts` when they exist there; keep local ABI declarations only for Blue-specific viem surfaces absent from `morpho-ts`. -- Vault V2 BluePublicAllocator fetchers always accept the allocator address explicitly; there is no chain-address registry entry. The hydrated-vault batch fetch derives supported adapter/market/allocation ids, returns active adapters as a set separate from market configs, defaults to one deployless read, and falls back to direct reads. +- Vault V2 BluePublicAllocator fetchers resolve the chain's single allocator from `bluePublicAllocator` in the address registry, using `parameters.chainId` or the client chain id. The hydrated-vault batch fetch derives supported adapter/market/allocation ids, returns active adapters as a set separate from market configs, defaults to one deployless read, and falls back to direct reads. ## Continuous Improvement diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts index b34ca2731..7a50911b1 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts @@ -1,4 +1,8 @@ -import { AccrualVaultV2MorphoMarketV1AdapterV2 } from "@morpho-org/blue-sdk"; +import { + AccrualVaultV2MorphoMarketV1AdapterV2, + ChainId, + getChainAddress, +} from "@morpho-org/blue-sdk"; import { assert, describe, expect } from "vitest"; import { abi as fixtureAbi, @@ -29,9 +33,16 @@ describe("Vault V2 public allocator fetchers on fork", () => { abi: fixtureAbi, bytecode: fixtureCode, }); - const { contractAddress: allocator } = + const { contractAddress: fixture } = await client.waitForTransactionReceipt({ hash: deploymentHash }); - assert(allocator != null); + assert(fixture != null); + const fixtureBytecode = await client.getBytecode({ address: fixture }); + assert(fixtureBytecode != null); + const allocator = getChainAddress( + ChainId.EthMainnet, + "bluePublicAllocator", + ); + await client.setCode({ address: allocator, bytecode: fixtureBytecode }); const forkMarketParamsId = forkAdapter.ids(forkMarket.params)[2]; await client.writeContract({ @@ -60,17 +71,16 @@ describe("Vault V2 public allocator fetchers on fork", () => { }); const [deployless, direct] = await Promise.all([ - fetchVaultV2PublicAllocatorData(allocator, forkVault, client, { + fetchVaultV2PublicAllocatorData(forkVault, client, { deployless: "force", }), - fetchVaultV2PublicAllocatorData(allocator, forkVault, client, { + fetchVaultV2PublicAllocatorData(forkVault, client, { deployless: false, }), ]); expect(deployless).toStrictEqual(direct); expect(deployless.publicAllocatorConfig).toStrictEqual({ - allocator, vault: forkVault.address, canPullFromIdle: true, penalty: 12n, @@ -81,7 +91,6 @@ describe("Vault V2 public allocator fetchers on fork", () => { expect( deployless.marketPublicAllocatorConfigs[forkMarketParamsId], ).toStrictEqual({ - allocator, vault: forkVault.address, adapter: forkAdapter.address, marketParamsId: forkMarketParamsId, diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts index d6bcd4063..f37e606ab 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts @@ -1,6 +1,7 @@ import { AccrualVaultV2, AccrualVaultV2MorphoMarketV1AdapterV2, + getChainAddress, Market, MarketParams, MathLib, @@ -22,7 +23,7 @@ import { fetchVaultV2PublicAllocatorData, } from "./VaultV2PublicAllocatorConfig.js"; -const ALLOCATOR: Address = "0x0000000000000000000000000000000000000001"; +const ALLOCATOR = getChainAddress(mainnet.id, "bluePublicAllocator"); const VAULT: Address = "0x0000000000000000000000000000000000000002"; const ADAPTER: Address = "0x0000000000000000000000000000000000000003"; const ASSET: Address = "0x0000000000000000000000000000000000000004"; @@ -82,7 +83,6 @@ const marketParamsId = ids[2]; const expected = { publicAllocatorConfig: { - allocator: ALLOCATOR, vault: VAULT, canPullFromIdle: true, penalty: 12n, @@ -90,7 +90,6 @@ const expected = { activeAdapters: new Set([ADAPTER]), marketPublicAllocatorConfigs: { [marketParamsId]: { - allocator: ALLOCATOR, vault: VAULT, adapter: ADAPTER, marketParamsId, @@ -160,16 +159,15 @@ const mockDirectReads = ( }; describe("Vault V2 public allocator fetchers", () => { - test("default: leaf fetchers preserve the explicit allocator identity", async () => { + test("default: leaf fetchers use the chain allocator", async () => { const handle = createMockClient(mainnet); mockDirectReads(handle); await expect( - fetchVaultV2PublicAllocatorConfig(ALLOCATOR, VAULT, handle.client), + fetchVaultV2PublicAllocatorConfig(VAULT, handle.client), ).resolves.toStrictEqual(expected.publicAllocatorConfig); await expect( fetchVaultV2MarketPublicAllocatorConfig( - ALLOCATOR, VAULT, ADAPTER, marketParamsId, @@ -203,7 +201,7 @@ describe("Vault V2 public allocator fetchers", () => { }); await expect( - fetchVaultV2PublicAllocatorData(ALLOCATOR, vault, handle.client), + fetchVaultV2PublicAllocatorData(vault, handle.client), ).resolves.toStrictEqual(expected); }); @@ -213,7 +211,7 @@ describe("Vault V2 public allocator fetchers", () => { mockDirectReads(handle); await expect( - fetchVaultV2PublicAllocatorData(ALLOCATOR, vault, handle.client), + fetchVaultV2PublicAllocatorData(vault, handle.client), ).resolves.toStrictEqual(expected); }); @@ -222,11 +220,7 @@ describe("Vault V2 public allocator fetchers", () => { mockDeploylessReads(handle, [new Error("deployless unavailable")]); mockDirectReads(handle, false); - const result = await fetchVaultV2PublicAllocatorData( - ALLOCATOR, - vault, - handle.client, - ); + const result = await fetchVaultV2PublicAllocatorData(vault, handle.client); expect(result.activeAdapters).toStrictEqual(new Set()); }); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts index 387273f0b..f5c7ca456 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts @@ -1,12 +1,13 @@ import { type AccrualVaultV2, AccrualVaultV2MorphoMarketV1AdapterV2, + getChainAddress, type IVaultV2Allocation, type VaultV2MarketPublicAllocatorConfig, type VaultV2PublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import type { Address, Client, Hash } from "viem"; -import { readContract } from "viem/actions"; +import { getChainId, readContract } from "viem/actions"; import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; import { abi, @@ -20,14 +21,16 @@ import type { /** * Fetches a Vault V2's BluePublicAllocator-wide configuration. * - * @param allocator - Explicit BluePublicAllocator contract address. * @param vault - Vault V2 address. * @param client - Viem client used for the contract read. * @param parameters.account - Optional account passed to viem calls. * @param parameters.blockNumber - Optional block number for historical reads. * @param parameters.blockTag - Optional block tag for historical reads. * @param parameters.stateOverride - Optional viem state override. + * @param parameters.chainId - Optional chain id; defaults to `getChainId(client)`. * @returns The vault's idle-pull permission and WAD-scaled vault-asset penalty. + * @throws {UnknownAddressError} when the chain has no BluePublicAllocator deployment. + * @throws {UnsupportedChainIdError} when the chain is absent from the address registry. * @throws {viem.BaseError} when the contract read fails. * @example * ```ts @@ -38,20 +41,20 @@ import type { * * const client = createPublicClient({ chain: mainnet, transport: http() }); * export async function fetchAllocatorConfig( - * allocator: Address, * vault: Address, * ): Promise { - * return fetchVaultV2PublicAllocatorConfig(allocator, vault, client); + * return fetchVaultV2PublicAllocatorConfig(vault, client); * } * ``` */ -// biome-ignore lint/complexity/useMaxParams: identity fields mirror the allocator's mapping keys +// biome-ignore lint/complexity/useMaxParams: follows the package's address/client/options fetcher convention export async function fetchVaultV2PublicAllocatorConfig( - allocator: Address, vault: Address, client: Client, parameters: FetchParameters = {}, ): Promise { + const chainId = parameters.chainId ?? (await getChainId(client)); + const allocator = getChainAddress(chainId, "bluePublicAllocator"); const [canPullFromIdle, penalty] = await readContract(client, { ...parameters, address: allocator, @@ -61,7 +64,6 @@ export async function fetchVaultV2PublicAllocatorConfig( }); return { - allocator, vault, canPullFromIdle, penalty, @@ -71,7 +73,6 @@ export async function fetchVaultV2PublicAllocatorConfig( /** * Fetches BluePublicAllocator permission and cap state for one Vault V2 adapter-market pair. * - * @param allocator - Explicit BluePublicAllocator contract address. * @param vault - Vault V2 address. * @param adapter - MorphoMarketV1AdapterV2 address. * @param marketParamsId - Adapter-scoped market-parameters id. @@ -80,7 +81,10 @@ export async function fetchVaultV2PublicAllocatorConfig( * @param parameters.blockNumber - Optional block number for historical reads. * @param parameters.blockTag - Optional block tag for historical reads. * @param parameters.stateOverride - Optional viem state override. + * @param parameters.chainId - Optional chain id; defaults to `getChainId(client)`. * @returns The allocator cap and pull permission for the adapter-market pair. + * @throws {UnknownAddressError} when the chain has no BluePublicAllocator deployment. + * @throws {UnsupportedChainIdError} when the chain is absent from the address registry. * @throws {viem.BaseError} when one of the contract reads fails. * @example * ```ts @@ -91,13 +95,11 @@ export async function fetchVaultV2PublicAllocatorConfig( * * const client = createPublicClient({ chain: mainnet, transport: http() }); * export async function fetchMarketAllocatorConfig( - * allocator: Address, * vault: Address, * adapter: Address, * marketParamsId: Hash, * ): Promise { * return fetchVaultV2MarketPublicAllocatorConfig( - * allocator, * vault, * adapter, * marketParamsId, @@ -106,15 +108,16 @@ export async function fetchVaultV2PublicAllocatorConfig( * } * ``` */ -// biome-ignore lint/complexity/useMaxParams: identity fields mirror the allocator's mapping keys +// biome-ignore lint/complexity/useMaxParams: follows the package's vault/adapter/id/client/options fetcher convention export async function fetchVaultV2MarketPublicAllocatorConfig( - allocator: Address, vault: Address, adapter: Address, marketParamsId: Hash, client: Client, parameters: FetchParameters = {}, ): Promise { + const chainId = parameters.chainId ?? (await getChainId(client)); + const allocator = getChainAddress(chainId, "bluePublicAllocator"); const [absoluteCap, canPullFromMarket] = await Promise.all([ readContract(client, { ...parameters, @@ -133,7 +136,6 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( ]); return { - allocator, vault, adapter, marketParamsId, @@ -151,45 +153,43 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( * hydrated vault, uses one deployless `eth_call` by default, and falls back to * direct reads unless deployless mode is forced. * - * @param allocator - Explicit BluePublicAllocator contract address. * @param vault - Hydrated Vault V2 whose accrued adapters provide the candidate markets. * @param client - Viem client used for deployless or direct reads. * @param parameters.account - Optional account passed to viem calls. * @param parameters.blockNumber - Optional block number for historical reads. * @param parameters.blockTag - Optional block tag for historical reads. * @param parameters.stateOverride - Optional viem state override. + * @param parameters.chainId - Optional chain id; defaults to `getChainId(client)`. * @param parameters.deployless - Deployless mode; defaults to `true`, with direct-read fallback. * @returns Vault-wide config, active-adapter set, adapter-market configs keyed by `marketParamsId`, and allocations keyed by derived id. + * @throws {UnknownAddressError} when the chain has no BluePublicAllocator deployment. + * @throws {UnsupportedChainIdError} when the chain is absent from the address registry. * @throws {viem.BaseError} when deployless mode is forced and fails, or when a direct contract read fails. * @example * ```ts * import type { AccrualVaultV2 } from "@morpho-org/blue-sdk"; * import { fetchVaultV2PublicAllocatorData } from "@morpho-org/blue-sdk-viem"; - * import { type Address, createPublicClient, http } from "viem"; + * import { createPublicClient, http } from "viem"; * import { mainnet } from "viem/chains"; * * const client = createPublicClient({ chain: mainnet, transport: http() }); * export async function fetchAllocatorData( - * allocator: Address, * vault: AccrualVaultV2, * ) { - * const data = await fetchVaultV2PublicAllocatorData( - * allocator, - * vault, - * client, - * ); + * const data = await fetchVaultV2PublicAllocatorData(vault, client); * // data contains publicAllocatorConfig, activeAdapters, marketPublicAllocatorConfigs, and allocations. * return data; * } * ``` */ -// biome-ignore lint/complexity/useMaxParams: follows the package's address/entity/client/options fetcher convention +// biome-ignore lint/complexity/useMaxParams: follows the package's entity/client/options fetcher convention export async function fetchVaultV2PublicAllocatorData( - allocator: Address, vault: AccrualVaultV2, client: Client, { deployless = true, ...parameters }: DeploylessFetchParameters = {}, ) { + const chainId = parameters.chainId ?? (await getChainId(client)); + const allocator = getChainAddress(chainId, "bluePublicAllocator"); const marketRequests: { readonly adapter: Address; readonly marketParamsId: Hash; @@ -236,7 +236,6 @@ export async function fetchVaultV2PublicAllocatorData( > = {}; for (const config of result.marketConfigs) { marketPublicAllocatorConfigs[config.marketParamsId] = { - allocator, vault: vault.address, ...config, }; @@ -249,7 +248,6 @@ export async function fetchVaultV2PublicAllocatorData( return { publicAllocatorConfig: { - allocator, vault: vault.address, canPullFromIdle: result.canPullFromIdle, penalty: result.penalty, @@ -272,12 +270,10 @@ export async function fetchVaultV2PublicAllocatorData( marketConfigs, allocationValues, ] = await Promise.all([ - fetchVaultV2PublicAllocatorConfig( - allocator, - vault.address, - client, - parameters, - ), + fetchVaultV2PublicAllocatorConfig(vault.address, client, { + ...parameters, + chainId, + }), Promise.all( adapterList.map((adapter) => readContract(client, { @@ -292,12 +288,11 @@ export async function fetchVaultV2PublicAllocatorData( Promise.all( marketRequests.map(({ adapter, marketParamsId }) => fetchVaultV2MarketPublicAllocatorConfig( - allocator, vault.address, adapter, marketParamsId, client, - parameters, + { ...parameters, chainId }, ), ), ), diff --git a/packages/blue-sdk/AGENTS.md b/packages/blue-sdk/AGENTS.md index 12b92fab6..01254d556 100644 --- a/packages/blue-sdk/AGENTS.md +++ b/packages/blue-sdk/AGENTS.md @@ -11,7 +11,7 @@ - Protocol entity folders (`market/`, `vault/`, `token/`, `position/`, `holding/`, `user/`) own their classes and folder barrels. - Getters may throw typed `Unknown*Error`; nullable lookup paths should use `_try` or `tryGet*`-style helpers deliberately. - Vault V2 absolute/relative allocation-cap math is canonical in `VaultV2Utils.allocationHeadroom`; consumers such as `AccrualVaultV2.maxDeposit` and shared-liquidity simulation delegate to it. -- Vault V2 BluePublicAllocator config interfaces are readonly identity-bearing projections: they include the explicit allocator and vault, plus the adapter and derived market-params id for market-scoped state. Adapter activation is normalized separately as a vault-keyed set of adapter addresses. +- Vault V2 BluePublicAllocator config interfaces are readonly vault-bearing projections; the allocator is canonical per chain and comes from the address registry. Market-scoped state also carries the adapter and derived market-params id. Adapter activation is normalized separately as a vault-keyed set of adapter addresses. - `marketParamsAbi` is owned by `@morpho-org/morpho-ts/abis` and re-exported from `MarketParams.ts` for backward compatibility; do not define a second copy in this package. ## Continuous Improvement diff --git a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts index c8d2181a0..e2b13a513 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts @@ -2,8 +2,6 @@ import type { Address, Hash } from "../../types.js"; /** Public allocator configuration for one Vault V2. */ export interface VaultV2PublicAllocatorConfig { - /** BluePublicAllocator contract from which the configuration was read. */ - readonly allocator: Address; /** Configured Vault V2 address. */ readonly vault: Address; /** Whether the allocator may pull the vault's idle assets into a Blue market. */ @@ -14,8 +12,6 @@ export interface VaultV2PublicAllocatorConfig { /** Public allocator permission and cap for one Vault V2 adapter-market pair. */ export interface VaultV2MarketPublicAllocatorConfig { - /** BluePublicAllocator contract from which the configuration was read. */ - readonly allocator: Address; /** Configured Vault V2 address. */ readonly vault: Address; /** Vault V2 MorphoMarketV1AdapterV2 address. */ diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index 5f024d54d..92671f21d 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -27,7 +27,7 @@ Protocol terms used across this package's docs and JSDoc: - **bundler3** — the bundler entry point; receives a sequence of adapter actions in one transaction. - **GeneralAdapter1** — the bundler-side adapter that holds approvals/auth and executes Morpho calls on the user's behalf. Required as the spender for ERC-20 approvals on every bundled path; required as authorized operator on Morpho for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, and `withdraw` (the supplier-side path). - **PublicAllocator V1** — MetaMorpho allocator that moves liquidity from one or more sorted source markets into a target via `reallocateTo(...)`; each call pays one `fee`. -- **BluePublicAllocator** — Vault V2 allocator that moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies the allocator and adapter addresses explicitly because no canonical deployment is registered. Each call passes the vault's configured WAD-scaled `uint64 penalty`; the allocator pulls `ceil(assets × penalty / WAD)` of the target loan token from Bundler3 and donates it directly to the vault. Its canonical ABI export is `vaultV2BluePublicAllocatorAbi`. +- **BluePublicAllocator** — the single canonical Vault V2 allocator registered per chain, which moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies adapter addresses; the SDK resolves the allocator from the chain registry. Each call passes the vault's configured WAD-scaled `uint64 penalty`; the allocator pulls `ceil(assets × penalty / WAD)` of the target loan token from Bundler3 and donates it directly to the vault. Its canonical ABI export is `vaultV2BluePublicAllocatorAbi`. - **VaultExitBundlesV1** — standalone periphery for exiting an illiquid VaultV1 or single-adapter VaultV2 into idle underlying assets and/or Morpho Blue supply positions. - **Shared-liquidity naming** — `VaultV1ReallocationData`, `InputVaultV1ReallocationData`, `computeVaultV1Reallocations`, and `VaultV1BlueReallocation` are canonical for PublicAllocator V1. Their published predecessors (`ReallocationData`, `InputReallocationData`, `computeReallocations`, `getMarketPublicReallocations`, and `VaultReallocation`) remain deprecated aliases. `VaultV2ReallocationData.computeVaultV2Reallocations` and `computeVaultV2Reallocations` accept `VaultV2BluePublicAllocatorOptions` and produce flat, action-ready `VaultV2BlueReallocation` calls. @@ -42,7 +42,7 @@ The action verbs an integrator sees in the bundle (`BundlerAction.encode...`): - **`nativeTransfer` + `wrapNative`** — pair that converts an attached native amount (`tx.value`) into the chain's wNative for a deposit/supply path. - **`forceDeallocate`** — VaultV2 multicall entry that pulls liquidity out of a specific adapter before withdraw/redeem. - **`reallocateTo`** — PublicAllocator V1 call that shifts liquidity from sorted source markets into the target market. -- **`vaultV2BluePublicAllocatorReallocate` / `vaultV2BluePublicAllocatorAllocateFromIdle`** — BluePublicAllocator calls that move one market source or vault idle liquidity into the enclosing Blue action's target market. Both target an explicit allocator address, approve the exact loan-token penalty from Bundler3, and carry the configured penalty rate in calldata. +- **`vaultV2BluePublicAllocatorReallocate` / `vaultV2BluePublicAllocatorAllocateFromIdle`** — BluePublicAllocator calls that move one market source or vault idle liquidity into the enclosing Blue action's target market. Both target the chain's registered allocator, approve the exact loan-token penalty from Bundler3, and carry the configured penalty rate in calldata. ### Constants and conventions diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index d186b511b..aa31e8cf3 100644 --- a/packages/morpho-sdk/src/actions/AGENTS.md +++ b/packages/morpho-sdk/src/actions/AGENTS.md @@ -24,7 +24,7 @@ Only valid for assets/collateral configured as wNative. When `nativeAmount > 0`: ## Shared liquidity / reallocations (canonical statement) -`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). Legacy-untagged or explicitly `publicAllocatorV1` `VaultV1BlueReallocation` entries preserve PublicAllocator V1: each becomes `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. Tagged `VaultV2BlueReallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies the allocator/adapters explicitly, and each call passes the vault's configured WAD-scaled `penalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. High-level builders pull the aggregate V2 penalty in the target loan token through GeneralAdapter1, then each low-level allocator action approves and spends its independently rounded `ceil(assets × penalty / WAD)` amount from Bundler3. Only V1 fees contribute to `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects unknown top-level variants; malformed allocator, vault, and adapter addresses; absent, incomplete, or unknown BluePublicAllocator sources; penalties above WAD; and inconsistent penalties for the same allocator-vault pair. +`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). `VaultV1BlueReallocation` entries are identified by `withdrawals` and become `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. `VaultV2BlueReallocation` entries are identified by `from` and map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies adapters, the chain registry supplies the allocator, and each call passes the vault's configured WAD-scaled `penalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. High-level builders pull the aggregate V2 penalty in the target loan token through GeneralAdapter1, then each low-level allocator action approves and spends its independently rounded `ceil(assets × penalty / WAD)` amount from Bundler3. Only V1 fees contribute to `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects entries matching both or neither shape; malformed vault and adapter addresses; absent, incomplete, or unknown BluePublicAllocator sources; penalties above WAD; and inconsistent penalties for the same vault. ## Discriminated unions diff --git a/packages/morpho-sdk/src/actions/blue/AGENTS.md b/packages/morpho-sdk/src/actions/blue/AGENTS.md index 0d73a1ef0..9101d708a 100644 --- a/packages/morpho-sdk/src/actions/blue/AGENTS.md +++ b/packages/morpho-sdk/src/actions/blue/AGENTS.md @@ -39,7 +39,7 @@ ERC-20 approval spender is **GeneralAdapter1** for any bundled path — never th | `withdraw` (with reallocations) | `[V2 penalty transfer?] → [allocator reallocation × N] → morphoWithdraw` | An allocator reallocation is PublicAllocator V1 `reallocateTo` or BluePublicAllocator -`reallocate`/`allocateFromIdle` according to the `BlueReallocation` discriminator. One bundle may +`reallocate`/`allocateFromIdle` according to the `BlueReallocation` shape. One bundle may mix both allocator contracts. For non-zero V2 penalties, the builder adds one aggregate loan-token `erc20TransferFrom` into Bundler3 and each allocator action expands to an exact token approval plus the nonpayable allocator call. `BundlerAction.encodeBundle` derives `tx.value` only from native diff --git a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts index d92dc74af..c13ede105 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -14,7 +14,7 @@ import { } from "../../types/index.js"; import { blueBorrow } from "./borrow.js"; -const allocator = "0x0000000000000000000000000000000000000011"; +const allocator = getChainAddresses(ChainId.EthMainnet).bluePublicAllocator; const vaultV1 = "0x0000000000000000000000000000000000000012"; const sourceAdapter = "0x0000000000000000000000000000000000000013"; const targetAdapter = "0x0000000000000000000000000000000000000014"; @@ -44,14 +44,11 @@ describe("blueBorrow Blue Public Allocator", () => { } = getChainAddresses(ChainId.EthMainnet); const reallocations: readonly BlueReallocation[] = [ { - type: "publicAllocatorV1", vault: vaultV1, fee: 2n, withdrawals: [{ marketParams: sourceMarket, amount: 1n }], }, { - type: "bluePublicAllocator", - allocator, vault: vaultV2, from: { type: "market", @@ -63,8 +60,6 @@ describe("blueBorrow Blue Public Allocator", () => { penalty: 5n, }, { - type: "bluePublicAllocator", - allocator, vault: vaultV2, from: { type: "idle" }, to: { adapter: targetAdapter }, @@ -159,8 +154,6 @@ describe("blueBorrow Blue Public Allocator", () => { receiver, reallocations: [ { - type: "bluePublicAllocator", - allocator, vault: vaultV2, from: { type: "market", @@ -172,8 +165,6 @@ describe("blueBorrow Blue Public Allocator", () => { penalty: 5n, }, { - type: "bluePublicAllocator", - allocator, vault: vaultV2, from: { type: "idle" }, to: { adapter: targetAdapter }, diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index 9ee08753f..3442fcd44 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -67,10 +67,10 @@ export interface BlueBorrowParams { * @throws {NonPositiveInputError} when `amount <= 0n` or any reallocation withdrawal amount * is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. * @throws {NegativeInputError} when `minSharePrice < 0n`, a V1 fee, or a V2 penalty is negative. * @throws {EmptyReallocationWithdrawalsError} from `buildReallocationActions` when any * `reallocation.withdrawals` is empty. diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index f70e08384..495cc8844 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -25,10 +25,10 @@ import type { BlueReallocation } from "../../types/index.js"; * @throws {EmptyReallocationWithdrawalsError} when a PublicAllocator V1 reallocation has no withdrawals. * @throws {NonPositiveInputError} when a PublicAllocator V1 withdrawal or BluePublicAllocator asset amount is non-positive. * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a BluePublicAllocator identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a BluePublicAllocator vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when PublicAllocator V1 withdrawals are not strictly market-id sorted. * @internal @@ -86,12 +86,11 @@ export const buildReallocationActions = ({ } for (const reallocation of reallocationList) { - if (reallocation.type === "bluePublicAllocator") { + if ("from" in reallocation) { if (reallocation.from.type === "market") { actions.push({ type: "vaultV2BluePublicAllocatorReallocate", args: [ - reallocation.allocator, reallocation.vault, reallocation.from.adapter, reallocation.from.marketParams, @@ -106,7 +105,6 @@ export const buildReallocationActions = ({ actions.push({ type: "vaultV2BluePublicAllocatorAllocateFromIdle", args: [ - reallocation.allocator, reallocation.vault, reallocation.to.adapter, targetMarketParams, diff --git a/packages/morpho-sdk/src/actions/blue/refinance.test.ts b/packages/morpho-sdk/src/actions/blue/refinance.test.ts index fb54f9b50..e9d2a9805 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.test.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.test.ts @@ -394,7 +394,7 @@ describe("blueRefinance", () => { }); const VAULT: Address = "0xBEEf5aFE88eF73337e5070aB2855d37dBF5493A4"; const REALLOC_FEE = parseUnits("0.01", 18); - const V2_ALLOCATOR: Address = "0x0000000000000000000000000000000000000011"; + const V2_ALLOCATOR = getChainAddresses(mainnet.id).bluePublicAllocator!; const V2_VAULT: Address = "0x0000000000000000000000000000000000000012"; const SOURCE_ADAPTER: Address = "0x0000000000000000000000000000000000000013"; @@ -487,8 +487,6 @@ describe("blueRefinance", () => { } = getChainAddresses(mainnet.id); const targetReallocations: readonly BlueReallocation[] = [ { - type: "bluePublicAllocator", - allocator: V2_ALLOCATOR, vault: V2_VAULT, from: { type: "market", @@ -500,8 +498,6 @@ describe("blueRefinance", () => { penalty: 500_000_000_000_000_000n, }, { - type: "bluePublicAllocator", - allocator: V2_ALLOCATOR, vault: V2_VAULT, from: { type: "idle" }, to: { adapter: TARGET_ADAPTER }, diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index 84607c178..b87a82fed 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -113,10 +113,10 @@ export interface BlueRefinanceParams { * @throws {NonPositiveInputError} when `collateralAmount <= 0n`, a repay leg has a non-positive * `maxRepaySharePrice`, or any reallocation withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. * @throws {NegativeInputError} when `borrowAssets`, `borrowShares`, `minBorrowSharePrice`, * `maxRepaySharePrice`, a V1 fee, or a V2 penalty is negative. * @throws {RefinanceSameMarketError} when source and target market ids are equal. diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts index d2b311aec..fc77c4c99 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts @@ -228,8 +228,6 @@ describe("blueSupplyCollateralBorrow unit tests", () => { }); const reallocations: readonly BlueReallocation[] = [ { - type: "bluePublicAllocator", - allocator: WethUsdsBlue.irm, vault: WethUsdsBlue.oracle, from: { type: "idle" }, to: { adapter: WethUsdsBlue.collateralToken }, diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 14b8fc110..e45a7a63b 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -88,10 +88,10 @@ export interface BlueSupplyCollateralBorrowParams { * @throws {NonPositiveInputError} when `borrowAmount <= 0n`, both collateral amounts resolve to * zero, or any reallocation withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. * @throws {ChainWNativeMissingError} when `nativeAmount > 0n` but the chain has no configured wNative. * @throws {NativeAmountOnNonWNativeAssetError} when `nativeAmount > 0n` but the collateral * token is not the chain's wNative. diff --git a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts index 7cd8dff52..9a2968bb4 100644 --- a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts +++ b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts @@ -69,7 +69,12 @@ describe("Blue actions with Vault V2 reallocations", () => { client, }) => { const anvilClient = client as AnvilTestClient; - const { morpho, bundler3 } = getChainAddresses(base.id); + const { + morpho, + bundler3, + bluePublicAllocator: allocator, + } = getChainAddresses(base.id); + assert(allocator != null); const sourceAssets = parseUnits("20", 6); const idleAssets = parseUnits("10", 6); const sourceDeposit = parseUnits("100", 6); @@ -196,8 +201,11 @@ describe("Blue actions with Vault V2 reallocations", () => { const deploymentReceipt = await client.waitForTransactionReceipt({ hash: deploymentHash, }); - const allocator = deploymentReceipt.contractAddress; - assert(allocator != null); + const fixture = deploymentReceipt.contractAddress; + assert(fixture != null); + const fixtureBytecode = await client.getBytecode({ address: fixture }); + assert(fixtureBytecode != null); + await client.setCode({ address: allocator, bytecode: fixtureBytecode }); await submitAndAcceptVaultV2Call(anvilClient, { vault, @@ -252,8 +260,6 @@ describe("Blue actions with Vault V2 reallocations", () => { const reallocations: readonly VaultV2BlueReallocation[] = [ { - allocator, - type: "bluePublicAllocator", vault, from: { type: "market", @@ -265,8 +271,6 @@ describe("Blue actions with Vault V2 reallocations", () => { penalty, }, { - allocator, - type: "bluePublicAllocator", vault, from: { type: "idle" }, to: { adapter: targetAdapter }, @@ -372,7 +376,10 @@ describe("Blue actions with Vault V2 reallocations", () => { client, }) => { const anvilClient = client as AnvilTestClient; - const { morpho } = getChainAddresses(base.id); + const { morpho, bluePublicAllocator: allocator } = getChainAddresses( + base.id, + ); + assert(allocator != null); const depositAssets = parseUnits("100", 6); const seedAssets = parseUnits("1", 6); const postLossIdleAssets = parseUnits("89", 6); @@ -455,8 +462,11 @@ describe("Blue actions with Vault V2 reallocations", () => { const deploymentReceipt = await client.waitForTransactionReceipt({ hash: deploymentHash, }); - const allocator = deploymentReceipt.contractAddress; - assert(allocator != null); + const fixture = deploymentReceipt.contractAddress; + assert(fixture != null); + const fixtureBytecode = await client.getBytecode({ address: fixture }); + assert(fixtureBytecode != null); + await client.setCode({ address: allocator, bytecode: fixtureBytecode }); await submitAndAcceptVaultV2Call(anvilClient, { vault, @@ -521,7 +531,6 @@ describe("Blue actions with Vault V2 reallocations", () => { client.getBlock(), ]); const allocatorData = await fetchVaultV2PublicAllocatorData( - allocator, vaultData, client, ); @@ -537,7 +546,6 @@ describe("Blue actions with Vault V2 reallocations", () => { targetAllocation.allocation; const reallocationData = new VaultV2ReallocationData({ chainId: base.id, - allocator, markets: { [targetMarket.id]: targetMarketData }, vaults: { [vault]: vaultData }, allocations: { [vault]: allocatorData.allocations }, diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts index cdb7c061b..20df3d3b3 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts @@ -14,7 +14,7 @@ import { import type { BlueReallocation } from "../../types/index.js"; import { blueWithdraw } from "./withdraw.js"; -const allocator: Address = "0x0000000000000000000000000000000000000011"; +const allocator = getChainAddresses(mainnet.id).bluePublicAllocator!; const vault: Address = "0x0000000000000000000000000000000000000012"; const sourceAdapter: Address = "0x0000000000000000000000000000000000000013"; const targetAdapter: Address = "0x0000000000000000000000000000000000000014"; @@ -27,8 +27,6 @@ describe("blueWithdraw Blue Public Allocator", () => { } = getChainAddresses(mainnet.id); const reallocations: readonly BlueReallocation[] = [ { - type: "bluePublicAllocator", - allocator, vault, from: { type: "market", @@ -40,8 +38,6 @@ describe("blueWithdraw Blue Public Allocator", () => { penalty: 500_000_000_000_000_000n, }, { - type: "bluePublicAllocator", - allocator, vault, from: { type: "idle" }, to: { adapter: targetAdapter }, diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index affd16487..147cecd71 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -86,10 +86,10 @@ export interface BlueWithdrawParams { * @throws {NonPositiveInputError} when both `assets` and `shares` are zero or any reallocation * withdrawal amount is non-positive. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. * @throws {MutuallyExclusiveWithdrawAmountsError} when both `assets` and `shares` are non-zero. * @throws {EmptyReallocationWithdrawalsError} when any reallocation has no withdrawals. * @throws {ReallocationWithdrawalOnTargetMarketError} when a reallocation withdrawal references diff --git a/packages/morpho-sdk/src/bundler/actions.test.ts b/packages/morpho-sdk/src/bundler/actions.test.ts index 1445c6666..c1e6c0ebb 100644 --- a/packages/morpho-sdk/src/bundler/actions.test.ts +++ b/packages/morpho-sdk/src/bundler/actions.test.ts @@ -39,6 +39,7 @@ describe("BundlerAction", () => { morpho, permit2, publicAllocator, + bluePublicAllocator: allocator, bundler3: { bundler3, generalAdapter1 }, } = getChainAddresses(chainId); @@ -48,7 +49,6 @@ describe("BundlerAction", () => { const adapter = "0x0000000000000000000000000000000000000004"; const erc4626 = "0x0000000000000000000000000000000000000005"; const vault = "0x0000000000000000000000000000000000000006"; - const allocator = "0x0000000000000000000000000000000000000011"; const deallocateAdapter = "0x0000000000000000000000000000000000000012"; const allocateAdapter = "0x0000000000000000000000000000000000000013"; const loanToken = "0x0000000000000000000000000000000000000007"; @@ -353,7 +353,6 @@ describe("BundlerAction", () => { ), fc .tuple( - addressArbitrary, addressArbitrary, addressArbitrary, marketArbitrary, @@ -372,7 +371,6 @@ describe("BundlerAction", () => { ), fc .tuple( - addressArbitrary, addressArbitrary, addressArbitrary, marketArbitrary, @@ -620,7 +618,6 @@ describe("BundlerAction", () => { { type: "vaultV2BluePublicAllocatorReallocate", args: [ - allocator, vault, deallocateAdapter, market, @@ -633,7 +630,7 @@ describe("BundlerAction", () => { }, { type: "vaultV2BluePublicAllocatorAllocateFromIdle", - args: [allocator, vault, allocateAdapter, market, 3n, 4n, false], + args: [vault, allocateAdapter, market, 3n, 4n, false], }, ]); @@ -1056,7 +1053,6 @@ describe("BundlerAction", () => { { type: "vaultV2BluePublicAllocatorReallocate", args: [ - allocator, vault, deallocateAdapter, market, @@ -1068,7 +1064,7 @@ describe("BundlerAction", () => { ], }, BundlerAction.vaultV2BluePublicAllocatorReallocate( - allocator, + chainId, vault, deallocateAdapter, market, @@ -1083,10 +1079,10 @@ describe("BundlerAction", () => { "vaultV2BluePublicAllocatorAllocateFromIdle", { type: "vaultV2BluePublicAllocatorAllocateFromIdle", - args: [allocator, vault, allocateAdapter, market, 22n, 23n, false], + args: [vault, allocateAdapter, market, 22n, 23n, false], }, BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( - allocator, + chainId, vault, allocateAdapter, market, @@ -1571,7 +1567,7 @@ describe("BundlerAction", () => { test("vaultV2BluePublicAllocatorReallocate", () => { const penalty = 1_000_000_000_000_000n; const [approval, call] = BundlerAction.vaultV2BluePublicAllocatorReallocate( - allocator, + chainId, vault, deallocateAdapter, market, @@ -1616,7 +1612,7 @@ describe("BundlerAction", () => { test("vaultV2BluePublicAllocatorReallocate with zero penalty", () => { const call = onlyCall( BundlerAction.vaultV2BluePublicAllocatorReallocate( - allocator, + chainId, vault, deallocateAdapter, market, @@ -1650,7 +1646,7 @@ describe("BundlerAction", () => { test("vaultV2BluePublicAllocatorReallocate rejects a skippable penalty approval", () => { expect(() => BundlerAction.vaultV2BluePublicAllocatorReallocate( - allocator, + chainId, vault, deallocateAdapter, market, @@ -1667,7 +1663,7 @@ describe("BundlerAction", () => { const penalty = 1_000_000_000_000_000n; const [approval, call] = BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( - allocator, + chainId, vault, allocateAdapter, market, @@ -1708,7 +1704,7 @@ describe("BundlerAction", () => { test("vaultV2BluePublicAllocatorAllocateFromIdle with zero penalty", () => { const call = onlyCall( BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( - allocator, + chainId, vault, allocateAdapter, market, @@ -1732,7 +1728,7 @@ describe("BundlerAction", () => { test("vaultV2BluePublicAllocatorAllocateFromIdle rejects a skippable penalty approval", () => { expect(() => BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( - allocator, + chainId, vault, allocateAdapter, market, @@ -1793,5 +1789,29 @@ describe("BundlerAction", () => { market, ), ).toThrow(BundlerErrors.UnexpectedAction); + + expect(() => + BundlerAction.vaultV2BluePublicAllocatorReallocate( + ChainId.FraxtalMainnet, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 1n, + 0n, + ), + ).toThrow(BundlerErrors.UnexpectedAction); + + expect(() => + BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + ChainId.FraxtalMainnet, + vault, + allocateAdapter, + market, + 1n, + 0n, + ), + ).toThrow(BundlerErrors.UnexpectedAction); }); }); diff --git a/packages/morpho-sdk/src/bundler/actions.ts b/packages/morpho-sdk/src/bundler/actions.ts index 9801eeff7..0f206d52c 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -346,10 +346,14 @@ export namespace BundlerAction { return BundlerAction.publicAllocatorReallocateTo(chainId, ...args); } case "vaultV2BluePublicAllocatorReallocate": { - return BundlerAction.vaultV2BluePublicAllocatorReallocate(...args); + return BundlerAction.vaultV2BluePublicAllocatorReallocate( + chainId, + ...args, + ); } case "vaultV2BluePublicAllocatorAllocateFromIdle": { return BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + chainId, ...args, ); } @@ -1458,7 +1462,7 @@ export namespace BundlerAction { * @remarks Bundler3 must already hold the computed penalty assets. The * high-level Blue builders add the corresponding GeneralAdapter1 transfer. * - * @param allocator - Explicit Blue Public Allocator contract address. + * @param chainId - Chain whose canonical Blue Public Allocator is called. * @param vault - Vault whose liquidity is reallocated. * @param deallocateAdapter - Vault V2 adapter supplying the source market. * @param deallocateMarket - Source Morpho Blue market parameters. @@ -1468,18 +1472,17 @@ export namespace BundlerAction { * @param penalty - Vault-configured proportional penalty, scaled by WAD. * @param skipRevert - Whether Bundler3 should tolerate a revert. * @returns An exact token approval when needed, followed by the allocator call. + * @throws {BundlerErrors.UnexpectedAction} when the chain has no Blue Public Allocator deployment. * @throws {BundlerErrors.SkippableAllocatorPenalty} when `skipRevert` is true and a token approval is required. * @example * ```ts - * import type { InputMarketParams } from "@morpho-org/blue-sdk"; + * import { ChainId, type InputMarketParams } from "@morpho-org/blue-sdk"; * import { * BundlerAction, * type BundlerCall, * } from "@morpho-org/morpho-sdk/bundler"; * import type { Address } from "viem"; * - * const allocatorFixture = - * "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" satisfies Address; * const keyrockUsdcVault = * "0x04422053aDDbc9bB2759b248B574e3FCA76Bc145" satisfies Address; * const sourceAdapterFixture = @@ -1504,7 +1507,7 @@ export namespace BundlerAction { * } satisfies InputMarketParams; * * const calls: BundlerCall[] = BundlerAction.vaultV2BluePublicAllocatorReallocate( - * allocatorFixture, + * ChainId.EthMainnet, * keyrockUsdcVault, * sourceAdapterFixture, * sourceMarket, @@ -1518,7 +1521,7 @@ export namespace BundlerAction { */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call export function vaultV2BluePublicAllocatorReallocate( - allocator: Address, + chainId: number, vault: Address, deallocateAdapter: Address, deallocateMarket: InputMarketParams, @@ -1528,6 +1531,13 @@ export namespace BundlerAction { penalty: bigint, skipRevert = false, ): BundlerCall[] { + const { bluePublicAllocator: allocator } = getChainAddresses(chainId); + if (allocator == null) { + throw new BundlerErrors.UnexpectedAction( + "vaultV2BluePublicAllocatorReallocate", + chainId, + ); + } const calls: BundlerCall[] = []; const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( assets, @@ -1580,7 +1590,7 @@ export namespace BundlerAction { * @remarks Bundler3 must already hold the computed penalty assets. The * high-level Blue builders add the corresponding GeneralAdapter1 transfer. * - * @param allocator - Explicit Blue Public Allocator contract address. + * @param chainId - Chain whose canonical Blue Public Allocator is called. * @param vault - Vault whose idle liquidity is allocated. * @param adapter - Vault V2 adapter supplying the target market. * @param market - Target Morpho Blue market parameters. @@ -1588,18 +1598,17 @@ export namespace BundlerAction { * @param penalty - Vault-configured proportional penalty, scaled by WAD. * @param skipRevert - Whether Bundler3 should tolerate a revert. * @returns An exact token approval when needed, followed by the allocator call. + * @throws {BundlerErrors.UnexpectedAction} when the chain has no Blue Public Allocator deployment. * @throws {BundlerErrors.SkippableAllocatorPenalty} when `skipRevert` is true and a token approval is required. * @example * ```ts - * import type { InputMarketParams } from "@morpho-org/blue-sdk"; + * import { ChainId, type InputMarketParams } from "@morpho-org/blue-sdk"; * import { * BundlerAction, * type BundlerCall, * } from "@morpho-org/morpho-sdk/bundler"; * import type { Address } from "viem"; * - * const allocatorFixture = - * "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" satisfies Address; * const keyrockUsdcVault = * "0x04422053aDDbc9bB2759b248B574e3FCA76Bc145" satisfies Address; * const targetAdapterFixture = @@ -1617,7 +1626,7 @@ export namespace BundlerAction { * } satisfies InputMarketParams; * * const calls: BundlerCall[] = BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( - * allocatorFixture, + * ChainId.EthMainnet, * keyrockUsdcVault, * targetAdapterFixture, * targetMarket, @@ -1629,7 +1638,7 @@ export namespace BundlerAction { */ // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call export function vaultV2BluePublicAllocatorAllocateFromIdle( - allocator: Address, + chainId: number, vault: Address, adapter: Address, market: InputMarketParams, @@ -1637,6 +1646,13 @@ export namespace BundlerAction { penalty: bigint, skipRevert = false, ): BundlerCall[] { + const { bluePublicAllocator: allocator } = getChainAddresses(chainId); + if (allocator == null) { + throw new BundlerErrors.UnexpectedAction( + "vaultV2BluePublicAllocatorAllocateFromIdle", + chainId, + ); + } const calls: BundlerCall[] = []; const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( assets, diff --git a/packages/morpho-sdk/src/bundler/types.ts b/packages/morpho-sdk/src/bundler/types.ts index d39a01098..10ca3438f 100644 --- a/packages/morpho-sdk/src/bundler/types.ts +++ b/packages/morpho-sdk/src/bundler/types.ts @@ -210,9 +210,8 @@ export interface ActionArgs { skipRevert?: boolean, ]; - /** Vault V2 Blue Public Allocator market-to-market reallocation with an explicit allocator address and WAD-scaled penalty. */ + /** Vault V2 Blue Public Allocator market-to-market reallocation with a WAD-scaled penalty. */ readonly vaultV2BluePublicAllocatorReallocate: [ - allocator: Address, vault: Address, deallocateAdapter: Address, deallocateMarket: InputMarketParams, @@ -223,9 +222,8 @@ export interface ActionArgs { skipRevert?: boolean, ]; - /** Vault V2 Blue Public Allocator idle-to-market allocation with an explicit allocator address and WAD-scaled penalty. */ + /** Vault V2 Blue Public Allocator idle-to-market allocation with a WAD-scaled penalty. */ readonly vaultV2BluePublicAllocatorAllocateFromIdle: [ - allocator: Address, vault: Address, adapter: Address, market: InputMarketParams, diff --git a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts index 78bffff87..f878cf1a7 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts @@ -81,8 +81,6 @@ describe("MorphoBlue BluePublicAllocator requirements", () => { positionData, reallocations: [ { - type: "bluePublicAllocator", - allocator: CbbtcUsdcBlue.irm, vault: CbbtcUsdcBlue.oracle, from: { type: "idle" }, to: { adapter: CbbtcUsdcBlue.collateralToken }, @@ -159,8 +157,6 @@ describe("MorphoBlue BluePublicAllocator requirements", () => { positionData, reallocations: [ { - type: "bluePublicAllocator", - allocator: CbbtcUsdcBlue.irm, vault: CbbtcUsdcBlue.oracle, from: { type: "idle" }, to: { adapter: CbbtcUsdcBlue.collateralToken }, @@ -243,8 +239,6 @@ describe("MorphoBlue BluePublicAllocator requirements", () => { positionData, reallocations: [ { - type: "bluePublicAllocator", - allocator: CbbtcUsdcBlue.irm, vault: CbbtcUsdcBlue.oracle, from: { type: "idle" }, to: { adapter: CbbtcUsdcBlue.collateralToken }, diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index d8564f619..7aa0133e6 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -202,10 +202,10 @@ export interface BlueActions { * @param params - Withdraw parameters including pre-fetched `positionData`. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. */ withdraw: ( params: { @@ -247,10 +247,10 @@ export interface BlueActions { * @param params - Borrow parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. */ borrow: (params: { userAddress: Address; @@ -400,10 +400,10 @@ export interface BlueActions { * @param params - Combined parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. */ supplyCollateralBorrow: ( params: { @@ -459,10 +459,10 @@ export interface BlueActions { * @param params.targetReallocations - Public Allocator V1 or V2 reallocations into the target market. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when V2 entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a V2 identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. */ refinance: (params: { userAddress: Address; diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index d05e4d798..2a1231774 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -27,7 +27,6 @@ import { } from "./vaultV2ReallocationData.js"; const TIMESTAMP = 1_700_000_000n; -const ALLOCATOR = "0x0000000000000000000000000000000000000001"; const VAULT = "0x0000000000000000000000000000000000000002"; const TARGET_ADAPTER = "0x0000000000000000000000000000000000000003"; const SOURCE_ADAPTER = "0x0000000000000000000000000000000000000004"; @@ -271,7 +270,6 @@ const makeFixture = ({ return { data: new VaultV2ReallocationData({ chainId: ChainId.EthMainnet, - allocator: ALLOCATOR, markets: { [targetMarket.id]: targetMarket, [sourceMarket.id]: sourceMarket, @@ -280,7 +278,6 @@ const makeFixture = ({ allocations: { [VAULT]: allocations }, publicAllocatorConfigs: { [VAULT]: { - allocator: ALLOCATOR, vault: VAULT, canPullFromIdle, penalty, @@ -293,7 +290,6 @@ const makeFixture = ({ marketPublicAllocatorConfigs: { [VAULT]: { [targetIds[2]]: { - allocator: ALLOCATOR, vault: VAULT, adapter: TARGET_ADAPTER, marketParamsId: targetIds[2], @@ -301,7 +297,6 @@ const makeFixture = ({ canPullFromMarket: false, }, [sourceIds[2]]: { - allocator: ALLOCATOR, vault: VAULT, adapter: sourceAdapterAddress, marketParamsId: sourceIds[2], @@ -329,8 +324,6 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { expect(result.reallocations).toStrictEqual([ { - allocator: ALLOCATOR, - type: "bluePublicAllocator", vault: VAULT, from: { type: "market", @@ -411,7 +404,6 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ); const sharedData = new VaultV2ReallocationData({ chainId: data.chainId, - allocator: data.allocator, markets: data.markets, vaults: { [VAULT]: firstVault, @@ -424,7 +416,6 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { publicAllocatorConfigs: { [VAULT]: data.publicAllocatorConfigs[VAULT], [SECOND_VAULT]: { - allocator: ALLOCATOR, vault: SECOND_VAULT, canPullFromIdle: true, penalty: 0n, @@ -438,7 +429,6 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { [VAULT]: data.marketPublicAllocatorConfigs[VAULT], [SECOND_VAULT]: { [secondTargetIds[2]]: { - allocator: ALLOCATOR, vault: SECOND_VAULT, adapter: SECOND_TARGET_ADAPTER, marketParamsId: secondTargetIds[2], @@ -565,7 +555,6 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ); const input = new VaultV2ReallocationData({ chainId: data.chainId, - allocator: data.allocator, markets: data.markets, vaults: { [VAULT]: inputVault }, allocations: data.allocations, diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index aee175a36..4b6499aa7 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -45,8 +45,6 @@ import { export interface InputVaultV2ReallocationData { /** Chain id associated with the fetched state. */ readonly chainId: number; - /** Explicit BluePublicAllocator contract used by every returned call. */ - readonly allocator: Address; /** Markets indexed by market id. */ readonly markets?: Readonly>; /** Accrued Vault V2 entities indexed by vault address. */ @@ -212,8 +210,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { private readonly firstTotalAssets: Record; /** Chain id associated with this snapshot. */ public readonly chainId: number; - /** Explicit BluePublicAllocator address used in returned calls. */ - public readonly allocator: Address; /** Markets indexed by market id. */ public readonly markets: Record; /** Vault V2 entities indexed by address. */ @@ -246,7 +242,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { */ public constructor(input: InputVaultV2ReallocationData) { this.chainId = input.chainId; - this.allocator = input.allocator; this.markets = {}; this.vaults = {}; this.allocations = {}; @@ -674,10 +669,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { const publicAllocatorConfig = data.getPublicAllocatorConfig(vaultAddress); if ( - !isAddressEqual( - publicAllocatorConfig.allocator, - data.allocator, - ) || !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) || (options.maxPenalty != null && publicAllocatorConfig.penalty > options.maxPenalty) @@ -717,10 +708,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { const marketPublicAllocatorConfig = data.getMarketPublicAllocatorConfig(vaultAddress, ids[2]); if ( - !isAddressEqual( - marketPublicAllocatorConfig.allocator, - data.allocator, - ) || !isAddressEqual( marketPublicAllocatorConfig.vault, vaultAddress, @@ -776,8 +763,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { ); if (assets > 0n) { rawCandidates.push({ - allocator: data.allocator, - type: "bluePublicAllocator", vault: vaultAddress, from: { type: "idle" }, to: { adapter: targetContext.adapter.address }, @@ -821,7 +806,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { sourceIds[2], ); if ( - !isAddressEqual(sourceConfig.allocator, data.allocator) || !isAddressEqual(sourceConfig.vault, vaultAddress) || !isAddressEqual( sourceConfig.adapter, @@ -857,8 +841,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { if (assets <= 0n) return; return { - allocator: data.allocator, - type: "bluePublicAllocator", vault: vaultAddress, from: { type: "market", @@ -1232,7 +1214,6 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * }); * const reallocationData = new VaultV2ReallocationData({ * chainId: 1, - * allocator: "0x0000000000000000000000000000000000000005", * markets: { [marketParams.id]: market }, * }); * diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts index d4c00ae7a..8e02ec268 100644 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts @@ -33,8 +33,6 @@ describe("computeVaultV2ReallocationPenaltyAssets", () => { withdrawals: [{ marketParams, amount: 1n }], }, { - type: "bluePublicAllocator", - allocator: CbbtcUsdcBlue.irm, vault: CbbtcUsdcBlue.oracle, from: { type: "idle" }, to: { adapter: CbbtcUsdcBlue.collateralToken }, @@ -42,8 +40,6 @@ describe("computeVaultV2ReallocationPenaltyAssets", () => { penalty: 1n, }, { - type: "bluePublicAllocator", - allocator: CbbtcUsdcBlue.irm, vault: CbbtcUsdcBlue.oracle, from: { type: "idle" }, to: { adapter: CbbtcUsdcBlue.collateralToken }, diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts index 470b64b25..379dec1ec 100644 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts @@ -45,7 +45,7 @@ export const computeVaultV2ReallocationPenaltyAssets = ( ) => { let total = 0n; for (const reallocation of reallocations) { - if (reallocation.type === "bluePublicAllocator") + if ("from" in reallocation) total += computeBluePublicAllocatorPenaltyAssets( reallocation.assets, reallocation.penalty, diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index 642b1848b..24d9d1c69 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -27,8 +27,8 @@ import { InconsistentReallocationPenaltyError, InputExceedsMaxError, InvalidReallocationAddressError, + InvalidReallocationShapeError, InvalidReallocationSourceTypeError, - InvalidReallocationTypeError, MarketIdMismatchError, MissingClientPropertyError, MissingMarketPriceError, @@ -562,8 +562,6 @@ describe("validateReallocations", () => { }; const validBluePublicAllocatorReallocation: VaultV2BlueReallocation = { - type: "bluePublicAllocator", - allocator: USER_A, vault: USER_B, from: { type: "idle" }, to: { adapter: USER_A }, @@ -577,15 +575,6 @@ describe("validateReallocations", () => { ).not.toThrow(); }); - test("behavior: accepts an explicitly tagged V1 reallocation", () => { - expect(() => - validateReallocations( - [{ ...validReallocation, type: "publicAllocatorV1" }], - targetMarketId, - ), - ).not.toThrow(); - }); - test("behavior: accepts a valid Blue Public Allocator idle reallocation", () => { expect(() => validateReallocations( @@ -639,7 +628,7 @@ describe("validateReallocations", () => { }, ); - test("error: InconsistentReallocationPenaltyError for one allocator-vault pair", () => { + test("error: InconsistentReallocationPenaltyError for one vault", () => { expect(() => validateReallocations( [ @@ -665,14 +654,14 @@ describe("validateReallocations", () => { ).not.toThrow(); }); - test("behavior: allows different penalties for different allocator-vault pairs", () => { + test("behavior: allows different penalties for different vaults", () => { expect(() => validateReallocations( [ { ...validBluePublicAllocatorReallocation, penalty: 5n }, { ...validBluePublicAllocatorReallocation, - allocator: USER_B, + vault: USER_A, penalty: 11n, }, ], @@ -740,7 +729,6 @@ describe("validateReallocations", () => { }); test.each([ - { name: "missing allocator", overrides: { allocator: undefined } }, { name: "invalid vault", overrides: { vault: "not-an-address" } }, { name: "missing target", overrides: { to: undefined } }, { @@ -800,22 +788,25 @@ describe("validateReallocations", () => { test.each([ { - name: "unknown top-level discriminator", + name: "entry matching neither shape", reallocation: { - ...validReallocation, - type: "publicAllocatorV3", + vault: USER_A, + fee: 0n, } as unknown as BlueReallocation, }, { - name: "untagged entry without V1 withdrawals", + name: "entry matching both shapes", reallocation: { - vault: USER_A, - fee: 0n, + ...validReallocation, + from: { type: "idle" }, + to: { adapter: USER_A }, + assets: 1n, + penalty: 0n, } as unknown as BlueReallocation, }, - ])("error: InvalidReallocationTypeError for $name", ({ reallocation }) => { + ])("error: InvalidReallocationShapeError for $name", ({ reallocation }) => { expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( - InvalidReallocationTypeError, + InvalidReallocationShapeError, ); }); diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index c0e3c6d6b..c880c37c7 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -21,8 +21,8 @@ import { InconsistentReallocationPenaltyError, InputExceedsMaxError, InvalidReallocationAddressError, + InvalidReallocationShapeError, InvalidReallocationSourceTypeError, - InvalidReallocationTypeError, MarketIdMismatchError, MissingClientPropertyError, MissingMarketPriceError, @@ -339,7 +339,7 @@ export const validateRepayShares = (params: { * - Withdrawal market IDs must be strictly ascending. * * BluePublicAllocator entries enforce a WAD-bounded `penalty`, one consistent - * penalty per allocator-vault pair, positive `uint128`-bounded `assets`, and a + * penalty per vault, positive `uint128`-bounded `assets`, and a * market source distinct from the target market. Idle sources * have no market or sorting rule. * @@ -350,10 +350,10 @@ export const validateRepayShares = (params: { * @throws {EmptyReallocationWithdrawalsError} when a reallocation has no withdrawals. * @throws {NonPositiveInputError} when a withdrawal or BluePublicAllocator asset amount is non-positive. * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when entries for one allocator-vault pair use different penalties. - * @throws {InvalidReallocationAddressError} when a BluePublicAllocator identity or adapter address is malformed. + * @throws {InconsistentReallocationPenaltyError} when entries for one vault use different penalties. + * @throws {InvalidReallocationAddressError} when a BluePublicAllocator vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. + * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. * @throws {ReallocationWithdrawalOnTargetMarketError} when a V1 or V2 source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when withdrawals are not strictly market-id sorted. * @example @@ -369,13 +369,14 @@ export const validateReallocations = ( reallocations: Iterable, targetMarketId: MarketId, ): void => { - const penaltyByAllocatorVault = new Map(); + const penaltyByVault = new Map(); for (const r of reallocations) { - if (r.type === "bluePublicAllocator") { - if (typeof r.allocator !== "string" || !isAddress(r.allocator)) { - throw new InvalidReallocationAddressError("allocator"); - } + if ("from" in r === "withdrawals" in r) { + throw new InvalidReallocationShapeError(); + } + + if ("from" in r) { if (typeof r.vault !== "string" || !isAddress(r.vault)) { throw new InvalidReallocationAddressError("vault"); } @@ -436,17 +437,16 @@ export const validateReallocations = ( }); } - const penaltyKey = `${r.allocator.toLowerCase()}:${r.vault.toLowerCase()}`; - const expectedPenalty = penaltyByAllocatorVault.get(penaltyKey); + const penaltyKey = r.vault.toLowerCase(); + const expectedPenalty = penaltyByVault.get(penaltyKey); if (expectedPenalty !== undefined && expectedPenalty !== r.penalty) { throw new InconsistentReallocationPenaltyError({ - allocator: r.allocator, vault: r.vault, expected: expectedPenalty, actual: r.penalty, }); } - penaltyByAllocatorVault.set(penaltyKey, r.penalty); + penaltyByVault.set(penaltyKey, r.penalty); if ( sourceMarketId !== undefined && @@ -459,16 +459,6 @@ export const validateReallocations = ( } continue; } - const reallocationType = r.type; - if ( - reallocationType !== undefined && - reallocationType !== "publicAllocatorV1" - ) { - throw new InvalidReallocationTypeError(reallocationType); - } - if (!("withdrawals" in r)) { - throw new InvalidReallocationTypeError(reallocationType); - } if (r.fee < 0n) { throw new NegativeInputError("reallocation.fee", r.fee); } diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index 398118b46..edfbaf50c 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -18,8 +18,8 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. ## Shared liquidity (`sharedLiquidity.ts`) -- `VaultV1BlueReallocation` — legacy-untagged or explicitly `publicAllocatorV1` vault address + fee + sorted withdrawals; maps to `reallocateTo()`. `VaultReallocation` is its deprecated compatibility alias. -- `VaultV2BlueReallocation` — tagged BluePublicAllocator/vault/source/target-adapter/assets/WAD-scaled-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. +- `VaultV1BlueReallocation` — vault address + fee + sorted withdrawals; maps to `reallocateTo()`. `VaultReallocation` is its deprecated compatibility alias. +- `VaultV2BlueReallocation` — BluePublicAllocator vault/source/target-adapter/assets/WAD-scaled-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. - `VaultV2BluePublicAllocatorOptions` — canonical Vault V2 discovery and planner options for timestamp, enablement, vault allowlisting, and the maximum proportional penalty. - `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. @@ -29,7 +29,7 @@ One class per error case. Never throw a generic `Error` from SDK source. - **Generic input bounds:** `NegativeInputError` for values that must be non-negative, `NonPositiveInputError` for values that must be positive, and `InputExceedsMaxError` for protocol upper bounds such as BluePublicAllocator's `uint128` assets and WAD-scaled `uint64` penalty. All expose the invalid `field` and `value`; reuse them across Vault, Blue, and Midnight instead of adding operation-specific scalar-bound errors. - **Market-specific:** `BorrowExceedsSafeLtvError`, `MissingMarketPriceError`, `NativeAmountOnNonWNativeAssetError`, `MutuallyExclusiveWithdrawAmountsError`, `WithdrawExceedsSupplyError`, `WithdrawSharesExceedSupplyError`. -- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationTypeError` for an unknown top-level Public Allocator variant, `InvalidReallocationAddressError` for malformed BluePublicAllocator identity or adapter addresses, `InvalidReallocationSourceTypeError` for an absent, incomplete, or unknown BluePublicAllocator source, `InconsistentReallocationPenaltyError` for conflicting penalties on one allocator-vault pair, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. +- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationShapeError` when an entry matches both or neither V1/V2 shape, `InvalidReallocationAddressError` for malformed BluePublicAllocator vault or adapter addresses, `InvalidReallocationSourceTypeError` for an absent, incomplete, or unknown BluePublicAllocator source, `InconsistentReallocationPenaltyError` for conflicting penalties on one vault, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. ## Adding a new operation diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 7d7327d22..1a0863c5a 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -55,6 +55,36 @@ export class NonPositiveInputError extends Error { } } +/** Thrown when an integer input exceeds its protocol-defined maximum. */ +export class InputExceedsMaxError extends Error { + /** + * @param params - Maximum-bound validation details. + * @param params.field - Public input field whose value is invalid. + * @param params.value - Supplied value. + * @param params.max - Largest accepted value. + */ + public constructor(params: { + readonly field: string; + readonly value: bigint; + readonly max: bigint; + }) { + super( + `Input "${params.field}" must be at most "${params.max}", got "${params.value}".`, + ); + this.field = params.field; + this.value = params.value; + this.max = params.max; + this.name = "InputExceedsMaxError"; + } + + /** Public input field whose value is invalid. */ + public readonly field: string; + /** Supplied value. */ + public readonly value: bigint; + /** Largest accepted value. */ + public readonly max: bigint; +} + /** Thrown when an in-kind redemption does not include any Morpho Blue market parameters. */ export class EmptyMarketParamsListError extends Error { public constructor() { @@ -284,36 +314,6 @@ export class VaultExitBundlesV1PermitMismatchError extends Error { } } -/** Thrown when an integer input exceeds its protocol-defined maximum. */ -export class InputExceedsMaxError extends Error { - /** - * @param params - Maximum-bound validation details. - * @param params.field - Public input field whose value is invalid. - * @param params.value - Supplied value. - * @param params.max - Largest accepted value. - */ - public constructor(params: { - readonly field: string; - readonly value: bigint; - readonly max: bigint; - }) { - super( - `Input "${params.field}" must be at most "${params.max}", got "${params.value}".`, - ); - this.field = params.field; - this.value = params.value; - this.max = params.max; - this.name = "InputExceedsMaxError"; - } - - /** Public input field whose value is invalid. */ - public readonly field: string; - /** Supplied value. */ - public readonly value: bigint; - /** Largest accepted value. */ - public readonly max: bigint; -} - /** @deprecated Use {@link NonPositiveInputError}. */ export const NonPositiveAssetAmountError = NonPositiveInputError; /** @deprecated Use {@link NonPositiveInputError}. */ @@ -824,33 +824,28 @@ export class ReallocationWithdrawalOnTargetMarketError extends Error { } /** - * Thrown when a Public Allocator reallocation has an unknown top-level discriminator. + * Thrown when a Public Allocator reallocation does not match exactly one V1 or + * V2 input shape. * * @example * ```ts - * import { InvalidReallocationTypeError } from "@morpho-org/morpho-sdk"; + * import { InvalidReallocationShapeError } from "@morpho-org/morpho-sdk"; * - * const error = new InvalidReallocationTypeError("publicAllocatorV3"); + * const error = new InvalidReallocationShapeError(); * ``` */ -export class InvalidReallocationTypeError extends Error { - /** - * @param reallocationType - Invalid runtime value received for `reallocation.type`, or - * `undefined` when an untagged entry lacks the V1 `withdrawals` field. - */ - public constructor(public readonly reallocationType: string | undefined) { +export class InvalidReallocationShapeError extends Error { + public constructor() { super( - reallocationType === undefined - ? 'Reallocation must be an untagged Public Allocator V1 entry with "withdrawals" or specify type "publicAllocatorV1" or "bluePublicAllocator".' - : `Reallocation type must be "publicAllocatorV1" or "bluePublicAllocator", got "${reallocationType}".`, + 'Reallocation must contain either V1 "withdrawals" or V2 "from", but not both.', ); - this.name = "InvalidReallocationTypeError"; + this.name = "InvalidReallocationShapeError"; } } /** - * Thrown when a Blue Public Allocator reallocation contains a malformed - * identity or adapter address. + * Thrown when a Blue Public Allocator reallocation contains a malformed vault + * or adapter address. * * @example * ```ts @@ -867,11 +862,7 @@ export class InvalidReallocationAddressError extends Error { * @param field - Reallocation address field that is absent or malformed. */ public constructor( - public readonly field: - | "allocator" - | "vault" - | "from.adapter" - | "to.adapter", + public readonly field: "vault" | "from.adapter" | "to.adapter", ) { super(`Reallocation "${field}" must be a valid address.`); this.name = "InvalidReallocationAddressError"; @@ -911,20 +902,16 @@ export class InvalidReallocationSourceTypeError extends Error { } /** - * Thrown when one bundle assigns different penalty rates to the same Blue - * Public Allocator and Vault V2 pair. + * Thrown when one bundle assigns different penalty rates to the same Vault V2. * * @example * ```ts * import { InconsistentReallocationPenaltyError } from "@morpho-org/morpho-sdk"; * import type { Address } from "viem"; * - * const allocatorFixture = - * "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" satisfies Address; * const vaultFixture = * "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" satisfies Address; * const error = new InconsistentReallocationPenaltyError({ - * allocator: allocatorFixture, * vault: vaultFixture, * expected: 5n, * actual: 11n, @@ -932,8 +919,6 @@ export class InvalidReallocationSourceTypeError extends Error { * ``` */ export class InconsistentReallocationPenaltyError extends Error { - /** Blue Public Allocator contract address. */ - public readonly allocator: Address; /** Vault whose configured penalty must be reused. */ public readonly vault: Address; /** Penalty rate established by the first matching bundle entry. */ @@ -942,22 +927,19 @@ export class InconsistentReallocationPenaltyError extends Error { public readonly actual: bigint; /** - * @param params - Conflicting allocator-vault penalty details. - * @param params.allocator - Blue Public Allocator contract address. + * @param params - Conflicting vault penalty details. * @param params.vault - Vault whose configured penalty applies. * @param params.expected - Penalty rate established by the first matching entry. * @param params.actual - Conflicting penalty rate supplied by a later entry. */ public constructor(params: { - readonly allocator: Address; readonly vault: Address; readonly expected: bigint; readonly actual: bigint; }) { super( - `Penalty for Blue Public Allocator "${params.allocator}" and vault "${params.vault}" must remain "${params.expected}" across the bundle, got "${params.actual}". Use the vault's configured penalty for every call.`, + `Penalty for vault "${params.vault}" must remain "${params.expected}" across the bundle, got "${params.actual}". Use the vault's configured penalty for every call.`, ); - this.allocator = params.allocator; this.vault = params.vault; this.expected = params.expected; this.actual = params.actual; diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 206fee2fd..54a83fd0d 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -97,8 +97,6 @@ export interface ReallocationWithdrawal { * Withdraws from source markets and supplies to the target market. */ export interface VaultV1BlueReallocation { - /** Optional discriminator; omitted by legacy Public Allocator V1 callers. */ - readonly type?: "publicAllocatorV1"; readonly vault: Address; /** Fee in native token (ETH) paid to the PublicAllocator for this vault. */ readonly fee: bigint; @@ -127,10 +125,6 @@ export type BluePublicAllocatorSource = * The target market parameters are derived from the enclosing Blue action. */ export interface VaultV2BlueReallocation { - /** Explicit allocator contract address because BluePublicAllocator has no deployment registry entry. */ - readonly allocator: Address; - /** Discriminator separating BluePublicAllocator reallocations from PublicAllocator V1 reallocations. */ - readonly type: "bluePublicAllocator"; /** Vault whose liquidity is moved. */ readonly vault: Address; /** Liquidity source. */ @@ -146,9 +140,8 @@ export interface VaultV2BlueReallocation { /** * Reallocation accepted by Blue actions that support PublicAllocator V1 or BluePublicAllocator. * - * V1 entries remain valid without a `type` field and may optionally use - * `type: "publicAllocatorV1"`; Blue Public Allocator entries use - * `type: "bluePublicAllocator"`. + * V1 entries are identified by `withdrawals`; V2 entries are identified by + * `from`. */ export type BlueReallocation = | VaultV1BlueReallocation diff --git a/packages/morpho-ts/src/addresses.test.ts b/packages/morpho-ts/src/addresses.test.ts index 0f0b5fa8f..eda64b0e9 100644 --- a/packages/morpho-ts/src/addresses.test.ts +++ b/packages/morpho-ts/src/addresses.test.ts @@ -142,6 +142,32 @@ describe("addressesRegistry", () => { expect("midnight" in addressesRegistry[1]).toBe(false); }); + test.each([ + [ChainId.EthMainnet, "0x00b8e1509398ED692C3F326CbAf1694F9A881e27"], + [ChainId.BaseMainnet, "0xAED282B8aD9257BB1272e93aE63A32A53621e412"], + [ChainId.ArbitrumMainnet, "0x85b66Fe31e6788E5a6825EAe689f4c6c38AF3704"], + [ChainId.OptimismMainnet, "0xc6945A915Bb7e2A365469f120A33D2FA42951cF3"], + [ChainId.PolygonMainnet, "0xAb06a92cd253Bc12Dec8f719a693a6b472CCDfF4"], + [ChainId.WorldChainMainnet, "0x5Fe47f63ACd84f8A69b97E0a5122fCBff08Df48F"], + [ChainId.Unichain, "0x2b7Bf2f2027bcfE3A1F6Bc93EA80220a883a6851"], + [ChainId.HyperliquidMainnet, "0x056dd7D4B373ED26c788190085CC6C52B8e7479d"], + [ChainId.KatanaMainnet, "0xd952175e940D97775cBC5a523977a6f091D0d702"], + [ChainId.MonadMainnet, "0x0A503aB026EFACBC0F7feE7795F34B80b5B9a662"], + [ChainId.StableMainnet, "0x5C884d4B1510EAd302EC50A2AB4DE9c0b9E407ce"], + [ChainId.TempoMainnet, "0xDC9693CE6488640faEf173Ec2635ff99fdC25a07"], + [ChainId.RobinhoodMainnet, "0xCe5c1aFa115fF8b1D6913509bfc79D9AE08CC857"], + ] as const)( + "behavior: exposes BluePublicAllocator on chain %i", + (chainId, bluePublicAllocator) => { + expect(addressesRegistry[chainId].bluePublicAllocator).toBe( + bluePublicAllocator, + ); + expect(getChainAddress(chainId, "bluePublicAllocator")).toBe( + bluePublicAllocator, + ); + }, + ); + test("behavior: exposes World Chain USDC permit v2 token", () => { const usdc = "0x79A02482A880bCE3F13e09Da970dC34db4CD24d1"; diff --git a/packages/morpho-ts/src/addresses.ts b/packages/morpho-ts/src/addresses.ts index 9ace9cc18..20eeeaa68 100644 --- a/packages/morpho-ts/src/addresses.ts +++ b/packages/morpho-ts/src/addresses.ts @@ -64,6 +64,8 @@ export interface ChainAddresses { adaptiveCurveIrm: `0x${string}`; /** PublicAllocator contract for permissionless MetaMorpho reallocations subject to flow caps and vault fees. */ publicAllocator?: `0x${string}`; + /** BluePublicAllocator contract for permissionless Vault V2 reallocations subject to allocation caps and penalties. */ + bluePublicAllocator?: `0x${string}`; /** MetaMorpho factory that creates and indexes Morpho Vault V1 ERC4626 vaults. */ metaMorphoFactory?: `0x${string}`; /** VaultV2 factory that creates and indexes Morpho Vault V2 ERC4626/ERC2612 vaults. */ @@ -145,6 +147,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC", publicAllocator: "0xfd32fA2ca22c76dD6E550706Ad913FC6CE91c75D", + bluePublicAllocator: "0x00b8e1509398ED692C3F326CbAf1694F9A881e27", metaMorphoFactory: "0x1897A8997241C1cD4bD0698647e4EB7213535c24", vaultV2Factory: "0xA1D94F746dEfa1928926b84fB2596c06926C0405", morphoMarketV1AdapterFactory: "0xb049465969ac6355127cDf9E88deE63d25204d5D", @@ -237,6 +240,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0x46415998764C29aB2a25CbeA6254146D50D22687", publicAllocator: "0xA090dD1a701408Df1d4d0B85b716c87565f90467", + bluePublicAllocator: "0xAED282B8aD9257BB1272e93aE63A32A53621e412", metaMorphoFactory: "0xFf62A7c278C62eD665133147129245053Bbf5918", vaultV2Factory: "0x4501125508079A99ebBebCE205DeC9593C2b5857", morphoMarketV1AdapterFactory: "0x133baC94306B99f6dAD85c381a5be851d8DD717c", @@ -281,6 +285,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0xe675A2161D4a6E2de2eeD70ac98EEBf257FBF0B0", publicAllocator: "0xfac15aff53ADd2ff80C2962127C434E8615Df0d3", + bluePublicAllocator: "0xAb06a92cd253Bc12Dec8f719a693a6b472CCDfF4", metaMorphoFactory: "0xa9c87daB340631C34BB738625C70499e29ddDC98", vaultV2Factory: "0xC11a53eE9B1eCc7a068D8e40F8F17926584F97Cf", morphoMarketV1AdapterFactory: "0xD1A0C86F28ecD1657Ad06415c2B230cC89D9b6dd", @@ -311,6 +316,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0x66F30587FB8D4206918deb78ecA7d5eBbafD06DA", publicAllocator: "0x769583Af5e9D03589F159EbEC31Cc2c23E8C355E", + bluePublicAllocator: "0x85b66Fe31e6788E5a6825EAe689f4c6c38AF3704", metaMorphoFactory: "0x878988f5f561081deEa117717052164ea1Ef0c82", vaultV2Factory: "0x6b46fa3cc9EBF8aB230aBAc664E37F2966Bf7971", morphoMarketV1AdapterFactory: "0x96456Bf888D4de607Bf3ca0b3C8e4DF9b0d0Ad47", @@ -339,6 +345,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0x8cD70A8F399428456b29546BC5dBe10ab6a06ef6", publicAllocator: "0x0d68a97324E602E02799CD83B42D337207B40658", + bluePublicAllocator: "0xc6945A915Bb7e2A365469f120A33D2FA42951cF3", metaMorphoFactory: "0x3Bb6A6A0Bc85b367EFE0A5bAc81c5E52C892839a", vaultV2Factory: "0x6128b680b277Bf4Df80DFE9D8c55A498660870ef", morphoMarketV1AdapterFactory: "0x65956d5Ba4974983ecCe111612FC0A0c22650A11", @@ -365,6 +372,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0x34E99D604751a72cF8d0CFDf87069292d82De472", publicAllocator: "0xef9889B4e443DEd35FA0Bd060f2104Cca94e6A43", + bluePublicAllocator: "0x5Fe47f63ACd84f8A69b97E0a5122fCBff08Df48F", metaMorphoFactory: "0x4DBB3a642a2146d5413750Cca3647086D9ba5F12", vaultV2Factory: "0x6846EA318B6B987Ee6b28eBFd87c3409F1d13108", morphoMarketV1AdapterFactory: "0xAf93F2d8508053432659d509b0210fdF1472493D", @@ -445,6 +453,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0x9a6061d51743B31D2c3Be75D83781Fa423f53F0E", publicAllocator: "0xB0c9a107fA17c779B3378210A7a593e88938C7C9", + bluePublicAllocator: "0x2b7Bf2f2027bcfE3A1F6Bc93EA80220a883a6851", metaMorphoFactory: "0xe9EdE3929F43a7062a007C3e8652e4ACa610Bdc0", vaultV2Factory: "0xC9b34c108014B44e5a189A830e7e04c56704a0c9", morphoMarketV1AdapterFactory: "0x117b92Ab1C025B175ED38a0CDe5A067a745224a0", @@ -575,6 +584,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0x4F708C0ae7deD3d74736594C2109C2E3c065B428", publicAllocator: "0x39EB6Da5e88194C82B13491Df2e8B3E213eD2412", + bluePublicAllocator: "0xd952175e940D97775cBC5a523977a6f091D0d702", metaMorphoFactory: "0x1c8De6889acee12257899BFeAa2b7e534de32E16", vaultV2Factory: "0xFcb8b57E56787bB29e130Fca67f3c5a1232975D1", morphoMarketV1AdapterFactory: "0x2e6BE3a3A27fb45c6AbA2D1833eeA48E8788538e", @@ -648,6 +658,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0xD4a426F010986dCad727e8dd6eed44cA4A9b7483", publicAllocator: "0x517505be22D9068687334e69ae7a02fC77edf4Fc", + bluePublicAllocator: "0x056dd7D4B373ED26c788190085CC6C52B8e7479d", metaMorphoFactory: "0xec051b19d654C48c357dC974376DeB6272f24e53", vaultV2Factory: "0xD7217E5687FF1071356C780b5fe4803D9D967da7", morphoMarketV1AdapterFactory: "0xc6b8B565C715134b0Ca3D6fa3D29B25759D0b9e2", @@ -725,6 +736,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0x09475a3D6eA8c314c592b1a3799bDE044E2F400F", publicAllocator: "0xfd70575B732F9482F4197FE1075492e114E97302", + bluePublicAllocator: "0x0A503aB026EFACBC0F7feE7795F34B80b5B9a662", metaMorphoFactory: "0x33f20973275B2F574488b18929cd7DCBf1AbF275", vaultV2Factory: "0x8B2F922162FBb60A6a072cC784A2E4168fB0bb0c", morphoMarketV1AdapterFactory: "0x8Da54fbF89B3D6fC6DCC92F31CF75a211ACF3d46", @@ -749,6 +761,7 @@ const _addressesRegistry = { }, adaptiveCurveIrm: "0x41e846FC8108b8527C1D4EDB4c9564E56442940f", publicAllocator: "0xbCB063D4B6D479b209C186e462828CBACaC82DbE", + bluePublicAllocator: "0x5C884d4B1510EAd302EC50A2AB4DE9c0b9E407ce", metaMorphoFactory: "0xb4ae5673c48621189E2bEfBA96F31912032DD1AE", vaultV2Factory: "0x7fc35488803D49D00a94b206A223f7661898BE3a", morphoMarketV1AdapterFactory: "0x2A5F218FE4Dac3b1f4E096e8ae83074bB1713833", @@ -874,6 +887,7 @@ const _addressesRegistry = { vaultExitBundlesV1: "0x8225192b8638bDe9D41a6d96aBb824F660Ef57E1", }, adaptiveCurveIrm: "0x112fd4042E442C3C12C67AD23587b0afe36eB74E", + bluePublicAllocator: "0xDC9693CE6488640faEf173Ec2635ff99fdC25a07", vaultV2Factory: "0x3DE400E3F79113194fa5AF6Ae5C474947E0C82Db", morphoMarketV1AdapterV2Factory: "0xF85aD5f14cC903533FC409B8098B58b4C2f36697", @@ -1046,6 +1060,7 @@ const _addressesRegistry = { vaultExitBundlesV1: "0xCE29862924756584BBD0D75CA1249d22007E2813", }, adaptiveCurveIrm: "0x2BD3d5965B26B51814AC95127B2b80dD6CcC0fa1", + bluePublicAllocator: "0xCe5c1aFa115fF8b1D6913509bfc79D9AE08CC857", vaultV2Factory: "0x0FBad98595b0186dA120E41f77C102beb49f803c", morphoMarketV1AdapterV2Factory: "0x79370Ed003CE325C088E530d5e8655c99c2993e1", diff --git a/packages/wdk-protocol-lending-morpho-evm/README.md b/packages/wdk-protocol-lending-morpho-evm/README.md index 4f0ff8455..17763433a 100644 --- a/packages/wdk-protocol-lending-morpho-evm/README.md +++ b/packages/wdk-protocol-lending-morpho-evm/README.md @@ -152,8 +152,6 @@ const options = { token: usdc, amount: 1_000_000n, reallocations: [{ - allocator, - type: 'bluePublicAllocator', vault, from: { type: 'idle' }, to: { adapter }, diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index 278e56cd1..fe2efd5b1 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -486,8 +486,6 @@ describe.sequential("MorphoProtocolEvm", () => { test("should forward Vault V2 BluePublicAllocator reallocations", async () => { const reallocation = { - allocator: "0x0000000000000000000000000000000000000010", - type: "bluePublicAllocator", vault: VAULT, from: { type: "idle" }, to: { @@ -578,8 +576,6 @@ describe.sequential("MorphoProtocolEvm", () => { amount: 100_000n, reallocations: [ { - allocator: "0x0000000000000000000000000000000000000010", - type: "bluePublicAllocator", vault: VAULT, from: { type: "idle" }, to: { From 15112a8e42359cdcc14a1c5babd9e5c88d0430cb Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Tue, 18 Aug 2026 09:38:54 +0200 Subject: [PATCH 21/41] refactor: clarify Vault V2 cap IDs --- .changeset/brave-vaults-reallocate.md | 2 + ...lt-v2-public-allocator-shared-liquidity.md | 16 ++--- .../GetVaultV2PublicAllocatorConfig.sol | 10 +-- .../src/fetch/vault-v2/VaultV2.ts | 10 +-- ...2PublicAllocatorConfig.integration.test.ts | 10 +-- .../VaultV2PublicAllocatorConfig.test.ts | 12 ++-- .../vault-v2/VaultV2PublicAllocatorConfig.ts | 28 ++++---- .../src/fetch/vault-v2/vault-v2.test.ts | 14 ++-- .../GetVaultV2PublicAllocatorConfig.ts | 6 +- packages/blue-sdk-viem/test/VaultV2.test.ts | 4 +- .../blue-sdk-viem/test/VaultV2Adapter.test.ts | 8 ++- .../blue-sdk/src/vault/v2/VaultV2.test.ts | 42 +++++++++-- .../vault/v2/VaultV2MorphoMarketV1Adapter.ts | 70 +++++++++++++++++-- .../v2/VaultV2MorphoMarketV1AdapterV2.ts | 68 ++++++++++++++++-- .../vault/v2/VaultV2MorphoVaultV1Adapter.ts | 21 +++++- .../vault/v2/VaultV2PublicAllocatorConfig.ts | 4 +- .../entities/vaultV2ReallocationData.test.ts | 6 +- .../src/entities/vaultV2ReallocationData.ts | 16 +++-- packages/morpho-sdk/src/types/error.ts | 6 +- 19 files changed, 264 insertions(+), 89 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 0cee2e7c9..a21c5670a 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -14,4 +14,6 @@ Use coherent versioned names across the V1 and V2 reallocation APIs, including ` Compatibility note: `VaultV2MorphoMarketV1AdapterV2.ids()` now declares its existing three-element result as `readonly [Hash, Hash, Hash]`. The runtime values and ordering are unchanged, and derived allocation identifiers are immutable descriptors. We intentionally accept this TypeScript assignability tightening in the minor release; callers that explicitly require a mutable `Hash[]` can copy the tuple with `[...adapter.ids(params)]`. +Name allocation-cap helpers `adapterCapId`, `collateralCapId`, and `adapterMarketCapId`. Preserve the published `adapterId`, `collateralId`, and `marketParamsId` helpers as deprecated aliases. + Add an explicit `MorphoBorrowWithV2ReallocationsOptions` WDK opt-in for the combined V1/V2 reallocation union and its possible approval requirement while preserving the legacy `MorphoBorrowOptions` input and authorization-only requirement result type. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index f71e59c48..6e4cbabf4 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -172,17 +172,17 @@ ordering requirement, or multi-source fee refund. The proportional penalty is rounded up and charged per call. The allocator cap is a post-state ceiling on the target adapter's -`marketParamsId`, not a consumable flow budget. It must be non-zero before the -vault call. Source-side allocator state is only `canPullFromMarket`. +`adapterMarketCapId`, not a consumable flow budget. It must be non-zero +before the vault call. Source-side allocator state is only `canPullFromMarket`. ## Derived allocation IDs `VaultV2MorphoMarketV1AdapterV2.ids(params)` returns: -1. `adapterId(address)` — shared by every market on the adapter; -2. `collateralId(collateralToken)` — shared across adapters for the same +1. `adapterCapId(address)` — shared by every market on the adapter; +2. `collateralCapId(collateralToken)` — shared across adapters for the same collateral; -3. `marketParamsId(adapter, params)` — unique to an adapter/market pair. +3. `adapterMarketCapId(adapter, params)` — unique to an adapter/market pair. State is therefore keyed by `(vault, derivedId)`, not by a projected `(vault, adapter, market)` tuple: @@ -214,7 +214,7 @@ The readonly config projections are self-identifying. Vault-wide state carries `vault`, `canPullFromIdle`, and `penalty`. Adapter activation input accepts arrays, readonly arrays, sets, or any other iterable, and is normalized as a vault-keyed set of adapter addresses. Market state carries `adapter`, -`marketParamsId`, `absoluteCap`, and `canPullFromMarket`. +`adapterMarketCapId`, `absoluteCap`, and `canPullFromMarket`. ## Fetching @@ -224,7 +224,7 @@ defaulting to the client chain id: - `fetchVaultV2PublicAllocatorConfig(vault, client, parameters?)`; - `fetchVaultV2MarketPublicAllocatorConfig(vault, adapter, - marketParamsId, client, parameters?)`; + adapterMarketCapId, client, parameters?)`; - `fetchVaultV2PublicAllocatorData(hydratedVault, client, parameters?)`. @@ -264,7 +264,7 @@ For adapter `a` and market `m`: expectedSupplyAssets(a, m) = market.toSupplyAssets(adapter.supplyShares[m]) untracked(a, m) = zeroFloorSub( expectedSupplyAssets(a, m), - allocation[marketParamsId(a, m)] + allocation[adapterMarketCapId(a, m)] ) ``` diff --git a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol index 95f171303..7a8e7863b 100644 --- a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol +++ b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol @@ -6,12 +6,12 @@ import {IVaultV2} from "./interfaces/IVaultV2.sol"; struct VaultV2MarketPublicAllocatorRequest { address adapter; - bytes32 marketParamsId; + bytes32 adapterMarketCapId; } struct VaultV2MarketPublicAllocatorResponse { address adapter; - bytes32 marketParamsId; + bytes32 adapterMarketCapId; uint256 absoluteCap; bool canPullFromMarket; } @@ -53,9 +53,9 @@ contract GetVaultV2PublicAllocatorConfig { VaultV2MarketPublicAllocatorRequest calldata request = marketRequests[i]; res.marketConfigs[i] = VaultV2MarketPublicAllocatorResponse({ adapter: request.adapter, - marketParamsId: request.marketParamsId, - absoluteCap: allocator.absoluteCap(address(vault), request.marketParamsId), - canPullFromMarket: allocator.canPullFromMarket(address(vault), request.marketParamsId) + adapterMarketCapId: request.adapterMarketCapId, + absoluteCap: allocator.absoluteCap(address(vault), request.adapterMarketCapId), + canPullFromMarket: allocator.canPullFromMarket(address(vault), request.adapterMarketCapId) }); } diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2.ts index fe17e32d3..9cc775ac5 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2.ts @@ -325,14 +325,16 @@ export async function fetchVaultV2( let liquidityAdapterIds: Hash[] | undefined; if (hasMorphoVaultV1LiquidityAdapter) liquidityAdapterIds = [ - VaultV2MorphoVaultV1Adapter.adapterId(liquidityAdapter), + VaultV2MorphoVaultV1Adapter.adapterCapId(liquidityAdapter), ]; if (hasMorphoMarketV1AdapterV2LiquidityAdapter) { const marketParams = MarketParams.fromHex(liquidityData); liquidityAdapterIds = [ - VaultV2MorphoMarketV1AdapterV2.adapterId(liquidityAdapter), - VaultV2MorphoMarketV1AdapterV2.collateralId(marketParams.collateralToken), - VaultV2MorphoMarketV1AdapterV2.marketParamsId( + VaultV2MorphoMarketV1AdapterV2.adapterCapId(liquidityAdapter), + VaultV2MorphoMarketV1AdapterV2.collateralCapId( + marketParams.collateralToken, + ), + VaultV2MorphoMarketV1AdapterV2.adapterMarketCapId( liquidityAdapter, marketParams, ), diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts index 7a50911b1..87b30b93a 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts @@ -44,7 +44,7 @@ describe("Vault V2 public allocator fetchers on fork", () => { ); await client.setCode({ address: allocator, bytecode: fixtureBytecode }); - const forkMarketParamsId = forkAdapter.ids(forkMarket.params)[2]; + const forkAdapterMarketCapId = forkAdapter.ids(forkMarket.params)[2]; await client.writeContract({ address: allocator, abi: fixtureAbi, @@ -55,13 +55,13 @@ describe("Vault V2 public allocator fetchers on fork", () => { address: allocator, abi: fixtureAbi, functionName: "setAbsoluteCap", - args: [forkVault.address, forkMarketParamsId, 500n], + args: [forkVault.address, forkAdapterMarketCapId, 500n], }); await client.writeContract({ address: allocator, abi: fixtureAbi, functionName: "setCanPullFromMarket", - args: [forkVault.address, forkMarketParamsId, true], + args: [forkVault.address, forkAdapterMarketCapId, true], }); await client.writeContract({ address: allocator, @@ -89,11 +89,11 @@ describe("Vault V2 public allocator fetchers on fork", () => { new Set([forkAdapter.address]), ); expect( - deployless.marketPublicAllocatorConfigs[forkMarketParamsId], + deployless.marketPublicAllocatorConfigs[forkAdapterMarketCapId], ).toStrictEqual({ vault: forkVault.address, adapter: forkAdapter.address, - marketParamsId: forkMarketParamsId, + adapterMarketCapId: forkAdapterMarketCapId, absoluteCap: 500n, canPullFromMarket: true, }); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts index f37e606ab..83244d466 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts @@ -79,7 +79,7 @@ const vault = new AccrualVaultV2( {}, ); const ids = adapter.ids(marketParams); -const marketParamsId = ids[2]; +const adapterMarketCapId = ids[2]; const expected = { publicAllocatorConfig: { @@ -89,10 +89,10 @@ const expected = { }, activeAdapters: new Set([ADAPTER]), marketPublicAllocatorConfigs: { - [marketParamsId]: { + [adapterMarketCapId]: { vault: VAULT, adapter: ADAPTER, - marketParamsId, + adapterMarketCapId, absoluteCap: 500n, canPullFromMarket: true, }, @@ -170,11 +170,11 @@ describe("Vault V2 public allocator fetchers", () => { fetchVaultV2MarketPublicAllocatorConfig( VAULT, ADAPTER, - marketParamsId, + adapterMarketCapId, handle.client, ), ).resolves.toStrictEqual( - expected.marketPublicAllocatorConfigs[marketParamsId], + expected.marketPublicAllocatorConfigs[adapterMarketCapId], ); }); @@ -187,7 +187,7 @@ describe("Vault V2 public allocator fetchers", () => { marketConfigs: [ { adapter: ADAPTER, - marketParamsId, + adapterMarketCapId, absoluteCap: 500n, canPullFromMarket: true, }, diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts index f5c7ca456..314375ba7 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts @@ -75,7 +75,7 @@ export async function fetchVaultV2PublicAllocatorConfig( * * @param vault - Vault V2 address. * @param adapter - MorphoMarketV1AdapterV2 address. - * @param marketParamsId - Adapter-scoped market-parameters id. + * @param adapterMarketCapId - Adapter-scoped market cap id. * @param client - Viem client used for contract reads. * @param parameters.account - Optional account passed to viem calls. * @param parameters.blockNumber - Optional block number for historical reads. @@ -97,12 +97,12 @@ export async function fetchVaultV2PublicAllocatorConfig( * export async function fetchMarketAllocatorConfig( * vault: Address, * adapter: Address, - * marketParamsId: Hash, + * adapterMarketCapId: Hash, * ): Promise { * return fetchVaultV2MarketPublicAllocatorConfig( * vault, * adapter, - * marketParamsId, + * adapterMarketCapId, * client, * ); * } @@ -112,7 +112,7 @@ export async function fetchVaultV2PublicAllocatorConfig( export async function fetchVaultV2MarketPublicAllocatorConfig( vault: Address, adapter: Address, - marketParamsId: Hash, + adapterMarketCapId: Hash, client: Client, parameters: FetchParameters = {}, ): Promise { @@ -124,21 +124,21 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( address: allocator, abi: vaultV2BluePublicAllocatorAbi, functionName: "absoluteCap", - args: [vault, marketParamsId], + args: [vault, adapterMarketCapId], }), readContract(client, { ...parameters, address: allocator, abi: vaultV2BluePublicAllocatorAbi, functionName: "canPullFromMarket", - args: [vault, marketParamsId], + args: [vault, adapterMarketCapId], }), ]); return { vault, adapter, - marketParamsId, + adapterMarketCapId, absoluteCap, canPullFromMarket, }; @@ -161,7 +161,7 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( * @param parameters.stateOverride - Optional viem state override. * @param parameters.chainId - Optional chain id; defaults to `getChainId(client)`. * @param parameters.deployless - Deployless mode; defaults to `true`, with direct-read fallback. - * @returns Vault-wide config, active-adapter set, adapter-market configs keyed by `marketParamsId`, and allocations keyed by derived id. + * @returns Vault-wide config, active-adapter set, adapter-market configs keyed by `adapterMarketCapId`, and allocations keyed by derived id. * @throws {UnknownAddressError} when the chain has no BluePublicAllocator deployment. * @throws {UnsupportedChainIdError} when the chain is absent from the address registry. * @throws {viem.BaseError} when deployless mode is forced and fails, or when a direct contract read fails. @@ -192,7 +192,7 @@ export async function fetchVaultV2PublicAllocatorData( const allocator = getChainAddress(chainId, "bluePublicAllocator"); const marketRequests: { readonly adapter: Address; - readonly marketParamsId: Hash; + readonly adapterMarketCapId: Hash; }[] = []; const adapters = new Set
(); const allocationIds = new Set(); @@ -205,7 +205,7 @@ export async function fetchVaultV2PublicAllocatorData( const ids = adapter.ids(market.params); marketRequests.push({ adapter: adapter.address, - marketParamsId: ids[2], + adapterMarketCapId: ids[2], }); for (const id of ids) allocationIds.add(id); } @@ -235,7 +235,7 @@ export async function fetchVaultV2PublicAllocatorData( VaultV2MarketPublicAllocatorConfig | undefined > = {}; for (const config of result.marketConfigs) { - marketPublicAllocatorConfigs[config.marketParamsId] = { + marketPublicAllocatorConfigs[config.adapterMarketCapId] = { vault: vault.address, ...config, }; @@ -286,11 +286,11 @@ export async function fetchVaultV2PublicAllocatorData( ), ), Promise.all( - marketRequests.map(({ adapter, marketParamsId }) => + marketRequests.map(({ adapter, adapterMarketCapId }) => fetchVaultV2MarketPublicAllocatorConfig( vault.address, adapter, - marketParamsId, + adapterMarketCapId, client, { ...parameters, chainId }, ), @@ -332,7 +332,7 @@ export async function fetchVaultV2PublicAllocatorData( VaultV2MarketPublicAllocatorConfig | undefined > = {}; for (const config of marketConfigs) { - marketPublicAllocatorConfigs[config.marketParamsId] = config; + marketPublicAllocatorConfigs[config.adapterMarketCapId] = config; } const allocations: Record = {}; diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/vault-v2.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/vault-v2.test.ts index 3462e89c1..3613ec1d7 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/vault-v2.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/vault-v2.test.ts @@ -138,7 +138,7 @@ const vaultV2Result = { isLiquidityAdapterKnown: true, liquidityAllocations: [ { - id: VaultV2MorphoVaultV1Adapter.adapterId(ADAPTER), + id: VaultV2MorphoVaultV1Adapter.adapterCapId(ADAPTER), absoluteCap: 1_000n, relativeCap: 1_000000000000000000n, allocation: 100n, @@ -357,7 +357,7 @@ describe("fetchVaultV2", () => { result: false, }); mockVaultV2AllocationReads(handle, [ - VaultV2MorphoVaultV1Adapter.adapterId(ADAPTER), + VaultV2MorphoVaultV1Adapter.adapterCapId(ADAPTER), ]); const vault = await fetchVaultV2(VAULT, handle.client, { @@ -366,7 +366,7 @@ describe("fetchVaultV2", () => { expect(vault.liquidityAllocations).toHaveLength(1); expect(vault.liquidityAllocations?.[0]?.id).toBe( - VaultV2MorphoVaultV1Adapter.adapterId(ADAPTER), + VaultV2MorphoVaultV1Adapter.adapterCapId(ADAPTER), ); expect(vault.performanceFeeRecipientCanReceiveShares).toBe(false); expect(vault.managementFeeRecipientCanReceiveShares).toBe(false); @@ -542,9 +542,9 @@ describe("fetchVaultV2", () => { result: true, }); mockVaultV2AllocationReads(handle, [ - VaultV2MorphoMarketV1AdapterV2.adapterId(ADAPTER), - VaultV2MorphoMarketV1AdapterV2.collateralId(COLLATERAL), - VaultV2MorphoMarketV1AdapterV2.marketParamsId(ADAPTER, MARKET_PARAMS), + VaultV2MorphoMarketV1AdapterV2.adapterCapId(ADAPTER), + VaultV2MorphoMarketV1AdapterV2.collateralCapId(COLLATERAL), + VaultV2MorphoMarketV1AdapterV2.adapterMarketCapId(ADAPTER, MARKET_PARAMS), ]); const vault = await fetchVaultV2(VAULT, handle.client, { @@ -1816,7 +1816,7 @@ const accrualVaultV2Result = { isLiquidityAdapterKnown: true, liquidityAllocations: [ { - id: VaultV2MorphoMarketV1AdapterV2.adapterId(ADAPTER), + id: VaultV2MorphoMarketV1AdapterV2.adapterCapId(ADAPTER), absoluteCap: 1_000n, relativeCap: 1_000000000000000000n, allocation: 100n, diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts index 7df732017..ed9af0ee3 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts @@ -26,7 +26,7 @@ export const abi = [ }, { internalType: "bytes32", - name: "marketParamsId", + name: "adapterMarketCapId", type: "bytes32", }, ], @@ -68,7 +68,7 @@ export const abi = [ }, { internalType: "bytes32", - name: "marketParamsId", + name: "adapterMarketCapId", type: "bytes32", }, { @@ -126,4 +126,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2PublicAllocatorConfig` query bytecode. */ export const code = - "0x608080604052346015576108bb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c6352ae457214610025575f80fd5b3461030e5760a036600319011261030e576004356001600160a01b038116919082900361030e576024356001600160a01b0381169081900361030e576044356001600160401b03811161030e57610080903690600401610785565b606492919235906001600160401b03821161030e573660238301121561030e578160040135946001600160401b03861161030e573660248760061b8501011161030e576084356001600160401b03811161030e576100e2903690600401610785565b60a083949294018381106001600160401b03821117610771576040525f835260208301935f8552604084019160608352606085019860608a526080860194606086528c60408b6024825180948193636b97fbcd60e11b835260048301525afa801561031a575f915f91610718575b506001600160401b0316895215158752610169816107fe565b61017660405191826107d0565b818152601f19610185836107fe565b0136602083013785528c5f5b82811061067257505050506101a58a6107fe565b6101b260405191826107d0565b8a8152601f196101c18c6107fe565b015f5b81811061065b57505089525f5b8a81101561035e578b908060061b890161022d60208c60446101f560248601610839565b60405163011f009b60e31b81526001600160a01b03909316600484015294013560248201819052959092839190829081906044820190565b03915afa801561031a578f8d86935f93610325575b506040516369f1e26b60e01b81526001600160a01b039190911660048201526024810193909352602090839060449082905afa91821561031a575f926102cf575b509183916102c8936001966040519361029b856107b5565b888060a01b0316845260208401526040830152151560608201528d51906102c2838361084d565b5261084d565b50016101d1565b9150916020823d8211610312575b816102ea602093836107d0565b8101031261030e576001946102c89361030386946107f1565b935091935094610283565b5f80fd5b3d91506102dd565b6040513d5f823e3d90fd5b93505050506020813d8211610356575b81610342602093836107d0565b8101031261030e575183908f8d6020610242565b3d9150610335565b50889291889161036d816107fe565b61037a60405191826107d0565b818152601f19610389836107fe565b015f5b81811061064457505086525f5b8181106104dc576001600160401b0389898989896040519586956020875260c0870195511515602088015251166040860152519260a060608601528351809152602060e086019401905f5b8181106104c1575050505191601f19848203016080850152602080845192838152019301905f5b81811061047a575050505190601f198382030160a0840152602080835192838152019201905f5b818110610440575050500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610432565b825180516001600160a01b0316865260208181015181880152604080830151908801526060918201511515918701919091528796506080909501949092019160010161040b565b825115158652879650602095860195909201916001016103e4565b6104e7818385610815565b359060405191632f0374dd60e21b8352806004840152602083602481895afa92831561031a575f93610611575b5060405163a68bafa360e01b8152600481018290526020816024818a5afa90811561031a575f916105e0575b5060405163c69507dd60e01b815260048101839052906020826024818b5afa91821561031a575f926105aa575b509183916105a39360019660405193610585856107b5565b84526020840152604083015260608201528a51906102c2838361084d565b5001610399565b9150916020823d82116105d8575b816105c5602093836107d0565b8101031261030e5790519091600161056d565b3d91506105b8565b90506020813d8211610609575b816105fa602093836107d0565b8101031261030e57518c610540565b3d91506105ed565b9092506020813d821161063c575b8161062c602093836107d0565b8101031261030e5751918b610514565b3d915061061f565b60209061064f610861565b8282860101520161038c565b602090610666610861565b828286010152016101c4565b60208c604461068a61068585888a610815565b610839565b6040516366faa83960e01b815260048101939093526001600160a01b03166024830152909384919082905afa801561031a575f906106df575b600192506106d282895161084d565b9015159052018d90610191565b506020823d8211610710575b816106f8602093836107d0565b8101031261030e5761070b6001926107f1565b6106c3565b3d91506106eb565b9150506040813d604011610769575b81610734604093836107d0565b8101031261030e576020610747826107f1565b910151906001600160401b038216820361030e57906001600160401b03610150565b3d9150610727565b634e487b7160e01b5f52604160045260245ffd5b9181601f8401121561030e578235916001600160401b03831161030e576020808501948460051b01011161030e57565b608081019081106001600160401b0382111761077157604052565b90601f801991011681019081106001600160401b0382111761077157604052565b5190811515820361030e57565b6001600160401b0381116107715760051b60200190565b91908110156108255760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b038116810361030e5790565b80518210156108255760209160051b010190565b6040519061086e826107b5565b5f606083828152826020820152826040820152015256fea264697066735822122000c10ec804ce24c308983455a410ac3fd542112e1d058ca4af402aeebd1bdf3164736f6c63430008240033"; + "0x608080604052346015576108bb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c6352ae457214610025575f80fd5b3461030e5760a036600319011261030e576004356001600160a01b038116919082900361030e576024356001600160a01b0381169081900361030e576044356001600160401b03811161030e57610080903690600401610785565b606492919235906001600160401b03821161030e573660238301121561030e578160040135946001600160401b03861161030e573660248760061b8501011161030e576084356001600160401b03811161030e576100e2903690600401610785565b60a083949294018381106001600160401b03821117610771576040525f835260208301935f8552604084019160608352606085019860608a526080860194606086528c60408b6024825180948193636b97fbcd60e11b835260048301525afa801561031a575f915f91610718575b506001600160401b0316895215158752610169816107fe565b61017660405191826107d0565b818152601f19610185836107fe565b0136602083013785528c5f5b82811061067257505050506101a58a6107fe565b6101b260405191826107d0565b8a8152601f196101c18c6107fe565b015f5b81811061065b57505089525f5b8a81101561035e578b908060061b890161022d60208c60446101f560248601610839565b60405163011f009b60e31b81526001600160a01b03909316600484015294013560248201819052959092839190829081906044820190565b03915afa801561031a578f8d86935f93610325575b506040516369f1e26b60e01b81526001600160a01b039190911660048201526024810193909352602090839060449082905afa91821561031a575f926102cf575b509183916102c8936001966040519361029b856107b5565b888060a01b0316845260208401526040830152151560608201528d51906102c2838361084d565b5261084d565b50016101d1565b9150916020823d8211610312575b816102ea602093836107d0565b8101031261030e576001946102c89361030386946107f1565b935091935094610283565b5f80fd5b3d91506102dd565b6040513d5f823e3d90fd5b93505050506020813d8211610356575b81610342602093836107d0565b8101031261030e575183908f8d6020610242565b3d9150610335565b50889291889161036d816107fe565b61037a60405191826107d0565b818152601f19610389836107fe565b015f5b81811061064457505086525f5b8181106104dc576001600160401b0389898989896040519586956020875260c0870195511515602088015251166040860152519260a060608601528351809152602060e086019401905f5b8181106104c1575050505191601f19848203016080850152602080845192838152019301905f5b81811061047a575050505190601f198382030160a0840152602080835192838152019201905f5b818110610440575050500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610432565b825180516001600160a01b0316865260208181015181880152604080830151908801526060918201511515918701919091528796506080909501949092019160010161040b565b825115158652879650602095860195909201916001016103e4565b6104e7818385610815565b359060405191632f0374dd60e21b8352806004840152602083602481895afa92831561031a575f93610611575b5060405163a68bafa360e01b8152600481018290526020816024818a5afa90811561031a575f916105e0575b5060405163c69507dd60e01b815260048101839052906020826024818b5afa91821561031a575f926105aa575b509183916105a39360019660405193610585856107b5565b84526020840152604083015260608201528a51906102c2838361084d565b5001610399565b9150916020823d82116105d8575b816105c5602093836107d0565b8101031261030e5790519091600161056d565b3d91506105b8565b90506020813d8211610609575b816105fa602093836107d0565b8101031261030e57518c610540565b3d91506105ed565b9092506020813d821161063c575b8161062c602093836107d0565b8101031261030e5751918b610514565b3d915061061f565b60209061064f610861565b8282860101520161038c565b602090610666610861565b828286010152016101c4565b60208c604461068a61068585888a610815565b610839565b6040516366faa83960e01b815260048101939093526001600160a01b03166024830152909384919082905afa801561031a575f906106df575b600192506106d282895161084d565b9015159052018d90610191565b506020823d8211610710575b816106f8602093836107d0565b8101031261030e5761070b6001926107f1565b6106c3565b3d91506106eb565b9150506040813d604011610769575b81610734604093836107d0565b8101031261030e576020610747826107f1565b910151906001600160401b038216820361030e57906001600160401b03610150565b3d9150610727565b634e487b7160e01b5f52604160045260245ffd5b9181601f8401121561030e578235916001600160401b03831161030e576020808501948460051b01011161030e57565b608081019081106001600160401b0382111761077157604052565b90601f801991011681019081106001600160401b0382111761077157604052565b5190811515820361030e57565b6001600160401b0381116107715760051b60200190565b91908110156108255760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b038116810361030e5790565b80518210156108255760209160051b010190565b6040519061086e826107b5565b5f606083828152826020820152826040820152015256fea26469706673582212200386873a9ac711f6a4b93b2c556cdaebfe2c1bbaaa1fb3cb4e4ab78ee41c6dbc64736f6c63430008240033"; diff --git a/packages/blue-sdk-viem/test/VaultV2.test.ts b/packages/blue-sdk-viem/test/VaultV2.test.ts index 6a52f49db..e385384fe 100644 --- a/packages/blue-sdk-viem/test/VaultV2.test.ts +++ b/packages/blue-sdk-viem/test/VaultV2.test.ts @@ -112,7 +112,7 @@ describe("AccrualVaultV2", () => { liquidityData: "0x", liquidityAllocations: [ { - id: VaultV2MorphoVaultV1Adapter.adapterId( + id: VaultV2MorphoVaultV1Adapter.adapterCapId( "0x2C32fF5E1d976015AdbeA8cC73c7Da3A6677C25F", ), absoluteCap: 1000000000000n, @@ -150,7 +150,7 @@ describe("AccrualVaultV2", () => { liquidityData: "0x", liquidityAllocations: [ { - id: VaultV2MorphoVaultV1Adapter.adapterId( + id: VaultV2MorphoVaultV1Adapter.adapterCapId( "0x2C32fF5E1d976015AdbeA8cC73c7Da3A6677C25F", ), absoluteCap: 1000000000000n, diff --git a/packages/blue-sdk-viem/test/VaultV2Adapter.test.ts b/packages/blue-sdk-viem/test/VaultV2Adapter.test.ts index a9c2e4cb7..843a98082 100644 --- a/packages/blue-sdk-viem/test/VaultV2Adapter.test.ts +++ b/packages/blue-sdk-viem/test/VaultV2Adapter.test.ts @@ -212,7 +212,9 @@ describe("LiquidityAdapter vaultV1", () => { abi: vaultV2Abi, functionName: "absoluteCap", args: [ - VaultV2MorphoVaultV1Adapter.adapterId(vaultV2AdapterVaultV1Address), + VaultV2MorphoVaultV1Adapter.adapterCapId( + vaultV2AdapterVaultV1Address, + ), ], }), readContract(client, { @@ -220,7 +222,9 @@ describe("LiquidityAdapter vaultV1", () => { abi: vaultV2Abi, functionName: "allocation", args: [ - VaultV2MorphoVaultV1Adapter.adapterId(vaultV2AdapterVaultV1Address), + VaultV2MorphoVaultV1Adapter.adapterCapId( + vaultV2AdapterVaultV1Address, + ), ], }), ]); diff --git a/packages/blue-sdk/src/vault/v2/VaultV2.test.ts b/packages/blue-sdk/src/vault/v2/VaultV2.test.ts index 343b53ab8..f032ac2af 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2.test.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2.test.ts @@ -347,14 +347,27 @@ describe("VaultV2MorphoMarketV1Adapter", () => { expect(adapter.type).toBe("VaultV2MorphoMarketV1Adapter"); expect(adapter.adapterId).toBe( - VaultV2MorphoMarketV1Adapter.adapterId(adapter.address), + VaultV2MorphoMarketV1Adapter.adapterCapId(adapter.address), ); expect(adapter.marketParamsList[0]).toBeInstanceOf(MarketParams); expect(adapter.ids(params)).toStrictEqual([ adapter.adapterId, - VaultV2MorphoMarketV1Adapter.collateralId(params.collateralToken), - VaultV2MorphoMarketV1Adapter.marketParamsId(adapter.address, params), + VaultV2MorphoMarketV1Adapter.collateralCapId(params.collateralToken), + VaultV2MorphoMarketV1Adapter.adapterMarketCapId(adapter.address, params), ]); + expect( + VaultV2MorphoMarketV1Adapter.marketParamsId(adapter.address, params), + ).toBe( + VaultV2MorphoMarketV1Adapter.adapterMarketCapId(adapter.address, params), + ); + expect(VaultV2MorphoMarketV1Adapter.adapterId(adapter.address)).toBe( + VaultV2MorphoMarketV1Adapter.adapterCapId(adapter.address), + ); + expect( + VaultV2MorphoMarketV1Adapter.collateralId(params.collateralToken), + ).toBe( + VaultV2MorphoMarketV1Adapter.collateralCapId(params.collateralToken), + ); }); }); @@ -418,12 +431,28 @@ describe("VaultV2MorphoMarketV1AdapterV2", () => { expect(adapter.type).toBe("VaultV2MorphoMarketV1AdapterV2"); expect(adapter.adapterId).toBe( - VaultV2MorphoMarketV1AdapterV2.adapterId(adapter.address), + VaultV2MorphoMarketV1AdapterV2.adapterCapId(adapter.address), ); expect(adapter.marketIds).toStrictEqual([m.id]); expect(adapter.adaptiveCurveIrm).toBe(ADAPTER); expect(adapter.supplyShares[m.id]).toBe(123n); expect(adapter.ids(m.params)[0]).toBe(adapter.adapterId); + expect( + VaultV2MorphoMarketV1AdapterV2.marketParamsId(adapter.address, m.params), + ).toBe( + VaultV2MorphoMarketV1AdapterV2.adapterMarketCapId( + adapter.address, + m.params, + ), + ); + expect(VaultV2MorphoMarketV1AdapterV2.adapterId(adapter.address)).toBe( + VaultV2MorphoMarketV1AdapterV2.adapterCapId(adapter.address), + ); + expect( + VaultV2MorphoMarketV1AdapterV2.collateralId(m.params.collateralToken), + ).toBe( + VaultV2MorphoMarketV1AdapterV2.collateralCapId(m.params.collateralToken), + ); }); }); @@ -530,10 +559,13 @@ describe("VaultV2MorphoVaultV1Adapter", () => { expect(adapter.type).toBe("VaultV2MorphoVaultV1Adapter"); expect(adapter.adapterId).toBe( - VaultV2MorphoVaultV1Adapter.adapterId(adapter.address), + VaultV2MorphoVaultV1Adapter.adapterCapId(adapter.address), ); expect(adapter.morphoVaultV1).toBe(RECIPIENT); expect(adapter.ids()).toStrictEqual([adapter.adapterId]); + expect(VaultV2MorphoVaultV1Adapter.adapterId(adapter.address)).toBe( + VaultV2MorphoVaultV1Adapter.adapterCapId(adapter.address), + ); }); }); diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts index 93d5cc712..78101044d 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts @@ -27,7 +27,17 @@ export class VaultV2MorphoMarketV1Adapter { public declare readonly type: "VaultV2MorphoMarketV1Adapter"; - static adapterId(address: Address) { + /** + * Returns the adapter-wide allocation-cap id. + * + * @param address - Adapter address. + * @returns The adapter-wide allocation-cap id. + * @example + * ```ts + * const id = VaultV2MorphoMarketV1Adapter.adapterCapId(adapter); + * ``` + */ + static adapterCapId(address: Address) { return keccak256( encodeAbiParameters( [{ type: "string" }, { type: "address" }], @@ -36,7 +46,22 @@ export class VaultV2MorphoMarketV1Adapter ); } - static collateralId(address: Address) { + /** @deprecated Use {@link VaultV2MorphoMarketV1Adapter.adapterCapId}. */ + static adapterId(address: Address) { + return VaultV2MorphoMarketV1Adapter.adapterCapId(address); + } + + /** + * Returns the collateral-wide allocation-cap id. + * + * @param address - Collateral token address. + * @returns The collateral-wide allocation-cap id. + * @example + * ```ts + * const id = VaultV2MorphoMarketV1Adapter.collateralCapId(collateral); + * ``` + */ + static collateralCapId(address: Address) { return keccak256( encodeAbiParameters( [{ type: "string" }, { type: "address" }], @@ -45,7 +70,26 @@ export class VaultV2MorphoMarketV1Adapter ); } - static marketParamsId(address: Address, params: MarketParams) { + /** @deprecated Use {@link VaultV2MorphoMarketV1Adapter.collateralCapId}. */ + static collateralId(address: Address) { + return VaultV2MorphoMarketV1Adapter.collateralCapId(address); + } + + /** + * Returns the adapter-market allocation-cap id. + * + * @param address - Adapter address. + * @param params - Morpho Blue market parameters. + * @returns The adapter-market allocation-cap id. + * @example + * ```ts + * const id = VaultV2MorphoMarketV1Adapter.adapterMarketCapId( + * adapter, + * marketParams, + * ); + * ``` + */ + static adapterMarketCapId(address: Address, params: MarketParams) { return keccak256( encodeAbiParameters( [{ type: "string" }, { type: "address" }, marketParamsAbi], @@ -54,6 +98,18 @@ export class VaultV2MorphoMarketV1Adapter ); } + /** + * Returns the adapter-market allocation-cap id. + * + * @param address - Adapter address. + * @param params - Morpho Blue market parameters. + * @returns The adapter-market allocation-cap id. + * @deprecated Use {@link VaultV2MorphoMarketV1Adapter.adapterMarketCapId}. + */ + static marketParamsId(address: Address, params: MarketParams) { + return VaultV2MorphoMarketV1Adapter.adapterMarketCapId(address, params); + } + public marketParamsList: MarketParams[]; constructor({ @@ -63,7 +119,9 @@ export class VaultV2MorphoMarketV1Adapter super({ ...vaultV2Adapter, type: "VaultV2MorphoMarketV1Adapter", - adapterId: VaultV2MorphoMarketV1Adapter.adapterId(vaultV2Adapter.address), + adapterId: VaultV2MorphoMarketV1Adapter.adapterCapId( + vaultV2Adapter.address, + ), }); this.marketParamsList = marketParamsList.map( @@ -74,8 +132,8 @@ export class VaultV2MorphoMarketV1Adapter public ids(params: MarketParams) { return [ this.adapterId, - VaultV2MorphoMarketV1Adapter.collateralId(params.collateralToken), - VaultV2MorphoMarketV1Adapter.marketParamsId(this.address, params), + VaultV2MorphoMarketV1Adapter.collateralCapId(params.collateralToken), + VaultV2MorphoMarketV1Adapter.adapterMarketCapId(this.address, params), ]; } } diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts index 19f355a0a..b31fab325 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts @@ -34,7 +34,17 @@ export class VaultV2MorphoMarketV1AdapterV2 { public declare readonly type: "VaultV2MorphoMarketV1AdapterV2"; - static adapterId(address: Address) { + /** + * Returns the adapter-wide allocation-cap id. + * + * @param address - Adapter address. + * @returns The adapter-wide allocation-cap id. + * @example + * ```ts + * const id = VaultV2MorphoMarketV1AdapterV2.adapterCapId(adapter); + * ``` + */ + static adapterCapId(address: Address) { return keccak256( encodeAbiParameters( [{ type: "string" }, { type: "address" }], @@ -43,7 +53,22 @@ export class VaultV2MorphoMarketV1AdapterV2 ); } - static collateralId(address: Address) { + /** @deprecated Use {@link VaultV2MorphoMarketV1AdapterV2.adapterCapId}. */ + static adapterId(address: Address) { + return VaultV2MorphoMarketV1AdapterV2.adapterCapId(address); + } + + /** + * Returns the collateral-wide allocation-cap id. + * + * @param address - Collateral token address. + * @returns The collateral-wide allocation-cap id. + * @example + * ```ts + * const id = VaultV2MorphoMarketV1AdapterV2.collateralCapId(collateral); + * ``` + */ + static collateralCapId(address: Address) { return keccak256( encodeAbiParameters( [{ type: "string" }, { type: "address" }], @@ -52,7 +77,26 @@ export class VaultV2MorphoMarketV1AdapterV2 ); } - static marketParamsId(address: Address, params: MarketParams) { + /** @deprecated Use {@link VaultV2MorphoMarketV1AdapterV2.collateralCapId}. */ + static collateralId(address: Address) { + return VaultV2MorphoMarketV1AdapterV2.collateralCapId(address); + } + + /** + * Returns the adapter-market allocation-cap id. + * + * @param address - Adapter address. + * @param params - Morpho Blue market parameters. + * @returns The adapter-market allocation-cap id. + * @example + * ```ts + * const id = VaultV2MorphoMarketV1AdapterV2.adapterMarketCapId( + * adapter, + * marketParams, + * ); + * ``` + */ + static adapterMarketCapId(address: Address, params: MarketParams) { return keccak256( encodeAbiParameters( [{ type: "string" }, { type: "address" }, marketParamsAbi], @@ -61,6 +105,18 @@ export class VaultV2MorphoMarketV1AdapterV2 ); } + /** + * Returns the adapter-market allocation-cap id. + * + * @param address - Adapter address. + * @param params - Morpho Blue market parameters. + * @returns The adapter-market allocation-cap id. + * @deprecated Use {@link VaultV2MorphoMarketV1AdapterV2.adapterMarketCapId}. + */ + static marketParamsId(address: Address, params: MarketParams) { + return VaultV2MorphoMarketV1AdapterV2.adapterMarketCapId(address, params); + } + public marketIds: MarketId[]; public adaptiveCurveIrm: Address; public supplyShares: Record; @@ -74,7 +130,7 @@ export class VaultV2MorphoMarketV1AdapterV2 super({ ...vaultV2Adapter, type: "VaultV2MorphoMarketV1AdapterV2", - adapterId: VaultV2MorphoMarketV1AdapterV2.adapterId( + adapterId: VaultV2MorphoMarketV1AdapterV2.adapterCapId( vaultV2Adapter.address, ), }); @@ -87,8 +143,8 @@ export class VaultV2MorphoMarketV1AdapterV2 public ids(params: MarketParams): readonly [Hash, Hash, Hash] { return [ this.adapterId, - VaultV2MorphoMarketV1AdapterV2.collateralId(params.collateralToken), - VaultV2MorphoMarketV1AdapterV2.marketParamsId(this.address, params), + VaultV2MorphoMarketV1AdapterV2.collateralCapId(params.collateralToken), + VaultV2MorphoMarketV1AdapterV2.adapterMarketCapId(this.address, params), ]; } } diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts index 8510a0a39..8d0212185 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts @@ -23,7 +23,17 @@ export class VaultV2MorphoVaultV1Adapter { public declare readonly type: "VaultV2MorphoVaultV1Adapter"; - static adapterId(address: Address) { + /** + * Returns the adapter-wide allocation-cap id. + * + * @param address - Adapter address. + * @returns The adapter-wide allocation-cap id. + * @example + * ```ts + * const id = VaultV2MorphoVaultV1Adapter.adapterCapId(adapter); + * ``` + */ + static adapterCapId(address: Address) { return keccak256( encodeAbiParameters( [{ type: "string" }, { type: "address" }], @@ -32,6 +42,11 @@ export class VaultV2MorphoVaultV1Adapter ); } + /** @deprecated Use {@link VaultV2MorphoVaultV1Adapter.adapterCapId}. */ + static adapterId(address: Address) { + return VaultV2MorphoVaultV1Adapter.adapterCapId(address); + } + public readonly morphoVaultV1: Address; constructor({ @@ -41,7 +56,9 @@ export class VaultV2MorphoVaultV1Adapter super({ ...vaultV2Adapter, type: "VaultV2MorphoVaultV1Adapter", - adapterId: VaultV2MorphoVaultV1Adapter.adapterId(vaultV2Adapter.address), + adapterId: VaultV2MorphoVaultV1Adapter.adapterCapId( + vaultV2Adapter.address, + ), }); this.morphoVaultV1 = morphoVaultV1; diff --git a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts index e2b13a513..8ff1d02c1 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts @@ -16,8 +16,8 @@ export interface VaultV2MarketPublicAllocatorConfig { readonly vault: Address; /** Vault V2 MorphoMarketV1AdapterV2 address. */ readonly adapter: Address; - /** Adapter-scoped `marketParamsId` used by the allocator mappings. */ - readonly marketParamsId: Hash; + /** Adapter-scoped market-parameters id used by the allocator mappings. */ + readonly adapterMarketCapId: Hash; /** Maximum post-state allocation accepted by the allocator. */ readonly absoluteCap: bigint; /** Whether the allocator may pull assets from this adapter-market pair. */ diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts index 2a1231774..7b2e072d5 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -292,14 +292,14 @@ const makeFixture = ({ [targetIds[2]]: { vault: VAULT, adapter: TARGET_ADAPTER, - marketParamsId: targetIds[2], + adapterMarketCapId: targetIds[2], absoluteCap: allocatorTargetCap, canPullFromMarket: false, }, [sourceIds[2]]: { vault: VAULT, adapter: sourceAdapterAddress, - marketParamsId: sourceIds[2], + adapterMarketCapId: sourceIds[2], absoluteCap: 0n, canPullFromMarket, }, @@ -431,7 +431,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { [secondTargetIds[2]]: { vault: SECOND_VAULT, adapter: SECOND_TARGET_ADAPTER, - marketParamsId: secondTargetIds[2], + adapterMarketCapId: secondTargetIds[2], absoluteCap: 10_000n, canPullFromMarket: false, }, diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts index 4b6499aa7..ff68ab973 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -67,7 +67,7 @@ export interface InputVaultV2ReallocationData { readonly activeAdapters?: Readonly< Record | undefined> >; - /** Adapter-market BluePublicAllocator configuration indexed by vault and `marketParamsId`. */ + /** Adapter-market BluePublicAllocator configuration indexed by vault and `adapterMarketCapId`. */ readonly marketPublicAllocatorConfigs?: Readonly< Record< Address, @@ -411,20 +411,24 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * Gets one adapter-market BluePublicAllocator configuration. * * @param vault - Vault V2 address. - * @param marketParamsId - Adapter-scoped market-parameters id. + * @param adapterMarketCapId - Adapter-scoped market cap id. * @returns The allocator cap and permissions. * @throws {UnknownReallocationMarketPublicAllocatorConfigError} when it is absent. * @example * ```ts - * const config = data.getMarketPublicAllocatorConfig(vaultAddress, marketParamsId); + * const config = data.getMarketPublicAllocatorConfig(vaultAddress, adapterMarketCapId); * ``` */ - public getMarketPublicAllocatorConfig(vault: Address, marketParamsId: Hash) { - const config = this.marketPublicAllocatorConfigs[vault]?.[marketParamsId]; + public getMarketPublicAllocatorConfig( + vault: Address, + adapterMarketCapId: Hash, + ) { + const config = + this.marketPublicAllocatorConfigs[vault]?.[adapterMarketCapId]; if (config == null) throw new UnknownReallocationMarketPublicAllocatorConfigError( vault, - marketParamsId, + adapterMarketCapId, ); return config; } diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 1a0863c5a..a574df058 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -1164,14 +1164,14 @@ export class UnknownReallocationPublicAllocatorConfigError extends UnknownDataEr export class UnknownReallocationMarketPublicAllocatorConfigError extends UnknownDataError { /** * @param vault - Vault V2 address for the missing configuration. - * @param marketParamsId - Missing adapter-scoped market-parameters id. + * @param adapterMarketCapId - Missing adapter-scoped market cap id. */ constructor( public readonly vault: Address, - public readonly marketParamsId: Hash, + public readonly adapterMarketCapId: Hash, ) { super( - `unknown public allocator configuration "${marketParamsId}" for vault "${vault}"`, + `unknown public allocator configuration "${adapterMarketCapId}" for vault "${vault}"`, ); } } From 6aa6be4bb88c963785ba264a7bb53cb1d6aec0ff Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Tue, 18 Aug 2026 10:23:49 +0200 Subject: [PATCH 22/41] chore: drop unrelated PR changes --- .agents/pr-review-engine/agents/documentation.md | 6 +----- packages/blue-sdk-viem/src/queries/GetHolding.ts | 2 +- packages/blue-sdk-viem/src/queries/GetMarket.ts | 2 +- packages/blue-sdk-viem/src/queries/GetToken.ts | 2 +- packages/blue-sdk-viem/src/queries/GetVault.ts | 2 +- packages/blue-sdk-viem/src/queries/GetVaultUser.ts | 2 +- .../blue-sdk-viem/src/queries/vault-v2/GetAccrualVaultV2.ts | 2 +- packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2.ts | 2 +- .../src/queries/vault-v2/GetVaultV2MorphoMarketV1Adapter.ts | 2 +- .../queries/vault-v2/GetVaultV2MorphoMarketV1AdapterV2.ts | 2 +- .../src/queries/vault-v2/GetVaultV2MorphoVaultV1Adapter.ts | 2 +- 11 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.agents/pr-review-engine/agents/documentation.md b/.agents/pr-review-engine/agents/documentation.md index 52d85d578..475eaed3c 100644 --- a/.agents/pr-review-engine/agents/documentation.md +++ b/.agents/pr-review-engine/agents/documentation.md @@ -11,7 +11,6 @@ focus: | 2. Markdown documentation accuracy across the repo (README, AGENTS.md, MISSION.md, docs/**, .agents/**). 3. Pointer / link integrity for every internal reference touched by the diff. 4. AGENTS.md ↔ persona backlink consistency. - 5. Immutability of implemented TIBs already present on the target branch. canonical-rules: docs/jsdoc-style.md --- @@ -46,11 +45,8 @@ Files in scope (read each one whose content is in the diff OR which references s - `.agents/pr-review-engine/SKILL.md`, `.agents/pr-review-engine/agents/*.md`, `.agents/pr-review-engine/references/*.md`, `.agents/commands/*.md`. - Any `*.md` colocated with a package (`packages//*.md`). -Implemented TIBs already present on the target branch are exempt from current-code freshness and pointer-renaming checks below. Their implementation-time symbols, examples, and paths are historical evidence, even when the current code has moved on. - For each Markdown file affected, flag: -- **Implemented TIB rewrites.** A TIB already present on the target branch is a historical implementation-time record: do not update its prose, examples, symbols, or paths to follow later code. Changed decisions require a new superseding TIB; operational clarifications belong in a dated addendum. A TIB introduced for the current implementation may still be updated before it lands. - **Stale prose.** A statement that no longer matches the code after the diff — e.g. README documents a function that was removed/renamed; AGENTS.md lists a rule the code change just violated; an example that no longer compiles. - **Out-of-sync inventories.** A file enumerating personas, packages, slash commands, scripts, supported chains, etc. that no longer matches reality after the diff. E.g. a README that lists "supported chains: mainnet, base" while the diff just added arbitrum. - **Cross-doc consistency.** When the diff changes a rule in `AGENTS.md`, every persona that enforces it (per the backlink `> Applied by personas: …`) should reflect the new rule. When the diff renames a section heading in `AGENTS.md`, every doc that references that section by title needs an update. @@ -63,7 +59,7 @@ For every Markdown link, path reference, or symbol pointer in the changed files - **Internal Markdown links must resolve.** `[label](./path/to/file.md)` — the path must exist. Anchors `#section-name` must match a heading in the target file (slugified — GitHub's convention). - **Path references in prose must resolve.** Lines like `Reference \`docs/jsdoc-style.md\`` or `Read \`.agents/pr-review-engine/agents/web3-security.md\`` are pointers; the file must exist. - **Frontmatter references must resolve.** Persona frontmatter (`applies:`, `trigger:`, `canonical-rules:`, `out-of-scope:` mentions) must reference real `AGENTS.md` sections, real flag names from `.agents/pr-review-engine/SKILL.md` Step 4, and real file paths. -- **Renames cascade.** If the diff renames or moves a file (detect via `git diff --name-status --find-renames`), every reference to the old path in current Markdown / persona / skill / command files must be updated. Grep for the old basename in the repo and surface unresolved hits, excluding implemented TIBs already present on the target branch. +- **Renames cascade.** If the diff renames or moves a file (detect via `git diff --name-status --find-renames`), every reference to the old path in any tracked Markdown / persona / skill / command file must be updated. Grep for the old basename in the repo and surface unresolved hits. - **Removed exports / removed files.** If the diff removes a public export or a file, grep the repo for references and flag any that survive. ## 4. AGENTS.md ↔ persona backlink consistency diff --git a/packages/blue-sdk-viem/src/queries/GetHolding.ts b/packages/blue-sdk-viem/src/queries/GetHolding.ts index a9cda86b6..1b42cb252 100644 --- a/packages/blue-sdk-viem/src/queries/GetHolding.ts +++ b/packages/blue-sdk-viem/src/queries/GetHolding.ts @@ -119,4 +119,4 @@ export const abi = [ /** @internal Deployless `GetHolding` query bytecode. */ export const code = - "0x60808060405234601557610794908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c634755ff3e14610024575f80fd5b346104735760e0366003190112610473576004356001600160a01b0381168103610473576024356001600160a01b038116810361047357604435906001600160a01b038216820361047357606435926001600160a01b038416840361047357608435906001600160a01b03821682036104735760a43580151581036104735760c4358015158103610473576101406040525f60805260a0966100c4610709565b885260c0946100d1610709565b86525f60e0818152610100829052610120919091526040516370a0823160e01b81526001600160a01b038981166004830152919991602090829060249082908b165afa9081156105d7575f91610685575b50608052604051636eb1769f60e11b81526001600160a01b0389811660048301529182166024820152838216151591602090829060449082908b165afa9081156105d7575f91610653575b50811561064a57604051636eb1769f60e11b81526001600160a01b03808b16600483015285166024820152602081806044810103816001600160a01b038c165afa80156105d7575f90610616575b6101f491505b604051636eb1769f60e11b81526001600160a01b03808d1660048301528616602482015291602090839081906044820190565b03816001600160a01b038d165afa9182156105d7575f926105e2575b506040519261021e846106cb565b8352602083015260408201528a5261050f575b5050604051623f675f60e91b81526001600160a01b0386811660048301526020908290602490829088165afa5f91816104db575b506104cd575b50604051624b894760e91b81526001600160a01b0386811660048301526020908290602490829088165afa5f91816104ac575b5061048957501561047f576102b760015b610120610752565b610351575b5065ffffffffffff91506040908180519560805187525180516020880152602081015182880152015160608601525160018060a01b0381511660808601528260208201511660a086015201511660c083015251151560e0820152608080015161010082015260a06080015190600382101561033d5761014091610120820152f35b634e487b7160e01b5f52602160045260245ffd5b5f6101205260405163650369bf60e01b815290602090829060049082906001600160a01b03165afa5f918161042d575b509065ffffffffffff936040939261039c575b5050906102bc565b8351633af32abf60e01b81526001600160a01b0391821660048201529160209183916024918391165afa5f91816103fc575b506103da575b80610394565b156103f2576103ec6002610120610752565b5f6103d4565b6103ec60016102af565b61041f91925060203d602011610426575b61041781836106e7565b81019061073a565b905f6103ce565b503d61040d565b909291506020813d602011610477575b8161044a602093836106e7565b810103126104735751916001600160a01b03831683036104735790919065ffffffffffff610381565b5f80fd5b3d915061043d565b6102b760026102af565b1590506104a25761049d6002610120610752565b6102b7565b61049d60016102af565b6104c691925060203d6020116104265761041781836106e7565b905f61029e565b60018752610100525f61026b565b9091506020813d602011610507575b816104f7602093836106e7565b810103126104735751905f610265565b3d91506104ea565b60405163927da10560e01b81526001600160a01b038881166004830152868116602483015291821660448201529160609183916064918391165afa9081156105d7575f91610562575b5084525f80610231565b90506060813d6060116105cf575b8161057d606093836106e7565b810103126104735760405190610592826106cb565b80516001600160a01b0381168103610473576105c49160409184526105b960208201610727565b602085015201610727565b60408201525f610558565b3d9150610570565b6040513d5f823e3d90fd5b9091506020813d60201161060e575b816105fe602093836106e7565b810103126104735751905f610210565b3d91506105f1565b506020813d602011610642575b81610630602093836106e7565b81010312610473576101f490516101bb565b3d9150610623565b6101f45f6101c1565b90506020813d60201161067d575b8161066e602093836106e7565b8101031261047357515f61016d565b3d9150610661565b90506020813d6020116106af575b816106a0602093836106e7565b8101031261047357515f610122565b3d9150610693565b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff8211176106b757604052565b90601f8019910116810190811067ffffffffffffffff8211176106b757604052565b60405190610716826106cb565b5f6040838281528260208201520152565b519065ffffffffffff8216820361047357565b90816020910312610473575180151581036104735790565b600382101561033d575256fea264697066735822122095a40c118b49c33ea63eedac0f2c5b9f776b8e0143bd1d1ec1e7e427a022bf7c64736f6c63430008240033"; + "0x60808060405234601557610794908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c634755ff3e14610024575f80fd5b346104735760e0366003190112610473576004356001600160a01b0381168103610473576024356001600160a01b038116810361047357604435906001600160a01b038216820361047357606435926001600160a01b038416840361047357608435906001600160a01b03821682036104735760a43580151581036104735760c4358015158103610473576101406040525f60805260a0966100c4610709565b885260c0946100d1610709565b86525f60e0818152610100829052610120919091526040516370a0823160e01b81526001600160a01b038981166004830152919991602090829060249082908b165afa9081156105d7575f91610685575b50608052604051636eb1769f60e11b81526001600160a01b0389811660048301529182166024820152838216151591602090829060449082908b165afa9081156105d7575f91610653575b50811561064a57604051636eb1769f60e11b81526001600160a01b03808b16600483015285166024820152602081806044810103816001600160a01b038c165afa80156105d7575f90610616575b6101f491505b604051636eb1769f60e11b81526001600160a01b03808d1660048301528616602482015291602090839081906044820190565b03816001600160a01b038d165afa9182156105d7575f926105e2575b506040519261021e846106cb565b8352602083015260408201528a5261050f575b5050604051623f675f60e91b81526001600160a01b0386811660048301526020908290602490829088165afa5f91816104db575b506104cd575b50604051624b894760e91b81526001600160a01b0386811660048301526020908290602490829088165afa5f91816104ac575b5061048957501561047f576102b760015b610120610752565b610351575b5065ffffffffffff91506040908180519560805187525180516020880152602081015182880152015160608601525160018060a01b0381511660808601528260208201511660a086015201511660c083015251151560e0820152608080015161010082015260a06080015190600382101561033d5761014091610120820152f35b634e487b7160e01b5f52602160045260245ffd5b5f6101205260405163650369bf60e01b815290602090829060049082906001600160a01b03165afa5f918161042d575b509065ffffffffffff936040939261039c575b5050906102bc565b8351633af32abf60e01b81526001600160a01b0391821660048201529160209183916024918391165afa5f91816103fc575b506103da575b80610394565b156103f2576103ec6002610120610752565b5f6103d4565b6103ec60016102af565b61041f91925060203d602011610426575b61041781836106e7565b81019061073a565b905f6103ce565b503d61040d565b909291506020813d602011610477575b8161044a602093836106e7565b810103126104735751916001600160a01b03831683036104735790919065ffffffffffff610381565b5f80fd5b3d915061043d565b6102b760026102af565b1590506104a25761049d6002610120610752565b6102b7565b61049d60016102af565b6104c691925060203d6020116104265761041781836106e7565b905f61029e565b60018752610100525f61026b565b9091506020813d602011610507575b816104f7602093836106e7565b810103126104735751905f610265565b3d91506104ea565b60405163927da10560e01b81526001600160a01b038881166004830152868116602483015291821660448201529160609183916064918391165afa9081156105d7575f91610562575b5084525f80610231565b90506060813d6060116105cf575b8161057d606093836106e7565b810103126104735760405190610592826106cb565b80516001600160a01b0381168103610473576105c49160409184526105b960208201610727565b602085015201610727565b60408201525f610558565b3d9150610570565b6040513d5f823e3d90fd5b9091506020813d60201161060e575b816105fe602093836106e7565b810103126104735751905f610210565b3d91506105f1565b506020813d602011610642575b81610630602093836106e7565b81010312610473576101f490516101bb565b3d9150610623565b6101f45f6101c1565b90506020813d60201161067d575b8161066e602093836106e7565b8101031261047357515f61016d565b3d9150610661565b90506020813d6020116106af575b816106a0602093836106e7565b8101031261047357515f610122565b3d9150610693565b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff8211176106b757604052565b90601f8019910116810190811067ffffffffffffffff8211176106b757604052565b60405190610716826106cb565b5f6040838281528260208201520152565b519065ffffffffffff8216820361047357565b90816020910312610473575180151581036104735790565b600382101561033d575256fea26469706673582212208fbc642a6a063550b533e25297d2425360449477f7c40e84e4179d222f687dac64736f6c63430008230033"; diff --git a/packages/blue-sdk-viem/src/queries/GetMarket.ts b/packages/blue-sdk-viem/src/queries/GetMarket.ts index d4779c9ee..e9fe8f163 100644 --- a/packages/blue-sdk-viem/src/queries/GetMarket.ts +++ b/packages/blue-sdk-viem/src/queries/GetMarket.ts @@ -119,4 +119,4 @@ export const abi = [ /** @internal Deployless `GetMarket` query bytecode. */ export const code = - "0x608080604052346015576104f8908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63d8f172c414610025575f80fd5b34610285576060366003190112610285576004356001600160a01b0381169190829003610285576044356001600160a01b0381169290602435908490036102855761006f8361042c565b60405161007b8161042c565b5f81525f60208201525f60408201525f60608201525f60808201528352602083016040516100a88161045c565b5f81525f60208201525f60408201525f60608201525f60808201525f60a0820152815260408401905f825260608501925f845260808601945f8652604051632c3c915760e01b815282600482015260a081602481855afa908115610291575f9161039f575b5060249160c091895260405192838092632e3071cd60e11b82528660048301525afa908115610291575f91610302575b5082528551604001516001600160a01b03168061029c575b508551606001516001600160a01b0316871461021e575b5060408051955180516001600160a01b0390811688526020808301518216818a015282840151821689850152606080840151909216828a015260809283015189840152935180516001600160801b0390811660a08b81019190915295820151811660c08b015293810151841660e08a0152908101518316610100890152908101518216610120880152909101511661014085015251151561016084015251610180830152516101a08201526101c09150f35b6020906024604051809981936301977b5760e01b835260048301525afa958615610291575f96610258575b509483526101c09460a061016c565b95506020863d602011610289575b8161027360209383610478565b810103126102855794519460a0610249565b5f80fd5b3d9150610266565b6040513d5f823e3d90fd5b60206004916040519283809263501ad8ff60e11b82525afa5f91816102ce575b5015610155576001845284525f610155565b9091506020813d6020116102fa575b816102ea60209383610478565b810103126102855751905f6102bc565b3d91506102dd565b905060c0813d60c011610397575b8161031d60c09383610478565b810103126102855761038c60a0604051926103378461045c565b610340816104ae565b845261034e602082016104ae565b602085015261035f604082016104ae565b6040850152610370606082016104ae565b6060850152610381608082016104ae565b6080850152016104ae565b60a08201525f61013d565b3d9150610310565b905060a0813d60a011610424575b816103ba60a09383610478565b810103126102855760249160c0916080604051916103d78361042c565b6103e08161049a565b83526103ee6020820161049a565b60208401526103ff6040820161049a565b60408401526104106060820161049a565b60608401520151608082015291509161010d565b3d91506103ad565b60a0810190811067ffffffffffffffff82111761044857604052565b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff82111761044857604052565b90601f8019910116810190811067ffffffffffffffff82111761044857604052565b51906001600160a01b038216820361028557565b51906001600160801b03821682036102855756fea2646970667358221220b6ade9a1ccfe49ca6800384146bc973387a5593355ce6b644f37a62beabd7be964736f6c63430008240033"; + "0x608080604052346015576104f8908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63d8f172c414610025575f80fd5b34610285576060366003190112610285576004356001600160a01b0381169190829003610285576044356001600160a01b0381169290602435908490036102855761006f8361042c565b60405161007b8161042c565b5f81525f60208201525f60408201525f60608201525f60808201528352602083016040516100a88161045c565b5f81525f60208201525f60408201525f60608201525f60808201525f60a0820152815260408401905f825260608501925f845260808601945f8652604051632c3c915760e01b815282600482015260a081602481855afa908115610291575f9161039f575b5060249160c091895260405192838092632e3071cd60e11b82528660048301525afa908115610291575f91610302575b5082528551604001516001600160a01b03168061029c575b508551606001516001600160a01b0316871461021e575b5060408051955180516001600160a01b0390811688526020808301518216818a015282840151821689850152606080840151909216828a015260809283015189840152935180516001600160801b0390811660a08b81019190915295820151811660c08b015293810151841660e08a0152908101518316610100890152908101518216610120880152909101511661014085015251151561016084015251610180830152516101a08201526101c09150f35b6020906024604051809981936301977b5760e01b835260048301525afa958615610291575f96610258575b509483526101c09460a061016c565b95506020863d602011610289575b8161027360209383610478565b810103126102855794519460a0610249565b5f80fd5b3d9150610266565b6040513d5f823e3d90fd5b60206004916040519283809263501ad8ff60e11b82525afa5f91816102ce575b5015610155576001845284525f610155565b9091506020813d6020116102fa575b816102ea60209383610478565b810103126102855751905f6102bc565b3d91506102dd565b905060c0813d60c011610397575b8161031d60c09383610478565b810103126102855761038c60a0604051926103378461045c565b610340816104ae565b845261034e602082016104ae565b602085015261035f604082016104ae565b6040850152610370606082016104ae565b6060850152610381608082016104ae565b6080850152016104ae565b60a08201525f61013d565b3d9150610310565b905060a0813d60a011610424575b816103ba60a09383610478565b810103126102855760249160c0916080604051916103d78361042c565b6103e08161049a565b83526103ee6020820161049a565b60208401526103ff6040820161049a565b60408401526104106060820161049a565b60608401520151608082015291509161010d565b3d91506103ad565b60a0810190811067ffffffffffffffff82111761044857604052565b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff82111761044857604052565b90601f8019910116810190811067ffffffffffffffff82111761044857604052565b51906001600160a01b038216820361028557565b51906001600160801b03821682036102855756fea2646970667358221220acbd98f027aaca3ed2f90675c4eece5d7bd1a9fbbbb62956dae5a7491ebc745564736f6c634300081b0033"; diff --git a/packages/blue-sdk-viem/src/queries/GetToken.ts b/packages/blue-sdk-viem/src/queries/GetToken.ts index 05c028ef5..4730e6bad 100644 --- a/packages/blue-sdk-viem/src/queries/GetToken.ts +++ b/packages/blue-sdk-viem/src/queries/GetToken.ts @@ -107,4 +107,4 @@ export const abi = [ /** @internal Deployless `GetToken` query bytecode. */ export const code = - "0x608080604052346015576108b5908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63287861f914610025575f80fd5b346102cd5760403660031901126102cd576004356001600160a01b03811691908290036102cd576024359182151583036102cd57610100820182811067ffffffffffffffff821117610307576040525f8252602082015f8152604083019160608352606084015f8152608085016060815260a08601945f865260c08701946100ab61037d565b865260e08801985f8a526100dd6040516306fdde0360e01b6020820152600481526100d760248261035b565b87610444565b906102fb575b506040516395d89b4160e01b602082015260048152610107906100d760248261035b565b906102ef575b5060405163313ce56760e01b6020820152600481526101379061013160248261035b565b87610579565b906102e4575b5061026d575b90610185916101546101a0966105cb565b90610261575b506040519860208a525160208a0152511515604089015251610100606089015261012088019061031b565b91511515608087015251858203601f190160a087015261031b565b915160c08401525192601f198383030160e084015260ff60f81b845116825260c06101ef6101dd602087015160e0602087015260e086019061031b565b6040870151858203604087015261031b565b946060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c08186039101526020808351958681520192015f945b8086106102495750508293505115156101008301520390f35b90926020806001928651815201940195019490610230565b60018b5287525f61015a565b6040516301afd7c160e11b815294602086600481845afa9586156102d9575f9661029b575b50948752610143565b955091906020863d6020116102d1575b816102b86020938361035b565b810103126102cd579451949091610154610292565b5f80fd5b3d91506102ab565b6040513d5f823e3d90fd5b60ff1689525f61013d565b6001835283525f61010d565b6001865284525f6100e3565b634e487b7160e01b5f52604160045260245ffd5b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b60e0810190811067ffffffffffffffff82111761030757604052565b90601f8019910116810190811067ffffffffffffffff82111761030757604052565b6040519061038a8261033f565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b67ffffffffffffffff811161030757601f01601f191660200190565b3d156103f9573d906103e0826103b3565b916103ee604051938461035b565b82523d5f602084013e565b606090565b81601f820112156102cd57805190610415826103b3565b92610423604051948561035b565b828452602083830101116102cd57815f9260208093018386015e8301015290565b5f918291602082519201905afa6104596103cf565b90156105285761046881610772565b61053f5760208151036105285760200151905f5b602081108061050c575b156104935760010161047c565b9161049d836103b3565b926104ab604051948561035b565b808452601f196104ba826103b3565b013660208601375f5b8181106104d35750505060019190565b60208110156104f85784518110156104f85780836001921a60208288010153016104c3565b634e487b7160e01b5f52603260045260245ffd5b156104f85782811a60f81b6001600160f81b0319161515610486565b505f9060405161053960208261035b565b5f815290565b80518101906020818303126102cd5760208101519167ffffffffffffffff83116102cd576105749260208092019201016103fe565b600191565b5f918291602082519201905afa61058e6103cf565b901580156105bf575b6105b857602001519060ff82116105b15760ff6001921690565b5f91508190565b505f905f90565b50602081511415610597565b5f806105d561037d565b9260405160208101906342580cb760e11b8252600481526105f760248261035b565b51915afa906106046103cf565b91158015610762575b61075b57508051810160e082602083019203126102cd5760208201516001600160f81b0319811692908390036102cd57604081015167ffffffffffffffff81116102cd57826020610660928401016103fe565b606082015167ffffffffffffffff81116102cd57836020610683928501016103fe565b608083015160a08401516001600160a01b03811693919291908490036102cd5760c08501519460e08101519067ffffffffffffffff82116102cd57019580603f880112156102cd5760208701519667ffffffffffffffff8811610307578760051b90604051986106f6602084018b61035b565b8952602080808b0193830101019283116102cd57604001905b82821061074b57505050604051966107268861033f565b8752602087015260408601526060850152608084015260a083015260c0820152600191565b815181526020918201910161070f565b5f92909150565b5061076c8261079f565b1561060d565b604081511061079a5760208101516020810361079457610791916107fd565b90565b50505f90565b505f90565b60e081511061079a57602081015160081b61079a5760a081015160a01c61079a57604081015160608201516107d960e08401519284610823565b156107f6576107e89083610823565b156107945761079191610832565b5050505f90565b80519061080c6020848461085b565b156107f657820160200151919003601f1901101590565b80519061080c60e0848461085b565b80519061084160e0848461085b565b156107f657820160200151919003601f190160051c101590565b909182108015610873575b61079457601f1901101590565b50601f8216151561086656fea264697066735822122001e988100482fa1cba104f82ee731d109995554ba9278f9f8954f6cd9bd6d46f64736f6c63430008240033"; + "0x608080604052346015576108b5908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63287861f914610025575f80fd5b346102cd5760403660031901126102cd576004356001600160a01b03811691908290036102cd576024359182151583036102cd57610100820182811067ffffffffffffffff821117610307576040525f8252602082015f8152604083019160608352606084015f8152608085016060815260a08601945f865260c08701946100ab61037d565b865260e08801985f8a526100dd6040516306fdde0360e01b6020820152600481526100d760248261035b565b87610444565b906102fb575b506040516395d89b4160e01b602082015260048152610107906100d760248261035b565b906102ef575b5060405163313ce56760e01b6020820152600481526101379061013160248261035b565b87610579565b906102e4575b5061026d575b90610185916101546101a0966105cb565b90610261575b506040519860208a525160208a0152511515604089015251610100606089015261012088019061031b565b91511515608087015251858203601f190160a087015261031b565b915160c08401525192601f198383030160e084015260ff60f81b845116825260c06101ef6101dd602087015160e0602087015260e086019061031b565b6040870151858203604087015261031b565b946060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c08186039101526020808351958681520192015f945b8086106102495750508293505115156101008301520390f35b90926020806001928651815201940195019490610230565b60018b5287525f61015a565b6040516301afd7c160e11b815294602086600481845afa9586156102d9575f9661029b575b50948752610143565b955091906020863d6020116102d1575b816102b86020938361035b565b810103126102cd579451949091610154610292565b5f80fd5b3d91506102ab565b6040513d5f823e3d90fd5b60ff1689525f61013d565b6001835283525f61010d565b6001865284525f6100e3565b634e487b7160e01b5f52604160045260245ffd5b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b60e0810190811067ffffffffffffffff82111761030757604052565b90601f8019910116810190811067ffffffffffffffff82111761030757604052565b6040519061038a8261033f565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b67ffffffffffffffff811161030757601f01601f191660200190565b3d156103f9573d906103e0826103b3565b916103ee604051938461035b565b82523d5f602084013e565b606090565b81601f820112156102cd57805190610415826103b3565b92610423604051948561035b565b828452602083830101116102cd57815f9260208093018386015e8301015290565b5f918291602082519201905afa6104596103cf565b90156105285761046881610772565b61053f5760208151036105285760200151905f5b602081108061050c575b156104935760010161047c565b9161049d836103b3565b926104ab604051948561035b565b808452601f196104ba826103b3565b013660208601375f5b8181106104d35750505060019190565b60208110156104f85784518110156104f85780836001921a60208288010153016104c3565b634e487b7160e01b5f52603260045260245ffd5b156104f85782811a60f81b6001600160f81b0319161515610486565b505f9060405161053960208261035b565b5f815290565b80518101906020818303126102cd5760208101519167ffffffffffffffff83116102cd576105749260208092019201016103fe565b600191565b5f918291602082519201905afa61058e6103cf565b901580156105bf575b6105b857602001519060ff82116105b15760ff6001921690565b5f91508190565b505f905f90565b50602081511415610597565b5f806105d561037d565b9260405160208101906342580cb760e11b8252600481526105f760248261035b565b51915afa906106046103cf565b91158015610762575b61075b57508051810160e082602083019203126102cd5760208201516001600160f81b0319811692908390036102cd57604081015167ffffffffffffffff81116102cd57826020610660928401016103fe565b606082015167ffffffffffffffff81116102cd57836020610683928501016103fe565b608083015160a08401516001600160a01b03811693919291908490036102cd5760c08501519460e08101519067ffffffffffffffff82116102cd57019580603f880112156102cd5760208701519667ffffffffffffffff8811610307578760051b90604051986106f6602084018b61035b565b8952602080808b0193830101019283116102cd57604001905b82821061074b57505050604051966107268861033f565b8752602087015260408601526060850152608084015260a083015260c0820152600191565b815181526020918201910161070f565b5f92909150565b5061076c8261079f565b1561060d565b604081511061079a5760208101516020810361079457610791916107fd565b90565b50505f90565b505f90565b60e081511061079a57602081015160081b61079a5760a081015160a01c61079a57604081015160608201516107d960e08401519284610823565b156107f6576107e89083610823565b156107945761079191610832565b5050505f90565b80519061080c6020848461085b565b156107f657820160200151919003601f1901101590565b80519061080c60e0848461085b565b80519061084160e0848461085b565b156107f657820160200151919003601f190160051c101590565b909182108015610873575b61079457601f1901101590565b50601f8216151561086656fea2646970667358221220435fb6a1dbeb66dccc928b0ed9c2dec64283566ad336124d8fa31e5ecf03b89464736f6c634300081b0033"; diff --git a/packages/blue-sdk-viem/src/queries/GetVault.ts b/packages/blue-sdk-viem/src/queries/GetVault.ts index 87846db97..892d776f8 100644 --- a/packages/blue-sdk-viem/src/queries/GetVault.ts +++ b/packages/blue-sdk-viem/src/queries/GetVault.ts @@ -256,4 +256,4 @@ export const abi = [ /** @internal Deployless `GetVault` query bytecode. */ export const code = - "0x60808060405234601557611638908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63c93eac5414610024575f80fd5b34610bb7576060366003190112610bb7576004356001600160a01b0381168103610bb7576024356001600160a01b0381168103610bb7576044356001600160a01b0381168103610bb7576102e0604052604051610080816113bd565b5f815260606020820152606060408201525f60608201525f60808201526100a561144a565b60a08201526080525f6020608001525f6040608001525f6060608001525f60808001526040516100d48161140e565b5f8082526020820152610120526040516100ed8161140e565b5f80825260208201819052610140919091526101608190526101808190526101a08190526101c08190526101e08190526102008190526102208190526102408190526102605260606102808190526102a05260405161014b816113d8565b5f81525f60208201525f604082015261024060800152604051630a6d4d4b60e21b815260018060a01b038416600482015260208160248160018060a01b0386165afa908115610bc3575f91611333575b5015611281575b506040516338d52e0f60e01b81526020816004816001600160a01b0387165afa908115610bc3575f91611247575b506040516395d89b4160e01b81525f816004816001600160a01b0388165afa908115610bc3575f9161122d575b506040516306fdde0360e01b81525f816004816001600160a01b0389165afa908115610bc3575f9161120b575b5060405163313ce56760e01b8152906020826004816001600160a01b038a165afa918215610bc3575f926111ea575b50604051632ba9c2b360e21b8152926020846004816001600160a01b038b165afa938415610bc3575f946111b9575b5061029161144a565b505f8060405160208101906342580cb760e11b8252600481526102b5602482611429565b51906001600160a01b038b165afa6102cb611562565b901561117e57805181019060e08160208401930312610bb75760208101516001600160f81b031981168103610bb75760408201516001600160401b038111610bb75783602061031c928501016114c7565b60608301516001600160401b038111610bb75784602061033e928601016114c7565b608084015160a0850151939092906001600160a01b0385168503610bb75760c08601519560e0810151976001600160401b038911610bb75780603f8a8401011215610bb757602089830101519161039483611591565b996103a26040519b8c611429565b838b5260208b01926040838301600587901b010111610bb757604081830101925b6040838301600587901b0101841061116e575050505050926103f69a98959260ff9a9794928b9996936040519d8e6113f3565b8a60f81b168d5260208d015260408c015260608b015260018060a01b031660808a015260a089015260c088015260405197610430896113bd565b60018060a01b031688526020880152604087015216606085015216608083015260a0820152608052604051638da5cb5b60e01b815260208160048160018060a01b0387165afa908115610bc3575f91611134575b506001600160a01b0390811660a05260405163e66f53b760e01b8152906020908290600490829087165afa908115610bc3575f916110fa575b506001600160a01b0390811660c052604051630229549960e51b8152906020908290600490829087165afa908115610bc3575f916110c0575b506001600160a01b0390811660e0526040516334cc866d60e21b8152906020908290600490829087165afa908115610bc3575f9161108e575b506101005260408051637cc4d9a160e01b815290816004816001600160a01b0387165afa908115610bc3575f9161102b575b506101205260408051633b1618dd60e11b815290816004816001600160a01b0387165afa908115610bc3575f91610fd2575b5061014052604051631c61872f60e31b81526020816004816001600160a01b0387165afa908115610bc3575f91610f98575b506001600160a01b039081166101605260405163ddca3f4360e01b8152906020908290600490829087165afa8015610bc3575f90610f4f575b6001600160601b0316610180525060405163011a412160e61b81526020816004816001600160a01b0387165afa908115610bc3575f91610f15575b506001600160a01b039081166101a05260405163388af5b560e01b8152906020908290600490829087165afa908115610bc3575f91610edb575b506001600160a01b039081166101c0526040516318160ddd60e01b8152906020908290600490829087165afa908115610bc3575f91610ea9575b506101e0526040516278744560e21b81526020816004816001600160a01b0387165afa908115610bc3575f91610e77575b506102005260405163568efc0760e01b81526020816004816001600160a01b0387165afa908115610bc3575f91610e45575b5061022052604051630872d2c560e21b60208201908152600482525f9182919061073b602482611429565b51906001600160a01b0386165afa610751611562565b9080610e39575b610e14575b50604051630a17b31360e41b81526020816004816001600160a01b0387165afa908115610bc3575f91610de2575b50610795816115a8565b610280525f5b818110610d615750506040516333f91ebb60e01b81526020816004816001600160a01b0387165afa908115610bc3575f91610d2f575b506107db816115a8565b6102a0525f5b818110610cae5750506001600160a01b038116151580610c3f575b610aaa575b505060405160208152806080516102e0602083015260018060a01b0381511661030083015260a061085e610846602084015160c06103208701526103c0860190611366565b60408401518582036102ff1901610340870152611366565b916060810151610360850152608081015161038085015201516102ff19838303016103a084015260ff60f81b815116825260c06108bf6108ad602084015160e0602087015260e0860190611366565b60408401518582036040870152611366565b916060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c0818303910152602080835192838152019201905f5b818110610a9157505050610a4b610a619160018060a01b0360206080015116604085015260018060a01b0360406080015116606085015260018060a01b03606060800151166080850152608080015160a08501526001600160401b03602060a06080015160018060c01b0381511660c088015201511660e08501526001600160401b03602060c06080015160018060a01b0381511661010088015201511661012085015260018060a01b0360e060800151166101408501526101006080015161016085015260018060a01b03610120608001511661018085015260018060a01b0361014060800151166101a0850152610160608001516101c0850152610180608001516101e08501526101a0608001516102008501526101c06080015115156102208501526101e06080015161024085015261020060800151601f198583030161026086015261138a565b6102a051838203601f190161028085015261138a565b6102c0805180516001600160a01b03166102a0850152602081015191840191909152604001516102e08301520390f35b8251845285945060209384019390920191600101610900565b604051630c7508df60e31b81526001600160a01b0380841660048301529092906020908490602490829086165afa928315610bc3575f93610c03575b50604051636fcca69b60e01b81526001600160a01b0380831660048301529091906020908390602490829087165afa918215610bc3575f92610bce575b506040516348d88a5960e11b81526001600160a01b0391821660048201529260209184916024918391165afa918215610bc3575f92610b8b575b5060405192610b6b846113d8565b6001600160a01b03168352602083015260408201526102c0525f80610801565b9091506020813d602011610bbb575b81610ba760209383611429565b81010312610bb75751905f610b5d565b5f80fd5b3d9150610b9a565b6040513d5f823e3d90fd5b9091506020813d602011610bfb575b81610bea60209383611429565b81010312610bb75751906020610b23565b3d9150610bdd565b9092506020813d602011610c37575b81610c1f60209383611429565b81010312610bb757610c3090611498565b915f610ae6565b3d9150610c12565b506040516326f6f90760e11b81526001600160a01b0382811660048301526020908290602490829087165afa908115610bc3575f91610c7f575b506107fc565b610ca1915060203d602011610ca7575b610c998183611429565b810190611480565b5f610c79565b503d610c8f565b6040516362518ddf60e01b815260048101829052906020826024816001600160a01b0389165afa8015610bc3575f90610cfd575b60019250610cf682610220608001516115da565b52016107e1565b506020823d8211610d27575b81610d1660209383611429565b81010312610bb75760019151610ce2565b3d9150610d09565b90506020813d602011610d59575b81610d4a60209383611429565b81010312610bb757515f6107d1565b3d9150610d3d565b60405163f7d1852160e01b815260048101829052906020826024816001600160a01b0389165afa8015610bc3575f90610db0575b60019250610da982610200608001516115da565b520161079b565b506020823d8211610dda575b81610dc960209383611429565b81010312610bb75760019151610d95565b3d9150610dbc565b90506020813d602011610e0c575b81610dfd60209383611429565b81010312610bb757515f61078b565b3d9150610df0565b60016102405280516020828101929182019190910312610bb75751610260525f61075d565b50602081511015610758565b90506020813d602011610e6f575b81610e6060209383611429565b81010312610bb757515f610710565b3d9150610e53565b90506020813d602011610ea1575b81610e9260209383611429565b81010312610bb757515f6106de565b3d9150610e85565b90506020813d602011610ed3575b81610ec460209383611429565b81010312610bb757515f6106ad565b3d9150610eb7565b90506020813d602011610f0d575b81610ef660209383611429565b81010312610bb757610f0790611498565b5f610673565b3d9150610ee9565b90506020813d602011610f47575b81610f3060209383611429565b81010312610bb757610f4190611498565b5f610639565b3d9150610f23565b506020813d602011610f90575b81610f6960209383611429565b81010312610bb757516001600160601b0381168103610bb7576001600160601b03906105fe565b3d9150610f5c565b90506020813d602011610fca575b81610fb360209383611429565b81010312610bb757610fc490611498565b5f6105c5565b3d9150610fa6565b90506040813d604011611023575b81610fed60409383611429565b81010312610bb7576110186020604051926110078461140e565b61101081611498565b84520161154e565b60208201525f610593565b3d9150610fe0565b90506040813d604011611086575b8161104660409383611429565b81010312610bb7576040519061105b8261140e565b80516001600160c01b0381168103610bb757825261107b9060200161154e565b60208201525f610561565b3d9150611039565b90506020813d6020116110b8575b816110a960209383611429565b81010312610bb757515f61052f565b3d915061109c565b90506020813d6020116110f2575b816110db60209383611429565b81010312610bb7576110ec90611498565b5f6104f6565b3d91506110ce565b90506020813d60201161112c575b8161111560209383611429565b81010312610bb75761112690611498565b5f6104bd565b3d9150611108565b90506020813d602011611166575b8161114f60209383611429565b81010312610bb75761116090611498565b5f610484565b3d9150611142565b83518152602093840193016103c3565b60405162461bcd60e51b8152602060048201526013602482015272195a5c0dcc4c911bdb585a5b8819985a5b1959606a1b6044820152606490fd5b6111dc91945060203d6020116111e3575b6111d48183611429565b810190611535565b925f610288565b503d6111ca565b61120491925060203d6020116111e3576111d48183611429565b905f610259565b61122791503d805f833e61121f8183611429565b81019061150d565b5f61022a565b61124191503d805f833e61121f8183611429565b5f6101fd565b90506020813d602011611279575b8161126260209383611429565b81010312610bb75761127390611498565b5f6101d0565b3d9150611255565b600146148015611328575b806112bb575b6101a25763634ba39d60e11b5f9081526001600160a01b03918216600452921660245250604490fd5b50604051630a6d4d4b60e21b81526001600160a01b038416600482015260208160248173a9c3d3a366466fa809d1ae982fb2c46e5fc411015afa908115610bc3575f91611309575b50611292565b611322915060203d602011610ca757610c998183611429565b5f611303565b50612105461461128c565b61134c915060203d602011610ca757610c998183611429565b5f61019b565b634e487b7160e01b5f52604160045260245ffd5b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106113a75750505090565b825184526020938401939092019160010161139a565b60c081019081106001600160401b0382111761135257604052565b606081019081106001600160401b0382111761135257604052565b60e081019081106001600160401b0382111761135257604052565b604081019081106001600160401b0382111761135257604052565b90601f801991011681019081106001600160401b0382111761135257604052565b60405190611457826113f3565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b90816020910312610bb757518015158103610bb75790565b51906001600160a01b0382168203610bb757565b6001600160401b03811161135257601f01601f191660200190565b81601f82011215610bb7578051906114de826114ac565b926114ec6040519485611429565b82845260208383010111610bb757815f9260208093018386015e8301015290565b90602082820312610bb75781516001600160401b038111610bb75761153292016114c7565b90565b90816020910312610bb7575160ff81168103610bb75790565b51906001600160401b0382168203610bb757565b3d1561158c573d90611573826114ac565b916115816040519384611429565b82523d5f602084013e565b606090565b6001600160401b0381116113525760051b60200190565b906115b282611591565b6115bf6040519182611429565b82815280926115d0601f1991611591565b0190602036910137565b80518210156115ee5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea264697066735822122098628d5ddcc8838da003b780c240384c1be843b6d03b8205462eb3e49247d83864736f6c63430008240033"; + "0x60808060405234601557611638908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63c93eac5414610024575f80fd5b34610bb7576060366003190112610bb7576004356001600160a01b0381168103610bb7576024356001600160a01b0381168103610bb7576044356001600160a01b0381168103610bb7576102e0604052604051610080816113bd565b5f815260606020820152606060408201525f60608201525f60808201526100a561144a565b60a08201526080525f6020608001525f6040608001525f6060608001525f60808001526040516100d48161140e565b5f8082526020820152610120526040516100ed8161140e565b5f80825260208201819052610140919091526101608190526101808190526101a08190526101c08190526101e08190526102008190526102208190526102408190526102605260606102808190526102a05260405161014b816113d8565b5f81525f60208201525f604082015261024060800152604051630a6d4d4b60e21b815260018060a01b038416600482015260208160248160018060a01b0386165afa908115610bc3575f91611333575b5015611281575b506040516338d52e0f60e01b81526020816004816001600160a01b0387165afa908115610bc3575f91611247575b506040516395d89b4160e01b81525f816004816001600160a01b0388165afa908115610bc3575f9161122d575b506040516306fdde0360e01b81525f816004816001600160a01b0389165afa908115610bc3575f9161120b575b5060405163313ce56760e01b8152906020826004816001600160a01b038a165afa918215610bc3575f926111ea575b50604051632ba9c2b360e21b8152926020846004816001600160a01b038b165afa938415610bc3575f946111b9575b5061029161144a565b505f8060405160208101906342580cb760e11b8252600481526102b5602482611429565b51906001600160a01b038b165afa6102cb611562565b901561117e57805181019060e08160208401930312610bb75760208101516001600160f81b031981168103610bb75760408201516001600160401b038111610bb75783602061031c928501016114c7565b60608301516001600160401b038111610bb75784602061033e928601016114c7565b608084015160a0850151939092906001600160a01b0385168503610bb75760c08601519560e0810151976001600160401b038911610bb75780603f8a8401011215610bb757602089830101519161039483611591565b996103a26040519b8c611429565b838b5260208b01926040838301600587901b010111610bb757604081830101925b6040838301600587901b0101841061116e575050505050926103f69a98959260ff9a9794928b9996936040519d8e6113f3565b8a60f81b168d5260208d015260408c015260608b015260018060a01b031660808a015260a089015260c088015260405197610430896113bd565b60018060a01b031688526020880152604087015216606085015216608083015260a0820152608052604051638da5cb5b60e01b815260208160048160018060a01b0387165afa908115610bc3575f91611134575b506001600160a01b0390811660a05260405163e66f53b760e01b8152906020908290600490829087165afa908115610bc3575f916110fa575b506001600160a01b0390811660c052604051630229549960e51b8152906020908290600490829087165afa908115610bc3575f916110c0575b506001600160a01b0390811660e0526040516334cc866d60e21b8152906020908290600490829087165afa908115610bc3575f9161108e575b506101005260408051637cc4d9a160e01b815290816004816001600160a01b0387165afa908115610bc3575f9161102b575b506101205260408051633b1618dd60e11b815290816004816001600160a01b0387165afa908115610bc3575f91610fd2575b5061014052604051631c61872f60e31b81526020816004816001600160a01b0387165afa908115610bc3575f91610f98575b506001600160a01b039081166101605260405163ddca3f4360e01b8152906020908290600490829087165afa8015610bc3575f90610f4f575b6001600160601b0316610180525060405163011a412160e61b81526020816004816001600160a01b0387165afa908115610bc3575f91610f15575b506001600160a01b039081166101a05260405163388af5b560e01b8152906020908290600490829087165afa908115610bc3575f91610edb575b506001600160a01b039081166101c0526040516318160ddd60e01b8152906020908290600490829087165afa908115610bc3575f91610ea9575b506101e0526040516278744560e21b81526020816004816001600160a01b0387165afa908115610bc3575f91610e77575b506102005260405163568efc0760e01b81526020816004816001600160a01b0387165afa908115610bc3575f91610e45575b5061022052604051630872d2c560e21b60208201908152600482525f9182919061073b602482611429565b51906001600160a01b0386165afa610751611562565b9080610e39575b610e14575b50604051630a17b31360e41b81526020816004816001600160a01b0387165afa908115610bc3575f91610de2575b50610795816115a8565b610280525f5b818110610d615750506040516333f91ebb60e01b81526020816004816001600160a01b0387165afa908115610bc3575f91610d2f575b506107db816115a8565b6102a0525f5b818110610cae5750506001600160a01b038116151580610c3f575b610aaa575b505060405160208152806080516102e0602083015260018060a01b0381511661030083015260a061085e610846602084015160c06103208701526103c0860190611366565b60408401518582036102ff1901610340870152611366565b916060810151610360850152608081015161038085015201516102ff19838303016103a084015260ff60f81b815116825260c06108bf6108ad602084015160e0602087015260e0860190611366565b60408401518582036040870152611366565b916060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c0818303910152602080835192838152019201905f5b818110610a9157505050610a4b610a619160018060a01b0360206080015116604085015260018060a01b0360406080015116606085015260018060a01b03606060800151166080850152608080015160a08501526001600160401b03602060a06080015160018060c01b0381511660c088015201511660e08501526001600160401b03602060c06080015160018060a01b0381511661010088015201511661012085015260018060a01b0360e060800151166101408501526101006080015161016085015260018060a01b03610120608001511661018085015260018060a01b0361014060800151166101a0850152610160608001516101c0850152610180608001516101e08501526101a0608001516102008501526101c06080015115156102208501526101e06080015161024085015261020060800151601f198583030161026086015261138a565b6102a051838203601f190161028085015261138a565b6102c0805180516001600160a01b03166102a0850152602081015191840191909152604001516102e08301520390f35b8251845285945060209384019390920191600101610900565b604051630c7508df60e31b81526001600160a01b0380841660048301529092906020908490602490829086165afa928315610bc3575f93610c03575b50604051636fcca69b60e01b81526001600160a01b0380831660048301529091906020908390602490829087165afa918215610bc3575f92610bce575b506040516348d88a5960e11b81526001600160a01b0391821660048201529260209184916024918391165afa918215610bc3575f92610b8b575b5060405192610b6b846113d8565b6001600160a01b03168352602083015260408201526102c0525f80610801565b9091506020813d602011610bbb575b81610ba760209383611429565b81010312610bb75751905f610b5d565b5f80fd5b3d9150610b9a565b6040513d5f823e3d90fd5b9091506020813d602011610bfb575b81610bea60209383611429565b81010312610bb75751906020610b23565b3d9150610bdd565b9092506020813d602011610c37575b81610c1f60209383611429565b81010312610bb757610c3090611498565b915f610ae6565b3d9150610c12565b506040516326f6f90760e11b81526001600160a01b0382811660048301526020908290602490829087165afa908115610bc3575f91610c7f575b506107fc565b610ca1915060203d602011610ca7575b610c998183611429565b810190611480565b5f610c79565b503d610c8f565b6040516362518ddf60e01b815260048101829052906020826024816001600160a01b0389165afa8015610bc3575f90610cfd575b60019250610cf682610220608001516115da565b52016107e1565b506020823d8211610d27575b81610d1660209383611429565b81010312610bb75760019151610ce2565b3d9150610d09565b90506020813d602011610d59575b81610d4a60209383611429565b81010312610bb757515f6107d1565b3d9150610d3d565b60405163f7d1852160e01b815260048101829052906020826024816001600160a01b0389165afa8015610bc3575f90610db0575b60019250610da982610200608001516115da565b520161079b565b506020823d8211610dda575b81610dc960209383611429565b81010312610bb75760019151610d95565b3d9150610dbc565b90506020813d602011610e0c575b81610dfd60209383611429565b81010312610bb757515f61078b565b3d9150610df0565b60016102405280516020828101929182019190910312610bb75751610260525f61075d565b50602081511015610758565b90506020813d602011610e6f575b81610e6060209383611429565b81010312610bb757515f610710565b3d9150610e53565b90506020813d602011610ea1575b81610e9260209383611429565b81010312610bb757515f6106de565b3d9150610e85565b90506020813d602011610ed3575b81610ec460209383611429565b81010312610bb757515f6106ad565b3d9150610eb7565b90506020813d602011610f0d575b81610ef660209383611429565b81010312610bb757610f0790611498565b5f610673565b3d9150610ee9565b90506020813d602011610f47575b81610f3060209383611429565b81010312610bb757610f4190611498565b5f610639565b3d9150610f23565b506020813d602011610f90575b81610f6960209383611429565b81010312610bb757516001600160601b0381168103610bb7576001600160601b03906105fe565b3d9150610f5c565b90506020813d602011610fca575b81610fb360209383611429565b81010312610bb757610fc490611498565b5f6105c5565b3d9150610fa6565b90506040813d604011611023575b81610fed60409383611429565b81010312610bb7576110186020604051926110078461140e565b61101081611498565b84520161154e565b60208201525f610593565b3d9150610fe0565b90506040813d604011611086575b8161104660409383611429565b81010312610bb7576040519061105b8261140e565b80516001600160c01b0381168103610bb757825261107b9060200161154e565b60208201525f610561565b3d9150611039565b90506020813d6020116110b8575b816110a960209383611429565b81010312610bb757515f61052f565b3d915061109c565b90506020813d6020116110f2575b816110db60209383611429565b81010312610bb7576110ec90611498565b5f6104f6565b3d91506110ce565b90506020813d60201161112c575b8161111560209383611429565b81010312610bb75761112690611498565b5f6104bd565b3d9150611108565b90506020813d602011611166575b8161114f60209383611429565b81010312610bb75761116090611498565b5f610484565b3d9150611142565b83518152602093840193016103c3565b60405162461bcd60e51b8152602060048201526013602482015272195a5c0dcc4c911bdb585a5b8819985a5b1959606a1b6044820152606490fd5b6111dc91945060203d6020116111e3575b6111d48183611429565b810190611535565b925f610288565b503d6111ca565b61120491925060203d6020116111e3576111d48183611429565b905f610259565b61122791503d805f833e61121f8183611429565b81019061150d565b5f61022a565b61124191503d805f833e61121f8183611429565b5f6101fd565b90506020813d602011611279575b8161126260209383611429565b81010312610bb75761127390611498565b5f6101d0565b3d9150611255565b600146148015611328575b806112bb575b6101a25763634ba39d60e11b5f9081526001600160a01b03918216600452921660245250604490fd5b50604051630a6d4d4b60e21b81526001600160a01b038416600482015260208160248173a9c3d3a366466fa809d1ae982fb2c46e5fc411015afa908115610bc3575f91611309575b50611292565b611322915060203d602011610ca757610c998183611429565b5f611303565b50612105461461128c565b61134c915060203d602011610ca757610c998183611429565b5f61019b565b634e487b7160e01b5f52604160045260245ffd5b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106113a75750505090565b825184526020938401939092019160010161139a565b60c081019081106001600160401b0382111761135257604052565b606081019081106001600160401b0382111761135257604052565b60e081019081106001600160401b0382111761135257604052565b604081019081106001600160401b0382111761135257604052565b90601f801991011681019081106001600160401b0382111761135257604052565b60405190611457826113f3565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b90816020910312610bb757518015158103610bb75790565b51906001600160a01b0382168203610bb757565b6001600160401b03811161135257601f01601f191660200190565b81601f82011215610bb7578051906114de826114ac565b926114ec6040519485611429565b82845260208383010111610bb757815f9260208093018386015e8301015290565b90602082820312610bb75781516001600160401b038111610bb75761153292016114c7565b90565b90816020910312610bb7575160ff81168103610bb75790565b51906001600160401b0382168203610bb757565b3d1561158c573d90611573826114ac565b916115816040519384611429565b82523d5f602084013e565b606090565b6001600160401b0381116113525760051b60200190565b906115b282611591565b6115bf6040519182611429565b82815280926115d0601f1991611591565b0190602036910137565b80518210156115ee5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea26469706673582212208a840402e999073d07e228ab73197725911c16597958760b3632810f70e1ebd464736f6c63430008230033"; diff --git a/packages/blue-sdk-viem/src/queries/GetVaultUser.ts b/packages/blue-sdk-viem/src/queries/GetVaultUser.ts index 5929bd92a..9d4a01e32 100644 --- a/packages/blue-sdk-viem/src/queries/GetVaultUser.ts +++ b/packages/blue-sdk-viem/src/queries/GetVaultUser.ts @@ -40,4 +40,4 @@ export const abi = [ /** @internal Deployless `GetVaultUser` query bytecode. */ export const code = - "0x6080806040523460155761025b908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610156576040366003190112610156576004356001600160a01b0381169190829003610156576024356001600160a01b0381169290839003610156576040820182811067ffffffffffffffff8211176101ef576040525f825260208201905f82526040516326f6f90760e11b8152846004820152602081602481855afa908115610162575f916101b4575b50151583526040516338d52e0f60e01b815293602085600481855afa948515610162575f9561016d575b509060446020926040519687938492636eb1769f60e11b84526004840152602483015260018060a01b03165afa8015610162575f9061012b575b6040935081528251915115158252516020820152f35b506020833d60201161015a575b8161014560209383610203565b810103126101565760409251610115565b5f80fd5b3d9150610138565b6040513d5f823e3d90fd5b9094506020813d6020116101ac575b8161018960209383610203565b810103126101565751906001600160a01b038216820361015657909360446100db565b3d915061017c565b90506020813d6020116101e7575b816101cf60209383610203565b8101031261015657518015158103610156575f6100b1565b3d91506101c2565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff8211176101ef5760405256fea2646970667358221220a5cf3b5661ba130f9f6beb24e7b3e584d9ba455572013775bb30810d2238ce5a64736f6c63430008240033"; + "0x6080806040523460155761025b908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610156576040366003190112610156576004356001600160a01b0381169190829003610156576024356001600160a01b0381169290839003610156576040820182811067ffffffffffffffff8211176101ef576040525f825260208201905f82526040516326f6f90760e11b8152846004820152602081602481855afa908115610162575f916101b4575b50151583526040516338d52e0f60e01b815293602085600481855afa948515610162575f9561016d575b509060446020926040519687938492636eb1769f60e11b84526004840152602483015260018060a01b03165afa8015610162575f9061012b575b6040935081528251915115158252516020820152f35b506020833d60201161015a575b8161014560209383610203565b810103126101565760409251610115565b5f80fd5b3d9150610138565b6040513d5f823e3d90fd5b9094506020813d6020116101ac575b8161018960209383610203565b810103126101565751906001600160a01b038216820361015657909360446100db565b3d915061017c565b90506020813d6020116101e7575b816101cf60209383610203565b8101031261015657518015158103610156575f6100b1565b3d91506101c2565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff8211176101ef5760405256fea2646970667358221220dab07071235db46cf4ce3e33469d528ea840db8713e312abcc6046560cb9b95d64736f6c634300081b0033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetAccrualVaultV2.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetAccrualVaultV2.ts index 6cff6972a..d23f06e5c 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetAccrualVaultV2.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetAccrualVaultV2.ts @@ -1588,4 +1588,4 @@ export const abi = [ /** @internal Deployless `GetAccrualVaultV2` query bytecode. */ export const code = - "0x60808060405234601557613cde908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c630f0d54d814610024575f80fd5b34610afb57610100366003190112610afb576004356001600160a01b0381169003610afb576024356001600160a01b0381168103610afb576044356001600160a01b0381169003610afb576064356001600160a01b0381169003610afb576084356001600160a01b0381169003610afb5760a4356001600160a01b0381169003610afb5760c4356001600160a01b0381169003610afb5760e4356001600160a01b0381169003610afb576103206040526040516100e081611a96565b5f8082526060602083018190526040830181905280830182905260809290925260a081905260c081905260e08190526101008190526101208190526101408190526101608190526101808290526101a08190526101c0919091526101e08190526102008190526102208190526102408190526102608190526102808190526102a08190526102c052610170611cae565b6102e052606061030052604051635edec50d60e01b81526001600160a01b03600480358216908301526020908290602490829086165afa908115610b07575f9161145b575b5015611433576040516338d52e0f60e01b815260208160048181356001600160a01b03165afa908115610b07575f916113f9575b506040516395d89b4160e01b81525f8160048181356001600160a01b03165afa908115610b07575f916113df575b506040516306fdde0360e01b8152905f8260048181356001600160a01b03165afa918215610b07575f926113bb575b5060405163313ce56760e01b81529160208360048181356001600160a01b03165afa918215610b075760ff935f9361138a575b506040519461028786611a96565b60018060a01b03168552602085015260408401521660608201526080526040516338d52e0f60e01b815260208160048160018060a01b038235165afa908115610b07575f91611350575b506001600160a01b0390811660a05260405163ce04bebb60e01b815290602090829060049082908235165afa8015610b07575f90611310575b6001600160801b031660c052506040516318160ddd60e01b815260208160048181356001600160a01b03165afa908115610b07575f916112de575b5060e0526040516331c6651b60e21b815260208160048181356001600160a01b03165afa908115610b07575f916112ac575b506101005260405163ece1d6e560e01b815260208160048181356001600160a01b03165afa8015610b07575f9061126c575b6001600160401b0316610120525060405163c046371160e01b815260208160048181356001600160a01b03165afa8015610b07575f9061122c575b6001600160401b0316610140525060405163ad468d1160e01b815260208160048181356001600160a01b03165afa908115610b07575f916111f2575b506001600160a01b03908116610160526040516305c0524560e31b8152905f90829060049082908235165afa908115610b07575f916111a2575b50610180526040516343bc43c160e11b815260208160048181356001600160a01b03165afa8015610b07576001600160601b03915f91611183575b50166101e05260405163537bfaeb60e11b815260208160048181356001600160a01b03165afa8015610b07576001600160601b03915f91611154575b50166102005260405163ed27f7c960e01b815260208160048181356001600160a01b03165afa908115610b07575f9161111a575b506001600160a01b03908116610220526040516306d9a30160e41b815290602090829060049082908235165afa908115610b07575f916110e0575b506001600160a01b0316610240526101e0516001600160601b0316156110d957610220516040516326326d2760e21b81526001600160a01b039182166004808301919091529091602091839160249183919035165afa908115610b07575f9161109f575b505b151561026052610200516001600160601b03161561109857610240516040516326326d2760e21b81526001600160a01b039182166004808301919091529091602091839160249183919035165afa908115610b07575f9161105e575b505b15156102805260a0516040516370a0823160e01b81526001600160a01b0360048035821690830152909160209183916024918391165afa908115610b07575f9161102c575b506102a0526044356001600160a01b0316151580610fab575b6084356001600160a01b031615159081610f28575b8080610f1b575b610ef857808115610ef1575b15156101a05215610d38575060408051906106a58183611b38565b600182525f5b601f1982018110610cf55750506101406080015261073f60018060a01b0360e0608001511660405160208101916040835260046060830152637468697360e01b608083015260408201526080815261070460a082611b38565b5190206040519061071482611a96565b81525f60208201525f60408201525f6060820152610140608001519061073982611e49565b52611e49565b505b6101c051515f5b818110610b8f57610160516001600160a01b031680610b5b575b50604051630b54457960e31b815260208160048181356001600160a01b03165afa908115610b07575f91610b29575b5061079b81611e32565b6107a86040519182611b38565b818152601f196107b783611e32565b015f5b818110610b12575050610300525f5b818110610a425760405160208152806108b96080516102a0602084015260018060a01b038151166102c0840152606061082e610816602084015160806102e0880152610340870190611495565b60408401518682036102bf1901610300880152611495565b91015161032084015260a080516001600160a01b03908116604086015260c080516001600160801b0316606087015260e08051608088015261010080519488019490945261012080516001600160401b03908116938901939093526101405190921690870152610160519091169185019190915261018051848303601f190191850191909152611495565b6101a05115156101408301526101c051828203601f19016101608401528051808352602092830192909101905f5b818110610a085750505061099e906001600160601b0361016060800151166101808401526001600160601b0361018060800151166101a084015260018060a01b036101a060800151166101c084015260018060a01b036101c060800151166101e08401526101e06080015115156102008401526102006080015115156102208401526102206080015161024084015261024060800151151561026084015261026060800151601f19848303016102808501526115ac565b61030051601f19838303016102a084015280518083526020600582901b8401810193928101925f918101905b8383106109d75786860387f35b9193955091936020806109f6600193601f1986820301875289516115ac565b970193019301909286959492936109ca565b91935091602060806001926060875180518352848101518584015260408101516040840152015160608201520194019101918493926108e7565b604051906313bd406b60e21b825280600483015260208260248160018060a01b03600435165afa918215610b07575f92610abe575b50610ab781610aa260019460e4359060c4359060a43590608435906064359060443590600435612014565b6103005190610ab18383611e6a565b52611e6a565b50016107c9565b91506020823d8211610aff575b81610ad860209383611b38565b81010312610afb57610ab781610aa2610af2600195611d2b565b94505050610a77565b5f80fd5b3d9150610acb565b6040513d5f823e3d90fd5b602090610b1d611cae565b828286010152016107ba565b90506020813d602011610b53575b81610b4460209383611b38565b81010312610afb575181610791565b3d9150610b37565b60016102c052610b859060e4359060c4359060a43590608435906064359060443590600435612014565b6102e05280610762565b610b9f8161014060800151611e6a565b5190815160405190632f0374dd60e21b8252600482015260208160248160018060a01b03600435165afa908115610b07575f91610cc4575b50602083015281516040519063a68bafa360e01b8252600482015260208160248160018060a01b03600435165afa908115610b07575f91610c93575b5060408301528151916040519263c69507dd60e01b8452600484015260208360248160018060a01b03600435165afa928315610b07575f93610c5f575b50916060600193015201610748565b92506020833d8211610c8b575b81610c7960209383611b38565b81010312610afb579151916060610c50565b3d9150610c6c565b90506020813d8211610cbc575b81610cad60209383611b38565b81010312610afb575184610c13565b3d9150610ca0565b90506020813d8211610ced575b81610cde60209383611b38565b81010312610afb575184610bd7565b3d9150610cd1565b602090604051610d0481611a96565b5f81525f838201525f60408201525f6060820152828286010152016106ab565b634e487b7160e01b5f52604160045260245ffd5b1561074157610d566101006080015160208082518301019101611e7e565b6101605160405163cc3802bf60e01b8152915f91839160a49183916001600160a01b0390911690610d8b9060048401906114ec565b5afa908115610b07575f91610e61575b508051610da781611e32565b90610db56040519283611b38565b808252610dc4601f1991611e32565b015f5b818110610e325750506101c0525f5b8151811015610e2b5780610e2481610df060019486611e6a565b5160405190610dfe82611a96565b81525f60208201525f60408201525f60608201526101406080015190610ab18383611e6a565b5001610dd6565b5050610741565b602090604051610e4181611a96565b5f81525f838201525f60408201525f606082015282828601015201610dc7565b90503d805f833e610e728183611b38565b810190602081830312610afb578051906001600160401b038211610afb57019080601f83011215610afb578151610ea881611e32565b92610eb66040519485611b38565b81845260208085019260051b820101928311610afb57602001905b828210610ee15750505081610d9b565b8151815260209182019101610ed1565b508161068a565b61016051636364223f60e01b5f9081526001600160a01b03909116600452602490fd5b506101805151151561067e565b610160516040516335abafe560e21b81526001600160a01b03918216600482015291925060209082906024908290608435165afa908115610b07575f91610f71575b5090610677565b90506020813d602011610fa3575b81610f8c60209383611b38565b81010312610afb57610f9d90611d1e565b82610f6a565b3d9150610f7f565b5061016051604051632c77566560e01b81526001600160a01b0391821660048201529060209082906024908290604435165afa908115610b07575f91610ff2575b50610662565b90506020813d602011611024575b8161100d60209383611b38565b81010312610afb5761101e90611d1e565b81610fec565b3d9150611000565b90506020813d602011611056575b8161104760209383611b38565b81010312610afb575181610649565b3d915061103a565b90506020813d602011611090575b8161107960209383611b38565b81010312610afb5761108a90611d1e565b81610602565b3d915061106c565b6001610604565b90506020813d6020116110d1575b816110ba60209383611b38565b81010312610afb576110cb90611d1e565b816105a4565b3d91506110ad565b60016105a6565b90506020813d602011611112575b816110fb60209383611b38565b81010312610afb5761110c90611d2b565b81610540565b3d91506110ee565b90506020813d60201161114c575b8161113560209383611b38565b81010312610afb5761114690611d2b565b81610505565b3d9150611128565b611176915060203d60201161117c575b61116e8183611b38565b810190611e13565b826104d1565b503d611164565b61119c915060203d60201161117c5761116e8183611b38565b82610495565b90503d805f833e6111b38183611b38565b810190602081830312610afb578051906001600160401b038211610afb57019080601f83011215610afb5781516111ec92602001611d5a565b8161045a565b90506020813d602011611224575b8161120d60209383611b38565b81010312610afb5761121e90611d2b565b81610420565b3d9150611200565b506020813d602011611264575b8161124660209383611b38565b81010312610afb5761125f6001600160401b0391611dff565b6103e4565b3d9150611239565b506020813d6020116112a4575b8161128660209383611b38565b81010312610afb5761129f6001600160401b0391611dff565b6103a9565b3d9150611279565b90506020813d6020116112d6575b816112c760209383611b38565b81010312610afb575181610377565b3d91506112ba565b90506020813d602011611308575b816112f960209383611b38565b81010312610afb575181610345565b3d91506112ec565b506020813d602011611348575b8161132a60209383611b38565b81010312610afb576113436001600160801b0391611deb565b61030a565b3d915061131d565b90506020813d602011611382575b8161136b60209383611b38565b81010312610afb5761137c90611d2b565b816102d1565b3d915061135e565b6113ad91935060203d6020116113b4575b6113a58183611b38565b810190611dd2565b9185610279565b503d61139b565b6113d89192503d805f833e6113d08183611b38565b810190611dad565b9083610246565b6113f391503d805f833e6113d08183611b38565b82610217565b90506020813d60201161142b575b8161141460209383611b38565b81010312610afb5761142590611d2b565b816101e9565b3d9150611407565b63634ba39d60e11b5f9081526001600160a01b03918216600490815235909116602452604490fd5b90506020813d60201161148d575b8161147660209383611b38565b81010312610afb5761148790611d1e565b5f6101b5565b3d9150611469565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106114d65750505090565b82518452602093840193909201916001016114c9565b80516001600160a01b03908116835260208083015182169084015260408083015182169084015260608083015190911690830152608090810151910152565b60806101a09161153c8482516114ec565b6001600160801b0360a0602083015182815116828801528260208201511660c08801528260408201511660e0880152826060820151166101008801528285820151166101208801520151166101408501526040810151151561016085015260608101516101808501520151910152565b60018060a01b03815116825260ff602082015116602083015260018060a01b03604082015116604083015260018060a01b0360608201511660608301526080810151608083015260018060a01b0360a08201511660a083015260c08101519061018060c084015281516102e061018085015260018060a01b0381511661046085015260a061166661164e602084015160c0610480890152610520880190611495565b604084015187820361045f19016104a0890152611495565b9160608101516104c087015260808101516104e0870152015161045f198583030161050086015260ff60f81b815116825260c06116c76116b5602084015160e0602087015260e0860190611495565b60408401518582036040870152611495565b916060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c0818303910152602080835192838152019201905f5b818110611a80575050506020838101516001600160a01b039081166101a087015260408581015182166101c088015260608601519091166101e0870152608085015161020087015260a085015180516001600160c01b0316610220880152909101516001600160401b031661024086810191909152909161184c906118339060c087015180516001600160a01b039081166102608b01526020909101516001600160401b03166102808a015260e088015181166102a08a01526101008801516102c08a015261012088015181166102e08a0152610140880151166103008901526101608701516103208901526101808701516103408901526101a087015115156103608901526101c08701516103808901526101e087015161017f19898303016103a08a01526114b9565b61020086015187820361017f19016103c08901526114b9565b9361022081015115156103e0870152015160018060a01b038151166104008601526020810151610420860152015161044084015260e08101519183810360e0850152602080845192838152019301905f5b8181106119b65750505061010081015161010084015261012081015191838103610120850152602080845192838152019301905f5b818110611951575050506101609060018060a01b0361014082015116610140850152015191610160818303910152602080835192838152019201905f5b81811061191c5750505090565b9091926020610200600192611946604088518051845285810151868501520151604083019061152b565b01940192910161190f565b90919360206102c06001926119ab6040895161196e8482516114ec565b61199e8682015160a08601906001600160801b036040809280518552826020820151166020860152015116910152565b015161010083019061152b565b0195019291016118d2565b90919360206103006001926001600160801b0360e0895180518452858101511515868501526001600160401b036040820151166040850152611a1b606082015160608601906001600160401b036020809260018060c01b038151168552015116910152565b611a4c608082015160a08601906001600160801b036040809280518552826020820151166020860152015116910152565b611a5f60a082015161010086019061152b565b60c081015183166102c08501520151166102e082015201950192910161189d565b8251845260209384019390920191600101611708565b608081019081106001600160401b03821117610d2457604052565b60e081019081106001600160401b03821117610d2457604052565b604081019081106001600160401b03821117610d2457604052565b606081019081106001600160401b03821117610d2457604052565b60c081019081106001600160401b03821117610d2457604052565b60a081019081106001600160401b03821117610d2457604052565b90601f801991011681019081106001600160401b03821117610d2457604052565b60405190611b6682611ab1565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b60405190611b9c82611ae7565b5f6040838281528260208201520152565b6040519061026082018281106001600160401b03821117610d245760405281604051611bd881611b02565b5f815260606020820152606060408201525f60608201525f6080820152611bfd611b59565b60a082015281525f60208201525f60408201525f60608201525f6080820152604051611c2881611acc565b5f81525f602082015260a0820152604051611c4281611acc565b5f81525f602082015260c08201525f60e08201525f6101008201525f6101208201525f6101408201525f6101608201525f6101808201525f6101a08201525f6101c082015260606101e082015260606102008201525f610220820152610240611ca9611b8f565b910152565b6040519061018082018281106001600160401b03821117610d24576040526060610160835f81525f60208201525f60408201525f838201525f60808201525f60a0820152611cfa611bad565b60c08201528260e08201525f610100820152826101208201525f6101408201520152565b51908115158203610afb57565b51906001600160a01b0382168203610afb57565b6001600160401b038111610d2457601f01601f191660200190565b929192611d6682611d3f565b91611d746040519384611b38565b829481845281830111610afb578281602093845f96015e010152565b9080601f83011215610afb578151611daa92602001611d5a565b90565b90602082820312610afb5781516001600160401b038111610afb57611daa9201611d90565b90816020910312610afb575160ff81168103610afb5790565b51906001600160801b0382168203610afb57565b51906001600160401b0382168203610afb57565b90816020910312610afb57516001600160601b0381168103610afb5790565b6001600160401b038111610d245760051b60200190565b805115611e565760200190565b634e487b7160e01b5f52603260045260245ffd5b8051821015611e565760209160051b010190565b908160a0910312610afb57608060405191611e9883611b1d565b611ea181611d2b565b8352611eaf60208201611d2b565b6020840152611ec060408201611d2b565b6040840152611ed160608201611d2b565b60608401520151608082015290565b60405190611eed82611b1d565b5f6080838281528260208201528260408201528260608201520152565b60405190611f1782611b1d565b5f608083611f23611ee0565b8152604051611f3181611b02565b83815283602082015283604082015283606082015283838201528360a082015260208201528260408201528260608201520152565b90816060910312610afb57611f9d6040805192611f8284611ae7565b80518452611f9260208201611deb565b602085015201611deb565b604082015290565b6040519061010082018281106001600160401b03821117610d24576040525f60e083828152826020820152826040820152604051611fe281611acc565b8381528360208201526060820152611ff8611b8f565b6080820152612005611f0a565b60a08201528260c08201520152565b95939091979692612023611cae565b6001600160a01b038481168083526040516399e9918360e01b815260048101829052929b909990929160209183916024918391165afa908115610b07575f9161399d575b5060808b01526001600160a01b0316801515908161392e575b50156131dc57505050600160208701526040516307f1b29b60e11b8152602081600481885afa908115610b07575f916131a2575b506001600160a01b03166040878101919091525163388af5b560e01b8152602081600481885afa908115610b07575f91613168575b506001600160a01b0316606087015260405163e4baaddf60e01b815292602084600481885afa938415610b07575f9461312c575b506001600160a01b0390931660a08701818152939061213a611bad565b916040516338d52e0f60e01b8152602081600481865afa908115610b07575f916130f2575b506040516395d89b4160e01b81525f81600481875afa908115610b07575f916130d8575b506040516306fdde0360e01b81525f81600481885afa908115610b07575f916130be575b5060405163313ce56760e01b815290602082600481895afa918215610b07575f9261309d575b50604051632ba9c2b360e21b8152926020846004818a5afa938415610b07575f9461307c575b506121fc611b59565b505f8060405160208101906342580cb760e11b825260048152612220602482611b38565b51908a5afa61222d613c47565b901561304157805181019060e08160208401930312610afb5760208101516001600160f81b0319811690819003610afb5760408201516001600160401b038111610afb5783602061228092850101611d90565b60608301516001600160401b038111610afb578460206122a292860101611d90565b608084015160a08501516001600160a01b0381169491939190859003610afb5760c08601519560e0810151906001600160401b038211610afb57019680603f89011215610afb5760208801516122f781611e32565b986123056040519a8b611b38565b818a52602080808c019360051b83010101928311610afb57604001905b82821061303157505050926123489a98959260ff9a9794928b9996936040519d8e611ab1565b8d5260208d015260408c015260608b015260808a015260a089015260c08801526040519761237589611b02565b60018060a01b031688526020880152604087015216606085015216608083015260a08201528352604051638da5cb5b60e01b8152602081600481865afa908115610b07575f91612ff7575b506001600160a01b031660208481019190915260405163e66f53b760e01b81529081600481865afa908115610b07575f91612fbd575b506001600160a01b031660408481019190915251630229549960e51b8152602081600481865afa908115610b07575f91612f83575b506001600160a01b031660608401526040516334cc866d60e21b8152602081600481865afa908115610b07575f91612f51575b50608084015260408051637cc4d9a160e01b81529081600481865afa908115610b07575f91612f32575b5060a084015260408051633b1618dd60e11b81529081600481865afa908115610b07575f91612ed9575b5060c0840152604051631c61872f60e31b8152602081600481865afa908115610b07575f91612e9f575b506001600160a01b031660e084015260405163ddca3f4360e01b8152602081600481865afa8015610b07576001600160601b03915f91612e80575b501661010084015260405163011a412160e61b8152602081600481865afa908115610b07575f91612e46575b506001600160a01b031661012084015260405163388af5b560e01b8152602081600481865afa908115610b07575f91612e0c575b506001600160a01b03166101408401526040516318160ddd60e01b8152602081600481865afa908115610b07575f91612dda575b5061016084015260405163568efc0760e01b8152602081600481865afa908115610b07575f91612da8575b506101808401525f806040516020810190630872d2c560e21b825260048152612600602482611b38565b5190855afa61260d613c47565b9080612d9c575b612d73575b50604051630a17b31360e41b8152602081600481865afa908115610b07575f91612d41575b5061264881613c76565b6101e085019081525f5b828110612cce5750506040516333f91ebb60e01b8152949050602085600481865afa948515610b07575f95612c9a575b5061268c85613c76565b9461020085019586525f5b818110612c275750506001600160a01b0316801515949092908580612bbb575b612a61575b60c08b019485525151946126cf86611e32565b946126dd6040519687611b38565b868652601f196126ec88611e32565b015f5b818110612a4a57505060e08c019586525f5b87811061278757505096516040516370a0823160e01b8152600481019990995260209750889650602495508694506001600160a01b0316925050505afa908115610b07575f91612755575b50610100830152565b90506020813d60201161277f575b8161277060209383611b38565b81010312610afb57515f61274c565b3d9150612763565b6127978161020084510151611e6a565b51906127a1611fa5565b91604051636638c7bb60e11b81528160048201526060816024818a5afa908115610b07575f916129cd575b5080516001600160b81b031684526020808201511515908501526040908101516001600160401b031684820152805163518df2eb60e11b81526004810183905290816024818a5afa908115610b07575f9161299f575b506060848101919091526040516349e2903160e11b8152600481018390526001600160a01b03881660248201529081806044810103816001600160a01b038c165afa908115610b07575f91612971575b506080840152846128848c838a6139cf565b60a08501526128aa575b506128a3816001938a5190610ab18383611e6a565b5001612701565b9160405192639dbcd5b960e01b845286600485015260248401526040836044818b5afa928315610b07575f93612904575b5082516001600160801b0390811660c083015260209093015190921660e08301526128a361288e565b92506040833d8211612969575b8161291e60409383611b38565b81010312610afb57816128a3916001600160801b03602060019661295a826040519261294984611acc565b61295281611deb565b845201611deb565b828201529650505091506128db565b3d9150612911565b612992915060603d8111612998575b61298a8183611b38565b810190611f66565b5f612872565b503d612980565b6129c0915060403d81116129c6575b6129b88183611b38565b810190613c07565b5f612822565b503d6129ae565b90506060813d8211612a42575b816129e760609383611b38565b81010312610afb576040516129fb81611ae7565b8151906001600160b81b0382168203610afb57612a3760406001600160401b039481948452612a2c60208201611d1e565b602085015201611dff565b8282015291506127cc565b3d91506129da565b602090612a55611fa5565b82828b010152016126ef565b6001610220860152604051630c7508df60e31b815260048101839052602081602481885afa908115610b07575f91612b81575b50604051636fcca69b60e01b815260048101849052602081602481895afa908115610b07575f91612b4f575b506040516348d88a5960e11b815260048101859052906020826024818a5afa918215610b07575f92612b1b575b5060405192612afb84611ae7565b6001600160a01b03168352602083015260408201526102408601526126bc565b9091506020813d602011612b47575b81612b3760209383611b38565b81010312610afb5751905f612aed565b3d9150612b2a565b90506020813d602011612b79575b81612b6a60209383611b38565b81010312610afb57515f612ac0565b3d9150612b5d565b90506020813d602011612bb3575b81612b9c60209383611b38565b81010312610afb57612bad90611d2b565b5f612a94565b3d9150612b8f565b506040516326f6f90760e11b815260048101859052602081602481865afa908115610b07575f91612bed575b506126b7565b90506020813d602011612c1f575b81612c0860209383611b38565b81010312610afb57612c1990611d1e565b5f612be7565b3d9150612bfb565b6040516362518ddf60e01b81526004810182905290602082602481895afa8015610b07575f90612c68575b60019250612c61828a51611e6a565b5201612697565b506020823d8211612c92575b81612c8160209383611b38565b81010312610afb5760019151612c52565b3d9150612c74565b9094506020813d602011612cc6575b81612cb660209383611b38565b81010312610afb5751935f612682565b3d9150612ca9565b60405163f7d1852160e01b81526004810182905290602082602481895afa8015610b07575f90612d0f575b60019250612d08828551611e6a565b5201612652565b506020823d8211612d39575b81612d2860209383611b38565b81010312610afb5760019151612cf9565b3d9150612d1b565b90506020813d602011612d6b575b81612d5c60209383611b38565b81010312610afb57515f61263e565b3d9150612d4f565b60016101a085015260208151918180820193849201010312610afb57516101c08401525f612619565b50602081511015612614565b90506020813d602011612dd2575b81612dc360209383611b38565b81010312610afb57515f6125d6565b3d9150612db6565b90506020813d602011612e04575b81612df560209383611b38565b81010312610afb57515f6125ab565b3d9150612de8565b90506020813d602011612e3e575b81612e2760209383611b38565b81010312610afb57612e3890611d2b565b5f612577565b3d9150612e1a565b90506020813d602011612e78575b81612e6160209383611b38565b81010312610afb57612e7290611d2b565b5f612543565b3d9150612e54565b612e99915060203d60201161117c5761116e8183611b38565b5f612517565b90506020813d602011612ed1575b81612eba60209383611b38565b81010312610afb57612ecb90611d2b565b5f6124dc565b3d9150612ead565b90506040813d604011612f2a575b81612ef460409383611b38565b81010312610afb57612f1f602060405192612f0e84611acc565b612f1781611d2b565b845201611dff565b60208201525f6124b2565b3d9150612ee7565b612f4b915060403d6040116129c6576129b88183611b38565b5f612488565b90506020813d602011612f7b575b81612f6c60209383611b38565b81010312610afb57515f61245e565b3d9150612f5f565b90506020813d602011612fb5575b81612f9e60209383611b38565b81010312610afb57612faf90611d2b565b5f61242b565b3d9150612f91565b90506020813d602011612fef575b81612fd860209383611b38565b81010312610afb57612fe990611d2b565b5f6123f6565b3d9150612fcb565b90506020813d602011613029575b8161301260209383611b38565b81010312610afb5761302390611d2b565b5f6123c0565b3d9150613005565b8151815260209182019101612322565b60405162461bcd60e51b8152602060048201526013602482015272195a5c0dcc4c911bdb585a5b8819985a5b1959606a1b6044820152606490fd5b61309691945060203d6020116113b4576113a58183611b38565b925f6121f3565b6130b791925060203d6020116113b4576113a58183611b38565b905f6121cd565b6130d291503d805f833e6113d08183611b38565b5f6121a7565b6130ec91503d805f833e6113d08183611b38565b5f612183565b90506020813d602011613124575b8161310d60209383611b38565b81010312610afb5761311e90611d2b565b5f61215f565b3d9150613100565b9093506020813d602011613160575b8161314860209383611b38565b81010312610afb5761315990611d2b565b925f61211d565b3d915061313b565b90506020813d60201161319a575b8161318360209383611b38565b81010312610afb5761319490611d2b565b5f6120e9565b3d9150613176565b90506020813d6020116131d4575b816131bd60209383611b38565b81010312610afb576131ce90611d2b565b5f6120b4565b3d91506131b0565b939591949193919250906001600160a01b031680151590816138bf575b50156134f15750600260208701526040516307f1b29b60e11b8152602081600481865afa908115610b07575f916134b7575b506001600160a01b03166040878101919091525163388af5b560e01b8152602081600481865afa908115610b07575f9161347d575b506001600160a01b0316606087015260405163b045ff5b60e01b815292602084600481865afa938415610b07575f94613449575b5061329e84611e32565b926132ac6040519485611b38565b848452601f196132bb86611e32565b015f5b81811061340b57505061012088019384525f5b8581106132e15750505050505050565b604051631f1a892160e11b8152600481018290529060a082602481865afa918215610b07575f926133db575b50604051602081019061332082856114ec565b60a0815261332f60c082611b38565b5190209161333e828851611e6a565b51526040516349e2903160e11b8152600481018390526001600160a01b0385166024820152606081806044810103816001600160a01b038a165afa908115610b07576001936133a7928b925f916133bd575b50602061339e868c51611e6a565b510152876139cf565b60406133b4838951611e6a565b510152016132d1565b6133d5915060603d81116129985761298a8183611b38565b5f613390565b6133fd91925060a03d8111613404575b6133f58183611b38565b810190611e7e565b905f61330d565b503d6133eb565b60209060405161341a81611ae7565b613422611ee0565b815261342c611b8f565b83820152613438611f0a565b6040820152828289010152016132be565b9093506020813d602011613475575b8161346560209383611b38565b81010312610afb5751925f613294565b3d9150613458565b90506020813d6020116134af575b8161349860209383611b38565b81010312610afb576134a990611d2b565b5f613260565b3d915061348b565b90506020813d6020116134e9575b816134d260209383611b38565b81010312610afb576134e390611d2b565b5f61322b565b3d91506134c5565b91939250906001600160a01b03168015159081613850575b501561383d57600360208601526040516307f1b29b60e11b8152602081600481875afa908115610b07575f91613803575b506001600160a01b03166040868101919091525163388af5b560e01b8152602081600481875afa908115610b07575f916137c9575b506001600160a01b03166060860152604051630399e3a560e41b8152602081600481875afa908115610b07575f9161378f575b506001600160a01b031661014086015260405163ace48b4560e01b815291602083600481875afa928315610b07575f9361375b575b506135e183611e32565b916135ef6040519384611b38565b838352601f196135fe85611e32565b015f5b81811061372b57505061016087019283525f5b84811061362357505050505050565b60405163779a968360e01b815260048101829052906020826024818a5afa918215610b07575f926136f8575b508161365c828751611e6a565b5152604051630dd5aa9b60e31b815260048101839052916020836024818b5afa8015610b075785935f916136c0575b50926136aa9160019460206136a1868b51611e6a565b510152856139cf565b60406136b7838851611e6a565b51015201613614565b9350506020833d82116136f0575b816136db60209383611b38565b81010312610afb5791518492906136aa61368b565b3d91506136ce565b9091506020813d8211613723575b8161371360209383611b38565b81010312610afb5751905f61364f565b3d9150613706565b60209060405161373a81611ae7565b5f81525f8382015261374a611f0a565b604082015282828801015201613601565b9092506020813d602011613787575b8161377760209383611b38565b81010312610afb5751915f6135d7565b3d915061376a565b90506020813d6020116137c1575b816137aa60209383611b38565b81010312610afb576137bb90611d2b565b5f6135a2565b3d915061379d565b90506020813d6020116137fb575b816137e460209383611b38565b81010312610afb576137f590611d2b565b5f61356f565b3d91506137d7565b90506020813d602011613835575b8161381e60209383611b38565b81010312610afb5761382f90611d2b565b5f61353a565b3d9150613811565b82636364223f60e01b5f5260045260245ffd5b60249150602090604051928380926335abafe560e21b82528860048301525afa908115610b07575f91613885575b505f613509565b90506020813d6020116138b7575b816138a060209383611b38565b81010312610afb576138b190611d1e565b5f61387e565b3d9150613893565b602491506020906040519283809263230dbab560e01b82528860048301525afa908115610b07575f916138f4575b505f6131f9565b90506020813d602011613926575b8161390f60209383611b38565b81010312610afb5761392090611d1e565b5f6138ed565b3d9150613902565b6024915060209060405192838092632c77566560e01b82528c60048301525afa908115610b07575f91613963575b505f612080565b90506020813d602011613995575b8161397e60209383611b38565b81010312610afb5761398f90611d1e565b5f61395c565b3d9150613971565b90506020813d6020116139c7575b816139b860209383611b38565b81010312610afb57515f612067565b3d91506139ab565b9291906139da611f0a565b604051632c3c915760e01b81526004810183905290946001600160a01b03169060a081602481855afa918215610b075760249260c0925f91613be8575b50875260405192838092632e3071cd60e11b82528660048301525afa908115610b07575f91613b4b575b5060208501528351604001516001600160a01b031680613adf575b508351606001516001600160a01b0392831692168214613a7a575050565b6020906024604051809481936301977b5760e01b835260048301525afa908115610b07575f91613aad575b506080830152565b90506020813d602011613ad7575b81613ac860209383611b38565b81010312610afb57515f613aa5565b3d9150613abb565b60206004916040519283809263501ad8ff60e11b82525afa5f9181613b17575b5015613a5c576001604086015260608501525f613a5c565b9091506020813d602011613b43575b81613b3360209383611b38565b81010312610afb5751905f613aff565b3d9150613b26565b905060c0813d60c011613be0575b81613b6660c09383611b38565b81010312610afb57613bd560a060405192613b8084611b02565b613b8981611deb565b8452613b9760208201611deb565b6020850152613ba860408201611deb565b6040850152613bb960608201611deb565b6060850152613bca60808201611deb565b608085015201611deb565b60a08201525f613a41565b3d9150613b59565b613c01915060a03d60a011613404576133f58183611b38565b5f613a17565b90816040910312610afb5760405190613c1f82611acc565b80516001600160c01b0381168103610afb578252613c3f90602001611dff565b602082015290565b3d15613c71573d90613c5882611d3f565b91613c666040519384611b38565b82523d5f602084013e565b606090565b90613c8082611e32565b613c8d6040519182611b38565b8281528092613c9e601f1991611e32565b019060203691013756fea26469706673582212203e675bf0e683983964ae8cc072b121082d3c6c0bb834d392a811927381e6b0e764736f6c63430008240033"; + "0x60808060405234601557613cde908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c630f0d54d814610024575f80fd5b34610afb57610100366003190112610afb576004356001600160a01b0381169003610afb576024356001600160a01b0381168103610afb576044356001600160a01b0381169003610afb576064356001600160a01b0381169003610afb576084356001600160a01b0381169003610afb5760a4356001600160a01b0381169003610afb5760c4356001600160a01b0381169003610afb5760e4356001600160a01b0381169003610afb576103206040526040516100e081611a96565b5f8082526060602083018190526040830181905280830182905260809290925260a081905260c081905260e08190526101008190526101208190526101408190526101608190526101808290526101a08190526101c0919091526101e08190526102008190526102208190526102408190526102608190526102808190526102a08190526102c052610170611cae565b6102e052606061030052604051635edec50d60e01b81526001600160a01b03600480358216908301526020908290602490829086165afa908115610b07575f9161145b575b5015611433576040516338d52e0f60e01b815260208160048181356001600160a01b03165afa908115610b07575f916113f9575b506040516395d89b4160e01b81525f8160048181356001600160a01b03165afa908115610b07575f916113df575b506040516306fdde0360e01b8152905f8260048181356001600160a01b03165afa918215610b07575f926113bb575b5060405163313ce56760e01b81529160208360048181356001600160a01b03165afa918215610b075760ff935f9361138a575b506040519461028786611a96565b60018060a01b03168552602085015260408401521660608201526080526040516338d52e0f60e01b815260208160048160018060a01b038235165afa908115610b07575f91611350575b506001600160a01b0390811660a05260405163ce04bebb60e01b815290602090829060049082908235165afa8015610b07575f90611310575b6001600160801b031660c052506040516318160ddd60e01b815260208160048181356001600160a01b03165afa908115610b07575f916112de575b5060e0526040516331c6651b60e21b815260208160048181356001600160a01b03165afa908115610b07575f916112ac575b506101005260405163ece1d6e560e01b815260208160048181356001600160a01b03165afa8015610b07575f9061126c575b6001600160401b0316610120525060405163c046371160e01b815260208160048181356001600160a01b03165afa8015610b07575f9061122c575b6001600160401b0316610140525060405163ad468d1160e01b815260208160048181356001600160a01b03165afa908115610b07575f916111f2575b506001600160a01b03908116610160526040516305c0524560e31b8152905f90829060049082908235165afa908115610b07575f916111a2575b50610180526040516343bc43c160e11b815260208160048181356001600160a01b03165afa8015610b07576001600160601b03915f91611183575b50166101e05260405163537bfaeb60e11b815260208160048181356001600160a01b03165afa8015610b07576001600160601b03915f91611154575b50166102005260405163ed27f7c960e01b815260208160048181356001600160a01b03165afa908115610b07575f9161111a575b506001600160a01b03908116610220526040516306d9a30160e41b815290602090829060049082908235165afa908115610b07575f916110e0575b506001600160a01b0316610240526101e0516001600160601b0316156110d957610220516040516326326d2760e21b81526001600160a01b039182166004808301919091529091602091839160249183919035165afa908115610b07575f9161109f575b505b151561026052610200516001600160601b03161561109857610240516040516326326d2760e21b81526001600160a01b039182166004808301919091529091602091839160249183919035165afa908115610b07575f9161105e575b505b15156102805260a0516040516370a0823160e01b81526001600160a01b0360048035821690830152909160209183916024918391165afa908115610b07575f9161102c575b506102a0526044356001600160a01b0316151580610fab575b6084356001600160a01b031615159081610f28575b8080610f1b575b610ef857808115610ef1575b15156101a05215610d38575060408051906106a58183611b38565b600182525f5b601f1982018110610cf55750506101406080015261073f60018060a01b0360e0608001511660405160208101916040835260046060830152637468697360e01b608083015260408201526080815261070460a082611b38565b5190206040519061071482611a96565b81525f60208201525f60408201525f6060820152610140608001519061073982611e49565b52611e49565b505b6101c051515f5b818110610b8f57610160516001600160a01b031680610b5b575b50604051630b54457960e31b815260208160048181356001600160a01b03165afa908115610b07575f91610b29575b5061079b81611e32565b6107a86040519182611b38565b818152601f196107b783611e32565b015f5b818110610b12575050610300525f5b818110610a425760405160208152806108b96080516102a0602084015260018060a01b038151166102c0840152606061082e610816602084015160806102e0880152610340870190611495565b60408401518682036102bf1901610300880152611495565b91015161032084015260a080516001600160a01b03908116604086015260c080516001600160801b0316606087015260e08051608088015261010080519488019490945261012080516001600160401b03908116938901939093526101405190921690870152610160519091169185019190915261018051848303601f190191850191909152611495565b6101a05115156101408301526101c051828203601f19016101608401528051808352602092830192909101905f5b818110610a085750505061099e906001600160601b0361016060800151166101808401526001600160601b0361018060800151166101a084015260018060a01b036101a060800151166101c084015260018060a01b036101c060800151166101e08401526101e06080015115156102008401526102006080015115156102208401526102206080015161024084015261024060800151151561026084015261026060800151601f19848303016102808501526115ac565b61030051601f19838303016102a084015280518083526020600582901b8401810193928101925f918101905b8383106109d75786860387f35b9193955091936020806109f6600193601f1986820301875289516115ac565b970193019301909286959492936109ca565b91935091602060806001926060875180518352848101518584015260408101516040840152015160608201520194019101918493926108e7565b604051906313bd406b60e21b825280600483015260208260248160018060a01b03600435165afa918215610b07575f92610abe575b50610ab781610aa260019460e4359060c4359060a43590608435906064359060443590600435612014565b6103005190610ab18383611e6a565b52611e6a565b50016107c9565b91506020823d8211610aff575b81610ad860209383611b38565b81010312610afb57610ab781610aa2610af2600195611d2b565b94505050610a77565b5f80fd5b3d9150610acb565b6040513d5f823e3d90fd5b602090610b1d611cae565b828286010152016107ba565b90506020813d602011610b53575b81610b4460209383611b38565b81010312610afb575181610791565b3d9150610b37565b60016102c052610b859060e4359060c4359060a43590608435906064359060443590600435612014565b6102e05280610762565b610b9f8161014060800151611e6a565b5190815160405190632f0374dd60e21b8252600482015260208160248160018060a01b03600435165afa908115610b07575f91610cc4575b50602083015281516040519063a68bafa360e01b8252600482015260208160248160018060a01b03600435165afa908115610b07575f91610c93575b5060408301528151916040519263c69507dd60e01b8452600484015260208360248160018060a01b03600435165afa928315610b07575f93610c5f575b50916060600193015201610748565b92506020833d8211610c8b575b81610c7960209383611b38565b81010312610afb579151916060610c50565b3d9150610c6c565b90506020813d8211610cbc575b81610cad60209383611b38565b81010312610afb575184610c13565b3d9150610ca0565b90506020813d8211610ced575b81610cde60209383611b38565b81010312610afb575184610bd7565b3d9150610cd1565b602090604051610d0481611a96565b5f81525f838201525f60408201525f6060820152828286010152016106ab565b634e487b7160e01b5f52604160045260245ffd5b1561074157610d566101006080015160208082518301019101611e7e565b6101605160405163cc3802bf60e01b8152915f91839160a49183916001600160a01b0390911690610d8b9060048401906114ec565b5afa908115610b07575f91610e61575b508051610da781611e32565b90610db56040519283611b38565b808252610dc4601f1991611e32565b015f5b818110610e325750506101c0525f5b8151811015610e2b5780610e2481610df060019486611e6a565b5160405190610dfe82611a96565b81525f60208201525f60408201525f60608201526101406080015190610ab18383611e6a565b5001610dd6565b5050610741565b602090604051610e4181611a96565b5f81525f838201525f60408201525f606082015282828601015201610dc7565b90503d805f833e610e728183611b38565b810190602081830312610afb578051906001600160401b038211610afb57019080601f83011215610afb578151610ea881611e32565b92610eb66040519485611b38565b81845260208085019260051b820101928311610afb57602001905b828210610ee15750505081610d9b565b8151815260209182019101610ed1565b508161068a565b61016051636364223f60e01b5f9081526001600160a01b03909116600452602490fd5b506101805151151561067e565b610160516040516335abafe560e21b81526001600160a01b03918216600482015291925060209082906024908290608435165afa908115610b07575f91610f71575b5090610677565b90506020813d602011610fa3575b81610f8c60209383611b38565b81010312610afb57610f9d90611d1e565b82610f6a565b3d9150610f7f565b5061016051604051632c77566560e01b81526001600160a01b0391821660048201529060209082906024908290604435165afa908115610b07575f91610ff2575b50610662565b90506020813d602011611024575b8161100d60209383611b38565b81010312610afb5761101e90611d1e565b81610fec565b3d9150611000565b90506020813d602011611056575b8161104760209383611b38565b81010312610afb575181610649565b3d915061103a565b90506020813d602011611090575b8161107960209383611b38565b81010312610afb5761108a90611d1e565b81610602565b3d915061106c565b6001610604565b90506020813d6020116110d1575b816110ba60209383611b38565b81010312610afb576110cb90611d1e565b816105a4565b3d91506110ad565b60016105a6565b90506020813d602011611112575b816110fb60209383611b38565b81010312610afb5761110c90611d2b565b81610540565b3d91506110ee565b90506020813d60201161114c575b8161113560209383611b38565b81010312610afb5761114690611d2b565b81610505565b3d9150611128565b611176915060203d60201161117c575b61116e8183611b38565b810190611e13565b826104d1565b503d611164565b61119c915060203d60201161117c5761116e8183611b38565b82610495565b90503d805f833e6111b38183611b38565b810190602081830312610afb578051906001600160401b038211610afb57019080601f83011215610afb5781516111ec92602001611d5a565b8161045a565b90506020813d602011611224575b8161120d60209383611b38565b81010312610afb5761121e90611d2b565b81610420565b3d9150611200565b506020813d602011611264575b8161124660209383611b38565b81010312610afb5761125f6001600160401b0391611dff565b6103e4565b3d9150611239565b506020813d6020116112a4575b8161128660209383611b38565b81010312610afb5761129f6001600160401b0391611dff565b6103a9565b3d9150611279565b90506020813d6020116112d6575b816112c760209383611b38565b81010312610afb575181610377565b3d91506112ba565b90506020813d602011611308575b816112f960209383611b38565b81010312610afb575181610345565b3d91506112ec565b506020813d602011611348575b8161132a60209383611b38565b81010312610afb576113436001600160801b0391611deb565b61030a565b3d915061131d565b90506020813d602011611382575b8161136b60209383611b38565b81010312610afb5761137c90611d2b565b816102d1565b3d915061135e565b6113ad91935060203d6020116113b4575b6113a58183611b38565b810190611dd2565b9185610279565b503d61139b565b6113d89192503d805f833e6113d08183611b38565b810190611dad565b9083610246565b6113f391503d805f833e6113d08183611b38565b82610217565b90506020813d60201161142b575b8161141460209383611b38565b81010312610afb5761142590611d2b565b816101e9565b3d9150611407565b63634ba39d60e11b5f9081526001600160a01b03918216600490815235909116602452604490fd5b90506020813d60201161148d575b8161147660209383611b38565b81010312610afb5761148790611d1e565b5f6101b5565b3d9150611469565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106114d65750505090565b82518452602093840193909201916001016114c9565b80516001600160a01b03908116835260208083015182169084015260408083015182169084015260608083015190911690830152608090810151910152565b60806101a09161153c8482516114ec565b6001600160801b0360a0602083015182815116828801528260208201511660c08801528260408201511660e0880152826060820151166101008801528285820151166101208801520151166101408501526040810151151561016085015260608101516101808501520151910152565b60018060a01b03815116825260ff602082015116602083015260018060a01b03604082015116604083015260018060a01b0360608201511660608301526080810151608083015260018060a01b0360a08201511660a083015260c08101519061018060c084015281516102e061018085015260018060a01b0381511661046085015260a061166661164e602084015160c0610480890152610520880190611495565b604084015187820361045f19016104a0890152611495565b9160608101516104c087015260808101516104e0870152015161045f198583030161050086015260ff60f81b815116825260c06116c76116b5602084015160e0602087015260e0860190611495565b60408401518582036040870152611495565b916060810151606085015260018060a01b03608082015116608085015260a081015160a085015201519160c0818303910152602080835192838152019201905f5b818110611a80575050506020838101516001600160a01b039081166101a087015260408581015182166101c088015260608601519091166101e0870152608085015161020087015260a085015180516001600160c01b0316610220880152909101516001600160401b031661024086810191909152909161184c906118339060c087015180516001600160a01b039081166102608b01526020909101516001600160401b03166102808a015260e088015181166102a08a01526101008801516102c08a015261012088015181166102e08a0152610140880151166103008901526101608701516103208901526101808701516103408901526101a087015115156103608901526101c08701516103808901526101e087015161017f19898303016103a08a01526114b9565b61020086015187820361017f19016103c08901526114b9565b9361022081015115156103e0870152015160018060a01b038151166104008601526020810151610420860152015161044084015260e08101519183810360e0850152602080845192838152019301905f5b8181106119b65750505061010081015161010084015261012081015191838103610120850152602080845192838152019301905f5b818110611951575050506101609060018060a01b0361014082015116610140850152015191610160818303910152602080835192838152019201905f5b81811061191c5750505090565b9091926020610200600192611946604088518051845285810151868501520151604083019061152b565b01940192910161190f565b90919360206102c06001926119ab6040895161196e8482516114ec565b61199e8682015160a08601906001600160801b036040809280518552826020820151166020860152015116910152565b015161010083019061152b565b0195019291016118d2565b90919360206103006001926001600160801b0360e0895180518452858101511515868501526001600160401b036040820151166040850152611a1b606082015160608601906001600160401b036020809260018060c01b038151168552015116910152565b611a4c608082015160a08601906001600160801b036040809280518552826020820151166020860152015116910152565b611a5f60a082015161010086019061152b565b60c081015183166102c08501520151166102e082015201950192910161189d565b8251845260209384019390920191600101611708565b608081019081106001600160401b03821117610d2457604052565b60e081019081106001600160401b03821117610d2457604052565b604081019081106001600160401b03821117610d2457604052565b606081019081106001600160401b03821117610d2457604052565b60c081019081106001600160401b03821117610d2457604052565b60a081019081106001600160401b03821117610d2457604052565b90601f801991011681019081106001600160401b03821117610d2457604052565b60405190611b6682611ab1565b606060c0835f81528260208201528260408201525f838201525f60808201525f60a08201520152565b60405190611b9c82611ae7565b5f6040838281528260208201520152565b6040519061026082018281106001600160401b03821117610d245760405281604051611bd881611b02565b5f815260606020820152606060408201525f60608201525f6080820152611bfd611b59565b60a082015281525f60208201525f60408201525f60608201525f6080820152604051611c2881611acc565b5f81525f602082015260a0820152604051611c4281611acc565b5f81525f602082015260c08201525f60e08201525f6101008201525f6101208201525f6101408201525f6101608201525f6101808201525f6101a08201525f6101c082015260606101e082015260606102008201525f610220820152610240611ca9611b8f565b910152565b6040519061018082018281106001600160401b03821117610d24576040526060610160835f81525f60208201525f60408201525f838201525f60808201525f60a0820152611cfa611bad565b60c08201528260e08201525f610100820152826101208201525f6101408201520152565b51908115158203610afb57565b51906001600160a01b0382168203610afb57565b6001600160401b038111610d2457601f01601f191660200190565b929192611d6682611d3f565b91611d746040519384611b38565b829481845281830111610afb578281602093845f96015e010152565b9080601f83011215610afb578151611daa92602001611d5a565b90565b90602082820312610afb5781516001600160401b038111610afb57611daa9201611d90565b90816020910312610afb575160ff81168103610afb5790565b51906001600160801b0382168203610afb57565b51906001600160401b0382168203610afb57565b90816020910312610afb57516001600160601b0381168103610afb5790565b6001600160401b038111610d245760051b60200190565b805115611e565760200190565b634e487b7160e01b5f52603260045260245ffd5b8051821015611e565760209160051b010190565b908160a0910312610afb57608060405191611e9883611b1d565b611ea181611d2b565b8352611eaf60208201611d2b565b6020840152611ec060408201611d2b565b6040840152611ed160608201611d2b565b60608401520151608082015290565b60405190611eed82611b1d565b5f6080838281528260208201528260408201528260608201520152565b60405190611f1782611b1d565b5f608083611f23611ee0565b8152604051611f3181611b02565b83815283602082015283604082015283606082015283838201528360a082015260208201528260408201528260608201520152565b90816060910312610afb57611f9d6040805192611f8284611ae7565b80518452611f9260208201611deb565b602085015201611deb565b604082015290565b6040519061010082018281106001600160401b03821117610d24576040525f60e083828152826020820152826040820152604051611fe281611acc565b8381528360208201526060820152611ff8611b8f565b6080820152612005611f0a565b60a08201528260c08201520152565b95939091979692612023611cae565b6001600160a01b038481168083526040516399e9918360e01b815260048101829052929b909990929160209183916024918391165afa908115610b07575f9161399d575b5060808b01526001600160a01b0316801515908161392e575b50156131dc57505050600160208701526040516307f1b29b60e11b8152602081600481885afa908115610b07575f916131a2575b506001600160a01b03166040878101919091525163388af5b560e01b8152602081600481885afa908115610b07575f91613168575b506001600160a01b0316606087015260405163e4baaddf60e01b815292602084600481885afa938415610b07575f9461312c575b506001600160a01b0390931660a08701818152939061213a611bad565b916040516338d52e0f60e01b8152602081600481865afa908115610b07575f916130f2575b506040516395d89b4160e01b81525f81600481875afa908115610b07575f916130d8575b506040516306fdde0360e01b81525f81600481885afa908115610b07575f916130be575b5060405163313ce56760e01b815290602082600481895afa918215610b07575f9261309d575b50604051632ba9c2b360e21b8152926020846004818a5afa938415610b07575f9461307c575b506121fc611b59565b505f8060405160208101906342580cb760e11b825260048152612220602482611b38565b51908a5afa61222d613c47565b901561304157805181019060e08160208401930312610afb5760208101516001600160f81b0319811690819003610afb5760408201516001600160401b038111610afb5783602061228092850101611d90565b60608301516001600160401b038111610afb578460206122a292860101611d90565b608084015160a08501516001600160a01b0381169491939190859003610afb5760c08601519560e0810151906001600160401b038211610afb57019680603f89011215610afb5760208801516122f781611e32565b986123056040519a8b611b38565b818a52602080808c019360051b83010101928311610afb57604001905b82821061303157505050926123489a98959260ff9a9794928b9996936040519d8e611ab1565b8d5260208d015260408c015260608b015260808a015260a089015260c08801526040519761237589611b02565b60018060a01b031688526020880152604087015216606085015216608083015260a08201528352604051638da5cb5b60e01b8152602081600481865afa908115610b07575f91612ff7575b506001600160a01b031660208481019190915260405163e66f53b760e01b81529081600481865afa908115610b07575f91612fbd575b506001600160a01b031660408481019190915251630229549960e51b8152602081600481865afa908115610b07575f91612f83575b506001600160a01b031660608401526040516334cc866d60e21b8152602081600481865afa908115610b07575f91612f51575b50608084015260408051637cc4d9a160e01b81529081600481865afa908115610b07575f91612f32575b5060a084015260408051633b1618dd60e11b81529081600481865afa908115610b07575f91612ed9575b5060c0840152604051631c61872f60e31b8152602081600481865afa908115610b07575f91612e9f575b506001600160a01b031660e084015260405163ddca3f4360e01b8152602081600481865afa8015610b07576001600160601b03915f91612e80575b501661010084015260405163011a412160e61b8152602081600481865afa908115610b07575f91612e46575b506001600160a01b031661012084015260405163388af5b560e01b8152602081600481865afa908115610b07575f91612e0c575b506001600160a01b03166101408401526040516318160ddd60e01b8152602081600481865afa908115610b07575f91612dda575b5061016084015260405163568efc0760e01b8152602081600481865afa908115610b07575f91612da8575b506101808401525f806040516020810190630872d2c560e21b825260048152612600602482611b38565b5190855afa61260d613c47565b9080612d9c575b612d73575b50604051630a17b31360e41b8152602081600481865afa908115610b07575f91612d41575b5061264881613c76565b6101e085019081525f5b828110612cce5750506040516333f91ebb60e01b8152949050602085600481865afa948515610b07575f95612c9a575b5061268c85613c76565b9461020085019586525f5b818110612c275750506001600160a01b0316801515949092908580612bbb575b612a61575b60c08b019485525151946126cf86611e32565b946126dd6040519687611b38565b868652601f196126ec88611e32565b015f5b818110612a4a57505060e08c019586525f5b87811061278757505096516040516370a0823160e01b8152600481019990995260209750889650602495508694506001600160a01b0316925050505afa908115610b07575f91612755575b50610100830152565b90506020813d60201161277f575b8161277060209383611b38565b81010312610afb57515f61274c565b3d9150612763565b6127978161020084510151611e6a565b51906127a1611fa5565b91604051636638c7bb60e11b81528160048201526060816024818a5afa908115610b07575f916129cd575b5080516001600160b81b031684526020808201511515908501526040908101516001600160401b031684820152805163518df2eb60e11b81526004810183905290816024818a5afa908115610b07575f9161299f575b506060848101919091526040516349e2903160e11b8152600481018390526001600160a01b03881660248201529081806044810103816001600160a01b038c165afa908115610b07575f91612971575b506080840152846128848c838a6139cf565b60a08501526128aa575b506128a3816001938a5190610ab18383611e6a565b5001612701565b9160405192639dbcd5b960e01b845286600485015260248401526040836044818b5afa928315610b07575f93612904575b5082516001600160801b0390811660c083015260209093015190921660e08301526128a361288e565b92506040833d8211612969575b8161291e60409383611b38565b81010312610afb57816128a3916001600160801b03602060019661295a826040519261294984611acc565b61295281611deb565b845201611deb565b828201529650505091506128db565b3d9150612911565b612992915060603d8111612998575b61298a8183611b38565b810190611f66565b5f612872565b503d612980565b6129c0915060403d81116129c6575b6129b88183611b38565b810190613c07565b5f612822565b503d6129ae565b90506060813d8211612a42575b816129e760609383611b38565b81010312610afb576040516129fb81611ae7565b8151906001600160b81b0382168203610afb57612a3760406001600160401b039481948452612a2c60208201611d1e565b602085015201611dff565b8282015291506127cc565b3d91506129da565b602090612a55611fa5565b82828b010152016126ef565b6001610220860152604051630c7508df60e31b815260048101839052602081602481885afa908115610b07575f91612b81575b50604051636fcca69b60e01b815260048101849052602081602481895afa908115610b07575f91612b4f575b506040516348d88a5960e11b815260048101859052906020826024818a5afa918215610b07575f92612b1b575b5060405192612afb84611ae7565b6001600160a01b03168352602083015260408201526102408601526126bc565b9091506020813d602011612b47575b81612b3760209383611b38565b81010312610afb5751905f612aed565b3d9150612b2a565b90506020813d602011612b79575b81612b6a60209383611b38565b81010312610afb57515f612ac0565b3d9150612b5d565b90506020813d602011612bb3575b81612b9c60209383611b38565b81010312610afb57612bad90611d2b565b5f612a94565b3d9150612b8f565b506040516326f6f90760e11b815260048101859052602081602481865afa908115610b07575f91612bed575b506126b7565b90506020813d602011612c1f575b81612c0860209383611b38565b81010312610afb57612c1990611d1e565b5f612be7565b3d9150612bfb565b6040516362518ddf60e01b81526004810182905290602082602481895afa8015610b07575f90612c68575b60019250612c61828a51611e6a565b5201612697565b506020823d8211612c92575b81612c8160209383611b38565b81010312610afb5760019151612c52565b3d9150612c74565b9094506020813d602011612cc6575b81612cb660209383611b38565b81010312610afb5751935f612682565b3d9150612ca9565b60405163f7d1852160e01b81526004810182905290602082602481895afa8015610b07575f90612d0f575b60019250612d08828551611e6a565b5201612652565b506020823d8211612d39575b81612d2860209383611b38565b81010312610afb5760019151612cf9565b3d9150612d1b565b90506020813d602011612d6b575b81612d5c60209383611b38565b81010312610afb57515f61263e565b3d9150612d4f565b60016101a085015260208151918180820193849201010312610afb57516101c08401525f612619565b50602081511015612614565b90506020813d602011612dd2575b81612dc360209383611b38565b81010312610afb57515f6125d6565b3d9150612db6565b90506020813d602011612e04575b81612df560209383611b38565b81010312610afb57515f6125ab565b3d9150612de8565b90506020813d602011612e3e575b81612e2760209383611b38565b81010312610afb57612e3890611d2b565b5f612577565b3d9150612e1a565b90506020813d602011612e78575b81612e6160209383611b38565b81010312610afb57612e7290611d2b565b5f612543565b3d9150612e54565b612e99915060203d60201161117c5761116e8183611b38565b5f612517565b90506020813d602011612ed1575b81612eba60209383611b38565b81010312610afb57612ecb90611d2b565b5f6124dc565b3d9150612ead565b90506040813d604011612f2a575b81612ef460409383611b38565b81010312610afb57612f1f602060405192612f0e84611acc565b612f1781611d2b565b845201611dff565b60208201525f6124b2565b3d9150612ee7565b612f4b915060403d6040116129c6576129b88183611b38565b5f612488565b90506020813d602011612f7b575b81612f6c60209383611b38565b81010312610afb57515f61245e565b3d9150612f5f565b90506020813d602011612fb5575b81612f9e60209383611b38565b81010312610afb57612faf90611d2b565b5f61242b565b3d9150612f91565b90506020813d602011612fef575b81612fd860209383611b38565b81010312610afb57612fe990611d2b565b5f6123f6565b3d9150612fcb565b90506020813d602011613029575b8161301260209383611b38565b81010312610afb5761302390611d2b565b5f6123c0565b3d9150613005565b8151815260209182019101612322565b60405162461bcd60e51b8152602060048201526013602482015272195a5c0dcc4c911bdb585a5b8819985a5b1959606a1b6044820152606490fd5b61309691945060203d6020116113b4576113a58183611b38565b925f6121f3565b6130b791925060203d6020116113b4576113a58183611b38565b905f6121cd565b6130d291503d805f833e6113d08183611b38565b5f6121a7565b6130ec91503d805f833e6113d08183611b38565b5f612183565b90506020813d602011613124575b8161310d60209383611b38565b81010312610afb5761311e90611d2b565b5f61215f565b3d9150613100565b9093506020813d602011613160575b8161314860209383611b38565b81010312610afb5761315990611d2b565b925f61211d565b3d915061313b565b90506020813d60201161319a575b8161318360209383611b38565b81010312610afb5761319490611d2b565b5f6120e9565b3d9150613176565b90506020813d6020116131d4575b816131bd60209383611b38565b81010312610afb576131ce90611d2b565b5f6120b4565b3d91506131b0565b939591949193919250906001600160a01b031680151590816138bf575b50156134f15750600260208701526040516307f1b29b60e11b8152602081600481865afa908115610b07575f916134b7575b506001600160a01b03166040878101919091525163388af5b560e01b8152602081600481865afa908115610b07575f9161347d575b506001600160a01b0316606087015260405163b045ff5b60e01b815292602084600481865afa938415610b07575f94613449575b5061329e84611e32565b926132ac6040519485611b38565b848452601f196132bb86611e32565b015f5b81811061340b57505061012088019384525f5b8581106132e15750505050505050565b604051631f1a892160e11b8152600481018290529060a082602481865afa918215610b07575f926133db575b50604051602081019061332082856114ec565b60a0815261332f60c082611b38565b5190209161333e828851611e6a565b51526040516349e2903160e11b8152600481018390526001600160a01b0385166024820152606081806044810103816001600160a01b038a165afa908115610b07576001936133a7928b925f916133bd575b50602061339e868c51611e6a565b510152876139cf565b60406133b4838951611e6a565b510152016132d1565b6133d5915060603d81116129985761298a8183611b38565b5f613390565b6133fd91925060a03d8111613404575b6133f58183611b38565b810190611e7e565b905f61330d565b503d6133eb565b60209060405161341a81611ae7565b613422611ee0565b815261342c611b8f565b83820152613438611f0a565b6040820152828289010152016132be565b9093506020813d602011613475575b8161346560209383611b38565b81010312610afb5751925f613294565b3d9150613458565b90506020813d6020116134af575b8161349860209383611b38565b81010312610afb576134a990611d2b565b5f613260565b3d915061348b565b90506020813d6020116134e9575b816134d260209383611b38565b81010312610afb576134e390611d2b565b5f61322b565b3d91506134c5565b91939250906001600160a01b03168015159081613850575b501561383d57600360208601526040516307f1b29b60e11b8152602081600481875afa908115610b07575f91613803575b506001600160a01b03166040868101919091525163388af5b560e01b8152602081600481875afa908115610b07575f916137c9575b506001600160a01b03166060860152604051630399e3a560e41b8152602081600481875afa908115610b07575f9161378f575b506001600160a01b031661014086015260405163ace48b4560e01b815291602083600481875afa928315610b07575f9361375b575b506135e183611e32565b916135ef6040519384611b38565b838352601f196135fe85611e32565b015f5b81811061372b57505061016087019283525f5b84811061362357505050505050565b60405163779a968360e01b815260048101829052906020826024818a5afa918215610b07575f926136f8575b508161365c828751611e6a565b5152604051630dd5aa9b60e31b815260048101839052916020836024818b5afa8015610b075785935f916136c0575b50926136aa9160019460206136a1868b51611e6a565b510152856139cf565b60406136b7838851611e6a565b51015201613614565b9350506020833d82116136f0575b816136db60209383611b38565b81010312610afb5791518492906136aa61368b565b3d91506136ce565b9091506020813d8211613723575b8161371360209383611b38565b81010312610afb5751905f61364f565b3d9150613706565b60209060405161373a81611ae7565b5f81525f8382015261374a611f0a565b604082015282828801015201613601565b9092506020813d602011613787575b8161377760209383611b38565b81010312610afb5751915f6135d7565b3d915061376a565b90506020813d6020116137c1575b816137aa60209383611b38565b81010312610afb576137bb90611d2b565b5f6135a2565b3d915061379d565b90506020813d6020116137fb575b816137e460209383611b38565b81010312610afb576137f590611d2b565b5f61356f565b3d91506137d7565b90506020813d602011613835575b8161381e60209383611b38565b81010312610afb5761382f90611d2b565b5f61353a565b3d9150613811565b82636364223f60e01b5f5260045260245ffd5b60249150602090604051928380926335abafe560e21b82528860048301525afa908115610b07575f91613885575b505f613509565b90506020813d6020116138b7575b816138a060209383611b38565b81010312610afb576138b190611d1e565b5f61387e565b3d9150613893565b602491506020906040519283809263230dbab560e01b82528860048301525afa908115610b07575f916138f4575b505f6131f9565b90506020813d602011613926575b8161390f60209383611b38565b81010312610afb5761392090611d1e565b5f6138ed565b3d9150613902565b6024915060209060405192838092632c77566560e01b82528c60048301525afa908115610b07575f91613963575b505f612080565b90506020813d602011613995575b8161397e60209383611b38565b81010312610afb5761398f90611d1e565b5f61395c565b3d9150613971565b90506020813d6020116139c7575b816139b860209383611b38565b81010312610afb57515f612067565b3d91506139ab565b9291906139da611f0a565b604051632c3c915760e01b81526004810183905290946001600160a01b03169060a081602481855afa918215610b075760249260c0925f91613be8575b50875260405192838092632e3071cd60e11b82528660048301525afa908115610b07575f91613b4b575b5060208501528351604001516001600160a01b031680613adf575b508351606001516001600160a01b0392831692168214613a7a575050565b6020906024604051809481936301977b5760e01b835260048301525afa908115610b07575f91613aad575b506080830152565b90506020813d602011613ad7575b81613ac860209383611b38565b81010312610afb57515f613aa5565b3d9150613abb565b60206004916040519283809263501ad8ff60e11b82525afa5f9181613b17575b5015613a5c576001604086015260608501525f613a5c565b9091506020813d602011613b43575b81613b3360209383611b38565b81010312610afb5751905f613aff565b3d9150613b26565b905060c0813d60c011613be0575b81613b6660c09383611b38565b81010312610afb57613bd560a060405192613b8084611b02565b613b8981611deb565b8452613b9760208201611deb565b6020850152613ba860408201611deb565b6040850152613bb960608201611deb565b6060850152613bca60808201611deb565b608085015201611deb565b60a08201525f613a41565b3d9150613b59565b613c01915060a03d60a011613404576133f58183611b38565b5f613a17565b90816040910312610afb5760405190613c1f82611acc565b80516001600160c01b0381168103610afb578252613c3f90602001611dff565b602082015290565b3d15613c71573d90613c5882611d3f565b91613c666040519384611b38565b82523d5f602084013e565b606090565b90613c8082611e32565b613c8d6040519182611b38565b8281528092613c9e601f1991611e32565b019060203691013756fea2646970667358221220a6e108e7d4b1ff604e7cc99616f34f83acd5fbda44325a0aefb522a2513439a864736f6c63430008230033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2.ts index 39b58972f..cdfc813ac 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2.ts @@ -201,4 +201,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2` query bytecode. */ export const code = - "0x6080806040523460155761147b908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63f12c3a9214610024575f80fd5b34610a02576080366003190112610a02576004356001600160a01b0381168103610a02576024356001600160a01b0381168103610a02576044356001600160a01b0381168103610a0257606435916001600160a01b0383168303610a02576102c0604052604051610094816112cf565b5f80825260606020808401829052604080850183905282850184905260809490945260a083905260c083905260e08390526101008390526101208390526101408390526101608290526101808390526101a08290526101c08390526101e0919091526102008290526102208290526102408290526102608290526102808290526102a0919091529051635edec50d60e01b81526001600160a01b0386811660048301529091908290602490829086165afa908115610a0e575f9161128c575b501561126757506040516338d52e0f60e01b81526020816004816001600160a01b0388165afa908115610a0e575f9161122d575b506040516395d89b4160e01b81525f816004816001600160a01b0389165afa908115610a0e575f91611213575b506040516306fdde0360e01b8152905f826004816001600160a01b038a165afa918215610a0e575f926111ef575b5060405163313ce56760e01b8152916020836004816001600160a01b038b165afa918215610a0e575f926111ae575b60ff935060405194610222866112cf565b60018060a01b03168552602085015260408401521660608201526080526040516338d52e0f60e01b815260208160048160018060a01b0388165afa908115610a0e575f91611174575b506001600160a01b0390811660a05260405163ce04bebb60e01b8152906020908290600490829088165afa8015610a0e575f9061112b575b6001600160801b031660c052506040516318160ddd60e01b81526020816004816001600160a01b0388165afa908115610a0e575f916110f9575b5060e0526040516331c6651b60e21b81526020816004816001600160a01b0388165afa908115610a0e575f916110c7575b506101005260405163ece1d6e560e01b81526020816004816001600160a01b0388165afa8015610a0e5767ffffffffffffffff915f916110a8575b50166101205260405163c046371160e01b81526020816004816001600160a01b0388165afa8015610a0e5767ffffffffffffffff915f91611079575b50166101405260405163ad468d1160e01b81526020816004816001600160a01b0388165afa908115610a0e575f9161103f575b506001600160a01b03908116610180526040516305c0524560e31b8152905f908290600490829088165afa908115610a0e575f91610fee575b506101a0526040516343bc43c160e11b81526020816004816001600160a01b0388165afa8015610a0e576001600160601b03915f91610fcf575b50166102005260405163537bfaeb60e11b81526020816004816001600160a01b0388165afa8015610a0e576001600160601b03915f91610fa0575b50166102205260405163ed27f7c960e01b81526020816004816001600160a01b0388165afa908115610a0e575f91610f66575b506001600160a01b03908116610240526040516306d9a30160e41b8152906020908290600490829088165afa908115610a0e575f91610f2c575b506001600160a01b031661026052610200516001600160601b031615610f2557610240516040516326326d2760e21b81526001600160a01b039182166004820152906020908290602490829088165afa908115610a0e575f91610f06575b505b151561028052610220516001600160601b031615610eff57610260516040516326326d2760e21b81526001600160a01b039182166004820152906020908290602490829088165afa908115610a0e575f91610ee0575b505b15156102a052604051630b54457960e31b81526020816004816001600160a01b0388165afa908115610a0e575f91610eae575b506105c8816113f8565b6105d560405191826112eb565b818152601f196105e4836113f8565b01366020830137610160525f5b818110610e1e5750506001600160a01b03811615159081610dba575b506001600160a01b03821615159182610d44575b508080610d37575b610d1457808115610d0d575b15156101c05215610abe5750604080519061065081836112eb565b60018252601f19015f5b818110610a7b575050610160608001526106eb60018060a01b03610100608001511660405160208101916040835260046060830152637468697360e01b60808301526040820152608081526106b060a0826112eb565b519020604051906106c0826112cf565b81525f60208201525f60408201525f606082015261016060800151906106e582611410565b52611410565b505b6101e051515f5b818110610907576040516020815280608051610240602083015260018060a01b03815116610260830152606061075661073e602084015160806102808701526102e08601906112ab565b604084015185820361025f19016102a08701526112ab565b9101516102c083015260a080516001600160a01b0316604084015260c080516001600160801b0316606085015260e0805160808601526101008051938601939093526101205167ffffffffffffffff90811692860192909252610140519091169084015261016051838303601f1901918401919091528051808352602092830192909101905f5b8181106108e5575050610180516001600160a01b0316610120840152506101a051828203601f190161014084015261081591906112ab565b6101c05115156101608301526101e051828203601f19016101808401528051808352602092830192909101905f5b8181106108ab57505061020080516001600160601b039081166101a086015261022080519091166101c086015261024080516001600160a01b039081166101e0880152610260511692860192909252610280511515908501526102a051151590840152500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610843565b82516001600160a01b03168452859450602093840193909201916001016107dd565b6109178161016060800151611431565b518051604051632f0374dd60e21b815260048101919091529091906020816024816001600160a01b0389165afa908115610a0e575f91610a4a575b50602083015281516040519063a68bafa360e01b8252600482015260208160248160018060a01b0389165afa908115610a0e575f91610a19575b5060408301528151916040519263c69507dd60e01b8452600484015260208360248160018060a01b0389165afa928315610a0e575f936109d6575b509160606001930152016106f4565b92506020833d8211610a06575b816109f0602093836112eb565b81010312610a025791519160606109c7565b5f80fd5b3d91506109e3565b6040513d5f823e3d90fd5b90506020813d8211610a42575b81610a33602093836112eb565b81010312610a0257515f61098c565b3d9150610a26565b90506020813d8211610a73575b81610a64602093836112eb565b81010312610a0257515f610952565b3d9150610a57565b602090604051610a8a816112cf565b5f81525f838201525f60408201525f60608201528282860101520161065a565b634e487b7160e01b5f52604160045260245ffd5b156106ed57610120608001519060a082805181010312610a02576040519160a083019083821067ffffffffffffffff831117610aaa5760a091604052610b0660208201611325565b8452610b1460408201611325565b6020850152610b2560608201611325565b6040850152610b3660808201611325565b6060850190815291015160808401908152610180516040805163cc3802bf60e01b815286516001600160a01b039081166004830152602088015181166024830152919096015181166044870152925183166064860152905160848501525f91849160a4918391165afa918215610a0e575f92610c7a575b508151610bb9816113f8565b90610bc760405192836112eb565b808252610bd6601f19916113f8565b015f5b818110610c4b5750506101e0525f5b8251811015610c435780610c3c81610c0260019487611431565b5160405190610c10826112cf565b81525f60208201525f60408201525f60608201526101606080015190610c368383611431565b52611431565b5001610be8565b5090506106ed565b602090604051610c5a816112cf565b5f81525f838201525f60408201525f606082015282828601015201610bd9565b9091503d805f833e610c8c81836112eb565b810190602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a02578151610cc3816113f8565b92610cd160405194856112eb565b81845260208085019260051b820101928311610a0257602001905b828210610cfd57505050905f610bad565b8151815260209182019101610cec565b5081610635565b61018051636364223f60e01b5f9081526001600160a01b03909116600452602490fd5b506101a051511515610629565b610180516040516335abafe560e21b81526001600160a01b03918216600482015292935060209183916024918391165afa908115610a0e575f91610d8b575b50905f610621565b610dad915060203d602011610db3575b610da581836112eb565b81019061130d565b5f610d83565b503d610d9b565b61018051604051632c77566560e01b81526001600160a01b039182166004820152925060209183916024918391165afa908115610a0e575f91610dff575b505f61060d565b610e18915060203d602011610db357610da581836112eb565b5f610df8565b6040516313bd406b60e21b815260048101829052906020826024816001600160a01b038a165afa8015610a0e575f90610e75575b60019250610e658260e060800151611431565b90838060a01b03169052016105f1565b506020823d8211610ea6575b81610e8e602093836112eb565b81010312610a0257610ea1600192611325565b610e52565b3d9150610e81565b90506020813d602011610ed8575b81610ec9602093836112eb565b81010312610a0257515f6105be565b3d9150610ebc565b610ef9915060203d602011610db357610da581836112eb565b5f610589565b600161058b565b610f1f915060203d602011610db357610da581836112eb565b5f610531565b6001610533565b90506020813d602011610f5e575b81610f47602093836112eb565b81010312610a0257610f5890611325565b5f6104d3565b3d9150610f3a565b90506020813d602011610f98575b81610f81602093836112eb565b81010312610a0257610f9290611325565b5f610499565b3d9150610f74565b610fc2915060203d602011610fc8575b610fba81836112eb565b8101906113d9565b5f610466565b503d610fb0565b610fe8915060203d602011610fc857610fba81836112eb565b5f61042b565b90503d805f833e610fff81836112eb565b810190602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a0257815161103992602001611339565b5f6103f1565b90506020813d602011611071575b8161105a602093836112eb565b81010312610a025761106b90611325565b5f6103b8565b3d915061104d565b61109b915060203d6020116110a1575b61109381836112eb565b8101906113b9565b5f610385565b503d611089565b6110c1915060203d6020116110a15761109381836112eb565b5f610349565b90506020813d6020116110f1575b816110e2602093836112eb565b81010312610a0257515f61030e565b3d91506110d5565b90506020813d602011611123575b81611114602093836112eb565b81010312610a0257515f6102dd565b3d9150611107565b506020813d60201161116c575b81611145602093836112eb565b81010312610a0257516001600160801b0381168103610a02576001600160801b03906102a3565b3d9150611138565b90506020813d6020116111a6575b8161118f602093836112eb565b81010312610a02576111a090611325565b5f61026b565b3d9150611182565b9150916020813d6020116111e7575b816111ca602093836112eb565b81010312610a0257519160ff83168303610a025760ff9291610211565b3d91506111bd565b61120c9192503d805f833e61120481836112eb565b81019061137f565b905f6101e2565b61122791503d805f833e61120481836112eb565b5f6101b4565b90506020813d60201161125f575b81611248602093836112eb565b81010312610a025761125990611325565b5f610187565b3d915061123b565b63634ba39d60e11b5f9081526001600160a01b03918216600452908416602452604490fd5b6112a5915060203d602011610db357610da581836112eb565b5f610153565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b6080810190811067ffffffffffffffff821117610aaa57604052565b90601f8019910116810190811067ffffffffffffffff821117610aaa57604052565b90816020910312610a0257518015158103610a025790565b51906001600160a01b0382168203610a0257565b92919267ffffffffffffffff8211610aaa5760405191611363601f8201601f1916602001846112eb565b829481845281830111610a02578281602093845f96015e010152565b602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a025781516113b692602001611339565b90565b90816020910312610a02575167ffffffffffffffff81168103610a025790565b90816020910312610a0257516001600160601b0381168103610a025790565b67ffffffffffffffff8111610aaa5760051b60200190565b80511561141d5760200190565b634e487b7160e01b5f52603260045260245ffd5b805182101561141d5760209160051b01019056fea2646970667358221220e503bacf467e61444adc05af2f0e81ecb697388cff9d14d825058d01a57b9bf564736f6c63430008240033"; + "0x6080806040523460155761147b908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63f12c3a9214610024575f80fd5b34610a02576080366003190112610a02576004356001600160a01b0381168103610a02576024356001600160a01b0381168103610a02576044356001600160a01b0381168103610a0257606435916001600160a01b0383168303610a02576102c0604052604051610094816112cf565b5f80825260606020808401829052604080850183905282850184905260809490945260a083905260c083905260e08390526101008390526101208390526101408390526101608290526101808390526101a08290526101c08390526101e0919091526102008290526102208290526102408290526102608290526102808290526102a0919091529051635edec50d60e01b81526001600160a01b0386811660048301529091908290602490829086165afa908115610a0e575f9161128c575b501561126757506040516338d52e0f60e01b81526020816004816001600160a01b0388165afa908115610a0e575f9161122d575b506040516395d89b4160e01b81525f816004816001600160a01b0389165afa908115610a0e575f91611213575b506040516306fdde0360e01b8152905f826004816001600160a01b038a165afa918215610a0e575f926111ef575b5060405163313ce56760e01b8152916020836004816001600160a01b038b165afa918215610a0e575f926111ae575b60ff935060405194610222866112cf565b60018060a01b03168552602085015260408401521660608201526080526040516338d52e0f60e01b815260208160048160018060a01b0388165afa908115610a0e575f91611174575b506001600160a01b0390811660a05260405163ce04bebb60e01b8152906020908290600490829088165afa8015610a0e575f9061112b575b6001600160801b031660c052506040516318160ddd60e01b81526020816004816001600160a01b0388165afa908115610a0e575f916110f9575b5060e0526040516331c6651b60e21b81526020816004816001600160a01b0388165afa908115610a0e575f916110c7575b506101005260405163ece1d6e560e01b81526020816004816001600160a01b0388165afa8015610a0e5767ffffffffffffffff915f916110a8575b50166101205260405163c046371160e01b81526020816004816001600160a01b0388165afa8015610a0e5767ffffffffffffffff915f91611079575b50166101405260405163ad468d1160e01b81526020816004816001600160a01b0388165afa908115610a0e575f9161103f575b506001600160a01b03908116610180526040516305c0524560e31b8152905f908290600490829088165afa908115610a0e575f91610fee575b506101a0526040516343bc43c160e11b81526020816004816001600160a01b0388165afa8015610a0e576001600160601b03915f91610fcf575b50166102005260405163537bfaeb60e11b81526020816004816001600160a01b0388165afa8015610a0e576001600160601b03915f91610fa0575b50166102205260405163ed27f7c960e01b81526020816004816001600160a01b0388165afa908115610a0e575f91610f66575b506001600160a01b03908116610240526040516306d9a30160e41b8152906020908290600490829088165afa908115610a0e575f91610f2c575b506001600160a01b031661026052610200516001600160601b031615610f2557610240516040516326326d2760e21b81526001600160a01b039182166004820152906020908290602490829088165afa908115610a0e575f91610f06575b505b151561028052610220516001600160601b031615610eff57610260516040516326326d2760e21b81526001600160a01b039182166004820152906020908290602490829088165afa908115610a0e575f91610ee0575b505b15156102a052604051630b54457960e31b81526020816004816001600160a01b0388165afa908115610a0e575f91610eae575b506105c8816113f8565b6105d560405191826112eb565b818152601f196105e4836113f8565b01366020830137610160525f5b818110610e1e5750506001600160a01b03811615159081610dba575b506001600160a01b03821615159182610d44575b508080610d37575b610d1457808115610d0d575b15156101c05215610abe5750604080519061065081836112eb565b60018252601f19015f5b818110610a7b575050610160608001526106eb60018060a01b03610100608001511660405160208101916040835260046060830152637468697360e01b60808301526040820152608081526106b060a0826112eb565b519020604051906106c0826112cf565b81525f60208201525f60408201525f606082015261016060800151906106e582611410565b52611410565b505b6101e051515f5b818110610907576040516020815280608051610240602083015260018060a01b03815116610260830152606061075661073e602084015160806102808701526102e08601906112ab565b604084015185820361025f19016102a08701526112ab565b9101516102c083015260a080516001600160a01b0316604084015260c080516001600160801b0316606085015260e0805160808601526101008051938601939093526101205167ffffffffffffffff90811692860192909252610140519091169084015261016051838303601f1901918401919091528051808352602092830192909101905f5b8181106108e5575050610180516001600160a01b0316610120840152506101a051828203601f190161014084015261081591906112ab565b6101c05115156101608301526101e051828203601f19016101808401528051808352602092830192909101905f5b8181106108ab57505061020080516001600160601b039081166101a086015261022080519091166101c086015261024080516001600160a01b039081166101e0880152610260511692860192909252610280511515908501526102a051151590840152500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610843565b82516001600160a01b03168452859450602093840193909201916001016107dd565b6109178161016060800151611431565b518051604051632f0374dd60e21b815260048101919091529091906020816024816001600160a01b0389165afa908115610a0e575f91610a4a575b50602083015281516040519063a68bafa360e01b8252600482015260208160248160018060a01b0389165afa908115610a0e575f91610a19575b5060408301528151916040519263c69507dd60e01b8452600484015260208360248160018060a01b0389165afa928315610a0e575f936109d6575b509160606001930152016106f4565b92506020833d8211610a06575b816109f0602093836112eb565b81010312610a025791519160606109c7565b5f80fd5b3d91506109e3565b6040513d5f823e3d90fd5b90506020813d8211610a42575b81610a33602093836112eb565b81010312610a0257515f61098c565b3d9150610a26565b90506020813d8211610a73575b81610a64602093836112eb565b81010312610a0257515f610952565b3d9150610a57565b602090604051610a8a816112cf565b5f81525f838201525f60408201525f60608201528282860101520161065a565b634e487b7160e01b5f52604160045260245ffd5b156106ed57610120608001519060a082805181010312610a02576040519160a083019083821067ffffffffffffffff831117610aaa5760a091604052610b0660208201611325565b8452610b1460408201611325565b6020850152610b2560608201611325565b6040850152610b3660808201611325565b6060850190815291015160808401908152610180516040805163cc3802bf60e01b815286516001600160a01b039081166004830152602088015181166024830152919096015181166044870152925183166064860152905160848501525f91849160a4918391165afa918215610a0e575f92610c7a575b508151610bb9816113f8565b90610bc760405192836112eb565b808252610bd6601f19916113f8565b015f5b818110610c4b5750506101e0525f5b8251811015610c435780610c3c81610c0260019487611431565b5160405190610c10826112cf565b81525f60208201525f60408201525f60608201526101606080015190610c368383611431565b52611431565b5001610be8565b5090506106ed565b602090604051610c5a816112cf565b5f81525f838201525f60408201525f606082015282828601015201610bd9565b9091503d805f833e610c8c81836112eb565b810190602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a02578151610cc3816113f8565b92610cd160405194856112eb565b81845260208085019260051b820101928311610a0257602001905b828210610cfd57505050905f610bad565b8151815260209182019101610cec565b5081610635565b61018051636364223f60e01b5f9081526001600160a01b03909116600452602490fd5b506101a051511515610629565b610180516040516335abafe560e21b81526001600160a01b03918216600482015292935060209183916024918391165afa908115610a0e575f91610d8b575b50905f610621565b610dad915060203d602011610db3575b610da581836112eb565b81019061130d565b5f610d83565b503d610d9b565b61018051604051632c77566560e01b81526001600160a01b039182166004820152925060209183916024918391165afa908115610a0e575f91610dff575b505f61060d565b610e18915060203d602011610db357610da581836112eb565b5f610df8565b6040516313bd406b60e21b815260048101829052906020826024816001600160a01b038a165afa8015610a0e575f90610e75575b60019250610e658260e060800151611431565b90838060a01b03169052016105f1565b506020823d8211610ea6575b81610e8e602093836112eb565b81010312610a0257610ea1600192611325565b610e52565b3d9150610e81565b90506020813d602011610ed8575b81610ec9602093836112eb565b81010312610a0257515f6105be565b3d9150610ebc565b610ef9915060203d602011610db357610da581836112eb565b5f610589565b600161058b565b610f1f915060203d602011610db357610da581836112eb565b5f610531565b6001610533565b90506020813d602011610f5e575b81610f47602093836112eb565b81010312610a0257610f5890611325565b5f6104d3565b3d9150610f3a565b90506020813d602011610f98575b81610f81602093836112eb565b81010312610a0257610f9290611325565b5f610499565b3d9150610f74565b610fc2915060203d602011610fc8575b610fba81836112eb565b8101906113d9565b5f610466565b503d610fb0565b610fe8915060203d602011610fc857610fba81836112eb565b5f61042b565b90503d805f833e610fff81836112eb565b810190602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a0257815161103992602001611339565b5f6103f1565b90506020813d602011611071575b8161105a602093836112eb565b81010312610a025761106b90611325565b5f6103b8565b3d915061104d565b61109b915060203d6020116110a1575b61109381836112eb565b8101906113b9565b5f610385565b503d611089565b6110c1915060203d6020116110a15761109381836112eb565b5f610349565b90506020813d6020116110f1575b816110e2602093836112eb565b81010312610a0257515f61030e565b3d91506110d5565b90506020813d602011611123575b81611114602093836112eb565b81010312610a0257515f6102dd565b3d9150611107565b506020813d60201161116c575b81611145602093836112eb565b81010312610a0257516001600160801b0381168103610a02576001600160801b03906102a3565b3d9150611138565b90506020813d6020116111a6575b8161118f602093836112eb565b81010312610a02576111a090611325565b5f61026b565b3d9150611182565b9150916020813d6020116111e7575b816111ca602093836112eb565b81010312610a0257519160ff83168303610a025760ff9291610211565b3d91506111bd565b61120c9192503d805f833e61120481836112eb565b81019061137f565b905f6101e2565b61122791503d805f833e61120481836112eb565b5f6101b4565b90506020813d60201161125f575b81611248602093836112eb565b81010312610a025761125990611325565b5f610187565b3d915061123b565b63634ba39d60e11b5f9081526001600160a01b03918216600452908416602452604490fd5b6112a5915060203d602011610db357610da581836112eb565b5f610153565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b6080810190811067ffffffffffffffff821117610aaa57604052565b90601f8019910116810190811067ffffffffffffffff821117610aaa57604052565b90816020910312610a0257518015158103610a025790565b51906001600160a01b0382168203610a0257565b92919267ffffffffffffffff8211610aaa5760405191611363601f8201601f1916602001846112eb565b829481845281830111610a02578281602093845f96015e010152565b602081830312610a025780519067ffffffffffffffff8211610a0257019080601f83011215610a025781516113b692602001611339565b90565b90816020910312610a02575167ffffffffffffffff81168103610a025790565b90816020910312610a0257516001600160601b0381168103610a025790565b67ffffffffffffffff8111610aaa5760051b60200190565b80511561141d5760200190565b634e487b7160e01b5f52603260045260245ffd5b805182101561141d5760209160051b01019056fea26469706673582212204960061b86c1b207a8b92f2eadd53aaee751b9574d274bc613cea5449aaf759164736f6c63430008230033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1Adapter.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1Adapter.ts index 423c63f7b..faebd54d4 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1Adapter.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1Adapter.ts @@ -88,4 +88,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2MorphoMarketV1Adapter` query bytecode. */ export const code = - "0x60808060405234601557610538908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610310576040366003190112610310576004356001600160a01b0381169190829003610310576024356001600160a01b03811690819003610310576060820182811067ffffffffffffffff82111761045c576040525f825260208201925f845260408301916060835260405163230dbab560e01b8152826004820152602081602481855afa90811561031c575f91610421575b501561040c57506040516307f1b29b60e11b8152602081600481855afa90811561031c575f916103d2575b506001600160a01b0316835260405163388af5b560e01b8152602081600481855afa90811561031c575f91610398575b506001600160a01b0316845260405163b045ff5b60e01b815290602082600481845afa91821561031c575f92610364575b5061015382959493956104c2565b610160604051918261048c565b828152601f1961016f846104c2565b015f5b81811061032757505085525f5b82811061023557505060408051602080825293516001600160a01b03908116858301529451909416908401525091516060808301528051608083018190529192839260a0840192909101905f5b8181106101da575050500390f35b825180516001600160a01b039081168652602082810151821681880152604080840151831690880152606080840151909216918701919091526080918201519186019190915286955060a090940193909201916001016101cc565b604051631f1a892160e11b815260048101829052949593949060a082602481865afa91821561031c575f9261028b575b506102808160019387519061027a83836104da565b526104da565b50019493929461017f565b915060a0823d8211610314575b816102a560a0938361048c565b8101031261031057610280816001936080604051916102c383610470565b6102cc816104ae565b83526102da602082016104ae565b60208401526102eb604082016104ae565b60408401526102fc606082016104ae565b606084015201516080820152935050610265565b5f80fd5b3d9150610298565b6040513d5f823e3d90fd5b6020906040989697985161033a81610470565b5f81525f838201525f60408201525f60608201525f60808201528282860101520196959496610172565b9091506020813d602011610390575b816103806020938361048c565b810103126103105751905f610145565b3d9150610373565b90506020813d6020116103ca575b816103b36020938361048c565b81010312610310576103c4906104ae565b5f610114565b3d91506103a6565b90506020813d602011610404575b816103ed6020938361048c565b81010312610310576103fe906104ae565b5f6100e4565b3d91506103e0565b63634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610454575b8161043c6020938361048c565b8101031261031057518015158103610310575f6100b9565b3d915061042f565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761045c57604052565b90601f8019910116810190811067ffffffffffffffff82111761045c57604052565b51906001600160a01b038216820361031057565b67ffffffffffffffff811161045c5760051b60200190565b80518210156104ee5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220ded6e791bc86d2589da25a4cd9fd0cc6554083b90f948b1bd94bde469481ee5464736f6c63430008240033"; + "0x60808060405234601557610538908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610310576040366003190112610310576004356001600160a01b0381169190829003610310576024356001600160a01b03811690819003610310576060820182811067ffffffffffffffff82111761045c576040525f825260208201925f845260408301916060835260405163230dbab560e01b8152826004820152602081602481855afa90811561031c575f91610421575b501561040c57506040516307f1b29b60e11b8152602081600481855afa90811561031c575f916103d2575b506001600160a01b0316835260405163388af5b560e01b8152602081600481855afa90811561031c575f91610398575b506001600160a01b0316845260405163b045ff5b60e01b815290602082600481845afa91821561031c575f92610364575b5061015382959493956104c2565b610160604051918261048c565b828152601f1961016f846104c2565b015f5b81811061032757505085525f5b82811061023557505060408051602080825293516001600160a01b03908116858301529451909416908401525091516060808301528051608083018190529192839260a0840192909101905f5b8181106101da575050500390f35b825180516001600160a01b039081168652602082810151821681880152604080840151831690880152606080840151909216918701919091526080918201519186019190915286955060a090940193909201916001016101cc565b604051631f1a892160e11b815260048101829052949593949060a082602481865afa91821561031c575f9261028b575b506102808160019387519061027a83836104da565b526104da565b50019493929461017f565b915060a0823d8211610314575b816102a560a0938361048c565b8101031261031057610280816001936080604051916102c383610470565b6102cc816104ae565b83526102da602082016104ae565b60208401526102eb604082016104ae565b60408401526102fc606082016104ae565b606084015201516080820152935050610265565b5f80fd5b3d9150610298565b6040513d5f823e3d90fd5b6020906040989697985161033a81610470565b5f81525f838201525f60408201525f60608201525f60808201528282860101520196959496610172565b9091506020813d602011610390575b816103806020938361048c565b810103126103105751905f610145565b3d9150610373565b90506020813d6020116103ca575b816103b36020938361048c565b81010312610310576103c4906104ae565b5f610114565b3d91506103a6565b90506020813d602011610404575b816103ed6020938361048c565b81010312610310576103fe906104ae565b5f6100e4565b3d91506103e0565b63634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610454575b8161043c6020938361048c565b8101031261031057518015158103610310575f6100b9565b3d915061042f565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761045c57604052565b90601f8019910116810190811067ffffffffffffffff82111761045c57604052565b51906001600160a01b038216820361031057565b67ffffffffffffffff811161045c5760051b60200190565b80518210156104ee5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220dfb21c34432d601f765bfd2882d5cb1ad3536aa834246292ce550ba864b28ea664736f6c634300081b0033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1AdapterV2.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1AdapterV2.ts index 0b7635ba8..47237c0b0 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1AdapterV2.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoMarketV1AdapterV2.ts @@ -78,4 +78,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2MorphoMarketV1AdapterV2` query bytecode. */ export const code = - "0x60808060405234601557610552908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610305576040366003190112610305576004356001600160a01b0381169190829003610305576024356001600160a01b03811690819003610305576080820182811067ffffffffffffffff82111761046b576040525f825260208201925f845260408301905f82526060840192606084526040516335abafe560e21b8152826004820152602081602481855afa908115610311575f91610430575b501561041b57506040516307f1b29b60e11b8152602081600481855afa908115610311575f916103fc575b506001600160a01b0316845260405163388af5b560e01b8152602081600481855afa908115610311575f916103dd575b506001600160a01b03168552604051630399e3a560e41b8152602081600481855afa908115610311575f916103ae575b506001600160a01b0316825260405163ace48b4560e01b8152602081600481855afa908115610311575f9161037c575b5061018b8196959493966104dc565b610198604051918261049b565b818152601f196101a7836104dc565b015f5b81811061034f57505083525f5b81811061024157505060408051602080825294516001600160a01b039081168683015295518616918101919091529451909316606085015251608080850152805160a0850181905284935060c084019291909101905f5b81811061021c575050500390f35b825180518552602090810151818601528695506040909401939092019160010161020e565b60409693949596519063779a968360e01b8252806004830152602082602481875afa918215610311575f9261031c575b50604051630dd5aa9b60e31b81526004810183905291602083602481885afa928315610311575f936102da575b50816102ce91600194604051916102b48361047f565b825260208201528851906102c883836104f4565b526104f4565b500195949392956101b7565b92506020833d8211610309575b816102f46020938361049b565b81010312610305579151918161029e565b5f80fd5b3d91506102e7565b6040513d5f823e3d90fd5b9091506020813d8211610347575b816103376020938361049b565b810103126103055751905f610271565b3d915061032a565b60209060409996979899516103638161047f565b5f81525f838201528282860101520197969594976101aa565b90506020813d6020116103a6575b816103976020938361049b565b8101031261030557515f61017c565b3d915061038a565b6103d0915060203d6020116103d6575b6103c8818361049b565b8101906104bd565b5f61014c565b503d6103be565b6103f6915060203d6020116103d6576103c8818361049b565b5f61011c565b610415915060203d6020116103d6576103c8818361049b565b5f6100ec565b63634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610463575b8161044b6020938361049b565b8101031261030557518015158103610305575f6100c1565b3d915061043e565b634e487b7160e01b5f52604160045260245ffd5b6040810190811067ffffffffffffffff82111761046b57604052565b90601f8019910116810190811067ffffffffffffffff82111761046b57604052565b9081602091031261030557516001600160a01b03811681036103055790565b67ffffffffffffffff811161046b5760051b60200190565b80518210156105085760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea26469706673582212208c72ce6097e17757df43c0f405ac807cbbe835f98f7df5d4f1d29179d2595f1d64736f6c63430008240033"; + "0x60808060405234601557610552908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610305576040366003190112610305576004356001600160a01b0381169190829003610305576024356001600160a01b03811690819003610305576080820182811067ffffffffffffffff82111761046b576040525f825260208201925f845260408301905f82526060840192606084526040516335abafe560e21b8152826004820152602081602481855afa908115610311575f91610430575b501561041b57506040516307f1b29b60e11b8152602081600481855afa908115610311575f916103fc575b506001600160a01b0316845260405163388af5b560e01b8152602081600481855afa908115610311575f916103dd575b506001600160a01b03168552604051630399e3a560e41b8152602081600481855afa908115610311575f916103ae575b506001600160a01b0316825260405163ace48b4560e01b8152602081600481855afa908115610311575f9161037c575b5061018b8196959493966104dc565b610198604051918261049b565b818152601f196101a7836104dc565b015f5b81811061034f57505083525f5b81811061024157505060408051602080825294516001600160a01b039081168683015295518616918101919091529451909316606085015251608080850152805160a0850181905284935060c084019291909101905f5b81811061021c575050500390f35b825180518552602090810151818601528695506040909401939092019160010161020e565b60409693949596519063779a968360e01b8252806004830152602082602481875afa918215610311575f9261031c575b50604051630dd5aa9b60e31b81526004810183905291602083602481885afa928315610311575f936102da575b50816102ce91600194604051916102b48361047f565b825260208201528851906102c883836104f4565b526104f4565b500195949392956101b7565b92506020833d8211610309575b816102f46020938361049b565b81010312610305579151918161029e565b5f80fd5b3d91506102e7565b6040513d5f823e3d90fd5b9091506020813d8211610347575b816103376020938361049b565b810103126103055751905f610271565b3d915061032a565b60209060409996979899516103638161047f565b5f81525f838201528282860101520197969594976101aa565b90506020813d6020116103a6575b816103976020938361049b565b8101031261030557515f61017c565b3d915061038a565b6103d0915060203d6020116103d6575b6103c8818361049b565b8101906104bd565b5f61014c565b503d6103be565b6103f6915060203d6020116103d6576103c8818361049b565b5f61011c565b610415915060203d6020116103d6576103c8818361049b565b5f6100ec565b63634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610463575b8161044b6020938361049b565b8101031261030557518015158103610305575f6100c1565b3d915061043e565b634e487b7160e01b5f52604160045260245ffd5b6040810190811067ffffffffffffffff82111761046b57604052565b90601f8019910116810190811067ffffffffffffffff82111761046b57604052565b9081602091031261030557516001600160a01b03811681036103055790565b67ffffffffffffffff811161046b5760051b60200190565b80518210156105085760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea26469706673582212202b6841f6278647fbce4a7e04c1e286d66d533ceaccf4cbe6aa91bf714e1623cc64736f6c634300081b0033"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoVaultV1Adapter.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoVaultV1Adapter.ts index 5e214a36a..50a21f618 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoVaultV1Adapter.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2MorphoVaultV1Adapter.ts @@ -61,4 +61,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2MorphoVaultV1Adapter` query bytecode. */ export const code = - "0x608080604052346015576102cb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610234576040366003190112610234576004356001600160a01b0381169190829003610234576024356001600160a01b03811690819003610234576060820182811067ffffffffffffffff821117610240576040525f8252602082015f815260408301915f8352604051632c77566560e01b8152856004820152602081602481855afa9081156101a3575f91610201575b50156101ea575060405163e4baaddf60e01b8152602081600481885afa9081156101a3575f916101cb575b506001600160a01b031683526040516307f1b29b60e11b8152602081600481885afa9485156101a3576004956020925f916101ae575b506001600160a01b0316835260405163388af5b560e01b815295869182905afa80156101a3576060945f91610174575b506001600160a01b03908116835260408051945182168552915181166020850152915190911690820152f35b610196915060203d60201161019c575b61018e8183610254565b810190610276565b5f610148565b503d610184565b6040513d5f823e3d90fd5b6101c59150833d851161019c5761018e8183610254565b5f610118565b6101e4915060203d60201161019c5761018e8183610254565b5f6100e2565b849063634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610238575b8161021c60209383610254565b8101031261023457518015158103610234575f6100b7565b5f80fd5b3d915061020f565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff82111761024057604052565b9081602091031261023457516001600160a01b0381168103610234579056fea26469706673582212208a2b78d082f0a82f3ec4b99f59a2484bdb11271cefce4c01e4af4a1ddcb05a7f64736f6c63430008240033"; + "0x608080604052346015576102cb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63f6f030ce14610025575f80fd5b34610234576040366003190112610234576004356001600160a01b0381169190829003610234576024356001600160a01b03811690819003610234576060820182811067ffffffffffffffff821117610240576040525f8252602082015f815260408301915f8352604051632c77566560e01b8152856004820152602081602481855afa9081156101a3575f91610201575b50156101ea575060405163e4baaddf60e01b8152602081600481885afa9081156101a3575f916101cb575b506001600160a01b031683526040516307f1b29b60e11b8152602081600481885afa9485156101a3576004956020925f916101ae575b506001600160a01b0316835260405163388af5b560e01b815295869182905afa80156101a3576060945f91610174575b506001600160a01b03908116835260408051945182168552915181166020850152915190911690820152f35b610196915060203d60201161019c575b61018e8183610254565b810190610276565b5f610148565b503d610184565b6040513d5f823e3d90fd5b6101c59150833d851161019c5761018e8183610254565b5f610118565b6101e4915060203d60201161019c5761018e8183610254565b5f6100e2565b849063634ba39d60e11b5f5260045260245260445ffd5b90506020813d602011610238575b8161021c60209383610254565b8101031261023457518015158103610234575f6100b7565b5f80fd5b3d915061020f565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff82111761024057604052565b9081602091031261023457516001600160a01b0381168103610234579056fea264697066735822122085a2452553e2f4801b563c99b5b576d6f292803c08eb7cf6a8f6d9f4b42facd664736f6c634300081b0033"; From 25840c3ac7b39f8c6fc11321bbfcaed128e8d815 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Tue, 18 Aug 2026 16:57:24 +0200 Subject: [PATCH 23/41] refactor: align Vault V2 Blue reallocation API --- .changeset/brave-vaults-reallocate.md | 4 +- ...lt-v2-public-allocator-shared-liquidity.md | 6 +- ...26-08-18-vault-v2-blue-reallocation-api.md | 65 +++++ packages/blue-sdk-viem/AGENTS.md | 2 +- packages/blue-sdk-viem/src/abis.ts | 1 + packages/blue-sdk-viem/src/fetch/Vault.ts | 28 +- .../fetch/VaultMarketPublicAllocatorConfig.ts | 10 +- .../blue-sdk-viem/src/fetch/fetch.test.ts | 12 +- ...2PublicAllocatorConfig.integration.test.ts | 2 +- .../VaultV2PublicAllocatorConfig.test.ts | 2 +- .../vault-v2/VaultV2PublicAllocatorConfig.ts | 6 +- packages/blue-sdk-viem/test/Vault.test.ts | 11 +- .../test/VaultMarketConfig.test.ts | 15 +- .../liquidity-sdk-viem/src/loader.test.ts | 28 +- packages/morpho-sdk/AGENTS.md | 2 +- packages/morpho-sdk/src/abis.ts | 1 + .../blue/borrow.bluePublicAllocator.test.ts | 11 +- .../src/actions/blue/refinance.test.ts | 3 +- .../vaultV2Reallocations.integration.test.ts | 47 ++-- .../blue/withdraw.bluePublicAllocator.test.ts | 2 +- .../morpho-sdk/src/bundler/actions.test.ts | 6 +- packages/morpho-sdk/src/bundler/actions.ts | 28 +- packages/morpho-sdk/src/entities/AGENTS.md | 4 +- .../entities/blue/blue.reallocations.test.ts | 42 ++- packages/morpho-sdk/src/entities/blue/blue.ts | 190 ++++++++++++- packages/morpho-sdk/src/entities/index.ts | 7 +- ...1ReallocationData.publicAllocator.test.ts} | 2 +- .../src/entities/vaultV1ReallocationData.ts | 6 +- ...ts => vaultV2BlueReallocationData.test.ts} | 214 +++++++-------- ...Data.ts => vaultV2BlueReallocationData.ts} | 259 +++++++----------- packages/morpho-sdk/src/helpers/AGENTS.md | 2 +- .../src/helpers/bluePublicAllocator.test.ts | 20 +- .../src/helpers/bluePublicAllocator.ts | 30 +- .../helpers/computeVaultV1Reallocations.ts | 2 +- packages/morpho-sdk/src/index.test.ts | 8 - packages/morpho-sdk/src/index.ts | 1 - packages/morpho-sdk/src/utils.ts | 1 - .../test/actions/blue/reallocations.test.ts | 36 +-- packages/morpho-ts/src/abis.ts | 11 +- packages/morpho-ts/src/addresses.test.ts | 41 ++- packages/morpho-ts/src/addresses.ts | 153 +++++++++-- 41 files changed, 797 insertions(+), 524 deletions(-) create mode 100644 docs/tibs/TIB-2026-08-18-vault-v2-blue-reallocation-api.md rename packages/morpho-sdk/{test/reallocationData/publicAllocator.test.ts => src/entities/vaultV1ReallocationData.publicAllocator.test.ts} (99%) rename packages/morpho-sdk/src/entities/{vaultV2ReallocationData.test.ts => vaultV2BlueReallocationData.test.ts} (86%) rename packages/morpho-sdk/src/entities/{vaultV2ReallocationData.ts => vaultV2BlueReallocationData.ts} (84%) delete mode 100644 packages/morpho-sdk/src/index.test.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index a21c5670a..c2baf3a7d 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -6,11 +6,11 @@ "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add the canonical `vaultV2BluePublicAllocatorAbi` and per-chain `bluePublicAllocator` deployments to `morpho-ts`, move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while normalizing them before repeated use, add chain-registry-backed deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. +Add canonical `vaultV1PublicAllocatorAbi` and `vaultV2BluePublicAllocatorAbi` exports plus per-chain `vaultV1PublicAllocator` and `vaultV2BluePublicAllocator` registry entries to `morpho-ts`, preserving `publicAllocatorAbi` and `publicAllocator` as deprecated V1 aliases. Move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while normalizing them before repeated use, add chain-registry-backed deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. -Use coherent versioned names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2ReallocationData`, `computeVaultV1Reallocations`, `computeVaultV2Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Preserve the published V1 names as deprecated aliases. +Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Compatibility note: `VaultV2MorphoMarketV1AdapterV2.ids()` now declares its existing three-element result as `readonly [Hash, Hash, Hash]`. The runtime values and ordering are unchanged, and derived allocation identifiers are immutable descriptors. We intentionally accept this TypeScript assignability tightening in the minor release; callers that explicitly require a mutable `Hash[]` can copy the tuple with `[...adapter.ids(params)]`. diff --git a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md index 6e4cbabf4..409dc424d 100644 --- a/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -77,7 +77,7 @@ This TIB freezes that Vault V2 design. | V2 action input | `VaultV2BlueReallocation` | | V2 Bundler actions | `vaultV2BluePublicAllocatorReallocate`, `vaultV2BluePublicAllocatorAllocateFromIdle` | | V2 allocator ABI | `vaultV2BluePublicAllocatorAbi` | -| V2 allocator address | `ChainAddresses.bluePublicAllocator` | +| V2 allocator address | `ChainAddresses.vaultV2BluePublicAllocator` | | Shared action union | `BlueReallocation` | | V2 options | `VaultV2BluePublicAllocatorOptions` | | V2 config | `VaultV2PublicAllocatorConfig`, `VaultV2MarketPublicAllocatorConfig` | @@ -219,7 +219,7 @@ as a vault-keyed set of adapter addresses. Market state carries `adapter`, ## Fetching `vaultV2BluePublicAllocatorAbi` includes the three allocator mapping reads and -`vaultData`. Fetchers resolve `bluePublicAllocator` from `parameters.chainId`, +`vaultData`. Fetchers resolve `vaultV2BluePublicAllocator` from `parameters.chainId`, defaulting to the client chain id: - `fetchVaultV2PublicAllocatorConfig(vault, client, parameters?)`; @@ -456,7 +456,7 @@ source and target thresholds plus an internal 100% fallback. - [BluePublicAllocator deployments](https://github.com/morpho-org/deployments/pull/233) - [TIB-2026-06-16 shared-liquidity target-utilization metric](./TIB-2026-06-16-shared-liquidity-target-utilization-metric.md) - `packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts` -- `packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts` +- `packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts` - `packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts` - `packages/blue-sdk/src/vault/v2/VaultV2Utils.ts` - `packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts` diff --git a/docs/tibs/TIB-2026-08-18-vault-v2-blue-reallocation-api.md b/docs/tibs/TIB-2026-08-18-vault-v2-blue-reallocation-api.md new file mode 100644 index 000000000..4704fa2fa --- /dev/null +++ b/docs/tibs/TIB-2026-08-18-vault-v2-blue-reallocation-api.md @@ -0,0 +1,65 @@ +# TIB-2026-08-18: Vault V2 Blue reallocation API + +| Field | Value | +| -------------- | --------------------------------------------------------- | +| **Status** | Accepted | +| **Date** | 2026-08-18 | +| **Author** | @Rubilmax | +| **Scope** | Package: `morpho-sdk` | +| **Supersedes** | TIB-2026-07-29 V2 reallocation API naming and entrypoints | + +--- + +## Context + +TIB-2026-07-29 named the state `VaultV2ReallocationData` and exposed both an +operation-aware entity method and a standalone function delegating to that +method. The generic Vault V2 name is ambiguous because Vault V2 can allocate +through Morpho Blue and Midnight adapters. The standalone function adds no +behavior and preserves no released API. + +The existing unversioned `MorphoBlue.getReallocationData` method only fetches +Vault V1 state, making its protocol scope unclear. + +## Decision + +- Name the entity `VaultV2BlueReallocationData` and its input + `InputVaultV2BlueReallocationData`. +- Use one `computeVaultV2BlueReallocations` method. Without an operation it + discovers every friendly call; with `options.operation` it returns the + amount-aware plan. Both modes return the calls and simulated state. +- Remove the standalone V2 planner and the separate unreleased + `computeVaultV2BlueReallocationsForOperation` method. +- Add `MorphoBlue.getVaultV2BlueReallocationData` to fetch the target market, + Vault V2 accrual trees, and BluePublicAllocator state at one block. +- Add `MorphoBlue.getVaultV1ReallocationData`. Keep `getReallocationData` as a + deprecated delegating alias. +- Use the same unversioned liquidity-metric method names on both data classes; + the class name supplies protocol context. + +V1's configurable source and trigger utilization options remain deprecated. +V2 therefore retains fixed 90% friendly source and target thresholds plus its +internal 100% fallback. + +V2 keeps the latest market or vault `lastUpdate` as its default simulation +timestamp. A target market can be older than a source or vault; using only its +timestamp would evaluate one fetched snapshot at inconsistent times. Callers +can pass the fetched block timestamp explicitly. + +V2 candidate cap sizing remains a binary search. Cap fit is monotonic but not +linear because the candidate amount changes penalty donations, +`firstTotalAssets`, rounded market shares, and potentially shared allocation +IDs. Direct headroom subtraction cannot reproduce the contract-exact boundary. + +The V2 mutation helper remains private and returns a cloned state. It must keep +penalty accounting, vault accrual, adapter shares, allocations, and canonical +market references coherent as one transition. V1's protected helper is a +legacy test seam, not a public extension point to copy. + +## Consequences + +- V2 Blue-specific symbols are unambiguous beside future Midnight state APIs. +- Root, `/utils`, and `/entities` expose no standalone V2 planner. +- No V2 compatibility aliases are needed because the renamed surface was + unreleased. +- Published V1 names continue through the existing deprecation policy. diff --git a/packages/blue-sdk-viem/AGENTS.md b/packages/blue-sdk-viem/AGENTS.md index 7ec59df63..cb5db342b 100644 --- a/packages/blue-sdk-viem/AGENTS.md +++ b/packages/blue-sdk-viem/AGENTS.md @@ -12,7 +12,7 @@ - Normalize unsafe user addresses with `safeGetAddress`, not lowercasing alone. - Typed-data helpers return `TypedDataDefinition`, e.g. `getPermitTypedData(...)`. - Re-export ABI literals from `@morpho-org/morpho-ts` when they exist there; keep local ABI declarations only for Blue-specific viem surfaces absent from `morpho-ts`. -- Vault V2 BluePublicAllocator fetchers resolve the chain's single allocator from `bluePublicAllocator` in the address registry, using `parameters.chainId` or the client chain id. The hydrated-vault batch fetch derives supported adapter/market/allocation ids, returns active adapters as a set separate from market configs, defaults to one deployless read, and falls back to direct reads. +- Vault V2 BluePublicAllocator fetchers resolve the chain's single allocator from `vaultV2BluePublicAllocator` in the address registry, using `parameters.chainId` or the client chain id. The hydrated-vault batch fetch derives supported adapter/market/allocation ids, returns active adapters as a set separate from market configs, defaults to one deployless read, and falls back to direct reads. ## Continuous Improvement diff --git a/packages/blue-sdk-viem/src/abis.ts b/packages/blue-sdk-viem/src/abis.ts index 821a7845c..db76695f1 100644 --- a/packages/blue-sdk-viem/src/abis.ts +++ b/packages/blue-sdk-viem/src/abis.ts @@ -14,6 +14,7 @@ export { publicAllocatorAbi, vaultV1AdapterAbi, vaultV1AdapterFactoryAbi, + vaultV1PublicAllocatorAbi, vaultV2Abi, vaultV2BluePublicAllocatorAbi, vaultV2FactoryAbi, diff --git a/packages/blue-sdk-viem/src/fetch/Vault.ts b/packages/blue-sdk-viem/src/fetch/Vault.ts index 638119400..f5524bfc6 100644 --- a/packages/blue-sdk-viem/src/fetch/Vault.ts +++ b/packages/blue-sdk-viem/src/fetch/Vault.ts @@ -15,7 +15,7 @@ import { getChainId, readContract } from "viem/actions"; import { metaMorphoAbi, metaMorphoFactoryAbi, - publicAllocatorAbi, + vaultV1PublicAllocatorAbi, } from "../abis.js"; import { abi, code } from "../queries/GetVault.js"; import type { DeploylessFetchParameters } from "../types.js"; @@ -60,7 +60,7 @@ export async function fetchVault( ) { parameters.chainId ??= await getChainId(client); - const { publicAllocator, metaMorphoFactory } = getChainAddresses( + const { vaultV1PublicAllocator, metaMorphoFactory } = getChainAddresses( parameters.chainId, ); @@ -95,7 +95,11 @@ export async function fetchVault( abi, code, functionName: "query", - args: [address, publicAllocator ?? zeroAddress, metaMorphoFactory], + args: [ + address, + vaultV1PublicAllocator ?? zeroAddress, + metaMorphoFactory, + ], }); return new Vault({ @@ -115,7 +119,7 @@ export async function fetchVault( pendingGuardian, pendingTimelock, publicAllocatorConfig: - publicAllocator != null ? publicAllocatorConfig : undefined, + vaultV1PublicAllocator != null ? publicAllocatorConfig : undefined, supplyQueue: supplyQueue as MarketId[], withdrawQueue: withdrawQueue as MarketId[], totalSupply, @@ -247,13 +251,13 @@ export async function fetchVault( abi: metaMorphoAbi, functionName: "withdrawQueueLength", }), - publicAllocator != null && + vaultV1PublicAllocator != null && readContract(client, { ...parameters, address, abi: metaMorphoAbi, functionName: "isAllocator", - args: [publicAllocator], + args: [vaultV1PublicAllocator], }), readContract(client, { ...parameters, @@ -284,22 +288,22 @@ export async function fetchVault( publicAllocatorConfigPromise = Promise.all([ readContract(client, { ...parameters, - address: publicAllocator!, - abi: publicAllocatorAbi, + address: vaultV1PublicAllocator!, + abi: vaultV1PublicAllocatorAbi, functionName: "admin", args: [address], }), readContract(client, { ...parameters, - address: publicAllocator!, - abi: publicAllocatorAbi, + address: vaultV1PublicAllocator!, + abi: vaultV1PublicAllocatorAbi, functionName: "fee", args: [address], }), readContract(client, { ...parameters, - address: publicAllocator!, - abi: publicAllocatorAbi, + address: vaultV1PublicAllocator!, + abi: vaultV1PublicAllocatorAbi, functionName: "accruedFee", args: [address], }), diff --git a/packages/blue-sdk-viem/src/fetch/VaultMarketPublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/VaultMarketPublicAllocatorConfig.ts index 9d4b929e8..36da44757 100644 --- a/packages/blue-sdk-viem/src/fetch/VaultMarketPublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/VaultMarketPublicAllocatorConfig.ts @@ -5,7 +5,7 @@ import { } from "@morpho-org/blue-sdk"; import type { Address, Client } from "viem"; import { getChainId, readContract } from "viem/actions"; -import { publicAllocatorAbi } from "../abis.js"; +import { vaultV1PublicAllocatorAbi } from "../abis.js"; import type { FetchParameters } from "../types.js"; /** @@ -49,14 +49,14 @@ export async function fetchVaultMarketPublicAllocatorConfig( ) { parameters.chainId ??= await getChainId(client); - const { publicAllocator } = getChainAddresses(parameters.chainId); + const { vaultV1PublicAllocator } = getChainAddresses(parameters.chainId); /* v8 ignore next: V8 does not credit this guard's empty false branch; both paths are tested. */ - if (publicAllocator == null) return; + if (vaultV1PublicAllocator == null) return; const [maxIn, maxOut] = await readContract(client, { ...parameters, - address: publicAllocator, - abi: publicAllocatorAbi, + address: vaultV1PublicAllocator, + abi: vaultV1PublicAllocatorAbi, functionName: "flowCaps", args: [vault, marketId], }); diff --git a/packages/blue-sdk-viem/src/fetch/fetch.test.ts b/packages/blue-sdk-viem/src/fetch/fetch.test.ts index bd37a61bd..cc0bb2a21 100644 --- a/packages/blue-sdk-viem/src/fetch/fetch.test.ts +++ b/packages/blue-sdk-viem/src/fetch/fetch.test.ts @@ -52,7 +52,7 @@ import { permissionedErc20WrapperAbi, permit2Abi, preLiquidationAbi, - publicAllocatorAbi, + vaultV1PublicAllocatorAbi, whitelistControllerAggregatorV2Abi, wrappedBackedTokenAbi, wstEthAbi, @@ -223,7 +223,7 @@ function mockVaultMarketConfigReads( }); mockRead(handle, { address: ADDRESSES.publicAllocator, - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, functionName: "flowCaps", result: [33n, 34n], }); @@ -1171,7 +1171,7 @@ describe("vault fetchers", () => { const handle = createMockClient(mainnet); mockRead(handle, { address: ADDRESSES.publicAllocator, - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, functionName: "flowCaps", result: [27n, 28n], }); @@ -1527,19 +1527,19 @@ describe("vault fetchers", () => { }); mockRead(handle, { address: ADDRESSES.publicAllocator, - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, functionName: "admin", result: USER, }); mockRead(handle, { address: ADDRESSES.publicAllocator, - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, functionName: "fee", result: 56n, }); mockRead(handle, { address: ADDRESSES.publicAllocator, - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, functionName: "accruedFee", result: 57n, }); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts index 87b30b93a..6f8a71072 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts @@ -40,7 +40,7 @@ describe("Vault V2 public allocator fetchers on fork", () => { assert(fixtureBytecode != null); const allocator = getChainAddress( ChainId.EthMainnet, - "bluePublicAllocator", + "vaultV2BluePublicAllocator", ); await client.setCode({ address: allocator, bytecode: fixtureBytecode }); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts index 83244d466..f2dc5aae6 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts @@ -23,7 +23,7 @@ import { fetchVaultV2PublicAllocatorData, } from "./VaultV2PublicAllocatorConfig.js"; -const ALLOCATOR = getChainAddress(mainnet.id, "bluePublicAllocator"); +const ALLOCATOR = getChainAddress(mainnet.id, "vaultV2BluePublicAllocator"); const VAULT: Address = "0x0000000000000000000000000000000000000002"; const ADAPTER: Address = "0x0000000000000000000000000000000000000003"; const ASSET: Address = "0x0000000000000000000000000000000000000004"; diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts index 314375ba7..a40dca3dc 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts @@ -54,7 +54,7 @@ export async function fetchVaultV2PublicAllocatorConfig( parameters: FetchParameters = {}, ): Promise { const chainId = parameters.chainId ?? (await getChainId(client)); - const allocator = getChainAddress(chainId, "bluePublicAllocator"); + const allocator = getChainAddress(chainId, "vaultV2BluePublicAllocator"); const [canPullFromIdle, penalty] = await readContract(client, { ...parameters, address: allocator, @@ -117,7 +117,7 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( parameters: FetchParameters = {}, ): Promise { const chainId = parameters.chainId ?? (await getChainId(client)); - const allocator = getChainAddress(chainId, "bluePublicAllocator"); + const allocator = getChainAddress(chainId, "vaultV2BluePublicAllocator"); const [absoluteCap, canPullFromMarket] = await Promise.all([ readContract(client, { ...parameters, @@ -189,7 +189,7 @@ export async function fetchVaultV2PublicAllocatorData( { deployless = true, ...parameters }: DeploylessFetchParameters = {}, ) { const chainId = parameters.chainId ?? (await getChainId(client)); - const allocator = getChainAddress(chainId, "bluePublicAllocator"); + const allocator = getChainAddress(chainId, "vaultV2BluePublicAllocator"); const marketRequests: { readonly adapter: Address; readonly adapterMarketCapId: Hash; diff --git a/packages/blue-sdk-viem/test/Vault.test.ts b/packages/blue-sdk-viem/test/Vault.test.ts index 5cd62a19c..8a6004be2 100644 --- a/packages/blue-sdk-viem/test/Vault.test.ts +++ b/packages/blue-sdk-viem/test/Vault.test.ts @@ -8,7 +8,7 @@ import { vaults } from "@morpho-org/morpho-test"; import { zeroAddress, zeroHash } from "viem"; import { describe, expect } from "vitest"; import { Vault } from "../src/augment/Vault.js"; -import { metaMorphoAbi, publicAllocatorAbi } from "../src/index.js"; +import { metaMorphoAbi, vaultV1PublicAllocatorAbi } from "../src/index.js"; import { test, test2 } from "./setup.js"; const { steakUsdc, steakPaxg } = vaults[ChainId.EthMainnet]; @@ -27,12 +27,15 @@ describe("augment/Vault", () => { address: steakUsdc.address, abi: metaMorphoAbi, functionName: "setIsAllocator", - args: [addressesRegistry[ChainId.EthMainnet].publicAllocator, true], + args: [ + addressesRegistry[ChainId.EthMainnet].vaultV1PublicAllocator, + true, + ], }); await client.writeContract({ account: owner, - address: addressesRegistry[ChainId.EthMainnet].publicAllocator, - abi: publicAllocatorAbi, + address: addressesRegistry[ChainId.EthMainnet].vaultV1PublicAllocator, + abi: vaultV1PublicAllocatorAbi, functionName: "setFee", args: [steakUsdc.address, 1n], }); diff --git a/packages/blue-sdk-viem/test/VaultMarketConfig.test.ts b/packages/blue-sdk-viem/test/VaultMarketConfig.test.ts index 7f1aab91c..b26f1d285 100644 --- a/packages/blue-sdk-viem/test/VaultMarketConfig.test.ts +++ b/packages/blue-sdk-viem/test/VaultMarketConfig.test.ts @@ -6,7 +6,7 @@ import { import { markets, vaults } from "@morpho-org/morpho-test"; import { describe, expect } from "vitest"; import { VaultMarketConfig } from "../src/augment/VaultMarketConfig.js"; -import { metaMorphoAbi, publicAllocatorAbi } from "../src/index.js"; +import { metaMorphoAbi, vaultV1PublicAllocatorAbi } from "../src/index.js"; import { test } from "./setup.js"; const { usdc_wstEth } = markets[ChainId.EthMainnet]; @@ -26,19 +26,22 @@ describe("augment/VaultMarketConfig", () => { address: steakUsdc.address, abi: metaMorphoAbi, functionName: "setIsAllocator", - args: [addressesRegistry[ChainId.EthMainnet].publicAllocator, true], + args: [ + addressesRegistry[ChainId.EthMainnet].vaultV1PublicAllocator, + true, + ], }); await client.writeContract({ account: owner, - address: addressesRegistry[ChainId.EthMainnet].publicAllocator, - abi: publicAllocatorAbi, + address: addressesRegistry[ChainId.EthMainnet].vaultV1PublicAllocator, + abi: vaultV1PublicAllocatorAbi, functionName: "setFee", args: [steakUsdc.address, 1n], }); await client.writeContract({ account: owner, - address: addressesRegistry[ChainId.EthMainnet].publicAllocator, - abi: publicAllocatorAbi, + address: addressesRegistry[ChainId.EthMainnet].vaultV1PublicAllocator, + abi: vaultV1PublicAllocatorAbi, functionName: "setFlowCaps", args: [ steakUsdc.address, diff --git a/packages/liquidity-sdk-viem/src/loader.test.ts b/packages/liquidity-sdk-viem/src/loader.test.ts index 32ed1f845..2c8ec1a49 100644 --- a/packages/liquidity-sdk-viem/src/loader.test.ts +++ b/packages/liquidity-sdk-viem/src/loader.test.ts @@ -9,7 +9,7 @@ import { blueAbi, metaMorphoAbi, metaMorphoFactoryAbi, - publicAllocatorAbi, + vaultV1PublicAllocatorAbi, } from "@morpho-org/blue-sdk-viem"; import { BLUE_API_GRAPHQL_URL } from "@morpho-org/morpho-ts"; import { createMockClient, type MockClientHandle } from "@morpho-org/test/mock"; @@ -29,11 +29,11 @@ import { mainnet } from "viem/chains"; import { afterEach, describe, expect, test } from "vitest"; import { LiquidityLoader } from "./loader.js"; -const { morpho, publicAllocator, metaMorphoFactory } = getChainAddresses( +const { morpho, vaultV1PublicAllocator, metaMorphoFactory } = getChainAddresses( ChainId.EthMainnet, ); -if (publicAllocator == null || metaMorphoFactory == null) { +if (vaultV1PublicAllocator == null || metaMorphoFactory == null) { throw new Error( "Ethereum mainnet addresses must include allocator contracts", ); @@ -340,7 +340,7 @@ const setupLoaderMockClient = ({ address: vault, abi: metaMorphoAbi, functionName: "isAllocator", - args: [publicAllocator], + args: [vaultV1PublicAllocator], result: true, }); addRead({ @@ -380,22 +380,22 @@ const setupLoaderMockClient = ({ }); addRead({ - address: publicAllocator, - abi: publicAllocatorAbi, + address: vaultV1PublicAllocator, + abi: vaultV1PublicAllocatorAbi, functionName: "admin", args: [vault], result: owner, }); addRead({ - address: publicAllocator, - abi: publicAllocatorAbi, + address: vaultV1PublicAllocator, + abi: vaultV1PublicAllocatorAbi, functionName: "fee", args: [vault], result: 0n, }); addRead({ - address: publicAllocator, - abi: publicAllocatorAbi, + address: vaultV1PublicAllocator, + abi: vaultV1PublicAllocatorAbi, functionName: "accruedFee", args: [vault], result: 0n, @@ -416,8 +416,8 @@ const setupLoaderMockClient = ({ result: [targetPendingCapValue, targetPendingCapValidAt], }); addRead({ - address: publicAllocator, - abi: publicAllocatorAbi, + address: vaultV1PublicAllocator, + abi: vaultV1PublicAllocatorAbi, functionName: "flowCaps", args: [vault, targetMarketId], result: [10_000n, 0n], @@ -437,8 +437,8 @@ const setupLoaderMockClient = ({ result: [10_000n, 0n], }); addRead({ - address: publicAllocator, - abi: publicAllocatorAbi, + address: vaultV1PublicAllocator, + abi: vaultV1PublicAllocatorAbi, functionName: "flowCaps", args: [vault, sourceMarketId], result: [0n, 10_000n], diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index 92671f21d..c21a4bb42 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -29,7 +29,7 @@ Protocol terms used across this package's docs and JSDoc: - **PublicAllocator V1** — MetaMorpho allocator that moves liquidity from one or more sorted source markets into a target via `reallocateTo(...)`; each call pays one `fee`. - **BluePublicAllocator** — the single canonical Vault V2 allocator registered per chain, which moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies adapter addresses; the SDK resolves the allocator from the chain registry. Each call passes the vault's configured WAD-scaled `uint64 penalty`; the allocator pulls `ceil(assets × penalty / WAD)` of the target loan token from Bundler3 and donates it directly to the vault. Its canonical ABI export is `vaultV2BluePublicAllocatorAbi`. - **VaultExitBundlesV1** — standalone periphery for exiting an illiquid VaultV1 or single-adapter VaultV2 into idle underlying assets and/or Morpho Blue supply positions. -- **Shared-liquidity naming** — `VaultV1ReallocationData`, `InputVaultV1ReallocationData`, `computeVaultV1Reallocations`, and `VaultV1BlueReallocation` are canonical for PublicAllocator V1. Their published predecessors (`ReallocationData`, `InputReallocationData`, `computeReallocations`, `getMarketPublicReallocations`, and `VaultReallocation`) remain deprecated aliases. `VaultV2ReallocationData.computeVaultV2Reallocations` and `computeVaultV2Reallocations` accept `VaultV2BluePublicAllocatorOptions` and produce flat, action-ready `VaultV2BlueReallocation` calls. +- **Shared-liquidity naming** — `VaultV1ReallocationData`, `InputVaultV1ReallocationData`, `computeVaultV1Reallocations`, and `VaultV1BlueReallocation` are canonical for PublicAllocator V1. Their published predecessors (`ReallocationData`, `InputReallocationData`, `computeReallocations`, `getMarketPublicReallocations`, and `VaultReallocation`) remain deprecated aliases. `MorphoBlue.getVaultV1ReallocationData` is the canonical V1 fetcher; unversioned `getReallocationData` is its deprecated alias. `MorphoBlue.getVaultV2BlueReallocationData` fetches the Blue-specific V2 snapshot. `VaultV2BlueReallocationData.computeVaultV2BlueReallocations` discovers every friendly call by default or accepts an optional operation to produce an amount-aware plan; both modes return flat, action-ready `VaultV2BlueReallocation` calls and their simulated state. ### Bundler actions diff --git a/packages/morpho-sdk/src/abis.ts b/packages/morpho-sdk/src/abis.ts index 111286e1d..76da26a7f 100644 --- a/packages/morpho-sdk/src/abis.ts +++ b/packages/morpho-sdk/src/abis.ts @@ -22,6 +22,7 @@ export { publicAllocatorAbi, vaultV1AdapterAbi, vaultV1AdapterFactoryAbi, + vaultV1PublicAllocatorAbi, vaultV2Abi, vaultV2BluePublicAllocatorAbi, vaultV2FactoryAbi, diff --git a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts index c13ede105..4238822bc 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -6,6 +6,7 @@ import { bundler3Abi, generalAdapter1Abi, publicAllocatorAbi, + vaultV1PublicAllocatorAbi, vaultV2BluePublicAllocatorAbi, } from "../../abis.js"; import { @@ -14,7 +15,9 @@ import { } from "../../types/index.js"; import { blueBorrow } from "./borrow.js"; -const allocator = getChainAddresses(ChainId.EthMainnet).bluePublicAllocator; +const allocator = getChainAddresses( + ChainId.EthMainnet, +).vaultV2BluePublicAllocator; const vaultV1 = "0x0000000000000000000000000000000000000012"; const sourceAdapter = "0x0000000000000000000000000000000000000013"; const targetAdapter = "0x0000000000000000000000000000000000000014"; @@ -104,7 +107,7 @@ describe("blueBorrow Blue Public Allocator", () => { }); const publicAllocatorCall = decodeFunctionData({ - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, data: calls[1]!.data, }); expect(publicAllocatorCall.functionName).toBe("reallocateTo"); @@ -182,4 +185,8 @@ describe("blueBorrow Blue Public Allocator", () => { canonicalVaultV2BluePublicAllocatorAbi, ); }); + + test("keeps the deprecated Vault V1 PublicAllocator ABI alias", () => { + expect(publicAllocatorAbi).toBe(vaultV1PublicAllocatorAbi); + }); }); diff --git a/packages/morpho-sdk/src/actions/blue/refinance.test.ts b/packages/morpho-sdk/src/actions/blue/refinance.test.ts index e9d2a9805..566add3bb 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.test.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.test.ts @@ -394,7 +394,8 @@ describe("blueRefinance", () => { }); const VAULT: Address = "0xBEEf5aFE88eF73337e5070aB2855d37dBF5493A4"; const REALLOC_FEE = parseUnits("0.01", 18); - const V2_ALLOCATOR = getChainAddresses(mainnet.id).bluePublicAllocator!; + const V2_ALLOCATOR = getChainAddresses(mainnet.id) + .vaultV2BluePublicAllocator!; const V2_VAULT: Address = "0x0000000000000000000000000000000000000012"; const SOURCE_ADAPTER: Address = "0x0000000000000000000000000000000000000013"; diff --git a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts index 9a2968bb4..34b520d08 100644 --- a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts +++ b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts @@ -7,8 +7,6 @@ import { import { blueAbi, fetchAccrualVaultV2, - fetchMarket, - fetchVaultV2PublicAllocatorData, readContractRestructured, vaultV2Abi, } from "@morpho-org/blue-sdk-viem"; @@ -34,7 +32,6 @@ import { deployVaultV2, submitAndAcceptVaultV2Call, } from "../../../test/helpers/vaultV2.js"; -import { VaultV2ReallocationData } from "../../entities/vaultV2ReallocationData.js"; import { isRequirementApproval, isRequirementBlueAuthorization, @@ -72,7 +69,7 @@ describe("Blue actions with Vault V2 reallocations", () => { const { morpho, bundler3, - bluePublicAllocator: allocator, + vaultV2BluePublicAllocator: allocator, } = getChainAddresses(base.id); assert(allocator != null); const sourceAssets = parseUnits("20", 6); @@ -376,7 +373,7 @@ describe("Blue actions with Vault V2 reallocations", () => { client, }) => { const anvilClient = client as AnvilTestClient; - const { morpho, bluePublicAllocator: allocator } = getChainAddresses( + const { morpho, vaultV2BluePublicAllocator: allocator } = getChainAddresses( base.id, ); assert(allocator != null); @@ -525,18 +522,20 @@ describe("Blue actions with Vault V2 reallocations", () => { amount: postLossIdleAssets, }); - const [vaultData, targetMarketData, block] = await Promise.all([ - fetchAccrualVaultV2(vault, client), - fetchMarket(targetMarket.id, client), - client.getBlock(), - ]); - const allocatorData = await fetchVaultV2PublicAllocatorData( - vaultData, - client, - ); + const block = await client.getBlock(); + const market = client + .extend(morphoViemExtension()) + .morpho.blue(targetMarket, base.id); + const reallocationData = await market.getVaultV2BlueReallocationData({ + vaultAddresses: [vault], + block, + }); + const vaultData = reallocationData.getVault(vault); const targetMarketParamsId = keccak256(targetIdData[2]); - const targetAllocation = allocatorData.allocations[targetMarketParamsId]; - assert(targetAllocation != null); + const targetAllocation = reallocationData.getAllocation( + vault, + targetMarketParamsId, + ); const realTotalAssets = vaultData.accrualAdapters.reduce( (assets, adapter) => assets + adapter.realAssets(block.timestamp), vaultData.assetBalance, @@ -544,23 +543,9 @@ describe("Blue actions with Vault V2 reallocations", () => { const expectedMaximum = MathLib.wMulDown(realTotalAssets, relativeCap) - targetAllocation.allocation; - const reallocationData = new VaultV2ReallocationData({ - chainId: base.id, - markets: { [targetMarket.id]: targetMarketData }, - vaults: { [vault]: vaultData }, - allocations: { [vault]: allocatorData.allocations }, - publicAllocatorConfigs: { - [vault]: allocatorData.publicAllocatorConfig, - }, - activeAdapters: { [vault]: allocatorData.activeAdapters }, - marketPublicAllocatorConfigs: { - [vault]: allocatorData.marketPublicAllocatorConfigs, - }, - }); - expect(block.timestamp).toBe(vaultData.lastUpdate); expect(realTotalAssets).toBeLessThan(vaultData._totalAssets); - const result = reallocationData.computeVaultV2Reallocations( + const result = reallocationData.computeVaultV2BlueReallocations( targetMarket.id, { timestamp: block.timestamp }, ); diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts index 20df3d3b3..7a431bf38 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts @@ -14,7 +14,7 @@ import { import type { BlueReallocation } from "../../types/index.js"; import { blueWithdraw } from "./withdraw.js"; -const allocator = getChainAddresses(mainnet.id).bluePublicAllocator!; +const allocator = getChainAddresses(mainnet.id).vaultV2BluePublicAllocator!; const vault: Address = "0x0000000000000000000000000000000000000012"; const sourceAdapter: Address = "0x0000000000000000000000000000000000000013"; const targetAdapter: Address = "0x0000000000000000000000000000000000000014"; diff --git a/packages/morpho-sdk/src/bundler/actions.test.ts b/packages/morpho-sdk/src/bundler/actions.test.ts index c1e6c0ebb..48d63a245 100644 --- a/packages/morpho-sdk/src/bundler/actions.test.ts +++ b/packages/morpho-sdk/src/bundler/actions.test.ts @@ -7,7 +7,7 @@ import { blueAbi, erc2612Abi, permit2Abi, - publicAllocatorAbi, + vaultV1PublicAllocatorAbi, vaultV2BluePublicAllocatorAbi, } from "@morpho-org/blue-sdk-viem"; import fc from "fast-check"; @@ -39,7 +39,7 @@ describe("BundlerAction", () => { morpho, permit2, publicAllocator, - bluePublicAllocator: allocator, + vaultV2BluePublicAllocator: allocator, bundler3: { bundler3, generalAdapter1 }, } = getChainAddresses(chainId); @@ -1553,7 +1553,7 @@ describe("BundlerAction", () => { ), ); const decoded = decodeFunctionData({ - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, data: call.data, }); diff --git a/packages/morpho-sdk/src/bundler/actions.ts b/packages/morpho-sdk/src/bundler/actions.ts index 0f206d52c..17890b500 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -1,12 +1,13 @@ import { getChainAddresses, type InputMarketParams, + MathLib, } from "@morpho-org/blue-sdk"; import { blueAbi, erc2612Abi, permit2Abi, - publicAllocatorAbi, + vaultV1PublicAllocatorAbi, vaultV2BluePublicAllocatorAbi, } from "@morpho-org/blue-sdk-viem"; import { @@ -23,7 +24,6 @@ import { zeroHash, } from "viem"; import { bundler3Abi, coreAdapterAbi, generalAdapter1Abi } from "../abis.js"; -import { computeBluePublicAllocatorPenaltyAssets } from "../helpers/bluePublicAllocator.js"; import { BundlerErrors } from "../types/error.js"; import type { Action, @@ -1436,16 +1436,16 @@ export namespace BundlerAction { supplyMarketParams: InputMarketParams, skipRevert = false, ): BundlerCall[] { - const { publicAllocator } = getChainAddresses(chainId); - if (publicAllocator == null) { + const { vaultV1PublicAllocator } = getChainAddresses(chainId); + if (vaultV1PublicAllocator == null) { throw new BundlerErrors.UnexpectedAction("reallocateTo", chainId); } return [ { - to: publicAllocator, + to: vaultV1PublicAllocator, data: encodeFunctionData({ - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, functionName: "reallocateTo", args: [vault, withdrawals, supplyMarketParams], }), @@ -1531,7 +1531,8 @@ export namespace BundlerAction { penalty: bigint, skipRevert = false, ): BundlerCall[] { - const { bluePublicAllocator: allocator } = getChainAddresses(chainId); + const { vaultV2BluePublicAllocator: allocator } = + getChainAddresses(chainId); if (allocator == null) { throw new BundlerErrors.UnexpectedAction( "vaultV2BluePublicAllocatorReallocate", @@ -1539,10 +1540,7 @@ export namespace BundlerAction { ); } const calls: BundlerCall[] = []; - const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( - assets, - penalty, - ); + const penaltyAssets = MathLib.wMulUp(assets, penalty); if (skipRevert && penaltyAssets > 0n) { throw new BundlerErrors.SkippableAllocatorPenalty(penaltyAssets); } @@ -1646,7 +1644,8 @@ export namespace BundlerAction { penalty: bigint, skipRevert = false, ): BundlerCall[] { - const { bluePublicAllocator: allocator } = getChainAddresses(chainId); + const { vaultV2BluePublicAllocator: allocator } = + getChainAddresses(chainId); if (allocator == null) { throw new BundlerErrors.UnexpectedAction( "vaultV2BluePublicAllocatorAllocateFromIdle", @@ -1654,10 +1653,7 @@ export namespace BundlerAction { ); } const calls: BundlerCall[] = []; - const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( - assets, - penalty, - ); + const penaltyAssets = MathLib.wMulUp(assets, penalty); if (skipRevert && penaltyAssets > 0n) { throw new BundlerErrors.SkippableAllocatorPenalty(penaltyAssets); } diff --git a/packages/morpho-sdk/src/entities/AGENTS.md b/packages/morpho-sdk/src/entities/AGENTS.md index e9c7cfaf5..54f3b0f7a 100644 --- a/packages/morpho-sdk/src/entities/AGENTS.md +++ b/packages/morpho-sdk/src/entities/AGENTS.md @@ -15,6 +15,6 @@ See [`packages/morpho-sdk/AGENTS.md`](../../AGENTS.md) routing summary. ## Shared liquidity -`MorphoBlue.borrow()`, `supplyCollateralBorrow()`, `withdraw()`, and `refinance()` accept optional reallocations. Consumer-supplied reallocation plans and vault allowlists accept any iterable and are normalized once before lazy or repeated use; ordered outputs remain readonly arrays. The entity validates their state-independent shape before returning requirements, and the pure action repeats the same validation before encoding. `getReallocationData` may fetch the inputs needed to compute reallocations, but action encoding stays outside the entity fetch path. +`MorphoBlue.borrow()`, `supplyCollateralBorrow()`, `withdraw()`, and `refinance()` accept optional reallocations. Consumer-supplied reallocation plans and vault allowlists accept any iterable and are normalized once before lazy or repeated use; ordered outputs remain readonly arrays. The entity validates their state-independent shape before returning requirements, and the pure action repeats the same validation before encoding. `getVaultV1ReallocationData` and `getVaultV2BlueReallocationData` fetch the versioned inputs needed to compute reallocations; deprecated `getReallocationData` delegates to the V1 fetcher. Action encoding stays outside every entity fetch path. -`VaultV1ReallocationData` is the entity-level state container for PublicAllocator V1 simulations; `ReallocationData` remains its deprecated compatibility alias. `VaultV2ReallocationData` owns the separate BluePublicAllocator state model. Their public maps are readable snapshots for inspection; state transitions stay on their methods and return cloned instances of the same versioned class. +`VaultV1ReallocationData` is the entity-level state container for PublicAllocator V1 simulations; `ReallocationData` remains its deprecated compatibility alias. `VaultV2BlueReallocationData` owns the separate BluePublicAllocator state model. Their public maps are readable snapshots for inspection; state transitions stay on their methods and return cloned instances of the same versioned class. diff --git a/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts b/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts index d5cc57727..abe7658b5 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts @@ -1,12 +1,12 @@ import { createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { CbbtcUsdcBlue } from "../../../test/fixtures/blue.js"; import { morphoViemExtension } from "../../client/index.js"; import { ChainIdMismatchError } from "../../types/index.js"; import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; -describe("MorphoBlue.getReallocations", () => { +describe("MorphoBlue reallocation APIs", () => { test("error: ChainIdMismatchError when reallocation data chain differs from market chain", () => { const publicClient = createPublicClient({ chain: mainnet, @@ -24,4 +24,42 @@ describe("MorphoBlue.getReallocations", () => { }), ).toThrow(ChainIdMismatchError); }); + + test("deprecated getReallocationData delegates to the Vault V1 fetcher", async () => { + const publicClient = createPublicClient({ + chain: mainnet, + transport: http("https://rpc.example"), + }); + const market = publicClient + .extend(morphoViemExtension()) + .morpho.blue(CbbtcUsdcBlue, mainnet.id); + const expected = new VaultV1ReallocationData({ chainId: mainnet.id }); + const canonical = vi + .spyOn(market, "getVaultV1ReallocationData") + .mockResolvedValue(expected); + const params = { + vaultAddresses: [], + block: { number: 0n, timestamp: 0n }, + } as const; + + await expect(market.getReallocationData(params)).resolves.toBe(expected); + expect(canonical).toHaveBeenCalledWith(params); + }); + + test("error: getVaultV2BlueReallocationData validates the client chain", async () => { + const publicClient = createPublicClient({ + chain: mainnet, + transport: http("https://rpc.example"), + }); + const market = publicClient + .extend(morphoViemExtension()) + .morpho.blue(CbbtcUsdcBlue, mainnet.id + 1); + + await expect( + market.getVaultV2BlueReallocationData({ + vaultAddresses: [], + block: { number: 0n, timestamp: 0n }, + }), + ).rejects.toBeInstanceOf(ChainIdMismatchError); + }); }); diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 7aa0133e6..10be58dba 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -11,10 +11,12 @@ import { } from "@morpho-org/blue-sdk"; import { fetchAccrualPosition, + fetchAccrualVaultV2, fetchMarket, fetchPosition, fetchVault, fetchVaultMarketConfig, + fetchVaultV2PublicAllocatorData, } from "@morpho-org/blue-sdk-viem"; import { Time } from "@morpho-org/morpho-ts"; import { type Address, isAddressEqual } from "viem"; @@ -90,6 +92,7 @@ import { WithdrawExceedsCollateralError, } from "../../types/index.js"; import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; +import { VaultV2BlueReallocationData } from "../vaultV2BlueReallocationData.js"; export interface BlueActions { /** @@ -510,6 +513,24 @@ export interface BlueActions { * @returns A VaultV1ReallocationData instance populated with all required data. * @throws {ChainIdMismatchError} when the client chain does not match this market. */ + getVaultV1ReallocationData: (params: { + vaultAddresses: readonly Address[]; + block: { + readonly number: bigint; + readonly timestamp: bigint; + }; + }) => Promise; + + /** + * Fetches Vault V1 PublicAllocator state using the deprecated unversioned name. + * + * @param params.vaultAddresses - Addresses of MetaMorpho vaults that allocate to this market. + * @param params.block.number - Block number used for every RPC read. + * @param params.block.timestamp - Timestamp corresponding to the fetched block. + * @returns A `VaultV1ReallocationData` snapshot populated from one block. + * @throws {ChainIdMismatchError} when the client chain does not match this market. + * @deprecated Use {@link getVaultV1ReallocationData} instead. + */ getReallocationData: (params: { vaultAddresses: readonly Address[]; block: { @@ -518,6 +539,26 @@ export interface BlueActions { }; }) => Promise; + /** + * Fetches Vault V2 BluePublicAllocator state for this target market. + * + * Reads the target Morpho Blue market, each Vault V2 accrual tree, and each + * vault's BluePublicAllocator permissions and allocation caps at one block. + * + * @param params.vaultAddresses - Vault V2 addresses to inspect for market or idle liquidity. + * @param params.block.number - Block number used for every RPC read. + * @param params.block.timestamp - Timestamp corresponding to the fetched block. + * @returns A `VaultV2BlueReallocationData` snapshot ready for discovery or operation planning. + * @throws {ChainIdMismatchError} when the client chain does not match this market. + */ + getVaultV2BlueReallocationData: (params: { + vaultAddresses: readonly Address[]; + block: { + readonly number: bigint; + readonly timestamp: bigint; + }; + }) => Promise; + /** * Computes vault reallocations for a borrow or withdraw on this market. * @@ -527,7 +568,7 @@ export interface BlueActions { * Pass `{ borrowAmount }` for a borrow (legacy alias, equivalent to `{ operation: "borrow", * amount }`) or `{ operation: "withdraw", amount }` for a loan-asset withdraw. * - * @param params.reallocationData - The current on-chain state (from {@link getReallocationData}). + * @param params.reallocationData - The current on-chain state (from {@link getVaultV1ReallocationData}). * @param params.operation - The operation driving the reallocation (`"borrow"` or `"withdraw"`). * Defaults to `"borrow"` when `borrowAmount` is provided. * @param params.amount - The borrow or withdraw amount used to compute the post-state utilization. @@ -1749,13 +1790,30 @@ export class MorphoBlue implements BlueActions { /** * Fetches all on-chain inputs needed to compute public allocator reallocations. * - * @param params - Reallocation data fetch parameters. * @param params.vaultAddresses - Vaults to inspect for source-market liquidity. - * @param params.block - Block number and timestamp used for consistent RPC reads. + * @param params.block.number - Block number used for every RPC read. + * @param params.block.timestamp - Timestamp corresponding to the fetched block. * @returns Reallocation data ready for {@link getReallocations}. * @throws {ChainIdMismatchError} when the client chain does not match this market. + * @example + * ```ts + * import { markets, vaults } from "@morpho-org/morpho-test"; + * import { createPublicClient, http } from "viem"; + * import { mainnet } from "viem/chains"; + * import { morphoViemExtension } from "@morpho-org/morpho-sdk"; + * import type { VaultV1ReallocationData } from "@morpho-org/morpho-sdk/entities"; + * + * const client = createPublicClient({ chain: mainnet, transport: http() }) + * .extend(morphoViemExtension()); + * const market = client.morpho.blue(markets[mainnet.id].usdc_wbtc, mainnet.id); + * const block = await client.getBlock(); + * const data: VaultV1ReallocationData = await market.getVaultV1ReallocationData({ + * vaultAddresses: [vaults[mainnet.id].steakUsdc.address], + * block, + * }); + * ``` */ - async getReallocationData({ + async getVaultV1ReallocationData({ vaultAddresses, block, }: { @@ -1866,6 +1924,128 @@ export class MorphoBlue implements BlueActions { }); } + /** + * Fetches Vault V1 PublicAllocator state using the deprecated unversioned name. + * + * @param params.vaultAddresses - Addresses of MetaMorpho vaults that allocate to this market. + * @param params.block.number - Block number used for every RPC read. + * @param params.block.timestamp - Timestamp corresponding to the fetched block. + * @returns A `VaultV1ReallocationData` snapshot populated from one block. + * @throws {ChainIdMismatchError} when the client chain does not match this market. + * @deprecated Use {@link getVaultV1ReallocationData} instead. + * @example + * ```ts + * const data = await market.getReallocationData({ vaultAddresses, block }); + * // Equivalent to market.getVaultV1ReallocationData({ vaultAddresses, block }). + * ``` + */ + getReallocationData(params: { + vaultAddresses: readonly Address[]; + block: { + readonly number: bigint; + readonly timestamp: bigint; + }; + }): Promise { + return this.getVaultV1ReallocationData(params); + } + + /** + * Fetches Vault V2 BluePublicAllocator state for this target market. + * + * Reads the target Morpho Blue market, each Vault V2 accrual tree, and each + * vault's BluePublicAllocator permissions and allocation caps at one block. + * + * @param params.vaultAddresses - Vault V2 addresses to inspect for market or idle liquidity. + * @param params.block.number - Block number used for every RPC read. + * @param params.block.timestamp - Timestamp corresponding to the fetched block. + * @returns A `VaultV2BlueReallocationData` snapshot ready for discovery or operation planning. + * @throws {ChainIdMismatchError} when the client chain does not match this market. + * @example + * ```ts + * import { markets } from "@morpho-org/morpho-test"; + * import { createPublicClient, http } from "viem"; + * import { mainnet } from "viem/chains"; + * import { morphoViemExtension } from "@morpho-org/morpho-sdk"; + * import type { VaultV2BlueReallocationData } from "@morpho-org/morpho-sdk/entities"; + * + * const client = createPublicClient({ chain: mainnet, transport: http() }) + * .extend(morphoViemExtension()); + * const market = client.morpho.blue(markets[mainnet.id].usdc_wbtc, mainnet.id); + * const block = await client.getBlock(); + * const keyrockUsdcVaultV2 = "0xfDE48B9B8568189f629Bc5209bf5FA826336557a"; + * const data: VaultV2BlueReallocationData = + * await market.getVaultV2BlueReallocationData({ + * vaultAddresses: [keyrockUsdcVaultV2], + * block, + * }); + * ``` + */ + async getVaultV2BlueReallocationData({ + vaultAddresses, + block, + }: { + vaultAddresses: readonly Address[]; + block: { + readonly number: bigint; + readonly timestamp: bigint; + }; + }): Promise { + validateChainId(this.client.viemClient.chain?.id, this.chainId); + + const client = this.client.viemClient; + const fetchParams = { + blockNumber: block.number, + chainId: this.chainId, + deployless: this.client.options.supportDeployless, + }; + const [targetMarket, vaultEntries] = await Promise.all([ + fetchMarket(this.marketParams.id, client, fetchParams), + Promise.all( + vaultAddresses.map(async (address) => { + const vault = await fetchAccrualVaultV2(address, client, fetchParams); + const publicAllocatorData = await fetchVaultV2PublicAllocatorData( + vault, + client, + fetchParams, + ); + return { publicAllocatorData, vault }; + }), + ), + ]); + + return new VaultV2BlueReallocationData({ + chainId: this.chainId, + markets: { [targetMarket.id]: targetMarket }, + vaults: Object.fromEntries( + vaultEntries.map(({ vault }) => [vault.address, vault]), + ), + allocations: Object.fromEntries( + vaultEntries.map(({ publicAllocatorData, vault }) => [ + vault.address, + publicAllocatorData.allocations, + ]), + ), + publicAllocatorConfigs: Object.fromEntries( + vaultEntries.map(({ publicAllocatorData, vault }) => [ + vault.address, + publicAllocatorData.publicAllocatorConfig, + ]), + ), + activeAdapters: Object.fromEntries( + vaultEntries.map(({ publicAllocatorData, vault }) => [ + vault.address, + publicAllocatorData.activeAdapters, + ]), + ), + marketPublicAllocatorConfigs: Object.fromEntries( + vaultEntries.map(({ publicAllocatorData, vault }) => [ + vault.address, + publicAllocatorData.marketPublicAllocatorConfigs, + ]), + ), + }); + } + /** * Computes public allocator reallocations for a borrow or withdraw on this market. * @@ -1873,7 +2053,7 @@ export class MorphoBlue implements BlueActions { * or `{ operation, amount }` for a borrow or loan-asset withdraw. * * @param params - Reallocation computation parameters. - * @param params.reallocationData - State returned by {@link getReallocationData}. + * @param params.reallocationData - State returned by {@link getVaultV1ReallocationData}. * @param params.operation - The operation driving the reallocation (`"borrow"` or `"withdraw"`). * @param params.amount - The borrow or withdraw amount used to compute the post-state utilization. * @param params.borrowAmount - {@deprecated Pass `{ operation: "borrow", amount }` instead.} diff --git a/packages/morpho-sdk/src/entities/index.ts b/packages/morpho-sdk/src/entities/index.ts index 6520960fd..4929ae20a 100644 --- a/packages/morpho-sdk/src/entities/index.ts +++ b/packages/morpho-sdk/src/entities/index.ts @@ -76,7 +76,6 @@ export { } from "./vaultV1ReallocationData.js"; export { MorphoVaultV2 } from "./vaultV2/index.js"; export { - computeVaultV2Reallocations, - type InputVaultV2ReallocationData, - VaultV2ReallocationData, -} from "./vaultV2ReallocationData.js"; + type InputVaultV2BlueReallocationData, + VaultV2BlueReallocationData, +} from "./vaultV2BlueReallocationData.js"; diff --git a/packages/morpho-sdk/test/reallocationData/publicAllocator.test.ts b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.publicAllocator.test.ts similarity index 99% rename from packages/morpho-sdk/test/reallocationData/publicAllocator.test.ts rename to packages/morpho-sdk/src/entities/vaultV1ReallocationData.publicAllocator.test.ts index 4afe18cb4..c2394e092 100644 --- a/packages/morpho-sdk/test/reallocationData/publicAllocator.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.publicAllocator.test.ts @@ -14,7 +14,7 @@ import { describe, expect, test } from "vitest"; import { type InputVaultV1ReallocationData, VaultV1ReallocationData, -} from "../../src/entities/vaultV1ReallocationData.js"; +} from "./vaultV1ReallocationData.js"; const timestamp = 12345n; diff --git a/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts index e5d5db4ad..e0221bee1 100644 --- a/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts @@ -360,7 +360,7 @@ export class VaultV1ReallocationData implements InputVaultV1ReallocationData { * const marketParams = markets[mainnet.id].usdc_wbtc; * const market = client.morpho.blue(marketParams, mainnet.id); * const block = await client.getBlock(); - * const reallocationData = await market.getReallocationData({ + * const reallocationData = await market.getVaultV1ReallocationData({ * vaultAddresses: [vaults[mainnet.id].steakUsdc.address], * block: { number: block.number, timestamp: block.timestamp }, * }); @@ -518,7 +518,7 @@ export class VaultV1ReallocationData implements InputVaultV1ReallocationData { * const marketParams = markets[mainnet.id].usdc_wbtc; * const market = client.morpho.blue(marketParams, mainnet.id); * const block = await client.getBlock(); - * const reallocationData = await market.getReallocationData({ + * const reallocationData = await market.getVaultV1ReallocationData({ * vaultAddresses: [vaults[mainnet.id].steakUsdc.address], * block: { number: block.number, timestamp: block.timestamp }, * }); @@ -577,7 +577,7 @@ export class VaultV1ReallocationData implements InputVaultV1ReallocationData { * const marketParams = markets[mainnet.id].usdc_wbtc; * const market = client.morpho.blue(marketParams, mainnet.id); * const block = await client.getBlock(); - * const reallocationData = await market.getReallocationData({ + * const reallocationData = await market.getVaultV1ReallocationData({ * vaultAddresses: [vaults[mainnet.id].steakUsdc.address], * block: { number: block.number, timestamp: block.timestamp }, * }); diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts similarity index 86% rename from packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts rename to packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index 7b2e072d5..791bcd6b6 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -21,10 +21,7 @@ import { ReallocationWithdrawExceedsMarketSupplyError, UnknownReallocationMarketError, } from "../types/index.js"; -import { - computeVaultV2Reallocations, - VaultV2ReallocationData, -} from "./vaultV2ReallocationData.js"; +import { VaultV2BlueReallocationData } from "./vaultV2BlueReallocationData.js"; const TIMESTAMP = 1_700_000_000n; const VAULT = "0x0000000000000000000000000000000000000002"; @@ -268,7 +265,7 @@ const makeFixture = ({ ); return { - data: new VaultV2ReallocationData({ + data: new VaultV2BlueReallocationData({ chainId: ChainId.EthMainnet, markets: { [targetMarket.id]: targetMarket, @@ -313,14 +310,14 @@ const makeFixture = ({ }; }; -describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { +describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { test("default: returns an action-ready market reallocation and cloned post-state", () => { const { data, sourceExpectedAssets, sourceIds, targetIds } = makeFixture(); expect(data.activeAdapters[VAULT]).toStrictEqual( new Set([TARGET_ADAPTER, SOURCE_ADAPTER]), ); - const result = data.computeVaultV2Reallocations(targetParams.id); + const result = data.computeVaultV2BlueReallocations(targetParams.id); expect(result.reallocations).toStrictEqual([ { @@ -353,7 +350,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { const { data } = makeFixture({ allocatorActiveAdapters }); expect( - data.computeVaultV2Reallocations(targetParams.id).reallocations, + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, ).toStrictEqual([]); } }); @@ -402,7 +399,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { 500n, {}, ); - const sharedData = new VaultV2ReallocationData({ + const sharedData = new VaultV2BlueReallocationData({ chainId: data.chainId, markets: data.markets, vaults: { @@ -447,7 +444,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { sharedData.getAdapter(SECOND_VAULT, SECOND_TARGET_ADAPTER).markets[0], ).toBe(initialCanonicalMarket); - const result = sharedData.computeVaultV2Reallocations(targetParams.id); + const result = sharedData.computeVaultV2BlueReallocations(targetParams.id); const finalCanonicalMarket = result.data.getMarket(targetParams.id); expect(result.reallocations).toHaveLength(2); @@ -553,7 +550,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { fixtureVault.assetBalance, fixtureVault.forceDeallocatePenalties, ); - const input = new VaultV2ReallocationData({ + const input = new VaultV2BlueReallocationData({ chainId: data.chainId, markets: data.markets, vaults: { [VAULT]: inputVault }, @@ -600,7 +597,9 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { inputNested?.accrualVaultV1.allocations, ); - const simulated = input.computeVaultV2Reallocations(targetMarket.id).data; + const simulated = input.computeVaultV2BlueReallocations( + targetMarket.id, + ).data; const simulatedLegacy = simulated .getVault(VAULT) .accrualAdapters.find( @@ -646,8 +645,8 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { const { data } = makeFixture({ idle: 300n }); const initialData = data.clone(); - const first = data.computeVaultV2Reallocations(targetParams.id); - const second = data.computeVaultV2Reallocations(targetParams.id); + const first = data.computeVaultV2BlueReallocations(targetParams.id); + const second = data.computeVaultV2BlueReallocations(targetParams.id); expect(second.reallocations).toStrictEqual(first.reallocations); expect(second.data).toStrictEqual(first.data); @@ -657,7 +656,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { test("behavior: ranks market liquidity before idle and depletes both sources", () => { const { data, sourceExpectedAssets } = makeFixture({ idle: 300n }); - const result = data.computeVaultV2Reallocations(targetParams.id); + const result = data.computeVaultV2BlueReallocations(targetParams.id); expect( result.reallocations.map(({ from, assets, penalty }) => ({ @@ -678,7 +677,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); expect( - data.computeVaultV2Reallocations(targetParams.id).reallocations, + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, ).toStrictEqual([]); }); @@ -687,7 +686,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { sourceUntracked: 900n, }); - const result = data.computeVaultV2Reallocations(targetParams.id); + const result = data.computeVaultV2BlueReallocations(targetParams.id); expect(result.reallocations[0]?.assets).toBe(sourceExpectedAssets); expect(result.data.getVault(VAULT).assetBalance).toBe(1n); @@ -701,7 +700,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); expect( - data.computeVaultV2Reallocations(targetParams.id).reallocations, + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, ).toStrictEqual([]); }); @@ -723,7 +722,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); expect( - data.computeVaultV2Reallocations(targetParams.id).reallocations, + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, ).toStrictEqual([]); }); @@ -737,7 +736,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ], }); - const result = data.computeVaultV2Reallocations(targetParams.id); + const result = data.computeVaultV2BlueReallocations(targetParams.id); expect(result.reallocations[0]?.assets).toBe(500n); expect(result.data.getVault(VAULT)._totalAssets).toBe(1_000n); @@ -758,7 +757,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ], }); - const result = data.computeVaultV2Reallocations(targetParams.id); + const result = data.computeVaultV2BlueReallocations(targetParams.id); expect(result.reallocations).toHaveLength(1); expect(result.reallocations[0]?.assets).toBe(450n); @@ -783,7 +782,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ], }); - const result = data.computeVaultV2Reallocations(targetParams.id); + const result = data.computeVaultV2BlueReallocations(targetParams.id); expect( result.reallocations.map(({ from, assets }) => ({ @@ -812,7 +811,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); expect( - data.computeVaultV2Reallocations(targetParams.id).reallocations[0] + data.computeVaultV2BlueReallocations(targetParams.id).reallocations[0] ?.assets, ).toBe(MathLib.MAX_UINT_128); }); @@ -830,7 +829,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); expect( - data.computeVaultV2Reallocations(targetParams.id).reallocations, + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, ).toStrictEqual([]); }); @@ -848,7 +847,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); expect( - data.computeVaultV2Reallocations(targetParams.id).reallocations, + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, ).toStrictEqual([]); }); @@ -859,7 +858,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); expect(() => - data.computeVaultV2Reallocations(targetParams.id), + data.computeVaultV2BlueReallocations(targetParams.id), ).not.toThrow(); }); @@ -879,7 +878,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { ], }); - const result = data.computeVaultV2Reallocations(targetParams.id); + const result = data.computeVaultV2BlueReallocations(targetParams.id); expect(result.reallocations[0]?.assets).toBe(500n); expect(result.data.getVault(VAULT)._totalAssets).toBe(1_000n); @@ -889,7 +888,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { const { data } = makeFixture(); expect( - data.computeVaultV2Reallocations(targetParams.id, { enabled: false }) + data.computeVaultV2BlueReallocations(targetParams.id, { enabled: false }) .reallocations, ).toStrictEqual([]); }); @@ -899,7 +898,7 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { data.markets[targetParams.id] = undefined; expect(() => - data.computeVaultV2Reallocations(targetParams.id, { + data.computeVaultV2BlueReallocations(targetParams.id, { timestamp: TIMESTAMP, }), ).toThrow(UnknownReallocationMarketError); @@ -912,13 +911,13 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); expect( - data.computeVaultV2Reallocations(targetParams.id, { + data.computeVaultV2BlueReallocations(targetParams.id, { maxPenalty: 7n, }).reallocations, ).toStrictEqual([]); expect( data - .computeVaultV2Reallocations(targetParams.id, { + .computeVaultV2BlueReallocations(targetParams.id, { maxPenalty: 8n, }) .reallocations.map(({ from, assets }) => ({ @@ -932,30 +931,26 @@ describe("VaultV2ReallocationData.computeVaultV2Reallocations", () => { }); }); -describe("computeVaultV2Reallocations", () => { +describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations operation", () => { test("default: caps friendly reallocations to the 90% target", () => { const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 90n }); - const reallocations = computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 20n, + const result = data.computeVaultV2BlueReallocations(targetParams.id, { + operation: { type: "borrow", amount: 20n }, }); - expect(reallocations).toHaveLength(1); - expect(reallocations[0]?.assets).toBe(23n); + expect(result.reallocations).toHaveLength(1); + expect(result.reallocations[0]?.assets).toBe(23n); + expect(result.data.getMarket(targetParams.id).totalSupplyAssets).toBe(123n); }); test("behavior: rounds required supply up to the utilization target", () => { const { data } = makeFixture({ targetSupply: 1n, targetBorrow: 0n }); - const reallocations = computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 1n, - }); + const { reallocations } = data.computeVaultV2BlueReallocations( + targetParams.id, + { operation: { type: "borrow", amount: 1n } }, + ); expect(reallocations[0]?.assets).toBe(1n); }); @@ -967,19 +962,17 @@ describe("computeVaultV2Reallocations", () => { sourceLastUpdate: TIMESTAMP + 2n, }); - const defaultReallocations = computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 20n, - }); - const explicitReallocations = computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 20n, - options: { timestamp: TIMESTAMP + 2n }, - }); + const defaultReallocations = data.computeVaultV2BlueReallocations( + targetParams.id, + { operation: { type: "borrow", amount: 20n } }, + ); + const explicitReallocations = data.computeVaultV2BlueReallocations( + targetParams.id, + { + timestamp: TIMESTAMP + 2n, + operation: { type: "borrow", amount: 20n }, + }, + ); expect(defaultReallocations).toStrictEqual(explicitReallocations); }); @@ -992,13 +985,13 @@ describe("computeVaultV2Reallocations", () => { sourceBorrow: 950n, }); - const reallocations = computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 40n, - options: { reallocatableVaults: [VAULT as Address].values() }, - }); + const { reallocations } = data.computeVaultV2BlueReallocations( + targetParams.id, + { + reallocatableVaults: [VAULT as Address].values(), + operation: { type: "borrow", amount: 40n }, + }, + ); expect(reallocations[0]?.assets).toBe(40n); }); @@ -1006,12 +999,10 @@ describe("computeVaultV2Reallocations", () => { test("behavior: plans a loan-asset withdraw", () => { const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 90n }); - const reallocations = computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "withdraw", - amount: 10n, - }); + const { reallocations } = data.computeVaultV2BlueReallocations( + targetParams.id, + { operation: { type: "withdraw", amount: 10n } }, + ); expect(reallocations[0]?.assets).toBe(10n); }); @@ -1022,12 +1013,10 @@ describe("computeVaultV2Reallocations", () => { targetBorrow: 100n, idle: 300n, }); - const reallocations = computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 1_100n, - }); + const { reallocations } = data.computeVaultV2BlueReallocations( + targetParams.id, + { operation: { type: "borrow", amount: 1_100n } }, + ); const tx = blueBorrow({ market: { @@ -1055,22 +1044,16 @@ describe("computeVaultV2Reallocations", () => { }); expect( - computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 1n, - options: { maxPenalty: 6n }, - }), + data.computeVaultV2BlueReallocations(targetParams.id, { + maxPenalty: 6n, + operation: { type: "borrow", amount: 1n }, + }).reallocations, ).toStrictEqual([]); expect( - computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 1n, - options: { maxPenalty: 7n }, - })[0]?.assets, + data.computeVaultV2BlueReallocations(targetParams.id, { + maxPenalty: 7n, + operation: { type: "borrow", amount: 1n }, + }).reallocations[0]?.assets, ).toBe(2n); }); @@ -1082,11 +1065,8 @@ describe("computeVaultV2Reallocations", () => { }); expect(() => - computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 100n, + data.computeVaultV2BlueReallocations(targetParams.id, { + operation: { type: "borrow", amount: 100n }, }), ).toThrow(InsufficientSharedLiquidityError); }); @@ -1095,11 +1075,8 @@ describe("computeVaultV2Reallocations", () => { const { data } = makeFixture({ targetSupply: 100n }); expect(() => - computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "withdraw", - amount: 101n, + data.computeVaultV2BlueReallocations(targetParams.id, { + operation: { type: "withdraw", amount: 101n }, }), ).toThrow(ReallocationWithdrawExceedsMarketSupplyError); }); @@ -1116,11 +1093,8 @@ describe("computeVaultV2Reallocations", () => { const initialData = data.clone(); expect(() => - computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation, - amount, + data.computeVaultV2BlueReallocations(targetParams.id, { + operation: { type: operation, amount }, }), ).toThrow(NonPositiveInputError); expect(data).toStrictEqual(initialData); @@ -1130,19 +1104,17 @@ describe("computeVaultV2Reallocations", () => { test("behavior: disabled planning returns no calls", () => { const { data } = makeFixture(); - expect( - computeVaultV2Reallocations({ - reallocationData: data, - marketId: targetParams.id, - operation: "borrow", - amount: 0n, - options: { enabled: false }, - }), - ).toStrictEqual([]); + const result = data.computeVaultV2BlueReallocations(targetParams.id, { + enabled: false, + operation: { type: "borrow", amount: 0n }, + }); + + expect(result.reallocations).toStrictEqual([]); + expect(result.data).toBe(data); }); }); -describe("VaultV2ReallocationData liquidity metrics", () => { +describe("VaultV2BlueReallocationData liquidity metrics", () => { test("default: sums idle and market liquidity in target-utilization math", () => { const { data, sourceExpectedAssets } = makeFixture({ targetSupply: 100n, @@ -1150,14 +1122,14 @@ describe("VaultV2ReallocationData liquidity metrics", () => { idle: 300n, }); - expect(data.getPublicReallocationLiquidityVaultV2(targetParams.id)).toBe( + expect(data.getPublicReallocationLiquidity(targetParams.id)).toBe( sourceExpectedAssets + 300n, ); + expect(data.getAvailableLiquidityToUtilization(targetParams.id)).toBe( + 1_210n, + ); expect( - data.getAvailableLiquidityToUtilizationVaultV2(targetParams.id), - ).toBe(1_210n); - expect( - data.getAvailableLiquidityToUtilizationVaultV2( + data.getAvailableLiquidityToUtilization( targetParams.id, (MathLib.WAD * 8n) / 10n, ), diff --git a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts similarity index 84% rename from packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts rename to packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index ff68ab973..107c1e4c4 100644 --- a/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -18,7 +18,6 @@ import { } from "@morpho-org/blue-sdk"; import { _try, bigIntComparator } from "@morpho-org/morpho-ts"; import { type Address, type Hash, isAddressEqual } from "viem"; -import { computeBluePublicAllocatorPenaltyAssets } from "../helpers/bluePublicAllocator.js"; import { DEFAULT_SUPPLY_TARGET_UTILIZATION, DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, @@ -42,7 +41,7 @@ import { } from "../types/index.js"; /** Input state required to simulate Vault V2 BluePublicAllocator reallocations. */ -export interface InputVaultV2ReallocationData { +export interface InputVaultV2BlueReallocationData { /** Chain id associated with the fetched state. */ readonly chainId: number; /** Markets indexed by market id. */ @@ -198,12 +197,14 @@ const cloneVault = ( * * @example * ```ts - * import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; + * import { VaultV2BlueReallocationData } from "@morpho-org/morpho-sdk/entities"; * - * const data = new VaultV2ReallocationData(input); + * const data = new VaultV2BlueReallocationData(input); * ``` */ -export class VaultV2ReallocationData implements InputVaultV2ReallocationData { +export class VaultV2BlueReallocationData + implements InputVaultV2BlueReallocationData +{ /** Penalty donations created by this simulation, excluded as fresh shared-liquidity sources. */ private readonly donatedPenaltyAssets: Record; /** Transaction-frozen cap denominator for each vault touched by this plan. */ @@ -240,7 +241,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * * @param input - State fetched at one consistent block. */ - public constructor(input: InputVaultV2ReallocationData) { + public constructor(input: InputVaultV2BlueReallocationData) { this.chainId = input.chainId; this.markets = {}; this.vaults = {}; @@ -249,11 +250,11 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { this.activeAdapters = {}; this.marketPublicAllocatorConfigs = {}; this.donatedPenaltyAssets = - input instanceof VaultV2ReallocationData + input instanceof VaultV2BlueReallocationData ? { ...input.donatedPenaltyAssets } : {}; this.firstTotalAssets = - input instanceof VaultV2ReallocationData + input instanceof VaultV2BlueReallocationData ? { ...input.firstTotalAssets } : {}; @@ -333,7 +334,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * ``` */ public clone() { - return new VaultV2ReallocationData(this); + return new VaultV2BlueReallocationData(this); } /** @@ -456,94 +457,74 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { } /** - * Computes every friendly Vault V2 BluePublicAllocator call currently - * available for a target market. + * Computes Vault V2 BluePublicAllocator calls available for a target market. * - * The algorithm ranks action-ready calls by obtainable assets, includes idle - * liquidity, applies each winner to cloned state, and stops when every - * candidate is exhausted. Vaults whose configured penalty exceeds - * `options.maxPenalty` are ignored. Source markets are held below the - * SDK's default withdrawal-utilization ceiling. + * Without `options.operation`, discovers every friendly call. With an + * operation, caps the calls to the amount required by that borrow or + * loan-asset withdrawal and falls back to 100% source utilization only when + * friendly liquidity cannot cover the absolute shortfall. Vaults whose + * configured penalty exceeds `options.maxPenalty` are ignored. * * @param marketId - Target Blue market id. - * @param options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. + * @param options - Optional discovery controls and operation to support. * @returns Flat action-ready reallocations and their post-simulation state. - * @throws {UnknownReallocationMarketError} when the target market is absent. - * @example - * ```ts - * import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; - * - * const data = new VaultV2ReallocationData(input); - * const result = data.computeVaultV2Reallocations(targetMarketId, { timestamp }); - * ``` - */ - public computeVaultV2Reallocations( - marketId: MarketId, - options: VaultV2BluePublicAllocatorOptions = {}, - ) { - return this.computeVaultV2ReallocationsAtUtilization({ - marketId, - maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, - options, - }); - } - - /** - * Computes the action-ready Vault V2 reallocations required by a Blue borrow - * or loan-asset withdrawal. - * - * Friendly liquidity is considered first. When it cannot cover the absolute - * liquidity shortfall, the planner continues from that post-state up to 100% - * source utilization. Fee-bearing partial plans are rejected. - * - * @param params - Operation and discovery parameters. - * @param params.marketId - Target Blue market id. - * @param params.operation - Operation driving the reallocation. - * @param params.amount - Borrow or withdraw amount. - * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. - * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. - * @throws {NonPositiveInputError} when `amount <= 0n` and planning is enabled. + * @throws {NonPositiveInputError} when the operation amount is not positive and planning is enabled. * @throws {UnknownReallocationMarketError} when the target market is absent. * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. * @example * ```ts - * const reallocations = data.computeVaultV2ReallocationsForOperation({ - * marketId: targetMarketId, - * operation: "borrow", - * amount: 1_000_000n, - * options: { timestamp }, + * import { VaultV2BlueReallocationData } from "@morpho-org/morpho-sdk/entities"; + * + * const data = new VaultV2BlueReallocationData(input); + * const discovery = data.computeVaultV2BlueReallocations(targetMarketId, { + * timestamp, + * }); + * const plan = data.computeVaultV2BlueReallocations(targetMarketId, { + * timestamp, + * operation: { type: "borrow", amount: 1_000_000n }, * }); * ``` */ - public computeVaultV2ReallocationsForOperation({ - marketId, - operation, - amount, - options, - }: { - readonly marketId: MarketId; - readonly operation: "borrow" | "withdraw"; - readonly amount: bigint; - readonly options?: VaultV2BluePublicAllocatorOptions; - }): readonly VaultV2BlueReallocation[] { - if (options?.enabled === false) return []; + public computeVaultV2BlueReallocations( + marketId: MarketId, + options: VaultV2BluePublicAllocatorOptions & { + readonly operation?: { + readonly type: "borrow" | "withdraw"; + readonly amount: bigint; + }; + } = {}, + ): { + readonly reallocations: readonly VaultV2BlueReallocation[]; + readonly data: VaultV2BlueReallocationData; + } { + if (options.enabled === false) return { reallocations: [], data: this }; + + const operation = options.operation; + if (operation == null) + return this.computeVaultV2BlueReallocationsAtUtilization({ + marketId, + maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, + options, + }); + + const { amount, type } = operation; if (amount <= 0n) throw new NonPositiveInputError("amount", amount); const timestamp = - options?.timestamp == null + options.timestamp == null ? this.getLatestSnapshotTimestamp() : BigInt(options.timestamp); - const normalizedOptions = { + const normalizedOptions: VaultV2BluePublicAllocatorOptions = { ...options, timestamp, reallocatableVaults: - options?.reallocatableVaults == null + options.reallocatableVaults == null ? undefined : [...options.reallocatableVaults], }; const market = this.getMarket(marketId).accrueInterest(timestamp); - if (operation === "withdraw" && amount > market.totalSupplyAssets) { + if (type === "withdraw" && amount > market.totalSupplyAssets) { throw new ReallocationWithdrawExceedsMarketSupplyError({ marketId, withdrawAmount: amount, @@ -552,11 +533,11 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { } const newTotalBorrowAssets = - operation === "borrow" + type === "borrow" ? market.totalBorrowAssets + amount : market.totalBorrowAssets; const newTotalSupplyAssets = - operation === "withdraw" + type === "withdraw" ? market.totalSupplyAssets - amount : market.totalSupplyAssets; @@ -566,31 +547,32 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { totalBorrowAssets: newTotalBorrowAssets, }) <= DEFAULT_SUPPLY_TARGET_UTILIZATION ) - return []; + return { reallocations: [], data: this }; let requiredAssets = MathLib.wDivUp(newTotalBorrowAssets, DEFAULT_SUPPLY_TARGET_UTILIZATION) - newTotalSupplyAssets; - const friendly = this.computeVaultV2Reallocations( + const friendly = this.computeVaultV2BlueReallocationsAtUtilization({ marketId, - normalizedOptions, - ); + maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, + options: normalizedOptions, + }); const discovered = [...friendly.reallocations]; const friendlyMarket = friendly.data.getMarket(marketId); const friendlyBorrow = - operation === "borrow" + type === "borrow" ? friendlyMarket.totalBorrowAssets + amount : friendlyMarket.totalBorrowAssets; const friendlySupply = - operation === "withdraw" + type === "withdraw" ? friendlyMarket.totalSupplyAssets - amount : friendlyMarket.totalSupplyAssets; if (friendlyBorrow > friendlySupply) { requiredAssets = newTotalBorrowAssets - newTotalSupplyAssets; discovered.push( - ...friendly.data.computeVaultV2ReallocationsAtUtilization({ + ...friendly.data.computeVaultV2BlueReallocationsAtUtilization({ marketId, maxWithdrawalUtilization: MathLib.WAD, options: normalizedOptions, @@ -598,7 +580,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { ); } - if (requiredAssets <= 0n) return []; + if (requiredAssets <= 0n) return { reallocations: [], data: this }; const absoluteShortfall = newTotalBorrowAssets > newTotalSupplyAssets @@ -625,10 +607,23 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { }); } - return reallocations; + let data = this.clone(); + for (const currentMarket of Object.values(data.markets)) { + if (currentMarket != null) + data.setMarket(currentMarket.accrueInterest(timestamp)); + } + for (const reallocation of reallocations) { + data = data.cloneWithPublicReallocation({ + reallocation, + targetMarketId: marketId, + timestamp, + }); + } + + return { reallocations, data }; } - private computeVaultV2ReallocationsAtUtilization({ + private computeVaultV2BlueReallocationsAtUtilization({ marketId, maxWithdrawalUtilization, options = {}, @@ -638,7 +633,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { readonly options?: VaultV2BluePublicAllocatorOptions; }): { readonly reallocations: readonly VaultV2BlueReallocation[]; - readonly data: VaultV2ReallocationData; + readonly data: VaultV2BlueReallocationData; } { if (options.enabled === false) return { reallocations: [], data: this }; @@ -863,6 +858,9 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { const capCompatibleCandidates: VaultV2BlueReallocation[] = []; for (const reallocation of rawCandidates) { + // Cap fit is monotonic but not linear in assets: the amount changes + // penalty donations, firstTotalAssets, rounded shares, and possibly + // shared allocation IDs. Binary search finds the exact largest fit. let lower = 0n; let upper = reallocation.assets; @@ -944,14 +942,14 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @throws {UnknownReallocationMarketError} when the target market is absent. * @example * ```ts - * const liquidity = data.getPublicReallocationLiquidityVaultV2(targetMarketId); + * const liquidity = data.getPublicReallocationLiquidity(targetMarketId); * ``` */ - public getPublicReallocationLiquidityVaultV2( + public getPublicReallocationLiquidity( marketId: MarketId, options?: VaultV2BluePublicAllocatorOptions, ) { - return this.computeVaultV2ReallocationsAtUtilization({ + return this.computeVaultV2BlueReallocationsAtUtilization({ marketId, maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, options, @@ -969,11 +967,11 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { * @throws {UnknownReallocationMarketError} when the target market is absent. * @example * ```ts - * const liquidity = data.getAvailableLiquidityToUtilizationVaultV2(targetMarketId); + * const liquidity = data.getAvailableLiquidityToUtilization(targetMarketId); * ``` */ // biome-ignore lint/complexity/useMaxParams: mirrors the existing V1 metric API - public getAvailableLiquidityToUtilizationVaultV2( + public getAvailableLiquidityToUtilization( marketId: MarketId, utilization: bigint = DEFAULT_SUPPLY_TARGET_UTILIZATION, options?: VaultV2BluePublicAllocatorOptions, @@ -986,10 +984,10 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { if (DEFAULT_SUPPLY_TARGET_UTILIZATION > utilization) return market.getBorrowToUtilization(utilization); - const availableLiquidity = this.getPublicReallocationLiquidityVaultV2( - marketId, - { ...options, timestamp }, - ); + const availableLiquidity = this.getPublicReallocationLiquidity(marketId, { + ...options, + timestamp, + }); return MarketUtils.getBorrowToUtilization( { totalSupplyAssets: market.totalSupplyAssets + availableLiquidity, @@ -1023,7 +1021,7 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { let vault = data.getVault(reallocation.vault); const targetMarket = data.getMarket(targetMarketId); - const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( + const penaltyAssets = MathLib.wMulUp( reallocation.assets, reallocation.penalty, ); @@ -1175,72 +1173,3 @@ export class VaultV2ReallocationData implements InputVaultV2ReallocationData { } } } - -/** - * Computes action-ready Vault V2 BluePublicAllocator reallocations for a Blue - * borrow or loan-asset withdraw. - * - * @param params.reallocationData - Vault V2 reallocation state fetched at one block. - * @param params.marketId - Target Blue market id. - * @param params.operation - Operation driving the reallocation. - * @param params.amount - Borrow or withdraw amount. - * @param params.options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. - * @returns Flat Vault V2 reallocations accepted directly by Blue action builders. - * @throws {NonPositiveInputError} when `amount <= 0n` and planning is enabled. - * @throws {UnknownReallocationMarketError} when the target market is absent. - * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. - * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. - * @example - * ```ts - * import { Market, MarketParams } from "@morpho-org/blue-sdk"; - * import { - * computeVaultV2Reallocations, - * type VaultV2BlueReallocation, - * } from "@morpho-org/morpho-sdk"; - * import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; - * - * const timestamp = 1_700_000_000n; - * const marketParams = new MarketParams({ - * loanToken: "0x0000000000000000000000000000000000000001", - * collateralToken: "0x0000000000000000000000000000000000000002", - * oracle: "0x0000000000000000000000000000000000000003", - * irm: "0x0000000000000000000000000000000000000004", - * lltv: 860_000_000_000_000_000n, - * }); - * const market = new Market({ - * params: marketParams, - * totalSupplyAssets: 1_000_000n, - * totalBorrowAssets: 500_000n, - * totalSupplyShares: 1_000_000n, - * totalBorrowShares: 500_000n, - * lastUpdate: timestamp, - * fee: 0n, - * }); - * const reallocationData = new VaultV2ReallocationData({ - * chainId: 1, - * markets: { [marketParams.id]: market }, - * }); - * - * const reallocations: readonly VaultV2BlueReallocation[] = - * computeVaultV2Reallocations({ - * reallocationData, - * marketId: marketParams.id, - * operation: "borrow", - * amount: 100_000n, - * options: { timestamp }, - * }); - * - * console.log(reallocations); // [] — projected utilization remains below 90%. - * ``` - */ -export const computeVaultV2Reallocations = ({ - reallocationData, - ...params -}: { - readonly reallocationData: VaultV2ReallocationData; - readonly marketId: MarketId; - readonly operation: "borrow" | "withdraw"; - readonly amount: bigint; - readonly options?: VaultV2BluePublicAllocatorOptions; -}): readonly VaultV2BlueReallocation[] => - reallocationData.computeVaultV2ReallocationsForOperation(params); diff --git a/packages/morpho-sdk/src/helpers/AGENTS.md b/packages/morpho-sdk/src/helpers/AGENTS.md index 56d4056e3..900b630d2 100644 --- a/packages/morpho-sdk/src/helpers/AGENTS.md +++ b/packages/morpho-sdk/src/helpers/AGENTS.md @@ -9,7 +9,7 @@ Per-function contracts (arguments, return shapes, behavior) live as JSDoc on eac - **Encoders** (ABI encoding plus input validation, no I/O) — e.g. `encodeForceDeallocateCall(deallocation, onBehalf)`. ABI-encodes a single `VaultV2.forceDeallocate` calldata entry and throws `NonPositiveInputError` on a non-positive `amount`. The `data` field carries ABI-encoded `MarketParams` for the Morpho Market V1 adapter, or empty bytes otherwise. Internal sub-helpers (e.g. `encodeDeallocateData`) are not exported. - **Validators** (pure, throw typed errors) — `validateReallocations(...)`, `validateSlippageTolerance(...)`, `validatePositionHealth(...)`. Each enforces a public-API invariant: see the `error.ts` exports for the full list of error classes a caller may pattern-match on. - **Math / share-price helpers** — `computeMaxRepaySharePrice`, `computeMinBorrowSharePrice`, etc. Use `MAX_SLIPPAGE_TOLERANCE` and cap at `MAX_ABSOLUTE_SHARE_PRICE`. -- **Shared-liquidity** — `computeVaultV1Reallocations` builds PublicAllocator V1 reallocations for a borrow/withdraw; `computeReallocations` remains its deprecated compatibility alias. Vault V2 planning and state transitions live on `VaultV2ReallocationData`; the standalone `computeVaultV2Reallocations` export is a compatibility wrapper from the entity module, not a helper-layer dependency. `getSupplyTargetUtilization(marketId, options)` resolves the per-market → default → `DEFAULT_SUPPLY_TARGET_UTILIZATION` supply target for V1. Read-only liquidity metrics live on the corresponding versioned reallocation-data entity, not in this layer. +- **Shared-liquidity** — `computeVaultV1Reallocations` builds PublicAllocator V1 reallocations for a borrow/withdraw; `computeReallocations` remains its deprecated compatibility alias. Vault V2 planning and state transitions live on `VaultV2BlueReallocationData`. `getSupplyTargetUtilization(marketId, options)` resolves the per-market → default → `DEFAULT_SUPPLY_TARGET_UTILIZATION` supply target for V1. Read-only liquidity metrics live on the corresponding versioned reallocation-data entity, not in this layer. - **Metadata** — `addTransactionMetadata(tx, metadata)` appends hex-encoded analytics bytes to `tx.data`: an optional 4-byte unix timestamp followed by a 4-byte origin (timestamp is omitted when `metadata.timestamp` is falsy). Callers gate on `metadata` being provided; the helper itself is a no-op when `tx.data` is empty. ## Constants diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts index 8e02ec268..54590e746 100644 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts @@ -2,28 +2,10 @@ import { MarketParams } from "@morpho-org/blue-sdk"; import { describe, expect, test } from "vitest"; import { CbbtcUsdcBlue } from "../../test/fixtures/blue.js"; import type { BlueReallocation } from "../types/index.js"; -import { - computeBluePublicAllocatorPenaltyAssets, - computeVaultV2ReallocationPenaltyAssets, -} from "./bluePublicAllocator.js"; +import { computeVaultV2ReallocationPenaltyAssets } from "./bluePublicAllocator.js"; const marketParams = new MarketParams(CbbtcUsdcBlue); -describe("computeBluePublicAllocatorPenaltyAssets", () => { - test("default", () => { - expect( - computeBluePublicAllocatorPenaltyAssets( - 1_000_000n, - 1_000_000_000_000_000n, - ), - ).toBe(1_000n); - }); - - test("behavior: rounds each positive fractional penalty up", () => { - expect(computeBluePublicAllocatorPenaltyAssets(1n, 1n)).toBe(1n); - }); -}); - describe("computeVaultV2ReallocationPenaltyAssets", () => { test("default", () => { const reallocations: BlueReallocation[] = [ diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts index 379dec1ec..645556e40 100644 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts @@ -1,31 +1,6 @@ import { MathLib } from "@morpho-org/blue-sdk"; import type { BlueReallocation } from "../types/index.js"; -/** - * Computes the vault-asset penalty charged for one BluePublicAllocator call. - * - * Mirrors the contract's upward-rounded `assets * penalty / WAD` calculation. - * Callers must validate that `assets` and `penalty` are within the contract's - * accepted ranges before encoding a transaction. - * - * @param assets - Assets moved by the allocator call. - * @param penalty - Vault-configured proportional penalty, scaled by WAD. - * @returns Vault assets transferred by the caller directly to the vault. - * @example - * ```ts - * const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( - * 1_000_000n, - * 1_000_000_000_000_000n, - * ); - * // penaltyAssets === 1_000n - * ``` - * @internal - */ -export const computeBluePublicAllocatorPenaltyAssets = ( - assets: bigint, - penalty: bigint, -) => MathLib.wMulUp(assets, penalty); - /** * Sums the independently rounded vault-asset penalties in a mixed V1/V2 plan. * @@ -46,10 +21,7 @@ export const computeVaultV2ReallocationPenaltyAssets = ( let total = 0n; for (const reallocation of reallocations) { if ("from" in reallocation) - total += computeBluePublicAllocatorPenaltyAssets( - reallocation.assets, - reallocation.penalty, - ); + total += MathLib.wMulUp(reallocation.assets, reallocation.penalty); } return total; }; diff --git a/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts index 13253c8c2..23dcdc0c3 100644 --- a/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts @@ -126,7 +126,7 @@ const capVaultWithdrawals = ( * const marketParams = markets[mainnet.id].usdc_wbtc; * const market = client.morpho.blue(marketParams, mainnet.id); * const block = await client.getBlock(); - * const reallocationData = await market.getReallocationData({ + * const reallocationData = await market.getVaultV1ReallocationData({ * vaultAddresses: [vaults[mainnet.id].steakUsdc.address], * block: { number: block.number, timestamp: block.timestamp }, * }); diff --git a/packages/morpho-sdk/src/index.test.ts b/packages/morpho-sdk/src/index.test.ts deleted file mode 100644 index dda7444f3..000000000 --- a/packages/morpho-sdk/src/index.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { computeVaultV2Reallocations } from "./index.js"; - -describe("package root exports", () => { - test("exports computeVaultV2Reallocations", () => { - expect(computeVaultV2Reallocations).toBeTypeOf("function"); - }); -}); diff --git a/packages/morpho-sdk/src/index.ts b/packages/morpho-sdk/src/index.ts index 2f0e88d40..4630402d8 100644 --- a/packages/morpho-sdk/src/index.ts +++ b/packages/morpho-sdk/src/index.ts @@ -1,5 +1,4 @@ export * from "./actions/index.js"; export * from "./client/index.js"; -export { computeVaultV2Reallocations } from "./entities/vaultV2ReallocationData.js"; export * from "./helpers/index.js"; export * from "./types/index.js"; diff --git a/packages/morpho-sdk/src/utils.ts b/packages/morpho-sdk/src/utils.ts index 4f38bdfb6..95e82eb9c 100644 --- a/packages/morpho-sdk/src/utils.ts +++ b/packages/morpho-sdk/src/utils.ts @@ -64,7 +64,6 @@ export { transformValue, values, } from "@morpho-org/morpho-ts"; -export { computeVaultV2Reallocations } from "./entities/vaultV2ReallocationData.js"; export { computeReallocations, computeVaultV1Reallocations, diff --git a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts index 26de4caf0..0348c2e7f 100644 --- a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts +++ b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts @@ -1,5 +1,5 @@ import { getChainAddresses } from "@morpho-org/blue-sdk"; -import { publicAllocatorAbi } from "@morpho-org/blue-sdk-viem"; +import { vaultV1PublicAllocatorAbi } from "@morpho-org/blue-sdk-viem"; import { type Address, encodeFunctionData, parseUnits } from "viem"; import { base, mainnet } from "viem/chains"; @@ -209,7 +209,7 @@ describe("Borrow with reallocation fee", () => { }); // Impersonate the PA admin to set a fee on the Steakhouse vault - const { publicAllocator } = getChainAddresses(mainnet.id); + const { vaultV1PublicAllocator } = getChainAddresses(mainnet.id); await client.impersonateAccount({ address: PA_ADMIN }); await client.setBalance({ address: PA_ADMIN, @@ -217,9 +217,9 @@ describe("Borrow with reallocation fee", () => { }); await client.sendTransaction({ account: PA_ADMIN, - to: publicAllocator, + to: vaultV1PublicAllocator, data: encodeFunctionData({ - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, functionName: "setFee", args: [SteakhouseUsdcVaultV1.address, reallocationFee], }), @@ -241,7 +241,7 @@ describe("Borrow with reallocation fee", () => { ]; const publicAllocatorBalanceBefore = await client.getBalance({ - address: publicAllocator!, + address: vaultV1PublicAllocator!, }); const { @@ -299,7 +299,7 @@ describe("Borrow with reallocation fee", () => { ).toEqual(reallocationAmount + marketAccruedInterest); const publicAllocatorBalanceAfter = await client.getBalance({ - address: publicAllocator!, + address: vaultV1PublicAllocator!, }); expect(publicAllocatorBalanceAfter).toEqual( publicAllocatorBalanceBefore + reallocationFee, @@ -505,7 +505,7 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { }); // Impersonate the PA admin to set a fee on the Steakhouse vault - const { publicAllocator } = getChainAddresses(mainnet.id); + const { vaultV1PublicAllocator } = getChainAddresses(mainnet.id); await client.impersonateAccount({ address: PA_ADMIN }); await client.setBalance({ address: PA_ADMIN, @@ -513,9 +513,9 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { }); await client.sendTransaction({ account: PA_ADMIN, - to: publicAllocator, + to: vaultV1PublicAllocator, data: encodeFunctionData({ - abi: publicAllocatorAbi, + abi: vaultV1PublicAllocatorAbi, functionName: "setFee", args: [SteakhouseUsdcVaultV1.address, reallocationFee], }), @@ -537,7 +537,7 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { ]; const publicAllocatorBalanceBefore = await client.getBalance({ - address: publicAllocator!, + address: vaultV1PublicAllocator!, }); const { @@ -608,7 +608,7 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { ).toEqual(reallocationAmount + marketAccruedInterest); const publicAllocatorBalanceAfter = await client.getBalance({ - address: publicAllocator!, + address: vaultV1PublicAllocator!, }); expect(publicAllocatorBalanceAfter).toEqual( publicAllocatorBalanceBefore + reallocationFee, @@ -616,22 +616,22 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { }); }); -describe("getReallocationData and getReallocations", () => { - test("should reject getReallocationData when the client chain differs from the market chain", async ({ +describe("getVaultV1ReallocationData and getReallocations", () => { + test("should reject getVaultV1ReallocationData when the client chain differs from the market chain", async ({ client, }) => { const morphoClient = client.extend(morphoViemExtension()).morpho; const market = morphoClient.blue(CbbtcUsdcBlue, base.id); await expect( - market.getReallocationData({ + market.getVaultV1ReallocationData({ vaultAddresses: [SteakhouseUsdcVaultV1.address], block: { number: 0n, timestamp: 0n }, }), ).rejects.toBeInstanceOf(ChainIdMismatchError); }); - test("should compute reallocations and borrow using getReallocationData + getReallocations", async ({ + test("should compute reallocations and borrow using getVaultV1ReallocationData + getReallocations", async ({ client, }) => { const collateralAmount = parseUnits("1000", 8); @@ -662,7 +662,7 @@ describe("getReallocationData and getReallocations", () => { const block = await client.getBlock(); - const reallocationData = await market.getReallocationData({ + const reallocationData = await market.getVaultV1ReallocationData({ vaultAddresses: [SteakhouseUsdcVaultV1.address], block, }); @@ -741,7 +741,7 @@ describe("getReallocationData and getReallocations", () => { const block = await client.getBlock(); - const reallocationData = await market.getReallocationData({ + const reallocationData = await market.getVaultV1ReallocationData({ vaultAddresses: [SteakhouseUsdcVaultV1.address], block, }); @@ -801,7 +801,7 @@ describe("getReallocationData and getReallocations", () => { const block = await client.getBlock(); - const reallocationData = await market.getReallocationData({ + const reallocationData = await market.getVaultV1ReallocationData({ vaultAddresses: [SteakhouseUsdcVaultV1.address], block, }); diff --git a/packages/morpho-ts/src/abis.ts b/packages/morpho-ts/src/abis.ts index 040f0453a..f96915682 100644 --- a/packages/morpho-ts/src/abis.ts +++ b/packages/morpho-ts/src/abis.ts @@ -4050,8 +4050,8 @@ export const marketParamsAbi = { ], } as const; -/** PublicAllocator ABI used to read vault allocator configuration and flow caps. */ -export const publicAllocatorAbi = [ +/** Vault V1 PublicAllocator ABI used to read vault allocator configuration and flow caps. */ +export const vaultV1PublicAllocatorAbi = [ { inputs: [ { @@ -4579,6 +4579,13 @@ export const publicAllocatorAbi = [ }, ] as const; +/** + * Deprecated alias for the Vault V1 PublicAllocator ABI. + * + * @deprecated Use `vaultV1PublicAllocatorAbi` instead. + */ +export const publicAllocatorAbi = vaultV1PublicAllocatorAbi; + /** Vault V2 Blue Public Allocator ABI used for market and idle reallocations. */ export const vaultV2BluePublicAllocatorAbi = [ { diff --git a/packages/morpho-ts/src/addresses.test.ts b/packages/morpho-ts/src/addresses.test.ts index eda64b0e9..d912699de 100644 --- a/packages/morpho-ts/src/addresses.test.ts +++ b/packages/morpho-ts/src/addresses.test.ts @@ -142,6 +142,16 @@ describe("addressesRegistry", () => { expect("midnight" in addressesRegistry[1]).toBe(false); }); + test("behavior: keeps the deprecated Vault V1 PublicAllocator alias", () => { + const { publicAllocator, vaultV1PublicAllocator } = + addressesRegistry[ChainId.EthMainnet]; + + expect(publicAllocator).toBe(vaultV1PublicAllocator); + expect(getChainAddress(ChainId.EthMainnet, "publicAllocator")).toBe( + vaultV1PublicAllocator, + ); + }); + test.each([ [ChainId.EthMainnet, "0x00b8e1509398ED692C3F326CbAf1694F9A881e27"], [ChainId.BaseMainnet, "0xAED282B8aD9257BB1272e93aE63A32A53621e412"], @@ -158,12 +168,12 @@ describe("addressesRegistry", () => { [ChainId.RobinhoodMainnet, "0xCe5c1aFa115fF8b1D6913509bfc79D9AE08CC857"], ] as const)( "behavior: exposes BluePublicAllocator on chain %i", - (chainId, bluePublicAllocator) => { - expect(addressesRegistry[chainId].bluePublicAllocator).toBe( - bluePublicAllocator, + (chainId, vaultV2BluePublicAllocator) => { + expect(addressesRegistry[chainId].vaultV2BluePublicAllocator).toBe( + vaultV2BluePublicAllocator, ); - expect(getChainAddress(chainId, "bluePublicAllocator")).toBe( - bluePublicAllocator, + expect(getChainAddress(chainId, "vaultV2BluePublicAllocator")).toBe( + vaultV2BluePublicAllocator, ); }, ); @@ -556,6 +566,27 @@ describe("registerCustomAddresses", () => { ); }); + test("behavior: normalizes Vault V1 PublicAllocator aliases", () => { + const chainId = 31_337_013; + const publicAllocator = randomAddress(); + + registerCustomAddresses({ + addresses: { + [chainId]: { ...createBlueAddresses(), publicAllocator }, + }, + deployments: { + [chainId]: { ...createBlueDeployments(), publicAllocator: 11n }, + }, + }); + + expect(addressesRegistry[chainId]?.vaultV1PublicAllocator).toBe( + publicAllocator, + ); + expect(addressesRegistry[chainId]?.publicAllocator).toBe(publicAllocator); + expect(deployments[chainId]?.vaultV1PublicAllocator).toBe(11n); + expect(deployments[chainId]?.publicAllocator).toBe(11n); + }); + test("error: RegistryValueAlreadyRegisteredError for addresses", () => { const chainId = 31_337_009; const chainAddresses = createChainAddresses(); diff --git a/packages/morpho-ts/src/addresses.ts b/packages/morpho-ts/src/addresses.ts index 20eeeaa68..d52362509 100644 --- a/packages/morpho-ts/src/addresses.ts +++ b/packages/morpho-ts/src/addresses.ts @@ -62,10 +62,16 @@ export interface ChainAddresses { }; /** AdaptiveCurveIrm contract that lets Morpho update utilization-responsive borrow rates per market. */ adaptiveCurveIrm: `0x${string}`; - /** PublicAllocator contract for permissionless MetaMorpho reallocations subject to flow caps and vault fees. */ + /** Vault V1 PublicAllocator contract for permissionless MetaMorpho reallocations subject to flow caps and vault fees. */ + vaultV1PublicAllocator?: `0x${string}`; + /** + * Deprecated alias for the Vault V1 PublicAllocator contract. + * + * @deprecated Use `vaultV1PublicAllocator` instead. + */ publicAllocator?: `0x${string}`; - /** BluePublicAllocator contract for permissionless Vault V2 reallocations subject to allocation caps and penalties. */ - bluePublicAllocator?: `0x${string}`; + /** Vault V2 BluePublicAllocator contract for permissionless reallocations subject to allocation caps and penalties. */ + vaultV2BluePublicAllocator?: `0x${string}`; /** MetaMorpho factory that creates and indexes Morpho Vault V1 ERC4626 vaults. */ metaMorphoFactory?: `0x${string}`; /** VaultV2 factory that creates and indexes Morpho Vault V2 ERC4626/ERC2612 vaults. */ @@ -146,8 +152,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0xaf85aF286637A033BE7d59ED8cC566afa3309B02", }, adaptiveCurveIrm: "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC", + vaultV1PublicAllocator: "0xfd32fA2ca22c76dD6E550706Ad913FC6CE91c75D", publicAllocator: "0xfd32fA2ca22c76dD6E550706Ad913FC6CE91c75D", - bluePublicAllocator: "0x00b8e1509398ED692C3F326CbAf1694F9A881e27", + vaultV2BluePublicAllocator: "0x00b8e1509398ED692C3F326CbAf1694F9A881e27", metaMorphoFactory: "0x1897A8997241C1cD4bD0698647e4EB7213535c24", vaultV2Factory: "0xA1D94F746dEfa1928926b84fB2596c06926C0405", morphoMarketV1AdapterFactory: "0xb049465969ac6355127cDf9E88deE63d25204d5D", @@ -239,8 +246,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0xE52E169C342C096C4949ABb944DC9f30E3F5Ea84", }, adaptiveCurveIrm: "0x46415998764C29aB2a25CbeA6254146D50D22687", + vaultV1PublicAllocator: "0xA090dD1a701408Df1d4d0B85b716c87565f90467", publicAllocator: "0xA090dD1a701408Df1d4d0B85b716c87565f90467", - bluePublicAllocator: "0xAED282B8aD9257BB1272e93aE63A32A53621e412", + vaultV2BluePublicAllocator: "0xAED282B8aD9257BB1272e93aE63A32A53621e412", metaMorphoFactory: "0xFf62A7c278C62eD665133147129245053Bbf5918", vaultV2Factory: "0x4501125508079A99ebBebCE205DeC9593C2b5857", morphoMarketV1AdapterFactory: "0x133baC94306B99f6dAD85c381a5be851d8DD717c", @@ -284,8 +292,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0x7Ae2B7012c82ea18a6BeE98ad09a684C88d6e36a", }, adaptiveCurveIrm: "0xe675A2161D4a6E2de2eeD70ac98EEBf257FBF0B0", + vaultV1PublicAllocator: "0xfac15aff53ADd2ff80C2962127C434E8615Df0d3", publicAllocator: "0xfac15aff53ADd2ff80C2962127C434E8615Df0d3", - bluePublicAllocator: "0xAb06a92cd253Bc12Dec8f719a693a6b472CCDfF4", + vaultV2BluePublicAllocator: "0xAb06a92cd253Bc12Dec8f719a693a6b472CCDfF4", metaMorphoFactory: "0xa9c87daB340631C34BB738625C70499e29ddDC98", vaultV2Factory: "0xC11a53eE9B1eCc7a068D8e40F8F17926584F97Cf", morphoMarketV1AdapterFactory: "0xD1A0C86F28ecD1657Ad06415c2B230cC89D9b6dd", @@ -315,8 +324,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0x7B885a940164eD51A068725f577a12197b76109b", }, adaptiveCurveIrm: "0x66F30587FB8D4206918deb78ecA7d5eBbafD06DA", + vaultV1PublicAllocator: "0x769583Af5e9D03589F159EbEC31Cc2c23E8C355E", publicAllocator: "0x769583Af5e9D03589F159EbEC31Cc2c23E8C355E", - bluePublicAllocator: "0x85b66Fe31e6788E5a6825EAe689f4c6c38AF3704", + vaultV2BluePublicAllocator: "0x85b66Fe31e6788E5a6825EAe689f4c6c38AF3704", metaMorphoFactory: "0x878988f5f561081deEa117717052164ea1Ef0c82", vaultV2Factory: "0x6b46fa3cc9EBF8aB230aBAc664E37F2966Bf7971", morphoMarketV1AdapterFactory: "0x96456Bf888D4de607Bf3ca0b3C8e4DF9b0d0Ad47", @@ -344,8 +354,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0x80De0F063aC662a4ee86c2F4Db0b52746094ad62", }, adaptiveCurveIrm: "0x8cD70A8F399428456b29546BC5dBe10ab6a06ef6", + vaultV1PublicAllocator: "0x0d68a97324E602E02799CD83B42D337207B40658", publicAllocator: "0x0d68a97324E602E02799CD83B42D337207B40658", - bluePublicAllocator: "0xc6945A915Bb7e2A365469f120A33D2FA42951cF3", + vaultV2BluePublicAllocator: "0xc6945A915Bb7e2A365469f120A33D2FA42951cF3", metaMorphoFactory: "0x3Bb6A6A0Bc85b367EFE0A5bAc81c5E52C892839a", vaultV2Factory: "0x6128b680b277Bf4Df80DFE9D8c55A498660870ef", morphoMarketV1AdapterFactory: "0x65956d5Ba4974983ecCe111612FC0A0c22650A11", @@ -371,8 +382,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0xcf7b4a40f25A6b839A93b8A8b45297F2a5383E73", }, adaptiveCurveIrm: "0x34E99D604751a72cF8d0CFDf87069292d82De472", + vaultV1PublicAllocator: "0xef9889B4e443DEd35FA0Bd060f2104Cca94e6A43", publicAllocator: "0xef9889B4e443DEd35FA0Bd060f2104Cca94e6A43", - bluePublicAllocator: "0x5Fe47f63ACd84f8A69b97E0a5122fCBff08Df48F", + vaultV2BluePublicAllocator: "0x5Fe47f63ACd84f8A69b97E0a5122fCBff08Df48F", metaMorphoFactory: "0x4DBB3a642a2146d5413750Cca3647086D9ba5F12", vaultV2Factory: "0x6846EA318B6B987Ee6b28eBFd87c3409F1d13108", morphoMarketV1AdapterFactory: "0xAf93F2d8508053432659d509b0210fdF1472493D", @@ -397,6 +409,7 @@ const _addressesRegistry = { generalAdapter1: "0x228dDF333DDf6D1895dA1dE8a846EDD27F1284eD", }, adaptiveCurveIrm: "0xA0D4D77b5D9933073572E19C172BFE866312673b", + vaultV1PublicAllocator: "0x37a888192165fC39884f87c64E2476BfD2C09675", publicAllocator: "0x37a888192165fC39884f87c64E2476BfD2C09675", metaMorphoFactory: "0x27D4Af0AC9E7FDfA6D0853236f249CC27AE79488", chainlinkOracleFactory: "0x39d8622C607A691D7705E8842fbB12E3c38dCD41", @@ -412,6 +425,7 @@ const _addressesRegistry = { generalAdapter1: "0xD2780fae0869cDc06EE202152304A39653361525", }, adaptiveCurveIrm: "0xa5EA7500A27C0079961D93366A6e93aafF18CB90", + vaultV1PublicAllocator: "0x8a7f671E45E51dE245649Cf916cA0256FB8a9927", publicAllocator: "0x8a7f671E45E51dE245649Cf916cA0256FB8a9927", metaMorphoFactory: "0x56b65742ade55015e6480959808229Ad6dbc9295", chainlinkOracleFactory: "0xb5961902E60b188b1c665B7b72Ef616656A9e24E", @@ -429,6 +443,7 @@ const _addressesRegistry = { generalAdapter1: "0xB8B2aDdCDe1cdC94AaE18a0F8A19df03D8683610", }, adaptiveCurveIrm: "0x9515407b1512F53388ffE699524100e7270Ee57B", + vaultV1PublicAllocator: "0x85416891752a6B81106c1C2999AE1AF5d8Cd3357", publicAllocator: "0x85416891752a6B81106c1C2999AE1AF5d8Cd3357", metaMorphoFactory: "0xd3f39505d0c48AFED3549D625982FdC38Ea9904b", chainlinkOracleFactory: "0x3FFFE273ee348b9E1ef89533025C7f165B17B439", @@ -452,8 +467,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0x0628B860947fA0c195988F65d53850546A489732", }, adaptiveCurveIrm: "0x9a6061d51743B31D2c3Be75D83781Fa423f53F0E", + vaultV1PublicAllocator: "0xB0c9a107fA17c779B3378210A7a593e88938C7C9", publicAllocator: "0xB0c9a107fA17c779B3378210A7a593e88938C7C9", - bluePublicAllocator: "0x2b7Bf2f2027bcfE3A1F6Bc93EA80220a883a6851", + vaultV2BluePublicAllocator: "0x2b7Bf2f2027bcfE3A1F6Bc93EA80220a883a6851", metaMorphoFactory: "0xe9EdE3929F43a7062a007C3e8652e4ACa610Bdc0", vaultV2Factory: "0xC9b34c108014B44e5a189A830e7e04c56704a0c9", morphoMarketV1AdapterFactory: "0x117b92Ab1C025B175ED38a0CDe5A067a745224a0", @@ -476,6 +492,7 @@ const _addressesRegistry = { generalAdapter1: "0x31D5aee8D75EEab548cfA0d11C4f9843a5201eaf", }, adaptiveCurveIrm: "0xDEfCf242226425f93d8DD0e314735C28517C473F", + vaultV1PublicAllocator: "0x6Cef2EDC70D87E8f1623f3096efF05d066E59B36", publicAllocator: "0x6Cef2EDC70D87E8f1623f3096efF05d066E59B36", metaMorphoFactory: "0x0cE9e3512CB4df8ae7e265e62Fb9258dc14f12e8", chainlinkOracleFactory: "0x7DA59Fa482F1F49fADc486d8e47BADc506fEb86d", @@ -492,6 +509,7 @@ const _addressesRegistry = { generalAdapter1: "0x9623090C3943ad63F7d794378273610Dd0deeFD4", }, adaptiveCurveIrm: "0xdEbdEa31624552DF904A065221cD14088ABDeD70", + vaultV1PublicAllocator: "0x4107Ea1746909028d6212B315dE5fE9538F9eb39", publicAllocator: "0x4107Ea1746909028d6212B315dE5fE9538F9eb39", metaMorphoFactory: "0x8e52179BeB18E882040b01632440d8Ca0f01da82", chainlinkOracleFactory: "0xB3cb32E6185446a6Bc7A047E4FfA138fA939e133", @@ -509,6 +527,7 @@ const _addressesRegistry = { generalAdapter1: "0xF53925b95Cc409447066cd5c1A7756084b2Ee0a4", }, adaptiveCurveIrm: "0xE3d46Ae190Cb39ccA3655E966DcEF96b4eAe1d1c", + vaultV1PublicAllocator: "0xEE868Bf3359DA30c10ea472EAEBFC0a06E8F0120", publicAllocator: "0xEE868Bf3359DA30c10ea472EAEBFC0a06E8F0120", metaMorphoFactory: "0xae5b0884bfff430493D6C844B9fd052Af7d79278", chainlinkOracleFactory: "0xf9380f7898423Bd7FDe3C9fDD1b2671A2471f39D", @@ -524,6 +543,7 @@ const _addressesRegistry = { generalAdapter1: "0x464a402244bCDdc0c2091D5193E8ffdb2be54Ca9", }, adaptiveCurveIrm: "0x58a42117d753a0e69694545DfA19d64c2fB759fB", + vaultV1PublicAllocator: "0xDFde06e2B2A2D718eE5560b73dA4F830E56A2f10", publicAllocator: "0xDFde06e2B2A2D718eE5560b73dA4F830E56A2f10", metaMorphoFactory: "0xe430821595602eA5DD0cD350f86987437c7362fA", chainlinkOracleFactory: "0x16278156D366fC91536b6b81482ffaC47EEa06D6", @@ -541,6 +561,7 @@ const _addressesRegistry = { generalAdapter1: "0x65ff368930Cb7eB4CA5C5eBC58bb69E6Ed198BA5", }, adaptiveCurveIrm: "0x7420302Ddd469031Cd2282cd64225cCd46F581eA", + vaultV1PublicAllocator: "0x58485338D93F4e3b4Bf2Af1C9f9C0aDF087AEf1C", publicAllocator: "0x58485338D93F4e3b4Bf2Af1C9f9C0aDF087AEf1C", metaMorphoFactory: "0x2525D453D9BA13921D5aB5D8c12F9202b0e19456", vaultV2Factory: "0x4f0a370bb367843CFd914c4d9972523aD2f8FCc9", @@ -563,6 +584,7 @@ const _addressesRegistry = { generalAdapter1: "0x29dcA26F9862CFb8064163ddc3401aaB4D4D05c6", }, adaptiveCurveIrm: "0xd5661D965cc60ed1954d4f6725b766051De3ef97", + vaultV1PublicAllocator: "0x0b7a3A49dafd98363B428cEC966106f29c0eee75", publicAllocator: "0x0b7a3A49dafd98363B428cEC966106f29c0eee75", metaMorphoFactory: "0x3F4b9246b7Cd3F7671c70BeBd5AAFC08e5bb5f16", chainlinkOracleFactory: "0x391A3fd481743FE48409e2e31eDac8a5f4C7653A", @@ -583,8 +605,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0xa434ABcc7e945b804c87B4f3c0a76b20651d4863", }, adaptiveCurveIrm: "0x4F708C0ae7deD3d74736594C2109C2E3c065B428", + vaultV1PublicAllocator: "0x39EB6Da5e88194C82B13491Df2e8B3E213eD2412", publicAllocator: "0x39EB6Da5e88194C82B13491Df2e8B3E213eD2412", - bluePublicAllocator: "0xd952175e940D97775cBC5a523977a6f091D0d702", + vaultV2BluePublicAllocator: "0xd952175e940D97775cBC5a523977a6f091D0d702", metaMorphoFactory: "0x1c8De6889acee12257899BFeAa2b7e534de32E16", vaultV2Factory: "0xFcb8b57E56787bB29e130Fca67f3c5a1232975D1", morphoMarketV1AdapterFactory: "0x2e6BE3a3A27fb45c6AbA2D1833eeA48E8788538e", @@ -605,6 +628,7 @@ const _addressesRegistry = { generalAdapter1: "0xEabdAC78A7f0a9B3dF0e23D69A5a5fF7f580a910", }, adaptiveCurveIrm: "0xC1523BE776e66ba07b609b1914D0925278f21FE5", + vaultV1PublicAllocator: "0x8b8B1bd41d36c06253203CD21463994aB752c1e6", publicAllocator: "0x8b8B1bd41d36c06253203CD21463994aB752c1e6", metaMorphoFactory: "0x997a79c3C04c5B9eb27d343ae126bcCFb5D74781", chainlinkOracleFactory: "0x12FA40f687a35611720E1DcB59976B6e51247298", @@ -621,6 +645,7 @@ const _addressesRegistry = { generalAdapter1: "0x6D94E7dCA6d8FAE2CF954633C2Cf9c286258E0af", }, adaptiveCurveIrm: "0x7E82b16496fA8CC04935528dA7F5A2C684A3C7A3", + vaultV1PublicAllocator: "0x414247afcf1fE3b94C617e7E3A7adB81D8D3208F", publicAllocator: "0x414247afcf1fE3b94C617e7E3A7adB81D8D3208F", metaMorphoFactory: "0xcDA78f4979d17Ec93052A84A12001fe0088AD734", chainlinkOracleFactory: "0xbf10eD52dD60C60E901BF022c3675303ad4a56b1", @@ -637,6 +662,7 @@ const _addressesRegistry = { generalAdapter1: "0x76cFE4BF840C7b461772fE7CDE399f58c4173584", }, adaptiveCurveIrm: "0x5576629f21D528A8c3e06C338dDa907B94563902", + vaultV1PublicAllocator: "0xb1E5B1De2a54ab55C412B5ee1E38e46799588103", publicAllocator: "0xb1E5B1De2a54ab55C412B5ee1E38e46799588103", metaMorphoFactory: "0x01dD876130690469F685a65C2B295A90a81BaD91", chainlinkOracleFactory: "0x2eb4D17C2AAf1EA62Bf83Fb49Dd1128b14AF4D93", @@ -657,8 +683,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0xC1749C8d50bc645D5116ccf4C858Bc45cB981Ac4", }, adaptiveCurveIrm: "0xD4a426F010986dCad727e8dd6eed44cA4A9b7483", + vaultV1PublicAllocator: "0x517505be22D9068687334e69ae7a02fC77edf4Fc", publicAllocator: "0x517505be22D9068687334e69ae7a02fC77edf4Fc", - bluePublicAllocator: "0x056dd7D4B373ED26c788190085CC6C52B8e7479d", + vaultV2BluePublicAllocator: "0x056dd7D4B373ED26c788190085CC6C52B8e7479d", metaMorphoFactory: "0xec051b19d654C48c357dC974376DeB6272f24e53", vaultV2Factory: "0xD7217E5687FF1071356C780b5fe4803D9D967da7", morphoMarketV1AdapterFactory: "0xc6b8B565C715134b0Ca3D6fa3D29B25759D0b9e2", @@ -682,6 +709,7 @@ const _addressesRegistry = { generalAdapter1: "0x02e0e71e145f254820B9D89c9E6068f08256F601", }, adaptiveCurveIrm: "0x6eFA8e3Aa8279eB2fd46b6083A9E52dA72EA56c4", + vaultV1PublicAllocator: "0xD878509446bE2C601f0f032F501851001B159D6B", publicAllocator: "0xD878509446bE2C601f0f032F501851001B159D6B", metaMorphoFactory: "0x8Dea49ec5bd5AeAc8bcf96B3E187F59354118291", chainlinkOracleFactory: "0x4bD68c2FF3274207EC07ED281C915758b6F23F07", @@ -697,6 +725,7 @@ const _addressesRegistry = { generalAdapter1: "0xFaD987d0EedfbAC709EC27ee4a94f36A1300a054", }, adaptiveCurveIrm: "0xf52e20C42FEc624819D4184226C4777D7cbd767e", + vaultV1PublicAllocator: "0x28a80F3713735CAD44bD9d7E1da9Fa16b0244497", publicAllocator: "0x28a80F3713735CAD44bD9d7E1da9Fa16b0244497", metaMorphoFactory: "0x41528AadC7314658b07Ca6e7213B9b77289B477f", chainlinkOracleFactory: "0x5115c1a74ABf096150593EecF3e20F016fc9dB43", @@ -712,6 +741,7 @@ const _addressesRegistry = { generalAdapter1: "0x454dAb6ce9891245696b239b4845a1cDC268255d", }, adaptiveCurveIrm: "0x85C2Ef4Bd69f42D7Da19Fb9dcdD7Fb8d0F59cDeE", + vaultV1PublicAllocator: "0x2d4cf00e18D48fD030d9b1E2FAAE6e0384C7610B", publicAllocator: "0x2d4cf00e18D48fD030d9b1E2FAAE6e0384C7610B", metaMorphoFactory: "0xA148a8223B622A72dC36472DE1492aBb5c089BA7", vaultV2Factory: "0x5DC11CF8BA4C39d1194F91218D35008d9F52A5d0", @@ -735,8 +765,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0xB04b831893A6E2E02Be347cD259690c5Bc7D0675", }, adaptiveCurveIrm: "0x09475a3D6eA8c314c592b1a3799bDE044E2F400F", + vaultV1PublicAllocator: "0xfd70575B732F9482F4197FE1075492e114E97302", publicAllocator: "0xfd70575B732F9482F4197FE1075492e114E97302", - bluePublicAllocator: "0x0A503aB026EFACBC0F7feE7795F34B80b5B9a662", + vaultV2BluePublicAllocator: "0x0A503aB026EFACBC0F7feE7795F34B80b5B9a662", metaMorphoFactory: "0x33f20973275B2F574488b18929cd7DCBf1AbF275", vaultV2Factory: "0x8B2F922162FBb60A6a072cC784A2E4168fB0bb0c", morphoMarketV1AdapterFactory: "0x8Da54fbF89B3D6fC6DCC92F31CF75a211ACF3d46", @@ -760,8 +791,9 @@ const _addressesRegistry = { vaultExitBundlesV1: "0x258d5c815CCE7017E24c63a7669F51ABcD0Dd4e5", }, adaptiveCurveIrm: "0x41e846FC8108b8527C1D4EDB4c9564E56442940f", + vaultV1PublicAllocator: "0xbCB063D4B6D479b209C186e462828CBACaC82DbE", publicAllocator: "0xbCB063D4B6D479b209C186e462828CBACaC82DbE", - bluePublicAllocator: "0x5C884d4B1510EAd302EC50A2AB4DE9c0b9E407ce", + vaultV2BluePublicAllocator: "0x5C884d4B1510EAd302EC50A2AB4DE9c0b9E407ce", metaMorphoFactory: "0xb4ae5673c48621189E2bEfBA96F31912032DD1AE", vaultV2Factory: "0x7fc35488803D49D00a94b206A223f7661898BE3a", morphoMarketV1AdapterFactory: "0x2A5F218FE4Dac3b1f4E096e8ae83074bB1713833", @@ -782,6 +814,7 @@ const _addressesRegistry = { generalAdapter1: "0xCa470cBBc3de56FDe336466f2107cC969174d513", }, adaptiveCurveIrm: "0x1Db002C086439d55B9f33E6c0693Eb850F7c0607", + vaultV1PublicAllocator: "0xFEAbEf95f3C937Ff4d5fD70005CF3392f8Ca02d5", publicAllocator: "0xFEAbEf95f3C937Ff4d5fD70005CF3392f8Ca02d5", metaMorphoFactory: "0xEA67e5566Ca2c0176d9db172A7f9A1e1F22E9D3A", vaultV2Factory: "0x05519a0835a1bFD90f110aA7ca46e9A5F81Ed3b4", @@ -804,6 +837,7 @@ const _addressesRegistry = { generalAdapter1: "0x3E7544a07157D03a49359eE89f2fCac9a6467230", }, adaptiveCurveIrm: "0x683CAAADdfA2F42e24880E202676526d501a5dED", + vaultV1PublicAllocator: "0x3Fe12193D178B76BaF4e23a083A64e49ACDE3188", publicAllocator: "0x3Fe12193D178B76BaF4e23a083A64e49ACDE3188", metaMorphoFactory: "0x6870aA9f66C1e5Efe8Dbe8730e86E9e91f688275", chainlinkOracleFactory: "0x3a4849b5174Dc6828c6Dc9BBD87e61Ed1ebE9fFA", @@ -819,6 +853,7 @@ const _addressesRegistry = { generalAdapter1: "0x3716AA06304D1bC70f553Da44904d13086A4a791", }, adaptiveCurveIrm: "0xd334eb112CfD1EB4a50FB871b7D9895EBB955C43", + vaultV1PublicAllocator: "0x609f3DF49806B5617A0Bd3301f04Ac3CB26d9e82", publicAllocator: "0x609f3DF49806B5617A0Bd3301f04Ac3CB26d9e82", metaMorphoFactory: "0x83A7f60c9fc57cEf1e8001bda98783AA1A53E4b1", chainlinkOracleFactory: "0x3585E3fD72F8d1b02250E1F6496b706c6e092884", @@ -835,6 +870,7 @@ const _addressesRegistry = { generalAdapter1: "0xa87F1422Df88B5f490203D71e2f8e7244843E62F", }, adaptiveCurveIrm: "0xefB565442B9Eb740B50Cf928C14d21c0111254F9", + vaultV1PublicAllocator: "0x3A1db0038361528756bED147abe3d41255c7128c", publicAllocator: "0x3A1db0038361528756bED147abe3d41255c7128c", metaMorphoFactory: "0xb95De4a9C81Ba6240378F383f88592d30937d048", chainlinkOracleFactory: "0xfDc69d06De855701731D142F28bD401802DA4daF", @@ -852,6 +888,7 @@ const _addressesRegistry = { generalAdapter1: "0x87c93660ECe6E68C6492EabBbBdbaafA102ae3a3", }, adaptiveCurveIrm: "0x7112D95cB5f6b13bF5F5B94a373bB3b2B381F979", + vaultV1PublicAllocator: "0x842bEccF8eBC11006c4bE96DEfE09b60326D0495", publicAllocator: "0x842bEccF8eBC11006c4bE96DEfE09b60326D0495", metaMorphoFactory: "0x92983687e672cA6d96530f9Dbe11a196cE905d72", chainlinkOracleFactory: "0xDf2035fC15919588526dBb5560863C812F135236", @@ -867,6 +904,7 @@ const _addressesRegistry = { generalAdapter1: "0xA47EeDE3Aac741B830E394B2e291f6774BD8bb48", }, adaptiveCurveIrm: "0x68F9b666b984527A7c145Db4103Cc6d3171C797F", + vaultV1PublicAllocator: "0x76f93A21573014Ab7d634D3204818922A234249e", publicAllocator: "0x76f93A21573014Ab7d634D3204818922A234249e", metaMorphoFactory: "0x7026b436f294e560b3C26E731f5cac5992cA2B33", chainlinkOracleFactory: "0x669F1A4cE3127740eCdB3E36adFC5Df6Db1EA74b", @@ -887,7 +925,7 @@ const _addressesRegistry = { vaultExitBundlesV1: "0x8225192b8638bDe9D41a6d96aBb824F660Ef57E1", }, adaptiveCurveIrm: "0x112fd4042E442C3C12C67AD23587b0afe36eB74E", - bluePublicAllocator: "0xDC9693CE6488640faEf173Ec2635ff99fdC25a07", + vaultV2BluePublicAllocator: "0xDC9693CE6488640faEf173Ec2635ff99fdC25a07", vaultV2Factory: "0x3DE400E3F79113194fa5AF6Ae5C474947E0C82Db", morphoMarketV1AdapterV2Factory: "0xF85aD5f14cC903533FC409B8098B58b4C2f36697", @@ -1060,7 +1098,7 @@ const _addressesRegistry = { vaultExitBundlesV1: "0xCE29862924756584BBD0D75CA1249d22007E2813", }, adaptiveCurveIrm: "0x2BD3d5965B26B51814AC95127B2b80dD6CcC0fa1", - bluePublicAllocator: "0xCe5c1aFa115fF8b1D6913509bfc79D9AE08CC857", + vaultV2BluePublicAllocator: "0xCe5c1aFa115fF8b1D6913509bfc79D9AE08CC857", vaultV2Factory: "0x0FBad98595b0186dA120E41f77C102beb49f803c", morphoMarketV1AdapterV2Factory: "0x79370Ed003CE325C088E530d5e8655c99c2993e1", @@ -1099,6 +1137,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 25_720_868n }, adaptiveCurveIrm: 18883124n, + vaultV1PublicAllocator: 19375099n, publicAllocator: 19375099n, metaMorphoFactory: 21439510n, vaultV2Factory: 23375073n, @@ -1123,6 +1162,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 49_765_458n }, adaptiveCurveIrm: 13977152n, + vaultV1PublicAllocator: 13979545n, publicAllocator: 13979545n, metaMorphoFactory: 23928808n, vaultV2Factory: 35615206n, @@ -1154,6 +1194,7 @@ const _deployments = { bundles: { vaultExitBundlesV1: 91_743_910n }, permit2: 35701901n, adaptiveCurveIrm: 66931042n, + vaultV1PublicAllocator: 66931042n, publicAllocator: 66931042n, metaMorphoFactory: 66931042n, vaultV2Factory: 77371907n, @@ -1177,6 +1218,7 @@ const _deployments = { bundles: { vaultExitBundlesV1: 492_901_559n }, permit2: 38692735n, adaptiveCurveIrm: 296446593n, + vaultV1PublicAllocator: 296446593n, publicAllocator: 296446593n, metaMorphoFactory: 296447195n, vaultV2Factory: 387016724n, @@ -1198,6 +1240,7 @@ const _deployments = { bundles: { vaultExitBundlesV1: 155_360_936n }, permit2: 38854427n, adaptiveCurveIrm: 130770075n, + vaultV1PublicAllocator: 130770075n, publicAllocator: 130770075n, metaMorphoFactory: 130770189n, vaultV2Factory: 142122059n, @@ -1217,6 +1260,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 33_492_822n }, adaptiveCurveIrm: 9025669n, + vaultV1PublicAllocator: 9025669n, publicAllocator: 9025669n, metaMorphoFactory: 9025733n, vaultV2Factory: 20253005n, @@ -1235,6 +1279,7 @@ const _deployments = { generalAdapter1: 16536231n, }, adaptiveCurveIrm: 15317931n, + vaultV1PublicAllocator: 15317931n, publicAllocator: 15317931n, metaMorphoFactory: 15318007n, chainlinkOracleFactory: 15318007n, @@ -1248,6 +1293,7 @@ const _deployments = { generalAdapter1: 13504587n, }, adaptiveCurveIrm: 12842868n, + vaultV1PublicAllocator: 12842868n, publicAllocator: 12842868n, metaMorphoFactory: 12842903n, chainlinkOracleFactory: 12842903n, @@ -1261,6 +1307,7 @@ const _deployments = { generalAdapter1: 6385077n, }, adaptiveCurveIrm: 4078776n, + vaultV1PublicAllocator: 4078776n, publicAllocator: 4078776n, metaMorphoFactory: 4078830n, chainlinkOracleFactory: 4078830n, @@ -1278,6 +1325,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 55_572_727n }, adaptiveCurveIrm: 9139027n, + vaultV1PublicAllocator: 9139027n, publicAllocator: 9139027n, metaMorphoFactory: 9316789n, vaultV2Factory: 29092109n, @@ -1296,6 +1344,7 @@ const _deployments = { generalAdapter1: 9102286n, }, adaptiveCurveIrm: 9100931n, + vaultV1PublicAllocator: 9100931n, publicAllocator: 9100931n, metaMorphoFactory: 9101319n, chainlinkOracleFactory: 9101319n, @@ -1309,6 +1358,7 @@ const _deployments = { generalAdapter1: 1188907n, }, adaptiveCurveIrm: 1188872n, + vaultV1PublicAllocator: 1188872n, publicAllocator: 1188872n, metaMorphoFactory: 1188885n, chainlinkOracleFactory: 1188885n, @@ -1322,6 +1372,7 @@ const _deployments = { generalAdapter1: 19983599n, }, adaptiveCurveIrm: 19983370n, + vaultV1PublicAllocator: 19983370n, publicAllocator: 19983370n, metaMorphoFactory: 19983443n, chainlinkOracleFactory: 19983443n, @@ -1335,6 +1386,7 @@ const _deployments = { generalAdapter1: 253107n, }, adaptiveCurveIrm: 251401n, + vaultV1PublicAllocator: 251401n, publicAllocator: 251401n, metaMorphoFactory: 253027n, chainlinkOracleFactory: 253027n, @@ -1348,6 +1400,7 @@ const _deployments = { generalAdapter1: 789925n, }, adaptiveCurveIrm: 765994n, + vaultV1PublicAllocator: 765994n, publicAllocator: 765994n, metaMorphoFactory: 766078n, vaultV2Factory: 32235414n, @@ -1366,6 +1419,7 @@ const _deployments = { generalAdapter1: 2471517n, }, adaptiveCurveIrm: 2410315n, + vaultV1PublicAllocator: 2410315n, publicAllocator: 2410315n, metaMorphoFactory: 2410440n, chainlinkOracleFactory: 2410440n, @@ -1380,6 +1434,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 39_579_123n }, adaptiveCurveIrm: 2741069n, + vaultV1PublicAllocator: 2741069n, publicAllocator: 2741069n, metaMorphoFactory: 2741420n, vaultV2Factory: 13096629n, @@ -1398,6 +1453,7 @@ const _deployments = { generalAdapter1: 21050766n, }, adaptiveCurveIrm: 21047448n, + vaultV1PublicAllocator: 21047448n, publicAllocator: 21047448n, metaMorphoFactory: 21050315n, chainlinkOracleFactory: 21050315n, @@ -1412,6 +1468,7 @@ const _deployments = { generalAdapter1: 978967n, }, adaptiveCurveIrm: 853025n, + vaultV1PublicAllocator: 853025n, publicAllocator: 853025n, metaMorphoFactory: 978654n, chainlinkOracleFactory: 978654n, @@ -1425,6 +1482,7 @@ const _deployments = { generalAdapter1: 15731595n, }, adaptiveCurveIrm: 15731231n, + vaultV1PublicAllocator: 15731231n, publicAllocator: 15731231n, metaMorphoFactory: 15731333n, chainlinkOracleFactory: 15731333n, @@ -1439,6 +1497,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 42_767_282n }, adaptiveCurveIrm: 1988429n, + vaultV1PublicAllocator: 1988429n, publicAllocator: 1988429n, metaMorphoFactory: 1988677n, vaultV2Factory: 14188393n, @@ -1458,6 +1517,7 @@ const _deployments = { }, permit2: 118721449n, adaptiveCurveIrm: 166036723n, + vaultV1PublicAllocator: 166036723n, publicAllocator: 166036723n, metaMorphoFactory: 168896078n, chainlinkOracleFactory: 168896078n, @@ -1472,6 +1532,7 @@ const _deployments = { generalAdapter1: 7527649n, }, adaptiveCurveIrm: 7526486n, + vaultV1PublicAllocator: 7526486n, publicAllocator: 7526486n, metaMorphoFactory: 7526768n, chainlinkOracleFactory: 7526768n, @@ -1485,6 +1546,7 @@ const _deployments = { generalAdapter1: 25072853n, }, adaptiveCurveIrm: 25072608n, + vaultV1PublicAllocator: 25072608n, publicAllocator: 25072608n, metaMorphoFactory: 25072665n, vaultV2Factory: 25072951n, @@ -1503,6 +1565,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 94_631_561n }, adaptiveCurveIrm: 31907457n, + vaultV1PublicAllocator: 31907457n, publicAllocator: 31907457n, metaMorphoFactory: 32320327n, vaultV2Factory: 32321811n, @@ -1523,6 +1586,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 34_970_501n }, adaptiveCurveIrm: 1504506n, + vaultV1PublicAllocator: 1504506n, publicAllocator: 1504506n, metaMorphoFactory: 1504774n, vaultV2Factory: 1506182n, @@ -1541,6 +1605,7 @@ const _deployments = { generalAdapter1: 38460388n, }, adaptiveCurveIrm: 38459435n, + vaultV1PublicAllocator: 38459435n, publicAllocator: 38459435n, metaMorphoFactory: 38459727n, vaultV2Factory: 38461643n, @@ -1559,6 +1624,7 @@ const _deployments = { generalAdapter1: 41808392n, }, adaptiveCurveIrm: 40249329n, + vaultV1PublicAllocator: 40249329n, publicAllocator: 40249329n, metaMorphoFactory: 40259931n, chainlinkOracleFactory: 40259931n, @@ -1573,6 +1639,7 @@ const _deployments = { generalAdapter1: 13949482n, }, adaptiveCurveIrm: 13947713n, + vaultV1PublicAllocator: 13947713n, publicAllocator: 13947713n, metaMorphoFactory: 13949369n, chainlinkOracleFactory: 13949369n, @@ -1587,6 +1654,7 @@ const _deployments = { generalAdapter1: 13638316n, }, adaptiveCurveIrm: 13516997n, + vaultV1PublicAllocator: 13516997n, publicAllocator: 13516997n, metaMorphoFactory: 13638155n, chainlinkOracleFactory: 13638155n, @@ -1602,6 +1670,7 @@ const _deployments = { generalAdapter1: 54346080n, }, adaptiveCurveIrm: 54344680n, + vaultV1PublicAllocator: 54344680n, publicAllocator: 54344680n, metaMorphoFactory: 54344985n, chainlinkOracleFactory: 54344985n, @@ -1615,6 +1684,7 @@ const _deployments = { generalAdapter1: 6443359n, }, adaptiveCurveIrm: 6440817n, + vaultV1PublicAllocator: 6440817n, publicAllocator: 6440817n, metaMorphoFactory: 6440899n, chainlinkOracleFactory: 6440899n, @@ -2297,7 +2367,14 @@ const refreshDeploymentViews = () => { blueDeployments = deployments; }; -const withBlueAlias = ({ +const withAliases = < + T extends { + blue?: unknown; + morpho?: unknown; + vaultV1PublicAllocator?: unknown; + publicAllocator?: unknown; + }, +>({ entry, label, type, @@ -2306,8 +2383,6 @@ const withBlueAlias = ({ label: string; type: string; }) => { - if (entry.blue == null && entry.morpho == null) return { ...entry } as T; - if ( entry.blue != null && entry.morpho != null && @@ -2324,10 +2399,42 @@ const withBlueAlias = ({ type, }); + if ( + entry.vaultV1PublicAllocator != null && + entry.publicAllocator != null && + !areRegistryValuesEqual({ + base: entry.vaultV1PublicAllocator, + patch: entry.publicAllocator, + type, + }) + ) + throw new RegistryValueAlreadyRegisteredError({ + label: `${label}.publicAllocator`, + registeredValue: isRegistryPrimitive(entry.vaultV1PublicAllocator) + ? entry.vaultV1PublicAllocator + : String(entry.vaultV1PublicAllocator), + requestedValue: isRegistryPrimitive(entry.publicAllocator) + ? entry.publicAllocator + : String(entry.publicAllocator), + type, + }); + return { ...entry, - blue: entry.blue ?? entry.morpho, - morpho: entry.morpho ?? entry.blue, + ...(entry.blue != null || entry.morpho != null + ? { + blue: entry.blue ?? entry.morpho, + morpho: entry.morpho ?? entry.blue, + } + : {}), + ...(entry.vaultV1PublicAllocator != null || entry.publicAllocator != null + ? { + vaultV1PublicAllocator: + entry.vaultV1PublicAllocator ?? entry.publicAllocator, + publicAllocator: + entry.publicAllocator ?? entry.vaultV1PublicAllocator, + } + : {}), } as T; }; @@ -2413,7 +2520,7 @@ export function registerCustomAddresses< const chainId = Number(chainIdString); const registeredEntry = nextRegistry[chainId]; const requestedEntry = cloneRegistryValue( - withBlueAlias({ + withAliases({ entry: requestedAddresses, label: String(chainId), type: "address", @@ -2454,7 +2561,7 @@ export function registerCustomAddresses< const chainId = Number(chainIdString); const registeredEntry = nextRegistry[chainId]; const requestedEntry = cloneRegistryValue( - withBlueAlias({ + withAliases({ entry: requestedDeployments, label: String(chainId), type: "deployment", From d6258503e2dd66df355ed575bd449061a98f978e Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Tue, 18 Aug 2026 17:52:28 +0200 Subject: [PATCH 24/41] feat: configure Vault V2 withdrawal utilization --- .changeset/brave-vaults-reallocate.md | 3 +- ...26-08-18-vault-v2-blue-reallocation-api.md | 8 ++-- packages/liquidity-sdk-viem/src/loader.ts | 20 ++++----- .../vaultV2BlueReallocationData.test.ts | 42 +++++++++++++++++++ .../entities/vaultV2BlueReallocationData.ts | 20 +++++---- packages/morpho-sdk/src/helpers/constant.ts | 4 +- packages/morpho-sdk/src/types/AGENTS.md | 2 +- .../morpho-sdk/src/types/sharedLiquidity.ts | 17 +++++--- 8 files changed, 82 insertions(+), 34 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index c2baf3a7d..ee37fd869 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -2,6 +2,7 @@ "@morpho-org/morpho-ts": minor "@morpho-org/blue-sdk": minor "@morpho-org/blue-sdk-viem": minor +"@morpho-org/liquidity-sdk-viem": patch "@morpho-org/morpho-sdk": minor "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- @@ -10,7 +11,7 @@ Add canonical `vaultV1PublicAllocatorAbi` and `vaultV2BluePublicAllocatorAbi` ex V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. -Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. +Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. Compatibility note: `VaultV2MorphoMarketV1AdapterV2.ids()` now declares its existing three-element result as `readonly [Hash, Hash, Hash]`. The runtime values and ordering are unchanged, and derived allocation identifiers are immutable descriptors. We intentionally accept this TypeScript assignability tightening in the minor release; callers that explicitly require a mutable `Hash[]` can copy the tuple with `[...adapter.ids(params)]`. diff --git a/docs/tibs/TIB-2026-08-18-vault-v2-blue-reallocation-api.md b/docs/tibs/TIB-2026-08-18-vault-v2-blue-reallocation-api.md index 4704fa2fa..8c5976d4a 100644 --- a/docs/tibs/TIB-2026-08-18-vault-v2-blue-reallocation-api.md +++ b/docs/tibs/TIB-2026-08-18-vault-v2-blue-reallocation-api.md @@ -37,9 +37,11 @@ Vault V1 state, making its protocol scope unclear. - Use the same unversioned liquidity-metric method names on both data classes; the class name supplies protocol context. -V1's configurable source and trigger utilization options remain deprecated. -V2 therefore retains fixed 90% friendly source and target thresholds plus its -internal 100% fallback. +V1's per-market source and trigger utilization options remain deprecated, while +`defaultMaxWithdrawalUtilization` remains configurable for its two-phase +planner. V2 exposes a scalar `maxWithdrawalUtilization` for the friendly source +phase, defaulting to 90%; its target threshold remains fixed at 90%, and its +second phase always uses the internal 100% source ceiling. V2 keeps the latest market or vault `lastUpdate` as its default simulation timestamp. A target market can be older than a source or vault; using only its diff --git a/packages/liquidity-sdk-viem/src/loader.ts b/packages/liquidity-sdk-viem/src/loader.ts index b824cafbf..edb40f95b 100644 --- a/packages/liquidity-sdk-viem/src/loader.ts +++ b/packages/liquidity-sdk-viem/src/loader.ts @@ -16,24 +16,21 @@ import { apiSdk } from "./api/index.js"; const REALLOCATION_SIMULATION_DELAY = 3_600n; /** * Optional tuning for the shared-liquidity source-market withdrawal ceiling. - * - * @deprecated The source-market withdrawal ceiling is fixed at 90% - * (`DEFAULT_WITHDRAWAL_TARGET_UTILIZATION` in `@morpho-org/morpho-sdk`) and will - * stop being configurable in the next major. Overrides are still honored for now. */ export interface LiquidityParameters { /** * The default maximum utilization allowed to reach to find shared liquidity (scaled by WAD). * - * @deprecated Fixed at 90% and will be removed in the next major. + * @default 90% (900000000000000000n) */ defaultMaxWithdrawalUtilization?: bigint; /** * If provided, defines the maximum utilization allowed to reach for each market, defaulting to `defaultMaxWithdrawalUtilization`. * - * @deprecated Fixed at 90% and will be removed in the next major. The Morpho - * API's `targetWithdrawUtilization` is no longer consulted. + * @deprecated Per-market source ceilings will be removed in the next major. + * Use `defaultMaxWithdrawalUtilization` to configure one ceiling for every + * source. The Morpho API's `targetWithdrawUtilization` is no longer consulted. */ maxWithdrawalUtilization?: Record; } @@ -51,10 +48,7 @@ export class LiquidityLoader { constructor( public client: Client, - /** - * @deprecated The source-market withdrawal ceiling is fixed at 90% and will - * stop being configurable in the next major. Overrides are still honored for now. - */ + /** Shared-liquidity source-market withdrawal tuning. */ public readonly parameters: LiquidityParameters = {}, ) { this.dataLoader = new DataLoader( @@ -190,8 +184,8 @@ export class LiquidityLoader { // The source-market withdrawal ceiling defaults to 90% // (DEFAULT_WITHDRAWAL_TARGET_UTILIZATION) inside // `getMarketPublicReallocations`; the API's per-market - // `targetWithdrawUtilization` is no longer consulted. Deprecated - // `parameters` overrides are still forwarded for backward compatibility. + // `targetWithdrawUtilization` is no longer consulted. + // Caller `parameters` overrides are forwarded to the planner. const { data: endState, withdrawals } = startState.getMarketPublicReallocations(uniqueKey, { ...parameters, diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index 791bcd6b6..ffa57ded8 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -342,6 +342,19 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ); }); + test("behavior: honors the configured source-utilization ceiling", () => { + const { data } = makeFixture({ sourceBorrow: 900n }); + + expect( + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, + ).toStrictEqual([]); + expect( + data.computeVaultV2BlueReallocations(targetParams.id, { + maxWithdrawalUtilization: MathLib.WAD, + }).reallocations[0]?.assets, + ).toBe(100n); + }); + test("behavior: ignores inactive source and target adapters", () => { for (const allocatorActiveAdapters of [ [TARGET_ADAPTER], @@ -944,6 +957,24 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations operation" expect(result.data.getMarket(targetParams.id).totalSupplyAssets).toBe(123n); }); + test("behavior: applies the configured ceiling during the friendly phase", () => { + const { data } = makeFixture({ + targetSupply: 100n, + targetBorrow: 90n, + sourceBorrow: 900n, + }); + + const { reallocations } = data.computeVaultV2BlueReallocations( + targetParams.id, + { + maxWithdrawalUtilization: MathLib.WAD, + operation: { type: "borrow", amount: 20n }, + }, + ); + + expect(reallocations[0]?.assets).toBe(23n); + }); + test("behavior: rounds required supply up to the utilization target", () => { const { data } = makeFixture({ targetSupply: 1n, targetBorrow: 0n }); @@ -989,6 +1020,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations operation" targetParams.id, { reallocatableVaults: [VAULT as Address].values(), + maxWithdrawalUtilization: 950_000_000_000_000_000n, operation: { type: "borrow", amount: 40n }, }, ); @@ -1135,4 +1167,14 @@ describe("VaultV2BlueReallocationData liquidity metrics", () => { ), ).toBe(30n); }); + + test("behavior: applies the configured source-utilization ceiling", () => { + const { data } = makeFixture({ sourceBorrow: 900n }); + + expect( + data.getPublicReallocationLiquidity(targetParams.id, { + maxWithdrawalUtilization: MathLib.WAD, + }), + ).toBe(100n); + }); }); diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index 107c1e4c4..3ab6accbb 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -462,8 +462,10 @@ export class VaultV2BlueReallocationData * Without `options.operation`, discovers every friendly call. With an * operation, caps the calls to the amount required by that borrow or * loan-asset withdrawal and falls back to 100% source utilization only when - * friendly liquidity cannot cover the absolute shortfall. Vaults whose - * configured penalty exceeds `options.maxPenalty` are ignored. + * friendly liquidity cannot cover the absolute shortfall. Friendly source + * utilization defaults to 90% and is configurable through + * `options.maxWithdrawalUtilization`. Vaults whose configured penalty exceeds + * `options.maxPenalty` are ignored. * * @param marketId - Target Blue market id. * @param options - Optional discovery controls and operation to support. @@ -500,11 +502,13 @@ export class VaultV2BlueReallocationData } { if (options.enabled === false) return { reallocations: [], data: this }; + const maxWithdrawalUtilization = + options.maxWithdrawalUtilization ?? DEFAULT_WITHDRAWAL_TARGET_UTILIZATION; const operation = options.operation; if (operation == null) return this.computeVaultV2BlueReallocationsAtUtilization({ marketId, - maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, + maxWithdrawalUtilization, options, }); @@ -555,7 +559,7 @@ export class VaultV2BlueReallocationData const friendly = this.computeVaultV2BlueReallocationsAtUtilization({ marketId, - maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, + maxWithdrawalUtilization, options: normalizedOptions, }); const discovered = [...friendly.reallocations]; @@ -937,7 +941,7 @@ export class VaultV2BlueReallocationData * Sums friendly Vault V2 shared liquidity available to a target market. * * @param marketId - Target Blue market id. - * @param options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. + * @param options - Optional timestamp, enable flag, vault allowlist, source utilization ceiling, and maximum penalty. * @returns Reallocatable market and idle assets, or `0n` when none are available. * @throws {UnknownReallocationMarketError} when the target market is absent. * @example @@ -951,7 +955,9 @@ export class VaultV2BlueReallocationData ) { return this.computeVaultV2BlueReallocationsAtUtilization({ marketId, - maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, + maxWithdrawalUtilization: + options?.maxWithdrawalUtilization ?? + DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, options, }).reallocations.reduce((total, { assets }) => total + assets, 0n); } @@ -962,7 +968,7 @@ export class VaultV2BlueReallocationData * * @param marketId - Target Blue market id. * @param utilization - Desired utilization, scaled by WAD. Defaults to 90%. - * @param options - Optional timestamp, enable flag, vault allowlist, and maximum penalty. + * @param options - Optional timestamp, enable flag, vault allowlist, source utilization ceiling, and maximum penalty. * @returns Borrowable assets while remaining at or below `utilization`. * @throws {UnknownReallocationMarketError} when the target market is absent. * @example diff --git a/packages/morpho-sdk/src/helpers/constant.ts b/packages/morpho-sdk/src/helpers/constant.ts index 36b41b20d..ac2089a92 100644 --- a/packages/morpho-sdk/src/helpers/constant.ts +++ b/packages/morpho-sdk/src/helpers/constant.ts @@ -12,9 +12,7 @@ export const MAX_ABSOLUTE_SHARE_PRICE = 100n * MathLib.RAY; /** * The default maximum utilization a source market may reach when withdrawing - * shared liquidity, scaled by WAD. Still overridable through the deprecated - * `maxWithdrawalUtilization` / `defaultMaxWithdrawalUtilization` options until - * the next major. + * shared liquidity, scaled by WAD. */ export const DEFAULT_WITHDRAWAL_TARGET_UTILIZATION = 90_0000000000000000n; diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index edfbaf50c..cb633b09d 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -20,7 +20,7 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. - `VaultV1BlueReallocation` — vault address + fee + sorted withdrawals; maps to `reallocateTo()`. `VaultReallocation` is its deprecated compatibility alias. - `VaultV2BlueReallocation` — BluePublicAllocator vault/source/target-adapter/assets/WAD-scaled-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. -- `VaultV2BluePublicAllocatorOptions` — canonical Vault V2 discovery and planner options for timestamp, enablement, vault allowlisting, and the maximum proportional penalty. +- `VaultV2BluePublicAllocatorOptions` — canonical Vault V2 discovery and planner options for timestamp, enablement, vault allowlisting, friendly source-market utilization, and the maximum proportional penalty. - `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. ## Errors (`error.ts`) diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 54a83fd0d..a246b5d7f 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -25,9 +25,8 @@ export interface PublicAllocatorOptions { * The maximum utilization each source market may reach when withdrawing * shared liquidity, scaled by WAD. * - * @deprecated The source-market withdrawal ceiling is fixed at 90% - * ({@link DEFAULT_WITHDRAWAL_TARGET_UTILIZATION}) and will stop being - * configurable in the next major. Per-market overrides are still honored for now. + * @deprecated Per-market source ceilings will be removed in the next major. + * Use `defaultMaxWithdrawalUtilization` to configure one ceiling for every source. */ readonly maxWithdrawalUtilization?: Readonly< Record @@ -38,9 +37,6 @@ export interface PublicAllocatorOptions { * shared liquidity, scaled by WAD. * * @default 90% (900000000000000000n) - * @deprecated The source-market withdrawal ceiling is fixed at 90% - * ({@link DEFAULT_WITHDRAWAL_TARGET_UTILIZATION}) and will stop being - * configurable in the next major. Overrides are still honored for now. */ readonly defaultMaxWithdrawalUtilization?: bigint; } @@ -59,6 +55,15 @@ export interface VaultV2BluePublicAllocatorOptions { */ readonly reallocatableVaults?: Iterable
; + /** + * Maximum utilization source markets may reach during friendly discovery, + * scaled by WAD. The amount-aware planner falls back to 100% only when the + * friendly phase cannot cover the operation's absolute shortfall. + * + * @default 90% (900000000000000000n) + */ + readonly maxWithdrawalUtilization?: bigint; + /** * Maximum proportional vault-asset penalty accepted for each * BluePublicAllocator call, scaled by WAD. Vaults with a higher configured From 93fbdea21a91a8b1f6411ea931ec40e608220cdb Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 19 Aug 2026 10:01:53 +0200 Subject: [PATCH 25/41] test: use deployed BluePublicAllocator --- .../BluePublicAllocatorReadFixture.sol | 31 -- ...2PublicAllocatorConfig.integration.test.ts | 74 +-- .../BluePublicAllocatorReadFixture.ts | 195 -------- .../BluePublicAllocatorWriteFixture.sol | 123 ----- packages/morpho-sdk/package.json | 1 - .../vaultV2Reallocations.integration.test.ts | 53 +-- .../BluePublicAllocatorWriteFixture.ts | 429 ------------------ packages/morpho-ts/src/abis.ts | 119 ++++- scripts/compile-solidity.js | 15 - 9 files changed, 176 insertions(+), 864 deletions(-) delete mode 100644 packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol delete mode 100644 packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts delete mode 100644 packages/morpho-sdk/contracts/fixtures/BluePublicAllocatorWriteFixture.sol delete mode 100644 packages/morpho-sdk/test/fixtures/BluePublicAllocatorWriteFixture.ts diff --git a/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol b/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol deleted file mode 100644 index a25a9e00b..000000000 --- a/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -struct VaultData { - bool canPullFromIdle; - uint64 penalty; -} - -/// @dev Stateful EVM fixture for exercising BluePublicAllocator read paths on an Anvil fork. -contract BluePublicAllocatorReadFixture { - mapping(address vault => mapping(bytes32 id => uint256)) public absoluteCap; - mapping(address vault => mapping(bytes32 id => bool)) public canPullFromMarket; - mapping(address vault => mapping(address adapter => bool)) public isActiveAdapter; - mapping(address vault => VaultData) public vaultData; - - function setAbsoluteCap(address vault, bytes32 id, uint256 value) external { - absoluteCap[vault][id] = value; - } - - function setCanPullFromMarket(address vault, bytes32 id, bool value) external { - canPullFromMarket[vault][id] = value; - } - - function setIsActiveAdapter(address vault, address adapter, bool value) external { - isActiveAdapter[vault][adapter] = value; - } - - function setVaultData(address vault, bool canPullFromIdle, uint64 penalty) external { - vaultData[vault] = VaultData(canPullFromIdle, penalty); - } -} diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts index 6f8a71072..416b43427 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts @@ -1,19 +1,23 @@ import { AccrualVaultV2MorphoMarketV1AdapterV2, - ChainId, getChainAddress, } from "@morpho-org/blue-sdk"; +import { createViemTest } from "@morpho-org/test/vitest"; +import { parseEther } from "viem"; +import { base } from "viem/chains"; import { assert, describe, expect } from "vitest"; -import { - abi as fixtureAbi, - code as fixtureCode, -} from "../../../test/fixtures/BluePublicAllocatorReadFixture.js"; -import { vaultV2Test } from "../../../test/setup.js"; +import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; import { fetchAccrualVaultV2 } from "./VaultV2.js"; import { fetchVaultV2PublicAllocatorData } from "./VaultV2PublicAllocatorConfig.js"; +const vaultV2BluePublicAllocatorTest = createViemTest(base, { + forkUrl: process.env.BASE_RPC_URL, + forkBlockNumber: 50_063_965, // BluePublicAllocator deployment block. + stepsTracing: false, +}); + describe("Vault V2 public allocator fetchers on fork", () => { - vaultV2Test( + vaultV2BluePublicAllocatorTest( "default: matches direct reads against the deployless query", async ({ client }) => { const forkVault = await fetchAccrualVaultV2( @@ -29,43 +33,57 @@ describe("Vault V2 public allocator fetchers on fork", () => { const forkMarket = forkAdapter.markets[0]; assert(forkMarket != null); - const deploymentHash = await client.deployContract({ - abi: fixtureAbi, - bytecode: fixtureCode, + const allocator = getChainAddress(base.id, "vaultV2BluePublicAllocator"); + const allocatorAccount = await client.readContract({ + address: forkVault.address, + abi: vaultV2Abi, + functionName: "curator", }); - const { contractAddress: fixture } = - await client.waitForTransactionReceipt({ hash: deploymentHash }); - assert(fixture != null); - const fixtureBytecode = await client.getBytecode({ address: fixture }); - assert(fixtureBytecode != null); - const allocator = getChainAddress( - ChainId.EthMainnet, - "vaultV2BluePublicAllocator", + assert( + await client.readContract({ + address: forkVault.address, + abi: vaultV2Abi, + functionName: "isAllocator", + args: [allocatorAccount], + }), ); - await client.setCode({ address: allocator, bytecode: fixtureBytecode }); - + await client.deal({ + account: allocatorAccount, + amount: parseEther("1"), + }); const forkAdapterMarketCapId = forkAdapter.ids(forkMarket.params)[2]; await client.writeContract({ + account: allocatorAccount, + address: allocator, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "setCanPullFromIdle", + args: [forkVault.address, true], + }); + await client.writeContract({ + account: allocatorAccount, address: allocator, - abi: fixtureAbi, - functionName: "setVaultData", - args: [forkVault.address, true, 12n], + abi: vaultV2BluePublicAllocatorAbi, + functionName: "setPenalty", + args: [forkVault.address, 12n], }); await client.writeContract({ + account: allocatorAccount, address: allocator, - abi: fixtureAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setAbsoluteCap", - args: [forkVault.address, forkAdapterMarketCapId, 500n], + args: [forkVault.address, forkAdapter.address, forkMarket.params, 500n], }); await client.writeContract({ + account: allocatorAccount, address: allocator, - abi: fixtureAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setCanPullFromMarket", - args: [forkVault.address, forkAdapterMarketCapId, true], + args: [forkVault.address, forkAdapter.address, forkMarket.params, true], }); await client.writeContract({ + account: allocatorAccount, address: allocator, - abi: fixtureAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setIsActiveAdapter", args: [forkVault.address, forkAdapter.address, true], }); diff --git a/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts b/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts deleted file mode 100644 index 3b9d68b0a..000000000 --- a/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** @internal Deployless `BluePublicAllocatorReadFixture` query ABI. */ -export const abi = [ - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "bytes32", - name: "id", - type: "bytes32", - }, - ], - name: "absoluteCap", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "bytes32", - name: "id", - type: "bytes32", - }, - ], - name: "canPullFromMarket", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "address", - name: "adapter", - type: "address", - }, - ], - name: "isActiveAdapter", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "bytes32", - name: "id", - type: "bytes32", - }, - { - internalType: "uint256", - name: "value", - type: "uint256", - }, - ], - name: "setAbsoluteCap", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "bytes32", - name: "id", - type: "bytes32", - }, - { - internalType: "bool", - name: "value", - type: "bool", - }, - ], - name: "setCanPullFromMarket", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "address", - name: "adapter", - type: "address", - }, - { - internalType: "bool", - name: "value", - type: "bool", - }, - ], - name: "setIsActiveAdapter", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "bool", - name: "canPullFromIdle", - type: "bool", - }, - { - internalType: "uint64", - name: "penalty", - type: "uint64", - }, - ], - name: "setVaultData", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - ], - name: "vaultData", - outputs: [ - { - internalType: "bool", - name: "canPullFromIdle", - type: "bool", - }, - { - internalType: "uint64", - name: "penalty", - type: "uint64", - }, - ], - stateMutability: "view", - type: "function", - }, -] as const; - -/** @internal Deployless `BluePublicAllocatorReadFixture` query bytecode. */ -export const code = - "0x608080604052346015576103d8908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c90816308f804d81461032957508063391a1d16146102e95780634d29c2d81461029457806366faa8391461023d57806369f1e26b146101f45780638aeed1d11461018f578063d72ff79a146101415763e156c1a814610074575f80fd5b3461013d57606036600319011261013d5761008d610367565b6024359081151580920361013d576044359067ffffffffffffffff821680920361013d57604051926040840184811067ffffffffffffffff8211176101295760405283526020830191825260018060a01b03165f52600360205261010460405f2092511515839060ff801983541691151516179055565b51815468ffffffffffffffff00191660089190911b68ffffffffffffffff0016179055005b634e487b7160e01b5f52604160045260245ffd5b5f80fd5b3461013d57602036600319011261013d576001600160a01b03610162610367565b165f5260036020526040805f205467ffffffffffffffff82519160ff81161515835260081c166020820152f35b3461013d57606036600319011261013d576101f26101ab610367565b6101b361037d565b6101bb610393565b9160018060a01b03165f52600260205260405f209060018060a01b03165f5260205260405f209060ff801983541691151516179055565b005b3461013d57604036600319011261013d576001600160a01b03610215610367565b165f52600160205260405f206024355f52602052602060ff60405f2054166040519015158152f35b3461013d57604036600319011261013d57610256610367565b61025e61037d565b9060018060a01b03165f52600260205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b3461013d57606036600319011261013d576101f26102b0610367565b6102b8610393565b9060018060a01b03165f52600160205260405f206024355f5260205260405f209060ff801983541691151516179055565b3461013d57606036600319011261013d576001600160a01b0361030a610367565b165f525f60205260405f206024355f5260205260443560405f20555f80f35b3461013d57604036600319011261013d576020906001600160a01b0361034d610367565b165f525f825260405f206024355f52825260405f20548152f35b600435906001600160a01b038216820361013d57565b602435906001600160a01b038216820361013d57565b60443590811515820361013d5756fea26469706673582212207e4c910413ca026005bc13c4f22d88556290c586ac09157584e6975b6ac52f5464736f6c63430008240033"; diff --git a/packages/morpho-sdk/contracts/fixtures/BluePublicAllocatorWriteFixture.sol b/packages/morpho-sdk/contracts/fixtures/BluePublicAllocatorWriteFixture.sol deleted file mode 100644 index 87bb3244c..000000000 --- a/packages/morpho-sdk/contracts/fixtures/BluePublicAllocatorWriteFixture.sol +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (c) 2026 Morpho Association -pragma solidity ^0.8.0; - -struct MarketParams { - address loanToken; - address collateralToken; - address oracle; - address irm; - uint256 lltv; -} - -struct VaultData { - bool canPullFromIdle; - uint64 penalty; -} - -interface IERC20 { - function transferFrom(address from, address to, uint256 assets) external returns (bool); -} - -interface IVaultV2 { - function isAllocator(address account) external view returns (bool); - function allocation(bytes32 id) external view returns (uint256); - function allocate(address adapter, bytes memory data, uint256 assets) external; - function deallocate(address adapter, bytes memory data, uint256 assets) external; -} - -/// @dev Test-only fixture pinned to the BluePublicAllocator write ordering and checks. -contract BluePublicAllocatorWriteFixture { - uint256 internal constant WAD = 1e18; - - mapping(address vault => mapping(bytes32 id => uint256)) public absoluteCap; - mapping(address vault => mapping(bytes32 id => bool)) public canPullFromMarket; - mapping(address vault => mapping(address adapter => bool)) public isActiveAdapter; - mapping(address vault => VaultData) public vaultData; - - function setIsActiveAdapter(address vault, address adapter, bool value) external { - require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); - isActiveAdapter[vault][adapter] = value; - } - - function setAbsoluteCap(address vault, address adapter, MarketParams calldata marketParams, uint256 value) - external - { - require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); - absoluteCap[vault][vaultBlueId(adapter, marketParams)] = value; - } - - function setCanPullFromMarket(address vault, address adapter, MarketParams calldata marketParams, bool value) - external - { - require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); - canPullFromMarket[vault][vaultBlueId(adapter, marketParams)] = value; - } - - function setCanPullFromIdle(address vault, bool value) external { - require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); - vaultData[vault].canPullFromIdle = value; - } - - function setPenalty(address vault, uint64 value) external { - require(IVaultV2(vault).isAllocator(msg.sender), "unauthorized"); - require(value <= WAD, "penalty too high"); - vaultData[vault].penalty = value; - } - - function reallocate( - address vault, - address deallocateAdapter, - MarketParams calldata deallocateMarketParams, - address allocateAdapter, - MarketParams calldata allocateMarketParams, - uint128 assets, - uint64 penalty - ) external { - require(vaultData[vault].penalty == penalty, "incorrect penalty"); - transferPenalty(allocateMarketParams.loanToken, msg.sender, vault, assets, penalty); - require(isActiveAdapter[vault][deallocateAdapter], "inactive source adapter"); - require(isActiveAdapter[vault][allocateAdapter], "inactive target adapter"); - - bytes32 deallocateId = vaultBlueId(deallocateAdapter, deallocateMarketParams); - require(canPullFromMarket[vault][deallocateId], "cannot pull from market"); - bytes32 allocateId = vaultBlueId(allocateAdapter, allocateMarketParams); - require(absoluteCap[vault][allocateId] > 0, "zero absolute cap"); - - IVaultV2(vault).deallocate(deallocateAdapter, abi.encode(deallocateMarketParams), assets); - IVaultV2(vault).allocate(allocateAdapter, abi.encode(allocateMarketParams), assets); - - require(IVaultV2(vault).allocation(allocateId) <= absoluteCap[vault][allocateId], "absolute cap exceeded"); - } - - function allocateFromIdle( - address vault, - address adapter, - MarketParams calldata marketParams, - uint128 assets, - uint64 penalty - ) external { - require(vaultData[vault].penalty == penalty, "incorrect penalty"); - transferPenalty(marketParams.loanToken, msg.sender, vault, assets, penalty); - require(isActiveAdapter[vault][adapter], "inactive adapter"); - require(vaultData[vault].canPullFromIdle, "cannot pull from idle"); - - bytes32 allocateId = vaultBlueId(adapter, marketParams); - require(absoluteCap[vault][allocateId] > 0, "zero absolute cap"); - IVaultV2(vault).allocate(adapter, abi.encode(marketParams), assets); - require(IVaultV2(vault).allocation(allocateId) <= absoluteCap[vault][allocateId], "absolute cap exceeded"); - } - - function transferPenalty(address token, address from, address vault, uint256 assets, uint256 penalty) internal { - uint256 penaltyAssets = assets * penalty == 0 ? 0 : (assets * penalty - 1) / WAD + 1; - if (penaltyAssets == 0) return; - - (bool success, bytes memory returnData) = - token.call(abi.encodeCall(IERC20.transferFrom, (from, vault, penaltyAssets))); - require(success && (returnData.length == 0 || abi.decode(returnData, (bool))), "transfer failed"); - } - - function vaultBlueId(address adapter, MarketParams calldata marketParams) internal pure returns (bytes32) { - return keccak256(abi.encode("this/marketParams", adapter, marketParams)); - } -} diff --git a/packages/morpho-sdk/package.json b/packages/morpho-sdk/package.json index 07869589a..c6fe79c2d 100644 --- a/packages/morpho-sdk/package.json +++ b/packages/morpho-sdk/package.json @@ -92,7 +92,6 @@ "build": "tsc --noEmit && $npm_execpath build:cjs && $npm_execpath build:esm", "build:cjs": "tsc --build tsconfig.build.cjs.json && echo '{\"type\":\"commonjs\"}' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.build.esm.json && echo '{\"type\":\"module\"}' > lib/esm/package.json", - "compile": "node ../../scripts/compile-solidity.js morpho-sdk", "test": "vitest --root ../.. --project morpho-sdk" }, "dependencies": { diff --git a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts index 34b520d08..f562243f2 100644 --- a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts +++ b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts @@ -9,6 +9,7 @@ import { fetchAccrualVaultV2, readContractRestructured, vaultV2Abi, + vaultV2BluePublicAllocatorAbi, } from "@morpho-org/blue-sdk-viem"; import type { AnvilTestClient } from "@morpho-org/test"; import { createViemTest } from "@morpho-org/test/vitest"; @@ -22,10 +23,6 @@ import { } from "viem"; import { base } from "viem/chains"; import { assert, describe, expect } from "vitest"; -import { - abi as allocatorAbi, - code as allocatorCode, -} from "../../../test/fixtures/BluePublicAllocatorWriteFixture.js"; import { supplyCollateral } from "../../../test/helpers/blue.js"; import { deployMorphoMarketV1AdapterV2, @@ -41,7 +38,7 @@ import type { VaultV2BlueReallocation } from "../../types/index.js"; const test = createViemTest(base, { forkUrl: process.env.BASE_RPC_URL, - forkBlockNumber: 41_290_768n, + forkBlockNumber: 50_063_965n, // BluePublicAllocator deployment block. stepsTracing: false, }); @@ -191,19 +188,6 @@ describe("Blue actions with Vault V2 reallocations", () => { amount: initialIdleAssets, }); - const deploymentHash = await client.deployContract({ - abi: allocatorAbi, - bytecode: allocatorCode, - }); - const deploymentReceipt = await client.waitForTransactionReceipt({ - hash: deploymentHash, - }); - const fixture = deploymentReceipt.contractAddress; - assert(fixture != null); - const fixtureBytecode = await client.getBytecode({ address: fixture }); - assert(fixtureBytecode != null); - await client.setCode({ address: allocator, bytecode: fixtureBytecode }); - await submitAndAcceptVaultV2Call(anvilClient, { vault, data: encodeFunctionData({ @@ -214,31 +198,31 @@ describe("Blue actions with Vault V2 reallocations", () => { }); await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setIsActiveAdapter", args: [vault, sourceAdapter, true], }); await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setAbsoluteCap", args: [vault, targetAdapter, targetMarket, maxUint128], }); await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setCanPullFromMarket", args: [vault, sourceAdapter, sourceMarket, true], }); await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setCanPullFromIdle", args: [vault, true], }); await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setPenalty", args: [vault, penalty], }); @@ -452,19 +436,6 @@ describe("Blue actions with Vault V2 reallocations", () => { }); } - const deploymentHash = await client.deployContract({ - abi: allocatorAbi, - bytecode: allocatorCode, - }); - const deploymentReceipt = await client.waitForTransactionReceipt({ - hash: deploymentHash, - }); - const fixture = deploymentReceipt.contractAddress; - assert(fixture != null); - const fixtureBytecode = await client.getBytecode({ address: fixture }); - assert(fixtureBytecode != null); - await client.setCode({ address: allocator, bytecode: fixtureBytecode }); - await submitAndAcceptVaultV2Call(anvilClient, { vault, data: encodeFunctionData({ @@ -475,19 +446,19 @@ describe("Blue actions with Vault V2 reallocations", () => { }); await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setIsActiveAdapter", args: [vault, targetAdapter, true], }); await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setAbsoluteCap", args: [vault, targetAdapter, targetMarket, maxUint128], }); await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "setCanPullFromIdle", args: [vault, true], }); @@ -509,7 +480,7 @@ describe("Blue actions with Vault V2 reallocations", () => { }); await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "allocateFromIdle", args: [vault, targetAdapter, targetMarket, seedAssets, 0n], }); @@ -554,7 +525,7 @@ describe("Blue actions with Vault V2 reallocations", () => { await client.writeContract({ address: allocator, - abi: allocatorAbi, + abi: vaultV2BluePublicAllocatorAbi, functionName: "allocateFromIdle", args: [ vault, diff --git a/packages/morpho-sdk/test/fixtures/BluePublicAllocatorWriteFixture.ts b/packages/morpho-sdk/test/fixtures/BluePublicAllocatorWriteFixture.ts deleted file mode 100644 index e378834ac..000000000 --- a/packages/morpho-sdk/test/fixtures/BluePublicAllocatorWriteFixture.ts +++ /dev/null @@ -1,429 +0,0 @@ -/** @internal Test-only `BluePublicAllocatorWriteFixture` contract ABI. */ -export const abi = [ - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "bytes32", - name: "id", - type: "bytes32", - }, - ], - name: "absoluteCap", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "address", - name: "adapter", - type: "address", - }, - { - components: [ - { - internalType: "address", - name: "loanToken", - type: "address", - }, - { - internalType: "address", - name: "collateralToken", - type: "address", - }, - { - internalType: "address", - name: "oracle", - type: "address", - }, - { - internalType: "address", - name: "irm", - type: "address", - }, - { - internalType: "uint256", - name: "lltv", - type: "uint256", - }, - ], - internalType: "struct MarketParams", - name: "marketParams", - type: "tuple", - }, - { - internalType: "uint128", - name: "assets", - type: "uint128", - }, - { - internalType: "uint64", - name: "penalty", - type: "uint64", - }, - ], - name: "allocateFromIdle", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "bytes32", - name: "id", - type: "bytes32", - }, - ], - name: "canPullFromMarket", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "address", - name: "adapter", - type: "address", - }, - ], - name: "isActiveAdapter", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "address", - name: "deallocateAdapter", - type: "address", - }, - { - components: [ - { - internalType: "address", - name: "loanToken", - type: "address", - }, - { - internalType: "address", - name: "collateralToken", - type: "address", - }, - { - internalType: "address", - name: "oracle", - type: "address", - }, - { - internalType: "address", - name: "irm", - type: "address", - }, - { - internalType: "uint256", - name: "lltv", - type: "uint256", - }, - ], - internalType: "struct MarketParams", - name: "deallocateMarketParams", - type: "tuple", - }, - { - internalType: "address", - name: "allocateAdapter", - type: "address", - }, - { - components: [ - { - internalType: "address", - name: "loanToken", - type: "address", - }, - { - internalType: "address", - name: "collateralToken", - type: "address", - }, - { - internalType: "address", - name: "oracle", - type: "address", - }, - { - internalType: "address", - name: "irm", - type: "address", - }, - { - internalType: "uint256", - name: "lltv", - type: "uint256", - }, - ], - internalType: "struct MarketParams", - name: "allocateMarketParams", - type: "tuple", - }, - { - internalType: "uint128", - name: "assets", - type: "uint128", - }, - { - internalType: "uint64", - name: "penalty", - type: "uint64", - }, - ], - name: "reallocate", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "address", - name: "adapter", - type: "address", - }, - { - components: [ - { - internalType: "address", - name: "loanToken", - type: "address", - }, - { - internalType: "address", - name: "collateralToken", - type: "address", - }, - { - internalType: "address", - name: "oracle", - type: "address", - }, - { - internalType: "address", - name: "irm", - type: "address", - }, - { - internalType: "uint256", - name: "lltv", - type: "uint256", - }, - ], - internalType: "struct MarketParams", - name: "marketParams", - type: "tuple", - }, - { - internalType: "uint256", - name: "value", - type: "uint256", - }, - ], - name: "setAbsoluteCap", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "bool", - name: "value", - type: "bool", - }, - ], - name: "setCanPullFromIdle", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "address", - name: "adapter", - type: "address", - }, - { - components: [ - { - internalType: "address", - name: "loanToken", - type: "address", - }, - { - internalType: "address", - name: "collateralToken", - type: "address", - }, - { - internalType: "address", - name: "oracle", - type: "address", - }, - { - internalType: "address", - name: "irm", - type: "address", - }, - { - internalType: "uint256", - name: "lltv", - type: "uint256", - }, - ], - internalType: "struct MarketParams", - name: "marketParams", - type: "tuple", - }, - { - internalType: "bool", - name: "value", - type: "bool", - }, - ], - name: "setCanPullFromMarket", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "address", - name: "adapter", - type: "address", - }, - { - internalType: "bool", - name: "value", - type: "bool", - }, - ], - name: "setIsActiveAdapter", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - { - internalType: "uint64", - name: "value", - type: "uint64", - }, - ], - name: "setPenalty", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "vault", - type: "address", - }, - ], - name: "vaultData", - outputs: [ - { - internalType: "bool", - name: "canPullFromIdle", - type: "bool", - }, - { - internalType: "uint64", - name: "penalty", - type: "uint64", - }, - ], - stateMutability: "view", - type: "function", - }, -] as const; - -/** @internal Test-only `BluePublicAllocatorWriteFixture` contract bytecode. */ -export const code = - "0x6080806040523460155761104e908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f905f3560e01c90816308f804d814610b4a575080635e0deb5414610a9e57806366faa83914610a4757806369f1e26b146109fe57806377b0aab1146109165780638aeed1d1146108565780638fdaa1a7146104d85780639a8a6795146104465780639a8b594414610391578063d72ff79a146103415763df31d68814610097575f80fd5b3461033e5761012036600319011261033e576100b1610b88565b6100b9610b9e565b9060a03660431901126102ac5760e4356001600160801b0381169182820361033c576101043567ffffffffffffffff8116809103610338576001600160a01b038216808752600360205260408720549094906101239060081c67ffffffffffffffff168314610c51565b604435926001600160a01b038416840361033457610142933390610ec6565b81845260026020526040842060018060a01b0384165f5260205260ff60405f205416156102fc57818452600360205260ff604085205416156102bf57839061018984610e3e565b9383835282602052604083208584526020526101aa60408420541515610c91565b6040516101b960208201610cd1565b60a081526101c860c082610bc8565b843b156102bb5783916101ef6040519485938493635c9ce04d60e01b855260048501610dae565b038183875af180156102b057610297575b505060405163c69507dd60e01b81526004810183905291602083602481855afa91821561028c578492610252575b61024f93508452836020526040842090845260205260408320541015610dfa565b80f35b91506020833d602011610284575b8161026d60209383610bc8565b810103126102805761024f92519161022e565b5f80fd5b3d9150610260565b6040513d86823e3d90fd5b816102a191610bc8565b6102ac57825f610200565b8280fd5b6040513d84823e3d90fd5b8380fd5b60405162461bcd60e51b815260206004820152601560248201527463616e6e6f742070756c6c2066726f6d2069646c6560581b6044820152606490fd5b60405162461bcd60e51b815260206004820152601060248201526f34b730b1ba34bb329030b230b83a32b960811b6044820152606490fd5b8780fd5b8580fd5b845b80fd5b503461033e57602036600319011261033e5760409081906001600160a01b03610368610b88565b1681526003602052205467ffffffffffffffff82519160ff81161515835260081c166020820152f35b503461033e57604036600319011261033e576103ab610b88565b6024359081151582036102ac576040516326f6f90760e11b815233600482015291906001600160a01b0316602083602481845afa92831561028c5761024f936103fb918691610417575b50610c16565b83526003602052604083209060ff801983541691151516179055565b610439915060203d60201161043f575b6104318183610bc8565b810190610bfe565b5f6103f5565b503d610427565b503461033e5761010036600319011261033e57610461610b88565b610469610b9e565b9060a03660431901126102ac576040516326f6f90760e11b81523360048201526001600160a01b039190911690602081602481855afa801561028c576104b59185916104175750610c16565b8252816020526104c86040832091610e3e565b825260205260e435604082205580f35b5034610280576101e0366003190112610280576104f3610b88565b6104fb610b9e565b9060a03660431901126102805760e4356001600160a01b038116908181036102805760a036610103190112610280576101a435916001600160801b038316808403610280576101c4359167ffffffffffffffff83168093036102805760018060a01b03861695865f5260036020526105858467ffffffffffffffff60405f205460081c1614610c51565b61010435936001600160a01b03851693848603610280576105a7923387610ec6565b855f52600260205260405f2060018060a01b0388165f5260205260ff60405f2054161561081157855f52600260205260405f20815f5260205260ff60405f205416156107cc576105f687610e3e565b865f52600160205260405f20905f5260205260ff60405f2054161561078757604051602081019160e08352601161010083015270746869732f6d61726b6574506172616d7360781b610120830152604082015261065860608201610104610d40565b610120815261066961014082610bc8565b51902095855f525f60205260405f20875f5260205261068d60405f20541515610c91565b6040519061069d60208301610cd1565b60a082526106ac60c083610bc8565b863b1561028057604051632590ce8b60e11b8152915f91839182916106d6918a9160048501610dae565b0381838a5af1801561077c57610761575b5060405160208101919091528693929150610124356001600160a01b0381169081900361033c576040820152610144356001600160a01b0381169081900361033c576060820152610164356001600160a01b0381169081900361033c5760808201526101843560a082015260a081526101c860c082610bc8565b6107719194939297505f90610bc8565b5f959091925f6106e7565b6040513d5f823e3d90fd5b60405162461bcd60e51b815260206004820152601760248201527f63616e6e6f742070756c6c2066726f6d206d61726b65740000000000000000006044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f696e6163746976652074617267657420616461707465720000000000000000006044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f696e61637469766520736f7572636520616461707465720000000000000000006044820152606490fd5b346102805760603660031901126102805761086f610b88565b610877610b9e565b90604435908115158203610280576040516326f6f90760e11b815233600482015292906001600160a01b0316602084602481845afa93841561077c576108f5946108c7915f916108f75750610c16565b5f52600260205260405f209060018060a01b03165f5260205260405f209060ff801983541691151516179055565b005b610910915060203d60201161043f576104318183610bc8565b866103f5565b346102805760403660031901126102805761092f610b88565b6024359067ffffffffffffffff821690818303610280576040516326f6f90760e11b81523360048201526001600160a01b039190911691602082602481865afa91821561077c57670de0b6b3a764000092610990915f916108f75750610c16565b116109c6575f52600360205260405f209068ffffffffffffffff0082549160081b169068ffffffffffffffff0019161790555f80f35b60405162461bcd60e51b815260206004820152601060248201526f0e0cadcc2d8e8f240e8dede40d0d2ced60831b6044820152606490fd5b34610280576040366003190112610280576001600160a01b03610a1f610b88565b165f52600160205260405f206024355f52602052602060ff60405f2054166040519015158152f35b3461028057604036600319011261028057610a60610b88565b610a68610b9e565b9060018060a01b03165f52600260205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b346102805761010036600319011261028057610ab8610b88565b610ac0610b9e565b9060a03660431901126102805760e435908115158203610280576040516326f6f90760e11b815233600482015292906001600160a01b0316602084602481845afa93841561077c576108f594610b1c915f916108f75750610c16565b5f526001602052610b3060405f2091610e3e565b5f5260205260405f209060ff801983541691151516179055565b34610280576040366003190112610280576020906001600160a01b03610b6e610b88565b165f525f825260405f206024355f52825260405f20548152f35b600435906001600160a01b038216820361028057565b602435906001600160a01b038216820361028057565b35906001600160a01b038216820361028057565b90601f8019910116810190811067ffffffffffffffff821117610bea57604052565b634e487b7160e01b5f52604160045260245ffd5b90816020910312610280575180151581036102805790565b15610c1d57565b60405162461bcd60e51b815260206004820152600c60248201526b1d5b985d5d1a1bdc9a5e995960a21b6044820152606490fd5b15610c5857565b60405162461bcd60e51b8152602060048201526011602482015270696e636f72726563742070656e616c747960781b6044820152606490fd5b15610c9857565b60405162461bcd60e51b815260206004820152601160248201527007a65726f206162736f6c7574652063617607c1b6044820152606490fd5b6044356001600160a01b038116908190036102805781526064356001600160a01b038116908190036102805760208201526084356001600160a01b0381169081900361028057604082015260a4356001600160a01b03811690819003610280576060820152608060c435910152565b60809081906001600160a01b03610d5682610bb4565b1684526001600160a01b03610d6d60208301610bb4565b1660208501526001600160a01b03610d8760408301610bb4565b1660408501526001600160a01b03610da160608301610bb4565b1660608501520135910152565b91608060206001600160801b039260409497969760018060a01b031686526060828701528051918291826060890152018387015e5f828287010152601f80199101168401019416910152565b15610e0157565b60405162461bcd60e51b815260206004820152601560248201527418589cdbdb1d5d194818d85c08195e18d959591959605a1b6044820152606490fd5b604051602081019160e08352601161010083015270746869732f6d61726b6574506172616d7360781b61012083015260018060a01b03166040820152610e88606082016044610d40565b6101208152610e9961014082610bc8565b51902090565b81810292918115918404141715610eb257565b634e487b7160e01b5f52601160045260245ffd5b91929093610ed48183610e9f565b610fe85750505f925b8315610fe2576040516323b872dd60e01b602082019081526001600160a01b0392831660248301529190931660448401526064808401949094529282525f9283928390610f2b608482610bc8565b51925af13d15610fdb573d67ffffffffffffffff8111610bea5760405190610f5d601f8201601f191660200183610bc8565b81523d5f602083013e5b81610fac575b5015610f7557565b60405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606490fd5b8051801592508215610fc1575b50505f610f6d565b610fd49250602080918301019101610bfe565b5f80610fb9565b6060610f67565b50505050565b610ff191610e9f565b5f198101908111610eb257670de0b6b3a7640000900460018101809111610eb25792610edd56fea264697066735822122070f851fe2a72e5c001f93dd70c5380fb09d85e34c5bf026e9cbd8e46d85451e864736f6c63430008240033"; diff --git a/packages/morpho-ts/src/abis.ts b/packages/morpho-ts/src/abis.ts index f96915682..3ae374f68 100644 --- a/packages/morpho-ts/src/abis.ts +++ b/packages/morpho-ts/src/abis.ts @@ -4586,7 +4586,7 @@ export const vaultV1PublicAllocatorAbi = [ */ export const publicAllocatorAbi = vaultV1PublicAllocatorAbi; -/** Vault V2 Blue Public Allocator ABI used for market and idle reallocations. */ +/** Vault V2 Blue Public Allocator ABI used for configuration and reallocations. */ export const vaultV2BluePublicAllocatorAbi = [ { inputs: [ @@ -4612,6 +4612,123 @@ export const vaultV2BluePublicAllocatorAbi = [ stateMutability: "view", type: "function", }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + internalType: "bool", + name: "newIsActiveAdapter", + type: "bool", + }, + ], + name: "setIsActiveAdapter", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + components: marketParamsAbi.components, + internalType: "struct MarketParams", + name: "marketParams", + type: "tuple", + }, + { + internalType: "uint256", + name: "newAbsoluteCap", + type: "uint256", + }, + ], + name: "setAbsoluteCap", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + components: marketParamsAbi.components, + internalType: "struct MarketParams", + name: "marketParams", + type: "tuple", + }, + { + internalType: "bool", + name: "newCanPullFromMarket", + type: "bool", + }, + ], + name: "setCanPullFromMarket", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "bool", + name: "newCanPullFromIdle", + type: "bool", + }, + ], + name: "setCanPullFromIdle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "uint64", + name: "newPenalty", + type: "uint64", + }, + ], + name: "setPenalty", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, { inputs: [ { diff --git a/scripts/compile-solidity.js b/scripts/compile-solidity.js index 274a39db9..b9f0514df 100644 --- a/scripts/compile-solidity.js +++ b/scripts/compile-solidity.js @@ -26,9 +26,6 @@ const packageConfigs = { if (sourceName.includes("/interfaces/")) return null; const parsed = parse(sourceName); - if (sourceName.includes("/fixtures/")) { - return join(packageDir, "test", "fixtures", `${parsed.name}.ts`); - } return join( packageDir, "src", @@ -54,18 +51,6 @@ const packageConfigs = { ); }, }, - "morpho-sdk": { - bytecodeExportName: "code", - describeArtifact(contractName) { - return `Test-only \`${contractName}\` contract`; - }, - resolveOutputPath(sourceName) { - if (!sourceName.includes("/fixtures/")) return null; - - const parsed = parse(sourceName); - return join(packageDir, "test", "fixtures", `${parsed.name}.ts`); - }, - }, }; const config = packageConfigs[packageName]; From 2bc9da85b30909bf8950e0e6b1325eb5b28873e5 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 19 Aug 2026 10:35:11 +0200 Subject: [PATCH 26/41] refactor: align allocator reallocation names --- .changeset/brave-vaults-reallocate.md | 4 +- ...> GetVaultV2BluePublicAllocatorConfig.sol} | 20 ++++---- ...PublicAllocatorConfig.integration.test.ts} | 8 ++-- ... VaultV2BluePublicAllocatorConfig.test.ts} | 25 +++++----- ...ts => VaultV2BluePublicAllocatorConfig.ts} | 46 +++++++++---------- .../blue-sdk-viem/src/fetch/vault-v2/index.ts | 2 +- ...=> GetVaultV2BluePublicAllocatorConfig.ts} | 12 ++--- ...ts => VaultV2BluePublicAllocatorConfig.ts} | 8 ++-- packages/blue-sdk/src/vault/v2/index.ts | 2 +- packages/morpho-sdk/AGENTS.md | 2 +- packages/morpho-sdk/src/actions/AGENTS.md | 2 +- .../actions/blue/buildReallocationActions.ts | 4 +- packages/morpho-sdk/src/entities/blue/blue.ts | 20 ++++---- .../entities/vaultV2BlueReallocationData.ts | 22 +++++---- .../src/helpers/bluePublicAllocator.test.ts | 6 +-- .../src/helpers/bluePublicAllocator.ts | 4 +- .../helpers/computeVaultV1Reallocations.ts | 6 +-- packages/morpho-sdk/src/types/AGENTS.md | 2 +- .../morpho-sdk/src/types/sharedLiquidity.ts | 18 ++++---- .../wdk-protocol-lending-morpho-evm/README.md | 8 ++-- .../src/index.ts | 4 +- .../src/morpho-protocol-evm.test.ts | 4 +- .../src/morpho-protocol-evm.ts | 6 +-- 23 files changed, 121 insertions(+), 114 deletions(-) rename packages/blue-sdk-viem/contracts/vault-v2/{GetVaultV2PublicAllocatorConfig.sol => GetVaultV2BluePublicAllocatorConfig.sol} (75%) rename packages/blue-sdk-viem/src/fetch/vault-v2/{VaultV2PublicAllocatorConfig.integration.test.ts => VaultV2BluePublicAllocatorConfig.integration.test.ts} (93%) rename packages/blue-sdk-viem/src/fetch/vault-v2/{VaultV2PublicAllocatorConfig.test.ts => VaultV2BluePublicAllocatorConfig.test.ts} (89%) rename packages/blue-sdk-viem/src/fetch/vault-v2/{VaultV2PublicAllocatorConfig.ts => VaultV2BluePublicAllocatorConfig.ts} (88%) rename packages/blue-sdk-viem/src/queries/vault-v2/{GetVaultV2PublicAllocatorConfig.ts => GetVaultV2BluePublicAllocatorConfig.ts} (93%) rename packages/blue-sdk/src/vault/v2/{VaultV2PublicAllocatorConfig.ts => VaultV2BluePublicAllocatorConfig.ts} (76%) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index ee37fd869..9f5e3ea8b 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -11,10 +11,10 @@ Add canonical `vaultV1PublicAllocatorAbi` and `vaultV2BluePublicAllocatorAbi` ex V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. -Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. +Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. Compatibility note: `VaultV2MorphoMarketV1AdapterV2.ids()` now declares its existing three-element result as `readonly [Hash, Hash, Hash]`. The runtime values and ordering are unchanged, and derived allocation identifiers are immutable descriptors. We intentionally accept this TypeScript assignability tightening in the minor release; callers that explicitly require a mutable `Hash[]` can copy the tuple with `[...adapter.ids(params)]`. Name allocation-cap helpers `adapterCapId`, `collateralCapId`, and `adapterMarketCapId`. Preserve the published `adapterId`, `collateralId`, and `marketParamsId` helpers as deprecated aliases. -Add an explicit `MorphoBorrowWithV2ReallocationsOptions` WDK opt-in for the combined V1/V2 reallocation union and its possible approval requirement while preserving the legacy `MorphoBorrowOptions` input and authorization-only requirement result type. +Add an explicit `MorphoBorrowWithVaultV2ReallocationsOptions` WDK opt-in for the combined V1/V2 reallocation union and its possible approval requirement while preserving the legacy `MorphoBorrowOptions` input and authorization-only requirement result type. diff --git a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2BluePublicAllocatorConfig.sol similarity index 75% rename from packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol rename to packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2BluePublicAllocatorConfig.sol index 7a8e7863b..6106cbc73 100644 --- a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2PublicAllocatorConfig.sol +++ b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2BluePublicAllocatorConfig.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.0; import {IBluePublicAllocator} from "./interfaces/IBluePublicAllocator.sol"; import {IVaultV2} from "./interfaces/IVaultV2.sol"; -struct VaultV2MarketPublicAllocatorRequest { +struct VaultV2BlueMarketPublicAllocatorRequest { address adapter; bytes32 adapterMarketCapId; } -struct VaultV2MarketPublicAllocatorResponse { +struct VaultV2BlueMarketPublicAllocatorResponse { address adapter; bytes32 adapterMarketCapId; uint256 absoluteCap; @@ -23,22 +23,22 @@ struct VaultV2AllocationResponse { uint256 allocation; } -struct VaultV2PublicAllocatorResponse { +struct VaultV2BluePublicAllocatorResponse { bool canPullFromIdle; uint64 penalty; bool[] isActiveAdapters; - VaultV2MarketPublicAllocatorResponse[] marketConfigs; + VaultV2BlueMarketPublicAllocatorResponse[] marketConfigs; VaultV2AllocationResponse[] allocations; } -contract GetVaultV2PublicAllocatorConfig { +contract GetVaultV2BluePublicAllocatorConfig { function query( IBluePublicAllocator allocator, IVaultV2 vault, address[] calldata adapters, - VaultV2MarketPublicAllocatorRequest[] calldata marketRequests, + VaultV2BlueMarketPublicAllocatorRequest[] calldata marketRequests, bytes32[] calldata allocationIds - ) external view returns (VaultV2PublicAllocatorResponse memory res) { + ) external view returns (VaultV2BluePublicAllocatorResponse memory res) { (res.canPullFromIdle, res.penalty) = allocator.vaultData(address(vault)); uint256 adaptersLength = adapters.length; @@ -48,10 +48,10 @@ contract GetVaultV2PublicAllocatorConfig { } uint256 marketRequestsLength = marketRequests.length; - res.marketConfigs = new VaultV2MarketPublicAllocatorResponse[](marketRequestsLength); + res.marketConfigs = new VaultV2BlueMarketPublicAllocatorResponse[](marketRequestsLength); for (uint256 i; i < marketRequestsLength; ++i) { - VaultV2MarketPublicAllocatorRequest calldata request = marketRequests[i]; - res.marketConfigs[i] = VaultV2MarketPublicAllocatorResponse({ + VaultV2BlueMarketPublicAllocatorRequest calldata request = marketRequests[i]; + res.marketConfigs[i] = VaultV2BlueMarketPublicAllocatorResponse({ adapter: request.adapter, adapterMarketCapId: request.adapterMarketCapId, absoluteCap: allocator.absoluteCap(address(vault), request.adapterMarketCapId), diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts similarity index 93% rename from packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts rename to packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts index 416b43427..26e6c978e 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts @@ -8,7 +8,7 @@ import { base } from "viem/chains"; import { assert, describe, expect } from "vitest"; import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; import { fetchAccrualVaultV2 } from "./VaultV2.js"; -import { fetchVaultV2PublicAllocatorData } from "./VaultV2PublicAllocatorConfig.js"; +import { fetchVaultV2BluePublicAllocatorData } from "./VaultV2BluePublicAllocatorConfig.js"; const vaultV2BluePublicAllocatorTest = createViemTest(base, { forkUrl: process.env.BASE_RPC_URL, @@ -16,7 +16,7 @@ const vaultV2BluePublicAllocatorTest = createViemTest(base, { stepsTracing: false, }); -describe("Vault V2 public allocator fetchers on fork", () => { +describe("Vault V2 BluePublicAllocator fetchers on fork", () => { vaultV2BluePublicAllocatorTest( "default: matches direct reads against the deployless query", async ({ client }) => { @@ -89,10 +89,10 @@ describe("Vault V2 public allocator fetchers on fork", () => { }); const [deployless, direct] = await Promise.all([ - fetchVaultV2PublicAllocatorData(forkVault, client, { + fetchVaultV2BluePublicAllocatorData(forkVault, client, { deployless: "force", }), - fetchVaultV2PublicAllocatorData(forkVault, client, { + fetchVaultV2BluePublicAllocatorData(forkVault, client, { deployless: false, }), ]); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts similarity index 89% rename from packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts rename to packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts index f2dc5aae6..3ee42d559 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts @@ -16,12 +16,12 @@ import { mockDeploylessReads, } from "../../__test__/viem.js"; import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; -import { abi as queryAbi } from "../../queries/vault-v2/GetVaultV2PublicAllocatorConfig.js"; +import { abi as queryAbi } from "../../queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.js"; import { - fetchVaultV2MarketPublicAllocatorConfig, - fetchVaultV2PublicAllocatorConfig, - fetchVaultV2PublicAllocatorData, -} from "./VaultV2PublicAllocatorConfig.js"; + fetchVaultV2BlueMarketPublicAllocatorConfig, + fetchVaultV2BluePublicAllocatorConfig, + fetchVaultV2BluePublicAllocatorData, +} from "./VaultV2BluePublicAllocatorConfig.js"; const ALLOCATOR = getChainAddress(mainnet.id, "vaultV2BluePublicAllocator"); const VAULT: Address = "0x0000000000000000000000000000000000000002"; @@ -158,16 +158,16 @@ const mockDirectReads = ( }); }; -describe("Vault V2 public allocator fetchers", () => { +describe("Vault V2 BluePublicAllocator fetchers", () => { test("default: leaf fetchers use the chain allocator", async () => { const handle = createMockClient(mainnet); mockDirectReads(handle); await expect( - fetchVaultV2PublicAllocatorConfig(VAULT, handle.client), + fetchVaultV2BluePublicAllocatorConfig(VAULT, handle.client), ).resolves.toStrictEqual(expected.publicAllocatorConfig); await expect( - fetchVaultV2MarketPublicAllocatorConfig( + fetchVaultV2BlueMarketPublicAllocatorConfig( VAULT, ADAPTER, adapterMarketCapId, @@ -201,7 +201,7 @@ describe("Vault V2 public allocator fetchers", () => { }); await expect( - fetchVaultV2PublicAllocatorData(vault, handle.client), + fetchVaultV2BluePublicAllocatorData(vault, handle.client), ).resolves.toStrictEqual(expected); }); @@ -211,7 +211,7 @@ describe("Vault V2 public allocator fetchers", () => { mockDirectReads(handle); await expect( - fetchVaultV2PublicAllocatorData(vault, handle.client), + fetchVaultV2BluePublicAllocatorData(vault, handle.client), ).resolves.toStrictEqual(expected); }); @@ -220,7 +220,10 @@ describe("Vault V2 public allocator fetchers", () => { mockDeploylessReads(handle, [new Error("deployless unavailable")]); mockDirectReads(handle, false); - const result = await fetchVaultV2PublicAllocatorData(vault, handle.client); + const result = await fetchVaultV2BluePublicAllocatorData( + vault, + handle.client, + ); expect(result.activeAdapters).toStrictEqual(new Set()); }); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts similarity index 88% rename from packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts rename to packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts index a40dca3dc..0e3c1e9c5 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts @@ -3,8 +3,8 @@ import { AccrualVaultV2MorphoMarketV1AdapterV2, getChainAddress, type IVaultV2Allocation, - type VaultV2MarketPublicAllocatorConfig, - type VaultV2PublicAllocatorConfig, + type VaultV2BlueMarketPublicAllocatorConfig, + type VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import type { Address, Client, Hash } from "viem"; import { getChainId, readContract } from "viem/actions"; @@ -12,7 +12,7 @@ import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; import { abi, code, -} from "../../queries/vault-v2/GetVaultV2PublicAllocatorConfig.js"; +} from "../../queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.js"; import type { DeploylessFetchParameters, FetchParameters, @@ -34,25 +34,25 @@ import type { * @throws {viem.BaseError} when the contract read fails. * @example * ```ts - * import type { VaultV2PublicAllocatorConfig } from "@morpho-org/blue-sdk"; - * import { fetchVaultV2PublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; + * import type { VaultV2BluePublicAllocatorConfig } from "@morpho-org/blue-sdk"; + * import { fetchVaultV2BluePublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; * import { type Address, createPublicClient, http } from "viem"; * import { mainnet } from "viem/chains"; * * const client = createPublicClient({ chain: mainnet, transport: http() }); * export async function fetchAllocatorConfig( * vault: Address, - * ): Promise { - * return fetchVaultV2PublicAllocatorConfig(vault, client); + * ): Promise { + * return fetchVaultV2BluePublicAllocatorConfig(vault, client); * } * ``` */ // biome-ignore lint/complexity/useMaxParams: follows the package's address/client/options fetcher convention -export async function fetchVaultV2PublicAllocatorConfig( +export async function fetchVaultV2BluePublicAllocatorConfig( vault: Address, client: Client, parameters: FetchParameters = {}, -): Promise { +): Promise { const chainId = parameters.chainId ?? (await getChainId(client)); const allocator = getChainAddress(chainId, "vaultV2BluePublicAllocator"); const [canPullFromIdle, penalty] = await readContract(client, { @@ -88,8 +88,8 @@ export async function fetchVaultV2PublicAllocatorConfig( * @throws {viem.BaseError} when one of the contract reads fails. * @example * ```ts - * import type { VaultV2MarketPublicAllocatorConfig } from "@morpho-org/blue-sdk"; - * import { fetchVaultV2MarketPublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; + * import type { VaultV2BlueMarketPublicAllocatorConfig } from "@morpho-org/blue-sdk"; + * import { fetchVaultV2BlueMarketPublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; * import { type Address, createPublicClient, type Hash, http } from "viem"; * import { mainnet } from "viem/chains"; * @@ -98,8 +98,8 @@ export async function fetchVaultV2PublicAllocatorConfig( * vault: Address, * adapter: Address, * adapterMarketCapId: Hash, - * ): Promise { - * return fetchVaultV2MarketPublicAllocatorConfig( + * ): Promise { + * return fetchVaultV2BlueMarketPublicAllocatorConfig( * vault, * adapter, * adapterMarketCapId, @@ -109,13 +109,13 @@ export async function fetchVaultV2PublicAllocatorConfig( * ``` */ // biome-ignore lint/complexity/useMaxParams: follows the package's vault/adapter/id/client/options fetcher convention -export async function fetchVaultV2MarketPublicAllocatorConfig( +export async function fetchVaultV2BlueMarketPublicAllocatorConfig( vault: Address, adapter: Address, adapterMarketCapId: Hash, client: Client, parameters: FetchParameters = {}, -): Promise { +): Promise { const chainId = parameters.chainId ?? (await getChainId(client)); const allocator = getChainAddress(chainId, "vaultV2BluePublicAllocator"); const [absoluteCap, canPullFromMarket] = await Promise.all([ @@ -168,7 +168,7 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( * @example * ```ts * import type { AccrualVaultV2 } from "@morpho-org/blue-sdk"; - * import { fetchVaultV2PublicAllocatorData } from "@morpho-org/blue-sdk-viem"; + * import { fetchVaultV2BluePublicAllocatorData } from "@morpho-org/blue-sdk-viem"; * import { createPublicClient, http } from "viem"; * import { mainnet } from "viem/chains"; * @@ -176,14 +176,14 @@ export async function fetchVaultV2MarketPublicAllocatorConfig( * export async function fetchAllocatorData( * vault: AccrualVaultV2, * ) { - * const data = await fetchVaultV2PublicAllocatorData(vault, client); + * const data = await fetchVaultV2BluePublicAllocatorData(vault, client); * // data contains publicAllocatorConfig, activeAdapters, marketPublicAllocatorConfigs, and allocations. * return data; * } * ``` */ // biome-ignore lint/complexity/useMaxParams: follows the package's entity/client/options fetcher convention -export async function fetchVaultV2PublicAllocatorData( +export async function fetchVaultV2BluePublicAllocatorData( vault: AccrualVaultV2, client: Client, { deployless = true, ...parameters }: DeploylessFetchParameters = {}, @@ -232,7 +232,7 @@ export async function fetchVaultV2PublicAllocatorData( const marketPublicAllocatorConfigs: Record< Hash, - VaultV2MarketPublicAllocatorConfig | undefined + VaultV2BlueMarketPublicAllocatorConfig | undefined > = {}; for (const config of result.marketConfigs) { marketPublicAllocatorConfigs[config.adapterMarketCapId] = { @@ -251,7 +251,7 @@ export async function fetchVaultV2PublicAllocatorData( vault: vault.address, canPullFromIdle: result.canPullFromIdle, penalty: result.penalty, - } satisfies VaultV2PublicAllocatorConfig, + } satisfies VaultV2BluePublicAllocatorConfig, activeAdapters: new Set( adapterList.filter((_, index) => result.isActiveAdapters[index]), ), @@ -270,7 +270,7 @@ export async function fetchVaultV2PublicAllocatorData( marketConfigs, allocationValues, ] = await Promise.all([ - fetchVaultV2PublicAllocatorConfig(vault.address, client, { + fetchVaultV2BluePublicAllocatorConfig(vault.address, client, { ...parameters, chainId, }), @@ -287,7 +287,7 @@ export async function fetchVaultV2PublicAllocatorData( ), Promise.all( marketRequests.map(({ adapter, adapterMarketCapId }) => - fetchVaultV2MarketPublicAllocatorConfig( + fetchVaultV2BlueMarketPublicAllocatorConfig( vault.address, adapter, adapterMarketCapId, @@ -329,7 +329,7 @@ export async function fetchVaultV2PublicAllocatorData( const marketPublicAllocatorConfigs: Record< Hash, - VaultV2MarketPublicAllocatorConfig | undefined + VaultV2BlueMarketPublicAllocatorConfig | undefined > = {}; for (const config of marketConfigs) { marketPublicAllocatorConfigs[config.adapterMarketCapId] = config; diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts index 1a8a3080a..d4078753d 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts @@ -1,6 +1,6 @@ export * from "./VaultV2.js"; export * from "./VaultV2Adapter.js"; +export * from "./VaultV2BluePublicAllocatorConfig.js"; export * from "./VaultV2MorphoMarketV1Adapter.js"; export * from "./VaultV2MorphoMarketV1AdapterV2.js"; export * from "./VaultV2MorphoVaultV1Adapter.js"; -export * from "./VaultV2PublicAllocatorConfig.js"; diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.ts similarity index 93% rename from packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts rename to packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.ts index ed9af0ee3..290ca1410 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.ts @@ -1,4 +1,4 @@ -/** @internal Deployless `GetVaultV2PublicAllocatorConfig` query ABI. */ +/** @internal Deployless `GetVaultV2BluePublicAllocatorConfig` query ABI. */ export const abi = [ { inputs: [ @@ -30,7 +30,7 @@ export const abi = [ type: "bytes32", }, ], - internalType: "struct VaultV2MarketPublicAllocatorRequest[]", + internalType: "struct VaultV2BlueMarketPublicAllocatorRequest[]", name: "marketRequests", type: "tuple[]", }, @@ -82,7 +82,7 @@ export const abi = [ type: "bool", }, ], - internalType: "struct VaultV2MarketPublicAllocatorResponse[]", + internalType: "struct VaultV2BlueMarketPublicAllocatorResponse[]", name: "marketConfigs", type: "tuple[]", }, @@ -114,7 +114,7 @@ export const abi = [ type: "tuple[]", }, ], - internalType: "struct VaultV2PublicAllocatorResponse", + internalType: "struct VaultV2BluePublicAllocatorResponse", name: "res", type: "tuple", }, @@ -124,6 +124,6 @@ export const abi = [ }, ] as const; -/** @internal Deployless `GetVaultV2PublicAllocatorConfig` query bytecode. */ +/** @internal Deployless `GetVaultV2BluePublicAllocatorConfig` query bytecode. */ export const code = - "0x608080604052346015576108bb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c6352ae457214610025575f80fd5b3461030e5760a036600319011261030e576004356001600160a01b038116919082900361030e576024356001600160a01b0381169081900361030e576044356001600160401b03811161030e57610080903690600401610785565b606492919235906001600160401b03821161030e573660238301121561030e578160040135946001600160401b03861161030e573660248760061b8501011161030e576084356001600160401b03811161030e576100e2903690600401610785565b60a083949294018381106001600160401b03821117610771576040525f835260208301935f8552604084019160608352606085019860608a526080860194606086528c60408b6024825180948193636b97fbcd60e11b835260048301525afa801561031a575f915f91610718575b506001600160401b0316895215158752610169816107fe565b61017660405191826107d0565b818152601f19610185836107fe565b0136602083013785528c5f5b82811061067257505050506101a58a6107fe565b6101b260405191826107d0565b8a8152601f196101c18c6107fe565b015f5b81811061065b57505089525f5b8a81101561035e578b908060061b890161022d60208c60446101f560248601610839565b60405163011f009b60e31b81526001600160a01b03909316600484015294013560248201819052959092839190829081906044820190565b03915afa801561031a578f8d86935f93610325575b506040516369f1e26b60e01b81526001600160a01b039190911660048201526024810193909352602090839060449082905afa91821561031a575f926102cf575b509183916102c8936001966040519361029b856107b5565b888060a01b0316845260208401526040830152151560608201528d51906102c2838361084d565b5261084d565b50016101d1565b9150916020823d8211610312575b816102ea602093836107d0565b8101031261030e576001946102c89361030386946107f1565b935091935094610283565b5f80fd5b3d91506102dd565b6040513d5f823e3d90fd5b93505050506020813d8211610356575b81610342602093836107d0565b8101031261030e575183908f8d6020610242565b3d9150610335565b50889291889161036d816107fe565b61037a60405191826107d0565b818152601f19610389836107fe565b015f5b81811061064457505086525f5b8181106104dc576001600160401b0389898989896040519586956020875260c0870195511515602088015251166040860152519260a060608601528351809152602060e086019401905f5b8181106104c1575050505191601f19848203016080850152602080845192838152019301905f5b81811061047a575050505190601f198382030160a0840152602080835192838152019201905f5b818110610440575050500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610432565b825180516001600160a01b0316865260208181015181880152604080830151908801526060918201511515918701919091528796506080909501949092019160010161040b565b825115158652879650602095860195909201916001016103e4565b6104e7818385610815565b359060405191632f0374dd60e21b8352806004840152602083602481895afa92831561031a575f93610611575b5060405163a68bafa360e01b8152600481018290526020816024818a5afa90811561031a575f916105e0575b5060405163c69507dd60e01b815260048101839052906020826024818b5afa91821561031a575f926105aa575b509183916105a39360019660405193610585856107b5565b84526020840152604083015260608201528a51906102c2838361084d565b5001610399565b9150916020823d82116105d8575b816105c5602093836107d0565b8101031261030e5790519091600161056d565b3d91506105b8565b90506020813d8211610609575b816105fa602093836107d0565b8101031261030e57518c610540565b3d91506105ed565b9092506020813d821161063c575b8161062c602093836107d0565b8101031261030e5751918b610514565b3d915061061f565b60209061064f610861565b8282860101520161038c565b602090610666610861565b828286010152016101c4565b60208c604461068a61068585888a610815565b610839565b6040516366faa83960e01b815260048101939093526001600160a01b03166024830152909384919082905afa801561031a575f906106df575b600192506106d282895161084d565b9015159052018d90610191565b506020823d8211610710575b816106f8602093836107d0565b8101031261030e5761070b6001926107f1565b6106c3565b3d91506106eb565b9150506040813d604011610769575b81610734604093836107d0565b8101031261030e576020610747826107f1565b910151906001600160401b038216820361030e57906001600160401b03610150565b3d9150610727565b634e487b7160e01b5f52604160045260245ffd5b9181601f8401121561030e578235916001600160401b03831161030e576020808501948460051b01011161030e57565b608081019081106001600160401b0382111761077157604052565b90601f801991011681019081106001600160401b0382111761077157604052565b5190811515820361030e57565b6001600160401b0381116107715760051b60200190565b91908110156108255760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b038116810361030e5790565b80518210156108255760209160051b010190565b6040519061086e826107b5565b5f606083828152826020820152826040820152015256fea26469706673582212200386873a9ac711f6a4b93b2c556cdaebfe2c1bbaaa1fb3cb4e4ab78ee41c6dbc64736f6c63430008240033"; + "0x608080604052346015576108bb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c6352ae457214610025575f80fd5b3461030e5760a036600319011261030e576004356001600160a01b038116919082900361030e576024356001600160a01b0381169081900361030e576044356001600160401b03811161030e57610080903690600401610785565b606492919235906001600160401b03821161030e573660238301121561030e578160040135946001600160401b03861161030e573660248760061b8501011161030e576084356001600160401b03811161030e576100e2903690600401610785565b60a083949294018381106001600160401b03821117610771576040525f835260208301935f8552604084019160608352606085019860608a526080860194606086528c60408b6024825180948193636b97fbcd60e11b835260048301525afa801561031a575f915f91610718575b506001600160401b0316895215158752610169816107fe565b61017660405191826107d0565b818152601f19610185836107fe565b0136602083013785528c5f5b82811061067257505050506101a58a6107fe565b6101b260405191826107d0565b8a8152601f196101c18c6107fe565b015f5b81811061065b57505089525f5b8a81101561035e578b908060061b890161022d60208c60446101f560248601610839565b60405163011f009b60e31b81526001600160a01b03909316600484015294013560248201819052959092839190829081906044820190565b03915afa801561031a578f8d86935f93610325575b506040516369f1e26b60e01b81526001600160a01b039190911660048201526024810193909352602090839060449082905afa91821561031a575f926102cf575b509183916102c8936001966040519361029b856107b5565b888060a01b0316845260208401526040830152151560608201528d51906102c2838361084d565b5261084d565b50016101d1565b9150916020823d8211610312575b816102ea602093836107d0565b8101031261030e576001946102c89361030386946107f1565b935091935094610283565b5f80fd5b3d91506102dd565b6040513d5f823e3d90fd5b93505050506020813d8211610356575b81610342602093836107d0565b8101031261030e575183908f8d6020610242565b3d9150610335565b50889291889161036d816107fe565b61037a60405191826107d0565b818152601f19610389836107fe565b015f5b81811061064457505086525f5b8181106104dc576001600160401b0389898989896040519586956020875260c0870195511515602088015251166040860152519260a060608601528351809152602060e086019401905f5b8181106104c1575050505191601f19848203016080850152602080845192838152019301905f5b81811061047a575050505190601f198382030160a0840152602080835192838152019201905f5b818110610440575050500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610432565b825180516001600160a01b0316865260208181015181880152604080830151908801526060918201511515918701919091528796506080909501949092019160010161040b565b825115158652879650602095860195909201916001016103e4565b6104e7818385610815565b359060405191632f0374dd60e21b8352806004840152602083602481895afa92831561031a575f93610611575b5060405163a68bafa360e01b8152600481018290526020816024818a5afa90811561031a575f916105e0575b5060405163c69507dd60e01b815260048101839052906020826024818b5afa91821561031a575f926105aa575b509183916105a39360019660405193610585856107b5565b84526020840152604083015260608201528a51906102c2838361084d565b5001610399565b9150916020823d82116105d8575b816105c5602093836107d0565b8101031261030e5790519091600161056d565b3d91506105b8565b90506020813d8211610609575b816105fa602093836107d0565b8101031261030e57518c610540565b3d91506105ed565b9092506020813d821161063c575b8161062c602093836107d0565b8101031261030e5751918b610514565b3d915061061f565b60209061064f610861565b8282860101520161038c565b602090610666610861565b828286010152016101c4565b60208c604461068a61068585888a610815565b610839565b6040516366faa83960e01b815260048101939093526001600160a01b03166024830152909384919082905afa801561031a575f906106df575b600192506106d282895161084d565b9015159052018d90610191565b506020823d8211610710575b816106f8602093836107d0565b8101031261030e5761070b6001926107f1565b6106c3565b3d91506106eb565b9150506040813d604011610769575b81610734604093836107d0565b8101031261030e576020610747826107f1565b910151906001600160401b038216820361030e57906001600160401b03610150565b3d9150610727565b634e487b7160e01b5f52604160045260245ffd5b9181601f8401121561030e578235916001600160401b03831161030e576020808501948460051b01011161030e57565b608081019081106001600160401b0382111761077157604052565b90601f801991011681019081106001600160401b0382111761077157604052565b5190811515820361030e57565b6001600160401b0381116107715760051b60200190565b91908110156108255760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b038116810361030e5790565b80518210156108255760209160051b010190565b6040519061086e826107b5565b5f606083828152826020820152826040820152015256fea26469706673582212206d1f8e3e28ba1c7dcc6b8545e35d5747e7f262aee60dcc1932572527089a903964736f6c63430008240033"; diff --git a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts similarity index 76% rename from packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts rename to packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts index 8ff1d02c1..daf2e7299 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts @@ -1,7 +1,7 @@ import type { Address, Hash } from "../../types.js"; -/** Public allocator configuration for one Vault V2. */ -export interface VaultV2PublicAllocatorConfig { +/** BluePublicAllocator configuration for one Vault V2. */ +export interface VaultV2BluePublicAllocatorConfig { /** Configured Vault V2 address. */ readonly vault: Address; /** Whether the allocator may pull the vault's idle assets into a Blue market. */ @@ -10,8 +10,8 @@ export interface VaultV2PublicAllocatorConfig { readonly penalty: bigint; } -/** Public allocator permission and cap for one Vault V2 adapter-market pair. */ -export interface VaultV2MarketPublicAllocatorConfig { +/** BluePublicAllocator permission and cap for one Vault V2 adapter-market pair. */ +export interface VaultV2BlueMarketPublicAllocatorConfig { /** Configured Vault V2 address. */ readonly vault: Address; /** Vault V2 MorphoMarketV1AdapterV2 address. */ diff --git a/packages/blue-sdk/src/vault/v2/index.ts b/packages/blue-sdk/src/vault/v2/index.ts index ac16936a8..b2c2cdd7e 100644 --- a/packages/blue-sdk/src/vault/v2/index.ts +++ b/packages/blue-sdk/src/vault/v2/index.ts @@ -1,7 +1,7 @@ export * from "./VaultV2.js"; export * from "./VaultV2Adapter.js"; +export * from "./VaultV2BluePublicAllocatorConfig.js"; export * from "./VaultV2MorphoMarketV1Adapter.js"; export * from "./VaultV2MorphoMarketV1AdapterV2.js"; export * from "./VaultV2MorphoVaultV1Adapter.js"; -export * from "./VaultV2PublicAllocatorConfig.js"; export * from "./VaultV2Utils.js"; diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index c21a4bb42..56d047291 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -29,7 +29,7 @@ Protocol terms used across this package's docs and JSDoc: - **PublicAllocator V1** — MetaMorpho allocator that moves liquidity from one or more sorted source markets into a target via `reallocateTo(...)`; each call pays one `fee`. - **BluePublicAllocator** — the single canonical Vault V2 allocator registered per chain, which moves one source market or vault idle liquidity into the enclosing Blue action's target market via `reallocate(...)` or `allocateFromIdle(...)`. The caller supplies adapter addresses; the SDK resolves the allocator from the chain registry. Each call passes the vault's configured WAD-scaled `uint64 penalty`; the allocator pulls `ceil(assets × penalty / WAD)` of the target loan token from Bundler3 and donates it directly to the vault. Its canonical ABI export is `vaultV2BluePublicAllocatorAbi`. - **VaultExitBundlesV1** — standalone periphery for exiting an illiquid VaultV1 or single-adapter VaultV2 into idle underlying assets and/or Morpho Blue supply positions. -- **Shared-liquidity naming** — `VaultV1ReallocationData`, `InputVaultV1ReallocationData`, `computeVaultV1Reallocations`, and `VaultV1BlueReallocation` are canonical for PublicAllocator V1. Their published predecessors (`ReallocationData`, `InputReallocationData`, `computeReallocations`, `getMarketPublicReallocations`, and `VaultReallocation`) remain deprecated aliases. `MorphoBlue.getVaultV1ReallocationData` is the canonical V1 fetcher; unversioned `getReallocationData` is its deprecated alias. `MorphoBlue.getVaultV2BlueReallocationData` fetches the Blue-specific V2 snapshot. `VaultV2BlueReallocationData.computeVaultV2BlueReallocations` discovers every friendly call by default or accepts an optional operation to produce an amount-aware plan; both modes return flat, action-ready `VaultV2BlueReallocation` calls and their simulated state. +- **Shared-liquidity naming** — `VaultV1ReallocationData`, `InputVaultV1ReallocationData`, `computeVaultV1Reallocations`, and `VaultV1Reallocation` are canonical for PublicAllocator V1. Their published predecessors (`ReallocationData`, `InputReallocationData`, `computeReallocations`, `getMarketPublicReallocations`, and `VaultReallocation`) remain deprecated aliases. `MorphoBlue.getVaultV1ReallocationData` is the canonical V1 fetcher; unversioned `getReallocationData` is its deprecated alias. `MorphoBlue.getVaultV2BlueReallocationData` fetches the Blue-specific V2 snapshot. `VaultV2BlueReallocationData.computeVaultV2BlueReallocations` discovers every friendly call by default or accepts an optional operation to produce an amount-aware plan; both modes return flat, action-ready `VaultV2BlueReallocation` calls and their simulated state. ### Bundler actions diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index aa31e8cf3..4ab40d1ae 100644 --- a/packages/morpho-sdk/src/actions/AGENTS.md +++ b/packages/morpho-sdk/src/actions/AGENTS.md @@ -24,7 +24,7 @@ Only valid for assets/collateral configured as wNative. When `nativeAmount > 0`: ## Shared liquidity / reallocations (canonical statement) -`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). `VaultV1BlueReallocation` entries are identified by `withdrawals` and become `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. `VaultV2BlueReallocation` entries are identified by `from` and map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies adapters, the chain registry supplies the allocator, and each call passes the vault's configured WAD-scaled `penalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. High-level builders pull the aggregate V2 penalty in the target loan token through GeneralAdapter1, then each low-level allocator action approves and spends its independently rounded `ceil(assets × penalty / WAD)` amount from Bundler3. Only V1 fees contribute to `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects entries matching both or neither shape; malformed vault and adapter addresses; absent, incomplete, or unknown BluePublicAllocator sources; penalties above WAD; and inconsistent penalties for the same vault. +`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). `VaultV1Reallocation` entries are identified by `withdrawals` and become `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. `VaultV2BlueReallocation` entries are identified by `from` and map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies adapters, the chain registry supplies the allocator, and each call passes the vault's configured WAD-scaled `penalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. High-level builders pull the aggregate V2 penalty in the target loan token through GeneralAdapter1, then each low-level allocator action approves and spends its independently rounded `ceil(assets × penalty / WAD)` amount from Bundler3. Only V1 fees contribute to `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects entries matching both or neither shape; malformed vault and adapter addresses; absent, incomplete, or unknown BluePublicAllocator sources; penalties above WAD; and inconsistent penalties for the same vault. ## Discriminated unions diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 495cc8844..4c339535e 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -1,6 +1,6 @@ import { getChainAddresses, type MarketParams } from "@morpho-org/blue-sdk"; import type { Action } from "../../bundler/index.js"; -import { computeVaultV2ReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; +import { computeVaultV2BlueReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; import { validateReallocations } from "../../helpers/index.js"; import type { BlueReallocation } from "../../types/index.js"; @@ -55,7 +55,7 @@ export const buildReallocationActions = ({ let fee = 0n; const actions: Action[] = []; const penaltyAssets = - computeVaultV2ReallocationPenaltyAssets(reallocationList); + computeVaultV2BlueReallocationPenaltyAssets(reallocationList); if (penaltyAssets > 0n) { const { diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 10be58dba..4bdc3ab21 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -16,7 +16,7 @@ import { fetchPosition, fetchVault, fetchVaultMarketConfig, - fetchVaultV2PublicAllocatorData, + fetchVaultV2BluePublicAllocatorData, } from "@morpho-org/blue-sdk-viem"; import { Time } from "@morpho-org/morpho-ts"; import { type Address, isAddressEqual } from "viem"; @@ -33,7 +33,7 @@ import { getBlueAuthorizationRequirement, getGeneralAdapterRequirements, } from "../../actions/index.js"; -import { computeVaultV2ReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; +import { computeVaultV2BlueReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; import { computeMaxRepaySharePrice, computeMaxSupplySharePrice, @@ -88,7 +88,7 @@ import { type RequirementSignature, selectRequirementSignatures, type Transaction, - type VaultV1BlueReallocation, + type VaultV1Reallocation, WithdrawExceedsCollateralError, } from "../../types/index.js"; import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; @@ -503,7 +503,7 @@ export interface BlueActions { * fees) into the resulting bundle. * * The returned reallocation data can be passed to {@link getReallocations} - * to compute the `VaultV1BlueReallocation[]` array for `borrow()` or + * to compute the `VaultV1Reallocation[]` array for `borrow()` or * `supplyCollateralBorrow()`. * * **Stale data reverts on-chain (fail-safe).** @@ -601,7 +601,7 @@ export interface BlueActions { amount?: never; } ), - ) => readonly VaultV1BlueReallocation[]; + ) => readonly VaultV1Reallocation[]; } export class MorphoBlue implements BlueActions { @@ -616,7 +616,9 @@ export class MorphoBlue implements BlueActions { userAddress: Address, reallocations: Iterable | undefined, ) { - const amount = computeVaultV2ReallocationPenaltyAssets(reallocations ?? []); + const amount = computeVaultV2BlueReallocationPenaltyAssets( + reallocations ?? [], + ); // Separate-token penalty funding uses a classic GeneralAdapter1 allowance so a collateral // permit and a loan-token penalty can coexist in one bundle. The shared-token path aggregates @@ -1478,7 +1480,7 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async (params?: { useSimplePermit?: boolean }) => { const penaltyAssets = - computeVaultV2ReallocationPenaltyAssets(reallocationList); + computeVaultV2BlueReallocationPenaltyAssets(reallocationList); const usesSharedFundingToken = isAddressEqual( this.marketParams.collateralToken, this.marketParams.loanToken, @@ -2003,7 +2005,7 @@ export class MorphoBlue implements BlueActions { Promise.all( vaultAddresses.map(async (address) => { const vault = await fetchAccrualVaultV2(address, client, fetchParams); - const publicAllocatorData = await fetchVaultV2PublicAllocatorData( + const publicAllocatorData = await fetchVaultV2BluePublicAllocatorData( vault, client, fetchParams, @@ -2082,7 +2084,7 @@ export class MorphoBlue implements BlueActions { amount?: never; } ), - ): readonly VaultV1BlueReallocation[] { + ): readonly VaultV1Reallocation[] { validateChainId(params.reallocationData.chainId, this.chainId); const marketId = this.marketParams.id; diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index 3ab6accbb..0721357ea 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -12,8 +12,8 @@ import { MarketUtils, MathLib, UnknownDataError, - type VaultV2MarketPublicAllocatorConfig, - type VaultV2PublicAllocatorConfig, + type VaultV2BlueMarketPublicAllocatorConfig, + type VaultV2BluePublicAllocatorConfig, VaultV2Utils, } from "@morpho-org/blue-sdk"; import { _try, bigIntComparator } from "@morpho-org/morpho-ts"; @@ -57,7 +57,7 @@ export interface InputVaultV2BlueReallocationData { >; /** Vault-wide BluePublicAllocator configuration indexed by vault address. */ readonly publicAllocatorConfigs?: Readonly< - Record + Record >; /** * BluePublicAllocator-active adapters indexed by vault address. @@ -70,7 +70,9 @@ export interface InputVaultV2BlueReallocationData { readonly marketPublicAllocatorConfigs?: Readonly< Record< Address, - | Readonly> + | Readonly< + Record + > | undefined > >; @@ -223,7 +225,7 @@ export class VaultV2BlueReallocationData /** Vault-wide allocator configuration indexed by vault. */ public readonly publicAllocatorConfigs: Record< Address, - VaultV2PublicAllocatorConfig | undefined + VaultV2BluePublicAllocatorConfig | undefined >; /** BluePublicAllocator-active adapters indexed by vault address. */ public readonly activeAdapters: Record< @@ -233,7 +235,7 @@ export class VaultV2BlueReallocationData /** Adapter-market allocator configuration indexed by vault and market-params id. */ public readonly marketPublicAllocatorConfigs: Record< Address, - Record | undefined + Record | undefined >; /** @@ -292,7 +294,7 @@ export class VaultV2BlueReallocationData for (const [vault, config] of Object.entries( input.publicAllocatorConfigs ?? {}, - ) as [Address, VaultV2PublicAllocatorConfig | undefined][]) { + ) as [Address, VaultV2BluePublicAllocatorConfig | undefined][]) { this.publicAllocatorConfigs[vault] = config == null ? undefined : { ...config }; } @@ -309,14 +311,16 @@ export class VaultV2BlueReallocationData ) as [ Address, ( - | Readonly> + | Readonly< + Record + > | undefined ), ][]) { this.marketPublicAllocatorConfigs[vault] = {}; for (const [id, config] of Object.entries(configs ?? {}) as [ Hash, - VaultV2MarketPublicAllocatorConfig | undefined, + VaultV2BlueMarketPublicAllocatorConfig | undefined, ][]) { this.marketPublicAllocatorConfigs[vault]![id] = config == null ? undefined : { ...config }; diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts index 54590e746..cfb61d0bc 100644 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts @@ -2,11 +2,11 @@ import { MarketParams } from "@morpho-org/blue-sdk"; import { describe, expect, test } from "vitest"; import { CbbtcUsdcBlue } from "../../test/fixtures/blue.js"; import type { BlueReallocation } from "../types/index.js"; -import { computeVaultV2ReallocationPenaltyAssets } from "./bluePublicAllocator.js"; +import { computeVaultV2BlueReallocationPenaltyAssets } from "./bluePublicAllocator.js"; const marketParams = new MarketParams(CbbtcUsdcBlue); -describe("computeVaultV2ReallocationPenaltyAssets", () => { +describe("computeVaultV2BlueReallocationPenaltyAssets", () => { test("default", () => { const reallocations: BlueReallocation[] = [ { @@ -30,6 +30,6 @@ describe("computeVaultV2ReallocationPenaltyAssets", () => { }, ]; - expect(computeVaultV2ReallocationPenaltyAssets(reallocations)).toBe(2n); + expect(computeVaultV2BlueReallocationPenaltyAssets(reallocations)).toBe(2n); }); }); diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts index 645556e40..abda4b02f 100644 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts @@ -11,11 +11,11 @@ import type { BlueReallocation } from "../types/index.js"; * @returns Total target loan-token assets needed for V2 penalties. * @example * ```ts - * const penaltyAssets = computeVaultV2ReallocationPenaltyAssets(reallocations); + * const penaltyAssets = computeVaultV2BlueReallocationPenaltyAssets(reallocations); * ``` * @internal */ -export const computeVaultV2ReallocationPenaltyAssets = ( +export const computeVaultV2BlueReallocationPenaltyAssets = ( reallocations: Iterable, ) => { let total = 0n; diff --git a/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts index 23dcdc0c3..68755480e 100644 --- a/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts @@ -7,7 +7,7 @@ import { type PublicReallocation, type ReallocationComputeOptions, ReallocationWithdrawExceedsMarketSupplyError, - type VaultV1BlueReallocation, + type VaultV1Reallocation, } from "../types/index.js"; import { getSupplyTargetUtilization } from "./utilization.js"; import { compareMarketIds } from "./validate.js"; @@ -160,7 +160,7 @@ export const computeVaultV1Reallocations = ({ readonly operation: "borrow" | "withdraw"; readonly amount: bigint; readonly options?: ReallocationComputeOptions; -}): readonly VaultV1BlueReallocation[] => { +}): readonly VaultV1Reallocation[] => { if (options?.enabled === false) return []; const normalizedOptions = { ...options, @@ -292,7 +292,7 @@ export const computeVaultV1Reallocations = ({ }); } - // Transform into VaultV1BlueReallocation[] format. + // Transform into VaultV1Reallocation[] format. return reallocations .filter(({ withdrawals: vaultWithdrawals }) => vaultWithdrawals.length > 0) .map(({ vault, withdrawals: vaultWithdrawals }) => ({ diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index cb633b09d..be61dd1d9 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -18,7 +18,7 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. ## Shared liquidity (`sharedLiquidity.ts`) -- `VaultV1BlueReallocation` — vault address + fee + sorted withdrawals; maps to `reallocateTo()`. `VaultReallocation` is its deprecated compatibility alias. +- `VaultV1Reallocation` — vault address + fee + sorted withdrawals; maps to `reallocateTo()`. `VaultReallocation` is its deprecated compatibility alias. - `VaultV2BlueReallocation` — BluePublicAllocator vault/source/target-adapter/assets/WAD-scaled-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. - `VaultV2BluePublicAllocatorOptions` — canonical Vault V2 discovery and planner options for timestamp, enablement, vault allowlisting, friendly source-market utilization, and the maximum proportional penalty. - `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index a246b5d7f..d0e523574 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -101,7 +101,7 @@ export interface ReallocationWithdrawal { * Maps 1:1 to a `PublicAllocator.reallocateTo()` call. * Withdraws from source markets and supplies to the target market. */ -export interface VaultV1BlueReallocation { +export interface VaultV1Reallocation { readonly vault: Address; /** Fee in native token (ETH) paid to the PublicAllocator for this vault. */ readonly fee: bigint; @@ -109,8 +109,8 @@ export interface VaultV1BlueReallocation { readonly withdrawals: readonly ReallocationWithdrawal[]; } -/** Source of a Blue Public Allocator reallocation. */ -export type BluePublicAllocatorSource = +/** Source of a Vault V2 BluePublicAllocator reallocation. */ +export type VaultV2BlueReallocationSource = | { /** Reallocate from a Morpho Blue market. */ readonly type: "market"; @@ -133,7 +133,7 @@ export interface VaultV2BlueReallocation { /** Vault whose liquidity is moved. */ readonly vault: Address; /** Liquidity source. */ - readonly from: BluePublicAllocatorSource; + readonly from: VaultV2BlueReallocationSource; /** Target Vault V2 adapter; the target market comes from the enclosing action. */ readonly to: { readonly adapter: Address }; /** Asset amount, which must fit in `uint128`. */ @@ -148,16 +148,14 @@ export interface VaultV2BlueReallocation { * V1 entries are identified by `withdrawals`; V2 entries are identified by * `from`. */ -export type BlueReallocation = - | VaultV1BlueReallocation - | VaultV2BlueReallocation; +export type BlueReallocation = VaultV1Reallocation | VaultV2BlueReallocation; /** - * Deprecated name for a Vault V1 Blue reallocation. + * Deprecated name for a Vault V1 reallocation. * - * @deprecated Use {@link VaultV1BlueReallocation} instead. + * @deprecated Use {@link VaultV1Reallocation} instead. */ -export type VaultReallocation = VaultV1BlueReallocation; +export type VaultReallocation = VaultV1Reallocation; /** * Options for computing vault reallocations via the public allocator. diff --git a/packages/wdk-protocol-lending-morpho-evm/README.md b/packages/wdk-protocol-lending-morpho-evm/README.md index 17763433a..0915d4f20 100644 --- a/packages/wdk-protocol-lending-morpho-evm/README.md +++ b/packages/wdk-protocol-lending-morpho-evm/README.md @@ -142,11 +142,11 @@ Morpho SDK enforces a builder/executor invariant for bundled actions. For that r Existing `MorphoBorrowOptions` callers keep the Vault V1 reallocation input and an authorization-only `getBorrowRequirements` result type. To include Vault V2 BluePublicAllocator calls, type the options as -`MorphoBorrowWithV2ReallocationsOptions`; this explicitly widens the result to -include the loan-token approval used for proportional penalty donations: +`MorphoBorrowWithVaultV2ReallocationsOptions`; this explicitly widens the +result to include the loan-token approval used for proportional penalty donations: ```typescript -import type { MorphoBorrowWithV2ReallocationsOptions } from '@morpho-org/wdk-protocol-lending-morpho-evm' +import type { MorphoBorrowWithVaultV2ReallocationsOptions } from '@morpho-org/wdk-protocol-lending-morpho-evm' const options = { token: usdc, @@ -158,7 +158,7 @@ const options = { assets: 1_000_000n, penalty: 1_000_000_000_000_000n }] -} satisfies MorphoBorrowWithV2ReallocationsOptions +} satisfies MorphoBorrowWithVaultV2ReallocationsOptions const requirements = await morpho.getBorrowRequirements(options) ``` diff --git a/packages/wdk-protocol-lending-morpho-evm/src/index.ts b/packages/wdk-protocol-lending-morpho-evm/src/index.ts index 0f89b5f38..302595d91 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/index.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/index.ts @@ -3,7 +3,7 @@ export type { BlueReallocation, RequirementSignature, VaultReallocation, - VaultV1BlueReallocation, + VaultV1Reallocation, VaultV2BlueReallocation, } from "@morpho-org/morpho-sdk"; export type { TransactionResult } from "@tetherto/wdk-wallet"; @@ -32,7 +32,7 @@ export type { Erc4337TransactionConfig, MarketPosition, MorphoBorrowOptions, - MorphoBorrowWithV2ReallocationsOptions, + MorphoBorrowWithVaultV2ReallocationsOptions, MorphoErc20SupplyOptions, MorphoEvmAccount, MorphoNativeSupplyOptions, diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index fe2efd5b1..42b34e7f5 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -6,7 +6,7 @@ import * as viem from "viem"; import { beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest"; import type { MorphoBorrowOptions, - MorphoBorrowWithV2ReallocationsOptions, + MorphoBorrowWithVaultV2ReallocationsOptions, RequirementApproval, RequirementAuthorization, RequirementSignatureRequest, @@ -585,7 +585,7 @@ describe.sequential("MorphoProtocolEvm", () => { penalty: 1n, }, ], - } satisfies MorphoBorrowWithV2ReallocationsOptions; + } satisfies MorphoBorrowWithVaultV2ReallocationsOptions; const promise = protocol.getBorrowRequirements(options); expectTypeOf(promise).toEqualTypeOf< diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 6931bb35a..ce3d5dbfc 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -172,7 +172,7 @@ export interface MorphoBorrowOptions { * include the loan-token approval that a Vault V2 penalty may require. Legacy * {@link MorphoBorrowOptions} callers retain the authorization-only result. */ -export type MorphoBorrowWithV2ReallocationsOptions = Omit< +export type MorphoBorrowWithVaultV2ReallocationsOptions = Omit< MorphoBorrowOptions, "reallocations" > & { @@ -182,7 +182,7 @@ export type MorphoBorrowWithV2ReallocationsOptions = Omit< type MorphoBorrowInput = | MorphoBorrowOptions - | MorphoBorrowWithV2ReallocationsOptions; + | MorphoBorrowWithVaultV2ReallocationsOptions; export interface MorphoRepayOptions { /** The address of the token to repay. */ @@ -698,7 +698,7 @@ export default class MorphoProtocolEvm extends LendingProtocol { options: MorphoBorrowOptions, ): Promise<(RequirementAuthorization | RequirementSignatureRequest)[]>; public getBorrowRequirements( - options: MorphoBorrowWithV2ReallocationsOptions, + options: MorphoBorrowWithVaultV2ReallocationsOptions, ): Promise< ( | RequirementApproval From fbe58b82a74a601cd5e78f0eade58e9d62e249d4 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 19 Aug 2026 11:07:20 +0200 Subject: [PATCH 27/41] refactor: separate allocator reallocation versions --- .changeset/brave-vaults-reallocate.md | 2 +- packages/morpho-sdk/BUNDLER3.md | 6 +- packages/morpho-sdk/src/actions/AGENTS.md | 2 +- .../morpho-sdk/src/actions/blue/AGENTS.md | 6 +- .../blue/borrow.bluePublicAllocator.test.ts | 72 +++-- .../morpho-sdk/src/actions/blue/borrow.ts | 55 ++-- .../actions/blue/buildAssetFundingActions.ts | 2 +- .../actions/blue/buildReallocationActions.ts | 162 +++++----- .../src/actions/blue/refinance.test.ts | 4 +- .../morpho-sdk/src/actions/blue/refinance.ts | 36 ++- .../blue/supplyCollateralBorrow.test.ts | 4 +- .../actions/blue/supplyCollateralBorrow.ts | 56 ++-- .../blue/withdraw.bluePublicAllocator.test.ts | 4 +- .../morpho-sdk/src/actions/blue/withdraw.ts | 40 ++- packages/morpho-sdk/src/entities/AGENTS.md | 2 +- packages/morpho-sdk/src/entities/blue/blue.ts | 133 ++++---- .../src/helpers/bluePublicAllocator.test.ts | 12 +- .../src/helpers/bluePublicAllocator.ts | 14 +- .../morpho-sdk/src/helpers/validate.test.ts | 73 +++-- packages/morpho-sdk/src/helpers/validate.ts | 291 ++++++++++-------- packages/morpho-sdk/src/types/AGENTS.md | 4 +- packages/morpho-sdk/src/types/error.ts | 19 ++ .../morpho-sdk/src/types/sharedLiquidity.ts | 11 +- .../src/index.ts | 1 - .../src/morpho-protocol-evm.ts | 6 +- 25 files changed, 556 insertions(+), 461 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 9f5e3ea8b..79d8986b7 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -17,4 +17,4 @@ Compatibility note: `VaultV2MorphoMarketV1AdapterV2.ids()` now declares its exis Name allocation-cap helpers `adapterCapId`, `collateralCapId`, and `adapterMarketCapId`. Preserve the published `adapterId`, `collateralId`, and `marketParamsId` helpers as deprecated aliases. -Add an explicit `MorphoBorrowWithVaultV2ReallocationsOptions` WDK opt-in for the combined V1/V2 reallocation union and its possible approval requirement while preserving the legacy `MorphoBorrowOptions` input and authorization-only requirement result type. +Add an explicit `MorphoBorrowWithVaultV2ReallocationsOptions` WDK opt-in for Vault V2 reallocations and their possible approval requirement while preserving the legacy Vault V1-only `MorphoBorrowOptions` input and authorization-only requirement result type. Reallocation plans must use exactly one vault version per transaction. diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index ea756f972..1beb9fcef 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -70,9 +70,9 @@ For every ERC-4626 deposit (VaultV1 / VaultV2), GeneralAdapter1 calls `erc4626De ### 4. Shared liquidity without an ad-hoc contract -`BlueReallocation`s encode as Public Allocator V1 `reallocateTo` calls or Blue Public Allocator -`reallocate`/`allocateFromIdle` calls. The same array may contain both, so Vault V1 and Vault V2 -liquidity can be reallocated atomically in one Bundler3 transaction. They are **prepended to the +`BlueReallocationPlan` encodes either Public Allocator V1 `reallocateTo` calls or Blue Public +Allocator `reallocate`/`allocateFromIdle` calls. A plan cannot mix allocator versions. Reallocations +are **prepended to the bundle** for borrow and loan-asset withdraw, **inserted between supply-collateral and borrow** for `supplyCollateralBorrow`, and run **before the supply-collateral callback** for `blueRefinance`. `BundlerAction.encodeBundle` includes Public Allocator V1 fees in `tx.value`. Blue Public Allocator diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index 4ab40d1ae..80ac4f725 100644 --- a/packages/morpho-sdk/src/actions/AGENTS.md +++ b/packages/morpho-sdk/src/actions/AGENTS.md @@ -24,7 +24,7 @@ Only valid for assets/collateral configured as wNative. When `nativeAmount > 0`: ## Shared liquidity / reallocations (canonical statement) -`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept optional `reallocations: BlueReallocation[]` (refinance names the field `targetReallocations`). `VaultV1Reallocation` entries are identified by `withdrawals` and become `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. `VaultV2BlueReallocation` entries are identified by `from` and map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies adapters, the chain registry supplies the allocator, and each call passes the vault's configured WAD-scaled `penalty`. A single array may mix PublicAllocator V1 and BluePublicAllocator entries in one Bundler3 transaction. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. High-level builders pull the aggregate V2 penalty in the target loan token through GeneralAdapter1, then each low-level allocator action approves and spends its independently rounded `ceil(assets × penalty / WAD)` amount from Bundler3. Only V1 fees contribute to `tx.value`; all high-level allocator calls use `skipRevert: false`. Validation lives in `helpers/validateReallocations` and rejects entries matching both or neither shape; malformed vault and adapter addresses; absent, incomplete, or unknown BluePublicAllocator sources; penalties above WAD; and inconsistent penalties for the same vault. +`blueBorrow`, `blueSupplyCollateralBorrow`, loan-asset `blueWithdraw`, and refinance target flows accept an optional homogeneous `BlueReallocationPlan` (refinance names the field `targetReallocations`). `VaultV1Reallocation` entries become `reallocateTo(vault, fee, sortedWithdrawals, targetMarket)` before the primary Blue action; `VaultReallocation` remains a deprecated alias. `VaultV2BlueReallocation` entries map 1:1 to `reallocate(...)` for a market source or `allocateFromIdle(...)` for idle liquidity; the enclosing action supplies the target market, the input supplies adapters, the chain registry supplies the allocator, and each call passes the vault's configured WAD-scaled `penalty`. Mixing allocator versions throws `MixedReallocationVersionsError`. BluePublicAllocator sources are not sorted and idle uses no synthetic zero-address market. High-level builders pull the aggregate V2 penalty in the target loan token through GeneralAdapter1, then each low-level allocator action approves and spends its independently rounded `ceil(assets × penalty / WAD)` amount from Bundler3. Only V1 fees contribute to `tx.value`; all high-level allocator calls use `skipRevert: false`. Normalization dispatches to the separate V1 and V2 validators and action builders. ## Discriminated unions diff --git a/packages/morpho-sdk/src/actions/blue/AGENTS.md b/packages/morpho-sdk/src/actions/blue/AGENTS.md index 9101d708a..c290fa5f6 100644 --- a/packages/morpho-sdk/src/actions/blue/AGENTS.md +++ b/packages/morpho-sdk/src/actions/blue/AGENTS.md @@ -38,9 +38,9 @@ ERC-20 approval spender is **GeneralAdapter1** for any bundled path — never th | `withdraw` | `morphoWithdraw` | | `withdraw` (with reallocations) | `[V2 penalty transfer?] → [allocator reallocation × N] → morphoWithdraw` | -An allocator reallocation is PublicAllocator V1 `reallocateTo` or BluePublicAllocator -`reallocate`/`allocateFromIdle` according to the `BlueReallocation` shape. One bundle may -mix both allocator contracts. For non-zero V2 penalties, the builder adds one aggregate loan-token +An allocator reallocation plan contains only PublicAllocator V1 `reallocateTo` calls or only +BluePublicAllocator `reallocate`/`allocateFromIdle` calls. Separate builders encode each version; +mixing versions throws `MixedReallocationVersionsError`. For non-zero V2 penalties, the V2 builder adds one aggregate loan-token `erc20TransferFrom` into Bundler3 and each allocator action expands to an exact token approval plus the nonpayable allocator call. `BundlerAction.encodeBundle` derives `tx.value` only from native wrapping calls and PublicAllocator V1 native fees. diff --git a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts index 4238822bc..d606b6dfb 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -10,8 +10,10 @@ import { vaultV2BluePublicAllocatorAbi, } from "../../abis.js"; import { - type BlueReallocation, + type BlueReallocationPlan, InconsistentReallocationPenaltyError, + MixedReallocationVersionsError, + type VaultV2BlueReallocation, } from "../../types/index.js"; import { blueBorrow } from "./borrow.js"; @@ -45,12 +47,7 @@ describe("blueBorrow Blue Public Allocator", () => { const { bundler3: { bundler3 }, } = getChainAddresses(ChainId.EthMainnet); - const reallocations: readonly BlueReallocation[] = [ - { - vault: vaultV1, - fee: 2n, - withdrawals: [{ marketParams: sourceMarket, amount: 1n }], - }, + const reallocations: readonly VaultV2BlueReallocation[] = [ { vault: vaultV2, from: { @@ -81,22 +78,14 @@ describe("blueBorrow Blue Public Allocator", () => { }, }); - expect(tx.value).toBe(2n); - expect(tx.action.args.reallocationFee).toBe(2n); + expect(tx.value).toBe(0n); + expect(tx.action.args.reallocationFee).toBe(0n); expect(tx.action.args.reallocationPenaltyAssets).toBe(2n); const bundle = decodeFunctionData({ abi: bundler3Abi, data: tx.data }); const calls = bundle.args[0] ?? []; - expect(calls).toHaveLength(7); - expect(calls.map((call) => call.value)).toEqual([ - 0n, - 2n, - 0n, - 0n, - 0n, - 0n, - 0n, - ]); + expect(calls).toHaveLength(6); + expect(calls.map((call) => call.value)).toEqual([0n, 0n, 0n, 0n, 0n, 0n]); expect(calls.every((call) => call.skipRevert === false)).toBe(true); expect( @@ -106,25 +95,19 @@ describe("blueBorrow Blue Public Allocator", () => { args: [targetMarket.loanToken, bundler3, 2n], }); - const publicAllocatorCall = decodeFunctionData({ - abi: vaultV1PublicAllocatorAbi, - data: calls[1]!.data, - }); - expect(publicAllocatorCall.functionName).toBe("reallocateTo"); - expect(publicAllocatorCall.args[0]).toBe(vaultV1); expect( decodeFunctionData({ abi: vaultV2BluePublicAllocatorAbi, - data: calls[3]!.data, + data: calls[2]!.data, }).functionName, ).toBe("reallocate"); expect( - decodeFunctionData({ abi: erc20Abi, data: calls[2]!.data }), + decodeFunctionData({ abi: erc20Abi, data: calls[1]!.data }), ).toMatchObject({ functionName: "approve", args: [allocator, 1n] }); const idleCall = decodeFunctionData({ abi: vaultV2BluePublicAllocatorAbi, - data: calls[5]!.data, + data: calls[4]!.data, }); expect(idleCall.functionName).toBe("allocateFromIdle"); expect(idleCall.args[0]).toBe(vaultV2); @@ -139,14 +122,43 @@ describe("blueBorrow Blue Public Allocator", () => { expect(idleCall.args[3]).toBe(7n); expect(idleCall.args[4]).toBe(5n); expect( - decodeFunctionData({ abi: erc20Abi, data: calls[4]!.data }), + decodeFunctionData({ abi: erc20Abi, data: calls[3]!.data }), ).toMatchObject({ functionName: "approve", args: [allocator, 1n] }); expect( - decodeFunctionData({ abi: generalAdapter1Abi, data: calls[6]!.data }) + decodeFunctionData({ abi: generalAdapter1Abi, data: calls[5]!.data }) .functionName, ).toBe("morphoBorrow"); }); + test("error: MixedReallocationVersionsError", () => { + const reallocations = [ + { + vault: vaultV1, + fee: 2n, + withdrawals: [{ marketParams: sourceMarket, amount: 1n }], + }, + { + vault: vaultV2, + from: { type: "idle" }, + to: { adapter: targetAdapter }, + assets: 7n, + penalty: 5n, + }, + ] as unknown as BlueReallocationPlan; + + expect(() => + blueBorrow({ + market: { chainId: ChainId.EthMainnet, marketParams: targetMarket }, + args: { + amount: 1n, + minSharePrice: 0n, + receiver, + reallocations, + }, + }), + ).toThrow(MixedReallocationVersionsError); + }); + test("error: InconsistentReallocationPenaltyError", () => { expect(() => blueBorrow({ diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index 3442fcd44..0de5ac601 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -3,17 +3,21 @@ import { deepFreeze } from "@morpho-org/morpho-ts"; import type { Address } from "viem"; import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; +import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import { type AuthorizationRequirementSignature, type BlueBorrowAction, - type BlueReallocation, + type BlueReallocationPlan, type Metadata, NegativeInputError, NonPositiveInputError, type Transaction, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; -import { buildReallocationActions } from "./buildReallocationActions.js"; +import { + buildVaultV1ReallocationActions, + buildVaultV2BlueReallocationActions, +} from "./buildReallocationActions.js"; /** Parameters for {@link blueBorrow}. */ export interface BlueBorrowParams { @@ -28,8 +32,8 @@ export interface BlueBorrowParams { receiver: Address; /** Minimum borrow share price (in ray). Protects against share price manipulation. */ minSharePrice: bigint; - /** Public Allocator V1 or V2 reallocations to execute before borrowing. */ - reallocations?: Iterable; + /** Homogeneous Vault V1 or Vault V2 reallocations to execute before borrowing. */ + reallocations?: BlueReallocationPlan; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a @@ -47,18 +51,18 @@ export interface BlueBorrowParams { * `onBehalf`. Uses `minSharePrice` to protect against share price manipulation between * transaction construction and execution. * - * When `reallocations` are provided, Public Allocator V1 entries encode `reallocateTo`, while V2 - * market and idle entries encode `reallocate` and `allocateFromIdle`. The calls run before the - * borrow. V1 fees accumulate in `tx.value`; V2 penalties are paid in the - * target loan token and donated directly to each vault. + * A `reallocations` plan contains either PublicAllocator V1 entries or Vault V2 + * BluePublicAllocator entries, never both. The calls run before the borrow. + * V1 fees accumulate in `tx.value`; V2 penalties are paid in the target loan + * token and donated directly to each vault. * * @param params.market.chainId - The chain the market lives on. * @param params.market.marketParams - Market params (loanToken, collateralToken, oracle, irm, lltv). * @param params.args.amount - Loan asset amount to borrow, in the loan token's smallest unit. * @param params.args.receiver - Address that receives the borrowed assets. * @param params.args.minSharePrice - Minimum borrow share price (in ray). Slippage protection. - * @param params.args.reallocations - Optional Public Allocator V1 or V2 reallocations to execute - * before borrowing. + * @param params.args.reallocations - Optional homogeneous Vault V1 or Vault V2 reallocations to + * execute before borrowing. * @param params.args.authorizationSignature - Optional signed Morpho authorization; when present, * a `setAuthorizationWithSig` call is prepended to the bundle. * @param params.metadata - Optional analytics metadata attached to the bundle. @@ -71,13 +75,13 @@ export interface BlueBorrowParams { * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. + * @throws {MixedReallocationVersionsError} when one plan contains both V1 and V2 entries. * @throws {NegativeInputError} when `minSharePrice < 0n`, a V1 fee, or a V2 penalty is negative. - * @throws {EmptyReallocationWithdrawalsError} from `buildReallocationActions` when any - * `reallocation.withdrawals` is empty. - * @throws {ReallocationWithdrawalOnTargetMarketError} from `buildReallocationActions` when any - * reallocation withdrawal references the target market. - * @throws {UnsortedReallocationWithdrawalsError} from `buildReallocationActions` when - * reallocation withdrawals are not strictly sorted by market id. + * @throws {EmptyReallocationWithdrawalsError} when any `reallocation.withdrawals` is empty. + * @throws {ReallocationWithdrawalOnTargetMarketError} when any reallocation withdrawal references + * the target market. + * @throws {UnsortedReallocationWithdrawalsError} when reallocation withdrawals are not strictly + * sorted by market id. * @example * ```ts * import { blueBorrow } from "@morpho-org/morpho-sdk"; @@ -118,15 +122,24 @@ export const blueBorrow = ({ actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } + const reallocationPlan = validateAndNormalizeReallocations( + reallocations, + marketParams.id, + ); const { actions: reallocationActions, fee: reallocationFee, penaltyAssets: reallocationPenaltyAssets, - } = buildReallocationActions({ - chainId, - reallocations, - targetMarketParams: marketParams, - }); + } = reallocationPlan.type === "vaultV1" + ? buildVaultV1ReallocationActions({ + reallocations: reallocationPlan.reallocations, + targetMarketParams: marketParams, + }) + : buildVaultV2BlueReallocationActions({ + chainId, + reallocations: reallocationPlan.reallocations, + targetMarketParams: marketParams, + }); actions.push(...reallocationActions); actions.push({ diff --git a/packages/morpho-sdk/src/actions/blue/buildAssetFundingActions.ts b/packages/morpho-sdk/src/actions/blue/buildAssetFundingActions.ts index ed3b53d6b..a3f7f2641 100644 --- a/packages/morpho-sdk/src/actions/blue/buildAssetFundingActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildAssetFundingActions.ts @@ -25,7 +25,7 @@ export interface BuildAssetFundingActionsParams { * `blueSupplyCollateral`, `blueSupplyCollateralBorrow`, `blueRepay`, `blueRepayWithdrawCollateral`). * * Encode-only and synchronous: returns a fresh `Action[]` (never mutates its inputs, mirroring - * `buildReallocationActions`) and reads no on-chain state. When `nativeAmount > 0n`, validates + * `buildVaultV2BlueReallocationActions`) and reads no on-chain state. When `nativeAmount > 0n`, validates * `asset` is the chain's wNative and emits `nativeTransfer → wrapNative`. When `erc20Amount > 0n`, * pulls the ERC-20 via a signed permit/permit2 (`requirementSignature`) or a plain * `erc20TransferFrom` — `getTokenRequirementActions` emits the latter itself when no signature is diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 4c339535e..e0ed29999 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -1,61 +1,57 @@ import { getChainAddresses, type MarketParams } from "@morpho-org/blue-sdk"; import type { Action } from "../../bundler/index.js"; import { computeVaultV2BlueReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; -import { validateReallocations } from "../../helpers/index.js"; -import type { BlueReallocation } from "../../types/index.js"; +import type { + VaultV1Reallocation, + VaultV2BlueReallocation, +} from "../../types/index.js"; -/** - * Builds Public Allocator V1 and Blue Public Allocator actions and their costs. - * - * PublicAllocator V1 entries preserve their `reallocateTo` ABI and validation. Each - * BluePublicAllocator entry maps 1:1 to either `reallocate` for a market source or - * `allocateFromIdle` for an idle source. The enclosing Blue action supplies the target market - * parameters. V2 penalties are moved once in the target loan token to Bundler3; each allocator - * action then approves and spends its independently rounded share. - * - * @param params - Reallocation encoding inputs. - * @param params.chainId - Chain where the bundle will execute. - * @param params.reallocations - Optional PublicAllocator V1 and BluePublicAllocator reallocations in execution order. - * @param params.targetMarketParams - Target market params derived from the enclosing Blue action. - * @param params.penaltyFundingSource - Account that already holds the aggregate V2 penalty. Uses - * the transaction initiator by default; same-token collateral funding can pre-fund - * `GeneralAdapter1` instead. - * @returns Encoded actions, the native V1 fee, and the V2 loan-token penalty total. - * @throws {NegativeInputError} when a PublicAllocator V1 fee or BluePublicAllocator penalty is negative. - * @throws {EmptyReallocationWithdrawalsError} when a PublicAllocator V1 reallocation has no withdrawals. - * @throws {NonPositiveInputError} when a PublicAllocator V1 withdrawal or BluePublicAllocator asset amount is non-positive. - * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when entries for one vault use different penalties. - * @throws {InvalidReallocationAddressError} when a BluePublicAllocator vault or adapter address is malformed. - * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. - * @throws {ReallocationWithdrawalOnTargetMarketError} when a source references the target market. - * @throws {UnsortedReallocationWithdrawalsError} when PublicAllocator V1 withdrawals are not strictly market-id sorted. - * @internal - */ -export const buildReallocationActions = ({ +/** @internal */ +export const buildVaultV1ReallocationActions = ({ + reallocations, + targetMarketParams, +}: { + readonly reallocations: readonly VaultV1Reallocation[]; + readonly targetMarketParams: MarketParams; +}) => { + let fee = 0n; + const actions: Action[] = []; + + for (const reallocation of reallocations) { + actions.push({ + type: "reallocateTo", + args: [ + reallocation.vault, + reallocation.fee, + reallocation.withdrawals.map((withdrawal) => ({ + marketParams: withdrawal.marketParams, + amount: withdrawal.amount, + })), + targetMarketParams, + false, + ], + }); + fee += reallocation.fee; + } + + return { actions, fee, penaltyAssets: 0n }; +}; + +/** @internal */ +export const buildVaultV2BlueReallocationActions = ({ chainId, - reallocations = [], + reallocations, targetMarketParams, penaltyFundingSource = "initiator", }: { readonly chainId: number; - readonly reallocations?: Iterable; + readonly reallocations: readonly VaultV2BlueReallocation[]; readonly targetMarketParams: MarketParams; readonly penaltyFundingSource?: "initiator" | "generalAdapter1"; -}): { - readonly actions: Action[]; - readonly fee: bigint; - readonly penaltyAssets: bigint; -} => { - const reallocationList = [...reallocations]; - // Validate the action descriptors before encoding; the validator returns void. - validateReallocations(reallocationList, targetMarketParams.id); - - let fee = 0n; +}) => { const actions: Action[] = []; const penaltyAssets = - computeVaultV2BlueReallocationPenaltyAssets(reallocationList); + computeVaultV2BlueReallocationPenaltyAssets(reallocations); if (penaltyAssets > 0n) { const { @@ -85,53 +81,35 @@ export const buildReallocationActions = ({ ); } - for (const reallocation of reallocationList) { - if ("from" in reallocation) { - if (reallocation.from.type === "market") { - actions.push({ - type: "vaultV2BluePublicAllocatorReallocate", - args: [ - reallocation.vault, - reallocation.from.adapter, - reallocation.from.marketParams, - reallocation.to.adapter, - targetMarketParams, - reallocation.assets, - reallocation.penalty, - false, - ], - }); - } else { - actions.push({ - type: "vaultV2BluePublicAllocatorAllocateFromIdle", - args: [ - reallocation.vault, - reallocation.to.adapter, - targetMarketParams, - reallocation.assets, - reallocation.penalty, - false, - ], - }); - } - continue; - } - - actions.push({ - type: "reallocateTo", - args: [ - reallocation.vault, - reallocation.fee, - reallocation.withdrawals.map((withdrawal) => ({ - marketParams: withdrawal.marketParams, - amount: withdrawal.amount, - })), - targetMarketParams, - false, - ], - }); - fee += reallocation.fee; + for (const reallocation of reallocations) { + actions.push( + reallocation.from.type === "market" + ? { + type: "vaultV2BluePublicAllocatorReallocate", + args: [ + reallocation.vault, + reallocation.from.adapter, + reallocation.from.marketParams, + reallocation.to.adapter, + targetMarketParams, + reallocation.assets, + reallocation.penalty, + false, + ], + } + : { + type: "vaultV2BluePublicAllocatorAllocateFromIdle", + args: [ + reallocation.vault, + reallocation.to.adapter, + targetMarketParams, + reallocation.assets, + reallocation.penalty, + false, + ], + }, + ); } - return { actions, fee, penaltyAssets }; + return { actions, fee: 0n, penaltyAssets }; }; diff --git a/packages/morpho-sdk/src/actions/blue/refinance.test.ts b/packages/morpho-sdk/src/actions/blue/refinance.test.ts index 566add3bb..c79df3cb4 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.test.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.test.ts @@ -15,7 +15,6 @@ import { vaultV2BluePublicAllocatorAbi, } from "../../abis.js"; import { - type BlueReallocation, NegativeInputError, NonPositiveInputError, ReallocationWithdrawalOnTargetMarketError, @@ -23,6 +22,7 @@ import { RefinanceSharesMissingBorrowAssetsError, RefinanceTokenMismatchError, type VaultReallocation, + type VaultV2BlueReallocation, } from "../../types/index.js"; import { blueRefinance } from "./refinance.js"; @@ -486,7 +486,7 @@ describe("blueRefinance", () => { const { bundler3: { bundler3 }, } = getChainAddresses(mainnet.id); - const targetReallocations: readonly BlueReallocation[] = [ + const targetReallocations: readonly VaultV2BlueReallocation[] = [ { vault: V2_VAULT, from: { diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index b87a82fed..158e00200 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -3,9 +3,10 @@ import { deepFreeze } from "@morpho-org/morpho-ts"; import { type Address, isAddressEqual, maxUint256 } from "viem"; import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; +import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import { type AuthorizationRequirementSignature, - type BlueReallocation, + type BlueReallocationPlan, type BlueRefinanceAction, type Metadata, NegativeInputError, @@ -16,7 +17,10 @@ import { type Transaction, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; -import { buildReallocationActions } from "./buildReallocationActions.js"; +import { + buildVaultV1ReallocationActions, + buildVaultV2BlueReallocationActions, +} from "./buildReallocationActions.js"; /** Parameters for {@link blueRefinance}. */ export interface BlueRefinanceParams { @@ -44,8 +48,8 @@ export interface BlueRefinanceParams { minBorrowSharePrice: bigint; /** Maximum repay share price on the source market (in ray); must be > 0 when a repay leg exists. */ maxRepaySharePrice: bigint; - /** Public Allocator V1 or V2 reallocations into the target market, run before the supply leg. */ - targetReallocations?: Iterable; + /** Homogeneous Vault V1 or Vault V2 reallocations into the target market. */ + targetReallocations?: BlueReallocationPlan; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a @@ -102,8 +106,8 @@ export interface BlueRefinanceParams { * @param params.args.borrowShares - Borrow shares to repay on the source; exclusive with `borrowAssets`. Defaults to `0n`. * @param params.args.minBorrowSharePrice - Minimum borrow share price (ray) on the target. * @param params.args.maxRepaySharePrice - Maximum repay share price (ray) on the source. - * @param params.args.targetReallocations - Public Allocator V1 or V2 reallocations into the target, - * run before the supply leg. V1 fees add to `tx.value`; V2 penalties are paid in the target loan token. + * @param params.args.targetReallocations - Homogeneous Vault V1 or Vault V2 reallocations into the + * target, run before the supply leg. V1 fees add to `tx.value`; V2 penalties are paid in the target loan token. * @param params.args.authorizationSignature - Optional signed Morpho authorization; when present, * a `setAuthorizationWithSig` call is prepended to the bundle. * @param params.metadata - Optional analytics metadata appended to `tx.data`. @@ -117,6 +121,7 @@ export interface BlueRefinanceParams { * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. + * @throws {MixedReallocationVersionsError} when one plan contains both V1 and V2 entries. * @throws {NegativeInputError} when `borrowAssets`, `borrowShares`, `minBorrowSharePrice`, * `maxRepaySharePrice`, a V1 fee, or a V2 penalty is negative. * @throws {RefinanceSameMarketError} when source and target market ids are equal. @@ -279,15 +284,24 @@ export const blueRefinance = ({ actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } + const reallocationPlan = validateAndNormalizeReallocations( + targetReallocations, + targetParams.id, + ); const { actions: reallocationActions, fee: reallocationFee, penaltyAssets: reallocationPenaltyAssets, - } = buildReallocationActions({ - chainId, - reallocations: targetReallocations, - targetMarketParams: targetParams, - }); + } = reallocationPlan.type === "vaultV1" + ? buildVaultV1ReallocationActions({ + reallocations: reallocationPlan.reallocations, + targetMarketParams: targetParams, + }) + : buildVaultV2BlueReallocationActions({ + chainId, + reallocations: reallocationPlan.reallocations, + targetMarketParams: targetParams, + }); actions.push(...reallocationActions); actions.push({ diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts index fc77c4c99..05e213a49 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.test.ts @@ -16,7 +16,6 @@ import { makePermit } from "../../../test/helpers/permit.js"; import { test } from "../../../test/setup.js"; import { bundler3Abi, generalAdapter1Abi } from "../../abis.js"; import { - type BlueReallocation, isRequirementApproval, isRequirementSignature, NativeAmountOnNonWNativeAssetError, @@ -24,6 +23,7 @@ import { NonPositiveInputError, type PermitRequirementSignature, type VaultReallocation, + type VaultV2BlueReallocation, } from "../../types/index.js"; import { getGeneralAdapterRequirements } from "../requirements/index.js"; import * as getTokenRequirementActionsModule from "../signatures/getTokenRequirementActions.js"; @@ -226,7 +226,7 @@ describe("blueSupplyCollateralBorrow unit tests", () => { ...WethUsdsBlue, loanToken: WethUsdsBlue.collateralToken, }); - const reallocations: readonly BlueReallocation[] = [ + const reallocations: readonly VaultV2BlueReallocation[] = [ { vault: WethUsdsBlue.oracle, from: { type: "idle" }, diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index e45a7a63b..f00899608 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -3,9 +3,10 @@ import { deepFreeze } from "@morpho-org/morpho-ts"; import { type Address, isAddressEqual } from "viem"; import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; +import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import { type AuthorizationRequirementSignature, - type BlueReallocation, + type BlueReallocationPlan, type BlueSupplyCollateralBorrowAction, type DepositAmountArgs, type Metadata, @@ -16,7 +17,10 @@ import { } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; import { buildAssetFundingActions } from "./buildAssetFundingActions.js"; -import { buildReallocationActions } from "./buildReallocationActions.js"; +import { + buildVaultV1ReallocationActions, + buildVaultV2BlueReallocationActions, +} from "./buildReallocationActions.js"; /** Parameters for {@link blueSupplyCollateralBorrow}. */ export interface BlueSupplyCollateralBorrowParams { @@ -35,8 +39,8 @@ export interface BlueSupplyCollateralBorrowParams { minSharePrice: bigint; /** Optional pre-signed permit/permit2 approval for the collateral transfer. */ requirementSignature?: PermitRequirementSignature; - /** Public Allocator V1 or V2 reallocations to execute before borrowing. */ - reallocations?: Iterable; + /** Homogeneous Vault V1 or Vault V2 reallocations to execute before borrowing. */ + reallocations?: BlueReallocationPlan; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a @@ -51,8 +55,8 @@ export interface BlueSupplyCollateralBorrowParams { * Prepares an atomic supply-collateral-and-borrow transaction for a Morpho Blue market. * * Routed through bundler3: collateral funding → `morphoSupplyCollateral` → optional Public - * Allocator calls → `morphoBorrow`. V1 entries encode `reallocateTo`; V2 market and idle entries - * encode `reallocate` and `allocateFromIdle`. When `nativeAmount > 0`, native ETH is wrapped via + * Allocator calls → `morphoBorrow`. Each plan contains either V1 or V2 entries, + * never both. When `nativeAmount > 0`, native ETH is wrapped via * `GeneralAdapter1.wrapNative()` before the supply leg. V1 fees add to * `tx.value`; V2 penalties are paid in the target loan token and donated to the vaults. When the * collateral and loan tokens match, one combined pull funds both collateral and penalties through @@ -76,8 +80,8 @@ export interface BlueSupplyCollateralBorrowParams { * collateral funding. When collateral and loan tokens match, its amount includes V2 penalties. * @param params.args.nativeAmount - Optional amount of native token to wrap into wNative for the * collateral supply. Requires the collateral token to be the chain's wNative. - * @param params.args.reallocations - Optional Public Allocator V1 or V2 reallocations to execute - * between the supply and borrow legs. + * @param params.args.reallocations - Optional homogeneous Vault V1 or Vault V2 reallocations to + * execute between the supply and borrow legs. * @param params.args.authorizationSignature - Optional signed Morpho authorization; when present, * a `setAuthorizationWithSig` call is prepended to the bundle. * @param params.metadata - Optional analytics metadata attached to the bundle. @@ -92,6 +96,7 @@ export interface BlueSupplyCollateralBorrowParams { * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. + * @throws {MixedReallocationVersionsError} when one plan contains both V1 and V2 entries. * @throws {ChainWNativeMissingError} when `nativeAmount > 0n` but the chain has no configured wNative. * @throws {NativeAmountOnNonWNativeAssetError} when `nativeAmount > 0n` but the collateral * token is not the chain's wNative. @@ -101,12 +106,11 @@ export interface BlueSupplyCollateralBorrowParams { * is provided and the signed amount differs from the total ERC-20 funding amount. * @throws {Permit2ExpirationMissingError} from `getTokenRequirementActions` when a Permit2 requirement * signature is missing its expiration. - * @throws {EmptyReallocationWithdrawalsError} from `buildReallocationActions` when any - * `reallocation.withdrawals` is empty. - * @throws {ReallocationWithdrawalOnTargetMarketError} from `buildReallocationActions` when any - * reallocation withdrawal references the target market. - * @throws {UnsortedReallocationWithdrawalsError} from `buildReallocationActions` when - * reallocation withdrawals are not strictly sorted by market id. + * @throws {EmptyReallocationWithdrawalsError} when any `reallocation.withdrawals` is empty. + * @throws {ReallocationWithdrawalOnTargetMarketError} when any reallocation withdrawal references + * the target market. + * @throws {UnsortedReallocationWithdrawalsError} when reallocation withdrawals are not strictly + * sorted by market id. * @example * ```ts * import { blueSupplyCollateralBorrow } from "@morpho-org/morpho-sdk"; @@ -167,14 +171,24 @@ export const blueSupplyCollateralBorrow = ({ marketParams.collateralToken, marketParams.loanToken, ); - const reallocationResult = buildReallocationActions({ - chainId, + const reallocationPlan = validateAndNormalizeReallocations( reallocations, - targetMarketParams: marketParams, - penaltyFundingSource: usesSharedFundingToken - ? "generalAdapter1" - : "initiator", - }); + marketParams.id, + ); + const reallocationResult = + reallocationPlan.type === "vaultV1" + ? buildVaultV1ReallocationActions({ + reallocations: reallocationPlan.reallocations, + targetMarketParams: marketParams, + }) + : buildVaultV2BlueReallocationActions({ + chainId, + reallocations: reallocationPlan.reallocations, + targetMarketParams: marketParams, + penaltyFundingSource: usesSharedFundingToken + ? "generalAdapter1" + : "initiator", + }); const erc20FundingAmount = amount + (usesSharedFundingToken ? reallocationResult.penaltyAssets : 0n); diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts index 7a431bf38..dad2f0352 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.bluePublicAllocator.test.ts @@ -11,7 +11,7 @@ import { generalAdapter1Abi, vaultV2BluePublicAllocatorAbi, } from "../../abis.js"; -import type { BlueReallocation } from "../../types/index.js"; +import type { VaultV2BlueReallocation } from "../../types/index.js"; import { blueWithdraw } from "./withdraw.js"; const allocator = getChainAddresses(mainnet.id).vaultV2BluePublicAllocator!; @@ -25,7 +25,7 @@ describe("blueWithdraw Blue Public Allocator", () => { const { bundler3: { bundler3 }, } = getChainAddresses(mainnet.id); - const reallocations: readonly BlueReallocation[] = [ + const reallocations: readonly VaultV2BlueReallocation[] = [ { vault, from: { diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index 147cecd71..a17e38f1c 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -3,9 +3,10 @@ import { deepFreeze } from "@morpho-org/morpho-ts"; import type { Address } from "viem"; import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; +import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import { type AuthorizationRequirementSignature, - type BlueReallocation, + type BlueReallocationPlan, type BlueWithdrawAction, type Metadata, MutuallyExclusiveWithdrawAmountsError, @@ -14,7 +15,10 @@ import { type Transaction, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; -import { buildReallocationActions } from "./buildReallocationActions.js"; +import { + buildVaultV1ReallocationActions, + buildVaultV2BlueReallocationActions, +} from "./buildReallocationActions.js"; /** Parameters for {@link blueWithdraw}. */ export interface BlueWithdrawParams { @@ -32,11 +36,11 @@ export interface BlueWithdrawParams { /** Minimum withdraw share price (in ray). Slippage protection. */ minSharePrice: bigint; /** - * Public Allocator V1 or V2 reallocations to execute before withdrawing. V1 entries can be + * Homogeneous Vault V1 or Vault V2 reallocations to execute before withdrawing. V1 entries can be * computed via `MorphoBlue.getReallocations({ operation: "withdraw", amount })` or directly * via `computeVaultV1Reallocations({ operation: "withdraw", amount, ... })`. */ - reallocations?: Iterable; + reallocations?: BlueReallocationPlan; /** * Optional signed Morpho authorization. When provided, a `setAuthorizationWithSig` call is * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a @@ -56,8 +60,8 @@ export interface BlueWithdrawParams { * - **By shares** (`assets = 0, shares > 0`): burns an exact share count (typical for a full * supplier position close; immune to interest accrual between tx construction and execution). * - * When `reallocations` are provided, V1 entries encode `reallocateTo`, while V2 market and idle - * entries encode `reallocate` and `allocateFromIdle`. The calls run before the withdraw. V1 + * A `reallocations` plan contains either V1 entries or V2 market/idle entries, + * never both. The calls run before the withdraw. V1 * fees accumulate in `tx.value`; V2 penalties are paid in the target loan * token and donated to the vaults. The on-chain `morphoWithdraw` sends the * assets computed on-chain directly to `receiver`; no skim is required. @@ -74,8 +78,8 @@ export interface BlueWithdrawParams { * @param params.args.receiver - Address that receives the withdrawn assets. * @param params.args.minSharePrice - Minimum acceptable withdraw share price (in ray). Slippage * protection. - * @param params.args.reallocations - Optional Public Allocator V1 or V2 reallocations to execute - * before withdrawing. + * @param params.args.reallocations - Optional homogeneous Vault V1 or Vault V2 reallocations to + * execute before withdrawing. * @param params.args.authorizationSignature - Optional signed Morpho authorization; when present, * a `setAuthorizationWithSig` call is prepended to the bundle. * @param params.metadata - Optional analytics metadata attached to the bundle. @@ -90,6 +94,7 @@ export interface BlueWithdrawParams { * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. + * @throws {MixedReallocationVersionsError} when one plan contains both V1 and V2 entries. * @throws {MutuallyExclusiveWithdrawAmountsError} when both `assets` and `shares` are non-zero. * @throws {EmptyReallocationWithdrawalsError} when any reallocation has no withdrawals. * @throws {ReallocationWithdrawalOnTargetMarketError} when a reallocation withdrawal references @@ -151,15 +156,24 @@ export const blueWithdraw = ({ actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } + const reallocationPlan = validateAndNormalizeReallocations( + reallocations, + marketParams.id, + ); const { actions: reallocationActions, fee: reallocationFee, penaltyAssets: reallocationPenaltyAssets, - } = buildReallocationActions({ - chainId, - reallocations, - targetMarketParams: marketParams, - }); + } = reallocationPlan.type === "vaultV1" + ? buildVaultV1ReallocationActions({ + reallocations: reallocationPlan.reallocations, + targetMarketParams: marketParams, + }) + : buildVaultV2BlueReallocationActions({ + chainId, + reallocations: reallocationPlan.reallocations, + targetMarketParams: marketParams, + }); actions.push(...reallocationActions); actions.push({ diff --git a/packages/morpho-sdk/src/entities/AGENTS.md b/packages/morpho-sdk/src/entities/AGENTS.md index 54f3b0f7a..9438c3d97 100644 --- a/packages/morpho-sdk/src/entities/AGENTS.md +++ b/packages/morpho-sdk/src/entities/AGENTS.md @@ -15,6 +15,6 @@ See [`packages/morpho-sdk/AGENTS.md`](../../AGENTS.md) routing summary. ## Shared liquidity -`MorphoBlue.borrow()`, `supplyCollateralBorrow()`, `withdraw()`, and `refinance()` accept optional reallocations. Consumer-supplied reallocation plans and vault allowlists accept any iterable and are normalized once before lazy or repeated use; ordered outputs remain readonly arrays. The entity validates their state-independent shape before returning requirements, and the pure action repeats the same validation before encoding. `getVaultV1ReallocationData` and `getVaultV2BlueReallocationData` fetch the versioned inputs needed to compute reallocations; deprecated `getReallocationData` delegates to the V1 fetcher. Action encoding stays outside every entity fetch path. +`MorphoBlue.borrow()`, `supplyCollateralBorrow()`, `withdraw()`, and `refinance()` accept optional homogeneous V1-or-V2 reallocation plans; mixing versions is rejected. Consumer-supplied reallocation plans and vault allowlists accept any iterable and are normalized once before lazy or repeated use; ordered outputs remain readonly arrays. The entity validates their state-independent shape before returning requirements, and the pure action repeats the same validation before encoding. `getVaultV1ReallocationData` and `getVaultV2BlueReallocationData` fetch the versioned inputs needed to compute reallocations; deprecated `getReallocationData` delegates to the V1 fetcher. Action encoding stays outside every entity fetch path. `VaultV1ReallocationData` is the entity-level state container for PublicAllocator V1 simulations; `ReallocationData` remains its deprecated compatibility alias. `VaultV2BlueReallocationData` owns the separate BluePublicAllocator state model. Their public maps are readable snapshots for inspection; state transitions stay on their methods and return cloned instances of the same versioned class. diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 4bdc3ab21..4d178dd41 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -45,19 +45,19 @@ import { validateNativeAsset, validatePositionHealth, validatePositionHealthAfterWithdraw, - validateReallocations, validateRepayAmount, validateRepayShares, validateSlippageTolerance, validateWithdrawAmount, validateWithdrawShares, } from "../../helpers/index.js"; +import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import type { FetchParameters } from "../../types/data.js"; import { type AssetsOrSharesArgs, type BlueAuthorizationAction, type BlueBorrowAction, - type BlueReallocation, + type BlueReallocationPlan, type BlueRefinanceAction, type BlueRepayAction, type BlueRepayWithdrawCollateralAction, @@ -89,6 +89,7 @@ import { selectRequirementSignatures, type Transaction, type VaultV1Reallocation, + type VaultV2BlueReallocation, WithdrawExceedsCollateralError, } from "../../types/index.js"; import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; @@ -192,8 +193,8 @@ export interface BlueActions { * * Computes `minSharePrice` from market supply state and `slippageTolerance`. * - * When `reallocations` is provided, V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` - * actions are prepended to move liquidity before withdrawing. V1 fees add + * When `reallocations` is provided, its homogeneous V1 or V2 actions are + * prepended to move liquidity before withdrawing. V1 fees add * to the transaction value; V2 penalties are paid in the loan token. * * `getRequirements` returns the loan-token approval needed for V2 penalties @@ -209,6 +210,7 @@ export interface BlueActions { * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. + * @throws {MixedReallocationVersionsError} when one plan contains both V1 and V2 entries. */ withdraw: ( params: { @@ -216,7 +218,7 @@ export interface BlueActions { receiver?: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: Iterable; + reallocations?: BlueReallocationPlan; } & AssetsOrSharesArgs, ) => { buildTx: ( @@ -238,8 +240,8 @@ export interface BlueActions { * Validates position health with LLTV buffer (0.5%) using the pre-fetched `positionData`. * Computes `minSharePrice` from market borrow state and `slippageTolerance`. * - * When `reallocations` is provided, V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` - * actions are prepended before borrowing. V1 fees add to the transaction + * When `reallocations` is provided, its homogeneous V1 or V2 actions are + * prepended before borrowing. V1 fees add to the transaction * value; V2 penalties are paid in the loan token. * * `getRequirements` returns the loan-token approval needed for V2 penalties @@ -254,13 +256,14 @@ export interface BlueActions { * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. + * @throws {MixedReallocationVersionsError} when one plan contains both V1 and V2 entries. */ borrow: (params: { userAddress: Address; amount: bigint; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: Iterable; + reallocations?: BlueReallocationPlan; }) => { buildTx: ( signatures?: readonly RequirementSignature[], @@ -389,8 +392,8 @@ export interface BlueActions { * Routed through the bundler. Validates position health with LLTV buffer * to prevent instant liquidation on new positions near the LLTV threshold. * - * When `reallocations` is provided, V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` - * actions run between the collateral supply and `morphoBorrow`. V1 fees add + * When `reallocations` is provided, its homogeneous V1 or V2 actions run + * between the collateral supply and `morphoBorrow`. V1 fees add * to the transaction value; V2 penalties are paid in the loan token. * * `getRequirements` returns in parallel: @@ -407,6 +410,7 @@ export interface BlueActions { * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. + * @throws {MixedReallocationVersionsError} when one plan contains both V1 and V2 entries. */ supplyCollateralBorrow: ( params: { @@ -414,7 +418,7 @@ export interface BlueActions { positionData: AccrualPosition; borrowAmount: bigint; slippageTolerance?: bigint; - reallocations?: Iterable; + reallocations?: BlueReallocationPlan; } & DepositAmountArgs, ) => { buildTx: ( @@ -444,8 +448,8 @@ export interface BlueActions { * both the residual source and the aggregate target position stay within LLTV − buffer. Both * markets are forward-accrued to `now`; in shares mode the target borrow is overshot by * `slippageTolerance` and the callback sweeps the residual. - * Target reallocations run first as V1 `reallocateTo` or V2 `reallocate`/`allocateFromIdle` - * actions; V1 fees add to the transaction value and V2 penalties are paid + * A homogeneous V1 or V2 target reallocation plan runs first; V1 fees add + * to the transaction value and V2 penalties are paid * in the loan token. * * `getRequirements` returns the loan-token approval needed for V2 penalties @@ -459,13 +463,14 @@ export interface BlueActions { * @param params.borrowAssets - Loan assets to repay on source; exclusive with `borrowShares`. * @param params.borrowShares - Borrow shares to repay on source; exclusive with `borrowAssets`. * @param params.slippageTolerance - WAD slippage tolerance. Defaults to `DEFAULT_SLIPPAGE_TOLERANCE`. - * @param params.targetReallocations - Public Allocator V1 or V2 reallocations into the target market. + * @param params.targetReallocations - Homogeneous Vault V1 or Vault V2 reallocations into the target market. * @returns Object with `buildTx` and `getRequirements`. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. * @throws {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. + * @throws {MixedReallocationVersionsError} when one plan contains both V1 and V2 entries. */ refinance: (params: { userAddress: Address; @@ -478,7 +483,7 @@ export interface BlueActions { borrowAssets?: bigint; borrowShares?: bigint; slippageTolerance?: bigint; - targetReallocations?: Iterable; + targetReallocations?: BlueReallocationPlan; }) => { buildTx: ( signatures?: readonly RequirementSignature[], @@ -614,11 +619,9 @@ export class MorphoBlue implements BlueActions { private getReallocationPenaltyRequirements( userAddress: Address, - reallocations: Iterable | undefined, + reallocations: Iterable, ) { - const amount = computeVaultV2BlueReallocationPenaltyAssets( - reallocations ?? [], - ); + const amount = computeVaultV2BlueReallocationPenaltyAssets(reallocations); // Separate-token penalty funding uses a classic GeneralAdapter1 allowance so a collateral // permit and a loan-token penalty can coexist in one bundle. The shared-token path aggregates @@ -742,7 +745,7 @@ export class MorphoBlue implements BlueActions { receiver?: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: Iterable; + reallocations?: BlueReallocationPlan; } & AssetsOrSharesArgs, ) { validateChainId(this.client.viemClient.chain?.id, this.chainId); @@ -754,7 +757,11 @@ export class MorphoBlue implements BlueActions { slippageTolerance = DEFAULT_SLIPPAGE_TOLERANCE, reallocations, } = params; - const reallocationList = [...(reallocations ?? [])]; + const reallocationPlan = validateAndNormalizeReallocations( + reallocations, + this.marketParams.id, + ); + const reallocationList = reallocationPlan.reallocations; // Mode normalization: a missing or undefined `assets`/`shares` key collapses to `0n` // so the mutual-exclusion and positivity checks below are pure value comparisons. @@ -779,11 +786,6 @@ export class MorphoBlue implements BlueActions { } validateSlippageTolerance(slippageTolerance); - if (reallocationList.length > 0) { - // Validate caller-supplied descriptors before reading state; the helper returns void. - validateReallocations(reallocationList, this.marketParams.id); - } - if (!positionData) { throw new MissingAccrualPositionError(this.marketParams.id); } @@ -818,10 +820,12 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { const [penaltyRequirements, authTx] = await Promise.all([ - this.getReallocationPenaltyRequirements( - userAddress, - reallocationList, - ), + reallocationPlan.type === "vaultV2Blue" + ? this.getReallocationPenaltyRequirements( + userAddress, + reallocationPlan.reallocations, + ) + : Promise.resolve([]), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -920,21 +924,20 @@ export class MorphoBlue implements BlueActions { userAddress: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: Iterable; + reallocations?: BlueReallocationPlan; }) { validateChainId(this.client.viemClient.chain?.id, this.chainId); - const reallocationList = [...(reallocations ?? [])]; + const reallocationPlan = validateAndNormalizeReallocations( + reallocations, + this.marketParams.id, + ); + const reallocationList = reallocationPlan.reallocations; if (amount <= 0n) { throw new NonPositiveInputError("amount", amount); } validateSlippageTolerance(slippageTolerance); - if (reallocationList.length > 0) { - // Validate caller-supplied descriptors before reading state; the helper returns void. - validateReallocations(reallocationList, this.marketParams.id); - } - if (!positionData) { throw new MissingAccrualPositionError(this.marketParams.id); } @@ -961,10 +964,12 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { const [penaltyRequirements, authTx] = await Promise.all([ - this.getReallocationPenaltyRequirements( - userAddress, - reallocationList, - ), + reallocationPlan.type === "vaultV2Blue" + ? this.getReallocationPenaltyRequirements( + userAddress, + reallocationPlan.reallocations, + ) + : Promise.resolve([]), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -1422,10 +1427,14 @@ export class MorphoBlue implements BlueActions { positionData: AccrualPosition; borrowAmount: bigint; slippageTolerance?: bigint; - reallocations?: Iterable; + reallocations?: BlueReallocationPlan; } & DepositAmountArgs) { validateChainId(this.client.viemClient.chain?.id, this.chainId); - const reallocationList = [...(reallocations ?? [])]; + const reallocationPlan = validateAndNormalizeReallocations( + reallocations, + this.marketParams.id, + ); + const reallocationList = reallocationPlan.reallocations; if (amount < 0n) { throw new NegativeInputError("amount", amount); @@ -1445,11 +1454,6 @@ export class MorphoBlue implements BlueActions { } validateSlippageTolerance(slippageTolerance); - if (reallocationList.length > 0) { - // Validate caller-supplied descriptors before reading state; the helper returns void. - validateReallocations(reallocationList, this.marketParams.id); - } - if (!positionData) { throw new MissingAccrualPositionError(this.marketParams.id); } @@ -1480,7 +1484,11 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async (params?: { useSimplePermit?: boolean }) => { const penaltyAssets = - computeVaultV2BlueReallocationPenaltyAssets(reallocationList); + reallocationPlan.type === "vaultV2Blue" + ? computeVaultV2BlueReallocationPenaltyAssets( + reallocationPlan.reallocations, + ) + : 0n; const usesSharedFundingToken = isAddressEqual( this.marketParams.collateralToken, this.marketParams.loanToken, @@ -1498,11 +1506,11 @@ export class MorphoBlue implements BlueActions { from: userAddress, }, }), - usesSharedFundingToken + usesSharedFundingToken || reallocationPlan.type === "vaultV1" ? Promise.resolve([]) : this.getReallocationPenaltyRequirements( userAddress, - reallocationList, + reallocationPlan.reallocations, ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, @@ -1567,11 +1575,15 @@ export class MorphoBlue implements BlueActions { borrowAssets?: bigint; borrowShares?: bigint; slippageTolerance?: bigint; - targetReallocations?: Iterable; + targetReallocations?: BlueReallocationPlan; }) { validateChainId(this.client.viemClient.chain?.id, this.chainId); validateSlippageTolerance(slippageTolerance); - const targetReallocationList = [...(targetReallocations ?? [])]; + const targetReallocationPlan = validateAndNormalizeReallocations( + targetReallocations, + target.marketParams.id, + ); + const targetReallocationList = targetReallocationPlan.reallocations; if (collateralAmount <= 0n) { throw new NonPositiveInputError("collateralAmount", collateralAmount); @@ -1588,11 +1600,6 @@ export class MorphoBlue implements BlueActions { if (requestedAssets > 0n && requestedShares > 0n) { throw new BorrowAmountAndSharesExclusiveError(this.marketParams.id); } - if (targetReallocationList.length > 0) { - // Validate caller-supplied descriptors before reading state; the helper returns void. - validateReallocations(targetReallocationList, target.marketParams.id); - } - if (!positionData) { throw new MissingAccrualPositionError(this.marketParams.id); } @@ -1748,10 +1755,12 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { const [penaltyRequirements, authTx] = await Promise.all([ - this.getReallocationPenaltyRequirements( - userAddress, - targetReallocationList, - ), + targetReallocationPlan.type === "vaultV2Blue" + ? this.getReallocationPenaltyRequirements( + userAddress, + targetReallocationPlan.reallocations, + ) + : Promise.resolve([]), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts index cfb61d0bc..dd752c149 100644 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts @@ -1,19 +1,11 @@ -import { MarketParams } from "@morpho-org/blue-sdk"; import { describe, expect, test } from "vitest"; import { CbbtcUsdcBlue } from "../../test/fixtures/blue.js"; -import type { BlueReallocation } from "../types/index.js"; +import type { VaultV2BlueReallocation } from "../types/index.js"; import { computeVaultV2BlueReallocationPenaltyAssets } from "./bluePublicAllocator.js"; -const marketParams = new MarketParams(CbbtcUsdcBlue); - describe("computeVaultV2BlueReallocationPenaltyAssets", () => { test("default", () => { - const reallocations: BlueReallocation[] = [ - { - vault: CbbtcUsdcBlue.oracle, - fee: 7n, - withdrawals: [{ marketParams, amount: 1n }], - }, + const reallocations: VaultV2BlueReallocation[] = [ { vault: CbbtcUsdcBlue.oracle, from: { type: "idle" }, diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts index abda4b02f..1c7b1dff4 100644 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts +++ b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts @@ -1,13 +1,12 @@ import { MathLib } from "@morpho-org/blue-sdk"; -import type { BlueReallocation } from "../types/index.js"; +import type { VaultV2BlueReallocation } from "../types/index.js"; /** - * Sums the independently rounded vault-asset penalties in a mixed V1/V2 plan. + * Sums the independently rounded vault-asset penalties in a Vault V2 plan. * - * PublicAllocator V1 entries are ignored because their fees are paid in native - * token. Each V2 call is rounded independently, matching contract execution. + * Each call is rounded independently, matching contract execution. * - * @param reallocations - Mixed PublicAllocator V1 and BluePublicAllocator plan. + * @param reallocations - Vault V2 BluePublicAllocator plan. * @returns Total target loan-token assets needed for V2 penalties. * @example * ```ts @@ -16,12 +15,11 @@ import type { BlueReallocation } from "../types/index.js"; * @internal */ export const computeVaultV2BlueReallocationPenaltyAssets = ( - reallocations: Iterable, + reallocations: Iterable, ) => { let total = 0n; for (const reallocation of reallocations) { - if ("from" in reallocation) - total += MathLib.wMulUp(reallocation.assets, reallocation.penalty); + total += MathLib.wMulUp(reallocation.assets, reallocation.penalty); } return total; }; diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index 24d9d1c69..d280f6e77 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -18,7 +18,7 @@ import { import { AccrualPositionUserMismatchError, AddressMismatchError, - type BlueReallocation, + type BlueReallocationPlan, BorrowExceedsSafeLtvError, ChainIdMismatchError, ChainWNativeMissingError, @@ -49,6 +49,7 @@ import { import { MAX_SLIPPAGE_TOLERANCE } from "./constant.js"; import { validateAccrualPosition, + validateAndNormalizeReallocations, validateChainId, validateMidnightMarketChainId, validateNativeAsset, @@ -59,6 +60,7 @@ import { validateRepayShares, validateSlippageTolerance, validateUserAddress, + validateVaultV2BlueReallocations, validateWithdrawAmount, validateWithdrawShares, } from "./validate.js"; @@ -547,7 +549,7 @@ describe("validateRepayShares", () => { // validateReallocations // --------------------------------------------------------------------------- -describe("validateReallocations", () => { +describe("reallocation validation", () => { const targetMarketId = marketParams.id; const sourceMarketA = new MarketParams(CbbtcUsdcBlue); const marketParamsWithId = (id: MarketId) => ({ @@ -577,7 +579,7 @@ describe("validateReallocations", () => { test("behavior: accepts a valid Blue Public Allocator idle reallocation", () => { expect(() => - validateReallocations( + validateVaultV2BlueReallocations( [validBluePublicAllocatorReallocation], targetMarketId, ), @@ -623,14 +625,14 @@ describe("validateReallocations", () => { "error: rejects Blue Public Allocator $name", ({ reallocation, ErrorClass }) => { expect(() => - validateReallocations([reallocation], targetMarketId), + validateVaultV2BlueReallocations([reallocation], targetMarketId), ).toThrow(ErrorClass); }, ); test("error: InconsistentReallocationPenaltyError for one vault", () => { expect(() => - validateReallocations( + validateVaultV2BlueReallocations( [ { ...validBluePublicAllocatorReallocation, penalty: 5n }, { ...validBluePublicAllocatorReallocation, penalty: 11n }, @@ -642,7 +644,7 @@ describe("validateReallocations", () => { test("behavior: accepts the maximum uint128 asset amount", () => { expect(() => - validateReallocations( + validateVaultV2BlueReallocations( [ { ...validBluePublicAllocatorReallocation, @@ -656,7 +658,7 @@ describe("validateReallocations", () => { test("behavior: allows different penalties for different vaults", () => { expect(() => - validateReallocations( + validateVaultV2BlueReallocations( [ { ...validBluePublicAllocatorReallocation, penalty: 5n }, { @@ -672,7 +674,7 @@ describe("validateReallocations", () => { test("error: ReallocationWithdrawalOnTargetMarketError for a Blue Public Allocator target-market source", () => { expect(() => - validateReallocations( + validateVaultV2BlueReallocations( [ { ...validBluePublicAllocatorReallocation, @@ -690,7 +692,7 @@ describe("validateReallocations", () => { test("error: target market through a different Vault V2 adapter", () => { expect(() => - validateReallocations( + validateVaultV2BlueReallocations( [ { ...validBluePublicAllocatorReallocation, @@ -721,11 +723,11 @@ describe("validateReallocations", () => { adapter: USER_A, marketParams: plainMarketParams, }, - } as unknown as BlueReallocation; + } as unknown as VaultV2BlueReallocation; - expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( - ReallocationWithdrawalOnTargetMarketError, - ); + expect(() => + validateVaultV2BlueReallocations([reallocation], targetMarketId), + ).toThrow(ReallocationWithdrawalOnTargetMarketError); }); test.each([ @@ -739,22 +741,22 @@ describe("validateReallocations", () => { const reallocation = { ...validBluePublicAllocatorReallocation, ...overrides, - } as unknown as BlueReallocation; + } as unknown as VaultV2BlueReallocation; - expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( - InvalidReallocationAddressError, - ); + expect(() => + validateVaultV2BlueReallocations([reallocation], targetMarketId), + ).toThrow(InvalidReallocationAddressError); }); test("error: InvalidReallocationSourceTypeError", () => { const reallocation = { ...validBluePublicAllocatorReallocation, from: { type: "marketTypo" }, - } as unknown as BlueReallocation; + } as unknown as VaultV2BlueReallocation; - expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( - InvalidReallocationSourceTypeError, - ); + expect(() => + validateVaultV2BlueReallocations([reallocation], targetMarketId), + ).toThrow(InvalidReallocationSourceTypeError); }); test.each([ @@ -768,22 +770,22 @@ describe("validateReallocations", () => { const reallocation = { ...validBluePublicAllocatorReallocation, from, - } as unknown as BlueReallocation; + } as unknown as VaultV2BlueReallocation; - expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( - InvalidReallocationSourceTypeError, - ); + expect(() => + validateVaultV2BlueReallocations([reallocation], targetMarketId), + ).toThrow(InvalidReallocationSourceTypeError); }); test("error: InvalidReallocationAddressError for missing source adapter", () => { const reallocation = { ...validBluePublicAllocatorReallocation, from: { type: "market", marketParams: sourceMarketA }, - } as unknown as BlueReallocation; + } as unknown as VaultV2BlueReallocation; - expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( - InvalidReallocationAddressError, - ); + expect(() => + validateVaultV2BlueReallocations([reallocation], targetMarketId), + ).toThrow(InvalidReallocationAddressError); }); test.each([ @@ -792,7 +794,7 @@ describe("validateReallocations", () => { reallocation: { vault: USER_A, fee: 0n, - } as unknown as BlueReallocation, + } as unknown as VaultV2BlueReallocation, }, { name: "entry matching both shapes", @@ -802,12 +804,15 @@ describe("validateReallocations", () => { to: { adapter: USER_A }, assets: 1n, penalty: 0n, - } as unknown as BlueReallocation, + } as unknown as VaultV2BlueReallocation, }, ])("error: InvalidReallocationShapeError for $name", ({ reallocation }) => { - expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( - InvalidReallocationShapeError, - ); + expect(() => + validateAndNormalizeReallocations( + [reallocation] as unknown as BlueReallocationPlan, + targetMarketId, + ), + ).toThrow(InvalidReallocationShapeError); }); test("should throw NegativeInputError when fee is negative", () => { diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index c880c37c7..612538d33 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -12,7 +12,7 @@ import { type Address, isAddress, isAddressEqual, maxUint128 } from "viem"; import { AccrualPositionUserMismatchError, AddressMismatchError, - type BlueReallocation, + type BlueReallocationPlan, BorrowExceedsSafeLtvError, ChainIdMismatchError, ChainWNativeMissingError, @@ -26,6 +26,7 @@ import { MarketIdMismatchError, MissingClientPropertyError, MissingMarketPriceError, + MixedReallocationVersionsError, NativeAmountOnNonWNativeAssetError, NegativeInputError, NonPositiveInputError, @@ -33,6 +34,8 @@ import { RepayExceedsDebtError, RepaySharesExceedDebtError, UnsortedReallocationWithdrawalsError, + type VaultV1Reallocation, + type VaultV2BlueReallocation, WithdrawExceedsCollateralError, WithdrawExceedsSupplyError, WithdrawMakesPositionUnhealthyError, @@ -329,32 +332,15 @@ export const validateRepayShares = (params: { }; /** - * Validates that Public Allocator V1 and Blue Public Allocator reallocations are well-formed. + * Validates that Vault V1 PublicAllocator reallocations are well-formed. * - * V1 entries preserve the following invariants: - * - `fee` must be non-negative. - * - `withdrawals` must be non-empty. - * - Every withdrawal `amount` must be strictly positive. - * - No withdrawal may target `targetMarketId`. - * - Withdrawal market IDs must be strictly ascending. - * - * BluePublicAllocator entries enforce a WAD-bounded `penalty`, one consistent - * penalty per vault, positive `uint128`-bounded `assets`, and a - * market source distinct from the target market. Idle sources - * have no market or sorting rule. - * - * @param reallocations - The reallocations to validate. - * @param targetMarketId - The operation's target market ID. Neither V1 nor V2 sources can reference it. + * @param reallocations - Vault V1 reallocations to validate. + * @param targetMarketId - The operation's target market ID. * @returns Nothing when every reallocation is valid. * @throws {NegativeInputError} when a reallocation fee is negative. * @throws {EmptyReallocationWithdrawalsError} when a reallocation has no withdrawals. - * @throws {NonPositiveInputError} when a withdrawal or BluePublicAllocator asset amount is non-positive. - * @throws {InputExceedsMaxError} when a BluePublicAllocator asset amount exceeds `uint128` or its penalty exceeds WAD. - * @throws {InconsistentReallocationPenaltyError} when entries for one vault use different penalties. - * @throws {InvalidReallocationAddressError} when a BluePublicAllocator vault or adapter address is malformed. - * @throws {InvalidReallocationSourceTypeError} when a BluePublicAllocator source is absent, incomplete, or has an unknown discriminator. - * @throws {InvalidReallocationShapeError} when an entry matches both or neither V1/V2 shape. - * @throws {ReallocationWithdrawalOnTargetMarketError} when a V1 or V2 source references the target market. + * @throws {NonPositiveInputError} when a withdrawal amount is non-positive. + * @throws {ReallocationWithdrawalOnTargetMarketError} when a withdrawal references the target market. * @throws {UnsortedReallocationWithdrawalsError} when withdrawals are not strictly market-id sorted. * @example * ```ts @@ -366,131 +352,176 @@ export const validateRepayShares = (params: { * ``` */ export const validateReallocations = ( - reallocations: Iterable, + reallocations: Iterable, targetMarketId: MarketId, ): void => { - const penaltyByVault = new Map(); - - for (const r of reallocations) { - if ("from" in r === "withdrawals" in r) { - throw new InvalidReallocationShapeError(); + for (const reallocation of reallocations) { + if (reallocation.fee < 0n) { + throw new NegativeInputError("reallocation.fee", reallocation.fee); } - - if ("from" in r) { - if (typeof r.vault !== "string" || !isAddress(r.vault)) { - throw new InvalidReallocationAddressError("vault"); - } - if ( - r.to == null || - typeof r.to.adapter !== "string" || - !isAddress(r.to.adapter) - ) { - throw new InvalidReallocationAddressError("to.adapter"); - } - - const source = r.from; - if (source == null) { - throw new InvalidReallocationSourceTypeError(undefined); - } - const sourceType: string | undefined = source.type; - if (sourceType !== "market" && sourceType !== "idle") { - throw new InvalidReallocationSourceTypeError(sourceType); - } - let sourceMarketId: MarketId | undefined; - if (source.type === "market") { - if (typeof source.adapter !== "string" || !isAddress(source.adapter)) { - throw new InvalidReallocationAddressError("from.adapter"); - } - if ( - source.marketParams == null || - !isAddress(source.marketParams.loanToken) || - !isAddress(source.marketParams.collateralToken) || - !isAddress(source.marketParams.oracle) || - !isAddress(source.marketParams.irm) || - typeof source.marketParams.lltv !== "bigint" - ) { - throw new InvalidReallocationSourceTypeError( - "market", - "marketParams", - ); - } - sourceMarketId = MarketUtils.getMarketId(source.marketParams); - } - if (r.penalty < 0n) { - throw new NegativeInputError("reallocation.penalty", r.penalty); - } - if (r.penalty > MathLib.WAD) { - throw new InputExceedsMaxError({ - field: "reallocation.penalty", - value: r.penalty, - max: MathLib.WAD, - }); - } - if (r.assets <= 0n) { - throw new NonPositiveInputError("reallocation.assets", r.assets); - } - if (r.assets > maxUint128) { - throw new InputExceedsMaxError({ - field: "reallocation.assets", - value: r.assets, - max: maxUint128, - }); + if (reallocation.withdrawals.length === 0) { + throw new EmptyReallocationWithdrawalsError(reallocation.vault); + } + let previousMarketId: MarketId | undefined; + for (const withdrawal of reallocation.withdrawals) { + if (withdrawal.amount <= 0n) { + throw new NonPositiveInputError( + `reallocation.withdrawals[${withdrawal.marketParams.id}].amount`, + withdrawal.amount, + ); } - - const penaltyKey = r.vault.toLowerCase(); - const expectedPenalty = penaltyByVault.get(penaltyKey); - if (expectedPenalty !== undefined && expectedPenalty !== r.penalty) { - throw new InconsistentReallocationPenaltyError({ - vault: r.vault, - expected: expectedPenalty, - actual: r.penalty, - }); + if (withdrawal.marketParams.id === targetMarketId) { + throw new ReallocationWithdrawalOnTargetMarketError( + reallocation.vault, + withdrawal.marketParams.id, + ); } - penaltyByVault.set(penaltyKey, r.penalty); - if ( - sourceMarketId !== undefined && - compareMarketIds(sourceMarketId, targetMarketId) === 0 + previousMarketId !== undefined && + compareMarketIds(withdrawal.marketParams.id, previousMarketId) <= 0 ) { - throw new ReallocationWithdrawalOnTargetMarketError( - r.vault, - sourceMarketId, + throw new UnsortedReallocationWithdrawalsError( + reallocation.vault, + withdrawal.marketParams.id, ); } - continue; + previousMarketId = withdrawal.marketParams.id; + } + } +}; + +/** @internal */ +export const validateVaultV2BlueReallocations = ( + reallocations: Iterable, + targetMarketId: MarketId, +): void => { + const penaltyByVault = new Map(); + + for (const reallocation of reallocations) { + if ( + typeof reallocation.vault !== "string" || + !isAddress(reallocation.vault) + ) { + throw new InvalidReallocationAddressError("vault"); } - if (r.fee < 0n) { - throw new NegativeInputError("reallocation.fee", r.fee); + if ( + reallocation.to == null || + typeof reallocation.to.adapter !== "string" || + !isAddress(reallocation.to.adapter) + ) { + throw new InvalidReallocationAddressError("to.adapter"); } - if (r.withdrawals.length === 0) { - throw new EmptyReallocationWithdrawalsError(r.vault); + + const source = reallocation.from; + if (source == null) { + throw new InvalidReallocationSourceTypeError(undefined); } - let prevId: MarketId | undefined; - for (const w of r.withdrawals) { - if (w.amount <= 0n) { - throw new NonPositiveInputError( - `reallocation.withdrawals[${w.marketParams.id}].amount`, - w.amount, - ); - } - if (w.marketParams.id === targetMarketId) { - throw new ReallocationWithdrawalOnTargetMarketError( - r.vault, - w.marketParams.id, - ); + const sourceType: string | undefined = source.type; + if (sourceType !== "market" && sourceType !== "idle") { + throw new InvalidReallocationSourceTypeError(sourceType); + } + let sourceMarketId: MarketId | undefined; + if (source.type === "market") { + if (typeof source.adapter !== "string" || !isAddress(source.adapter)) { + throw new InvalidReallocationAddressError("from.adapter"); } if ( - prevId !== undefined && - compareMarketIds(w.marketParams.id, prevId) <= 0 + source.marketParams == null || + !isAddress(source.marketParams.loanToken) || + !isAddress(source.marketParams.collateralToken) || + !isAddress(source.marketParams.oracle) || + !isAddress(source.marketParams.irm) || + typeof source.marketParams.lltv !== "bigint" ) { - throw new UnsortedReallocationWithdrawalsError( - r.vault, - w.marketParams.id, - ); + throw new InvalidReallocationSourceTypeError("market", "marketParams"); } - prevId = w.marketParams.id; + sourceMarketId = MarketUtils.getMarketId(source.marketParams); + } + if (reallocation.penalty < 0n) { + throw new NegativeInputError( + "reallocation.penalty", + reallocation.penalty, + ); + } + if (reallocation.penalty > MathLib.WAD) { + throw new InputExceedsMaxError({ + field: "reallocation.penalty", + value: reallocation.penalty, + max: MathLib.WAD, + }); + } + if (reallocation.assets <= 0n) { + throw new NonPositiveInputError( + "reallocation.assets", + reallocation.assets, + ); + } + if (reallocation.assets > maxUint128) { + throw new InputExceedsMaxError({ + field: "reallocation.assets", + value: reallocation.assets, + max: maxUint128, + }); + } + + const penaltyKey = reallocation.vault.toLowerCase(); + const expectedPenalty = penaltyByVault.get(penaltyKey); + if ( + expectedPenalty !== undefined && + expectedPenalty !== reallocation.penalty + ) { + throw new InconsistentReallocationPenaltyError({ + vault: reallocation.vault, + expected: expectedPenalty, + actual: reallocation.penalty, + }); + } + penaltyByVault.set(penaltyKey, reallocation.penalty); + + if ( + sourceMarketId !== undefined && + compareMarketIds(sourceMarketId, targetMarketId) === 0 + ) { + throw new ReallocationWithdrawalOnTargetMarketError( + reallocation.vault, + sourceMarketId, + ); + } + } +}; + +/** @internal */ +export const validateAndNormalizeReallocations = ( + reallocations: BlueReallocationPlan | undefined, + targetMarketId: MarketId, +) => { + const vaultV1Reallocations: VaultV1Reallocation[] = []; + const vaultV2Reallocations: VaultV2BlueReallocation[] = []; + + for (const reallocation of reallocations ?? []) { + if ("from" in reallocation === "withdrawals" in reallocation) { + throw new InvalidReallocationShapeError(); + } + if ("withdrawals" in reallocation) { + vaultV1Reallocations.push(reallocation); + } else { + vaultV2Reallocations.push(reallocation); } } + + if (vaultV1Reallocations.length > 0 && vaultV2Reallocations.length > 0) { + throw new MixedReallocationVersionsError(); + } + if (vaultV2Reallocations.length > 0) { + validateVaultV2BlueReallocations(vaultV2Reallocations, targetMarketId); + return { + type: "vaultV2Blue" as const, + reallocations: vaultV2Reallocations, + }; + } + + validateReallocations(vaultV1Reallocations, targetMarketId); + return { type: "vaultV1" as const, reallocations: vaultV1Reallocations }; }; /** diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index be61dd1d9..3fdb5785a 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -21,7 +21,7 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. - `VaultV1Reallocation` — vault address + fee + sorted withdrawals; maps to `reallocateTo()`. `VaultReallocation` is its deprecated compatibility alias. - `VaultV2BlueReallocation` — BluePublicAllocator vault/source/target-adapter/assets/WAD-scaled-penalty input; maps 1:1 to `reallocate()` or `allocateFromIdle()` while deriving target market params from the enclosing Blue action. - `VaultV2BluePublicAllocatorOptions` — canonical Vault V2 discovery and planner options for timestamp, enablement, vault allowlisting, friendly source-market utilization, and the maximum proportional penalty. -- `BlueReallocation` — additive union accepted by Blue action and entity pass-through surfaces; preserves every V1 caller. +- `BlueReallocationPlan` — homogeneous iterable accepted by Blue action and entity pass-through surfaces; a plan contains only V1 or only V2 reallocations. ## Errors (`error.ts`) @@ -29,7 +29,7 @@ One class per error case. Never throw a generic `Error` from SDK source. - **Generic input bounds:** `NegativeInputError` for values that must be non-negative, `NonPositiveInputError` for values that must be positive, and `InputExceedsMaxError` for protocol upper bounds such as BluePublicAllocator's `uint128` assets and WAD-scaled `uint64` penalty. All expose the invalid `field` and `value`; reuse them across Vault, Blue, and Midnight instead of adding operation-specific scalar-bound errors. - **Market-specific:** `BorrowExceedsSafeLtvError`, `MissingMarketPriceError`, `NativeAmountOnNonWNativeAssetError`, `MutuallyExclusiveWithdrawAmountsError`, `WithdrawExceedsSupplyError`, `WithdrawSharesExceedSupplyError`. -- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationShapeError` when an entry matches both or neither V1/V2 shape, `InvalidReallocationAddressError` for malformed BluePublicAllocator vault or adapter addresses, `InvalidReallocationSourceTypeError` for an absent, incomplete, or unknown BluePublicAllocator source, `InconsistentReallocationPenaltyError` for conflicting penalties on one vault, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. +- **Reallocation-specific:** `EmptyReallocationWithdrawalsError`, `InvalidReallocationShapeError` when an entry matches both or neither V1/V2 shape, `MixedReallocationVersionsError` when one plan contains both versions, `InvalidReallocationAddressError` for malformed BluePublicAllocator vault or adapter addresses, `InvalidReallocationSourceTypeError` for an absent, incomplete, or unknown BluePublicAllocator source, `InconsistentReallocationPenaltyError` for conflicting penalties on one vault, `ReallocationWithdrawalOnTargetMarketError`, `UnsortedReallocationWithdrawalsError`, `ReallocationWithdrawExceedsMarketSupplyError`. ## Adding a new operation diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index a574df058..024332b10 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -843,6 +843,25 @@ export class InvalidReallocationShapeError extends Error { } } +/** + * Thrown when one reallocation plan contains both Vault V1 and Vault V2 entries. + * + * @example + * ```ts + * import { MixedReallocationVersionsError } from "@morpho-org/morpho-sdk"; + * + * const error = new MixedReallocationVersionsError(); + * ``` + */ +export class MixedReallocationVersionsError extends Error { + public constructor() { + super( + "Reallocation plans cannot mix Vault V1 and Vault V2 entries. Submit one version per transaction.", + ); + this.name = "MixedReallocationVersionsError"; + } +} + /** * Thrown when a Blue Public Allocator reallocation contains a malformed vault * or adapter address. diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index d0e523574..a3547ba6e 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -142,13 +142,10 @@ export interface VaultV2BlueReallocation { readonly penalty: bigint; } -/** - * Reallocation accepted by Blue actions that support PublicAllocator V1 or BluePublicAllocator. - * - * V1 entries are identified by `withdrawals`; V2 entries are identified by - * `from`. - */ -export type BlueReallocation = VaultV1Reallocation | VaultV2BlueReallocation; +/** A homogeneous Blue action plan containing only Vault V1 or only Vault V2 reallocations. */ +export type BlueReallocationPlan = + | Iterable + | Iterable; /** * Deprecated name for a Vault V1 reallocation. diff --git a/packages/wdk-protocol-lending-morpho-evm/src/index.ts b/packages/wdk-protocol-lending-morpho-evm/src/index.ts index 302595d91..76627106f 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/index.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/index.ts @@ -1,6 +1,5 @@ export type { InputMarketParams } from "@morpho-org/blue-sdk"; export type { - BlueReallocation, RequirementSignature, VaultReallocation, VaultV1Reallocation, diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index ce3d5dbfc..ff07216ef 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -6,7 +6,6 @@ import { import { fetchMarket } from "@morpho-org/blue-sdk-viem"; import { type BlueAuthorizationAction, - type BlueReallocation, type ERC20ApprovalAction, type Metadata, type MorphoClientType, @@ -15,6 +14,7 @@ import { type RequirementSignature, type Transaction, type VaultReallocation, + type VaultV2BlueReallocation, } from "@morpho-org/morpho-sdk"; import type { BorrowResult, @@ -176,8 +176,8 @@ export type MorphoBorrowWithVaultV2ReallocationsOptions = Omit< MorphoBorrowOptions, "reallocations" > & { - /** Vault V1 and Vault V2 reallocations to include in the borrow action. */ - readonly reallocations: Iterable; + /** Vault V2 BluePublicAllocator reallocations to include in the borrow action. */ + readonly reallocations: Iterable; }; type MorphoBorrowInput = From ead1aadbb5ddfb77f90ba42c51b7b239c2101a9d Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 19 Aug 2026 11:27:38 +0200 Subject: [PATCH 28/41] refactor: model vault v2 allocator penalties --- .changeset/brave-vaults-reallocate.md | 2 +- ...ePublicAllocatorConfig.integration.test.ts | 16 +++-- .../VaultV2BluePublicAllocatorConfig.test.ts | 14 ++-- .../VaultV2BluePublicAllocatorConfig.ts | 12 ++-- packages/blue-sdk/AGENTS.md | 2 +- .../VaultV2BluePublicAllocatorConfig.test.ts | 15 ++++ .../v2/VaultV2BluePublicAllocatorConfig.ts | 69 ++++++++++++++++++- ...ltV2BluePublicAllocatorConfigUtils.test.ts | 13 ++++ .../VaultV2BluePublicAllocatorConfigUtils.ts | 30 ++++++++ packages/blue-sdk/src/vault/v2/index.ts | 1 + .../actions/blue/buildReallocationActions.ts | 18 +++-- packages/morpho-sdk/src/bundler/actions.ts | 14 +++- packages/morpho-sdk/src/entities/blue/blue.ts | 24 +++++-- .../vaultV2BlueReallocationData.test.ts | 7 ++ .../entities/vaultV2BlueReallocationData.ts | 21 +++--- .../src/helpers/bluePublicAllocator.test.ts | 27 -------- .../src/helpers/bluePublicAllocator.ts | 25 ------- 17 files changed, 217 insertions(+), 93 deletions(-) create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.test.ts create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfigUtils.test.ts create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfigUtils.ts delete mode 100644 packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts delete mode 100644 packages/morpho-sdk/src/helpers/bluePublicAllocator.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 79d8986b7..7b3a5fece 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -9,7 +9,7 @@ Add canonical `vaultV1PublicAllocatorAbi` and `vaultV2BluePublicAllocatorAbi` exports plus per-chain `vaultV1PublicAllocator` and `vaultV2BluePublicAllocator` registry entries to `morpho-ts`, preserving `publicAllocatorAbi` and `publicAllocator` as deprecated V1 aliases. Move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while normalizing them before repeated use, add chain-registry-backed deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. -V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. +V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. `VaultV2BluePublicAllocatorConfig` is hydrated as a class with exact per-call penalty calculation, while plan totals stay local to their consumers. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts index 26e6c978e..1bf2d0e92 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts @@ -1,6 +1,7 @@ import { AccrualVaultV2MorphoMarketV1AdapterV2, getChainAddress, + VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import { createViemTest } from "@morpho-org/test/vitest"; import { parseEther } from "viem"; @@ -98,11 +99,16 @@ describe("Vault V2 BluePublicAllocator fetchers on fork", () => { ]); expect(deployless).toStrictEqual(direct); - expect(deployless.publicAllocatorConfig).toStrictEqual({ - vault: forkVault.address, - canPullFromIdle: true, - penalty: 12n, - }); + expect(deployless.publicAllocatorConfig).toBeInstanceOf( + VaultV2BluePublicAllocatorConfig, + ); + expect(deployless.publicAllocatorConfig).toStrictEqual( + new VaultV2BluePublicAllocatorConfig({ + vault: forkVault.address, + canPullFromIdle: true, + penalty: 12n, + }), + ); expect(deployless.activeAdapters).toStrictEqual( new Set([forkAdapter.address]), ); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts index 3ee42d559..832cf5cd5 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts @@ -5,6 +5,7 @@ import { Market, MarketParams, MathLib, + VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import { createMockClient, mockRead } from "@morpho-org/test/mock"; import type { Address } from "viem"; @@ -82,11 +83,11 @@ const ids = adapter.ids(marketParams); const adapterMarketCapId = ids[2]; const expected = { - publicAllocatorConfig: { + publicAllocatorConfig: new VaultV2BluePublicAllocatorConfig({ vault: VAULT, canPullFromIdle: true, penalty: 12n, - }, + }), activeAdapters: new Set([ADAPTER]), marketPublicAllocatorConfigs: { [adapterMarketCapId]: { @@ -163,9 +164,12 @@ describe("Vault V2 BluePublicAllocator fetchers", () => { const handle = createMockClient(mainnet); mockDirectReads(handle); - await expect( - fetchVaultV2BluePublicAllocatorConfig(VAULT, handle.client), - ).resolves.toStrictEqual(expected.publicAllocatorConfig); + const config = await fetchVaultV2BluePublicAllocatorConfig( + VAULT, + handle.client, + ); + expect(config).toBeInstanceOf(VaultV2BluePublicAllocatorConfig); + expect(config).toStrictEqual(expected.publicAllocatorConfig); await expect( fetchVaultV2BlueMarketPublicAllocatorConfig( VAULT, diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts index 0e3c1e9c5..5404b14dd 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts @@ -4,7 +4,7 @@ import { getChainAddress, type IVaultV2Allocation, type VaultV2BlueMarketPublicAllocatorConfig, - type VaultV2BluePublicAllocatorConfig, + VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import type { Address, Client, Hash } from "viem"; import { getChainId, readContract } from "viem/actions"; @@ -28,7 +28,7 @@ import type { * @param parameters.blockTag - Optional block tag for historical reads. * @param parameters.stateOverride - Optional viem state override. * @param parameters.chainId - Optional chain id; defaults to `getChainId(client)`. - * @returns The vault's idle-pull permission and WAD-scaled vault-asset penalty. + * @returns Hydrated vault allocator configuration with penalty calculations. * @throws {UnknownAddressError} when the chain has no BluePublicAllocator deployment. * @throws {UnsupportedChainIdError} when the chain is absent from the address registry. * @throws {viem.BaseError} when the contract read fails. @@ -63,11 +63,11 @@ export async function fetchVaultV2BluePublicAllocatorConfig( args: [vault], }); - return { + return new VaultV2BluePublicAllocatorConfig({ vault, canPullFromIdle, penalty, - }; + }); } /** @@ -247,11 +247,11 @@ export async function fetchVaultV2BluePublicAllocatorData( } return { - publicAllocatorConfig: { + publicAllocatorConfig: new VaultV2BluePublicAllocatorConfig({ vault: vault.address, canPullFromIdle: result.canPullFromIdle, penalty: result.penalty, - } satisfies VaultV2BluePublicAllocatorConfig, + }), activeAdapters: new Set( adapterList.filter((_, index) => result.isActiveAdapters[index]), ), diff --git a/packages/blue-sdk/AGENTS.md b/packages/blue-sdk/AGENTS.md index 01254d556..ef3f17ff0 100644 --- a/packages/blue-sdk/AGENTS.md +++ b/packages/blue-sdk/AGENTS.md @@ -11,7 +11,7 @@ - Protocol entity folders (`market/`, `vault/`, `token/`, `position/`, `holding/`, `user/`) own their classes and folder barrels. - Getters may throw typed `Unknown*Error`; nullable lookup paths should use `_try` or `tryGet*`-style helpers deliberately. - Vault V2 absolute/relative allocation-cap math is canonical in `VaultV2Utils.allocationHeadroom`; consumers such as `AccrualVaultV2.maxDeposit` and shared-liquidity simulation delegate to it. -- Vault V2 BluePublicAllocator config interfaces are readonly vault-bearing projections; the allocator is canonical per chain and comes from the address registry. Market-scoped state also carries the adapter and derived market-params id. Adapter activation is normalized separately as a vault-keyed set of adapter addresses. +- `VaultV2BluePublicAllocatorConfig` is the hydrated vault-wide config class; its `I*` input remains plain-object compatible and its penalty math delegates to `VaultV2BluePublicAllocatorConfigUtils`. Market-scoped config remains a readonly vault-bearing projection carrying the adapter and derived market-params id. The allocator is canonical per chain and comes from the address registry; adapter activation is normalized separately as a vault-keyed set of adapter addresses. - `marketParamsAbi` is owned by `@morpho-org/morpho-ts/abis` and re-exported from `MarketParams.ts` for backward compatibility; do not define a second copy in this package. ## Continuous Improvement diff --git a/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.test.ts b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.test.ts new file mode 100644 index 000000000..0591ee88d --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.test.ts @@ -0,0 +1,15 @@ +import { zeroAddress } from "viem"; +import { describe, expect, test } from "vitest"; +import { VaultV2BluePublicAllocatorConfig } from "./VaultV2BluePublicAllocatorConfig.js"; + +describe("VaultV2BluePublicAllocatorConfig", () => { + test("default", () => { + const config = new VaultV2BluePublicAllocatorConfig({ + vault: zeroAddress, + canPullFromIdle: true, + penalty: 500_000_000_000_000_000n, + }); + + expect(config.getPenaltyAssets(3n)).toBe(2n); + }); +}); diff --git a/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts index daf2e7299..6c5442bc3 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts @@ -1,7 +1,8 @@ -import type { Address, Hash } from "../../types.js"; +import type { Address, BigIntish, Hash } from "../../types.js"; +import { VaultV2BluePublicAllocatorConfigUtils } from "./VaultV2BluePublicAllocatorConfigUtils.js"; -/** BluePublicAllocator configuration for one Vault V2. */ -export interface VaultV2BluePublicAllocatorConfig { +/** Plain input shape for one Vault V2's BluePublicAllocator configuration. */ +export interface IVaultV2BluePublicAllocatorConfig { /** Configured Vault V2 address. */ readonly vault: Address; /** Whether the allocator may pull the vault's idle assets into a Blue market. */ @@ -10,6 +11,68 @@ export interface VaultV2BluePublicAllocatorConfig { readonly penalty: bigint; } +/** + * Represents one Vault V2's BluePublicAllocator configuration. + * + * @example + * ```ts + * import { VaultV2BluePublicAllocatorConfig } from "@morpho-org/blue-sdk"; + * + * const config = new VaultV2BluePublicAllocatorConfig({ + * vault: "0x0000000000000000000000000000000000000001", + * canPullFromIdle: true, + * penalty: 500_000_000_000_000_000n, + * }); + * ``` + */ +export class VaultV2BluePublicAllocatorConfig + implements IVaultV2BluePublicAllocatorConfig +{ + /** Configured Vault V2 address. */ + public readonly vault: Address; + /** Whether the allocator may pull the vault's idle assets into a Blue market. */ + public readonly canPullFromIdle: boolean; + /** Proportional vault-asset penalty charged for each call, scaled by WAD. */ + public readonly penalty: bigint; + + /** + * Creates a Vault V2 BluePublicAllocator configuration. + * + * @param config - Plain allocator configuration. + */ + public constructor({ + vault, + canPullFromIdle, + penalty, + }: IVaultV2BluePublicAllocatorConfig) { + this.vault = vault; + this.canPullFromIdle = canPullFromIdle; + this.penalty = penalty; + } + + /** + * Computes the independently rounded penalty charged for one reallocation. + * + * @param assets - Assets reallocated by the allocator. + * @returns Penalty assets rounded up exactly as the allocator charges them. + * @example + * ```ts + * import { VaultV2BluePublicAllocatorConfig } from "@morpho-org/blue-sdk"; + * + * const config = new VaultV2BluePublicAllocatorConfig({ + * vault: "0x0000000000000000000000000000000000000001", + * canPullFromIdle: true, + * penalty: 500_000_000_000_000_000n, + * }); + * const penaltyAssets = config.getPenaltyAssets(3n); + * // penaltyAssets === 2n + * ``` + */ + public getPenaltyAssets(assets: BigIntish) { + return VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets(this, assets); + } +} + /** BluePublicAllocator permission and cap for one Vault V2 adapter-market pair. */ export interface VaultV2BlueMarketPublicAllocatorConfig { /** Configured Vault V2 address. */ diff --git a/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfigUtils.test.ts b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfigUtils.test.ts new file mode 100644 index 000000000..c99c9b90a --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfigUtils.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import { VaultV2BluePublicAllocatorConfigUtils } from "./VaultV2BluePublicAllocatorConfigUtils.js"; + +describe("VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets", () => { + test("default", () => { + expect( + VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets( + { penalty: 500_000_000_000_000_000n }, + 3n, + ), + ).toBe(2n); + }); +}); diff --git a/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfigUtils.ts b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfigUtils.ts new file mode 100644 index 000000000..e52b1e39d --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfigUtils.ts @@ -0,0 +1,30 @@ +import { MathLib } from "../../math/index.js"; +import type { BigIntish } from "../../types.js"; +import type { IVaultV2BluePublicAllocatorConfig } from "./VaultV2BluePublicAllocatorConfig.js"; + +/** Deterministic helpers for Vault V2 BluePublicAllocator configuration. */ +export namespace VaultV2BluePublicAllocatorConfigUtils { + /** + * Computes the independently rounded penalty charged for one reallocation. + * + * @param config - Configuration or compatible object carrying the WAD-scaled penalty. + * @param assets - Assets reallocated by the allocator. + * @returns Penalty assets rounded up exactly as the allocator charges them. + * @example + * ```ts + * import { VaultV2BluePublicAllocatorConfigUtils } from "@morpho-org/blue-sdk"; + * + * const penaltyAssets = VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets( + * { penalty: 500_000_000_000_000_000n }, + * 3n, + * ); + * // penaltyAssets === 2n + * ``` + */ + export function getPenaltyAssets( + config: Pick, + assets: BigIntish, + ) { + return MathLib.wMulUp(assets, config.penalty); + } +} diff --git a/packages/blue-sdk/src/vault/v2/index.ts b/packages/blue-sdk/src/vault/v2/index.ts index b2c2cdd7e..ef342a165 100644 --- a/packages/blue-sdk/src/vault/v2/index.ts +++ b/packages/blue-sdk/src/vault/v2/index.ts @@ -1,6 +1,7 @@ export * from "./VaultV2.js"; export * from "./VaultV2Adapter.js"; export * from "./VaultV2BluePublicAllocatorConfig.js"; +export * from "./VaultV2BluePublicAllocatorConfigUtils.js"; export * from "./VaultV2MorphoMarketV1Adapter.js"; export * from "./VaultV2MorphoMarketV1AdapterV2.js"; export * from "./VaultV2MorphoVaultV1Adapter.js"; diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index e0ed29999..91adca294 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -1,6 +1,9 @@ -import { getChainAddresses, type MarketParams } from "@morpho-org/blue-sdk"; +import { + getChainAddresses, + type MarketParams, + VaultV2BluePublicAllocatorConfigUtils, +} from "@morpho-org/blue-sdk"; import type { Action } from "../../bundler/index.js"; -import { computeVaultV2BlueReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; import type { VaultV1Reallocation, VaultV2BlueReallocation, @@ -50,8 +53,15 @@ export const buildVaultV2BlueReallocationActions = ({ readonly penaltyFundingSource?: "initiator" | "generalAdapter1"; }) => { const actions: Action[] = []; - const penaltyAssets = - computeVaultV2BlueReallocationPenaltyAssets(reallocations); + const penaltyAssets = reallocations.reduce( + (total, reallocation) => + total + + VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets( + reallocation, + reallocation.assets, + ), + 0n, + ); if (penaltyAssets > 0n) { const { diff --git a/packages/morpho-sdk/src/bundler/actions.ts b/packages/morpho-sdk/src/bundler/actions.ts index 17890b500..253ade92c 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -1,7 +1,7 @@ import { getChainAddresses, type InputMarketParams, - MathLib, + VaultV2BluePublicAllocatorConfigUtils, } from "@morpho-org/blue-sdk"; import { blueAbi, @@ -1540,7 +1540,11 @@ export namespace BundlerAction { ); } const calls: BundlerCall[] = []; - const penaltyAssets = MathLib.wMulUp(assets, penalty); + const penaltyAssets = + VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets( + { penalty }, + assets, + ); if (skipRevert && penaltyAssets > 0n) { throw new BundlerErrors.SkippableAllocatorPenalty(penaltyAssets); } @@ -1653,7 +1657,11 @@ export namespace BundlerAction { ); } const calls: BundlerCall[] = []; - const penaltyAssets = MathLib.wMulUp(assets, penalty); + const penaltyAssets = + VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets( + { penalty }, + assets, + ); if (skipRevert && penaltyAssets > 0n) { throw new BundlerErrors.SkippableAllocatorPenalty(penaltyAssets); } diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 4d178dd41..56f9b25ac 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -8,6 +8,7 @@ import { type Position, type Vault, type VaultMarketConfig, + VaultV2BluePublicAllocatorConfigUtils, } from "@morpho-org/blue-sdk"; import { fetchAccrualPosition, @@ -33,7 +34,6 @@ import { getBlueAuthorizationRequirement, getGeneralAdapterRequirements, } from "../../actions/index.js"; -import { computeVaultV2BlueReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; import { computeMaxRepaySharePrice, computeMaxSupplySharePrice, @@ -619,9 +619,17 @@ export class MorphoBlue implements BlueActions { private getReallocationPenaltyRequirements( userAddress: Address, - reallocations: Iterable, + reallocations: readonly VaultV2BlueReallocation[], ) { - const amount = computeVaultV2BlueReallocationPenaltyAssets(reallocations); + const amount = reallocations.reduce( + (total, reallocation) => + total + + VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets( + reallocation, + reallocation.assets, + ), + 0n, + ); // Separate-token penalty funding uses a classic GeneralAdapter1 allowance so a collateral // permit and a loan-token penalty can coexist in one bundle. The shared-token path aggregates @@ -1485,8 +1493,14 @@ export class MorphoBlue implements BlueActions { getRequirements: async (params?: { useSimplePermit?: boolean }) => { const penaltyAssets = reallocationPlan.type === "vaultV2Blue" - ? computeVaultV2BlueReallocationPenaltyAssets( - reallocationPlan.reallocations, + ? reallocationPlan.reallocations.reduce( + (total, reallocation) => + total + + VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets( + reallocation, + reallocation.assets, + ), + 0n, ) : 0n; const usesSharedFundingToken = isAddressEqual( diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index ffa57ded8..cd4d91fbb 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -10,6 +10,7 @@ import { Market, MarketParams, MathLib, + VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import type { Address, Hash } from "viem"; import { zeroAddress } from "viem"; @@ -578,6 +579,12 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { input.activeAdapters[VAULT], ); expect(cloned.activeAdapters[VAULT]).not.toBe(input.activeAdapters[VAULT]); + expect(cloned.getPublicAllocatorConfig(VAULT)).toBeInstanceOf( + VaultV2BluePublicAllocatorConfig, + ); + expect(cloned.getPublicAllocatorConfig(VAULT)).not.toBe( + input.getPublicAllocatorConfig(VAULT), + ); const inputLegacy = input .getVault(VAULT) .accrualAdapters.find( diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index 0721357ea..2a6834602 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -7,13 +7,15 @@ import { AccrualVaultV2MorphoVaultV1Adapter, type IAccrualVaultV2Adapter, type IVaultV2Allocation, + type IVaultV2BluePublicAllocatorConfig, Market, type MarketId, MarketUtils, MathLib, UnknownDataError, type VaultV2BlueMarketPublicAllocatorConfig, - type VaultV2BluePublicAllocatorConfig, + VaultV2BluePublicAllocatorConfig, + VaultV2BluePublicAllocatorConfigUtils, VaultV2Utils, } from "@morpho-org/blue-sdk"; import { _try, bigIntComparator } from "@morpho-org/morpho-ts"; @@ -57,7 +59,7 @@ export interface InputVaultV2BlueReallocationData { >; /** Vault-wide BluePublicAllocator configuration indexed by vault address. */ readonly publicAllocatorConfigs?: Readonly< - Record + Record >; /** * BluePublicAllocator-active adapters indexed by vault address. @@ -294,9 +296,11 @@ export class VaultV2BlueReallocationData for (const [vault, config] of Object.entries( input.publicAllocatorConfigs ?? {}, - ) as [Address, VaultV2BluePublicAllocatorConfig | undefined][]) { + ) as [Address, IVaultV2BluePublicAllocatorConfig | undefined][]) { this.publicAllocatorConfigs[vault] = - config == null ? undefined : { ...config }; + config == null + ? undefined + : new VaultV2BluePublicAllocatorConfig(config); } for (const [vault, adapters] of Object.entries( @@ -1031,10 +1035,11 @@ export class VaultV2BlueReallocationData let vault = data.getVault(reallocation.vault); const targetMarket = data.getMarket(targetMarketId); - const penaltyAssets = MathLib.wMulUp( - reallocation.assets, - reallocation.penalty, - ); + const penaltyAssets = + VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets( + reallocation, + reallocation.assets, + ); vault.assetBalance += penaltyAssets; data.donatedPenaltyAssets[reallocation.vault] = (data.donatedPenaltyAssets[reallocation.vault] ?? 0n) + penaltyAssets; diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts deleted file mode 100644 index dd752c149..000000000 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { CbbtcUsdcBlue } from "../../test/fixtures/blue.js"; -import type { VaultV2BlueReallocation } from "../types/index.js"; -import { computeVaultV2BlueReallocationPenaltyAssets } from "./bluePublicAllocator.js"; - -describe("computeVaultV2BlueReallocationPenaltyAssets", () => { - test("default", () => { - const reallocations: VaultV2BlueReallocation[] = [ - { - vault: CbbtcUsdcBlue.oracle, - from: { type: "idle" }, - to: { adapter: CbbtcUsdcBlue.collateralToken }, - assets: 1n, - penalty: 1n, - }, - { - vault: CbbtcUsdcBlue.oracle, - from: { type: "idle" }, - to: { adapter: CbbtcUsdcBlue.collateralToken }, - assets: 1n, - penalty: 1n, - }, - ]; - - expect(computeVaultV2BlueReallocationPenaltyAssets(reallocations)).toBe(2n); - }); -}); diff --git a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts b/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts deleted file mode 100644 index 1c7b1dff4..000000000 --- a/packages/morpho-sdk/src/helpers/bluePublicAllocator.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MathLib } from "@morpho-org/blue-sdk"; -import type { VaultV2BlueReallocation } from "../types/index.js"; - -/** - * Sums the independently rounded vault-asset penalties in a Vault V2 plan. - * - * Each call is rounded independently, matching contract execution. - * - * @param reallocations - Vault V2 BluePublicAllocator plan. - * @returns Total target loan-token assets needed for V2 penalties. - * @example - * ```ts - * const penaltyAssets = computeVaultV2BlueReallocationPenaltyAssets(reallocations); - * ``` - * @internal - */ -export const computeVaultV2BlueReallocationPenaltyAssets = ( - reallocations: Iterable, -) => { - let total = 0n; - for (const reallocation of reallocations) { - total += MathLib.wMulUp(reallocation.assets, reallocation.penalty); - } - return total; -}; From c2e46d3b06041593be98d9bc1c1102de058928bc Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 19 Aug 2026 11:39:57 +0200 Subject: [PATCH 29/41] refactor: hydrate vault v2 market allocator config --- .changeset/brave-vaults-reallocate.md | 2 +- .../VaultV2BlueMarketPublicAllocatorConfig.ts | 82 ++++++++++++++++ ...ePublicAllocatorConfig.integration.test.ts | 20 ++-- .../VaultV2BluePublicAllocatorConfig.test.ts | 23 ++--- .../VaultV2BluePublicAllocatorConfig.ts | 86 ++--------------- .../blue-sdk-viem/src/fetch/vault-v2/index.ts | 1 + packages/blue-sdk/AGENTS.md | 2 +- ...tV2BlueMarketPublicAllocatorConfig.test.ts | 17 ++++ .../VaultV2BlueMarketPublicAllocatorConfig.ts | 93 +++++++++++++++++++ ...ueMarketPublicAllocatorConfigUtils.test.ts | 13 +++ ...tV2BlueMarketPublicAllocatorConfigUtils.ts | 30 ++++++ .../v2/VaultV2BluePublicAllocatorConfig.ts | 16 +--- packages/blue-sdk/src/vault/v2/index.ts | 2 + .../vaultV2BlueReallocationData.test.ts | 9 +- .../entities/vaultV2BlueReallocationData.ts | 23 +++-- 15 files changed, 294 insertions(+), 125 deletions(-) create mode 100644 packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BlueMarketPublicAllocatorConfig.ts create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfig.test.ts create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfig.ts create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfigUtils.test.ts create mode 100644 packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfigUtils.ts diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 7b3a5fece..e6c79fb5a 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -9,7 +9,7 @@ Add canonical `vaultV1PublicAllocatorAbi` and `vaultV2BluePublicAllocatorAbi` exports plus per-chain `vaultV1PublicAllocator` and `vaultV2BluePublicAllocator` registry entries to `morpho-ts`, preserving `publicAllocatorAbi` and `publicAllocator` as deprecated V1 aliases. Move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while normalizing them before repeated use, add chain-registry-backed deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. -V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. `VaultV2BluePublicAllocatorConfig` is hydrated as a class with exact per-call penalty calculation, while plan totals stay local to their consumers. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. +V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. `VaultV2BluePublicAllocatorConfig` is hydrated as a class with exact per-call penalty calculation, `VaultV2BlueMarketPublicAllocatorConfig` computes max-in capacity from its absolute cap, and plan totals stay local to their consumers. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BlueMarketPublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BlueMarketPublicAllocatorConfig.ts new file mode 100644 index 000000000..c18cce788 --- /dev/null +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BlueMarketPublicAllocatorConfig.ts @@ -0,0 +1,82 @@ +import { + getChainAddress, + VaultV2BlueMarketPublicAllocatorConfig, +} from "@morpho-org/blue-sdk"; +import type { Address, Client, Hash } from "viem"; +import { getChainId, readContract } from "viem/actions"; +import { vaultV2BluePublicAllocatorAbi } from "../../abis.js"; +import type { FetchParameters } from "../../types.js"; + +/** + * Fetches BluePublicAllocator permission and cap state for one Vault V2 adapter-market pair. + * + * @param vault - Vault V2 address. + * @param adapter - MorphoMarketV1AdapterV2 address. + * @param adapterMarketCapId - Adapter-scoped market cap id. + * @param client - Viem client used for contract reads. + * @param parameters.account - Optional account passed to viem calls. + * @param parameters.blockNumber - Optional block number for historical reads. + * @param parameters.blockTag - Optional block tag for historical reads. + * @param parameters.stateOverride - Optional viem state override. + * @param parameters.chainId - Optional chain id; defaults to `getChainId(client)`. + * @returns Hydrated adapter-market config with max-in calculation. + * @throws {UnknownAddressError} when the chain has no BluePublicAllocator deployment. + * @throws {UnsupportedChainIdError} when the chain is absent from the address registry. + * @throws {viem.BaseError} when one of the contract reads fails. + * @example + * ```ts + * import type { VaultV2BlueMarketPublicAllocatorConfig } from "@morpho-org/blue-sdk"; + * import { fetchVaultV2BlueMarketPublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; + * import { type Address, createPublicClient, type Hash, http } from "viem"; + * import { mainnet } from "viem/chains"; + * + * const client = createPublicClient({ chain: mainnet, transport: http() }); + * export async function fetchMarketAllocatorConfig( + * vault: Address, + * adapter: Address, + * adapterMarketCapId: Hash, + * ): Promise { + * return fetchVaultV2BlueMarketPublicAllocatorConfig( + * vault, + * adapter, + * adapterMarketCapId, + * client, + * ); + * } + * ``` + */ +// biome-ignore lint/complexity/useMaxParams: follows the package's vault/adapter/id/client/options fetcher convention +export async function fetchVaultV2BlueMarketPublicAllocatorConfig( + vault: Address, + adapter: Address, + adapterMarketCapId: Hash, + client: Client, + parameters: FetchParameters = {}, +): Promise { + const chainId = parameters.chainId ?? (await getChainId(client)); + const allocator = getChainAddress(chainId, "vaultV2BluePublicAllocator"); + const [absoluteCap, canPullFromMarket] = await Promise.all([ + readContract(client, { + ...parameters, + address: allocator, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "absoluteCap", + args: [vault, adapterMarketCapId], + }), + readContract(client, { + ...parameters, + address: allocator, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "canPullFromMarket", + args: [vault, adapterMarketCapId], + }), + ]); + + return new VaultV2BlueMarketPublicAllocatorConfig({ + vault, + adapter, + adapterMarketCapId, + absoluteCap, + canPullFromMarket, + }); +} diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts index 1bf2d0e92..09cafd9f6 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts @@ -1,6 +1,7 @@ import { AccrualVaultV2MorphoMarketV1AdapterV2, getChainAddress, + VaultV2BlueMarketPublicAllocatorConfig, VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import { createViemTest } from "@morpho-org/test/vitest"; @@ -114,13 +115,18 @@ describe("Vault V2 BluePublicAllocator fetchers on fork", () => { ); expect( deployless.marketPublicAllocatorConfigs[forkAdapterMarketCapId], - ).toStrictEqual({ - vault: forkVault.address, - adapter: forkAdapter.address, - adapterMarketCapId: forkAdapterMarketCapId, - absoluteCap: 500n, - canPullFromMarket: true, - }); + ).toBeInstanceOf(VaultV2BlueMarketPublicAllocatorConfig); + expect( + deployless.marketPublicAllocatorConfigs[forkAdapterMarketCapId], + ).toStrictEqual( + new VaultV2BlueMarketPublicAllocatorConfig({ + vault: forkVault.address, + adapter: forkAdapter.address, + adapterMarketCapId: forkAdapterMarketCapId, + absoluteCap: 500n, + canPullFromMarket: true, + }), + ); expect( Object.values(deployless.allocations).some( (allocation) => diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts index 832cf5cd5..765d742dc 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts @@ -5,6 +5,7 @@ import { Market, MarketParams, MathLib, + VaultV2BlueMarketPublicAllocatorConfig, VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import { createMockClient, mockRead } from "@morpho-org/test/mock"; @@ -18,8 +19,8 @@ import { } from "../../__test__/viem.js"; import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; import { abi as queryAbi } from "../../queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.js"; +import { fetchVaultV2BlueMarketPublicAllocatorConfig } from "./VaultV2BlueMarketPublicAllocatorConfig.js"; import { - fetchVaultV2BlueMarketPublicAllocatorConfig, fetchVaultV2BluePublicAllocatorConfig, fetchVaultV2BluePublicAllocatorData, } from "./VaultV2BluePublicAllocatorConfig.js"; @@ -90,13 +91,13 @@ const expected = { }), activeAdapters: new Set([ADAPTER]), marketPublicAllocatorConfigs: { - [adapterMarketCapId]: { + [adapterMarketCapId]: new VaultV2BlueMarketPublicAllocatorConfig({ vault: VAULT, adapter: ADAPTER, adapterMarketCapId, absoluteCap: 500n, canPullFromMarket: true, - }, + }), }, allocations: Object.fromEntries( ids.map((id) => [ @@ -170,14 +171,14 @@ describe("Vault V2 BluePublicAllocator fetchers", () => { ); expect(config).toBeInstanceOf(VaultV2BluePublicAllocatorConfig); expect(config).toStrictEqual(expected.publicAllocatorConfig); - await expect( - fetchVaultV2BlueMarketPublicAllocatorConfig( - VAULT, - ADAPTER, - adapterMarketCapId, - handle.client, - ), - ).resolves.toStrictEqual( + const marketConfig = await fetchVaultV2BlueMarketPublicAllocatorConfig( + VAULT, + ADAPTER, + adapterMarketCapId, + handle.client, + ); + expect(marketConfig).toBeInstanceOf(VaultV2BlueMarketPublicAllocatorConfig); + expect(marketConfig).toStrictEqual( expected.marketPublicAllocatorConfigs[adapterMarketCapId], ); }); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts index 5404b14dd..7ac33c1ab 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts @@ -3,7 +3,7 @@ import { AccrualVaultV2MorphoMarketV1AdapterV2, getChainAddress, type IVaultV2Allocation, - type VaultV2BlueMarketPublicAllocatorConfig, + VaultV2BlueMarketPublicAllocatorConfig, VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import type { Address, Client, Hash } from "viem"; @@ -17,6 +17,7 @@ import type { DeploylessFetchParameters, FetchParameters, } from "../../types.js"; +import { fetchVaultV2BlueMarketPublicAllocatorConfig } from "./VaultV2BlueMarketPublicAllocatorConfig.js"; /** * Fetches a Vault V2's BluePublicAllocator-wide configuration. @@ -70,80 +71,6 @@ export async function fetchVaultV2BluePublicAllocatorConfig( }); } -/** - * Fetches BluePublicAllocator permission and cap state for one Vault V2 adapter-market pair. - * - * @param vault - Vault V2 address. - * @param adapter - MorphoMarketV1AdapterV2 address. - * @param adapterMarketCapId - Adapter-scoped market cap id. - * @param client - Viem client used for contract reads. - * @param parameters.account - Optional account passed to viem calls. - * @param parameters.blockNumber - Optional block number for historical reads. - * @param parameters.blockTag - Optional block tag for historical reads. - * @param parameters.stateOverride - Optional viem state override. - * @param parameters.chainId - Optional chain id; defaults to `getChainId(client)`. - * @returns The allocator cap and pull permission for the adapter-market pair. - * @throws {UnknownAddressError} when the chain has no BluePublicAllocator deployment. - * @throws {UnsupportedChainIdError} when the chain is absent from the address registry. - * @throws {viem.BaseError} when one of the contract reads fails. - * @example - * ```ts - * import type { VaultV2BlueMarketPublicAllocatorConfig } from "@morpho-org/blue-sdk"; - * import { fetchVaultV2BlueMarketPublicAllocatorConfig } from "@morpho-org/blue-sdk-viem"; - * import { type Address, createPublicClient, type Hash, http } from "viem"; - * import { mainnet } from "viem/chains"; - * - * const client = createPublicClient({ chain: mainnet, transport: http() }); - * export async function fetchMarketAllocatorConfig( - * vault: Address, - * adapter: Address, - * adapterMarketCapId: Hash, - * ): Promise { - * return fetchVaultV2BlueMarketPublicAllocatorConfig( - * vault, - * adapter, - * adapterMarketCapId, - * client, - * ); - * } - * ``` - */ -// biome-ignore lint/complexity/useMaxParams: follows the package's vault/adapter/id/client/options fetcher convention -export async function fetchVaultV2BlueMarketPublicAllocatorConfig( - vault: Address, - adapter: Address, - adapterMarketCapId: Hash, - client: Client, - parameters: FetchParameters = {}, -): Promise { - const chainId = parameters.chainId ?? (await getChainId(client)); - const allocator = getChainAddress(chainId, "vaultV2BluePublicAllocator"); - const [absoluteCap, canPullFromMarket] = await Promise.all([ - readContract(client, { - ...parameters, - address: allocator, - abi: vaultV2BluePublicAllocatorAbi, - functionName: "absoluteCap", - args: [vault, adapterMarketCapId], - }), - readContract(client, { - ...parameters, - address: allocator, - abi: vaultV2BluePublicAllocatorAbi, - functionName: "canPullFromMarket", - args: [vault, adapterMarketCapId], - }), - ]); - - return { - vault, - adapter, - adapterMarketCapId, - absoluteCap, - canPullFromMarket, - }; -} - /** * Fetches all BluePublicAllocator and Vault V2 cap data needed to simulate * reallocations for one hydrated Vault V2. @@ -235,10 +162,11 @@ export async function fetchVaultV2BluePublicAllocatorData( VaultV2BlueMarketPublicAllocatorConfig | undefined > = {}; for (const config of result.marketConfigs) { - marketPublicAllocatorConfigs[config.adapterMarketCapId] = { - vault: vault.address, - ...config, - }; + marketPublicAllocatorConfigs[config.adapterMarketCapId] = + new VaultV2BlueMarketPublicAllocatorConfig({ + vault: vault.address, + ...config, + }); } const allocations: Record = {}; diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts index d4078753d..245470ada 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/index.ts @@ -1,5 +1,6 @@ export * from "./VaultV2.js"; export * from "./VaultV2Adapter.js"; +export * from "./VaultV2BlueMarketPublicAllocatorConfig.js"; export * from "./VaultV2BluePublicAllocatorConfig.js"; export * from "./VaultV2MorphoMarketV1Adapter.js"; export * from "./VaultV2MorphoMarketV1AdapterV2.js"; diff --git a/packages/blue-sdk/AGENTS.md b/packages/blue-sdk/AGENTS.md index ef3f17ff0..ce0cc7aae 100644 --- a/packages/blue-sdk/AGENTS.md +++ b/packages/blue-sdk/AGENTS.md @@ -11,7 +11,7 @@ - Protocol entity folders (`market/`, `vault/`, `token/`, `position/`, `holding/`, `user/`) own their classes and folder barrels. - Getters may throw typed `Unknown*Error`; nullable lookup paths should use `_try` or `tryGet*`-style helpers deliberately. - Vault V2 absolute/relative allocation-cap math is canonical in `VaultV2Utils.allocationHeadroom`; consumers such as `AccrualVaultV2.maxDeposit` and shared-liquidity simulation delegate to it. -- `VaultV2BluePublicAllocatorConfig` is the hydrated vault-wide config class; its `I*` input remains plain-object compatible and its penalty math delegates to `VaultV2BluePublicAllocatorConfigUtils`. Market-scoped config remains a readonly vault-bearing projection carrying the adapter and derived market-params id. The allocator is canonical per chain and comes from the address registry; adapter activation is normalized separately as a vault-keyed set of adapter addresses. +- Vault V2 BluePublicAllocator configs are hydrated classes with plain-object-compatible `I*` inputs and math delegated to dedicated `*Utils` namespaces. Vault-wide config computes penalties; market config computes max-in capacity from its absolute cap and effective allocation. The allocator is canonical per chain and comes from the address registry; adapter activation is normalized separately as a vault-keyed set of adapter addresses. - `marketParamsAbi` is owned by `@morpho-org/morpho-ts/abis` and re-exported from `MarketParams.ts` for backward compatibility; do not define a second copy in this package. ## Continuous Improvement diff --git a/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfig.test.ts b/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfig.test.ts new file mode 100644 index 000000000..651f42de4 --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfig.test.ts @@ -0,0 +1,17 @@ +import { zeroAddress } from "viem"; +import { describe, expect, test } from "vitest"; +import { VaultV2BlueMarketPublicAllocatorConfig } from "./VaultV2BlueMarketPublicAllocatorConfig.js"; + +describe("VaultV2BlueMarketPublicAllocatorConfig", () => { + test("default", () => { + const config = new VaultV2BlueMarketPublicAllocatorConfig({ + vault: zeroAddress, + adapter: zeroAddress, + adapterMarketCapId: `0x${"00".repeat(32)}`, + absoluteCap: 100n, + canPullFromMarket: true, + }); + + expect(config.getMaxIn(40n)).toBe(60n); + }); +}); diff --git a/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfig.ts new file mode 100644 index 000000000..fd0e43bb1 --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfig.ts @@ -0,0 +1,93 @@ +import type { Address, BigIntish, Hash } from "../../types.js"; +import { VaultV2BlueMarketPublicAllocatorConfigUtils } from "./VaultV2BlueMarketPublicAllocatorConfigUtils.js"; + +/** Plain input shape for one Vault V2 adapter-market's BluePublicAllocator configuration. */ +export interface IVaultV2BlueMarketPublicAllocatorConfig { + /** Configured Vault V2 address. */ + readonly vault: Address; + /** Vault V2 MorphoMarketV1AdapterV2 address. */ + readonly adapter: Address; + /** Adapter-scoped market-parameters id used by the allocator mappings. */ + readonly adapterMarketCapId: Hash; + /** Maximum post-state allocation accepted by the allocator. */ + readonly absoluteCap: bigint; + /** Whether the allocator may pull assets from this adapter-market pair. */ + readonly canPullFromMarket: boolean; +} + +/** + * Represents BluePublicAllocator state for one Vault V2 adapter-market pair. + * + * @example + * ```ts + * import { VaultV2BlueMarketPublicAllocatorConfig } from "@morpho-org/blue-sdk"; + * + * const config = new VaultV2BlueMarketPublicAllocatorConfig({ + * vault: "0x0000000000000000000000000000000000000001", + * adapter: "0x0000000000000000000000000000000000000002", + * adapterMarketCapId: "0x0000000000000000000000000000000000000000000000000000000000000003", + * absoluteCap: 100n, + * canPullFromMarket: true, + * }); + * ``` + */ +export class VaultV2BlueMarketPublicAllocatorConfig + implements IVaultV2BlueMarketPublicAllocatorConfig +{ + /** Configured Vault V2 address. */ + public readonly vault: Address; + /** Vault V2 MorphoMarketV1AdapterV2 address. */ + public readonly adapter: Address; + /** Adapter-scoped market-parameters id used by the allocator mappings. */ + public readonly adapterMarketCapId: Hash; + /** Maximum post-state allocation accepted by the allocator. */ + public readonly absoluteCap: bigint; + /** Whether the allocator may pull assets from this adapter-market pair. */ + public readonly canPullFromMarket: boolean; + + /** + * Creates an adapter-market BluePublicAllocator configuration. + * + * @param config - Plain adapter-market allocator configuration. + */ + public constructor({ + vault, + adapter, + adapterMarketCapId, + absoluteCap, + canPullFromMarket, + }: IVaultV2BlueMarketPublicAllocatorConfig) { + this.vault = vault; + this.adapter = adapter; + this.adapterMarketCapId = adapterMarketCapId; + this.absoluteCap = absoluteCap; + this.canPullFromMarket = canPullFromMarket; + } + + /** + * Computes the assets that may still be allocated under the allocator cap. + * + * @param allocation - Effective current allocation, including untracked assets. + * @returns Remaining allocator capacity, floored at zero. + * @example + * ```ts + * import { VaultV2BlueMarketPublicAllocatorConfig } from "@morpho-org/blue-sdk"; + * + * const config = new VaultV2BlueMarketPublicAllocatorConfig({ + * vault: "0x0000000000000000000000000000000000000001", + * adapter: "0x0000000000000000000000000000000000000002", + * adapterMarketCapId: "0x0000000000000000000000000000000000000000000000000000000000000003", + * absoluteCap: 100n, + * canPullFromMarket: true, + * }); + * const maxIn = config.getMaxIn(40n); + * // maxIn === 60n + * ``` + */ + public getMaxIn(allocation: BigIntish) { + return VaultV2BlueMarketPublicAllocatorConfigUtils.getMaxIn( + this, + allocation, + ); + } +} diff --git a/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfigUtils.test.ts b/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfigUtils.test.ts new file mode 100644 index 000000000..a08272582 --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfigUtils.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import { VaultV2BlueMarketPublicAllocatorConfigUtils } from "./VaultV2BlueMarketPublicAllocatorConfigUtils.js"; + +describe("VaultV2BlueMarketPublicAllocatorConfigUtils.getMaxIn", () => { + test("behavior: floors exhausted capacity at zero", () => { + expect( + VaultV2BlueMarketPublicAllocatorConfigUtils.getMaxIn( + { absoluteCap: 100n }, + 101n, + ), + ).toBe(0n); + }); +}); diff --git a/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfigUtils.ts b/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfigUtils.ts new file mode 100644 index 000000000..cd211c923 --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2BlueMarketPublicAllocatorConfigUtils.ts @@ -0,0 +1,30 @@ +import { MathLib } from "../../math/index.js"; +import type { BigIntish } from "../../types.js"; +import type { IVaultV2BlueMarketPublicAllocatorConfig } from "./VaultV2BlueMarketPublicAllocatorConfig.js"; + +/** Deterministic helpers for Vault V2 adapter-market BluePublicAllocator configuration. */ +export namespace VaultV2BlueMarketPublicAllocatorConfigUtils { + /** + * Computes the assets that may still be allocated under the allocator cap. + * + * @param config - Configuration or compatible object carrying the absolute cap. + * @param allocation - Effective current allocation, including untracked assets. + * @returns Remaining allocator capacity, floored at zero. + * @example + * ```ts + * import { VaultV2BlueMarketPublicAllocatorConfigUtils } from "@morpho-org/blue-sdk"; + * + * const maxIn = VaultV2BlueMarketPublicAllocatorConfigUtils.getMaxIn( + * { absoluteCap: 100n }, + * 40n, + * ); + * // maxIn === 60n + * ``` + */ + export function getMaxIn( + config: Pick, + allocation: BigIntish, + ) { + return MathLib.zeroFloorSub(config.absoluteCap, allocation); + } +} diff --git a/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts index 6c5442bc3..4cfed9945 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2BluePublicAllocatorConfig.ts @@ -1,4 +1,4 @@ -import type { Address, BigIntish, Hash } from "../../types.js"; +import type { Address, BigIntish } from "../../types.js"; import { VaultV2BluePublicAllocatorConfigUtils } from "./VaultV2BluePublicAllocatorConfigUtils.js"; /** Plain input shape for one Vault V2's BluePublicAllocator configuration. */ @@ -72,17 +72,3 @@ export class VaultV2BluePublicAllocatorConfig return VaultV2BluePublicAllocatorConfigUtils.getPenaltyAssets(this, assets); } } - -/** BluePublicAllocator permission and cap for one Vault V2 adapter-market pair. */ -export interface VaultV2BlueMarketPublicAllocatorConfig { - /** Configured Vault V2 address. */ - readonly vault: Address; - /** Vault V2 MorphoMarketV1AdapterV2 address. */ - readonly adapter: Address; - /** Adapter-scoped market-parameters id used by the allocator mappings. */ - readonly adapterMarketCapId: Hash; - /** Maximum post-state allocation accepted by the allocator. */ - readonly absoluteCap: bigint; - /** Whether the allocator may pull assets from this adapter-market pair. */ - readonly canPullFromMarket: boolean; -} diff --git a/packages/blue-sdk/src/vault/v2/index.ts b/packages/blue-sdk/src/vault/v2/index.ts index ef342a165..e033a726f 100644 --- a/packages/blue-sdk/src/vault/v2/index.ts +++ b/packages/blue-sdk/src/vault/v2/index.ts @@ -1,5 +1,7 @@ export * from "./VaultV2.js"; export * from "./VaultV2Adapter.js"; +export * from "./VaultV2BlueMarketPublicAllocatorConfig.js"; +export * from "./VaultV2BlueMarketPublicAllocatorConfigUtils.js"; export * from "./VaultV2BluePublicAllocatorConfig.js"; export * from "./VaultV2BluePublicAllocatorConfigUtils.js"; export * from "./VaultV2MorphoMarketV1Adapter.js"; diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index cd4d91fbb..b79a58ecc 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -10,6 +10,7 @@ import { Market, MarketParams, MathLib, + VaultV2BlueMarketPublicAllocatorConfig, VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import type { Address, Hash } from "viem"; @@ -471,7 +472,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }); test("behavior: deep-clones legacy and nested accrued adapters", () => { - const { data } = makeFixture(); + const { data, targetIds } = makeFixture(); const targetMarket = data.getMarket(targetParams.id); const legacyPosition = new AccrualPosition( { @@ -585,6 +586,12 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { expect(cloned.getPublicAllocatorConfig(VAULT)).not.toBe( input.getPublicAllocatorConfig(VAULT), ); + expect( + cloned.getMarketPublicAllocatorConfig(VAULT, targetIds[2]), + ).toBeInstanceOf(VaultV2BlueMarketPublicAllocatorConfig); + expect(cloned.getMarketPublicAllocatorConfig(VAULT, targetIds[2])).not.toBe( + input.getMarketPublicAllocatorConfig(VAULT, targetIds[2]), + ); const inputLegacy = input .getVault(VAULT) .accrualAdapters.find( diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index 2a6834602..0513d2eac 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -7,13 +7,14 @@ import { AccrualVaultV2MorphoVaultV1Adapter, type IAccrualVaultV2Adapter, type IVaultV2Allocation, + type IVaultV2BlueMarketPublicAllocatorConfig, type IVaultV2BluePublicAllocatorConfig, Market, type MarketId, MarketUtils, MathLib, UnknownDataError, - type VaultV2BlueMarketPublicAllocatorConfig, + VaultV2BlueMarketPublicAllocatorConfig, VaultV2BluePublicAllocatorConfig, VaultV2BluePublicAllocatorConfigUtils, VaultV2Utils, @@ -73,7 +74,7 @@ export interface InputVaultV2BlueReallocationData { Record< Address, | Readonly< - Record + Record > | undefined > @@ -316,7 +317,7 @@ export class VaultV2BlueReallocationData Address, ( | Readonly< - Record + Record > | undefined ), @@ -324,10 +325,12 @@ export class VaultV2BlueReallocationData this.marketPublicAllocatorConfigs[vault] = {}; for (const [id, config] of Object.entries(configs ?? {}) as [ Hash, - VaultV2BlueMarketPublicAllocatorConfig | undefined, + IVaultV2BlueMarketPublicAllocatorConfig | undefined, ][]) { this.marketPublicAllocatorConfigs[vault]![id] = - config == null ? undefined : { ...config }; + config == null + ? undefined + : new VaultV2BlueMarketPublicAllocatorConfig(config); } } } @@ -756,11 +759,11 @@ export class VaultV2BlueReallocationData const targetMarketParamsAllocation = targetContext.allocations[2]!; - const allocatorHeadroom = MathLib.zeroFloorSub( - targetContext.marketPublicAllocatorConfig.absoluteCap, - targetMarketParamsAllocation.allocation + - targetContext.untracked, - ); + const allocatorHeadroom = + targetContext.marketPublicAllocatorConfig.getMaxIn( + targetMarketParamsAllocation.allocation + + targetContext.untracked, + ); if (publicAllocatorConfig.canPullFromIdle) { const assets = MathLib.min( From 0c378fd764d68c0e33b9b56c3a6cebb340752b00 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 19 Aug 2026 13:47:41 +0200 Subject: [PATCH 30/41] fix: harden Vault V2 allocator compatibility --- packages/blue-sdk-viem/package.json | 4 ++-- .../VaultV2BluePublicAllocatorConfig.test.ts | 11 +++++++++++ packages/blue-sdk/package.json | 2 +- .../vaultV2BlueReallocationData.test.ts | 18 +++++++++++++++++- .../entities/vaultV2BlueReallocationData.ts | 12 +++++++++--- .../morpho-sdk/src/helpers/validate.test.ts | 8 ++++++++ packages/morpho-sdk/src/helpers/validate.ts | 3 +++ 7 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/blue-sdk-viem/package.json b/packages/blue-sdk-viem/package.json index 2cc01675d..4cd69355e 100644 --- a/packages/blue-sdk-viem/package.json +++ b/packages/blue-sdk-viem/package.json @@ -30,8 +30,8 @@ "build:esm": "tsc --build tsconfig.build.esm.json && echo '{\"type\":\"module\"}' > lib/esm/package.json" }, "peerDependencies": { - "@morpho-org/blue-sdk": "^6.5.0", - "@morpho-org/morpho-ts": "^2.9.0", + "@morpho-org/blue-sdk": "^6.6.0", + "@morpho-org/morpho-ts": "^2.10.0", "viem": "^2.0.0" }, "devDependencies": { diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts index 765d742dc..572cc1e82 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts @@ -220,6 +220,17 @@ describe("Vault V2 BluePublicAllocator fetchers", () => { ).resolves.toStrictEqual(expected); }); + test("error: forced deployless failure does not fall back", async () => { + const handle = createMockClient(mainnet); + mockDeploylessReads(handle, [new Error("deployless unavailable")]); + + await expect( + fetchVaultV2BluePublicAllocatorData(vault, handle.client, { + deployless: "force", + }), + ).rejects.toThrow(); + }); + test("behavior: omits inactive adapters from the registry", async () => { const handle = createMockClient(mainnet); mockDeploylessReads(handle, [new Error("deployless unavailable")]); diff --git a/packages/blue-sdk/package.json b/packages/blue-sdk/package.json index 98469d224..89e2b014e 100644 --- a/packages/blue-sdk/package.json +++ b/packages/blue-sdk/package.json @@ -33,7 +33,7 @@ "@noble/hashes": "^2.2.0" }, "peerDependencies": { - "@morpho-org/morpho-ts": "^2.9.0" + "@morpho-org/morpho-ts": "^2.10.0" }, "devDependencies": { "@morpho-org/morpho-ts": "workspace:^", diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index b79a58ecc..6f8d3160d 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -27,7 +27,7 @@ import { VaultV2BlueReallocationData } from "./vaultV2BlueReallocationData.js"; const TIMESTAMP = 1_700_000_000n; const VAULT = "0x0000000000000000000000000000000000000002"; -const TARGET_ADAPTER = "0x0000000000000000000000000000000000000003"; +const TARGET_ADAPTER = "0x00000000000000000000000000000000000000A3"; const SOURCE_ADAPTER = "0x0000000000000000000000000000000000000004"; const LOAN_TOKEN = "0x0000000000000000000000000000000000000005"; const IRM = "0x0000000000000000000000000000000000000006"; @@ -370,6 +370,22 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { } }); + test("behavior: matches active adapters regardless of address casing", () => { + const sourceAdapter = + "0x00000000000000000000000000000000000000AB" as Address; + const { data } = makeFixture({ + sourceAdapter, + allocatorActiveAdapters: [ + `0x${TARGET_ADAPTER.slice(2).toUpperCase()}` as Address, + `0x${sourceAdapter.slice(2).toUpperCase()}` as Address, + ], + }); + + expect( + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, + ).toHaveLength(1); + }); + test("behavior: keeps two vault adapters on one canonical market", () => { const { data } = makeFixture({ sourceSupply: 0n, diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index 0513d2eac..30d4d0436 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -308,7 +308,11 @@ export class VaultV2BlueReallocationData input.activeAdapters ?? {}, ) as [Address, Iterable
| undefined][]) { this.activeAdapters[vault] = - adapters == null ? undefined : new Set(adapters); + adapters == null + ? undefined + : new Set( + [...adapters].map((adapter) => adapter.toLowerCase() as Address), + ); } for (const [vault, configs] of Object.entries( @@ -730,7 +734,7 @@ export class VaultV2BlueReallocationData marketPublicAllocatorConfig.adapter, adapter.address, ) || - !activeAdapters.has(adapter.address) + !activeAdapters.has(adapter.address.toLowerCase() as Address) ) return; @@ -825,7 +829,9 @@ export class VaultV2BlueReallocationData sourceConfig.adapter, sourceAdapter.address, ) || - !activeAdapters.has(sourceAdapter.address) || + !activeAdapters.has( + sourceAdapter.address.toLowerCase() as Address, + ) || !sourceConfig.canPullFromMarket ) return; diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index d280f6e77..4168d54d2 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -806,6 +806,14 @@ describe("reallocation validation", () => { penalty: 0n, } as unknown as VaultV2BlueReallocation, }, + { + name: "null entry", + reallocation: null as unknown as VaultV2BlueReallocation, + }, + { + name: "primitive entry", + reallocation: 1 as unknown as VaultV2BlueReallocation, + }, ])("error: InvalidReallocationShapeError for $name", ({ reallocation }) => { expect(() => validateAndNormalizeReallocations( diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index 612538d33..c8eb519bf 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -499,6 +499,9 @@ export const validateAndNormalizeReallocations = ( const vaultV2Reallocations: VaultV2BlueReallocation[] = []; for (const reallocation of reallocations ?? []) { + if (typeof reallocation !== "object" || reallocation === null) { + throw new InvalidReallocationShapeError(); + } if ("from" in reallocation === "withdrawals" in reallocation) { throw new InvalidReallocationShapeError(); } From a0019173e560e5d6a6e70c1860b7a43f224c7a35 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 19 Aug 2026 14:57:37 +0200 Subject: [PATCH 31/41] fix: address Vault V2 allocator review findings --- .../GetVaultV2BluePublicAllocatorConfig.sol | 2 + ...ePublicAllocatorConfig.integration.test.ts | 21 +- .../VaultV2BluePublicAllocatorConfig.test.ts | 39 +++ .../VaultV2BluePublicAllocatorConfig.ts | 24 +- .../GetVaultV2BluePublicAllocatorConfig.ts | 7 +- ...ue.bluePublicAllocatorRequirements.test.ts | 166 ++++++++++-- .../vaultV2BlueReallocationData.test.ts | 86 ++++++- .../entities/vaultV2BlueReallocationData.ts | 243 +++++++++++++----- 8 files changed, 493 insertions(+), 95 deletions(-) diff --git a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2BluePublicAllocatorConfig.sol b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2BluePublicAllocatorConfig.sol index 6106cbc73..c574e3836 100644 --- a/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2BluePublicAllocatorConfig.sol +++ b/packages/blue-sdk-viem/contracts/vault-v2/GetVaultV2BluePublicAllocatorConfig.sol @@ -24,6 +24,7 @@ struct VaultV2AllocationResponse { } struct VaultV2BluePublicAllocatorResponse { + bool isAllocator; bool canPullFromIdle; uint64 penalty; bool[] isActiveAdapters; @@ -39,6 +40,7 @@ contract GetVaultV2BluePublicAllocatorConfig { VaultV2BlueMarketPublicAllocatorRequest[] calldata marketRequests, bytes32[] calldata allocationIds ) external view returns (VaultV2BluePublicAllocatorResponse memory res) { + res.isAllocator = vault.isAllocator(address(allocator)); (res.canPullFromIdle, res.penalty) = allocator.vaultData(address(vault)); uint256 adaptersLength = adapters.length; diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts index 09cafd9f6..c25fed2c3 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts @@ -5,7 +5,7 @@ import { VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import { createViemTest } from "@morpho-org/test/vitest"; -import { parseEther } from "viem"; +import { encodeFunctionData, parseEther } from "viem"; import { base } from "viem/chains"; import { assert, describe, expect } from "vitest"; import { vaultV2Abi, vaultV2BluePublicAllocatorAbi } from "../../abis.js"; @@ -53,6 +53,25 @@ describe("Vault V2 BluePublicAllocator fetchers on fork", () => { account: allocatorAccount, amount: parseEther("1"), }); + const authorizeAllocator = encodeFunctionData({ + abi: vaultV2Abi, + functionName: "setIsAllocator", + args: [allocator, true], + }); + await client.writeContract({ + account: allocatorAccount, + address: forkVault.address, + abi: vaultV2Abi, + functionName: "submit", + args: [authorizeAllocator], + }); + await client.writeContract({ + account: allocatorAccount, + address: forkVault.address, + abi: vaultV2Abi, + functionName: "setIsAllocator", + args: [allocator, true], + }); const forkAdapterMarketCapId = forkAdapter.ids(forkMarket.params)[2]; await client.writeContract({ account: allocatorAccount, diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts index 572cc1e82..34e67b70e 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts @@ -116,6 +116,12 @@ const mockDirectReads = ( handle: ReturnType, isActiveAdapter = true, ) => { + mockRead(handle, { + address: VAULT, + abi: vaultV2Abi, + functionName: "isAllocator", + result: true, + }); mockRead(handle, { address: ALLOCATOR, abi: vaultV2BluePublicAllocatorAbi, @@ -186,6 +192,7 @@ describe("Vault V2 BluePublicAllocator fetchers", () => { test("behavior: deployless batching returns all derived ids", async () => { const handle = createMockClient(mainnet); mockDeploylessRead(handle, queryAbi, "query", { + isAllocator: true, canPullFromIdle: true, penalty: 12n, isActiveAdapters: [true], @@ -220,6 +227,38 @@ describe("Vault V2 BluePublicAllocator fetchers", () => { ).resolves.toStrictEqual(expected); }); + test("behavior: deployless batching omits config when the allocator is unauthorized", async () => { + const handle = createMockClient(mainnet); + mockDeploylessRead(handle, queryAbi, "query", { + isAllocator: false, + canPullFromIdle: true, + penalty: 12n, + isActiveAdapters: [true], + marketConfigs: [], + allocations: [], + }); + + await expect( + fetchVaultV2BluePublicAllocatorData(vault, handle.client), + ).resolves.toMatchObject({ publicAllocatorConfig: undefined }); + }); + + test("behavior: direct-read fallback omits config when the allocator is unauthorized", async () => { + const handle = createMockClient(mainnet); + mockDeploylessReads(handle, [new Error("deployless unavailable")]); + mockDirectReads(handle); + mockRead(handle, { + address: VAULT, + abi: vaultV2Abi, + functionName: "isAllocator", + result: false, + }); + + await expect( + fetchVaultV2BluePublicAllocatorData(vault, handle.client), + ).resolves.toMatchObject({ publicAllocatorConfig: undefined }); + }); + test("error: forced deployless failure does not fall back", async () => { const handle = createMockClient(mainnet); mockDeploylessReads(handle, [new Error("deployless unavailable")]); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts index 7ac33c1ab..1bcdf4bac 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts @@ -88,7 +88,7 @@ export async function fetchVaultV2BluePublicAllocatorConfig( * @param parameters.stateOverride - Optional viem state override. * @param parameters.chainId - Optional chain id; defaults to `getChainId(client)`. * @param parameters.deployless - Deployless mode; defaults to `true`, with direct-read fallback. - * @returns Vault-wide config, active-adapter set, adapter-market configs keyed by `adapterMarketCapId`, and allocations keyed by derived id. + * @returns Vault-wide config when the BluePublicAllocator is authorized, active-adapter set, adapter-market configs keyed by `adapterMarketCapId`, and allocations keyed by derived id. * @throws {UnknownAddressError} when the chain has no BluePublicAllocator deployment. * @throws {UnsupportedChainIdError} when the chain is absent from the address registry. * @throws {viem.BaseError} when deployless mode is forced and fails, or when a direct contract read fails. @@ -175,11 +175,13 @@ export async function fetchVaultV2BluePublicAllocatorData( } return { - publicAllocatorConfig: new VaultV2BluePublicAllocatorConfig({ - vault: vault.address, - canPullFromIdle: result.canPullFromIdle, - penalty: result.penalty, - }), + publicAllocatorConfig: result.isAllocator + ? new VaultV2BluePublicAllocatorConfig({ + vault: vault.address, + canPullFromIdle: result.canPullFromIdle, + penalty: result.penalty, + }) + : undefined, activeAdapters: new Set( adapterList.filter((_, index) => result.isActiveAdapters[index]), ), @@ -193,11 +195,19 @@ export async function fetchVaultV2BluePublicAllocatorData( } const [ + isAllocator, publicAllocatorConfig, isActiveAdapters, marketConfigs, allocationValues, ] = await Promise.all([ + readContract(client, { + ...parameters, + address: vault.address, + abi: vaultV2Abi, + functionName: "isAllocator", + args: [allocator], + }), fetchVaultV2BluePublicAllocatorConfig(vault.address, client, { ...parameters, chainId, @@ -269,7 +279,7 @@ export async function fetchVaultV2BluePublicAllocatorData( } return { - publicAllocatorConfig, + publicAllocatorConfig: isAllocator ? publicAllocatorConfig : undefined, activeAdapters: new Set( adapterList.filter((_, index) => isActiveAdapters[index]), ), diff --git a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.ts index 290ca1410..dbe591004 100644 --- a/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/queries/vault-v2/GetVaultV2BluePublicAllocatorConfig.ts @@ -44,6 +44,11 @@ export const abi = [ outputs: [ { components: [ + { + internalType: "bool", + name: "isAllocator", + type: "bool", + }, { internalType: "bool", name: "canPullFromIdle", @@ -126,4 +131,4 @@ export const abi = [ /** @internal Deployless `GetVaultV2BluePublicAllocatorConfig` query bytecode. */ export const code = - "0x608080604052346015576108bb908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c6352ae457214610025575f80fd5b3461030e5760a036600319011261030e576004356001600160a01b038116919082900361030e576024356001600160a01b0381169081900361030e576044356001600160401b03811161030e57610080903690600401610785565b606492919235906001600160401b03821161030e573660238301121561030e578160040135946001600160401b03861161030e573660248760061b8501011161030e576084356001600160401b03811161030e576100e2903690600401610785565b60a083949294018381106001600160401b03821117610771576040525f835260208301935f8552604084019160608352606085019860608a526080860194606086528c60408b6024825180948193636b97fbcd60e11b835260048301525afa801561031a575f915f91610718575b506001600160401b0316895215158752610169816107fe565b61017660405191826107d0565b818152601f19610185836107fe565b0136602083013785528c5f5b82811061067257505050506101a58a6107fe565b6101b260405191826107d0565b8a8152601f196101c18c6107fe565b015f5b81811061065b57505089525f5b8a81101561035e578b908060061b890161022d60208c60446101f560248601610839565b60405163011f009b60e31b81526001600160a01b03909316600484015294013560248201819052959092839190829081906044820190565b03915afa801561031a578f8d86935f93610325575b506040516369f1e26b60e01b81526001600160a01b039190911660048201526024810193909352602090839060449082905afa91821561031a575f926102cf575b509183916102c8936001966040519361029b856107b5565b888060a01b0316845260208401526040830152151560608201528d51906102c2838361084d565b5261084d565b50016101d1565b9150916020823d8211610312575b816102ea602093836107d0565b8101031261030e576001946102c89361030386946107f1565b935091935094610283565b5f80fd5b3d91506102dd565b6040513d5f823e3d90fd5b93505050506020813d8211610356575b81610342602093836107d0565b8101031261030e575183908f8d6020610242565b3d9150610335565b50889291889161036d816107fe565b61037a60405191826107d0565b818152601f19610389836107fe565b015f5b81811061064457505086525f5b8181106104dc576001600160401b0389898989896040519586956020875260c0870195511515602088015251166040860152519260a060608601528351809152602060e086019401905f5b8181106104c1575050505191601f19848203016080850152602080845192838152019301905f5b81811061047a575050505190601f198382030160a0840152602080835192838152019201905f5b818110610440575050500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610432565b825180516001600160a01b0316865260208181015181880152604080830151908801526060918201511515918701919091528796506080909501949092019160010161040b565b825115158652879650602095860195909201916001016103e4565b6104e7818385610815565b359060405191632f0374dd60e21b8352806004840152602083602481895afa92831561031a575f93610611575b5060405163a68bafa360e01b8152600481018290526020816024818a5afa90811561031a575f916105e0575b5060405163c69507dd60e01b815260048101839052906020826024818b5afa91821561031a575f926105aa575b509183916105a39360019660405193610585856107b5565b84526020840152604083015260608201528a51906102c2838361084d565b5001610399565b9150916020823d82116105d8575b816105c5602093836107d0565b8101031261030e5790519091600161056d565b3d91506105b8565b90506020813d8211610609575b816105fa602093836107d0565b8101031261030e57518c610540565b3d91506105ed565b9092506020813d821161063c575b8161062c602093836107d0565b8101031261030e5751918b610514565b3d915061061f565b60209061064f610861565b8282860101520161038c565b602090610666610861565b828286010152016101c4565b60208c604461068a61068585888a610815565b610839565b6040516366faa83960e01b815260048101939093526001600160a01b03166024830152909384919082905afa801561031a575f906106df575b600192506106d282895161084d565b9015159052018d90610191565b506020823d8211610710575b816106f8602093836107d0565b8101031261030e5761070b6001926107f1565b6106c3565b3d91506106eb565b9150506040813d604011610769575b81610734604093836107d0565b8101031261030e576020610747826107f1565b910151906001600160401b038216820361030e57906001600160401b03610150565b3d9150610727565b634e487b7160e01b5f52604160045260245ffd5b9181601f8401121561030e578235916001600160401b03831161030e576020808501948460051b01011161030e57565b608081019081106001600160401b0382111761077157604052565b90601f801991011681019081106001600160401b0382111761077157604052565b5190811515820361030e57565b6001600160401b0381116107715760051b60200190565b91908110156108255760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b038116810361030e5790565b80518210156108255760209160051b010190565b6040519061086e826107b5565b5f606083828152826020820152826040820152015256fea26469706673582212206d1f8e3e28ba1c7dcc6b8545e35d5747e7f262aee60dcc1932572527089a903964736f6c63430008240033"; + "0x60808060405234601557610980908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c6352ae457214610024575f80fd5b346103615760a0366003190112610361576004356001600160a01b038116810361036157602435906001600160a01b03821682036103615760443567ffffffffffffffff81116103615761007c903690600401610846565b919067ffffffffffffffff60643511610361573660236064350112156103615767ffffffffffffffff6064356004013511610361573660246064356004013560061b6064350101116103615760843567ffffffffffffffff8111610361576100e8903690600401610846565b93909161014060408190525f608081905260a081905260c0908152606060e0819052610100819052610120526326f6f90760e11b82526001600160a01b03868116600484015290939190602090829060249082908c165afa90811561036d575f916107f8575b50151560805260408051636b97fbcd60e11b81526001600160a01b038981166004830152909190829060249082908a165afa801561036d575f915f9161079d575b5067ffffffffffffffff168452151560a0526101aa816108c2565b6101b76040519182610893565b818152601f196101c6836108c2565b0136602083013760e0525f5b8181106106f1575050506101eb606435600401356108c2565b6101f86040519182610893565b60046064350135808252601f199061020f906108c2565b015f5b8181106106da575050610100525f5b606435600401358110156103a9578060061b6064350190610244602483016108fe565b60405163011f009b60e31b81526001600160a01b038981166004830152604485810135602484015260209183919082908b165afa90811561036d575f91610378575b506040516369f1e26b60e01b81526001600160a01b038a81166004830152604486810135602484015291929160209184919082908c165afa91821561036d575f9261031f575b50918391610318936044600197604051946102e686610877565b898060a01b03168552013560208401526040830152151560608201526080800151906103128383610912565b52610912565b5001610221565b9150916020823d8211610365575b8161033a60209383610893565b810103126103615760019461031893604461035587956108b5565b945050919350946102cc565b5f80fd5b3d915061032d565b6040513d5f823e3d90fd5b90506020813d82116103a1575b8161039260209383610893565b8101031261036157515f610286565b3d9150610385565b5084846103b5816108c2565b6103c26040519182610893565b818152601f196103d1836108c2565b015f5b8181106106c3575050610120525f5b81811061053b578360405180916020825267ffffffffffffffff60e08301916080511515602085015260206080015115156040850152511660608301526060608001519060c060808401528151809152602061010084019201905f5b81811061052057505061010051838303601f190160a0850152805180845260209384019390910191505f5b8181106104dd57505061012051838303601f190160c0850152805180845260209384019390910191505f5b8181106104a3575050500390f35b9193509160206080600192606087518051835284810151858401526040810151604084015201516060820152019401910191849392610495565b919350916020608060019260608751858060a01b03815116835284810151858401526040810151604084015201511515606082015201940191019184939261046a565b8251151584528594506020938401939092019160010161043f565b6105468183876108da565b604051632f0374dd60e21b81529035600482018190529091906020836024816001600160a01b0389165afa92831561036d575f93610690575b5060405163a68bafa360e01b8152600481018290526020816024816001600160a01b038a165afa90811561036d575f9161065f575b5060405163c69507dd60e01b815260048101839052906020826024816001600160a01b038b165afa91821561036d575f92610629575b50918391610622936001966040519361060285610877565b845260208401526040830152606082015261012051906103128383610912565b50016103e3565b9150916020823d8211610657575b8161064460209383610893565b81010312610361579051909160016105ea565b3d9150610637565b90506020813d8211610688575b8161067960209383610893565b810103126103615751886105b4565b3d915061066c565b9092506020813d82116106bb575b816106ab60209383610893565b810103126103615751918761057f565b3d915061069e565b6020906106ce610926565b828286010152016103d4565b6020906106e5610926565b82828601015201610212565b6107046106ff8284866108da565b6108fe565b6040516366faa83960e01b81526001600160a01b038a8116600483015291821660248201529190602090839060449082908b165afa801561036d575f90610764575b6001925061075982606060800151610912565b9015159052016101d2565b506020823d8211610795575b8161077d60209383610893565b81010312610361576107906001926108b5565b610746565b3d9150610770565b9150506040813d6040116107f0575b816107b960409383610893565b810103126103615760206107cc826108b5565b9101519067ffffffffffffffff82168203610361579067ffffffffffffffff61018f565b3d91506107ac565b90506020813d60201161082a575b8161081360209383610893565b8101031261036157610824906108b5565b5f61014e565b3d9150610806565b634e487b7160e01b5f52604160045260245ffd5b9181601f840112156103615782359167ffffffffffffffff8311610361576020808501948460051b01011161036157565b6080810190811067ffffffffffffffff82111761083257604052565b90601f8019910116810190811067ffffffffffffffff82111761083257604052565b5190811515820361036157565b67ffffffffffffffff81116108325760051b60200190565b91908110156108ea5760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b03811681036103615790565b80518210156108ea5760209160051b010190565b6040519061093382610877565b5f606083828152826020820152826040820152015256fea2646970667358221220e2386f7e83e0e06c5b5e8265f5ee8d981ceb81f18cbd7cec8c547c9462c3140164736f6c63430008240033"; diff --git a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts index f878cf1a7..fa1fdc71d 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts @@ -10,10 +10,14 @@ import { createMockClient, mockRead } from "@morpho-org/test/mock"; import { erc20Abi } from "viem"; import { mainnet } from "viem/chains"; import { describe, expect, test, vi } from "vitest"; -import { CbbtcUsdcBlue } from "../../../test/fixtures/blue.js"; +import { + CbbtcUsdcBlue, + CbbtcUsdcBlueAlt, +} from "../../../test/fixtures/blue.js"; import { morphoViemExtension } from "../../client/index.js"; import { isRequirementApproval, + isRequirementBlueAuthorization, isRequirementSignature, } from "../../types/index.js"; @@ -26,6 +30,32 @@ vi.mock("@morpho-org/blue-sdk-viem", async (importOriginal) => { const USER = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; const marketParams = new MarketParams(CbbtcUsdcBlue); +const makePosition = ( + params: MarketParams, + { + supplyShares = 0n, + borrowShares = 0n, + collateral = 0n, + }: { + supplyShares?: bigint; + borrowShares?: bigint; + collateral?: bigint; + }, +) => + new AccrualPosition( + { user: USER, supplyShares, borrowShares, collateral }, + new Market({ + params, + totalSupplyAssets: 10n ** 24n, + totalBorrowAssets: 10n ** 24n / 2n, + totalSupplyShares: 10n ** 24n, + totalBorrowShares: 10n ** 24n / 2n, + lastUpdate: 1_700_000_000n, + fee: 0n, + price: ORACLE_PRICE_SCALE, + }), + ); + describe("MorphoBlue BluePublicAllocator requirements", () => { test("default: includes the classic loan-token approval for V2 penalties", async () => { const handle = createMockClient(mainnet); @@ -52,24 +82,10 @@ describe("MorphoBlue BluePublicAllocator requirements", () => { result: 0n, }); - const positionData = new AccrualPosition( - { - user: USER, - supplyShares: 0n, - borrowShares: 10n ** 18n, - collateral: 10n ** 24n, - }, - new Market({ - params: marketParams, - totalSupplyAssets: 10n ** 24n, - totalBorrowAssets: 10n ** 24n / 2n, - totalSupplyShares: 10n ** 24n, - totalBorrowShares: 10n ** 24n / 2n, - lastUpdate: 1_700_000_000n, - fee: 0n, - price: ORACLE_PRICE_SCALE, - }), - ); + const positionData = makePosition(marketParams, { + borrowShares: 10n ** 18n, + collateral: 10n ** 24n, + }); const market = handle.client .extend(morphoViemExtension({ supportSignature: true })) .morpho.blue(CbbtcUsdcBlue, mainnet.id); @@ -99,6 +115,118 @@ describe("MorphoBlue BluePublicAllocator requirements", () => { }); }); + test("behavior: withdraw includes V2 penalty approval and Morpho authorization", async () => { + const handle = createMockClient(mainnet); + const { + morpho, + bundler3: { generalAdapter1 }, + } = getChainAddresses(mainnet.id); + mockRead(handle, { + address: morpho, + abi: blueAbi, + functionName: "isAuthorized", + result: false, + }); + mockRead(handle, { + address: marketParams.loanToken, + abi: erc20Abi, + functionName: "allowance", + result: 0n, + }); + const market = handle.client + .extend(morphoViemExtension({ supportSignature: false })) + .morpho.blue(marketParams, mainnet.id); + + const requirements = await market + .withdraw({ + assets: 1n, + userAddress: USER, + positionData: makePosition(marketParams, { supplyShares: 10n }), + reallocations: [ + { + vault: marketParams.oracle, + from: { type: "idle" }, + to: { adapter: marketParams.collateralToken }, + assets: 10n, + penalty: 500_000_000_000_000_000n, + }, + ], + }) + .getRequirements(); + + const approval = requirements.find(isRequirementApproval); + const authorization = requirements.find(isRequirementBlueAuthorization); + expect(approval?.to).toBe(marketParams.loanToken); + expect(approval?.action.args).toStrictEqual({ + spender: generalAdapter1, + amount: 5n, + }); + expect(authorization?.action.args).toStrictEqual({ + authorized: generalAdapter1, + isAuthorized: true, + }); + }); + + test("behavior: refinance includes V2 penalty approval and Morpho authorization", async () => { + const handle = createMockClient(mainnet); + const { + morpho, + bundler3: { generalAdapter1 }, + } = getChainAddresses(mainnet.id); + mockRead(handle, { + address: morpho, + abi: blueAbi, + functionName: "isAuthorized", + result: false, + }); + mockRead(handle, { + address: marketParams.loanToken, + abi: erc20Abi, + functionName: "allowance", + result: 0n, + }); + const market = handle.client + .extend(morphoViemExtension({ supportSignature: false })) + .morpho.blue(marketParams, mainnet.id); + + const requirements = await market + .refinance({ + userAddress: USER, + positionData: makePosition(marketParams, { + borrowShares: 10n, + collateral: 1_000n, + }), + target: { + marketParams: CbbtcUsdcBlueAlt, + positionData: makePosition(CbbtcUsdcBlueAlt, {}), + }, + collateralAmount: 100n, + borrowAssets: 1n, + targetReallocations: [ + { + vault: marketParams.oracle, + from: { type: "idle" }, + to: { adapter: marketParams.collateralToken }, + assets: 10n, + penalty: 500_000_000_000_000_000n, + }, + ], + }) + .getRequirements(); + + const approval = requirements.find(isRequirementApproval); + const authorization = requirements.find(isRequirementBlueAuthorization); + expect(approval?.to).toBe(marketParams.loanToken); + expect(approval?.action.args).toStrictEqual({ + spender: generalAdapter1, + amount: 5n, + }); + expect(authorization?.action.args).toStrictEqual({ + authorized: generalAdapter1, + isAuthorized: true, + }); + }); + test("behavior: aggregates collateral and penalty into one shared-token approval", async () => { const sharedTokenParams = new MarketParams({ ...CbbtcUsdcBlue, diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index 6f8d3160d..b78ea34ef 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -18,7 +18,9 @@ import { zeroAddress } from "viem"; import { describe, expect, test } from "vitest"; import { blueBorrow } from "../actions/index.js"; import { + InputExceedsMaxError, InsufficientSharedLiquidityError, + NegativeInputError, NonPositiveInputError, ReallocationWithdrawExceedsMarketSupplyError, UnknownReallocationMarketError, @@ -56,11 +58,13 @@ const sourceParams = new MarketParams({ const makeMarket = ({ params, supply, + supplyShares = supply * 1_000_000n, borrow, lastUpdate = TIMESTAMP, }: { readonly params: MarketParams; readonly supply: bigint; + readonly supplyShares?: bigint; readonly borrow: bigint; readonly lastUpdate?: bigint; }) => @@ -68,7 +72,7 @@ const makeMarket = ({ params, totalSupplyAssets: supply, totalBorrowAssets: borrow, - totalSupplyShares: supply * 1_000_000n, + totalSupplyShares: supplyShares, totalBorrowShares: borrow * 1_000_000n, lastUpdate, fee: 0n, @@ -81,6 +85,7 @@ interface FixtureOptions { readonly sourceBorrow?: bigint; readonly sourceUntracked?: bigint; readonly targetSupply?: bigint; + readonly targetTotalSupplyShares?: bigint; readonly targetBorrow?: bigint; readonly targetPositionAssets?: bigint; readonly targetUntracked?: bigint; @@ -109,6 +114,7 @@ const makeFixture = ({ sourceBorrow = 0n, sourceUntracked = 0n, targetSupply = 100n, + targetTotalSupplyShares, targetBorrow = 0n, targetPositionAssets = 0n, targetUntracked = 0n, @@ -133,6 +139,7 @@ const makeFixture = ({ const targetMarket = makeMarket({ params: targetParams, supply: sameMarket ? sourceSupply : targetSupply, + supplyShares: targetTotalSupplyShares, borrow: sameMarket ? sourceBorrow : targetBorrow, lastUpdate: targetLastUpdate, }); @@ -317,7 +324,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { const { data, sourceExpectedAssets, sourceIds, targetIds } = makeFixture(); expect(data.activeAdapters[VAULT]).toStrictEqual( - new Set([TARGET_ADAPTER, SOURCE_ADAPTER]), + new Set([TARGET_ADAPTER.toLowerCase(), SOURCE_ADAPTER.toLowerCase()]), ); const result = data.computeVaultV2BlueReallocations(targetParams.id); @@ -357,6 +364,53 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ).toBe(100n); }); + test.each([0n, MathLib.WAD])( + "behavior: accepts maxWithdrawalUtilization boundary %s", + (maxWithdrawalUtilization) => { + const { data } = makeFixture(); + + expect(() => + data.computeVaultV2BlueReallocations(targetParams.id, { + maxWithdrawalUtilization, + }), + ).not.toThrow(); + }, + ); + + test.each([ + { + maxWithdrawalUtilization: -1n, + ErrorClass: NegativeInputError, + }, + { + maxWithdrawalUtilization: MathLib.WAD + 1n, + ErrorClass: InputExceedsMaxError, + }, + ])( + "error: rejects maxWithdrawalUtilization $maxWithdrawalUtilization", + ({ maxWithdrawalUtilization, ErrorClass }) => { + const { data } = makeFixture(); + + expect(() => + data.computeVaultV2BlueReallocations(targetParams.id, { + maxWithdrawalUtilization, + }), + ).toThrow(ErrorClass); + }, + ); + + test("behavior: skips targets whose Morpho supply would mint fewer shares than assets", () => { + const { data } = makeFixture({ + targetSupply: 2_000_000n, + targetTotalSupplyShares: 0n, + idle: 300n, + }); + + expect( + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, + ).toStrictEqual([]); + }); + test("behavior: ignores inactive source and target adapters", () => { for (const allocatorActiveAdapters of [ [TARGET_ADAPTER], @@ -405,7 +459,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { adaptiveCurveIrm: IRM, supplyShares: { [targetMarket.id]: secondTargetShares }, }, - [targetMarket], + [new Market({ ...targetMarket })], ); const secondTargetIds = secondTargetAdapter.ids(targetParams); const secondAllocations: Record = {}; @@ -424,6 +478,9 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { address: SECOND_VAULT, _totalAssets: 550n, totalSupply: 550n, + liquidityAllocations: firstVault.liquidityAllocations?.map( + (allocation) => ({ ...allocation }), + ), }, undefined, [secondTargetAdapter], @@ -575,7 +632,12 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ); const fixtureVault = data.getVault(VAULT); const inputVault = new AccrualVaultV2( - fixtureVault, + { + ...fixtureVault, + liquidityAllocations: fixtureVault.liquidityAllocations?.map( + (allocation) => ({ ...allocation }), + ), + }, fixtureVault.accrualLiquidityAdapter, [...fixtureVault.accrualAdapters, legacyAdapter, nestedAdapter], fixtureVault.assetBalance, @@ -937,8 +999,9 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }); test("error: UnknownReallocationMarketError with an explicit timestamp", () => { - const { data } = makeFixture(); - data.markets[targetParams.id] = undefined; + const data = new VaultV2BlueReallocationData({ + chainId: ChainId.EthMainnet, + }); expect(() => data.computeVaultV2BlueReallocations(targetParams.id, { @@ -987,6 +1050,17 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations operation" expect(result.data.getMarket(targetParams.id).totalSupplyAssets).toBe(123n); }); + test("error: validates maxWithdrawalUtilization before an operation early return", () => { + const { data } = makeFixture({ targetSupply: 100n, targetBorrow: 0n }); + + expect(() => + data.computeVaultV2BlueReallocations(targetParams.id, { + maxWithdrawalUtilization: MathLib.WAD + 1n, + operation: { type: "borrow", amount: 1n }, + }), + ).toThrow(InputExceedsMaxError); + }); + test("behavior: applies the configured ceiling during the friendly phase", () => { const { data } = makeFixture({ targetSupply: 100n, diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index 30d4d0436..db964b4db 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -30,7 +30,9 @@ import type { VaultV2BlueReallocation, } from "../types/index.js"; import { + InputExceedsMaxError, InsufficientSharedLiquidityError, + NegativeInputError, NonPositiveInputError, ReallocationAdapterSupplySharesUnderflowError, ReallocationAllocationUnderflowError, @@ -43,14 +45,52 @@ import { UnknownReallocationVaultError, } from "../types/index.js"; +type ReadonlyMarketSnapshot = Readonly; + +type ReadonlyAdapterSnapshot = Readonly; + +type ReadonlyMarketAdapterSnapshot = Readonly< + Omit< + AccrualVaultV2MorphoMarketV1AdapterV2, + "marketIds" | "supplyShares" | "markets" + > +> & { + readonly marketIds: readonly MarketId[]; + readonly supplyShares: Readonly>; + readonly markets: readonly ReadonlyMarketSnapshot[]; +}; + +type ReadonlyVaultSnapshot = Readonly< + Omit< + AccrualVaultV2, + | "adapters" + | "liquidityAllocations" + | "accrualLiquidityAdapter" + | "accrualAdapters" + | "forceDeallocatePenalties" + > +> & { + readonly adapters: readonly Address[]; + readonly liquidityAllocations: + | readonly Readonly[] + | undefined; + readonly accrualLiquidityAdapter: ReadonlyAdapterSnapshot | undefined; + readonly accrualAdapters: readonly ReadonlyAdapterSnapshot[]; + readonly forceDeallocatePenalties: Readonly>; +}; + /** Input state required to simulate Vault V2 BluePublicAllocator reallocations. */ export interface InputVaultV2BlueReallocationData { /** Chain id associated with the fetched state. */ readonly chainId: number; /** Markets indexed by market id. */ - readonly markets?: Readonly>; + readonly markets?: Readonly< + Record + >; /** Accrued Vault V2 entities indexed by vault address. */ - readonly vaults?: Readonly>; + readonly vaults?: Readonly< + Record + >; /** Vault cap state indexed by vault address and derived allocation id. */ readonly allocations?: Readonly< Record< @@ -81,7 +121,21 @@ export interface InputVaultV2BlueReallocationData { >; } -const cloneMarket = (market: Market) => new Market({ ...market }); +const cloneMarket = (market: ReadonlyMarketSnapshot) => + new Market({ ...market }); + +const resolveMaxWithdrawalUtilization = (value: bigint | undefined) => { + const utilization = value ?? DEFAULT_WITHDRAWAL_TARGET_UTILIZATION; + if (utilization < 0n) + throw new NegativeInputError("maxWithdrawalUtilization", utilization); + if (utilization > MathLib.WAD) + throw new InputExceedsMaxError({ + field: "maxWithdrawalUtilization", + value: utilization, + max: MathLib.WAD, + }); + return utilization; +}; const getCanonicalMarket = ( markets: Record, @@ -162,7 +216,7 @@ const cloneAdapter = ( }; const cloneVault = ( - vault: AccrualVaultV2, + vault: ReadonlyVaultSnapshot, markets: Record, ) => { const adapters = vault.accrualAdapters.map((adapter) => @@ -210,6 +264,15 @@ const cloneVault = ( export class VaultV2BlueReallocationData implements InputVaultV2BlueReallocationData { + /** Mutable market state used only by cloned simulation transitions. */ + private readonly mutableMarkets: Record; + /** Mutable vault state used only by cloned simulation transitions. */ + private readonly mutableVaults: Record; + /** Mutable allocation state used only by cloned simulation transitions. */ + private readonly mutableAllocations: Record< + Address, + Record | undefined + >; /** Penalty donations created by this simulation, excluded as fresh shared-liquidity sources. */ private readonly donatedPenaltyAssets: Record; /** Transaction-frozen cap denominator for each vault touched by this plan. */ @@ -217,28 +280,38 @@ export class VaultV2BlueReallocationData /** Chain id associated with this snapshot. */ public readonly chainId: number; /** Markets indexed by market id. */ - public readonly markets: Record; + public readonly markets: Readonly< + Record + >; /** Vault V2 entities indexed by address. */ - public readonly vaults: Record; + public readonly vaults: Readonly< + Record + >; /** Vault cap state indexed by vault and derived allocation id. */ - public readonly allocations: Record< - Address, - Record | undefined + public readonly allocations: Readonly< + Record< + Address, + | Readonly | undefined>> + | undefined + > >; /** Vault-wide allocator configuration indexed by vault. */ - public readonly publicAllocatorConfigs: Record< - Address, - VaultV2BluePublicAllocatorConfig | undefined + public readonly publicAllocatorConfigs: Readonly< + Record >; /** BluePublicAllocator-active adapters indexed by vault address. */ - public readonly activeAdapters: Record< - Address, - ReadonlySet
| undefined + public readonly activeAdapters: Readonly< + Record | undefined> >; /** Adapter-market allocator configuration indexed by vault and market-params id. */ - public readonly marketPublicAllocatorConfigs: Record< - Address, - Record | undefined + public readonly marketPublicAllocatorConfigs: Readonly< + Record< + Address, + | Readonly< + Record + > + | undefined + > >; /** @@ -248,12 +321,26 @@ export class VaultV2BlueReallocationData */ public constructor(input: InputVaultV2BlueReallocationData) { this.chainId = input.chainId; - this.markets = {}; - this.vaults = {}; - this.allocations = {}; - this.publicAllocatorConfigs = {}; - this.activeAdapters = {}; - this.marketPublicAllocatorConfigs = {}; + this.mutableMarkets = {}; + this.mutableVaults = {}; + this.mutableAllocations = {}; + this.markets = this.mutableMarkets; + this.vaults = this.mutableVaults; + this.allocations = this.mutableAllocations; + const publicAllocatorConfigs: Record< + Address, + VaultV2BluePublicAllocatorConfig | undefined + > = {}; + this.publicAllocatorConfigs = publicAllocatorConfigs; + const activeAdapters: Record | undefined> = + {}; + this.activeAdapters = activeAdapters; + const marketPublicAllocatorConfigs: Record< + Address, + | Record + | undefined + > = {}; + this.marketPublicAllocatorConfigs = marketPublicAllocatorConfigs; this.donatedPenaltyAssets = input instanceof VaultV2BlueReallocationData ? { ...input.donatedPenaltyAssets } @@ -265,18 +352,19 @@ export class VaultV2BlueReallocationData for (const [marketId, market] of Object.entries(input.markets ?? {}) as [ MarketId, - Market | undefined, + ReadonlyMarketSnapshot | undefined, ][]) { - this.markets[marketId] = market == null ? undefined : cloneMarket(market); + this.mutableMarkets[marketId] = + market == null ? undefined : cloneMarket(market); } for (const [address, vault] of Object.entries(input.vaults ?? {}) as [ Address, - AccrualVaultV2 | undefined, + ReadonlyVaultSnapshot | undefined, ][]) { const clonedVault = - vault == null ? undefined : cloneVault(vault, this.markets); - this.vaults[address] = clonedVault; + vault == null ? undefined : cloneVault(vault, this.mutableMarkets); + this.mutableVaults[address] = clonedVault; } for (const [vault, allocations] of Object.entries( @@ -285,12 +373,12 @@ export class VaultV2BlueReallocationData Address, Readonly> | undefined, ][]) { - this.allocations[vault] = {}; + this.mutableAllocations[vault] = {}; for (const [id, allocation] of Object.entries(allocations ?? {}) as [ Hash, IVaultV2Allocation | undefined, ][]) { - this.allocations[vault]![id] = + this.mutableAllocations[vault]![id] = allocation == null ? undefined : { ...allocation }; } } @@ -298,7 +386,7 @@ export class VaultV2BlueReallocationData for (const [vault, config] of Object.entries( input.publicAllocatorConfigs ?? {}, ) as [Address, IVaultV2BluePublicAllocatorConfig | undefined][]) { - this.publicAllocatorConfigs[vault] = + publicAllocatorConfigs[vault] = config == null ? undefined : new VaultV2BluePublicAllocatorConfig(config); @@ -307,7 +395,7 @@ export class VaultV2BlueReallocationData for (const [vault, adapters] of Object.entries( input.activeAdapters ?? {}, ) as [Address, Iterable
| undefined][]) { - this.activeAdapters[vault] = + activeAdapters[vault] = adapters == null ? undefined : new Set( @@ -326,12 +414,12 @@ export class VaultV2BlueReallocationData | undefined ), ][]) { - this.marketPublicAllocatorConfigs[vault] = {}; + marketPublicAllocatorConfigs[vault] = {}; for (const [id, config] of Object.entries(configs ?? {}) as [ Hash, IVaultV2BlueMarketPublicAllocatorConfig | undefined, ][]) { - this.marketPublicAllocatorConfigs[vault]![id] = + marketPublicAllocatorConfigs[vault]![id] = config == null ? undefined : new VaultV2BlueMarketPublicAllocatorConfig(config); @@ -363,8 +451,8 @@ export class VaultV2BlueReallocationData * const market = data.getMarket(marketId); * ``` */ - public getMarket(marketId: MarketId) { - const market = this.markets[marketId]; + public getMarket(marketId: MarketId): ReadonlyMarketSnapshot { + const market = this.mutableMarkets[marketId]; if (market == null) throw new UnknownReallocationMarketError(marketId); return market; } @@ -380,8 +468,12 @@ export class VaultV2BlueReallocationData * const vault = data.getVault(vaultAddress); * ``` */ - public getVault(vault: Address) { - const data = this.vaults[vault]; + public getVault(vault: Address): ReadonlyVaultSnapshot { + return this.getMutableVault(vault); + } + + private getMutableVault(vault: Address) { + const data = this.mutableVaults[vault]; if (data == null) throw new UnknownReallocationVaultError(vault); return data; } @@ -461,8 +553,15 @@ export class VaultV2BlueReallocationData * const adapter = data.getAdapter(vaultAddress, adapterAddress); * ``` */ - public getAdapter(vault: Address, adapter: Address) { - const data = this.getVault(vault).accrualAdapters.find( + public getAdapter( + vault: Address, + adapter: Address, + ): ReadonlyMarketAdapterSnapshot { + return this.getMutableAdapter(vault, adapter); + } + + private getMutableAdapter(vault: Address, adapter: Address) { + const data = this.getMutableVault(vault).accrualAdapters.find( (candidate): candidate is AccrualVaultV2MorphoMarketV1AdapterV2 => candidate instanceof AccrualVaultV2MorphoMarketV1AdapterV2 && isAddressEqual(candidate.address, adapter), @@ -485,6 +584,8 @@ export class VaultV2BlueReallocationData * @param marketId - Target Blue market id. * @param options - Optional discovery controls and operation to support. * @returns Flat action-ready reallocations and their post-simulation state. + * @throws {NegativeInputError} when `maxWithdrawalUtilization` is negative. + * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` exceeds WAD. * @throws {NonPositiveInputError} when the operation amount is not positive and planning is enabled. * @throws {UnknownReallocationMarketError} when the target market is absent. * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. @@ -517,8 +618,9 @@ export class VaultV2BlueReallocationData } { if (options.enabled === false) return { reallocations: [], data: this }; - const maxWithdrawalUtilization = - options.maxWithdrawalUtilization ?? DEFAULT_WITHDRAWAL_TARGET_UTILIZATION; + const maxWithdrawalUtilization = resolveMaxWithdrawalUtilization( + options.maxWithdrawalUtilization, + ); const operation = options.operation; if (operation == null) return this.computeVaultV2BlueReallocationsAtUtilization({ @@ -879,6 +981,13 @@ export class VaultV2BlueReallocationData const capCompatibleCandidates: VaultV2BlueReallocation[] = []; for (const reallocation of rawCandidates) { + // MorphoMarketV1AdapterV2 rejects supplies that mint fewer shares than assets. + if ( + targetMarket.toSupplyShares(reallocation.assets, "Down") < + reallocation.assets + ) + continue; + // Cap fit is monotonic but not linear in assets: the amount changes // penalty donations, firstTotalAssets, rounded shares, and possibly // shared allocation IDs. Binary search finds the exact largest fit. @@ -960,6 +1069,8 @@ export class VaultV2BlueReallocationData * @param marketId - Target Blue market id. * @param options - Optional timestamp, enable flag, vault allowlist, source utilization ceiling, and maximum penalty. * @returns Reallocatable market and idle assets, or `0n` when none are available. + * @throws {NegativeInputError} when `maxWithdrawalUtilization` is negative. + * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` exceeds WAD. * @throws {UnknownReallocationMarketError} when the target market is absent. * @example * ```ts @@ -970,11 +1081,13 @@ export class VaultV2BlueReallocationData marketId: MarketId, options?: VaultV2BluePublicAllocatorOptions, ) { + if (options?.enabled === false) return 0n; + return this.computeVaultV2BlueReallocationsAtUtilization({ marketId, - maxWithdrawalUtilization: - options?.maxWithdrawalUtilization ?? - DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, + maxWithdrawalUtilization: resolveMaxWithdrawalUtilization( + options?.maxWithdrawalUtilization, + ), options, }).reallocations.reduce((total, { assets }) => total + assets, 0n); } @@ -987,6 +1100,8 @@ export class VaultV2BlueReallocationData * @param utilization - Desired utilization, scaled by WAD. Defaults to 90%. * @param options - Optional timestamp, enable flag, vault allowlist, source utilization ceiling, and maximum penalty. * @returns Borrowable assets while remaining at or below `utilization`. + * @throws {NegativeInputError} when `maxWithdrawalUtilization` is negative. + * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` exceeds WAD. * @throws {UnknownReallocationMarketError} when the target market is absent. * @example * ```ts @@ -999,6 +1114,10 @@ export class VaultV2BlueReallocationData utilization: bigint = DEFAULT_SUPPLY_TARGET_UTILIZATION, options?: VaultV2BluePublicAllocatorOptions, ) { + const maxWithdrawalUtilization = + options?.enabled === false + ? DEFAULT_WITHDRAWAL_TARGET_UTILIZATION + : resolveMaxWithdrawalUtilization(options?.maxWithdrawalUtilization); const timestamp = options?.timestamp == null ? this.getLatestSnapshotTimestamp() @@ -1007,10 +1126,12 @@ export class VaultV2BlueReallocationData if (DEFAULT_SUPPLY_TARGET_UTILIZATION > utilization) return market.getBorrowToUtilization(utilization); - const availableLiquidity = this.getPublicReallocationLiquidity(marketId, { - ...options, - timestamp, - }); + const availableLiquidity = + this.computeVaultV2BlueReallocationsAtUtilization({ + marketId, + maxWithdrawalUtilization, + options: { ...options, timestamp }, + }).reallocations.reduce((total, { assets }) => total + assets, 0n); return MarketUtils.getBorrowToUtilization( { totalSupplyAssets: market.totalSupplyAssets + availableLiquidity, @@ -1041,7 +1162,7 @@ export class VaultV2BlueReallocationData readonly timestamp: bigint; }) { const data = this.clone(); - let vault = data.getVault(reallocation.vault); + let vault = data.getMutableVault(reallocation.vault); const targetMarket = data.getMarket(targetMarketId); const penaltyAssets = @@ -1054,7 +1175,7 @@ export class VaultV2BlueReallocationData (data.donatedPenaltyAssets[reallocation.vault] ?? 0n) + penaltyAssets; if (reallocation.from.type === "market") { - const sourceAdapter = data.getAdapter( + const sourceAdapter = data.getMutableAdapter( reallocation.vault, reallocation.from.adapter, ); @@ -1094,7 +1215,7 @@ export class VaultV2BlueReallocationData change: sourceChange, }); } - data.allocations[reallocation.vault]![id] = { + data.mutableAllocations[reallocation.vault]![id] = { ...allocation, allocation: nextAllocation, }; @@ -1118,11 +1239,11 @@ export class VaultV2BlueReallocationData } else { vault = vault.accrueInterest(timestamp).vault; } - data.vaults[reallocation.vault] = vault; + data.mutableVaults[reallocation.vault] = vault; data.firstTotalAssets[reallocation.vault] = vault._totalAssets; } - const targetAdapter = data.getAdapter( + const targetAdapter = data.getMutableAdapter( reallocation.vault, reallocation.to.adapter, ); @@ -1156,7 +1277,7 @@ export class VaultV2BlueReallocationData change: targetChange, }); } - data.allocations[reallocation.vault]![id] = { + data.mutableAllocations[reallocation.vault]![id] = { ...allocation, allocation: nextAllocation, }; @@ -1167,12 +1288,12 @@ export class VaultV2BlueReallocationData } private setMarket(market: Market) { - this.markets[market.id] = market; + this.mutableMarkets[market.id] = market; // A Morpho market is global state shared by every vault position. Legacy // AccrualPosition constructors copy their Market, so rebuild those adapter // views as well as repointing V2 adapters whenever the canonical state moves. - for (const vault of Object.values(this.vaults)) { + for (const vault of Object.values(this.mutableVaults)) { if (vault == null) continue; const adapters = new Set(vault.accrualAdapters); if (vault.accrualLiquidityAdapter != null) @@ -1181,16 +1302,16 @@ export class VaultV2BlueReallocationData for (const adapter of adapters) { if (adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2) { adapter.markets = adapter.markets.map((adapterMarket) => - getCanonicalMarket(this.markets, adapterMarket), + getCanonicalMarket(this.mutableMarkets, adapterMarket), ); } else if (adapter instanceof AccrualVaultV2MorphoMarketV1Adapter) { adapter.positions = adapter.positions.map((position) => - clonePosition(position, this.markets), + clonePosition(position, this.mutableMarkets), ); } else if (adapter instanceof AccrualVaultV2MorphoVaultV1Adapter) { adapter.accrualVaultV1 = cloneAccrualVault( adapter.accrualVaultV1, - this.markets, + this.mutableMarkets, ); } } From 781059ef9d22d14e6a4bce580974809084e8ca79 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 19 Aug 2026 17:08:19 +0200 Subject: [PATCH 32/41] fix: handle Vault V2 allocator edge cases --- .changeset/brave-vaults-reallocate.md | 2 +- .../VaultV2BluePublicAllocatorConfig.test.ts | 39 +++++++++++++++++-- .../VaultV2BluePublicAllocatorConfig.ts | 23 +++++++++-- .../vaultV2Reallocations.integration.test.ts | 21 +++++++++- packages/morpho-sdk/src/entities/blue/blue.ts | 5 ++- .../vaultV2BlueReallocationData.test.ts | 33 ++++++++++++++-- .../entities/vaultV2BlueReallocationData.ts | 11 ++---- .../src/morpho-protocol-evm.test.ts | 10 +++++ .../src/morpho-protocol-evm.ts | 4 +- 9 files changed, 125 insertions(+), 23 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index e6c79fb5a..85bebede0 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -13,7 +13,7 @@ V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. -Compatibility note: `VaultV2MorphoMarketV1AdapterV2.ids()` now declares its existing three-element result as `readonly [Hash, Hash, Hash]`. The runtime values and ordering are unchanged, and derived allocation identifiers are immutable descriptors. We intentionally accept this TypeScript assignability tightening in the minor release; callers that explicitly require a mutable `Hash[]` can copy the tuple with `[...adapter.ids(params)]`. +Compatibility note: this minor intentionally accepts three TypeScript-level breaking changes. `VaultV2MorphoMarketV1AdapterV2.ids()` now returns `readonly [Hash, Hash, Hash]` instead of mutable `Hash[]`; `MorphoBlue.withdraw`, `borrow`, and `refinance` may now return `Transaction` from `getRequirements()` for Vault V2 penalty funding; and `BlueWithdrawAction`, `BlueBorrowAction`, `BlueSupplyCollateralBorrowAction`, and `BlueRefinanceAction` now require `reallocationPenaltyAssets`. Runtime ordering for `ids()` is unchanged. Consumers should spread `ids()` when a mutable array is required, handle approval transactions in exhaustive requirement consumers, and set `reallocationPenaltyAssets: 0n` in handwritten V1 or no-penalty action descriptors. Name allocation-cap helpers `adapterCapId`, `collateralCapId`, and `adapterMarketCapId`. Preserve the published `adapterId`, `collateralId`, and `marketParamsId` helpers as deprecated aliases. diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts index 34e67b70e..661f91e6a 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts @@ -80,6 +80,29 @@ const vault = new AccrualVaultV2( 0n, {}, ); +const unallocatedTargetAdapter = new AccrualVaultV2MorphoMarketV1AdapterV2( + { + address: ADAPTER, + parentVault: VAULT, + skimRecipient: zeroAddress, + marketIds: [], + adaptiveCurveIrm: IRM, + supplyShares: {}, + }, + [], +); +const unallocatedTargetVault = new AccrualVaultV2( + { + ...vault, + liquidityAllocations: vault.liquidityAllocations?.map((allocation) => ({ + ...allocation, + })), + }, + undefined, + [unallocatedTargetAdapter], + vault.assetBalance, + { ...vault.forceDeallocatePenalties }, +); const ids = adapter.ids(marketParams); const adapterMarketCapId = ids[2]; @@ -189,7 +212,7 @@ describe("Vault V2 BluePublicAllocator fetchers", () => { ); }); - test("behavior: deployless batching returns all derived ids", async () => { + test("behavior: deployless batching includes an unallocated target", async () => { const handle = createMockClient(mainnet); mockDeploylessRead(handle, queryAbi, "query", { isAllocator: true, @@ -213,17 +236,25 @@ describe("Vault V2 BluePublicAllocator fetchers", () => { }); await expect( - fetchVaultV2BluePublicAllocatorData(vault, handle.client), + fetchVaultV2BluePublicAllocatorData( + unallocatedTargetVault, + handle.client, + { targetMarketParams: marketParams }, + ), ).resolves.toStrictEqual(expected); }); - test("behavior: direct-read fallback matches deployless output", async () => { + test("behavior: direct-read fallback includes an unallocated target", async () => { const handle = createMockClient(mainnet); mockDeploylessReads(handle, [new Error("deployless unavailable")]); mockDirectReads(handle); await expect( - fetchVaultV2BluePublicAllocatorData(vault, handle.client), + fetchVaultV2BluePublicAllocatorData( + unallocatedTargetVault, + handle.client, + { targetMarketParams: marketParams }, + ), ).resolves.toStrictEqual(expected); }); diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts index 1bcdf4bac..74314534d 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts @@ -3,6 +3,7 @@ import { AccrualVaultV2MorphoMarketV1AdapterV2, getChainAddress, type IVaultV2Allocation, + type MarketParams, VaultV2BlueMarketPublicAllocatorConfig, VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; @@ -88,6 +89,7 @@ export async function fetchVaultV2BluePublicAllocatorConfig( * @param parameters.stateOverride - Optional viem state override. * @param parameters.chainId - Optional chain id; defaults to `getChainId(client)`. * @param parameters.deployless - Deployless mode; defaults to `true`, with direct-read fallback. + * @param parameters.targetMarketParams - Optional target market whose config and cap ids are fetched even when the adapter has no current position. * @returns Vault-wide config when the BluePublicAllocator is authorized, active-adapter set, adapter-market configs keyed by `adapterMarketCapId`, and allocations keyed by derived id. * @throws {UnknownAddressError} when the chain has no BluePublicAllocator deployment. * @throws {UnsupportedChainIdError} when the chain is absent from the address registry. @@ -113,7 +115,13 @@ export async function fetchVaultV2BluePublicAllocatorConfig( export async function fetchVaultV2BluePublicAllocatorData( vault: AccrualVaultV2, client: Client, - { deployless = true, ...parameters }: DeploylessFetchParameters = {}, + { + deployless = true, + targetMarketParams, + ...parameters + }: DeploylessFetchParameters & { + readonly targetMarketParams?: MarketParams; + } = {}, ) { const chainId = parameters.chainId ?? (await getChainId(client)); const allocator = getChainAddress(chainId, "vaultV2BluePublicAllocator"); @@ -128,8 +136,17 @@ export async function fetchVaultV2BluePublicAllocatorData( if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) continue; adapters.add(adapter.address); - for (const market of adapter.markets) { - const ids = adapter.ids(market.params); + const marketParamsList = adapter.markets.map((market) => market.params); + if ( + targetMarketParams != null && + !marketParamsList.some( + (marketParams) => marketParams.id === targetMarketParams.id, + ) + ) + marketParamsList.push(targetMarketParams); + + for (const marketParams of marketParamsList) { + const ids = adapter.ids(marketParams); marketRequests.push({ adapter: adapter.address, adapterMarketCapId: ids[2], diff --git a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts index f562243f2..4058f2dba 100644 --- a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts +++ b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts @@ -239,6 +239,25 @@ describe("Blue actions with Vault V2 reallocations", () => { amount: totalPenaltyAssets, }); + const morphoClient = client.extend(morphoViemExtension()).morpho; + const market = morphoClient.blue(targetMarket, base.id); + const block = await client.getBlock(); + const reallocationData = await market.getVaultV2BlueReallocationData({ + vaultAddresses: [vault], + block, + }); + expect( + reallocationData.getAdapter(vault, targetAdapter).marketIds, + ).not.toContain(targetMarket.id); + const discovery = reallocationData.computeVaultV2BlueReallocations( + targetMarket.id, + { timestamp: block.timestamp }, + ); + expect(discovery.reallocations.length).toBeGreaterThan(0); + expect(discovery.data.getAdapter(vault, targetAdapter).marketIds).toContain( + targetMarket.id, + ); + const reallocations: readonly VaultV2BlueReallocation[] = [ { vault, @@ -260,8 +279,6 @@ describe("Blue actions with Vault V2 reallocations", () => { }, ]; - const morphoClient = client.extend(morphoViemExtension()).morpho; - const market = morphoClient.blue(targetMarket, base.id); const positionData = await market.getPositionData(client.account.address); const borrow = market.borrow({ userAddress: client.account.address, diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 56f9b25ac..3b9aa7fd9 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -2031,7 +2031,10 @@ export class MorphoBlue implements BlueActions { const publicAllocatorData = await fetchVaultV2BluePublicAllocatorData( vault, client, - fetchParams, + { + ...fetchParams, + targetMarketParams: this.marketParams, + }, ); return { publicAllocatorData, vault }; }), diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index b78ea34ef..8971ad504 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -88,6 +88,7 @@ interface FixtureOptions { readonly targetTotalSupplyShares?: bigint; readonly targetBorrow?: bigint; readonly targetPositionAssets?: bigint; + readonly targetTracked?: boolean; readonly targetUntracked?: bigint; readonly targetCaps?: readonly [ { readonly absoluteCap: bigint; readonly relativeCap: bigint }, @@ -117,6 +118,7 @@ const makeFixture = ({ targetTotalSupplyShares, targetBorrow = 0n, targetPositionAssets = 0n, + targetTracked = true, targetUntracked = 0n, targetCaps = [ { absoluteCap: 10_000n, relativeCap: MathLib.WAD }, @@ -164,11 +166,13 @@ const makeFixture = ({ address: TARGET_ADAPTER, parentVault: VAULT, skimRecipient: zeroAddress, - marketIds: [targetMarket.id], + marketIds: targetTracked ? [targetMarket.id] : [], adaptiveCurveIrm: IRM, - supplyShares: { [targetMarket.id]: targetSupplyShares }, + supplyShares: targetTracked + ? { [targetMarket.id]: targetSupplyShares } + : {}, }, - [targetMarket], + targetTracked ? [targetMarket] : [], ); const sourceAdapter = new AccrualVaultV2MorphoMarketV1AdapterV2( { @@ -351,6 +355,29 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ); }); + test("behavior: allocates into a configured target with no existing position", () => { + const { data, sourceExpectedAssets, targetIds } = makeFixture({ + targetTracked: false, + }); + + expect(data.getAdapter(VAULT, TARGET_ADAPTER).marketIds).not.toContain( + targetParams.id, + ); + + const result = data.computeVaultV2BlueReallocations(targetParams.id); + const targetAdapter = result.data.getAdapter(VAULT, TARGET_ADAPTER); + + expect(result.reallocations).toHaveLength(1); + expect(targetAdapter.marketIds).toContain(targetParams.id); + expect(targetAdapter.markets.map(({ id }) => id)).toContain( + targetParams.id, + ); + expect(targetAdapter.supplyShares[targetParams.id]).toBeGreaterThan(0n); + expect(result.data.getAllocation(VAULT, targetIds[2]).allocation).toBe( + sourceExpectedAssets, + ); + }); + test("behavior: honors the configured source-utilization ceiling", () => { const { data } = makeFixture({ sourceBorrow: 900n }); diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index db964b4db..839e2a09f 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -815,13 +815,6 @@ export class VaultV2BlueReallocationData ) ) continue; - if ( - !adapter.markets.some( - (market) => - market.id.toLowerCase() === marketId.toLowerCase(), - ) - ) - continue; const targetContext = _try(() => { const ids = adapter.ids(targetMarket.params); @@ -1262,6 +1255,10 @@ export class VaultV2BlueReallocationData const targetSupplyShares = (targetAdapter.supplyShares[targetMarket.id] ?? 0n) + supply.shares; targetAdapter.supplyShares[targetMarket.id] = targetSupplyShares; + if (!targetAdapter.marketIds.includes(targetMarket.id)) + targetAdapter.marketIds.push(targetMarket.id); + if (!targetAdapter.markets.some(({ id }) => id === targetMarket.id)) + targetAdapter.markets.push(supply.market); data.setMarket(supply.market); const targetChange = diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index 42b34e7f5..34c377c46 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -1,5 +1,6 @@ import type { RequirementSignature, + VaultReallocation, VaultV2BlueReallocation, } from "@morpho-org/morpho-sdk"; import * as viem from "viem"; @@ -458,6 +459,15 @@ describe.sequential("MorphoProtocolEvm", () => { }); describe("borrow", () => { + test("types: borrow reallocations require replayable arrays", () => { + expectTypeOf< + NonNullable + >().toEqualTypeOf(); + expectTypeOf< + MorphoBorrowWithVaultV2ReallocationsOptions["reallocations"] + >().toEqualTypeOf(); + }); + test("should build a market borrow with morpho-sdk and send it", async () => { account.sendTransaction = vi .fn() diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index ff07216ef..dfae6eb90 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -158,7 +158,7 @@ export interface MorphoBorrowOptions { /** The address on behalf of which the borrow operation should be performed. Must match the wallet account address when set. */ onBehalfOf?: string; /** Optional Vault V1 PublicAllocator reallocations to include in the borrow action. */ - reallocations?: Iterable; + reallocations?: readonly VaultReallocation[]; /** Signature returned by a Morpho SDK authorization requirement, folded into the bundle as `setAuthorizationWithSig`. */ requirementSignature?: RequirementSignature; /** Optional Morpho SDK slippage tolerance in WAD precision. */ @@ -177,7 +177,7 @@ export type MorphoBorrowWithVaultV2ReallocationsOptions = Omit< "reallocations" > & { /** Vault V2 BluePublicAllocator reallocations to include in the borrow action. */ - readonly reallocations: Iterable; + readonly reallocations: readonly VaultV2BlueReallocation[]; }; type MorphoBorrowInput = From 913f6ee4fbe68ae52d1d1c1f7b47b8280c63b2ab Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Thu, 20 Aug 2026 16:35:39 +0200 Subject: [PATCH 33/41] fix: address Vault V2 allocator review findings --- .../pr-review-engine/agents/documentation.md | 1 + AGENTS.md | 2 +- .../vault/v2/VaultV2MorphoMarketV1Adapter.ts | 16 ++++- .../v2/VaultV2MorphoMarketV1AdapterV2.ts | 16 ++++- .../vault/v2/VaultV2MorphoVaultV1Adapter.ts | 8 ++- packages/morpho-sdk/BUNDLER3.md | 4 +- .../morpho-sdk/src/actions/blue/AGENTS.md | 10 +-- .../morpho-sdk/src/actions/blue/borrow.ts | 25 ++----- .../actions/blue/buildReallocationActions.ts | 71 ++++++++++++++++++- .../morpho-sdk/src/actions/blue/refinance.ts | 25 ++----- .../actions/blue/supplyCollateralBorrow.ts | 30 +++----- .../morpho-sdk/src/actions/blue/withdraw.ts | 25 ++----- .../morpho-sdk/src/entities/blue/AGENTS.md | 2 +- packages/morpho-ts/src/addresses.test.ts | 3 + packages/morpho-ts/src/addresses.ts | 6 ++ 15 files changed, 151 insertions(+), 93 deletions(-) diff --git a/.agents/pr-review-engine/agents/documentation.md b/.agents/pr-review-engine/agents/documentation.md index 475eaed3c..980962ea0 100644 --- a/.agents/pr-review-engine/agents/documentation.md +++ b/.agents/pr-review-engine/agents/documentation.md @@ -50,6 +50,7 @@ For each Markdown file affected, flag: - **Stale prose.** A statement that no longer matches the code after the diff — e.g. README documents a function that was removed/renamed; AGENTS.md lists a rule the code change just violated; an example that no longer compiles. - **Out-of-sync inventories.** A file enumerating personas, packages, slash commands, scripts, supported chains, etc. that no longer matches reality after the diff. E.g. a README that lists "supported chains: mainnet, base" while the diff just added arbitrum. - **Cross-doc consistency.** When the diff changes a rule in `AGENTS.md`, every persona that enforces it (per the backlink `> Applied by personas: …`) should reflect the new rule. When the diff renames a section heading in `AGENTS.md`, every doc that references that section by title needs an update. +- **Implemented TIB rewrites.** Treat a TIB already present on the target branch as a historical record. Flag edits that update its implementation-time names or examples; changed decisions require a superseding TIB, while operational clarifications require a dated addendum. A TIB introduced on the current branch may stay in sync with its implementation before landing. - **Code blocks that drift from the code.** A bash snippet in a `.md` that uses a flag the script no longer supports; a TypeScript snippet whose imports no longer resolve. ## 3. Pointer / link integrity diff --git a/AGENTS.md b/AGENTS.md index 587bef625..eb8c52158 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -155,7 +155,7 @@ A scannable list of patterns reviewers reject. Most are review-only today (per t - `@returns` describing the return shape. - `@throws` for each typed error class an integrator may pattern-match on. - One `@example` block with realistic working code: imports, client setup, the call, expected return. -- **AI-legibility is first-class.** Identical signatures across V1/V2 where protocols overlap. Discriminated unions with obvious `type` tags. Deterministic outputs verifiable byte-for-byte. Error messages read like instructions an agent can act on without guessing. Protocol-specific terms (`LLTV buffer`, `wNative`, `GeneralAdapter1`, `bundler3`, `PublicAllocator`, `MetaMorpho`, `Permit2`, `WAD`) live in the [`packages/morpho-sdk/AGENTS.md`](./packages/morpho-sdk/AGENTS.md) glossary. +- **AI-legibility is first-class.** Identical signatures across V1/V2 where protocols overlap. Discriminated unions with obvious `type` tags. Deterministic outputs verifiable byte-for-byte. Error messages read like instructions an agent can act on without guessing. Protocol-specific terms (`LLTV buffer`, `wNative`, `GeneralAdapter1`, `bundler3`, `PublicAllocator V1`, `BluePublicAllocator`, `MetaMorpho`, `Permit2`, `WAD`) live in the [`packages/morpho-sdk/AGENTS.md`](./packages/morpho-sdk/AGENTS.md) glossary. - **Implemented TIBs are historical records.** Do not rewrite a TIB already present on the target branch to follow later code, symbol, or path changes. Keep the TIB's implementation-time names and examples intact. A changed decision gets a new superseding TIB; an operational clarification gets a dated addendum. Only a TIB introduced for the current implementation may be kept in sync with that implementation before it lands. - **TypeDoc-generated reference** published per release. - **Feedback loop:** if the same question is asked twice, the answer goes into the relevant `AGENTS.md` or JSDoc on the export it concerns. diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts index 78101044d..44950d3a2 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts @@ -46,7 +46,13 @@ export class VaultV2MorphoMarketV1Adapter ); } - /** @deprecated Use {@link VaultV2MorphoMarketV1Adapter.adapterCapId}. */ + /** + * Returns the adapter-wide allocation-cap id. + * + * @param address - Adapter address. + * @returns The adapter-wide allocation-cap id. + * @deprecated Use {@link VaultV2MorphoMarketV1Adapter.adapterCapId}. + */ static adapterId(address: Address) { return VaultV2MorphoMarketV1Adapter.adapterCapId(address); } @@ -70,7 +76,13 @@ export class VaultV2MorphoMarketV1Adapter ); } - /** @deprecated Use {@link VaultV2MorphoMarketV1Adapter.collateralCapId}. */ + /** + * Returns the collateral-wide allocation-cap id. + * + * @param address - Collateral token address. + * @returns The collateral-wide allocation-cap id. + * @deprecated Use {@link VaultV2MorphoMarketV1Adapter.collateralCapId}. + */ static collateralId(address: Address) { return VaultV2MorphoMarketV1Adapter.collateralCapId(address); } diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts index b31fab325..0103bebd6 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts @@ -53,7 +53,13 @@ export class VaultV2MorphoMarketV1AdapterV2 ); } - /** @deprecated Use {@link VaultV2MorphoMarketV1AdapterV2.adapterCapId}. */ + /** + * Returns the adapter-wide allocation-cap id. + * + * @param address - Adapter address. + * @returns The adapter-wide allocation-cap id. + * @deprecated Use {@link VaultV2MorphoMarketV1AdapterV2.adapterCapId}. + */ static adapterId(address: Address) { return VaultV2MorphoMarketV1AdapterV2.adapterCapId(address); } @@ -77,7 +83,13 @@ export class VaultV2MorphoMarketV1AdapterV2 ); } - /** @deprecated Use {@link VaultV2MorphoMarketV1AdapterV2.collateralCapId}. */ + /** + * Returns the collateral-wide allocation-cap id. + * + * @param address - Collateral token address. + * @returns The collateral-wide allocation-cap id. + * @deprecated Use {@link VaultV2MorphoMarketV1AdapterV2.collateralCapId}. + */ static collateralId(address: Address) { return VaultV2MorphoMarketV1AdapterV2.collateralCapId(address); } diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts index 8d0212185..663ac2edb 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts @@ -42,7 +42,13 @@ export class VaultV2MorphoVaultV1Adapter ); } - /** @deprecated Use {@link VaultV2MorphoVaultV1Adapter.adapterCapId}. */ + /** + * Returns the adapter-wide allocation-cap id. + * + * @param address - Adapter address. + * @returns The adapter-wide allocation-cap id. + * @deprecated Use {@link VaultV2MorphoVaultV1Adapter.adapterCapId}. + */ static adapterId(address: Address) { return VaultV2MorphoVaultV1Adapter.adapterCapId(address); } diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index 1beb9fcef..f2fa3080d 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -79,7 +79,9 @@ bundle** for borrow and loan-asset withdraw, **inserted between supply-collatera penalties are different: the bundle pulls the aggregate amount in the target loan token through GeneralAdapter1, approves each exact per-call amount from Bundler3, and lets the allocator donate it directly to the vault. The entity's `getRequirements()` returns the corresponding classic -loan-token approval when a V2 penalty is non-zero. +loan-token approval when a V2 penalty is non-zero, except when `supplyCollateralBorrow` uses the +same collateral and loan token: that path folds the penalty into its single collateral approval or +permit and emits no separate penalty requirement. ### 5. A single user approval surface diff --git a/packages/morpho-sdk/src/actions/blue/AGENTS.md b/packages/morpho-sdk/src/actions/blue/AGENTS.md index c290fa5f6..dadf0062b 100644 --- a/packages/morpho-sdk/src/actions/blue/AGENTS.md +++ b/packages/morpho-sdk/src/actions/blue/AGENTS.md @@ -40,10 +40,12 @@ ERC-20 approval spender is **GeneralAdapter1** for any bundled path — never th An allocator reallocation plan contains only PublicAllocator V1 `reallocateTo` calls or only BluePublicAllocator `reallocate`/`allocateFromIdle` calls. Separate builders encode each version; -mixing versions throws `MixedReallocationVersionsError`. For non-zero V2 penalties, the V2 builder adds one aggregate loan-token -`erc20TransferFrom` into Bundler3 and each allocator action expands to an exact token approval plus -the nonpayable allocator call. `BundlerAction.encodeBundle` derives `tx.value` only from native -wrapping calls and PublicAllocator V1 native fees. +mixing versions throws `MixedReallocationVersionsError`. For non-zero V2 penalties, the V2 builder +adds one aggregate loan-token funding action into Bundler3: `erc20TransferFrom` from the initiator by +default, or `erc20Transfer` from GeneralAdapter1 when `supplyCollateralBorrow` uses the same token for +collateral and loan funding. Each allocator action expands to an exact token approval plus the +nonpayable allocator call. `BundlerAction.encodeBundle` derives `tx.value` only from native wrapping +calls and PublicAllocator V1 native fees. ## Mode and ordering rules diff --git a/packages/morpho-sdk/src/actions/blue/borrow.ts b/packages/morpho-sdk/src/actions/blue/borrow.ts index 0de5ac601..46eb45d1f 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.ts @@ -3,7 +3,6 @@ import { deepFreeze } from "@morpho-org/morpho-ts"; import type { Address } from "viem"; import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; -import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import { type AuthorizationRequirementSignature, type BlueBorrowAction, @@ -14,10 +13,7 @@ import { type Transaction, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; -import { - buildVaultV1ReallocationActions, - buildVaultV2BlueReallocationActions, -} from "./buildReallocationActions.js"; +import { buildBlueReallocationActions } from "./buildReallocationActions.js"; /** Parameters for {@link blueBorrow}. */ export interface BlueBorrowParams { @@ -122,24 +118,15 @@ export const blueBorrow = ({ actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } - const reallocationPlan = validateAndNormalizeReallocations( - reallocations, - marketParams.id, - ); const { actions: reallocationActions, fee: reallocationFee, penaltyAssets: reallocationPenaltyAssets, - } = reallocationPlan.type === "vaultV1" - ? buildVaultV1ReallocationActions({ - reallocations: reallocationPlan.reallocations, - targetMarketParams: marketParams, - }) - : buildVaultV2BlueReallocationActions({ - chainId, - reallocations: reallocationPlan.reallocations, - targetMarketParams: marketParams, - }); + } = buildBlueReallocationActions({ + chainId, + reallocations, + targetMarketParams: marketParams, + }); actions.push(...reallocationActions); actions.push({ diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 91adca294..91d0fc0c1 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -4,12 +4,23 @@ import { VaultV2BluePublicAllocatorConfigUtils, } from "@morpho-org/blue-sdk"; import type { Action } from "../../bundler/index.js"; +import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import type { + BlueReallocationPlan, VaultV1Reallocation, VaultV2BlueReallocation, } from "../../types/index.js"; -/** @internal */ +/** + * Builds PublicAllocator V1 reallocation actions for a Morpho Blue target market. + * + * Preserves the supplied reallocation order and aggregates each allocator call's native fee. + * + * @param params.reallocations - Validated Vault V1 reallocations to execute. + * @param params.targetMarketParams - Morpho Blue market receiving the reallocated liquidity. + * @returns The reallocation actions and aggregate native fee, with zero V2 penalty assets. + * @internal + */ export const buildVaultV1ReallocationActions = ({ reallocations, targetMarketParams, @@ -40,7 +51,20 @@ export const buildVaultV1ReallocationActions = ({ return { actions, fee, penaltyAssets: 0n }; }; -/** @internal */ +/** + * Builds BluePublicAllocator reallocation actions for a Morpho Blue target market. + * + * Prepends one aggregate loan-token funding action when penalties are non-zero, then builds one + * allocator action per validated Vault V2 reallocation. + * + * @param params.chainId - Chain whose registered Bundler3 and allocator addresses are used. + * @param params.reallocations - Validated Vault V2 Blue reallocations to execute. + * @param params.targetMarketParams - Morpho Blue market receiving the reallocated liquidity. + * @param params.penaltyFundingSource - Optional source of penalty assets. Defaults to the + * transaction initiator. + * @returns The funding and reallocation actions and aggregate penalty assets, with zero native fee. + * @internal + */ export const buildVaultV2BlueReallocationActions = ({ chainId, reallocations, @@ -123,3 +147,46 @@ export const buildVaultV2BlueReallocationActions = ({ return { actions, fee: 0n, penaltyAssets }; }; + +/** + * Validates a homogeneous Blue reallocation plan and builds its Bundler actions. + * + * Dispatches Vault V1 and Vault V2 plans to their version-specific builders. V1 plans aggregate + * native fees, while V2 plans aggregate loan-token penalties using the selected funding source. + * + * @param params.chainId - Chain whose registered Bundler3 and allocator addresses are used. + * @param params.reallocations - Optional homogeneous Vault V1 or Vault V2 reallocation plan. + * @param params.targetMarketParams - Morpho Blue market receiving the reallocated liquidity. + * @param params.penaltyFundingSource - Optional source of V2 penalty assets. Defaults to the + * transaction initiator. + * @returns The reallocation actions, aggregate V1 native fee, and aggregate V2 penalty assets. + * @internal + */ +export const buildBlueReallocationActions = ({ + chainId, + reallocations, + targetMarketParams, + penaltyFundingSource, +}: { + readonly chainId: number; + readonly reallocations: BlueReallocationPlan | undefined; + readonly targetMarketParams: MarketParams; + readonly penaltyFundingSource?: "initiator" | "generalAdapter1"; +}) => { + const reallocationPlan = validateAndNormalizeReallocations( + reallocations, + targetMarketParams.id, + ); + + return reallocationPlan.type === "vaultV1" + ? buildVaultV1ReallocationActions({ + reallocations: reallocationPlan.reallocations, + targetMarketParams, + }) + : buildVaultV2BlueReallocationActions({ + chainId, + reallocations: reallocationPlan.reallocations, + targetMarketParams, + penaltyFundingSource, + }); +}; diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index 158e00200..6229e6b20 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -3,7 +3,6 @@ import { deepFreeze } from "@morpho-org/morpho-ts"; import { type Address, isAddressEqual, maxUint256 } from "viem"; import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; -import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import { type AuthorizationRequirementSignature, type BlueReallocationPlan, @@ -17,10 +16,7 @@ import { type Transaction, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; -import { - buildVaultV1ReallocationActions, - buildVaultV2BlueReallocationActions, -} from "./buildReallocationActions.js"; +import { buildBlueReallocationActions } from "./buildReallocationActions.js"; /** Parameters for {@link blueRefinance}. */ export interface BlueRefinanceParams { @@ -284,24 +280,15 @@ export const blueRefinance = ({ actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } - const reallocationPlan = validateAndNormalizeReallocations( - targetReallocations, - targetParams.id, - ); const { actions: reallocationActions, fee: reallocationFee, penaltyAssets: reallocationPenaltyAssets, - } = reallocationPlan.type === "vaultV1" - ? buildVaultV1ReallocationActions({ - reallocations: reallocationPlan.reallocations, - targetMarketParams: targetParams, - }) - : buildVaultV2BlueReallocationActions({ - chainId, - reallocations: reallocationPlan.reallocations, - targetMarketParams: targetParams, - }); + } = buildBlueReallocationActions({ + chainId, + reallocations: targetReallocations, + targetMarketParams: targetParams, + }); actions.push(...reallocationActions); actions.push({ diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index f00899608..cf1d4f44e 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -3,7 +3,6 @@ import { deepFreeze } from "@morpho-org/morpho-ts"; import { type Address, isAddressEqual } from "viem"; import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; -import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import { type AuthorizationRequirementSignature, type BlueReallocationPlan, @@ -17,10 +16,7 @@ import { } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; import { buildAssetFundingActions } from "./buildAssetFundingActions.js"; -import { - buildVaultV1ReallocationActions, - buildVaultV2BlueReallocationActions, -} from "./buildReallocationActions.js"; +import { buildBlueReallocationActions } from "./buildReallocationActions.js"; /** Parameters for {@link blueSupplyCollateralBorrow}. */ export interface BlueSupplyCollateralBorrowParams { @@ -171,24 +167,14 @@ export const blueSupplyCollateralBorrow = ({ marketParams.collateralToken, marketParams.loanToken, ); - const reallocationPlan = validateAndNormalizeReallocations( + const reallocationResult = buildBlueReallocationActions({ + chainId, reallocations, - marketParams.id, - ); - const reallocationResult = - reallocationPlan.type === "vaultV1" - ? buildVaultV1ReallocationActions({ - reallocations: reallocationPlan.reallocations, - targetMarketParams: marketParams, - }) - : buildVaultV2BlueReallocationActions({ - chainId, - reallocations: reallocationPlan.reallocations, - targetMarketParams: marketParams, - penaltyFundingSource: usesSharedFundingToken - ? "generalAdapter1" - : "initiator", - }); + targetMarketParams: marketParams, + penaltyFundingSource: usesSharedFundingToken + ? "generalAdapter1" + : "initiator", + }); const erc20FundingAmount = amount + (usesSharedFundingToken ? reallocationResult.penaltyAssets : 0n); diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index a17e38f1c..c84012a64 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -3,7 +3,6 @@ import { deepFreeze } from "@morpho-org/morpho-ts"; import type { Address } from "viem"; import { type Action, BundlerAction } from "../../bundler/index.js"; import { addTransactionMetadata } from "../../helpers/index.js"; -import { validateAndNormalizeReallocations } from "../../helpers/validate.js"; import { type AuthorizationRequirementSignature, type BlueReallocationPlan, @@ -15,10 +14,7 @@ import { type Transaction, } from "../../types/index.js"; import { getBlueAuthorizationAction } from "../signatures/getBlueAuthorizationAction.js"; -import { - buildVaultV1ReallocationActions, - buildVaultV2BlueReallocationActions, -} from "./buildReallocationActions.js"; +import { buildBlueReallocationActions } from "./buildReallocationActions.js"; /** Parameters for {@link blueWithdraw}. */ export interface BlueWithdrawParams { @@ -156,24 +152,15 @@ export const blueWithdraw = ({ actions.push(getBlueAuthorizationAction(chainId, authorizationSignature)); } - const reallocationPlan = validateAndNormalizeReallocations( - reallocations, - marketParams.id, - ); const { actions: reallocationActions, fee: reallocationFee, penaltyAssets: reallocationPenaltyAssets, - } = reallocationPlan.type === "vaultV1" - ? buildVaultV1ReallocationActions({ - reallocations: reallocationPlan.reallocations, - targetMarketParams: marketParams, - }) - : buildVaultV2BlueReallocationActions({ - chainId, - reallocations: reallocationPlan.reallocations, - targetMarketParams: marketParams, - }); + } = buildBlueReallocationActions({ + chainId, + reallocations, + targetMarketParams: marketParams, + }); actions.push(...reallocationActions); actions.push({ diff --git a/packages/morpho-sdk/src/entities/blue/AGENTS.md b/packages/morpho-sdk/src/entities/blue/AGENTS.md index c4aee8b1e..479249658 100644 --- a/packages/morpho-sdk/src/entities/blue/AGENTS.md +++ b/packages/morpho-sdk/src/entities/blue/AGENTS.md @@ -21,7 +21,7 @@ `getRequirements` returns: - ERC-20 approval for **GeneralAdapter1** on the collateral token (any path that supplies collateral) or the loan token (`supply`, `repay`, `repayWithdrawCollateral`). The approved amount is the **ERC-20 portion actually pulled**, not the total: for a native-funded repay it is `amount` (assets mode) or `max(0, toBorrowAssets(shares) − nativeAmount)` (shares mode — clamped at 0 so a `nativeAmount` that covers or exceeds the borrow assets pulls nothing). A fully-native repay pulls no ERC-20, so no approval requirement is emitted; in shares mode any wrapped native beyond the on-chain repay is skimmed back to the receiver. -- A classic ERC-20 approval for **GeneralAdapter1** on the loan token when `borrow`, `supplyCollateralBorrow`, `withdraw`, or `refinance` includes BluePublicAllocator reallocations with a non-zero penalty. The approved amount is the sum of each call's independently rounded `ceil(assets × penalty / WAD)` donation. This path deliberately does not return a permit signature, so it can coexist with a collateral-token permit in `supplyCollateralBorrow`. +- A classic ERC-20 approval for **GeneralAdapter1** on the loan token when `borrow`, `withdraw`, or `refinance` includes BluePublicAllocator reallocations with a non-zero penalty. `supplyCollateralBorrow` does the same when the collateral and loan tokens differ; when they are identical, it adds the penalty to the single collateral approval or permit and emits no separate penalty requirement. The approved amount is the sum of each call's independently rounded `ceil(assets × penalty / WAD)` donation. The separate-token path deliberately does not return a permit signature, so it can coexist with a collateral-token permit. - `morpho.setAuthorization(generalAdapter1, true)` when authorization is not yet set on Morpho — read via `publicActions`. Required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, and `withdraw` (loan-asset). When `supportSignature` is enabled on the client, the authorization requirement is returned as a signable `Requirement` instead of a transaction; signing it produces an `AuthorizationRequirementSignature` that `buildTx` consumes and folds into the bundle as a `setAuthorizationWithSig` call, so no standalone authorization transaction is needed. `buildTx` accepts a `readonly RequirementSignature[]` and splits permit vs. authorization signatures via `isPermitSignature` / `isAuthorizationSignature`. diff --git a/packages/morpho-ts/src/addresses.test.ts b/packages/morpho-ts/src/addresses.test.ts index d912699de..cd21deeb8 100644 --- a/packages/morpho-ts/src/addresses.test.ts +++ b/packages/morpho-ts/src/addresses.test.ts @@ -165,6 +165,9 @@ describe("addressesRegistry", () => { [ChainId.MonadMainnet, "0x0A503aB026EFACBC0F7feE7795F34B80b5B9a662"], [ChainId.StableMainnet, "0x5C884d4B1510EAd302EC50A2AB4DE9c0b9E407ce"], [ChainId.TempoMainnet, "0xDC9693CE6488640faEf173Ec2635ff99fdC25a07"], + [ChainId.KaiaMainnet, "0x3b369B37eba1655e8c44bC08E3A604D592c4a14F"], + [ChainId.MorphMainnet, "0x20d990D9eBf8003Df8cAD3Aa36aeF4404e3Ccb86"], + [ChainId.MegaEthMainnet, "0xB4A1B0EF18d169c19fC7617aCE898A06Dc495a7C"], [ChainId.RobinhoodMainnet, "0xCe5c1aFa115fF8b1D6913509bfc79D9AE08CC857"], ] as const)( "behavior: exposes BluePublicAllocator on chain %i", diff --git a/packages/morpho-ts/src/addresses.ts b/packages/morpho-ts/src/addresses.ts index d52362509..cb0107bc4 100644 --- a/packages/morpho-ts/src/addresses.ts +++ b/packages/morpho-ts/src/addresses.ts @@ -1028,6 +1028,7 @@ const _addressesRegistry = { generalAdapter1: "0x8e36C2c6d7771820BF14a75f725f3cf0374a7823", }, adaptiveCurveIrm: "0xA4E2bA20Fc64D721D95BD5a28FF71844C5bb5cF2", + vaultV2BluePublicAllocator: "0x3b369B37eba1655e8c44bC08E3A604D592c4a14F", vaultV2Factory: "0xf2Aecd4a4d4C21d08770e34F392C4C271aBD9144", morphoMarketV1AdapterV2Factory: "0x4d04C39ca604b560c50F4045c558378FD9AEBCF4", @@ -1061,6 +1062,7 @@ const _addressesRegistry = { generalAdapter1: "0xcaeec65c85Fe964c8Bd814cb8E4CaF8B06bde776", }, adaptiveCurveIrm: "0xfB69467De332E03FF502B85bB2249d2f721F3319", + vaultV2BluePublicAllocator: "0x20d990D9eBf8003Df8cAD3Aa36aeF4404e3Ccb86", vaultV2Factory: "0x7D8BF8B276f967F7539c9e91E1a85a33fefE612B", morphoMarketV1AdapterV2Factory: "0xa01D7c41cf419405d4DF2e5750d26438DCAC28a6", @@ -1078,6 +1080,7 @@ const _addressesRegistry = { generalAdapter1: "0x74d3cbc721613C8461df92658d0a20dF275Ca31b", }, adaptiveCurveIrm: "0x56875764185548B0ca72A1877b3aE15E44e8A323", + vaultV2BluePublicAllocator: "0xB4A1B0EF18d169c19fC7617aCE898A06Dc495a7C", vaultV2Factory: "0xf133FA5A78C398B31Cc4a180E6Ae84111D6DCF5B", morphoMarketV1AdapterV2Factory: "0x00a58b7a9B3E86CB21f5F11f29F4A12346457012", @@ -1795,6 +1798,7 @@ const _deployments = { generalAdapter1: 208021118n, }, adaptiveCurveIrm: 208021118n, + vaultV2BluePublicAllocator: 224866814n, vaultV2Factory: 213463014n, morphoMarketV1AdapterV2Factory: 213463079n, registryList: 213463079n, @@ -1826,6 +1830,7 @@ const _deployments = { generalAdapter1: 23180020n, }, adaptiveCurveIrm: 23180020n, + vaultV2BluePublicAllocator: 25455933n, vaultV2Factory: 23180183n, morphoMarketV1AdapterV2Factory: 23180228n, registryList: 23180228n, @@ -1842,6 +1847,7 @@ const _deployments = { generalAdapter1: 16408957n, }, adaptiveCurveIrm: 16408957n, + vaultV2BluePublicAllocator: 24269516n, vaultV2Factory: 16409067n, morphoMarketV1AdapterV2Factory: 16409115n, registryList: 16409115n, From f2119a62453d0f8869163c7357b8fd1182ea6dc0 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Thu, 20 Aug 2026 17:15:43 +0200 Subject: [PATCH 34/41] fix: complete Vault V2 allocator review fixes --- packages/morpho-sdk/src/entities/blue/blue.ts | 42 +++++---- .../vaultV2BlueReallocationData.test.ts | 40 ++++++++- packages/morpho-ts/src/addresses.test.ts | 85 +++++++++++++++---- packages/morpho-ts/src/addresses.ts | 13 +++ 4 files changed, 139 insertions(+), 41 deletions(-) diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 3b9aa7fd9..eb8f8eb5f 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -89,7 +89,6 @@ import { selectRequirementSignatures, type Transaction, type VaultV1Reallocation, - type VaultV2BlueReallocation, WithdrawExceedsCollateralError, } from "../../types/index.js"; import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; @@ -619,8 +618,11 @@ export class MorphoBlue implements BlueActions { private getReallocationPenaltyRequirements( userAddress: Address, - reallocations: readonly VaultV2BlueReallocation[], + reallocationPlan: ReturnType, ) { + if (reallocationPlan.type === "vaultV1") return []; + + const { reallocations } = reallocationPlan; const amount = reallocations.reduce( (total, reallocation) => total + @@ -828,12 +830,10 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { const [penaltyRequirements, authTx] = await Promise.all([ - reallocationPlan.type === "vaultV2Blue" - ? this.getReallocationPenaltyRequirements( - userAddress, - reallocationPlan.reallocations, - ) - : Promise.resolve([]), + this.getReallocationPenaltyRequirements( + userAddress, + reallocationPlan, + ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -972,12 +972,10 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { const [penaltyRequirements, authTx] = await Promise.all([ - reallocationPlan.type === "vaultV2Blue" - ? this.getReallocationPenaltyRequirements( - userAddress, - reallocationPlan.reallocations, - ) - : Promise.resolve([]), + this.getReallocationPenaltyRequirements( + userAddress, + reallocationPlan, + ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -1520,11 +1518,11 @@ export class MorphoBlue implements BlueActions { from: userAddress, }, }), - usesSharedFundingToken || reallocationPlan.type === "vaultV1" - ? Promise.resolve([]) + usesSharedFundingToken + ? [] : this.getReallocationPenaltyRequirements( userAddress, - reallocationPlan.reallocations, + reallocationPlan, ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, @@ -1769,12 +1767,10 @@ export class MorphoBlue implements BlueActions { return { getRequirements: async () => { const [penaltyRequirements, authTx] = await Promise.all([ - targetReallocationPlan.type === "vaultV2Blue" - ? this.getReallocationPenaltyRequirements( - userAddress, - targetReallocationPlan.reallocations, - ) - : Promise.resolve([]), + this.getReallocationPenaltyRequirements( + userAddress, + targetReallocationPlan, + ), getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index 8971ad504..c5074d4b1 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -14,7 +14,7 @@ import { VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import type { Address, Hash } from "viem"; -import { zeroAddress } from "viem"; +import { zeroAddress, zeroHash } from "viem"; import { describe, expect, test } from "vitest"; import { blueBorrow } from "../actions/index.js"; import { @@ -23,7 +23,11 @@ import { NegativeInputError, NonPositiveInputError, ReallocationWithdrawExceedsMarketSupplyError, + UnknownReallocationAdapterError, + UnknownReallocationAllocationError, UnknownReallocationMarketError, + UnknownReallocationMarketPublicAllocatorConfigError, + UnknownReallocationPublicAllocatorConfigError, } from "../types/index.js"; import { VaultV2BlueReallocationData } from "./vaultV2BlueReallocationData.js"; @@ -323,6 +327,40 @@ const makeFixture = ({ }; }; +describe("VaultV2BlueReallocationData accessors", () => { + test("error: UnknownReallocationAllocationError", () => { + const { data } = makeFixture(); + + expect(() => data.getAllocation(VAULT, zeroHash)).toThrow( + UnknownReallocationAllocationError, + ); + }); + + test("error: UnknownReallocationPublicAllocatorConfigError", () => { + const { data } = makeFixture(); + + expect(() => data.getPublicAllocatorConfig(SECOND_VAULT)).toThrow( + UnknownReallocationPublicAllocatorConfigError, + ); + }); + + test("error: UnknownReallocationMarketPublicAllocatorConfigError", () => { + const { data } = makeFixture(); + + expect(() => data.getMarketPublicAllocatorConfig(VAULT, zeroHash)).toThrow( + UnknownReallocationMarketPublicAllocatorConfigError, + ); + }); + + test("error: UnknownReallocationAdapterError", () => { + const { data } = makeFixture(); + + expect(() => data.getAdapter(VAULT, zeroAddress)).toThrow( + UnknownReallocationAdapterError, + ); + }); +}); + describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { test("default: returns an action-ready market reallocation and cloned post-state", () => { const { data, sourceExpectedAssets, sourceIds, targetIds } = makeFixture(); diff --git a/packages/morpho-ts/src/addresses.test.ts b/packages/morpho-ts/src/addresses.test.ts index cd21deeb8..3219fa39a 100644 --- a/packages/morpho-ts/src/addresses.test.ts +++ b/packages/morpho-ts/src/addresses.test.ts @@ -153,31 +153,82 @@ describe("addressesRegistry", () => { }); test.each([ - [ChainId.EthMainnet, "0x00b8e1509398ED692C3F326CbAf1694F9A881e27"], - [ChainId.BaseMainnet, "0xAED282B8aD9257BB1272e93aE63A32A53621e412"], - [ChainId.ArbitrumMainnet, "0x85b66Fe31e6788E5a6825EAe689f4c6c38AF3704"], - [ChainId.OptimismMainnet, "0xc6945A915Bb7e2A365469f120A33D2FA42951cF3"], - [ChainId.PolygonMainnet, "0xAb06a92cd253Bc12Dec8f719a693a6b472CCDfF4"], - [ChainId.WorldChainMainnet, "0x5Fe47f63ACd84f8A69b97E0a5122fCBff08Df48F"], - [ChainId.Unichain, "0x2b7Bf2f2027bcfE3A1F6Bc93EA80220a883a6851"], - [ChainId.HyperliquidMainnet, "0x056dd7D4B373ED26c788190085CC6C52B8e7479d"], - [ChainId.KatanaMainnet, "0xd952175e940D97775cBC5a523977a6f091D0d702"], - [ChainId.MonadMainnet, "0x0A503aB026EFACBC0F7feE7795F34B80b5B9a662"], - [ChainId.StableMainnet, "0x5C884d4B1510EAd302EC50A2AB4DE9c0b9E407ce"], - [ChainId.TempoMainnet, "0xDC9693CE6488640faEf173Ec2635ff99fdC25a07"], - [ChainId.KaiaMainnet, "0x3b369B37eba1655e8c44bC08E3A604D592c4a14F"], - [ChainId.MorphMainnet, "0x20d990D9eBf8003Df8cAD3Aa36aeF4404e3Ccb86"], - [ChainId.MegaEthMainnet, "0xB4A1B0EF18d169c19fC7617aCE898A06Dc495a7C"], - [ChainId.RobinhoodMainnet, "0xCe5c1aFa115fF8b1D6913509bfc79D9AE08CC857"], + [ + ChainId.EthMainnet, + ["0x00b8e1509398ED692C3F326CbAf1694F9A881e27", 25770408n], + ], + [ + ChainId.BaseMainnet, + ["0xAED282B8aD9257BB1272e93aE63A32A53621e412", 50063965n], + ], + [ + ChainId.ArbitrumMainnet, + ["0x85b66Fe31e6788E5a6825EAe689f4c6c38AF3704", 495274087n], + ], + [ + ChainId.OptimismMainnet, + ["0xc6945A915Bb7e2A365469f120A33D2FA42951cF3", 155659263n], + ], + [ + ChainId.PolygonMainnet, + ["0xAb06a92cd253Bc12Dec8f719a693a6b472CCDfF4", 92141509n], + ], + [ + ChainId.WorldChainMainnet, + ["0x5Fe47f63ACd84f8A69b97E0a5122fCBff08Df48F", 33790828n], + ], + [ + ChainId.Unichain, + ["0x2b7Bf2f2027bcfE3A1F6Bc93EA80220a883a6851", 56168924n], + ], + [ + ChainId.HyperliquidMainnet, + ["0x056dd7D4B373ED26c788190085CC6C52B8e7479d", 43372279n], + ], + [ + ChainId.KatanaMainnet, + ["0xd952175e940D97775cBC5a523977a6f091D0d702", 40217302n], + ], + [ + ChainId.MonadMainnet, + ["0x0A503aB026EFACBC0F7feE7795F34B80b5B9a662", 96602489n], + ], + [ + ChainId.StableMainnet, + ["0x5C884d4B1510EAd302EC50A2AB4DE9c0b9E407ce", 35817019n], + ], + [ + ChainId.TempoMainnet, + ["0xDC9693CE6488640faEf173Ec2635ff99fdC25a07", 35177253n], + ], + [ + ChainId.KaiaMainnet, + ["0x3b369B37eba1655e8c44bC08E3A604D592c4a14F", 224866814n], + ], + [ + ChainId.MorphMainnet, + ["0x20d990D9eBf8003Df8cAD3Aa36aeF4404e3Ccb86", 25455933n], + ], + [ + ChainId.MegaEthMainnet, + ["0xB4A1B0EF18d169c19fC7617aCE898A06Dc495a7C", 24269516n], + ], + [ + ChainId.RobinhoodMainnet, + ["0xCe5c1aFa115fF8b1D6913509bfc79D9AE08CC857", 38318973n], + ], ] as const)( "behavior: exposes BluePublicAllocator on chain %i", - (chainId, vaultV2BluePublicAllocator) => { + (chainId, [vaultV2BluePublicAllocator, deploymentBlock]) => { expect(addressesRegistry[chainId].vaultV2BluePublicAllocator).toBe( vaultV2BluePublicAllocator, ); expect(getChainAddress(chainId, "vaultV2BluePublicAllocator")).toBe( vaultV2BluePublicAllocator, ); + expect(deployments[chainId].vaultV2BluePublicAllocator).toBe( + deploymentBlock, + ); }, ); diff --git a/packages/morpho-ts/src/addresses.ts b/packages/morpho-ts/src/addresses.ts index cb0107bc4..c9b327f73 100644 --- a/packages/morpho-ts/src/addresses.ts +++ b/packages/morpho-ts/src/addresses.ts @@ -1142,6 +1142,7 @@ const _deployments = { adaptiveCurveIrm: 18883124n, vaultV1PublicAllocator: 19375099n, publicAllocator: 19375099n, + vaultV2BluePublicAllocator: 25770408n, metaMorphoFactory: 21439510n, vaultV2Factory: 23375073n, morphoMarketV1AdapterFactory: 23375073n, @@ -1167,6 +1168,7 @@ const _deployments = { adaptiveCurveIrm: 13977152n, vaultV1PublicAllocator: 13979545n, publicAllocator: 13979545n, + vaultV2BluePublicAllocator: 50063965n, metaMorphoFactory: 23928808n, vaultV2Factory: 35615206n, morphoMarketV1AdapterFactory: 35615206n, @@ -1199,6 +1201,7 @@ const _deployments = { adaptiveCurveIrm: 66931042n, vaultV1PublicAllocator: 66931042n, publicAllocator: 66931042n, + vaultV2BluePublicAllocator: 92141509n, metaMorphoFactory: 66931042n, vaultV2Factory: 77371907n, morphoMarketV1AdapterFactory: 77371907n, @@ -1223,6 +1226,7 @@ const _deployments = { adaptiveCurveIrm: 296446593n, vaultV1PublicAllocator: 296446593n, publicAllocator: 296446593n, + vaultV2BluePublicAllocator: 495274087n, metaMorphoFactory: 296447195n, vaultV2Factory: 387016724n, morphoMarketV1AdapterFactory: 387016724n, @@ -1245,6 +1249,7 @@ const _deployments = { adaptiveCurveIrm: 130770075n, vaultV1PublicAllocator: 130770075n, publicAllocator: 130770075n, + vaultV2BluePublicAllocator: 155659263n, metaMorphoFactory: 130770189n, vaultV2Factory: 142122059n, morphoMarketV1AdapterFactory: 142122059n, @@ -1265,6 +1270,7 @@ const _deployments = { adaptiveCurveIrm: 9025669n, vaultV1PublicAllocator: 9025669n, publicAllocator: 9025669n, + vaultV2BluePublicAllocator: 33790828n, metaMorphoFactory: 9025733n, vaultV2Factory: 20253005n, morphoMarketV1AdapterFactory: 20253005n, @@ -1330,6 +1336,7 @@ const _deployments = { adaptiveCurveIrm: 9139027n, vaultV1PublicAllocator: 9139027n, publicAllocator: 9139027n, + vaultV2BluePublicAllocator: 56168924n, metaMorphoFactory: 9316789n, vaultV2Factory: 29092109n, morphoMarketV1AdapterFactory: 29092109n, @@ -1439,6 +1446,7 @@ const _deployments = { adaptiveCurveIrm: 2741069n, vaultV1PublicAllocator: 2741069n, publicAllocator: 2741069n, + vaultV2BluePublicAllocator: 40217302n, metaMorphoFactory: 2741420n, vaultV2Factory: 13096629n, morphoMarketV1AdapterFactory: 13096629n, @@ -1502,6 +1510,7 @@ const _deployments = { adaptiveCurveIrm: 1988429n, vaultV1PublicAllocator: 1988429n, publicAllocator: 1988429n, + vaultV2BluePublicAllocator: 43372279n, metaMorphoFactory: 1988677n, vaultV2Factory: 14188393n, morphoMarketV1AdapterFactory: 14188393n, @@ -1570,6 +1579,7 @@ const _deployments = { adaptiveCurveIrm: 31907457n, vaultV1PublicAllocator: 31907457n, publicAllocator: 31907457n, + vaultV2BluePublicAllocator: 96602489n, metaMorphoFactory: 32320327n, vaultV2Factory: 32321811n, morphoMarketV1AdapterFactory: 32321811n, @@ -1591,6 +1601,7 @@ const _deployments = { adaptiveCurveIrm: 1504506n, vaultV1PublicAllocator: 1504506n, publicAllocator: 1504506n, + vaultV2BluePublicAllocator: 35817019n, metaMorphoFactory: 1504774n, vaultV2Factory: 1506182n, morphoMarketV1AdapterFactory: 1506182n, @@ -1702,6 +1713,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 34_046_873n }, adaptiveCurveIrm: 2375313n, + vaultV2BluePublicAllocator: 35177253n, vaultV2Factory: 2375650n, morphoMarketV1AdapterV2Factory: 2375701n, morphoVaultV1AdapterFactory: 16475630n, @@ -1865,6 +1877,7 @@ const _deployments = { }, bundles: { vaultExitBundlesV1: 32_383_480n }, adaptiveCurveIrm: 286n, + vaultV2BluePublicAllocator: 38318973n, vaultV2Factory: 288n, morphoMarketV1AdapterV2Factory: 289n, morphoVaultV1AdapterFactory: 58_781n, From c7d9a57ac02a2b389d3f7a7e3cbad1aae7ee2890 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 21 Aug 2026 10:55:38 +0200 Subject: [PATCH 35/41] fix: finish Vault V2 allocator review fixes --- .../vaultV2BlueReallocationData.test.ts | 181 ++++- .../entities/vaultV2BlueReallocationData.ts | 658 ++++++++++-------- packages/morpho-sdk/src/types/error.ts | 8 + .../src/morpho-protocol-evm.ts | 20 +- 4 files changed, 549 insertions(+), 318 deletions(-) diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index c5074d4b1..4e8775891 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -6,23 +6,29 @@ import { AccrualVaultV2MorphoMarketV1AdapterV2, AccrualVaultV2MorphoVaultV1Adapter, ChainId, + type IAccrualVaultV2Adapter, type IVaultV2Allocation, Market, + type MarketId, MarketParams, MathLib, + UnsupportedVaultV2AdapterError, VaultV2BlueMarketPublicAllocatorConfig, VaultV2BluePublicAllocatorConfig, } from "@morpho-org/blue-sdk"; import type { Address, Hash } from "viem"; import { zeroAddress, zeroHash } from "viem"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { blueBorrow } from "../actions/index.js"; import { InputExceedsMaxError, InsufficientSharedLiquidityError, NegativeInputError, NonPositiveInputError, + ReallocationAdapterSupplySharesUnderflowError, + ReallocationAllocationUnderflowError, ReallocationWithdrawExceedsMarketSupplyError, + UnknownReallocationActiveAdaptersError, UnknownReallocationAdapterError, UnknownReallocationAllocationError, UnknownReallocationMarketError, @@ -327,7 +333,62 @@ const makeFixture = ({ }; }; +describe("VaultV2BlueReallocationData construction", () => { + test("error: UnsupportedVaultV2AdapterError", () => { + const { data } = makeFixture(); + const vault = data.getVault(VAULT); + const adapter = data.getAdapter(VAULT, TARGET_ADAPTER); + const unsupportedAdapter: IAccrualVaultV2Adapter = { + type: "UnsupportedAdapter", + address: SECOND_TARGET_ADAPTER, + parentVault: VAULT, + adapterId: adapter.adapterId, + skimRecipient: zeroAddress, + realAssets: adapter.realAssets.bind(adapter), + maxDeposit: adapter.maxDeposit.bind(adapter), + maxWithdraw: adapter.maxWithdraw.bind(adapter), + }; + const unsupportedVault = new AccrualVaultV2( + { + ...vault, + liquidityAllocations: vault.liquidityAllocations?.map((allocation) => ({ + ...allocation, + })), + }, + undefined, + [unsupportedAdapter], + vault.assetBalance, + { ...vault.forceDeallocatePenalties }, + ); + + expect( + () => + new VaultV2BlueReallocationData({ + chainId: data.chainId, + vaults: { [VAULT]: unsupportedVault }, + }), + ).toThrow(UnsupportedVaultV2AdapterError); + }); +}); + describe("VaultV2BlueReallocationData accessors", () => { + test("behavior: returns a fetched empty active-adapter set", () => { + const { data } = makeFixture({ allocatorActiveAdapters: [] }); + + expect(data.getActiveAdapters(VAULT)).toStrictEqual(new Set()); + }); + + test("error: UnknownReallocationActiveAdaptersError", () => { + const { data } = makeFixture(); + delete ( + data.activeAdapters as Record | undefined> + )[VAULT]; + + expect(() => data.getActiveAdapters(VAULT)).toThrow( + UnknownReallocationActiveAdaptersError, + ); + }); + test("error: UnknownReallocationAllocationError", () => { const { data } = makeFixture(); @@ -365,7 +426,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { test("default: returns an action-ready market reallocation and cloned post-state", () => { const { data, sourceExpectedAssets, sourceIds, targetIds } = makeFixture(); - expect(data.activeAdapters[VAULT]).toStrictEqual( + expect(data.getActiveAdapters(VAULT)).toStrictEqual( new Set([TARGET_ADAPTER.toLowerCase(), SOURCE_ADAPTER.toLowerCase()]), ); const result = data.computeVaultV2BlueReallocations(targetParams.id); @@ -393,6 +454,59 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ); }); + test("error: ReallocationAdapterSupplySharesUnderflowError", () => { + const { data } = makeFixture(); + const sourceAdapter = data.getAdapter(VAULT, SOURCE_ADAPTER); + (sourceAdapter.supplyShares as Record)[sourceParams.id] = + 0n; + + expect(() => + // biome-ignore lint/complexity/useLiteralKeys: exercise the private transition invariant directly. + data["cloneWithPublicReallocation"]({ + reallocation: { + vault: VAULT, + from: { + type: "market", + adapter: SOURCE_ADAPTER, + marketParams: sourceParams, + }, + to: { adapter: TARGET_ADAPTER }, + assets: 1n, + penalty: 7n, + }, + targetMarketId: targetParams.id, + timestamp: TIMESTAMP, + }), + ).toThrow(ReallocationAdapterSupplySharesUnderflowError); + }); + + test("error: ReallocationAllocationUnderflowError", () => { + const { data, sourceIds } = makeFixture(); + const allocation = data.getAllocation(VAULT, sourceIds[0]); + (data.allocations[VAULT] as Record)[ + sourceIds[0] + ] = { ...allocation, allocation: 0n }; + + expect(() => + // biome-ignore lint/complexity/useLiteralKeys: exercise the private transition invariant directly. + data["cloneWithPublicReallocation"]({ + reallocation: { + vault: VAULT, + from: { + type: "market", + adapter: SOURCE_ADAPTER, + marketParams: sourceParams, + }, + to: { adapter: TARGET_ADAPTER }, + assets: 1n, + penalty: 7n, + }, + targetMarketId: targetParams.id, + timestamp: TIMESTAMP, + }), + ).toThrow(ReallocationAllocationUnderflowError); + }); + test("behavior: allocates into a configured target with no existing position", () => { const { data, sourceExpectedAssets, targetIds } = makeFixture({ targetTracked: false, @@ -429,6 +543,28 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ).toBe(100n); }); + test("behavior: refreshes only adapter views for changed markets", () => { + const { data } = makeFixture(); + const targetAdapter = data.getAdapter(VAULT, TARGET_ADAPTER); + const sourceAdapter = data.getAdapter(VAULT, SOURCE_ADAPTER); + const previousTargetMarkets = targetAdapter.markets; + const previousSourceMarkets = sourceAdapter.markets; + const targetMarket = data + .getMarket(targetParams.id) + .accrueInterest(TIMESTAMP + 1n); + + ( + data as unknown as { + setMarkets(markets: Iterable): void; + } + ).setMarkets([targetMarket]); + + expect(data.getMarket(targetParams.id)).toBe(targetMarket); + expect(targetAdapter.markets).not.toBe(previousTargetMarkets); + expect(targetAdapter.markets[0]).toBe(targetMarket); + expect(sourceAdapter.markets).toBe(previousSourceMarkets); + }); + test.each([0n, MathLib.WAD])( "behavior: accepts maxWithdrawalUtilization boundary %s", (maxWithdrawalUtilization) => { @@ -719,22 +855,21 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }); const cloned = input.clone(); - expect(cloned.activeAdapters[VAULT]).toStrictEqual( - input.activeAdapters[VAULT], + expect(cloned.publicAllocatorConfigs).toBe(input.publicAllocatorConfigs); + expect(cloned.activeAdapters).toBe(input.activeAdapters); + expect(cloned.marketPublicAllocatorConfigs).toBe( + input.marketPublicAllocatorConfigs, + ); + expect(cloned.allocations[VAULT]).not.toBe(input.allocations[VAULT]); + expect(cloned.getAllocation(VAULT, targetIds[2])).toBe( + input.getAllocation(VAULT, targetIds[2]), ); - expect(cloned.activeAdapters[VAULT]).not.toBe(input.activeAdapters[VAULT]); expect(cloned.getPublicAllocatorConfig(VAULT)).toBeInstanceOf( VaultV2BluePublicAllocatorConfig, ); - expect(cloned.getPublicAllocatorConfig(VAULT)).not.toBe( - input.getPublicAllocatorConfig(VAULT), - ); expect( cloned.getMarketPublicAllocatorConfig(VAULT, targetIds[2]), ).toBeInstanceOf(VaultV2BlueMarketPublicAllocatorConfig); - expect(cloned.getMarketPublicAllocatorConfig(VAULT, targetIds[2])).not.toBe( - input.getMarketPublicAllocatorConfig(VAULT, targetIds[2]), - ); const inputLegacy = input .getVault(VAULT) .accrualAdapters.find( @@ -1075,6 +1210,30 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ).toThrow(UnknownReallocationMarketError); }); + test("error: UnknownReallocationMarketError for incomplete source state", () => { + const { data } = makeFixture(); + const incompleteData = data.clone(); + delete (incompleteData.markets as Record)[ + sourceParams.id + ]; + vi.spyOn(data, "clone").mockReturnValue(incompleteData); + + expect(() => data.computeVaultV2BlueReallocations(targetParams.id)).toThrow( + UnknownReallocationMarketError, + ); + }); + + test("error: UnknownReallocationActiveAdaptersError for incomplete vault state", () => { + const { data } = makeFixture(); + delete ( + data.activeAdapters as Record | undefined> + )[VAULT]; + + expect(() => data.computeVaultV2BlueReallocations(targetParams.id)).toThrow( + UnknownReallocationActiveAdaptersError, + ); + }); + test("behavior: ignores vault liquidity above the penalty threshold", () => { const { data, sourceExpectedAssets } = makeFixture({ idle: 300n, diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index 839e2a09f..c395674a7 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -14,6 +14,7 @@ import { MarketUtils, MathLib, UnknownDataError, + UnsupportedVaultV2AdapterError, VaultV2BlueMarketPublicAllocatorConfig, VaultV2BluePublicAllocatorConfig, VaultV2BluePublicAllocatorConfigUtils, @@ -37,6 +38,7 @@ import { ReallocationAdapterSupplySharesUnderflowError, ReallocationAllocationUnderflowError, ReallocationWithdrawExceedsMarketSupplyError, + UnknownReallocationActiveAdaptersError, UnknownReallocationAdapterError, UnknownReallocationAllocationError, UnknownReallocationMarketError, @@ -212,7 +214,7 @@ const cloneAdapter = ( adapter.shares, ); - return adapter; + throw new UnsupportedVaultV2AdapterError(adapter.address); }; const cloneVault = ( @@ -318,8 +320,10 @@ export class VaultV2BlueReallocationData * Creates a cloned Vault V2 reallocation snapshot. * * @param input - State fetched at one consistent block. + * @throws {UnsupportedVaultV2AdapterError} when a vault contains an unsupported adapter type. */ public constructor(input: InputVaultV2BlueReallocationData) { + const isClone = input instanceof VaultV2BlueReallocationData; this.chainId = input.chainId; this.mutableMarkets = {}; this.mutableVaults = {}; @@ -327,28 +331,75 @@ export class VaultV2BlueReallocationData this.markets = this.mutableMarkets; this.vaults = this.mutableVaults; this.allocations = this.mutableAllocations; - const publicAllocatorConfigs: Record< - Address, - VaultV2BluePublicAllocatorConfig | undefined - > = {}; - this.publicAllocatorConfigs = publicAllocatorConfigs; - const activeAdapters: Record | undefined> = - {}; - this.activeAdapters = activeAdapters; - const marketPublicAllocatorConfigs: Record< - Address, - | Record - | undefined - > = {}; - this.marketPublicAllocatorConfigs = marketPublicAllocatorConfigs; - this.donatedPenaltyAssets = - input instanceof VaultV2BlueReallocationData - ? { ...input.donatedPenaltyAssets } - : {}; - this.firstTotalAssets = - input instanceof VaultV2BlueReallocationData - ? { ...input.firstTotalAssets } - : {}; + if (isClone) { + this.publicAllocatorConfigs = input.publicAllocatorConfigs; + this.activeAdapters = input.activeAdapters; + this.marketPublicAllocatorConfigs = input.marketPublicAllocatorConfigs; + this.donatedPenaltyAssets = { ...input.donatedPenaltyAssets }; + this.firstTotalAssets = { ...input.firstTotalAssets }; + } else { + const publicAllocatorConfigs: Record< + Address, + VaultV2BluePublicAllocatorConfig | undefined + > = {}; + this.publicAllocatorConfigs = publicAllocatorConfigs; + const activeAdapters: Record | undefined> = + {}; + this.activeAdapters = activeAdapters; + const marketPublicAllocatorConfigs: Record< + Address, + | Record + | undefined + > = {}; + this.marketPublicAllocatorConfigs = marketPublicAllocatorConfigs; + this.donatedPenaltyAssets = {}; + this.firstTotalAssets = {}; + + for (const [vault, config] of Object.entries( + input.publicAllocatorConfigs ?? {}, + ) as [Address, IVaultV2BluePublicAllocatorConfig | undefined][]) { + publicAllocatorConfigs[vault] = + config == null + ? undefined + : new VaultV2BluePublicAllocatorConfig(config); + } + + for (const [vault, adapters] of Object.entries( + input.activeAdapters ?? {}, + ) as [Address, Iterable
| undefined][]) { + activeAdapters[vault] = + adapters == null + ? undefined + : new Set( + [...adapters].map( + (adapter) => adapter.toLowerCase() as Address, + ), + ); + } + + for (const [vault, configs] of Object.entries( + input.marketPublicAllocatorConfigs ?? {}, + ) as [ + Address, + ( + | Readonly< + Record + > + | undefined + ), + ][]) { + marketPublicAllocatorConfigs[vault] = {}; + for (const [id, config] of Object.entries(configs ?? {}) as [ + Hash, + IVaultV2BlueMarketPublicAllocatorConfig | undefined, + ][]) { + marketPublicAllocatorConfigs[vault]![id] = + config == null + ? undefined + : new VaultV2BlueMarketPublicAllocatorConfig(config); + } + } + } for (const [marketId, market] of Object.entries(input.markets ?? {}) as [ MarketId, @@ -373,6 +424,10 @@ export class VaultV2BlueReallocationData Address, Readonly> | undefined, ][]) { + if (isClone) { + this.mutableAllocations[vault] = { ...allocations }; + continue; + } this.mutableAllocations[vault] = {}; for (const [id, allocation] of Object.entries(allocations ?? {}) as [ Hash, @@ -382,49 +437,6 @@ export class VaultV2BlueReallocationData allocation == null ? undefined : { ...allocation }; } } - - for (const [vault, config] of Object.entries( - input.publicAllocatorConfigs ?? {}, - ) as [Address, IVaultV2BluePublicAllocatorConfig | undefined][]) { - publicAllocatorConfigs[vault] = - config == null - ? undefined - : new VaultV2BluePublicAllocatorConfig(config); - } - - for (const [vault, adapters] of Object.entries( - input.activeAdapters ?? {}, - ) as [Address, Iterable
| undefined][]) { - activeAdapters[vault] = - adapters == null - ? undefined - : new Set( - [...adapters].map((adapter) => adapter.toLowerCase() as Address), - ); - } - - for (const [vault, configs] of Object.entries( - input.marketPublicAllocatorConfigs ?? {}, - ) as [ - Address, - ( - | Readonly< - Record - > - | undefined - ), - ][]) { - marketPublicAllocatorConfigs[vault] = {}; - for (const [id, config] of Object.entries(configs ?? {}) as [ - Hash, - IVaultV2BlueMarketPublicAllocatorConfig | undefined, - ][]) { - marketPublicAllocatorConfigs[vault]![id] = - config == null - ? undefined - : new VaultV2BlueMarketPublicAllocatorConfig(config); - } - } } /** @@ -515,6 +527,24 @@ export class VaultV2BlueReallocationData return config; } + /** + * Gets the BluePublicAllocator-active adapters for a Vault V2. + * + * @param vault - Vault V2 address. + * @returns The active adapter addresses, or an empty set when none are active. + * @throws {UnknownReallocationActiveAdaptersError} when the active-adapter state is absent. + * @example + * ```ts + * const activeAdapters = data.getActiveAdapters(vaultAddress); + * ``` + */ + public getActiveAdapters(vault: Address): ReadonlySet
{ + const adapters = this.activeAdapters[vault]; + if (adapters == null) + throw new UnknownReallocationActiveAdaptersError(vault); + return adapters; + } + /** * Gets one adapter-market BluePublicAllocator configuration. * @@ -587,7 +617,8 @@ export class VaultV2BlueReallocationData * @throws {NegativeInputError} when `maxWithdrawalUtilization` is negative. * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` exceeds WAD. * @throws {NonPositiveInputError} when the operation amount is not positive and planning is enabled. - * @throws {UnknownReallocationMarketError} when the target market is absent. + * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent for a vault. * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. * @example @@ -729,10 +760,14 @@ export class VaultV2BlueReallocationData } let data = this.clone(); - for (const currentMarket of Object.values(data.markets)) { - if (currentMarket != null) - data.setMarket(currentMarket.accrueInterest(timestamp)); - } + data.setMarkets( + Object.values(data.markets) + .filter( + (currentMarket): currentMarket is ReadonlyMarketSnapshot => + currentMarket != null, + ) + .map((currentMarket) => currentMarket.accrueInterest(timestamp)), + ); for (const reallocation of reallocations) { data = data.cloneWithPublicReallocation({ reallocation, @@ -764,9 +799,11 @@ export class VaultV2BlueReallocationData ? this.getLatestSnapshotTimestamp() : BigInt(options.timestamp); let data = this.clone(); - for (const market of Object.values(data.markets)) { - if (market != null) data.setMarket(market.accrueInterest(timestamp)); - } + data.setMarkets( + Object.values(data.markets) + .filter((market): market is ReadonlyMarketSnapshot => market != null) + .map((market) => market.accrueInterest(timestamp)), + ); const reallocations: VaultV2BlueReallocation[] = []; const configuredVaults = Object.keys(data.vaults) as Address[]; const vaultKeyByLower = new Map( @@ -784,259 +821,253 @@ export class VaultV2BlueReallocationData const candidates = vaults .map((vaultAddress) => { const targetMarket = data.getMarket(marketId); - return _try(() => { - const vault = data.getVault(vaultAddress); - const publicAllocatorConfig = - data.getPublicAllocatorConfig(vaultAddress); + const vaultContext = _try( + () => ({ + vault: data.getVault(vaultAddress), + publicAllocatorConfig: + data.getPublicAllocatorConfig(vaultAddress), + }), + UnknownDataError, + ); + if (vaultContext == null) return; + const { vault, publicAllocatorConfig } = vaultContext; + if ( + !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) || + (options.maxPenalty != null && + publicAllocatorConfig.penalty > options.maxPenalty) + ) + return; + const activeAdapters = data.getActiveAdapters(vaultAddress); + + const targetSupplyHeadroom = MathLib.zeroFloorSub( + MathLib.MAX_UINT_128, + targetMarket.totalSupplyAssets, + ); + const rawCandidates: VaultV2BlueReallocation[] = []; + + for (const adapter of vault.accrualAdapters) { + if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) + continue; + if (!isAddressEqual(adapter.parentVault, vaultAddress)) continue; if ( - !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) || - (options.maxPenalty != null && - publicAllocatorConfig.penalty > options.maxPenalty) + !isAddressEqual(targetMarket.params.loanToken, vault.asset) || + !isAddressEqual(targetMarket.params.irm, adapter.adaptiveCurveIrm) ) - return; - const activeAdapters = data.activeAdapters[vaultAddress]; - if (activeAdapters == null) return; + continue; - const targetSupplyHeadroom = MathLib.zeroFloorSub( - MathLib.MAX_UINT_128, - targetMarket.totalSupplyAssets, - ); - const rawCandidates: VaultV2BlueReallocation[] = []; - - for (const adapter of vault.accrualAdapters) { - if (!(adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2)) - continue; - if (!isAddressEqual(adapter.parentVault, vaultAddress)) continue; + const targetContext = _try(() => { + const ids = adapter.ids(targetMarket.params); + const marketPublicAllocatorConfig = + data.getMarketPublicAllocatorConfig(vaultAddress, ids[2]); if ( - !isAddressEqual(targetMarket.params.loanToken, vault.asset) || !isAddressEqual( - targetMarket.params.irm, - adapter.adaptiveCurveIrm, + marketPublicAllocatorConfig.vault, + vaultAddress, + ) || + !isAddressEqual( + marketPublicAllocatorConfig.adapter, + adapter.address, + ) || + !activeAdapters.has(adapter.address.toLowerCase() as Address) + ) + return; + + const allocations = ids.map((id) => + data.getAllocation(vaultAddress, id), + ); + if (allocations.some(({ absoluteCap }) => absoluteCap === 0n)) + return; + + const expectedSupplyAssets = targetMarket.toSupplyAssets( + adapter.supplyShares[marketId] ?? 0n, + ); + const untracked = MathLib.zeroFloorSub( + expectedSupplyAssets, + allocations[2]!.allocation, + ); + + return { + adapter, + allocations, + marketPublicAllocatorConfig, + untracked, + }; + }, UnknownDataError); + if (targetContext == null) continue; + + const targetMarketParamsAllocation = targetContext.allocations[2]!; + const allocatorHeadroom = + targetContext.marketPublicAllocatorConfig.getMaxIn( + targetMarketParamsAllocation.allocation + + targetContext.untracked, + ); + + if (publicAllocatorConfig.canPullFromIdle) { + const assets = MathLib.min( + MathLib.MAX_UINT_128, + targetSupplyHeadroom, + allocatorHeadroom, + MathLib.zeroFloorSub( + vault.assetBalance, + data.donatedPenaltyAssets[vaultAddress] ?? 0n, + ), + ); + if (assets > 0n) { + rawCandidates.push({ + vault: vaultAddress, + from: { type: "idle" }, + to: { adapter: targetContext.adapter.address }, + assets, + penalty: publicAllocatorConfig.penalty, + }); + } + } + + for (const sourceAdapter of vault.accrualAdapters) { + if ( + !( + sourceAdapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2 ) ) continue; + if (!isAddressEqual(sourceAdapter.parentVault, vaultAddress)) + continue; - const targetContext = _try(() => { - const ids = adapter.ids(targetMarket.params); - const marketPublicAllocatorConfig = - data.getMarketPublicAllocatorConfig(vaultAddress, ids[2]); + for (const sourceMarketReference of sourceAdapter.markets) { + const sourceMarket = data.getMarket(sourceMarketReference.id); if ( + !isAddressEqual(sourceMarket.params.loanToken, vault.asset) || !isAddressEqual( - marketPublicAllocatorConfig.vault, - vaultAddress, - ) || - !isAddressEqual( - marketPublicAllocatorConfig.adapter, - adapter.address, - ) || - !activeAdapters.has(adapter.address.toLowerCase() as Address) - ) - return; - - const allocations = ids.map((id) => - data.getAllocation(vaultAddress, id), - ); - if (allocations.some(({ absoluteCap }) => absoluteCap === 0n)) - return; - - const expectedSupplyAssets = targetMarket.toSupplyAssets( - adapter.supplyShares[marketId] ?? 0n, - ); - const untracked = MathLib.zeroFloorSub( - expectedSupplyAssets, - allocations[2]!.allocation, - ); - - return { - adapter, - allocations, - marketPublicAllocatorConfig, - untracked, - }; - }, UnknownDataError); - if (targetContext == null) continue; - - const targetMarketParamsAllocation = - targetContext.allocations[2]!; - const allocatorHeadroom = - targetContext.marketPublicAllocatorConfig.getMaxIn( - targetMarketParamsAllocation.allocation + - targetContext.untracked, - ); - - if (publicAllocatorConfig.canPullFromIdle) { - const assets = MathLib.min( - MathLib.MAX_UINT_128, - targetSupplyHeadroom, - allocatorHeadroom, - MathLib.zeroFloorSub( - vault.assetBalance, - data.donatedPenaltyAssets[vaultAddress] ?? 0n, - ), - ); - if (assets > 0n) { - rawCandidates.push({ - vault: vaultAddress, - from: { type: "idle" }, - to: { adapter: targetContext.adapter.address }, - assets, - penalty: publicAllocatorConfig.penalty, - }); - } - } - - for (const sourceAdapter of vault.accrualAdapters) { - if ( - !( - sourceAdapter instanceof - AccrualVaultV2MorphoMarketV1AdapterV2 + sourceMarket.params.irm, + sourceAdapter.adaptiveCurveIrm, ) ) continue; - if (!isAddressEqual(sourceAdapter.parentVault, vaultAddress)) + if (sourceMarket.id.toLowerCase() === marketId.toLowerCase()) continue; - for (const sourceMarketReference of sourceAdapter.markets) { - const sourceMarket = data.getMarket(sourceMarketReference.id); + const candidate = _try(() => { + const sourceIds = sourceAdapter.ids(sourceMarket.params); + const sourceConfig = data.getMarketPublicAllocatorConfig( + vaultAddress, + sourceIds[2], + ); if ( + !isAddressEqual(sourceConfig.vault, vaultAddress) || !isAddressEqual( - sourceMarket.params.loanToken, - vault.asset, + sourceConfig.adapter, + sourceAdapter.address, ) || - !isAddressEqual( - sourceMarket.params.irm, - sourceAdapter.adaptiveCurveIrm, - ) + !activeAdapters.has( + sourceAdapter.address.toLowerCase() as Address, + ) || + !sourceConfig.canPullFromMarket ) - continue; - if (sourceMarket.id.toLowerCase() === marketId.toLowerCase()) - continue; - - const candidate = _try(() => { - const sourceIds = sourceAdapter.ids(sourceMarket.params); - const sourceConfig = data.getMarketPublicAllocatorConfig( - vaultAddress, - sourceIds[2], - ); - if ( - !isAddressEqual(sourceConfig.vault, vaultAddress) || - !isAddressEqual( - sourceConfig.adapter, - sourceAdapter.address, - ) || - !activeAdapters.has( - sourceAdapter.address.toLowerCase() as Address, - ) || - !sourceConfig.canPullFromMarket - ) - return; - - const sourceAllocations = sourceIds.map((id) => - data.getAllocation(vaultAddress, id), - ); - if ( - sourceAllocations.some( - ({ allocation }) => allocation === 0n, - ) + return; + + const sourceAllocations = sourceIds.map((id) => + data.getAllocation(vaultAddress, id), + ); + if ( + sourceAllocations.some( + ({ allocation }) => allocation === 0n, ) - return; - - const expectedSupplyAssets = sourceMarket.toSupplyAssets( - sourceAdapter.supplyShares[sourceMarket.id] ?? 0n, - ); - const assets = MathLib.min( - MathLib.MAX_UINT_128, - targetSupplyHeadroom, - allocatorHeadroom, - expectedSupplyAssets, - sourceMarket.getWithdrawToUtilization( - maxWithdrawalUtilization, - ), - ); - if (assets <= 0n) return; - - return { - vault: vaultAddress, - from: { - type: "market", - adapter: sourceAdapter.address, - marketParams: sourceMarket.params, - }, - to: { adapter: targetContext.adapter.address }, - assets, - penalty: publicAllocatorConfig.penalty, - } satisfies VaultV2BlueReallocation; - }, UnknownDataError); - if (candidate != null) rawCandidates.push(candidate); - } + ) + return; + + const expectedSupplyAssets = sourceMarket.toSupplyAssets( + sourceAdapter.supplyShares[sourceMarket.id] ?? 0n, + ); + const assets = MathLib.min( + MathLib.MAX_UINT_128, + targetSupplyHeadroom, + allocatorHeadroom, + expectedSupplyAssets, + sourceMarket.getWithdrawToUtilization( + maxWithdrawalUtilization, + ), + ); + if (assets <= 0n) return; + + return { + vault: vaultAddress, + from: { + type: "market", + adapter: sourceAdapter.address, + marketParams: sourceMarket.params, + }, + to: { adapter: targetContext.adapter.address }, + assets, + penalty: publicAllocatorConfig.penalty, + } satisfies VaultV2BlueReallocation; + }, UnknownDataError); + if (candidate != null) rawCandidates.push(candidate); } } + } - const capCompatibleCandidates: VaultV2BlueReallocation[] = []; - for (const reallocation of rawCandidates) { - // MorphoMarketV1AdapterV2 rejects supplies that mint fewer shares than assets. - if ( - targetMarket.toSupplyShares(reallocation.assets, "Down") < - reallocation.assets - ) - continue; - - // Cap fit is monotonic but not linear in assets: the amount changes - // penalty donations, firstTotalAssets, rounded shares, and possibly - // shared allocation IDs. Binary search finds the exact largest fit. - let lower = 0n; - let upper = reallocation.assets; - - while (lower < upper) { - const assets = (lower + upper + 1n) / 2n; - const postState = data.cloneWithPublicReallocation({ - reallocation: { ...reallocation, assets }, - targetMarketId: marketId, - timestamp: targetMarket.lastUpdate, - }); - const postVault = postState.getVault(reallocation.vault); - const postAdapter = postState.getAdapter( + const capCompatibleCandidates: VaultV2BlueReallocation[] = []; + for (const reallocation of rawCandidates) { + // MorphoMarketV1AdapterV2 rejects supplies that mint fewer shares than assets. + if ( + targetMarket.toSupplyShares(reallocation.assets, "Down") < + reallocation.assets + ) + continue; + + // Cap fit is monotonic but not linear in assets: the amount changes + // penalty donations, firstTotalAssets, rounded shares, and possibly + // shared allocation IDs. Binary search finds the exact largest fit. + let lower = 0n; + let upper = reallocation.assets; + const reallocationAdapter = data.getAdapter( + reallocation.vault, + reallocation.to.adapter, + ); + const targetIds = reallocationAdapter.ids(targetMarket.params); + + while (lower < upper) { + const assets = (lower + upper + 1n) / 2n; + const postState = data.cloneWithPublicReallocation({ + reallocation: { ...reallocation, assets }, + targetMarketId: marketId, + timestamp: targetMarket.lastUpdate, + }); + const postVault = postState.getVault(reallocation.vault); + // Vault V2 checks relative caps against the transient firstTotalAssets, + // which stays fixed after the vault's first allocation in a transaction. + const firstTotalAssets = + postState.firstTotalAssets[reallocation.vault] ?? + postVault._totalAssets; + const withinCaps = targetIds.every((id) => { + const allocation = postState.getAllocation( reallocation.vault, - reallocation.to.adapter, + id, ); - const targetIds = postAdapter.ids( - postState.getMarket(marketId).params, + const capacity = VaultV2Utils.allocationHeadroom( + { ...allocation, allocation: 0n }, + firstTotalAssets, + ).value; + return ( + allocation.absoluteCap > 0n && + allocation.allocation <= capacity ); - // Vault V2 checks relative caps against the transient firstTotalAssets, - // which stays fixed after the vault's first allocation in a transaction. - const firstTotalAssets = - postState.firstTotalAssets[reallocation.vault] ?? - postVault._totalAssets; - const withinCaps = targetIds.every((id) => { - const allocation = postState.getAllocation( - reallocation.vault, - id, - ); - const capacity = VaultV2Utils.allocationHeadroom( - { ...allocation, allocation: 0n }, - firstTotalAssets, - ).value; - return ( - allocation.absoluteCap > 0n && - allocation.allocation <= capacity - ); - }); - - if (withinCaps) lower = assets; - else upper = assets - 1n; - } + }); - if (lower > 0n) - capCompatibleCandidates.push({ - ...reallocation, - assets: lower, - }); + if (withinCaps) lower = assets; + else upper = assets - 1n; } - return capCompatibleCandidates.sort( - bigIntComparator(({ assets }) => assets, "desc"), - )[0]; - }, UnknownDataError); + if (lower > 0n) + capCompatibleCandidates.push({ + ...reallocation, + assets: lower, + }); + } + + return capCompatibleCandidates.sort( + bigIntComparator(({ assets }) => assets, "desc"), + )[0]; }) .filter( (candidate): candidate is VaultV2BlueReallocation => @@ -1064,7 +1095,8 @@ export class VaultV2BlueReallocationData * @returns Reallocatable market and idle assets, or `0n` when none are available. * @throws {NegativeInputError} when `maxWithdrawalUtilization` is negative. * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` exceeds WAD. - * @throws {UnknownReallocationMarketError} when the target market is absent. + * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent for a vault. * @example * ```ts * const liquidity = data.getPublicReallocationLiquidity(targetMarketId); @@ -1095,7 +1127,8 @@ export class VaultV2BlueReallocationData * @returns Borrowable assets while remaining at or below `utilization`. * @throws {NegativeInputError} when `maxWithdrawalUtilization` is negative. * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` exceeds WAD. - * @throws {UnknownReallocationMarketError} when the target market is absent. + * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent for a vault. * @example * ```ts * const liquidity = data.getAvailableLiquidityToUtilization(targetMarketId); @@ -1284,8 +1317,19 @@ export class VaultV2BlueReallocationData return data; } + /** Updates one canonical market and its dependent adapter views. */ private setMarket(market: Market) { - this.mutableMarkets[market.id] = market; + this.setMarkets([market]); + } + + /** Updates canonical markets in bulk and refreshes only dependent adapter views. */ + private setMarkets(markets: Iterable) { + const changedMarketIds = new Set(); + for (const market of markets) { + this.mutableMarkets[market.id] = market; + changedMarketIds.add(market.id); + } + if (changedMarketIds.size === 0) return; // A Morpho market is global state shared by every vault position. Legacy // AccrualPosition constructors copy their Market, so rebuild those adapter @@ -1298,14 +1342,28 @@ export class VaultV2BlueReallocationData for (const adapter of adapters) { if (adapter instanceof AccrualVaultV2MorphoMarketV1AdapterV2) { + if (!adapter.markets.some(({ id }) => changedMarketIds.has(id))) + continue; adapter.markets = adapter.markets.map((adapterMarket) => getCanonicalMarket(this.mutableMarkets, adapterMarket), ); } else if (adapter instanceof AccrualVaultV2MorphoMarketV1Adapter) { + if ( + !adapter.positions.some(({ marketId }) => + changedMarketIds.has(marketId), + ) + ) + continue; adapter.positions = adapter.positions.map((position) => clonePosition(position, this.mutableMarkets), ); } else if (adapter instanceof AccrualVaultV2MorphoVaultV1Adapter) { + if ( + ![...adapter.accrualVaultV1.allocations.keys()].some((marketId) => + changedMarketIds.has(marketId), + ) + ) + continue; adapter.accrualVaultV1 = cloneAccrualVault( adapter.accrualVaultV1, this.mutableMarkets, diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 024332b10..ad52311d1 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -1179,6 +1179,14 @@ export class UnknownReallocationPublicAllocatorConfigError extends UnknownDataEr } } +/** Thrown when Vault V2 reallocation state lacks the fetched active-adapter set. */ +export class UnknownReallocationActiveAdaptersError extends UnknownDataError { + /** @param vault - Vault V2 address with missing active-adapter state. */ + constructor(public readonly vault: Address) { + super(`unknown active adapters for reallocation vault "${vault}"`); + } +} + /** Thrown when Vault V2 reallocation state lacks an adapter-market allocator configuration. */ export class UnknownReallocationMarketPublicAllocatorConfigError extends UnknownDataError { /** diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index dfae6eb90..c5f6287cb 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -684,19 +684,25 @@ export default class MorphoProtocolEvm extends LendingProtocol { } /** - * Returns Morpho SDK requirements for a borrow. + * Returns Morpho SDK authorization requirements for a borrow without Vault V2 reallocations. * * @param options - The borrow options. - * @returns Token-approval and authorization requirements. Vault V2 public - * allocator reallocations can require a loan-token approval for their - * penalty donation. When offchain signatures are enabled - * (`supportSignature: true`), the authorization may instead be returned as - * a signable `RequirementSignatureRequest` to fold into the bundle via - * `setAuthorizationWithSig`. + * @returns Authorization requirements. When offchain signatures are enabled + * (`supportSignature: true`), the authorization may instead be returned as a signable + * `RequirementSignatureRequest` to fold into the bundle via `setAuthorizationWithSig`. */ public getBorrowRequirements( options: MorphoBorrowOptions, ): Promise<(RequirementAuthorization | RequirementSignatureRequest)[]>; + /** + * Returns Morpho SDK requirements for a borrow with Vault V2 reallocations. + * + * @param options - The Vault V2 reallocation borrow options. + * @returns Authorization requirements and any loan-token approval required for the public + * allocator penalty donation. When offchain signatures are enabled (`supportSignature: true`), + * the authorization may instead be returned as a signable `RequirementSignatureRequest` to + * fold into the bundle via `setAuthorizationWithSig`. + */ public getBorrowRequirements( options: MorphoBorrowWithVaultV2ReallocationsOptions, ): Promise< From bb9bd15a2332231880e657d874ca9b8f208fc487 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 21 Aug 2026 11:15:10 +0200 Subject: [PATCH 36/41] fix(morpho-sdk): validate allocator penalty limits --- .changeset/brave-vaults-reallocate.md | 2 +- .../blue/borrow.bluePublicAllocator.test.ts | 24 +++++++ packages/morpho-sdk/src/constants.ts | 2 + .../vaultV2BlueReallocationData.test.ts | 64 ++++++++++++++++--- .../entities/vaultV2BlueReallocationData.ts | 49 ++++++++++---- .../morpho-sdk/src/helpers/constant.test.ts | 7 ++ packages/morpho-sdk/src/helpers/constant.ts | 6 ++ packages/morpho-sdk/src/helpers/index.ts | 2 + .../morpho-sdk/src/helpers/validate.test.ts | 23 ++++++- packages/morpho-sdk/src/helpers/validate.ts | 10 ++- .../morpho-sdk/src/types/sharedLiquidity.ts | 4 +- 11 files changed, 164 insertions(+), 29 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 85bebede0..d8c9335a3 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -13,7 +13,7 @@ V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. -Compatibility note: this minor intentionally accepts three TypeScript-level breaking changes. `VaultV2MorphoMarketV1AdapterV2.ids()` now returns `readonly [Hash, Hash, Hash]` instead of mutable `Hash[]`; `MorphoBlue.withdraw`, `borrow`, and `refinance` may now return `Transaction` from `getRequirements()` for Vault V2 penalty funding; and `BlueWithdrawAction`, `BlueBorrowAction`, `BlueSupplyCollateralBorrowAction`, and `BlueRefinanceAction` now require `reallocationPenaltyAssets`. Runtime ordering for `ids()` is unchanged. Consumers should spread `ids()` when a mutable array is required, handle approval transactions in exhaustive requirement consumers, and set `reallocationPenaltyAssets: 0n` in handwritten V1 or no-penalty action descriptors. +Compatibility note: this minor intentionally accepts four breaking changes. `VaultV2MorphoMarketV1AdapterV2.ids()` now returns `readonly [Hash, Hash, Hash]` instead of mutable `Hash[]`; `MorphoBlue.withdraw`, `borrow`, and `refinance` may now return `Transaction` from `getRequirements()` for Vault V2 penalty funding; `BlueWithdrawAction`, `BlueBorrowAction`, `BlueSupplyCollateralBorrowAction`, and `BlueRefinanceAction` now require `reallocationPenaltyAssets`; and Vault V2 reallocation discovery now accepts only zero-penalty vaults by default. Runtime ordering for `ids()` is unchanged. Consumers should spread `ids()` when a mutable array is required, handle approval transactions in exhaustive requirement consumers, set `reallocationPenaltyAssets: 0n` in handwritten V1 or no-penalty action descriptors, and explicitly set `maxPenalty` when opting into a nonzero Vault V2 allocator penalty. Explicit and hand-built penalties remain supported up to WAD (100%), preserving the existing maximum. Name allocation-cap helpers `adapterCapId`, `collateralCapId`, and `adapterMarketCapId`. Preserve the published `adapterId`, `collateralId`, and `marketParamsId` helpers as deprecated aliases. diff --git a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts index d606b6dfb..dca75942d 100644 --- a/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -9,9 +9,11 @@ import { vaultV1PublicAllocatorAbi, vaultV2BluePublicAllocatorAbi, } from "../../abis.js"; +import { MAX_REALLOCATION_PENALTY } from "../../helpers/constant.js"; import { type BlueReallocationPlan, InconsistentReallocationPenaltyError, + InputExceedsMaxError, MixedReallocationVersionsError, type VaultV2BlueReallocation, } from "../../types/index.js"; @@ -192,6 +194,28 @@ describe("blueBorrow Blue Public Allocator", () => { ).toThrow(InconsistentReallocationPenaltyError); }); + test("error: InputExceedsMaxError for a penalty above WAD", () => { + expect(() => + blueBorrow({ + market: { chainId: ChainId.EthMainnet, marketParams: targetMarket }, + args: { + amount: 1n, + minSharePrice: 0n, + receiver, + reallocations: [ + { + vault: vaultV2, + from: { type: "idle" }, + to: { adapter: targetAdapter }, + assets: 1n, + penalty: MAX_REALLOCATION_PENALTY + 1n, + }, + ], + }, + }), + ).toThrow(InputExceedsMaxError); + }); + test("re-exports the canonical ABI", () => { expect(vaultV2BluePublicAllocatorAbi).toBe( canonicalVaultV2BluePublicAllocatorAbi, diff --git a/packages/morpho-sdk/src/constants.ts b/packages/morpho-sdk/src/constants.ts index 4351c39c6..6cf5e0fa3 100644 --- a/packages/morpho-sdk/src/constants.ts +++ b/packages/morpho-sdk/src/constants.ts @@ -39,9 +39,11 @@ export { export { APPROVE_ONLY_ONCE_TOKENS, DEFAULT_LLTV_BUFFER, + DEFAULT_MAX_REALLOCATION_PENALTY, DEFAULT_SUPPLY_TARGET_UTILIZATION, DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, MAX_ABSOLUTE_SHARE_PRICE, + MAX_REALLOCATION_PENALTY, MAX_SLIPPAGE_TOLERANCE, MAX_TOKEN_APPROVALS, } from "./helpers/constant.js"; diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index 4e8775891..624dbcfd0 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -20,6 +20,7 @@ import type { Address, Hash } from "viem"; import { zeroAddress, zeroHash } from "viem"; import { describe, expect, test, vi } from "vitest"; import { blueBorrow } from "../actions/index.js"; +import { MAX_REALLOCATION_PENALTY } from "../helpers/constant.js"; import { InputExceedsMaxError, InsufficientSharedLiquidityError, @@ -141,7 +142,7 @@ const makeFixture = ({ canPullFromIdle = true, canPullFromMarket = true, allocatorActiveAdapters, - penalty = 7n, + penalty = 0n, sourceLastUpdate = TIMESTAMP, targetLastUpdate = TIMESTAMP, vaultLastUpdate = TIMESTAMP, @@ -441,7 +442,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }, to: { adapter: TARGET_ADAPTER }, assets: sourceExpectedAssets, - penalty: 7n, + penalty: 0n, }, ]); expect(result.data).not.toBe(data); @@ -578,6 +579,17 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }, ); + test.each([0n, MAX_REALLOCATION_PENALTY])( + "behavior: accepts maxPenalty boundary %s", + (maxPenalty) => { + const { data } = makeFixture(); + + expect(() => + data.computeVaultV2BlueReallocations(targetParams.id, { maxPenalty }), + ).not.toThrow(); + }, + ); + test.each([ { maxWithdrawalUtilization: -1n, @@ -600,6 +612,20 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }, ); + test.each([ + { maxPenalty: -1n, ErrorClass: NegativeInputError }, + { + maxPenalty: MAX_REALLOCATION_PENALTY + 1n, + ErrorClass: InputExceedsMaxError, + }, + ])("error: rejects maxPenalty $maxPenalty", ({ maxPenalty, ErrorClass }) => { + const { data } = makeFixture(); + + expect(() => + data.computeVaultV2BlueReallocations(targetParams.id, { maxPenalty }), + ).toThrow(ErrorClass); + }); + test("behavior: skips targets whose Morpho supply would mint fewer shares than assets", () => { const { data } = makeFixture({ targetSupply: 2_000_000n, @@ -970,10 +996,10 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { penalty, })), ).toStrictEqual([ - { from: "market", assets: sourceExpectedAssets, penalty: 7n }, - { from: "idle", assets: 300n, penalty: 7n }, + { from: "market", assets: sourceExpectedAssets, penalty: 0n }, + { from: "idle", assets: 300n, penalty: 0n }, ]); - expect(result.data.getVault(VAULT).assetBalance).toBe(2n); + expect(result.data.getVault(VAULT).assetBalance).toBe(0n); }); test("behavior: excludes the target market through a different adapter", () => { @@ -994,7 +1020,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { const result = data.computeVaultV2BlueReallocations(targetParams.id); expect(result.reallocations[0]?.assets).toBe(sourceExpectedAssets); - expect(result.data.getVault(VAULT).assetBalance).toBe(1n); + expect(result.data.getVault(VAULT).assetBalance).toBe(0n); }); test("behavior: target untracked interest consumes allocator headroom", () => { @@ -1183,7 +1209,9 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ], }); - const result = data.computeVaultV2BlueReallocations(targetParams.id); + const result = data.computeVaultV2BlueReallocations(targetParams.id, { + maxPenalty: MathLib.WAD, + }); expect(result.reallocations[0]?.assets).toBe(500n); expect(result.data.getVault(VAULT)._totalAssets).toBe(1_000n); @@ -1240,6 +1268,9 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { penalty: 8n, }); + expect( + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, + ).toStrictEqual([]); expect( data.computeVaultV2BlueReallocations(targetParams.id, { maxPenalty: 7n, @@ -1372,10 +1403,14 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations operation" targetSupply: 100n, targetBorrow: 100n, idle: 300n, + penalty: 7n, }); const { reallocations } = data.computeVaultV2BlueReallocations( targetParams.id, - { operation: { type: "borrow", amount: 1_100n } }, + { + maxPenalty: 7n, + operation: { type: "borrow", amount: 1_100n }, + }, ); const tx = blueBorrow({ @@ -1505,4 +1540,17 @@ describe("VaultV2BlueReallocationData liquidity metrics", () => { }), ).toBe(100n); }); + + test("error: NegativeInputError for a negative maximum penalty", () => { + const { data } = makeFixture(); + + expect(() => + data.getPublicReallocationLiquidity(targetParams.id, { maxPenalty: -1n }), + ).toThrow(NegativeInputError); + expect(() => + data.getAvailableLiquidityToUtilization(targetParams.id, MathLib.WAD, { + maxPenalty: -1n, + }), + ).toThrow(NegativeInputError); + }); }); diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index c395674a7..77f3ef125 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -23,8 +23,10 @@ import { import { _try, bigIntComparator } from "@morpho-org/morpho-ts"; import { type Address, type Hash, isAddressEqual } from "viem"; import { + DEFAULT_MAX_REALLOCATION_PENALTY, DEFAULT_SUPPLY_TARGET_UTILIZATION, DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, + MAX_REALLOCATION_PENALTY, } from "../helpers/constant.js"; import type { VaultV2BluePublicAllocatorOptions, @@ -139,6 +141,18 @@ const resolveMaxWithdrawalUtilization = (value: bigint | undefined) => { return utilization; }; +const resolveMaxPenalty = (value: bigint | undefined) => { + const penalty = value ?? DEFAULT_MAX_REALLOCATION_PENALTY; + if (penalty < 0n) throw new NegativeInputError("maxPenalty", penalty); + if (penalty > MAX_REALLOCATION_PENALTY) + throw new InputExceedsMaxError({ + field: "maxPenalty", + value: penalty, + max: MAX_REALLOCATION_PENALTY, + }); + return penalty; +}; + const getCanonicalMarket = ( markets: Record, market: Market, @@ -609,13 +623,14 @@ export class VaultV2BlueReallocationData * friendly liquidity cannot cover the absolute shortfall. Friendly source * utilization defaults to 90% and is configurable through * `options.maxWithdrawalUtilization`. Vaults whose configured penalty exceeds - * `options.maxPenalty` are ignored. + * `options.maxPenalty` are ignored. By default, only zero-penalty vaults are + * considered. * * @param marketId - Target Blue market id. * @param options - Optional discovery controls and operation to support. * @returns Flat action-ready reallocations and their post-simulation state. - * @throws {NegativeInputError} when `maxWithdrawalUtilization` is negative. - * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` exceeds WAD. + * @throws {NegativeInputError} when `maxWithdrawalUtilization` or `maxPenalty` is negative. + * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` or `maxPenalty` exceeds WAD. * @throws {NonPositiveInputError} when the operation amount is not positive and planning is enabled. * @throws {UnknownReallocationMarketError} when a required market is absent. * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent for a vault. @@ -652,12 +667,14 @@ export class VaultV2BlueReallocationData const maxWithdrawalUtilization = resolveMaxWithdrawalUtilization( options.maxWithdrawalUtilization, ); + const maxPenalty = resolveMaxPenalty(options.maxPenalty); + const resolvedOptions = { ...options, maxPenalty }; const operation = options.operation; if (operation == null) return this.computeVaultV2BlueReallocationsAtUtilization({ marketId, maxWithdrawalUtilization, - options, + options: resolvedOptions, }); const { amount, type } = operation; @@ -668,7 +685,7 @@ export class VaultV2BlueReallocationData ? this.getLatestSnapshotTimestamp() : BigInt(options.timestamp); const normalizedOptions: VaultV2BluePublicAllocatorOptions = { - ...options, + ...resolvedOptions, timestamp, reallocatableVaults: options.reallocatableVaults == null @@ -833,8 +850,8 @@ export class VaultV2BlueReallocationData const { vault, publicAllocatorConfig } = vaultContext; if ( !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) || - (options.maxPenalty != null && - publicAllocatorConfig.penalty > options.maxPenalty) + publicAllocatorConfig.penalty > + (options.maxPenalty ?? DEFAULT_MAX_REALLOCATION_PENALTY) ) return; const activeAdapters = data.getActiveAdapters(vaultAddress); @@ -1093,8 +1110,8 @@ export class VaultV2BlueReallocationData * @param marketId - Target Blue market id. * @param options - Optional timestamp, enable flag, vault allowlist, source utilization ceiling, and maximum penalty. * @returns Reallocatable market and idle assets, or `0n` when none are available. - * @throws {NegativeInputError} when `maxWithdrawalUtilization` is negative. - * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` exceeds WAD. + * @throws {NegativeInputError} when `maxWithdrawalUtilization` or `maxPenalty` is negative. + * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` or `maxPenalty` exceeds WAD. * @throws {UnknownReallocationMarketError} when a required market is absent. * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent for a vault. * @example @@ -1108,12 +1125,14 @@ export class VaultV2BlueReallocationData ) { if (options?.enabled === false) return 0n; + const maxPenalty = resolveMaxPenalty(options?.maxPenalty); + return this.computeVaultV2BlueReallocationsAtUtilization({ marketId, maxWithdrawalUtilization: resolveMaxWithdrawalUtilization( options?.maxWithdrawalUtilization, ), - options, + options: { ...options, maxPenalty }, }).reallocations.reduce((total, { assets }) => total + assets, 0n); } @@ -1125,8 +1144,8 @@ export class VaultV2BlueReallocationData * @param utilization - Desired utilization, scaled by WAD. Defaults to 90%. * @param options - Optional timestamp, enable flag, vault allowlist, source utilization ceiling, and maximum penalty. * @returns Borrowable assets while remaining at or below `utilization`. - * @throws {NegativeInputError} when `maxWithdrawalUtilization` is negative. - * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` exceeds WAD. + * @throws {NegativeInputError} when `maxWithdrawalUtilization` or `maxPenalty` is negative. + * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` or `maxPenalty` exceeds WAD. * @throws {UnknownReallocationMarketError} when a required market is absent. * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent for a vault. * @example @@ -1144,6 +1163,10 @@ export class VaultV2BlueReallocationData options?.enabled === false ? DEFAULT_WITHDRAWAL_TARGET_UTILIZATION : resolveMaxWithdrawalUtilization(options?.maxWithdrawalUtilization); + const maxPenalty = + options?.enabled === false + ? DEFAULT_MAX_REALLOCATION_PENALTY + : resolveMaxPenalty(options?.maxPenalty); const timestamp = options?.timestamp == null ? this.getLatestSnapshotTimestamp() @@ -1156,7 +1179,7 @@ export class VaultV2BlueReallocationData this.computeVaultV2BlueReallocationsAtUtilization({ marketId, maxWithdrawalUtilization, - options: { ...options, timestamp }, + options: { ...options, timestamp, maxPenalty }, }).reallocations.reduce((total, { assets }) => total + assets, 0n); return MarketUtils.getBorrowToUtilization( { diff --git a/packages/morpho-sdk/src/helpers/constant.test.ts b/packages/morpho-sdk/src/helpers/constant.test.ts index 053bac37a..eae84baaa 100644 --- a/packages/morpho-sdk/src/helpers/constant.test.ts +++ b/packages/morpho-sdk/src/helpers/constant.test.ts @@ -2,7 +2,9 @@ import { MathLib } from "@morpho-org/blue-sdk"; import { describe, expect, test } from "vitest"; import { DEFAULT_LLTV_BUFFER, + DEFAULT_MAX_REALLOCATION_PENALTY, MAX_ABSOLUTE_SHARE_PRICE, + MAX_REALLOCATION_PENALTY, MAX_SLIPPAGE_TOLERANCE, } from "./constant.js"; @@ -21,6 +23,11 @@ describe("morpho-sdk helper constants", () => { expect(MAX_ABSOLUTE_SHARE_PRICE).toBe(100n * MathLib.RAY); }); + test("Vault V2 reallocation penalties default to zero and cap at WAD", () => { + expect(DEFAULT_MAX_REALLOCATION_PENALTY).toBe(0n); + expect(MAX_REALLOCATION_PENALTY).toBe(MathLib.WAD); + }); + test("constants are positive bigints", () => { expect(typeof MAX_SLIPPAGE_TOLERANCE).toBe("bigint"); expect(typeof DEFAULT_LLTV_BUFFER).toBe("bigint"); diff --git a/packages/morpho-sdk/src/helpers/constant.ts b/packages/morpho-sdk/src/helpers/constant.ts index ac2089a92..c0bf35b0e 100644 --- a/packages/morpho-sdk/src/helpers/constant.ts +++ b/packages/morpho-sdk/src/helpers/constant.ts @@ -4,6 +4,12 @@ import { type Address, maxUint96 } from "viem"; /** Maximum slippage tolerance: 10% */ export const MAX_SLIPPAGE_TOLERANCE = MathLib.WAD / 10n; +/** Default maximum Vault V2 reallocation penalty: zero. */ +export const DEFAULT_MAX_REALLOCATION_PENALTY = 0n; + +/** Maximum Vault V2 reallocation penalty: 100%. */ +export const MAX_REALLOCATION_PENALTY = MathLib.WAD; + /** Default LLTV buffer: 0.5% below LLTV. Prevents instant liquidation on new positions. */ export const DEFAULT_LLTV_BUFFER = MathLib.WAD / 200n; diff --git a/packages/morpho-sdk/src/helpers/index.ts b/packages/morpho-sdk/src/helpers/index.ts index 668e8aa44..6a723be2b 100644 --- a/packages/morpho-sdk/src/helpers/index.ts +++ b/packages/morpho-sdk/src/helpers/index.ts @@ -5,9 +5,11 @@ export { export { APPROVE_ONLY_ONCE_TOKENS, DEFAULT_LLTV_BUFFER, + DEFAULT_MAX_REALLOCATION_PENALTY, DEFAULT_SUPPLY_TARGET_UTILIZATION, DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, MAX_ABSOLUTE_SHARE_PRICE, + MAX_REALLOCATION_PENALTY, MAX_SLIPPAGE_TOLERANCE, MAX_TOKEN_APPROVALS, } from "./constant.js"; diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index 4168d54d2..c0ce7dc8b 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -46,7 +46,10 @@ import { WithdrawMakesPositionUnhealthyError, WithdrawSharesExceedSupplyError, } from "../types/index.js"; -import { MAX_SLIPPAGE_TOLERANCE } from "./constant.js"; +import { + MAX_REALLOCATION_PENALTY, + MAX_SLIPPAGE_TOLERANCE, +} from "./constant.js"; import { validateAccrualPosition, validateAndNormalizeReallocations, @@ -596,10 +599,10 @@ describe("reallocation validation", () => { ErrorClass: NegativeInputError, }, { - name: "penalty above WAD", + name: "penalty above the SDK maximum", reallocation: { ...validBluePublicAllocatorReallocation, - penalty: MathLib.WAD + 1n, + penalty: MAX_REALLOCATION_PENALTY + 1n, }, ErrorClass: InputExceedsMaxError, }, @@ -656,6 +659,20 @@ describe("reallocation validation", () => { ).not.toThrow(); }); + test("behavior: accepts the maximum reallocation penalty", () => { + expect(() => + validateVaultV2BlueReallocations( + [ + { + ...validBluePublicAllocatorReallocation, + penalty: MAX_REALLOCATION_PENALTY, + }, + ], + targetMarketId, + ), + ).not.toThrow(); + }); + test("behavior: allows different penalties for different vaults", () => { expect(() => validateVaultV2BlueReallocations( diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index c8eb519bf..b92980a19 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -41,7 +41,11 @@ import { WithdrawMakesPositionUnhealthyError, WithdrawSharesExceedSupplyError, } from "../types/index.js"; -import { DEFAULT_LLTV_BUFFER, MAX_SLIPPAGE_TOLERANCE } from "./constant.js"; +import { + DEFAULT_LLTV_BUFFER, + MAX_REALLOCATION_PENALTY, + MAX_SLIPPAGE_TOLERANCE, +} from "./constant.js"; /** @internal */ export const compareMarketIds = (idA: MarketId, idB: MarketId) => { @@ -443,11 +447,11 @@ export const validateVaultV2BlueReallocations = ( reallocation.penalty, ); } - if (reallocation.penalty > MathLib.WAD) { + if (reallocation.penalty > MAX_REALLOCATION_PENALTY) { throw new InputExceedsMaxError({ field: "reallocation.penalty", value: reallocation.penalty, - max: MathLib.WAD, + max: MAX_REALLOCATION_PENALTY, }); } if (reallocation.assets <= 0n) { diff --git a/packages/morpho-sdk/src/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index a3547ba6e..9b391a7e3 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -67,7 +67,9 @@ export interface VaultV2BluePublicAllocatorOptions { /** * Maximum proportional vault-asset penalty accepted for each * BluePublicAllocator call, scaled by WAD. Vaults with a higher configured - * penalty are ignored. Defaults to no limit. + * penalty are ignored. Must not exceed WAD (100%). + * + * @default 0n */ readonly maxPenalty?: bigint; } From 9037309b2708b0d33c0df36ea3c0d66009bdf1f3 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 21 Aug 2026 14:15:21 +0200 Subject: [PATCH 37/41] test(morpho-sdk): opt into allocator penalty --- .../src/actions/blue/vaultV2Reallocations.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts index 4058f2dba..7ed5b82d6 100644 --- a/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts +++ b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts @@ -251,7 +251,7 @@ describe("Blue actions with Vault V2 reallocations", () => { ).not.toContain(targetMarket.id); const discovery = reallocationData.computeVaultV2BlueReallocations( targetMarket.id, - { timestamp: block.timestamp }, + { timestamp: block.timestamp, maxPenalty: penalty }, ); expect(discovery.reallocations.length).toBeGreaterThan(0); expect(discovery.data.getAdapter(vault, targetAdapter).marketIds).toContain( From a8cd070022c528c0a571043d262c0f2062ca3043 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 21 Aug 2026 16:45:13 +0200 Subject: [PATCH 38/41] fix: clarify Vault V2 allocation IDs --- .changeset/brave-vaults-reallocate.md | 2 +- ...ePublicAllocatorConfig.integration.test.ts | 2 +- .../VaultV2BluePublicAllocatorConfig.test.ts | 2 +- .../VaultV2BluePublicAllocatorConfig.ts | 3 +- .../src/fetch/vault-v2/vault-v2.test.ts | 3 +- .../blue-sdk/src/vault/v2/VaultV2.test.ts | 9 ++- packages/blue-sdk/src/vault/v2/VaultV2.ts | 4 +- .../blue-sdk/src/vault/v2/VaultV2Adapter.ts | 4 +- .../vault/v2/VaultV2MorphoMarketV1Adapter.ts | 35 ++++++++- .../v2/VaultV2MorphoMarketV1AdapterV2.ts | 45 +++++++++--- .../vault/v2/VaultV2MorphoVaultV1Adapter.ts | 22 +++++- .../vaultV2BlueReallocationData.test.ts | 72 +++++++++++-------- .../entities/vaultV2BlueReallocationData.ts | 47 ++++++++---- 13 files changed, 185 insertions(+), 65 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index d8c9335a3..b5e0e0595 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -13,7 +13,7 @@ V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. -Compatibility note: this minor intentionally accepts four breaking changes. `VaultV2MorphoMarketV1AdapterV2.ids()` now returns `readonly [Hash, Hash, Hash]` instead of mutable `Hash[]`; `MorphoBlue.withdraw`, `borrow`, and `refinance` may now return `Transaction` from `getRequirements()` for Vault V2 penalty funding; `BlueWithdrawAction`, `BlueBorrowAction`, `BlueSupplyCollateralBorrowAction`, and `BlueRefinanceAction` now require `reallocationPenaltyAssets`; and Vault V2 reallocation discovery now accepts only zero-penalty vaults by default. Runtime ordering for `ids()` is unchanged. Consumers should spread `ids()` when a mutable array is required, handle approval transactions in exhaustive requirement consumers, set `reallocationPenaltyAssets: 0n` in handwritten V1 or no-penalty action descriptors, and explicitly set `maxPenalty` when opting into a nonzero Vault V2 allocator penalty. Explicit and hand-built penalties remain supported up to WAD (100%), preserving the existing maximum. +Compatibility note: this minor intentionally accepts four breaking changes. `VaultV2MorphoMarketV1Adapter.ids()` and `VaultV2MorphoMarketV1AdapterV2.ids()` now return the labeled readonly tuple `readonly [adapterCapId: Hash, collateralCapId: Hash, adapterMarketCapId: Hash]` instead of mutable `Hash[]`, while `VaultV2MorphoVaultV1Adapter.ids()` now returns `readonly [adapterCapId: Hash]`; `MorphoBlue.withdraw`, `borrow`, and `refinance` may now return `Transaction` from `getRequirements()` for Vault V2 penalty funding; `BlueWithdrawAction`, `BlueBorrowAction`, `BlueSupplyCollateralBorrowAction`, and `BlueRefinanceAction` now require `reallocationPenaltyAssets`; and Vault V2 reallocation discovery now accepts only zero-penalty vaults by default. Runtime ordering for `ids()` is unchanged. Consumers should spread `ids()` when a mutable array is required, handle approval transactions in exhaustive requirement consumers, set `reallocationPenaltyAssets: 0n` in handwritten V1 or no-penalty action descriptors, and explicitly set `maxPenalty` when opting into a nonzero Vault V2 allocator penalty. Explicit and hand-built penalties remain supported up to WAD (100%), preserving the existing maximum. Name allocation-cap helpers `adapterCapId`, `collateralCapId`, and `adapterMarketCapId`. Preserve the published `adapterId`, `collateralId`, and `marketParamsId` helpers as deprecated aliases. diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts index c25fed2c3..6d9227496 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.integration.test.ts @@ -72,7 +72,7 @@ describe("Vault V2 BluePublicAllocator fetchers on fork", () => { functionName: "setIsAllocator", args: [allocator, true], }); - const forkAdapterMarketCapId = forkAdapter.ids(forkMarket.params)[2]; + const [, , forkAdapterMarketCapId] = forkAdapter.ids(forkMarket.params); await client.writeContract({ account: allocatorAccount, address: allocator, diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts index 661f91e6a..aafabe4ff 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.test.ts @@ -104,7 +104,7 @@ const unallocatedTargetVault = new AccrualVaultV2( { ...vault.forceDeallocatePenalties }, ); const ids = adapter.ids(marketParams); -const adapterMarketCapId = ids[2]; +const [, , adapterMarketCapId] = ids; const expected = { publicAllocatorConfig: new VaultV2BluePublicAllocatorConfig({ diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts index 74314534d..f271272f7 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2BluePublicAllocatorConfig.ts @@ -147,9 +147,10 @@ export async function fetchVaultV2BluePublicAllocatorData( for (const marketParams of marketParamsList) { const ids = adapter.ids(marketParams); + const [, , adapterMarketCapId] = ids; marketRequests.push({ adapter: adapter.address, - adapterMarketCapId: ids[2], + adapterMarketCapId, }); for (const id of ids) allocationIds.add(id); } diff --git a/packages/blue-sdk-viem/src/fetch/vault-v2/vault-v2.test.ts b/packages/blue-sdk-viem/src/fetch/vault-v2/vault-v2.test.ts index 3613ec1d7..405a7e0ed 100644 --- a/packages/blue-sdk-viem/src/fetch/vault-v2/vault-v2.test.ts +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/vault-v2.test.ts @@ -252,6 +252,7 @@ function mockVaultV2AllocationReads( handle: ReturnType, ids: readonly `0x${string}`[], ) { + const [adapterCapId] = ids; for (const id of ids) { mockRead(handle, { address: VAULT, @@ -269,7 +270,7 @@ function mockVaultV2AllocationReads( address: VAULT, abi: vaultV2Abi, functionName: "allocation", - result: id === ids[0] ? 100n : 0n, + result: id === adapterCapId ? 100n : 0n, }); } } diff --git a/packages/blue-sdk/src/vault/v2/VaultV2.test.ts b/packages/blue-sdk/src/vault/v2/VaultV2.test.ts index f032ac2af..544798377 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2.test.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2.test.ts @@ -436,7 +436,14 @@ describe("VaultV2MorphoMarketV1AdapterV2", () => { expect(adapter.marketIds).toStrictEqual([m.id]); expect(adapter.adaptiveCurveIrm).toBe(ADAPTER); expect(adapter.supplyShares[m.id]).toBe(123n); - expect(adapter.ids(m.params)[0]).toBe(adapter.adapterId); + expect(adapter.ids(m.params)).toStrictEqual([ + adapter.adapterId, + VaultV2MorphoMarketV1AdapterV2.collateralCapId(m.params.collateralToken), + VaultV2MorphoMarketV1AdapterV2.adapterMarketCapId( + adapter.address, + m.params, + ), + ]); expect( VaultV2MorphoMarketV1AdapterV2.marketParamsId(adapter.address, m.params), ).toBe( diff --git a/packages/blue-sdk/src/vault/v2/VaultV2.ts b/packages/blue-sdk/src/vault/v2/VaultV2.ts index baf68d71b..41df24465 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2.ts @@ -1,8 +1,8 @@ -import { type Address, type Hash, type Hex, zeroAddress } from "viem"; +import { type Address, type Hex, zeroAddress } from "viem"; import { VaultV2Errors } from "../../errors.js"; import { MathLib, type RoundingDirection } from "../../math/index.js"; import { type IToken, WrappedToken } from "../../token/index.js"; -import type { BigIntish } from "../../types.js"; +import type { BigIntish, Hash } from "../../types.js"; import { type CapacityLimit, CapacityLimitReason } from "../../utils.js"; import type { IAccrualVaultV2Adapter } from "./VaultV2Adapter.js"; import { VaultV2Utils } from "./VaultV2Utils.js"; diff --git a/packages/blue-sdk/src/vault/v2/VaultV2Adapter.ts b/packages/blue-sdk/src/vault/v2/VaultV2Adapter.ts index e701bb5e8..20cc7daeb 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2Adapter.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2Adapter.ts @@ -1,5 +1,5 @@ -import type { Address, Hash, Hex } from "viem"; -import type { BigIntish } from "../../types.js"; +import type { Address, Hex } from "viem"; +import type { BigIntish, Hash } from "../../types.js"; import type { CapacityLimit } from "../../utils.js"; /** Plain input shape for a Morpho Vault V2 adapter. */ diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts index 44950d3a2..109344163 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts @@ -5,7 +5,7 @@ import { marketParamsAbi, } from "../../market/index.js"; import type { AccrualPosition } from "../../position/index.js"; -import type { BigIntish } from "../../types.js"; +import type { BigIntish, Hash } from "../../types.js"; import { CapacityLimitReason } from "../../utils.js"; import type { IAccrualVaultV2Adapter, @@ -141,7 +141,38 @@ export class VaultV2MorphoMarketV1Adapter ); } - public ids(params: MarketParams) { + /** + * Returns this adapter's allocation-cap ids for a Morpho Blue market. + * + * @param params - Morpho Blue market parameters. + * @returns A readonly tuple containing the adapter, collateral, and adapter-market + * allocation-cap ids, in that order. + * @example + * ```ts + * import { + * MarketParams, + * VaultV2MorphoMarketV1Adapter, + * } from "@morpho-org/blue-sdk"; + * import { ZERO_ADDRESS } from "@morpho-org/morpho-ts"; + * + * const marketParams = MarketParams.idle(ZERO_ADDRESS); + * const adapter = new VaultV2MorphoMarketV1Adapter({ + * address: ZERO_ADDRESS, + * parentVault: ZERO_ADDRESS, + * skimRecipient: ZERO_ADDRESS, + * marketParamsList: [marketParams], + * }); + * const [adapterCapId, collateralCapId, adapterMarketCapId] = + * adapter.ids(marketParams); + * ``` + */ + public ids( + params: MarketParams, + ): readonly [ + adapterCapId: Hash, + collateralCapId: Hash, + adapterMarketCapId: Hash, + ] { return [ this.adapterId, VaultV2MorphoMarketV1Adapter.collateralCapId(params.collateralToken), diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts index 0103bebd6..a02d3145f 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1AdapterV2.ts @@ -1,16 +1,10 @@ -import { - type Address, - encodeAbiParameters, - type Hash, - type Hex, - keccak256, -} from "viem"; +import { type Address, encodeAbiParameters, type Hex, keccak256 } from "viem"; import { type Market, MarketParams, marketParamsAbi, } from "../../market/index.js"; -import type { BigIntish, MarketId } from "../../types.js"; +import type { BigIntish, Hash, MarketId } from "../../types.js"; import { CapacityLimitReason } from "../../utils.js"; import type { IAccrualVaultV2Adapter, @@ -152,7 +146,40 @@ export class VaultV2MorphoMarketV1AdapterV2 this.supplyShares = supplyShares; } - public ids(params: MarketParams): readonly [Hash, Hash, Hash] { + /** + * Returns this adapter's allocation-cap ids for a Morpho Blue market. + * + * @param params - Morpho Blue market parameters. + * @returns A readonly tuple containing the adapter, collateral, and adapter-market + * allocation-cap ids, in that order. + * @example + * ```ts + * import { + * MarketParams, + * VaultV2MorphoMarketV1AdapterV2, + * } from "@morpho-org/blue-sdk"; + * import { ZERO_ADDRESS } from "@morpho-org/morpho-ts"; + * + * const marketParams = MarketParams.idle(ZERO_ADDRESS); + * const adapter = new VaultV2MorphoMarketV1AdapterV2({ + * address: ZERO_ADDRESS, + * parentVault: ZERO_ADDRESS, + * skimRecipient: ZERO_ADDRESS, + * marketIds: [], + * adaptiveCurveIrm: ZERO_ADDRESS, + * supplyShares: {}, + * }); + * const [adapterCapId, collateralCapId, adapterMarketCapId] = + * adapter.ids(marketParams); + * ``` + */ + public ids( + params: MarketParams, + ): readonly [ + adapterCapId: Hash, + collateralCapId: Hash, + adapterMarketCapId: Hash, + ] { return [ this.adapterId, VaultV2MorphoMarketV1AdapterV2.collateralCapId(params.collateralToken), diff --git a/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts b/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts index 663ac2edb..030cc2d9f 100644 --- a/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts +++ b/packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts @@ -9,7 +9,7 @@ export interface IVaultV2MorphoVaultV1Adapter morphoVaultV1: Address; } -import type { BigIntish } from "../../types.js"; +import type { BigIntish, Hash } from "../../types.js"; import type { AccrualVault } from "../Vault.js"; import type { IAccrualVaultV2Adapter, @@ -70,7 +70,25 @@ export class VaultV2MorphoVaultV1Adapter this.morphoVaultV1 = morphoVaultV1; } - public ids() { + /** + * Returns this adapter's allocation-cap ids. + * + * @returns A readonly tuple containing the adapter-wide allocation-cap id. + * @example + * ```ts + * import { VaultV2MorphoVaultV1Adapter } from "@morpho-org/blue-sdk"; + * import { ZERO_ADDRESS } from "@morpho-org/morpho-ts"; + * + * const adapter = new VaultV2MorphoVaultV1Adapter({ + * address: ZERO_ADDRESS, + * parentVault: ZERO_ADDRESS, + * skimRecipient: ZERO_ADDRESS, + * morphoVaultV1: ZERO_ADDRESS, + * }); + * const [adapterCapId] = adapter.ids(); + * ``` + */ + public ids(): readonly [adapterCapId: Hash] { return [this.adapterId]; } } diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index 624dbcfd0..31f920153 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -197,7 +197,9 @@ const makeFixture = ({ [sourceMarket], ); const targetIds = targetAdapter.ids(targetMarket.params); + const [, , targetAdapterMarketCapId] = targetIds; const sourceIds = sourceAdapter.ids(sourceMarket.params); + const [sourceAdapterCapId, , sourceAdapterMarketCapId] = sourceIds; const allocations: Record = {}; const addAllocation = ({ @@ -310,27 +312,28 @@ const makeFixture = ({ }, marketPublicAllocatorConfigs: { [VAULT]: { - [targetIds[2]]: { + [targetAdapterMarketCapId]: { vault: VAULT, adapter: TARGET_ADAPTER, - adapterMarketCapId: targetIds[2], + adapterMarketCapId: targetAdapterMarketCapId, absoluteCap: allocatorTargetCap, canPullFromMarket: false, }, - [sourceIds[2]]: { + [sourceAdapterMarketCapId]: { vault: VAULT, adapter: sourceAdapterAddress, - adapterMarketCapId: sourceIds[2], + adapterMarketCapId: sourceAdapterMarketCapId, absoluteCap: 0n, canPullFromMarket, }, }, }, }), + sourceAdapterCapId, + sourceAdapterMarketCapId, sourceExpectedAssets, - sourceIds, + targetAdapterMarketCapId, targetExpectedAssets, - targetIds, }; }; @@ -425,7 +428,12 @@ describe("VaultV2BlueReallocationData accessors", () => { describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { test("default: returns an action-ready market reallocation and cloned post-state", () => { - const { data, sourceExpectedAssets, sourceIds, targetIds } = makeFixture(); + const { + data, + sourceAdapterMarketCapId, + sourceExpectedAssets, + targetAdapterMarketCapId, + } = makeFixture(); expect(data.getActiveAdapters(VAULT)).toStrictEqual( new Set([TARGET_ADAPTER.toLowerCase(), SOURCE_ADAPTER.toLowerCase()]), @@ -446,10 +454,12 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }, ]); expect(result.data).not.toBe(data); - expect(result.data.getAllocation(VAULT, sourceIds[2]).allocation).toBe(0n); - expect(result.data.getAllocation(VAULT, targetIds[2]).allocation).toBe( - sourceExpectedAssets, - ); + expect( + result.data.getAllocation(VAULT, sourceAdapterMarketCapId).allocation, + ).toBe(0n); + expect( + result.data.getAllocation(VAULT, targetAdapterMarketCapId).allocation, + ).toBe(sourceExpectedAssets); expect(result.data.getVault(VAULT)._totalAssets).toBe( data.getVault(VAULT)._totalAssets, ); @@ -482,10 +492,10 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }); test("error: ReallocationAllocationUnderflowError", () => { - const { data, sourceIds } = makeFixture(); - const allocation = data.getAllocation(VAULT, sourceIds[0]); + const { data, sourceAdapterCapId } = makeFixture(); + const allocation = data.getAllocation(VAULT, sourceAdapterCapId); (data.allocations[VAULT] as Record)[ - sourceIds[0] + sourceAdapterCapId ] = { ...allocation, allocation: 0n }; expect(() => @@ -509,9 +519,10 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }); test("behavior: allocates into a configured target with no existing position", () => { - const { data, sourceExpectedAssets, targetIds } = makeFixture({ - targetTracked: false, - }); + const { data, sourceExpectedAssets, targetAdapterMarketCapId } = + makeFixture({ + targetTracked: false, + }); expect(data.getAdapter(VAULT, TARGET_ADAPTER).marketIds).not.toContain( targetParams.id, @@ -526,9 +537,9 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { targetParams.id, ); expect(targetAdapter.supplyShares[targetParams.id]).toBeGreaterThan(0n); - expect(result.data.getAllocation(VAULT, targetIds[2]).allocation).toBe( - sourceExpectedAssets, - ); + expect( + result.data.getAllocation(VAULT, targetAdapterMarketCapId).allocation, + ).toBe(sourceExpectedAssets); }); test("behavior: honors the configured source-utilization ceiling", () => { @@ -689,6 +700,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { [new Market({ ...targetMarket })], ); const secondTargetIds = secondTargetAdapter.ids(targetParams); + const [, , secondTargetAdapterMarketCapId] = secondTargetIds; const secondAllocations: Record = {}; for (const id of secondTargetIds) { secondAllocations[id] = { @@ -740,10 +752,10 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { marketPublicAllocatorConfigs: { [VAULT]: data.marketPublicAllocatorConfigs[VAULT], [SECOND_VAULT]: { - [secondTargetIds[2]]: { + [secondTargetAdapterMarketCapId]: { vault: SECOND_VAULT, adapter: SECOND_TARGET_ADAPTER, - adapterMarketCapId: secondTargetIds[2], + adapterMarketCapId: secondTargetAdapterMarketCapId, absoluteCap: 10_000n, canPullFromMarket: false, }, @@ -772,7 +784,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }); test("behavior: deep-clones legacy and nested accrued adapters", () => { - const { data, targetIds } = makeFixture(); + const { data, targetAdapterMarketCapId } = makeFixture(); const targetMarket = data.getMarket(targetParams.id); const legacyPosition = new AccrualPosition( { @@ -887,14 +899,14 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { input.marketPublicAllocatorConfigs, ); expect(cloned.allocations[VAULT]).not.toBe(input.allocations[VAULT]); - expect(cloned.getAllocation(VAULT, targetIds[2])).toBe( - input.getAllocation(VAULT, targetIds[2]), + expect(cloned.getAllocation(VAULT, targetAdapterMarketCapId)).toBe( + input.getAllocation(VAULT, targetAdapterMarketCapId), ); expect(cloned.getPublicAllocatorConfig(VAULT)).toBeInstanceOf( VaultV2BluePublicAllocatorConfig, ); expect( - cloned.getMarketPublicAllocatorConfig(VAULT, targetIds[2]), + cloned.getMarketPublicAllocatorConfig(VAULT, targetAdapterMarketCapId), ).toBeInstanceOf(VaultV2BlueMarketPublicAllocatorConfig); const inputLegacy = input .getVault(VAULT) @@ -1074,7 +1086,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { }); test("behavior: recognizes zero-elapsed losses at the one-unit relative-cap boundary", () => { - const { data, targetIds } = makeFixture({ + const { data, targetAdapterMarketCapId } = makeFixture({ sourceSupply: 0n, targetSupply: 0n, firstTotalAssets: 1_000n, @@ -1092,9 +1104,9 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { expect(result.reallocations).toHaveLength(1); expect(result.reallocations[0]?.assets).toBe(450n); - expect(result.data.getAllocation(VAULT, targetIds[2]).allocation).toBe( - 450n, - ); + expect( + result.data.getAllocation(VAULT, targetAdapterMarketCapId).allocation, + ).toBe(450n); expect(result.data.getVault(VAULT)._totalAssets).toBe(900n); }); diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index 77f3ef125..99b933214 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -873,9 +873,13 @@ export class VaultV2BlueReallocationData continue; const targetContext = _try(() => { - const ids = adapter.ids(targetMarket.params); + const [adapterCapId, collateralCapId, adapterMarketCapId] = + adapter.ids(targetMarket.params); const marketPublicAllocatorConfig = - data.getMarketPublicAllocatorConfig(vaultAddress, ids[2]); + data.getMarketPublicAllocatorConfig( + vaultAddress, + adapterMarketCapId, + ); if ( !isAddressEqual( marketPublicAllocatorConfig.vault, @@ -889,10 +893,25 @@ export class VaultV2BlueReallocationData ) return; - const allocations = ids.map((id) => - data.getAllocation(vaultAddress, id), + const adapterCapAllocation = data.getAllocation( + vaultAddress, + adapterCapId, + ); + const collateralCapAllocation = data.getAllocation( + vaultAddress, + collateralCapId, ); - if (allocations.some(({ absoluteCap }) => absoluteCap === 0n)) + const adapterMarketCapAllocation = data.getAllocation( + vaultAddress, + adapterMarketCapId, + ); + if ( + [ + adapterCapAllocation, + collateralCapAllocation, + adapterMarketCapAllocation, + ].some(({ absoluteCap }) => absoluteCap === 0n) + ) return; const expectedSupplyAssets = targetMarket.toSupplyAssets( @@ -900,22 +919,21 @@ export class VaultV2BlueReallocationData ); const untracked = MathLib.zeroFloorSub( expectedSupplyAssets, - allocations[2]!.allocation, + adapterMarketCapAllocation.allocation, ); return { adapter, - allocations, + adapterMarketCapAllocation, marketPublicAllocatorConfig, untracked, }; }, UnknownDataError); if (targetContext == null) continue; - const targetMarketParamsAllocation = targetContext.allocations[2]!; const allocatorHeadroom = targetContext.marketPublicAllocatorConfig.getMaxIn( - targetMarketParamsAllocation.allocation + + targetContext.adapterMarketCapAllocation.allocation + targetContext.untracked, ); @@ -965,9 +983,10 @@ export class VaultV2BlueReallocationData const candidate = _try(() => { const sourceIds = sourceAdapter.ids(sourceMarket.params); + const [, , sourceAdapterMarketCapId] = sourceIds; const sourceConfig = data.getMarketPublicAllocatorConfig( vaultAddress, - sourceIds[2], + sourceAdapterMarketCapId, ); if ( !isAddressEqual(sourceConfig.vault, vaultAddress) || @@ -1230,6 +1249,7 @@ export class VaultV2BlueReallocationData ); const sourceMarket = data.getMarket(reallocation.from.marketParams.id); const sourceIds = sourceAdapter.ids(sourceMarket.params); + const [, , sourceAdapterMarketCapId] = sourceIds; const currentSupplyShares = sourceAdapter.supplyShares[sourceMarket.id] ?? 0n; const withdrawal = sourceMarket.withdraw( @@ -1252,7 +1272,9 @@ export class VaultV2BlueReallocationData const sourceChange = withdrawal.market.toSupplyAssets( sourceAdapter.supplyShares[sourceMarket.id] ?? 0n, - ) - data.getAllocation(reallocation.vault, sourceIds[2]).allocation; + ) - + data.getAllocation(reallocation.vault, sourceAdapterMarketCapId) + .allocation; for (const id of sourceIds) { const allocation = data.getAllocation(reallocation.vault, id); const nextAllocation = allocation.allocation + sourceChange; @@ -1297,11 +1319,12 @@ export class VaultV2BlueReallocationData reallocation.to.adapter, ); const targetIds = targetAdapter.ids(targetMarket.params); + const [, , targetAdapterMarketCapId] = targetIds; const currentTargetMarket = data.getMarket(targetMarket.id); const oldTargetAllocation = data.getAllocation( reallocation.vault, - targetIds[2], + targetAdapterMarketCapId, ).allocation; const supply = currentTargetMarket.supply( reallocation.assets, From fddb0d6e8d2c2709a88fb268996bdbaeb388a25c Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 21 Aug 2026 17:11:05 +0200 Subject: [PATCH 39/41] fix(morpho-sdk): validate reallocation snapshots --- .changeset/brave-vaults-reallocate.md | 2 +- .../morpho-sdk/src/actions/blue/withdraw.ts | 2 +- ...ue.bluePublicAllocatorRequirements.test.ts | 68 ++++++ .../entities/blue/blue.reallocations.test.ts | 71 +++++- .../morpho-sdk/src/entities/blue/blue.test.ts | 4 +- packages/morpho-sdk/src/entities/blue/blue.ts | 216 ++++++++++++++---- .../test/actions/blue/reallocations.test.ts | 19 +- 7 files changed, 329 insertions(+), 53 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index b5e0e0595..01765c346 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -11,7 +11,7 @@ Add canonical `vaultV1PublicAllocatorAbi` and `vaultV2BluePublicAllocatorAbi` ex V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. `VaultV2BluePublicAllocatorConfig` is hydrated as a class with exact per-call penalty calculation, `VaultV2BlueMarketPublicAllocatorConfig` computes max-in capacity from its absolute cap, and plan totals stay local to their consumers. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. -Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData` and `getVaultV2BlueReallocationData`, preserving the published unversioned `getReallocationData` as a deprecated V1 alias. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. +Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData`, `getVaultV1Reallocations`, `getVaultV2BlueReallocationData`, and `getVaultV2BlueReallocations`; preserve the published unversioned `getReallocationData` and `getReallocations` as deprecated V1 aliases. Both versioned planners reject reallocation snapshots from another chain. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. Compatibility note: this minor intentionally accepts four breaking changes. `VaultV2MorphoMarketV1Adapter.ids()` and `VaultV2MorphoMarketV1AdapterV2.ids()` now return the labeled readonly tuple `readonly [adapterCapId: Hash, collateralCapId: Hash, adapterMarketCapId: Hash]` instead of mutable `Hash[]`, while `VaultV2MorphoVaultV1Adapter.ids()` now returns `readonly [adapterCapId: Hash]`; `MorphoBlue.withdraw`, `borrow`, and `refinance` may now return `Transaction` from `getRequirements()` for Vault V2 penalty funding; `BlueWithdrawAction`, `BlueBorrowAction`, `BlueSupplyCollateralBorrowAction`, and `BlueRefinanceAction` now require `reallocationPenaltyAssets`; and Vault V2 reallocation discovery now accepts only zero-penalty vaults by default. Runtime ordering for `ids()` is unchanged. Consumers should spread `ids()` when a mutable array is required, handle approval transactions in exhaustive requirement consumers, set `reallocationPenaltyAssets: 0n` in handwritten V1 or no-penalty action descriptors, and explicitly set `maxPenalty` when opting into a nonzero Vault V2 allocator penalty. Explicit and hand-built penalties remain supported up to WAD (100%), preserving the existing maximum. diff --git a/packages/morpho-sdk/src/actions/blue/withdraw.ts b/packages/morpho-sdk/src/actions/blue/withdraw.ts index c84012a64..37e112b45 100644 --- a/packages/morpho-sdk/src/actions/blue/withdraw.ts +++ b/packages/morpho-sdk/src/actions/blue/withdraw.ts @@ -33,7 +33,7 @@ export interface BlueWithdrawParams { minSharePrice: bigint; /** * Homogeneous Vault V1 or Vault V2 reallocations to execute before withdrawing. V1 entries can be - * computed via `MorphoBlue.getReallocations({ operation: "withdraw", amount })` or directly + * computed via `MorphoBlue.getVaultV1Reallocations({ operation: "withdraw", amount })` or directly * via `computeVaultV1Reallocations({ operation: "withdraw", amount, ... })`. */ reallocations?: BlueReallocationPlan; diff --git a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts index fa1fdc71d..0f24ae05c 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts @@ -115,6 +115,74 @@ describe("MorphoBlue BluePublicAllocator requirements", () => { }); }); + test("behavior: supplyCollateralBorrow approves distinct collateral and penalty tokens", async () => { + const handle = createMockClient(mainnet); + const { + morpho, + bundler3: { generalAdapter1 }, + } = getChainAddresses(mainnet.id); + mockRead(handle, { + address: morpho, + abi: blueAbi, + functionName: "isAuthorized", + result: true, + }); + mockRead(handle, { + address: marketParams.collateralToken, + abi: erc20Abi, + functionName: "allowance", + result: 0n, + }); + mockRead(handle, { + address: marketParams.loanToken, + abi: erc20Abi, + functionName: "allowance", + result: 0n, + }); + const market = handle.client + .extend(morphoViemExtension({ supportSignature: false })) + .morpho.blue(marketParams, mainnet.id); + + const requirements = await market + .supplyCollateralBorrow({ + amount: 100n, + borrowAmount: 1n, + userAddress: USER, + positionData: makePosition(marketParams, { + borrowShares: 1n, + collateral: 1_000_000n, + }), + reallocations: [ + { + vault: marketParams.oracle, + from: { type: "idle" }, + to: { adapter: marketParams.collateralToken }, + assets: 10n, + penalty: 500_000_000_000_000_000n, + }, + ], + }) + .getRequirements(); + + expect( + requirements.filter(isRequirementApproval).map((approval) => ({ + token: approval.to, + ...approval.action.args, + })), + ).toStrictEqual([ + { + token: marketParams.collateralToken, + spender: generalAdapter1, + amount: 100n, + }, + { + token: marketParams.loanToken, + spender: generalAdapter1, + amount: 5n, + }, + ]); + }); + test("behavior: withdraw includes V2 penalty approval and Morpho authorization", async () => { const handle = createMockClient(mainnet); const { diff --git a/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts b/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts index abe7658b5..c7eae3a50 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.reallocations.test.ts @@ -1,3 +1,4 @@ +import { MarketParams } from "@morpho-org/blue-sdk"; import { createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; import { describe, expect, test, vi } from "vitest"; @@ -5,9 +6,10 @@ import { CbbtcUsdcBlue } from "../../../test/fixtures/blue.js"; import { morphoViemExtension } from "../../client/index.js"; import { ChainIdMismatchError } from "../../types/index.js"; import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; +import { VaultV2BlueReallocationData } from "../vaultV2BlueReallocationData.js"; describe("MorphoBlue reallocation APIs", () => { - test("error: ChainIdMismatchError when reallocation data chain differs from market chain", () => { + test("getVaultV1Reallocations error: ChainIdMismatchError", () => { const publicClient = createPublicClient({ chain: mainnet, transport: http("https://rpc.example"), @@ -16,7 +18,7 @@ describe("MorphoBlue reallocation APIs", () => { const market = morphoClient.blue(CbbtcUsdcBlue, mainnet.id); expect(() => - market.getReallocations({ + market.getVaultV1Reallocations({ reallocationData: new VaultV1ReallocationData({ chainId: mainnet.id + 1, }), @@ -25,6 +27,27 @@ describe("MorphoBlue reallocation APIs", () => { ).toThrow(ChainIdMismatchError); }); + test("deprecated getReallocations delegates to the Vault V1 planner", () => { + const publicClient = createPublicClient({ + chain: mainnet, + transport: http("https://rpc.example"), + }); + const market = publicClient + .extend(morphoViemExtension()) + .morpho.blue(CbbtcUsdcBlue, mainnet.id); + const expected = [] as const; + const canonical = vi + .spyOn(market, "getVaultV1Reallocations") + .mockReturnValue(expected); + const params = { + reallocationData: new VaultV1ReallocationData({ chainId: mainnet.id }), + borrowAmount: 1n, + } as const; + + expect(market.getReallocations(params)).toBe(expected); + expect(canonical).toHaveBeenCalledWith(params); + }); + test("deprecated getReallocationData delegates to the Vault V1 fetcher", async () => { const publicClient = createPublicClient({ chain: mainnet, @@ -62,4 +85,48 @@ describe("MorphoBlue reallocation APIs", () => { }), ).rejects.toBeInstanceOf(ChainIdMismatchError); }); + + test("getVaultV2BlueReallocations default: delegates to the Vault V2 planner", () => { + const publicClient = createPublicClient({ + chain: mainnet, + transport: http("https://rpc.example"), + }); + const market = publicClient + .extend(morphoViemExtension()) + .morpho.blue(CbbtcUsdcBlue, mainnet.id); + const reallocationData = new VaultV2BlueReallocationData({ + chainId: mainnet.id, + }); + const expected = { reallocations: [], data: reallocationData } as const; + const planner = vi + .spyOn(reallocationData, "computeVaultV2BlueReallocations") + .mockReturnValue(expected); + const options = { enabled: false } as const; + + expect( + market.getVaultV2BlueReallocations({ reallocationData, options }), + ).toBe(expected); + expect(planner).toHaveBeenCalledWith( + new MarketParams(CbbtcUsdcBlue).id, + options, + ); + }); + + test("getVaultV2BlueReallocations error: ChainIdMismatchError", () => { + const publicClient = createPublicClient({ + chain: mainnet, + transport: http("https://rpc.example"), + }); + const market = publicClient + .extend(morphoViemExtension()) + .morpho.blue(CbbtcUsdcBlue, mainnet.id); + + expect(() => + market.getVaultV2BlueReallocations({ + reallocationData: new VaultV2BlueReallocationData({ + chainId: mainnet.id + 1, + }), + }), + ).toThrow(ChainIdMismatchError); + }); }); diff --git a/packages/morpho-sdk/src/entities/blue/blue.test.ts b/packages/morpho-sdk/src/entities/blue/blue.test.ts index b9c91925f..5dcf8606a 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.test.ts @@ -666,7 +666,7 @@ describe("MorphoBlue validation", () => { ).toThrow(NonPositiveInputError); }); - test("getReallocations accepts the operation/amount parameter shape", async ({ + test("getVaultV1Reallocations accepts the operation/amount parameter shape", async ({ client, }) => { const market = client @@ -674,7 +674,7 @@ describe("MorphoBlue validation", () => { .morpho.blue(CbbtcUsdcBlue, mainnet.id); expect( - market.getReallocations({ + market.getVaultV1Reallocations({ reallocationData: new VaultV1ReallocationData({ chainId: mainnet.id }), operation: "borrow", amount: 1n, diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index eb8f8eb5f..5ac119bce 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -89,11 +89,40 @@ import { selectRequirementSignatures, type Transaction, type VaultV1Reallocation, + type VaultV2BluePublicAllocatorOptions, + type VaultV2BlueReallocation, WithdrawExceedsCollateralError, } from "../../types/index.js"; import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; import { VaultV2BlueReallocationData } from "../vaultV2BlueReallocationData.js"; +type VaultV1ReallocationsParams = { + readonly reallocationData: VaultV1ReallocationData; + readonly options?: ReallocationComputeOptions; +} & ( + | { + readonly operation: "borrow" | "withdraw"; + readonly amount: bigint; + readonly borrowAmount?: never; + } + | { + /** @deprecated Pass `{ operation: "borrow", amount }` instead. */ + readonly borrowAmount: bigint; + readonly operation?: never; + readonly amount?: never; + } +); + +type VaultV2BlueReallocationsParams = { + readonly reallocationData: VaultV2BlueReallocationData; + readonly options?: VaultV2BluePublicAllocatorOptions & { + readonly operation?: { + readonly type: "borrow" | "withdraw"; + readonly amount: bigint; + }; + }; +}; + export interface BlueActions { /** * Fetches the latest market data with accrued interest. @@ -506,7 +535,7 @@ export interface BlueActions { * inject unnecessary `reallocateTo` actions (and their PublicAllocator * fees) into the resulting bundle. * - * The returned reallocation data can be passed to {@link getReallocations} + * The returned reallocation data can be passed to {@link getVaultV1Reallocations} * to compute the `VaultV1Reallocation[]` array for `borrow()` or * `supplyCollateralBorrow()`. * @@ -552,7 +581,7 @@ export interface BlueActions { * @param params.vaultAddresses - Vault V2 addresses to inspect for market or idle liquidity. * @param params.block.number - Block number used for every RPC read. * @param params.block.timestamp - Timestamp corresponding to the fetched block. - * @returns A `VaultV2BlueReallocationData` snapshot ready for discovery or operation planning. + * @returns A `VaultV2BlueReallocationData` snapshot ready for {@link getVaultV2BlueReallocations}. * @throws {ChainIdMismatchError} when the client chain does not match this market. */ getVaultV2BlueReallocationData: (params: { @@ -564,7 +593,7 @@ export interface BlueActions { }) => Promise; /** - * Computes vault reallocations for a borrow or withdraw on this market. + * Computes Vault V1 PublicAllocator reallocations for this market. * * Uses the shared-liquidity algorithm to determine which vaults should reallocate liquidity to * this market via the PublicAllocator, based on the post-operation utilization target. @@ -585,27 +614,76 @@ export interface BlueActions { * or `withdraw()`. Empty array if no reallocation is needed. * @throws {ChainIdMismatchError} when `reallocationData` belongs to a different chain than this market. * @throws {InsufficientSharedLiquidityError} when shared liquidity cannot cover the operation's absolute shortfall on the target market — preventing fee-bearing reallocations from being attached to a call that would still revert onchain. + * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdrawal exceeds the target market supply. * @throws {MissingPublicAllocatorConfigError} when a selected vault is missing its public allocator config. * @throws {UnknownReallocationMarketError} when the target market is absent from the reallocation data. + * @example + * ```ts + * const reallocations = market.getVaultV1Reallocations({ + * reallocationData, + * operation: "borrow", + * amount: 1_000_000n, + * }); + * ``` + */ + getVaultV1Reallocations: ( + params: VaultV1ReallocationsParams, + ) => readonly VaultV1Reallocation[]; + + /** + * Computes Vault V1 PublicAllocator reallocations using the deprecated unversioned name. + * + * @param params.reallocationData - State returned by {@link getVaultV1ReallocationData}. + * @param params.operation - The operation driving the reallocation (`"borrow"` or `"withdraw"`). + * @param params.amount - The borrow or withdraw amount used to compute post-state utilization. + * @param params.borrowAmount - Deprecated borrow amount alias. + * @param params.options - Optional allocator and utilization options. + * @returns Vault V1 reallocations ready for a Blue action. + * @throws {ChainIdMismatchError} when `reallocationData` belongs to another chain. + * @throws {InsufficientSharedLiquidityError} when shared liquidity cannot cover the operation. + * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdrawal exceeds market supply. + * @throws {MissingPublicAllocatorConfigError} when a selected vault lacks allocator state. + * @throws {UnknownReallocationMarketError} when the target market is absent. + * @deprecated Use {@link getVaultV1Reallocations} instead. + * @example + * ```ts + * const reallocations = market.getReallocations({ + * reallocationData, + * operation: "borrow", + * amount: 1_000_000n, + * }); + * ``` */ getReallocations: ( - params: { - reallocationData: VaultV1ReallocationData; - options?: ReallocationComputeOptions; - } & ( - | { - operation: "borrow" | "withdraw"; - amount: bigint; - borrowAmount?: never; - } - | { - /** @deprecated Pass `{ operation: "borrow", amount }` instead. */ - borrowAmount: bigint; - operation?: never; - amount?: never; - } - ), + params: VaultV1ReallocationsParams, ) => readonly VaultV1Reallocation[]; + + /** + * Computes Vault V2 BluePublicAllocator reallocations for this market. + * + * @param params.reallocationData - State returned by {@link getVaultV2BlueReallocationData}. + * @param params.options - Optional allocator discovery controls and operation to support. + * @returns Action-ready reallocations and their post-simulation state. + * @throws {ChainIdMismatchError} when `reallocationData` belongs to another chain. + * @throws {NegativeInputError} when a utilization or penalty limit is negative. + * @throws {InputExceedsMaxError} when a utilization or penalty limit exceeds WAD. + * @throws {NonPositiveInputError} when an enabled operation amount is not positive. + * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent. + * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the shortfall. + * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdrawal exceeds market supply. + * @example + * ```ts + * const result = market.getVaultV2BlueReallocations({ + * reallocationData, + * options: { operation: { type: "borrow", amount: 1_000_000n } }, + * }); + * ``` + */ + getVaultV2BlueReallocations: (params: VaultV2BlueReallocationsParams) => { + readonly reallocations: readonly VaultV2BlueReallocation[]; + readonly data: VaultV2BlueReallocationData; + }; } export class MorphoBlue implements BlueActions { @@ -1814,7 +1892,7 @@ export class MorphoBlue implements BlueActions { * @param params.vaultAddresses - Vaults to inspect for source-market liquidity. * @param params.block.number - Block number used for every RPC read. * @param params.block.timestamp - Timestamp corresponding to the fetched block. - * @returns Reallocation data ready for {@link getReallocations}. + * @returns Reallocation data ready for {@link getVaultV1Reallocations}. * @throws {ChainIdMismatchError} when the client chain does not match this market. * @example * ```ts @@ -1979,7 +2057,7 @@ export class MorphoBlue implements BlueActions { * @param params.vaultAddresses - Vault V2 addresses to inspect for market or idle liquidity. * @param params.block.number - Block number used for every RPC read. * @param params.block.timestamp - Timestamp corresponding to the fetched block. - * @returns A `VaultV2BlueReallocationData` snapshot ready for discovery or operation planning. + * @returns A `VaultV2BlueReallocationData` snapshot ready for {@link getVaultV2BlueReallocations}. * @throws {ChainIdMismatchError} when the client chain does not match this market. * @example * ```ts @@ -2071,7 +2149,7 @@ export class MorphoBlue implements BlueActions { } /** - * Computes public allocator reallocations for a borrow or withdraw on this market. + * Computes Vault V1 PublicAllocator reallocations for this market. * * Pass `{ borrowAmount }` for a borrow (legacy alias, equivalent to `{ operation: "borrow", amount }`) * or `{ operation, amount }` for a borrow or loan-asset withdraw. @@ -2088,24 +2166,17 @@ export class MorphoBlue implements BlueActions { * @throws {ReallocationWithdrawExceedsMarketSupplyError} when `operation === "withdraw"` and `amount` exceeds the target market's `totalSupplyAssets`. * @throws {MissingPublicAllocatorConfigError} when a selected vault is missing its public allocator config. * @throws {UnknownReallocationMarketError} when the target market is absent from the reallocation data. + * @example + * ```ts + * const reallocations = market.getVaultV1Reallocations({ + * reallocationData, + * operation: "borrow", + * amount: 1_000_000n, + * }); + * ``` */ - getReallocations( - params: { - reallocationData: VaultV1ReallocationData; - options?: ReallocationComputeOptions; - } & ( - | { - operation: "borrow" | "withdraw"; - amount: bigint; - borrowAmount?: never; - } - | { - /** @deprecated Pass `{ operation: "borrow", amount }` instead. */ - borrowAmount: bigint; - operation?: never; - amount?: never; - } - ), + getVaultV1Reallocations( + params: VaultV1ReallocationsParams, ): readonly VaultV1Reallocation[] { validateChainId(params.reallocationData.chainId, this.chainId); @@ -2130,4 +2201,71 @@ export class MorphoBlue implements BlueActions { options, }); } + + /** + * Computes Vault V1 PublicAllocator reallocations using the deprecated unversioned name. + * + * @param params.reallocationData - State returned by {@link getVaultV1ReallocationData}. + * @param params.operation - The operation driving the reallocation (`"borrow"` or `"withdraw"`). + * @param params.amount - The borrow or withdraw amount used to compute post-state utilization. + * @param params.borrowAmount - Deprecated borrow amount alias. + * @param params.options - Optional allocator and utilization options. + * @returns Vault V1 reallocations ready for a Blue action. + * @throws {ChainIdMismatchError} when `reallocationData` belongs to another chain. + * @throws {InsufficientSharedLiquidityError} when shared liquidity cannot cover the operation. + * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdrawal exceeds market supply. + * @throws {MissingPublicAllocatorConfigError} when a selected vault lacks allocator state. + * @throws {UnknownReallocationMarketError} when the target market is absent. + * @deprecated Use {@link getVaultV1Reallocations} instead. + * @example + * ```ts + * const reallocations = market.getReallocations({ + * reallocationData, + * operation: "borrow", + * amount: 1_000_000n, + * }); + * ``` + */ + getReallocations( + params: VaultV1ReallocationsParams, + ): readonly VaultV1Reallocation[] { + return this.getVaultV1Reallocations(params); + } + + /** + * Computes Vault V2 BluePublicAllocator reallocations for this market. + * + * @param params.reallocationData - State returned by {@link getVaultV2BlueReallocationData}. + * @param params.options - Optional allocator discovery controls and operation to support. + * @returns Action-ready reallocations and their post-simulation state. + * @throws {ChainIdMismatchError} when `reallocationData` belongs to another chain. + * @throws {NegativeInputError} when a utilization or penalty limit is negative. + * @throws {InputExceedsMaxError} when a utilization or penalty limit exceeds WAD. + * @throws {NonPositiveInputError} when an enabled operation amount is not positive. + * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent. + * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the shortfall. + * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdrawal exceeds market supply. + * @example + * ```ts + * const result = market.getVaultV2BlueReallocations({ + * reallocationData, + * options: { operation: { type: "borrow", amount: 1_000_000n } }, + * }); + * ``` + */ + getVaultV2BlueReallocations({ + reallocationData, + options, + }: VaultV2BlueReallocationsParams): { + readonly reallocations: readonly VaultV2BlueReallocation[]; + readonly data: VaultV2BlueReallocationData; + } { + validateChainId(reallocationData.chainId, this.chainId); + + return reallocationData.computeVaultV2BlueReallocations( + this.marketParams.id, + options, + ); + } } diff --git a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts index 0348c2e7f..888bbd247 100644 --- a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts +++ b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts @@ -616,7 +616,7 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { }); }); -describe("getVaultV1ReallocationData and getReallocations", () => { +describe("getVaultV1ReallocationData and getVaultV1Reallocations", () => { test("should reject getVaultV1ReallocationData when the client chain differs from the market chain", async ({ client, }) => { @@ -631,7 +631,7 @@ describe("getVaultV1ReallocationData and getReallocations", () => { ).rejects.toBeInstanceOf(ChainIdMismatchError); }); - test("should compute reallocations and borrow using getVaultV1ReallocationData + getReallocations", async ({ + test("should compute reallocations and borrow using getVaultV1ReallocationData + getVaultV1Reallocations", async ({ client, }) => { const collateralAmount = parseUnits("1000", 8); @@ -667,9 +667,10 @@ describe("getVaultV1ReallocationData and getReallocations", () => { block, }); - const reallocations = market.getReallocations({ + const reallocations = market.getVaultV1Reallocations({ reallocationData, - borrowAmount, + operation: "borrow", + amount: borrowAmount, options: { timestamp: block.timestamp }, }); @@ -746,9 +747,10 @@ describe("getVaultV1ReallocationData and getReallocations", () => { block, }); - const reallocations = market.getReallocations({ + const reallocations = market.getVaultV1Reallocations({ reallocationData, - borrowAmount, + operation: "borrow", + amount: borrowAmount, options: { timestamp: block.timestamp }, }); @@ -806,9 +808,10 @@ describe("getVaultV1ReallocationData and getReallocations", () => { block, }); - const reallocations = market.getReallocations({ + const reallocations = market.getVaultV1Reallocations({ reallocationData, - borrowAmount, + operation: "borrow", + amount: borrowAmount, options: { timestamp: block.timestamp }, }); From 3f0888cd9c9006a59f60d81e631c9894668721d8 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 21 Aug 2026 17:40:04 +0200 Subject: [PATCH 40/41] fix: validate public allocator availability --- .changeset/brave-vaults-reallocate.md | 2 +- .../actions/blue/buildReallocationActions.ts | 7 +- ...ue.bluePublicAllocatorRequirements.test.ts | 69 +++++++++++++++++++ packages/morpho-sdk/src/entities/blue/blue.ts | 34 +++++---- .../morpho-sdk/src/helpers/validate.test.ts | 7 +- packages/morpho-sdk/src/helpers/validate.ts | 34 +++++++-- packages/morpho-ts/src/addresses.test.ts | 51 ++++++++++++++ 7 files changed, 179 insertions(+), 25 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 01765c346..4ae765e69 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -9,7 +9,7 @@ Add canonical `vaultV1PublicAllocatorAbi` and `vaultV2BluePublicAllocatorAbi` exports plus per-chain `vaultV1PublicAllocator` and `vaultV2BluePublicAllocator` registry entries to `morpho-ts`, preserving `publicAllocatorAbi` and `publicAllocator` as deprecated V1 aliases. Move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while normalizing them before repeated use, add chain-registry-backed deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. -V2 bundles now pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. `VaultV2BluePublicAllocatorConfig` is hydrated as a class with exact per-call penalty calculation, `VaultV2BlueMarketPublicAllocatorConfig` computes max-in capacity from its absolute cap, and plan totals stay local to their consumers. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. +V2 bundles now reject chains without a registered BluePublicAllocator before exposing requirements, pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. `VaultV2BluePublicAllocatorConfig` is hydrated as a class with exact per-call penalty calculation, `VaultV2BlueMarketPublicAllocatorConfig` computes max-in capacity from its absolute cap, and plan totals stay local to their consumers. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData`, `getVaultV1Reallocations`, `getVaultV2BlueReallocationData`, and `getVaultV2BlueReallocations`; preserve the published unversioned `getReallocationData` and `getReallocations` as deprecated V1 aliases. Both versioned planners reject reallocation snapshots from another chain. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. diff --git a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts index 91d0fc0c1..21f534bc3 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -173,10 +173,11 @@ export const buildBlueReallocationActions = ({ readonly targetMarketParams: MarketParams; readonly penaltyFundingSource?: "initiator" | "generalAdapter1"; }) => { - const reallocationPlan = validateAndNormalizeReallocations( + const reallocationPlan = validateAndNormalizeReallocations({ reallocations, - targetMarketParams.id, - ); + targetMarketId: targetMarketParams.id, + chainId, + }); return reallocationPlan.type === "vaultV1" ? buildVaultV1ReallocationActions({ diff --git a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts index 0f24ae05c..d609002df 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts @@ -1,5 +1,6 @@ import { AccrualPosition, + ChainId, getChainAddresses, Market, MarketParams, @@ -16,6 +17,7 @@ import { } from "../../../test/fixtures/blue.js"; import { morphoViemExtension } from "../../client/index.js"; import { + BundlerErrors, isRequirementApproval, isRequirementBlueAuthorization, isRequirementSignature, @@ -57,6 +59,73 @@ const makePosition = ( ); describe("MorphoBlue BluePublicAllocator requirements", () => { + test("error: unsupported V2 deployment across every action flow", () => { + const chain = { ...mainnet, id: ChainId.CronosMainnet }; + const handle = createMockClient(chain); + const market = handle.client + .extend(morphoViemExtension()) + .morpho.blue(marketParams, chain.id); + const reallocations = [ + { + vault: marketParams.oracle, + from: { type: "idle" }, + to: { adapter: marketParams.collateralToken }, + assets: 10n, + penalty: 0n, + }, + ] as const; + + const actionFlows = [ + () => + market.borrow({ + amount: 1n, + userAddress: USER, + positionData: makePosition(marketParams, { + borrowShares: 1n, + collateral: 1_000_000n, + }), + reallocations, + }), + () => + market.withdraw({ + assets: 1n, + userAddress: USER, + positionData: makePosition(marketParams, { supplyShares: 10n }), + reallocations, + }), + () => + market.supplyCollateralBorrow({ + amount: 100n, + borrowAmount: 1n, + userAddress: USER, + positionData: makePosition(marketParams, { + borrowShares: 1n, + collateral: 1_000_000n, + }), + reallocations, + }), + () => + market.refinance({ + userAddress: USER, + positionData: makePosition(marketParams, { + borrowShares: 10n, + collateral: 1_000n, + }), + target: { + marketParams: CbbtcUsdcBlueAlt, + positionData: makePosition(CbbtcUsdcBlueAlt, {}), + }, + collateralAmount: 100n, + borrowAssets: 1n, + targetReallocations: reallocations, + }), + ]; + + for (const actionFlow of actionFlows) { + expect(actionFlow).toThrow(BundlerErrors.UnexpectedAction); + } + }); + test("default: includes the classic loan-token approval for V2 penalties", async () => { const handle = createMockClient(mainnet); const { diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 5ac119bce..424323928 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -233,6 +233,7 @@ export interface BlueActions { * * @param params - Withdraw parameters including pre-fetched `positionData`. * @returns Object with `buildTx` and `getRequirements`. + * @throws {BundlerErrors.UnexpectedAction} when a V2 plan is unsupported on the chain. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. @@ -279,6 +280,7 @@ export interface BlueActions { * * @param params - Borrow parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. + * @throws {BundlerErrors.UnexpectedAction} when a V2 plan is unsupported on the chain. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. @@ -433,6 +435,7 @@ export interface BlueActions { * * @param params - Combined parameters including pre-fetched `positionData` for health validation. * @returns Object with `buildTx` and `getRequirements`. + * @throws {BundlerErrors.UnexpectedAction} when a V2 plan is unsupported on the chain. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. @@ -493,6 +496,7 @@ export interface BlueActions { * @param params.slippageTolerance - WAD slippage tolerance. Defaults to `DEFAULT_SLIPPAGE_TOLERANCE`. * @param params.targetReallocations - Homogeneous Vault V1 or Vault V2 reallocations into the target market. * @returns Object with `buildTx` and `getRequirements`. + * @throws {BundlerErrors.UnexpectedAction} when a V2 plan is unsupported on the chain. * @throws {InputExceedsMaxError} when a V2 reallocation asset amount exceeds `uint128` or its penalty exceeds WAD. * @throws {InconsistentReallocationPenaltyError} when V2 entries for one vault use different penalties. * @throws {InvalidReallocationAddressError} when a V2 vault or adapter address is malformed. @@ -845,10 +849,11 @@ export class MorphoBlue implements BlueActions { slippageTolerance = DEFAULT_SLIPPAGE_TOLERANCE, reallocations, } = params; - const reallocationPlan = validateAndNormalizeReallocations( + const reallocationPlan = validateAndNormalizeReallocations({ reallocations, - this.marketParams.id, - ); + targetMarketId: this.marketParams.id, + chainId: this.chainId, + }); const reallocationList = reallocationPlan.reallocations; // Mode normalization: a missing or undefined `assets`/`shares` key collapses to `0n` @@ -1013,10 +1018,11 @@ export class MorphoBlue implements BlueActions { reallocations?: BlueReallocationPlan; }) { validateChainId(this.client.viemClient.chain?.id, this.chainId); - const reallocationPlan = validateAndNormalizeReallocations( + const reallocationPlan = validateAndNormalizeReallocations({ reallocations, - this.marketParams.id, - ); + targetMarketId: this.marketParams.id, + chainId: this.chainId, + }); const reallocationList = reallocationPlan.reallocations; if (amount <= 0n) { @@ -1514,10 +1520,11 @@ export class MorphoBlue implements BlueActions { reallocations?: BlueReallocationPlan; } & DepositAmountArgs) { validateChainId(this.client.viemClient.chain?.id, this.chainId); - const reallocationPlan = validateAndNormalizeReallocations( + const reallocationPlan = validateAndNormalizeReallocations({ reallocations, - this.marketParams.id, - ); + targetMarketId: this.marketParams.id, + chainId: this.chainId, + }); const reallocationList = reallocationPlan.reallocations; if (amount < 0n) { @@ -1669,10 +1676,11 @@ export class MorphoBlue implements BlueActions { }) { validateChainId(this.client.viemClient.chain?.id, this.chainId); validateSlippageTolerance(slippageTolerance); - const targetReallocationPlan = validateAndNormalizeReallocations( - targetReallocations, - target.marketParams.id, - ); + const targetReallocationPlan = validateAndNormalizeReallocations({ + reallocations: targetReallocations, + targetMarketId: target.marketParams.id, + chainId: this.chainId, + }); const targetReallocationList = targetReallocationPlan.reallocations; if (collateralAmount <= 0n) { diff --git a/packages/morpho-sdk/src/helpers/validate.test.ts b/packages/morpho-sdk/src/helpers/validate.test.ts index c0ce7dc8b..1c134f0ad 100644 --- a/packages/morpho-sdk/src/helpers/validate.test.ts +++ b/packages/morpho-sdk/src/helpers/validate.test.ts @@ -833,10 +833,11 @@ describe("reallocation validation", () => { }, ])("error: InvalidReallocationShapeError for $name", ({ reallocation }) => { expect(() => - validateAndNormalizeReallocations( - [reallocation] as unknown as BlueReallocationPlan, + validateAndNormalizeReallocations({ + reallocations: [reallocation] as unknown as BlueReallocationPlan, targetMarketId, - ), + chainId: mainnet.id, + }), ).toThrow(InvalidReallocationShapeError); }); diff --git a/packages/morpho-sdk/src/helpers/validate.ts b/packages/morpho-sdk/src/helpers/validate.ts index b92980a19..34837581e 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -14,6 +14,7 @@ import { AddressMismatchError, type BlueReallocationPlan, BorrowExceedsSafeLtvError, + BundlerErrors, ChainIdMismatchError, ChainWNativeMissingError, EmptyReallocationWithdrawalsError, @@ -494,11 +495,26 @@ export const validateVaultV2BlueReallocations = ( } }; -/** @internal */ -export const validateAndNormalizeReallocations = ( - reallocations: BlueReallocationPlan | undefined, - targetMarketId: MarketId, -) => { +/** + * Validates and normalizes a homogeneous Blue reallocation plan. + * + * @param params - Validation parameters. + * @param params.reallocations - Optional Vault V1 or Vault V2 reallocation plan. + * @param params.targetMarketId - Morpho Blue market receiving the liquidity. + * @param params.chainId - Chain whose allocator deployment is required for a V2 plan. + * @returns The validated plan tagged with its allocator version. + * @throws {BundlerErrors.UnexpectedAction} when a V2 plan is unsupported on the chain. + * @internal + */ +export const validateAndNormalizeReallocations = ({ + reallocations, + targetMarketId, + chainId, +}: { + readonly reallocations: BlueReallocationPlan | undefined; + readonly targetMarketId: MarketId; + readonly chainId: number; +}) => { const vaultV1Reallocations: VaultV1Reallocation[] = []; const vaultV2Reallocations: VaultV2BlueReallocation[] = []; @@ -521,6 +537,14 @@ export const validateAndNormalizeReallocations = ( } if (vaultV2Reallocations.length > 0) { validateVaultV2BlueReallocations(vaultV2Reallocations, targetMarketId); + if (getChainAddresses(chainId).vaultV2BluePublicAllocator == null) { + throw new BundlerErrors.UnexpectedAction( + vaultV2Reallocations[0]?.from.type === "market" + ? "vaultV2BluePublicAllocatorReallocate" + : "vaultV2BluePublicAllocatorAllocateFromIdle", + chainId, + ); + } return { type: "vaultV2Blue" as const, reallocations: vaultV2Reallocations, diff --git a/packages/morpho-ts/src/addresses.test.ts b/packages/morpho-ts/src/addresses.test.ts index 3219fa39a..bd2bf9d50 100644 --- a/packages/morpho-ts/src/addresses.test.ts +++ b/packages/morpho-ts/src/addresses.test.ts @@ -641,6 +641,25 @@ describe("registerCustomAddresses", () => { expect(deployments[chainId]?.publicAllocator).toBe(11n); }); + test("behavior: backfills deprecated PublicAllocator aliases", () => { + const chainId = 31_337_014; + const vaultV1PublicAllocator = randomAddress(); + + registerCustomAddresses({ + addresses: { + [chainId]: { ...createBlueAddresses(), vaultV1PublicAllocator }, + }, + deployments: { + [chainId]: { ...createBlueDeployments(), vaultV1PublicAllocator: 11n }, + }, + }); + + expect(addressesRegistry[chainId]?.publicAllocator).toBe( + vaultV1PublicAllocator, + ); + expect(deployments[chainId]?.publicAllocator).toBe(11n); + }); + test("error: RegistryValueAlreadyRegisteredError for addresses", () => { const chainId = 31_337_009; const chainAddresses = createChainAddresses(); @@ -666,6 +685,22 @@ describe("registerCustomAddresses", () => { expect(getChainAddress(chainId, "midnight")).toBe(chainAddresses.midnight); }); + test("error: conflicting PublicAllocator addresses", () => { + const chainId = 31_337_015; + + expect(() => + registerCustomAddresses({ + addresses: { + [chainId]: { + ...createBlueAddresses(), + vaultV1PublicAllocator: randomAddress(), + publicAllocator: randomAddress(), + }, + }, + }), + ).toThrow(RegistryValueAlreadyRegisteredError); + }); + test("error: IncompleteChainRegistryError for custom-chain addresses", () => { const chainId = 31_337_012; const partialAddresses = createMidnightAddresses() as ChainAddresses; @@ -765,6 +800,22 @@ describe("registerCustomAddresses", () => { expect(deployments[chainId]?.midnight).toBe(chainDeployments.midnight); }); + test("error: conflicting PublicAllocator deployments", () => { + const chainId = 31_337_108; + + expect(() => + registerCustomAddresses({ + deployments: { + [chainId]: { + ...createBlueDeployments(), + vaultV1PublicAllocator: 11n, + publicAllocator: 12n, + }, + }, + }), + ).toThrow(RegistryValueAlreadyRegisteredError); + }); + test("behavior: does not freeze caller-owned nested inputs", () => { const chainId = 31_337_106; const chainAddresses = createChainAddresses(); From a144423ce5fd8e0908fde71365eb6d4c9dd476ff Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 21 Aug 2026 18:03:08 +0200 Subject: [PATCH 41/41] fix(morpho-sdk): harden Vault V2 reallocation discovery --- .changeset/brave-vaults-reallocate.md | 4 +- packages/morpho-sdk/src/entities/blue/blue.ts | 4 + .../vaultV2BlueReallocationData.test.ts | 75 ++++- .../entities/vaultV2BlueReallocationData.ts | 283 ++++++++++++++---- 4 files changed, 292 insertions(+), 74 deletions(-) diff --git a/.changeset/brave-vaults-reallocate.md b/.changeset/brave-vaults-reallocate.md index 4ae765e69..4162365d4 100644 --- a/.changeset/brave-vaults-reallocate.md +++ b/.changeset/brave-vaults-reallocate.md @@ -7,9 +7,9 @@ "@morpho-org/wdk-protocol-lending-morpho-evm": minor --- -Add canonical `vaultV1PublicAllocatorAbi` and `vaultV2BluePublicAllocatorAbi` exports plus per-chain `vaultV1PublicAllocator` and `vaultV2BluePublicAllocator` registry entries to `morpho-ts`, preserving `publicAllocatorAbi` and `publicAllocator` as deprecated V1 aliases. Move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while normalizing them before repeated use, add chain-registry-backed deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. +Add canonical `vaultV1PublicAllocatorAbi` and `vaultV2BluePublicAllocatorAbi` exports plus per-chain `vaultV1PublicAllocator` and `vaultV2BluePublicAllocator` registry entries to `morpho-ts`, preserving `publicAllocatorAbi` and `publicAllocator` as deprecated V1 aliases. Move the shared `marketParamsAbi` source of truth there while preserving its `blue-sdk` re-export, and raise the `blue-sdk` peer range to the introducing `morpho-ts` minor. Add Vault V2 allocation-cap helpers and the updated `canPullFromIdle`/`canPullFromMarket`/WAD-scaled penalty config types to `blue-sdk`, accept iterable active-adapter, vault-allowlist, and reallocation-plan inputs while materializing them before repeated use, add chain-registry-backed deployless and fallback reads to `blue-sdk-viem`, and expose Vault V2 shared-liquidity discovery, planning, metrics, maximum-penalty filtering, and flat market/idle reallocations through `morpho-sdk` Blue flows. -V2 bundles now reject chains without a registered BluePublicAllocator before exposing requirements, pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. `VaultV2BluePublicAllocatorConfig` is hydrated as a class with exact per-call penalty calculation, `VaultV2BlueMarketPublicAllocatorConfig` computes max-in capacity from its absolute cap, and plan totals stay local to their consumers. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. +V2 bundles now reject chains without a registered BluePublicAllocator before exposing requirements, pull the proportional loan-token penalty through GeneralAdapter1, grant the allocator an exact non-skippable allowance from Bundler3, pass the configured `uint64 penalty` in calldata, and keep the nonpayable allocator calls out of `tx.value`. `VaultV2BluePublicAllocatorConfig` is hydrated as a class with exact per-call penalty calculation, `VaultV2BlueMarketPublicAllocatorConfig` computes max-in capacity from its absolute cap, and plan totals stay local to their consumers. The planner mirrors contract execution order for penalties, source deallocation, first vault accrual (including zero-elapsed loss recognition), and target allocation; freezes the resulting relative-cap denominator across later calls for that vault; keeps every adapter coherent with one canonical simulated state per Morpho market; preserves supplied address casing while matching vaults and adapters case-insensitively; rejects incomplete allocator snapshots instead of silently reporting no liquidity; rejects non-positive operation amounts and same-market moves across adapters; and uses the latest timestamp in its complete input snapshot by default. Use coherent protocol-specific names across the V1 and V2 reallocation APIs, including `VaultV1ReallocationData`, `VaultV2BlueReallocationData`, `computeVaultV1Reallocations`, `VaultV2BluePublicAllocatorOptions`, `VaultV2BluePublicAllocatorConfig`, its fetcher family, and Vault V2-prefixed Bundler actions. Add `MorphoBlue.getVaultV1ReallocationData`, `getVaultV1Reallocations`, `getVaultV2BlueReallocationData`, and `getVaultV2BlueReallocations`; preserve the published unversioned `getReallocationData` and `getReallocations` as deprecated V1 aliases. Both versioned planners reject reallocation snapshots from another chain. Keep V1's `defaultMaxWithdrawalUtilization` configurable, and add V2's scalar `maxWithdrawalUtilization` for its friendly phase while retaining the 100% adversarial fallback. diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 424323928..5995d7cb3 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -673,6 +673,8 @@ export interface BlueActions { * @throws {InputExceedsMaxError} when a utilization or penalty limit exceeds WAD. * @throws {NonPositiveInputError} when an enabled operation amount is not positive. * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationVaultError} when configured vault state is absent. + * @throws {UnknownReallocationPublicAllocatorConfigError} when allocator authorization state is absent. * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent. * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the shortfall. * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdrawal exceeds market supply. @@ -2251,6 +2253,8 @@ export class MorphoBlue implements BlueActions { * @throws {InputExceedsMaxError} when a utilization or penalty limit exceeds WAD. * @throws {NonPositiveInputError} when an enabled operation amount is not positive. * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationVaultError} when configured vault state is absent. + * @throws {UnknownReallocationPublicAllocatorConfigError} when allocator authorization state is absent. * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent. * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the shortfall. * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdrawal exceeds market supply. diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts index 31f920153..e336c56db 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.test.ts @@ -39,7 +39,7 @@ import { import { VaultV2BlueReallocationData } from "./vaultV2BlueReallocationData.js"; const TIMESTAMP = 1_700_000_000n; -const VAULT = "0x0000000000000000000000000000000000000002"; +const VAULT = "0x00000000000000000000000000000000000000A2"; const TARGET_ADAPTER = "0x00000000000000000000000000000000000000A3"; const SOURCE_ADAPTER = "0x0000000000000000000000000000000000000004"; const LOAN_TOKEN = "0x0000000000000000000000000000000000000005"; @@ -90,6 +90,7 @@ const makeMarket = ({ }); interface FixtureOptions { + readonly recordVault?: Address; readonly sourceMarketParams?: MarketParams; readonly sourceAdapter?: Address; readonly sourceSupply?: bigint; @@ -120,6 +121,7 @@ interface FixtureOptions { } const makeFixture = ({ + recordVault = VAULT, sourceMarketParams = sourceParams, sourceAdapter: sourceAdapterAddress = SOURCE_ADAPTER, sourceSupply = 1_000n, @@ -298,20 +300,20 @@ const makeFixture = ({ [sourceMarket.id]: sourceMarket, }, vaults: { [VAULT]: vault }, - allocations: { [VAULT]: allocations }, + allocations: { [recordVault]: allocations }, publicAllocatorConfigs: { - [VAULT]: { + [recordVault]: { vault: VAULT, canPullFromIdle, penalty, }, }, activeAdapters: { - [VAULT]: + [recordVault]: allocatorActiveAdapters ?? adapters.map((adapter) => adapter.address), }, marketPublicAllocatorConfigs: { - [VAULT]: { + [recordVault]: { [targetAdapterMarketCapId]: { vault: VAULT, adapter: TARGET_ADAPTER, @@ -376,6 +378,16 @@ describe("VaultV2BlueReallocationData construction", () => { }); describe("VaultV2BlueReallocationData accessors", () => { + test("behavior: preserves checksummed address keys and values", () => { + const { data } = makeFixture(); + + expect(Object.keys(data.vaults)).toStrictEqual([VAULT]); + expect(Object.keys(data.activeAdapters)).toStrictEqual([VAULT]); + expect( + data.getActiveAdapters(VAULT.toLowerCase() as Address), + ).toStrictEqual(new Set([TARGET_ADAPTER, SOURCE_ADAPTER])); + }); + test("behavior: returns a fetched empty active-adapter set", () => { const { data } = makeFixture({ allocatorActiveAdapters: [] }); @@ -436,7 +448,7 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { } = makeFixture(); expect(data.getActiveAdapters(VAULT)).toStrictEqual( - new Set([TARGET_ADAPTER.toLowerCase(), SOURCE_ADAPTER.toLowerCase()]), + new Set([TARGET_ADAPTER, SOURCE_ADAPTER]), ); const result = data.computeVaultV2BlueReallocations(targetParams.id); @@ -465,6 +477,19 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ); }); + test("behavior: matches differently cased vault record keys", () => { + const recordVault = VAULT.toLowerCase() as Address; + const { data, sourceExpectedAssets } = makeFixture({ recordVault }); + + const result = data.computeVaultV2BlueReallocations(targetParams.id); + + expect(Object.keys(data.vaults)).toStrictEqual([VAULT]); + expect(Object.keys(data.allocations)).toStrictEqual([recordVault]); + expect(result.reallocations[0]?.assets).toBe(sourceExpectedAssets); + expect(Object.keys(result.data.vaults)).toStrictEqual([VAULT]); + expect(Object.keys(result.data.allocations)).toStrictEqual([recordVault]); + }); + test("error: ReallocationAdapterSupplySharesUnderflowError", () => { const { data } = makeFixture(); const sourceAdapter = data.getAdapter(VAULT, SOURCE_ADAPTER); @@ -518,6 +543,16 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ).toThrow(ReallocationAllocationUnderflowError); }); + test("behavior: rejects underflowing cap probes without aborting discovery", () => { + const { data, sourceAdapterCapId } = makeFixture(); + const allocation = data.getAllocation(VAULT, sourceAdapterCapId); + (data.allocations[VAULT] as Record)[ + sourceAdapterCapId + ] = { ...allocation, allocation: 1n }; + + expect(data.getPublicReallocationLiquidity(targetParams.id)).toBe(1n); + }); + test("behavior: allocates into a configured target with no existing position", () => { const { data, sourceExpectedAssets, targetAdapterMarketCapId } = makeFixture({ @@ -1274,6 +1309,34 @@ describe("VaultV2BlueReallocationData.computeVaultV2BlueReallocations", () => { ); }); + test("error: UnknownReallocationPublicAllocatorConfigError for incomplete vault state", () => { + const { data } = makeFixture(); + delete ( + data.publicAllocatorConfigs as Record< + Address, + VaultV2BluePublicAllocatorConfig | undefined + > + )[VAULT]; + + expect(() => data.computeVaultV2BlueReallocations(targetParams.id)).toThrow( + UnknownReallocationPublicAllocatorConfigError, + ); + }); + + test("behavior: ignores a vault with fetched but absent allocator config", () => { + const { data } = makeFixture(); + ( + data.publicAllocatorConfigs as Record< + Address, + VaultV2BluePublicAllocatorConfig | undefined + > + )[VAULT] = undefined; + + expect( + data.computeVaultV2BlueReallocations(targetParams.id).reallocations, + ).toStrictEqual([]); + }); + test("behavior: ignores vault liquidity above the penalty threshold", () => { const { data, sourceExpectedAssets } = makeFixture({ idle: 300n, diff --git a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts index 99b933214..a015cd75c 100644 --- a/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts +++ b/packages/morpho-sdk/src/entities/vaultV2BlueReallocationData.ts @@ -83,6 +83,8 @@ type ReadonlyVaultSnapshot = Readonly< readonly forceDeallocatePenalties: Readonly>; }; +type AdapterIds = ReturnType; + /** Input state required to simulate Vault V2 BluePublicAllocator reallocations. */ export interface InputVaultV2BlueReallocationData { /** Chain id associated with the fetched state. */ @@ -108,7 +110,8 @@ export interface InputVaultV2BlueReallocationData { >; /** * BluePublicAllocator-active adapters indexed by vault address. - * Arrays, readonly arrays, sets, and other iterables are accepted and normalized to sets. + * Arrays, readonly arrays, sets, and other iterables are copied to sets + * without changing address casing. */ readonly activeAdapters?: Readonly< Record | undefined> @@ -128,6 +131,48 @@ export interface InputVaultV2BlueReallocationData { const cloneMarket = (market: ReadonlyMarketSnapshot) => new Market({ ...market }); +/** + * Finds an address key without changing its supplied casing. + * + * @param record - Address-keyed record to search. + * @param address - Address to match case-insensitively. + * @returns The stored key, or `undefined` when no address matches. + * @internal + */ +const findAddressKey = ( + record: Readonly>, + address: Address, +) => { + if (Object.hasOwn(record, address)) return address; + return (Object.keys(record) as Address[]).find((key) => + isAddressEqual(key, address), + ); +}; + +/** + * Returns cached allocation ids for one adapter-market pair. + * + * @param cache - Per-planning-call adapter id cache. + * @param adapter - Adapter that derives the allocation ids. + * @param market - Market whose params identify the allocations. + * @returns The adapter, collateral, and adapter-market cap ids. + * @internal + */ +// biome-ignore lint/complexity/useMaxParams: cache lookup requires both adapter and market identity. +const getAdapterIds = ( + cache: Map, + adapter: ReadonlyMarketAdapterSnapshot, + market: ReadonlyMarketSnapshot, +) => { + const key = `${adapter.address}:${market.id}`; + const cached = cache.get(key); + if (cached != null) return cached; + + const ids = adapter.ids(market.params); + cache.set(key, ids); + return ids; +}; + const resolveMaxWithdrawalUtilization = (value: bigint | undefined) => { const utilization = value ?? DEFAULT_WITHDRAWAL_TARGET_UTILIZATION; if (utilization < 0n) @@ -269,6 +314,8 @@ const cloneVault = ( * instance. The first allocation for each vault accrues it in contract order * after the penalty donation and any source deallocation, then freezes that * `_totalAssets` value as `firstTotalAssets` for the rest of the plan. + * Address keys and values retain their supplied casing; lookups compare them + * case-insensitively. * * @example * ```ts @@ -315,7 +362,7 @@ export class VaultV2BlueReallocationData public readonly publicAllocatorConfigs: Readonly< Record >; - /** BluePublicAllocator-active adapters indexed by vault address. */ + /** BluePublicAllocator-active adapters indexed by vault address, preserving vault and adapter casing. */ public readonly activeAdapters: Readonly< Record | undefined> >; @@ -382,13 +429,7 @@ export class VaultV2BlueReallocationData input.activeAdapters ?? {}, ) as [Address, Iterable
| undefined][]) { activeAdapters[vault] = - adapters == null - ? undefined - : new Set( - [...adapters].map( - (adapter) => adapter.toLowerCase() as Address, - ), - ); + adapters == null ? undefined : new Set(adapters); } for (const [vault, configs] of Object.entries( @@ -499,7 +540,8 @@ export class VaultV2BlueReallocationData } private getMutableVault(vault: Address) { - const data = this.mutableVaults[vault]; + const key = findAddressKey(this.mutableVaults, vault); + const data = key == null ? undefined : this.mutableVaults[key]; if (data == null) throw new UnknownReallocationVaultError(vault); return data; } @@ -517,7 +559,8 @@ export class VaultV2BlueReallocationData * ``` */ public getAllocation(vault: Address, id: Hash) { - const allocation = this.allocations[vault]?.[id]; + const key = findAddressKey(this.allocations, vault); + const allocation = key == null ? undefined : this.allocations[key]?.[id]; if (allocation == null) throw new UnknownReallocationAllocationError(vault, id); return allocation; @@ -535,17 +578,31 @@ export class VaultV2BlueReallocationData * ``` */ public getPublicAllocatorConfig(vault: Address) { - const config = this.publicAllocatorConfigs[vault]; + const config = this.getOptionalPublicAllocatorConfig(vault); if (config == null) throw new UnknownReallocationPublicAllocatorConfigError(vault); return config; } + /** + * Gets fetched allocator authorization state, including an explicit absent value. + * + * @param vault - Vault V2 address. + * @returns The allocator config, or `undefined` when the fetched vault has not authorized it. + * @throws {UnknownReallocationPublicAllocatorConfigError} when the fetch state is absent. + */ + private getOptionalPublicAllocatorConfig(vault: Address) { + const key = findAddressKey(this.publicAllocatorConfigs, vault); + if (key == null) + throw new UnknownReallocationPublicAllocatorConfigError(vault); + return this.publicAllocatorConfigs[key]; + } + /** * Gets the BluePublicAllocator-active adapters for a Vault V2. * * @param vault - Vault V2 address. - * @returns The active adapter addresses, or an empty set when none are active. + * @returns The active adapter addresses in their supplied casing, or an empty set when none are active. * @throws {UnknownReallocationActiveAdaptersError} when the active-adapter state is absent. * @example * ```ts @@ -553,7 +610,8 @@ export class VaultV2BlueReallocationData * ``` */ public getActiveAdapters(vault: Address): ReadonlySet
{ - const adapters = this.activeAdapters[vault]; + const key = findAddressKey(this.activeAdapters, vault); + const adapters = key == null ? undefined : this.activeAdapters[key]; if (adapters == null) throw new UnknownReallocationActiveAdaptersError(vault); return adapters; @@ -575,8 +633,11 @@ export class VaultV2BlueReallocationData vault: Address, adapterMarketCapId: Hash, ) { + const key = findAddressKey(this.marketPublicAllocatorConfigs, vault); const config = - this.marketPublicAllocatorConfigs[vault]?.[adapterMarketCapId]; + key == null + ? undefined + : this.marketPublicAllocatorConfigs[key]?.[adapterMarketCapId]; if (config == null) throw new UnknownReallocationMarketPublicAllocatorConfigError( vault, @@ -633,6 +694,8 @@ export class VaultV2BlueReallocationData * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` or `maxPenalty` exceeds WAD. * @throws {NonPositiveInputError} when the operation amount is not positive and planning is enabled. * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationVaultError} when configured vault state is absent. + * @throws {UnknownReallocationPublicAllocatorConfigError} when allocator authorization state is absent. * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent for a vault. * @throws {InsufficientSharedLiquidityError} when selected liquidity cannot cover the absolute shortfall. * @throws {ReallocationWithdrawExceedsMarketSupplyError} when a withdraw exceeds market supply. @@ -823,38 +886,31 @@ export class VaultV2BlueReallocationData ); const reallocations: VaultV2BlueReallocation[] = []; const configuredVaults = Object.keys(data.vaults) as Address[]; - const vaultKeyByLower = new Map( - configuredVaults.map((vault) => [vault.toLowerCase(), vault]), - ); const vaults = Array.from( new Set( [...(options.reallocatableVaults ?? configuredVaults)] - .map((vault) => vaultKeyByLower.get(vault.toLowerCase())) + .map((vault) => findAddressKey(data.vaults, vault)) .filter((vault): vault is Address => vault != null), ), ); + const normalizedMarketId = marketId.toLowerCase(); + const adapterIdsCache = new Map(); while (true) { const candidates = vaults .map((vaultAddress) => { const targetMarket = data.getMarket(marketId); - const vaultContext = _try( - () => ({ - vault: data.getVault(vaultAddress), - publicAllocatorConfig: - data.getPublicAllocatorConfig(vaultAddress), - }), - UnknownDataError, - ); - if (vaultContext == null) return; - const { vault, publicAllocatorConfig } = vaultContext; + const publicAllocatorConfig = + data.getOptionalPublicAllocatorConfig(vaultAddress); + if (publicAllocatorConfig == null) return; + const vault = data.getVault(vaultAddress); if ( !isAddressEqual(publicAllocatorConfig.vault, vaultAddress) || publicAllocatorConfig.penalty > (options.maxPenalty ?? DEFAULT_MAX_REALLOCATION_PENALTY) ) return; - const activeAdapters = data.getActiveAdapters(vaultAddress); + const activeAdapters = [...data.getActiveAdapters(vaultAddress)]; const targetSupplyHeadroom = MathLib.zeroFloorSub( MathLib.MAX_UINT_128, @@ -874,7 +930,7 @@ export class VaultV2BlueReallocationData const targetContext = _try(() => { const [adapterCapId, collateralCapId, adapterMarketCapId] = - adapter.ids(targetMarket.params); + getAdapterIds(adapterIdsCache, adapter, targetMarket); const marketPublicAllocatorConfig = data.getMarketPublicAllocatorConfig( vaultAddress, @@ -889,7 +945,9 @@ export class VaultV2BlueReallocationData marketPublicAllocatorConfig.adapter, adapter.address, ) || - !activeAdapters.has(adapter.address.toLowerCase() as Address) + !activeAdapters.some((activeAdapter) => + isAddressEqual(activeAdapter, adapter.address), + ) ) return; @@ -967,6 +1025,12 @@ export class VaultV2BlueReallocationData continue; if (!isAddressEqual(sourceAdapter.parentVault, vaultAddress)) continue; + if ( + !activeAdapters.some((activeAdapter) => + isAddressEqual(activeAdapter, sourceAdapter.address), + ) + ) + continue; for (const sourceMarketReference of sourceAdapter.markets) { const sourceMarket = data.getMarket(sourceMarketReference.id); @@ -978,11 +1042,15 @@ export class VaultV2BlueReallocationData ) ) continue; - if (sourceMarket.id.toLowerCase() === marketId.toLowerCase()) + if (sourceMarket.id.toLowerCase() === normalizedMarketId) continue; const candidate = _try(() => { - const sourceIds = sourceAdapter.ids(sourceMarket.params); + const sourceIds = getAdapterIds( + adapterIdsCache, + sourceAdapter, + sourceMarket, + ); const [, , sourceAdapterMarketCapId] = sourceIds; const sourceConfig = data.getMarketPublicAllocatorConfig( vaultAddress, @@ -994,9 +1062,6 @@ export class VaultV2BlueReallocationData sourceConfig.adapter, sourceAdapter.address, ) || - !activeAdapters.has( - sourceAdapter.address.toLowerCase() as Address, - ) || !sourceConfig.canPullFromMarket ) return; @@ -1056,19 +1121,36 @@ export class VaultV2BlueReallocationData // shared allocation IDs. Binary search finds the exact largest fit. let lower = 0n; let upper = reallocation.assets; + let probeUpper = true; const reallocationAdapter = data.getAdapter( reallocation.vault, reallocation.to.adapter, ); - const targetIds = reallocationAdapter.ids(targetMarket.params); + const targetIds = getAdapterIds( + adapterIdsCache, + reallocationAdapter, + targetMarket, + ); while (lower < upper) { - const assets = (lower + upper + 1n) / 2n; - const postState = data.cloneWithPublicReallocation({ - reallocation: { ...reallocation, assets }, - targetMarketId: marketId, - timestamp: targetMarket.lastUpdate, - }); + const assets = probeUpper ? upper : (lower + upper + 1n) / 2n; + probeUpper = false; + const postState = _try( + () => + data.cloneWithPublicReallocation({ + reallocation: { ...reallocation, assets }, + targetMarketId: marketId, + timestamp: targetMarket.lastUpdate, + adapterIdsCache, + probe: true, + }), + ReallocationAllocationUnderflowError, + ReallocationAdapterSupplySharesUnderflowError, + ); + if (postState == null) { + upper = assets - 1n; + continue; + } const postVault = postState.getVault(reallocation.vault); // Vault V2 checks relative caps against the transient firstTotalAssets, // which stays fixed after the vault's first allocation in a transaction. @@ -1119,6 +1201,7 @@ export class VaultV2BlueReallocationData reallocation: largest, targetMarketId: marketId, timestamp, + adapterIdsCache, }); } } @@ -1132,6 +1215,8 @@ export class VaultV2BlueReallocationData * @throws {NegativeInputError} when `maxWithdrawalUtilization` or `maxPenalty` is negative. * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` or `maxPenalty` exceeds WAD. * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationVaultError} when configured vault state is absent. + * @throws {UnknownReallocationPublicAllocatorConfigError} when allocator authorization state is absent. * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent for a vault. * @example * ```ts @@ -1166,6 +1251,8 @@ export class VaultV2BlueReallocationData * @throws {NegativeInputError} when `maxWithdrawalUtilization` or `maxPenalty` is negative. * @throws {InputExceedsMaxError} when `maxWithdrawalUtilization` or `maxPenalty` exceeds WAD. * @throws {UnknownReallocationMarketError} when a required market is absent. + * @throws {UnknownReallocationVaultError} when configured vault state is absent. + * @throws {UnknownReallocationPublicAllocatorConfigError} when allocator authorization state is absent. * @throws {UnknownReallocationActiveAdaptersError} when active-adapter state is absent for a vault. * @example * ```ts @@ -1178,27 +1265,28 @@ export class VaultV2BlueReallocationData utilization: bigint = DEFAULT_SUPPLY_TARGET_UTILIZATION, options?: VaultV2BluePublicAllocatorOptions, ) { - const maxWithdrawalUtilization = - options?.enabled === false - ? DEFAULT_WITHDRAWAL_TARGET_UTILIZATION - : resolveMaxWithdrawalUtilization(options?.maxWithdrawalUtilization); - const maxPenalty = - options?.enabled === false - ? DEFAULT_MAX_REALLOCATION_PENALTY - : resolveMaxPenalty(options?.maxPenalty); const timestamp = options?.timestamp == null ? this.getLatestSnapshotTimestamp() : BigInt(options.timestamp); const market = this.getMarket(marketId).accrueInterest(timestamp); - if (DEFAULT_SUPPLY_TARGET_UTILIZATION > utilization) + if ( + options?.enabled === false || + DEFAULT_SUPPLY_TARGET_UTILIZATION > utilization + ) return market.getBorrowToUtilization(utilization); const availableLiquidity = this.computeVaultV2BlueReallocationsAtUtilization({ marketId, - maxWithdrawalUtilization, - options: { ...options, timestamp, maxPenalty }, + maxWithdrawalUtilization: resolveMaxWithdrawalUtilization( + options?.maxWithdrawalUtilization, + ), + options: { + ...options, + timestamp, + maxPenalty: resolveMaxPenalty(options?.maxPenalty), + }, }).reallocations.reduce((total, { assets }) => total + assets, 0n); return MarketUtils.getBorrowToUtilization( { @@ -1224,13 +1312,68 @@ export class VaultV2BlueReallocationData reallocation, targetMarketId, timestamp, + adapterIdsCache = new Map(), + probe = false, }: { readonly reallocation: VaultV2BlueReallocation; readonly targetMarketId: MarketId; readonly timestamp: bigint; + readonly adapterIdsCache?: Map; + readonly probe?: boolean; }) { - const data = this.clone(); - let vault = data.getMutableVault(reallocation.vault); + const sourceVaultKey = findAddressKey( + this.mutableVaults, + reallocation.vault, + ); + const sourceAllocationsKey = findAddressKey( + this.mutableAllocations, + reallocation.vault, + ); + const data = probe + ? new VaultV2BlueReallocationData({ + chainId: this.chainId, + markets: { [targetMarketId]: this.getMarket(targetMarketId) }, + vaults: + sourceVaultKey == null + ? {} + : { [sourceVaultKey]: this.mutableVaults[sourceVaultKey] }, + allocations: + sourceAllocationsKey == null + ? {} + : { + [sourceAllocationsKey]: + this.mutableAllocations[sourceAllocationsKey], + }, + }) + : this.clone(); + if (probe) { + const sourceDonationKey = findAddressKey( + this.donatedPenaltyAssets, + reallocation.vault, + ); + if (sourceDonationKey != null) + data.donatedPenaltyAssets[sourceDonationKey] = + this.donatedPenaltyAssets[sourceDonationKey]!; + const sourceFirstTotalAssetsKey = findAddressKey( + this.firstTotalAssets, + reallocation.vault, + ); + if (sourceFirstTotalAssetsKey != null) + data.firstTotalAssets[sourceFirstTotalAssetsKey] = + this.firstTotalAssets[sourceFirstTotalAssetsKey]!; + } + + const vaultKey = + findAddressKey(data.mutableVaults, reallocation.vault) ?? + reallocation.vault; + const allocationsKey = + findAddressKey(data.mutableAllocations, reallocation.vault) ?? + reallocation.vault; + const donationKey = + findAddressKey(data.donatedPenaltyAssets, reallocation.vault) ?? vaultKey; + const firstTotalAssetsKey = + findAddressKey(data.firstTotalAssets, reallocation.vault) ?? vaultKey; + let vault = data.getMutableVault(vaultKey); const targetMarket = data.getMarket(targetMarketId); const penaltyAssets = @@ -1239,8 +1382,8 @@ export class VaultV2BlueReallocationData reallocation.assets, ); vault.assetBalance += penaltyAssets; - data.donatedPenaltyAssets[reallocation.vault] = - (data.donatedPenaltyAssets[reallocation.vault] ?? 0n) + penaltyAssets; + data.donatedPenaltyAssets[donationKey] = + (data.donatedPenaltyAssets[donationKey] ?? 0n) + penaltyAssets; if (reallocation.from.type === "market") { const sourceAdapter = data.getMutableAdapter( @@ -1248,7 +1391,11 @@ export class VaultV2BlueReallocationData reallocation.from.adapter, ); const sourceMarket = data.getMarket(reallocation.from.marketParams.id); - const sourceIds = sourceAdapter.ids(sourceMarket.params); + const sourceIds = getAdapterIds( + adapterIdsCache, + sourceAdapter, + sourceMarket, + ); const [, , sourceAdapterMarketCapId] = sourceIds; const currentSupplyShares = sourceAdapter.supplyShares[sourceMarket.id] ?? 0n; @@ -1286,7 +1433,7 @@ export class VaultV2BlueReallocationData change: sourceChange, }); } - data.mutableAllocations[reallocation.vault]![id] = { + data.mutableAllocations[allocationsKey]![id] = { ...allocation, allocation: nextAllocation, }; @@ -1294,7 +1441,7 @@ export class VaultV2BlueReallocationData vault.assetBalance += reallocation.assets; } - if (data.firstTotalAssets[reallocation.vault] == null) { + if (data.firstTotalAssets[firstTotalAssetsKey] == null) { // Vault V2's transient firstTotalAssets tracks the first allocation in a // transaction independently from elapsed time. Later allocations must not // recompute the denominator, even if their simulated balances have changed. @@ -1310,15 +1457,19 @@ export class VaultV2BlueReallocationData } else { vault = vault.accrueInterest(timestamp).vault; } - data.mutableVaults[reallocation.vault] = vault; - data.firstTotalAssets[reallocation.vault] = vault._totalAssets; + data.mutableVaults[vaultKey] = vault; + data.firstTotalAssets[firstTotalAssetsKey] = vault._totalAssets; } const targetAdapter = data.getMutableAdapter( reallocation.vault, reallocation.to.adapter, ); - const targetIds = targetAdapter.ids(targetMarket.params); + const targetIds = getAdapterIds( + adapterIdsCache, + targetAdapter, + targetMarket, + ); const [, , targetAdapterMarketCapId] = targetIds; const currentTargetMarket = data.getMarket(targetMarket.id); @@ -1353,7 +1504,7 @@ export class VaultV2BlueReallocationData change: targetChange, }); } - data.mutableAllocations[reallocation.vault]![id] = { + data.mutableAllocations[allocationsKey]![id] = { ...allocation, allocation: nextAllocation, };