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 new file mode 100644 index 000000000..808e99ace --- /dev/null +++ b/.changeset/brave-vaults-reallocate.md @@ -0,0 +1,17 @@ +--- +"@morpho-org/morpho-ts": minor +"@morpho-org/blue-sdk": minor +"@morpho-org/blue-sdk-viem": minor +"@morpho-org/morpho-sdk": 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 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; 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. + +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 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/.changeset/calm-vault-v2-liquidity.md b/.changeset/calm-vault-v2-liquidity.md new file mode 100644 index 000000000..154911a63 --- /dev/null +++ b/.changeset/calm-vault-v2-liquidity.md @@ -0,0 +1,5 @@ +--- +"@morpho-org/liquidity-sdk-viem": minor +--- + +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. 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-07-29-vault-v2-public-allocator-shared-liquidity.md b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md new file mode 100644 index 000000000..13a318609 --- /dev/null +++ b/docs/tibs/TIB-2026-07-29-vault-v2-public-allocator-shared-liquidity.md @@ -0,0 +1,459 @@ +# TIB-2026-07-29: Vault V2 public-allocator shared liquidity + +| Field | Value | +| ---------- | ---------------------------------------------------------------- | +| **Status** | Accepted | +| **Date** | 2026-07-29 | +| **Author** | @foulques | +| **Scope** | `morpho-sdk`, `liquidity-sdk-viem`, `blue-sdk-viem`, `blue-sdk`, and `morpho-ts` | + +## Context + +The SDK already models liquidity that a MetaMorpho Vault V1 can move into a +Morpho Blue market through PublicAllocator V1: + +```text +MorphoBlue.getReallocationData() + → VaultV1ReallocationData.computeVaultV1Reallocations() # discovery + → computeVaultV1Reallocations() # borrow/withdraw planner + → VaultV1BlueReallocation[] + → PublicAllocator.reallocateTo(...) +``` + +The historical V1 names `ReallocationData`, `InputReallocationData`, +`computeReallocations()`, `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 `VaultV2ReallocationData.computeVaultV2Reallocations(...)` for greedy, + largest-first discovery. +- 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 + 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, + 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`, `setCanPullFromMarket`, + or `setPenalty`. +- No penalty-efficiency optimizer beyond an explicit maximum-penalty + filter. Retained candidates are ranked by obtainable assets. + +## Public API and naming + +| Concern | Canonical symbol | +| --- | --- | +| 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 | `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 | `VaultV2BluePublicAllocatorOptions` | +| V2 config | `VaultV2PublicAllocatorConfig`, `VaultV2MarketPublicAllocatorConfig` | +| Fetchers | `fetchVaultV2PublicAllocatorConfig`, `fetchVaultV2MarketPublicAllocatorConfig`, `fetchVaultV2PublicAllocatorData` | + +`BluePublicAllocatorReallocation` was unreleased relative to `origin/main` and +is renamed directly to `VaultV2BlueReallocation`; it has no compatibility +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: + +```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 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 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 + +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 +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 canPullFromMarket; +mapping(address vault => mapping(address adapter => bool)) public isActiveAdapter; +mapping(address vault => VaultData) public vaultData; + +function reallocate( + address vault, + address deallocateAdapter, + MarketParams calldata deallocateMarketParams, + address allocateAdapter, + MarketParams calldata allocateMarketParams, + uint128 assets, + uint64 penalty +) external; + +function allocateFromIdle( + address vault, + address adapter, + MarketParams calldata marketParams, + 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. 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`. + +## 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 + collateral; +3. `marketParamsId(adapter, params)` — unique to an adapter/market pair. + +State is therefore keyed by `(vault, derivedId)`, not by a projected +`(vault, adapter, market)` tuple: + +```ts +export interface InputVaultV2ReallocationData { + readonly chainId: number; + readonly allocator: Address; + readonly markets?: Readonly>; + readonly vaults?: Readonly>; + readonly allocations?: Readonly< + Record>> + >; + readonly publicAllocatorConfigs?: Readonly< + Record + >; + readonly marketPublicAllocatorConfigs?: Readonly< + Record< + Address, + Readonly> + > + >; +} +``` + +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`. + +## Fetching + +`vaultV2BluePublicAllocatorAbi` includes the three allocator mapping reads and +`vaultData`. Fetchers always take the allocator address explicitly: + +- `fetchVaultV2PublicAllocatorConfig(allocator, vault, client, parameters?)`; +- `fetchVaultV2MarketPublicAllocatorConfig(allocator, vault, adapter, + marketParamsId, client, parameters?)`; +- `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. + +Only `AccrualVaultV2MorphoMarketV1AdapterV2` adapters participate. Other +adapter classes are ignored even if an allocator reports them as active. + +## Cap headroom + +`VaultV2Utils.allocationHeadroom(allocation, firstTotalAssets)` is the single +pure implementation of Vault V2 absolute/relative-cap capacity: + +```text +absolute = zeroFloorSub(absoluteCap, allocation) +relative = zeroFloorSub(mulDivDown(firstTotalAssets, relativeCap, WAD), allocation) +headroom = relativeCap == WAD ? absolute : min(absolute, relative) +``` + +It returns both the capacity and the binding `CapacityLimitReason`. +`AccrualVaultV2.maxDeposit` delegates to it, preserving its existing behavior. + +## Accrual and untracked interest + +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`: + +```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. + +## Candidate gates and bounds + +A candidate exists only when: + +- 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; +- 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 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: + +```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. 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; +- allocator target-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 | +| --- | --- | --- | +| 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 | `+= penaltyAssets`, then `+= assets`, then `-= assets` | `+= penaltyAssets`, then `-= assets` | +| 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 +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 + +`computeVaultV2Reallocations` 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. Both phases +ignore vaults above the configured `maxPenalty` threshold. + +The flat calls are capped in discovery order to the required amount. Every +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; +- `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. 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 +Blue market matches the target, regardless of adapter. + +`VaultV2ReallocationData` 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. +- `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`. +- 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 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. +- 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 + revision; generated queries alone do not detect upstream drift. + +## Dependencies + +- `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/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) +- `packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts` +- `packages/morpho-sdk/src/entities/vaultV2ReallocationData.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/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..69346666d --- /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 canPullFromMarket; + bool isActiveAdapter; +} + +struct VaultV2AllocationResponse { + bytes32 id; + uint256 absoluteCap; + uint256 relativeCap; + uint256 allocation; +} + +struct VaultV2PublicAllocatorResponse { + bool canPullFromIdle; + uint64 penalty; + 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.canPullFromIdle, res.penalty) = 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), + canPullFromMarket: allocator.canPullFromMarket(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/fixtures/BluePublicAllocatorReadFixture.sol b/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol new file mode 100644 index 000000000..a25a9e00b --- /dev/null +++ b/packages/blue-sdk-viem/contracts/vault-v2/fixtures/BluePublicAllocatorReadFixture.sol @@ -0,0 +1,31 @@ +// 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/contracts/vault-v2/interfaces/IBluePublicAllocator.sol b/packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol new file mode 100644 index 000000000..b6b788239 --- /dev/null +++ b/packages/blue-sdk-viem/contracts/vault-v2/interfaces/IBluePublicAllocator.sol @@ -0,0 +1,10 @@ +// 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 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 canPullFromIdle, uint64 penalty); +} diff --git a/packages/blue-sdk-viem/package.json b/packages/blue-sdk-viem/package.json index 29cadd0b4..2cc01675d 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.4.0", - "@morpho-org/morpho-ts": "^2.7.0", + "@morpho-org/blue-sdk": "^6.5.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..821a7845c 100644 --- a/packages/blue-sdk-viem/src/abis.ts +++ b/packages/blue-sdk-viem/src/abis.ts @@ -15,6 +15,7 @@ export { vaultV1AdapterAbi, vaultV1AdapterFactoryAbi, vaultV2Abi, + vaultV2BluePublicAllocatorAbi, vaultV2FactoryAbi, whitelistControllerAggregatorV2Abi, wrappedBackedTokenAbi, 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 new file mode 100644 index 000000000..1f1f89e92 --- /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 { vaultV2Abi, vaultV2BluePublicAllocatorAbi } 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, + canPullFromIdle: true, + penalty: 12n, + }, + marketPublicAllocatorConfigs: { + [marketParamsId]: { + allocator: ALLOCATOR, + vault: VAULT, + adapter: ADAPTER, + marketParamsId, + absoluteCap: 500n, + canPullFromMarket: 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: vaultV2BluePublicAllocatorAbi, + functionName: "vaultData", + result: [true, 12n], + }); + mockRead(handle, { + address: ALLOCATOR, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "absoluteCap", + result: 500n, + }); + mockRead(handle, { + address: ALLOCATOR, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "canPullFromMarket", + result: true, + }); + 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: 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", { + canPullFromIdle: true, + penalty: 12n, + marketConfigs: [ + { + adapter: ADAPTER, + marketParamsId, + absoluteCap: 500n, + canPullFromMarket: 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..b70af358a --- /dev/null +++ b/packages/blue-sdk-viem/src/fetch/vault-v2/VaultV2PublicAllocatorConfig.ts @@ -0,0 +1,334 @@ +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 { vaultV2Abi, vaultV2BluePublicAllocatorAbi } 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-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 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 +export async function fetchVaultV2PublicAllocatorConfig( + allocator: Address, + vault: Address, + client: Client, + parameters: FetchParameters = {}, +): Promise { + const [canPullFromIdle, penalty] = await readContract(client, { + ...parameters, + address: allocator, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "vaultData", + args: [vault], + }); + + return { + allocator, + vault, + canPullFromIdle, + penalty, + }; +} + +/** + * 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. + * @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 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 +export async function fetchVaultV2MarketPublicAllocatorConfig( + allocator: Address, + vault: Address, + adapter: Address, + marketParamsId: Hash, + client: Client, + parameters: FetchParameters = {}, +): Promise { + const [absoluteCap, canPullFromMarket, isActiveAdapter] = await Promise.all([ + readContract(client, { + ...parameters, + address: allocator, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "absoluteCap", + args: [vault, marketParamsId], + }), + readContract(client, { + ...parameters, + address: allocator, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "canPullFromMarket", + args: [vault, marketParamsId], + }), + readContract(client, { + ...parameters, + address: allocator, + abi: vaultV2BluePublicAllocatorAbi, + functionName: "isActiveAdapter", + args: [vault, adapter], + }), + ]); + + return { + allocator, + vault, + adapter, + marketParamsId, + absoluteCap, + canPullFromMarket, + 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. + * @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 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 +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, + canPullFromIdle: result.canPullFromIdle, + penalty: result.penalty, + } 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/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 new file mode 100644 index 000000000..b9ed65011 --- /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: "canPullFromIdle", + type: "bool", + }, + { + internalType: "uint64", + name: "penalty", + type: "uint64", + }, + { + components: [ + { + internalType: "address", + name: "adapter", + type: "address", + }, + { + internalType: "bytes32", + name: "marketParamsId", + type: "bytes32", + }, + { + internalType: "uint256", + name: "absoluteCap", + type: "uint256", + }, + { + internalType: "bool", + name: "canPullFromMarket", + 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 = + "0x60808060405234601557610808908161001a8239f35b5f80fdfe60a0806040526004361015610012575f80fd5b5f3560e01c635938912f14610025575f80fd5b3461030a57608036600319011261030a576004356001600160a01b03811660808190520361030a576024356001600160a01b0381169081900361030a576044359067ffffffffffffffff821161030a573660238301121561030a5781600401359167ffffffffffffffff831161030a573660248460061b8301011161030a576064359367ffffffffffffffff851161030a573660238601121561030a5784600401359067ffffffffffffffff821161030a573660248360051b8801011161030a576100ef81610703565b5f815260208101955f8752604082019660608852606083019260608452604051636b97fbcd60e11b81528760048201526040816024816080515afa8015610316575f915f916106a8575b5067ffffffffffffffff168352151581526101538861077e565b610160604051918261074f565b888152601f1961016f8a61077e565b015f5b81811061067357505089525f5b88811015610399578060061b87019061019a60248301610796565b60405163011f009b60e31b81526001600160a01b038b166004820152604480850135602483018190529294919060209082908190810103816080515afa908115610316578c84915f93610362575b506040516369f1e26b60e01b81526001600160a01b03909116600482015260248101919091529160208380604481015b03816080515afa928315610316575f93610321575b50602461023a9101610796565b92604051936366faa83960e01b85528d600486015260018060a01b031660248501526020846044816080515afa938415610316575f946102c9575b50928492600196926102c2956040519461028e86610733565b898060a01b031685526020850152604084015215156060830152151560808201528d51906102bc83836107aa565b526107aa565b500161017f565b92959193506020833d821161030e575b816102e66020938361074f565b8101031261030a576001956102c2946102ff8795610771565b955092965092610275565b5f80fd5b3d91506102d9565b6040513d5f823e3d90fd5b9092506020813d821161035a575b8161033c6020938361074f565b8101031261030a57602461035261023a92610771565b93915061022d565b3d915061032f565b925050506020813d8211610391575b8161037e6020938361074f565b8101031261030a5751828c6102186101e8565b3d9150610371565b50869550886103a78661077e565b6103b4604051918261074f565b868152601f196103c38861077e565b015f5b81811061064457505085525f5b868110156105415760248160051b860101359060405191632f0374dd60e21b83528060048401526020836024818d5afa928315610316575f9361050e575b5060405163a68bafa360e01b8152600481018290526020816024818e5afa8015610316575f906104dc575b60405163c69507dd60e01b81526004810184905291506020826024818f5afa918215610316575f926104a6575b5091839161049f936001966040519361048185610703565b84526020840152604083015260608201528951906102bc83836107aa565b50016103d3565b9150916020823d82116104d4575b816104c16020938361074f565b8101031261030a57905190916001610469565b3d91506104b4565b506020813d8211610506575b816104f56020938361074f565b8101031261030a576024905161043c565b3d91506104e8565b9092506020813d8211610539575b816105296020938361074f565b8101031261030a5751918a610411565b3d915061051c565b506040805160208082529351151584820152935167ffffffffffffffff16908401525160806060840152805160a08401819052839260c084019287929101905f5b8181106105f1575050505190601f19838203016080840152602080835192838152019201905f5b8181106105b7575050500390f35b91935091602060806001926060875180518352848101518584015260408101516040840152015160608201520194019101918493926105a9565b825180516001600160a01b03168652602081810151818801526040808301519088015260608083015115159088015260809182015115159187019190915287965060a09095019490920191600101610582565b60209060405161065381610703565b5f81525f838201525f60408201525f6060820152828286010152016103c6565b60209060405161068281610733565b5f81525f838201525f60408201525f60608201525f608082015282828601015201610172565b9150506040813d6040116106fb575b816106c46040938361074f565b8101031261030a5760206106d782610771565b9101519067ffffffffffffffff8216820361030a579067ffffffffffffffff610139565b3d91506106b7565b6080810190811067ffffffffffffffff82111761071f57604052565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761071f57604052565b90601f8019910116810190811067ffffffffffffffff82111761071f57604052565b5190811515820361030a57565b67ffffffffffffffff811161071f5760051b60200190565b356001600160a01b038116810361030a5790565b80518210156107be5760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220a77aa844da5d389376ee1433d996e0bb253977abafbd38ee8232086c67b7d22164736f6c63430008240033"; 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..3b9d68b0a --- /dev/null +++ b/packages/blue-sdk-viem/test/fixtures/BluePublicAllocatorReadFixture.ts @@ -0,0 +1,195 @@ +/** @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/blue-sdk/AGENTS.md b/packages/blue-sdk/AGENTS.md index 31e1f3f49..c88389b1c 100644 --- a/packages/blue-sdk/AGENTS.md +++ b/packages/blue-sdk/AGENTS.md @@ -10,6 +10,9 @@ - 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. +- `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 c085d70cd..4d6d25072 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/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/VaultV2.ts b/packages/blue-sdk/src/vault/v2/VaultV2.ts index 41ae77f8e..1a0ed843f 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 { @@ -177,28 +178,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..fa55be003 --- /dev/null +++ b/packages/blue-sdk/src/vault/v2/VaultV2PublicAllocatorConfig.ts @@ -0,0 +1,31 @@ +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. */ + 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. */ +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 pull assets from this adapter-market pair. */ + readonly canPullFromMarket: 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/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..fbce70c31 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,53 @@ 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 { + 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); +} ``` +`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 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 14fdf048d..16b50c671 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.0", "author": "Morpho Association ", "contributors": [ @@ -29,10 +29,10 @@ "codegen": "graphql-codegen --config codegen.ts" }, "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/morpho-ts": "^2.7.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.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 new file mode 100644 index 000000000..5a5cfeb5e --- /dev/null +++ b/packages/liquidity-sdk-viem/src/api/rest.ts @@ -0,0 +1,540 @@ +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 new file mode 100644 index 000000000..f12e1be53 --- /dev/null +++ b/packages/liquidity-sdk-viem/src/errors.ts @@ -0,0 +1,144 @@ +/** + * 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 dd8245678..faf3d0d14 100644 --- a/packages/liquidity-sdk-viem/src/index.ts +++ b/packages/liquidity-sdk-viem/src/index.ts @@ -1 +1,3 @@ +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 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/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts new file mode 100644 index 000000000..e09c82970 --- /dev/null +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.test.ts @@ -0,0 +1,515 @@ +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 new file mode 100644 index 000000000..aa4867143 --- /dev/null +++ b/packages/liquidity-sdk-viem/src/vaultV2LiquidityLoader.ts @@ -0,0 +1,566 @@ +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); + } +} diff --git a/packages/morpho-sdk/AGENTS.md b/packages/morpho-sdk/AGENTS.md index 72236ea8c..8d4e739ee 100644 --- a/packages/morpho-sdk/AGENTS.md +++ b/packages/morpho-sdk/AGENTS.md @@ -26,7 +26,9 @@ 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** — 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`. +- **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 @@ -38,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. +- **`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 849a9e1a9..ea756f972 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -15,20 +15,22 @@ 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. -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). +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 **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 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`, `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. -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 Blue Public Allocator calls. The business logic lives in the **order and selection of actions**, not in a dedicated contract. ## Flows overview @@ -37,8 +39,10 @@ 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 `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)_ | @@ -66,11 +70,20 @@ 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 +`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** 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 +### 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 @@ -101,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` or a `reallocateTo` (native fee) 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 12558189b..2fbe18643 100644 --- a/packages/morpho-sdk/README.md +++ b/packages/morpho-sdk/README.md @@ -220,12 +220,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| BPA[Blue Public Allocator] end subgraph Midnight Flow @@ -254,7 +255,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/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 7a8be98ca..150b1de18 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/abis.ts b/packages/morpho-sdk/src/abis.ts index 05b9540a8..4d849e71e 100644 --- a/packages/morpho-sdk/src/abis.ts +++ b/packages/morpho-sdk/src/abis.ts @@ -21,6 +21,7 @@ export { vaultV1AdapterAbi, vaultV1AdapterFactoryAbi, vaultV2Abi, + vaultV2BluePublicAllocatorAbi, vaultV2FactoryAbi, whitelistControllerAggregatorV2Abi, wrappedBackedTokenAbi, diff --git a/packages/morpho-sdk/src/actions/AGENTS.md b/packages/morpho-sdk/src/actions/AGENTS.md index 8c1b08c30..9a8b98728 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`). 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/AGENTS.md b/packages/morpho-sdk/src/actions/blue/AGENTS.md index 775e8b12e..0d73a1ef0 100644 --- a/packages/morpho-sdk/src/actions/blue/AGENTS.md +++ b/packages/morpho-sdk/src/actions/blue/AGENTS.md @@ -28,17 +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) | `[reallocateTo × N] → morphoBorrow` | +| `borrow` (with reallocations) | `[V2 penalty transfer?] → [allocator reallocation × N] → morphoBorrow` | | `supplyCollateralBorrow` | `[nativeWrap?] → [erc20Transfer?] → morphoSupplyCollateral → morphoBorrow` | -| `supplyCollateralBorrow` (with reallocations) | `[nativeWrap?] → [erc20Transfer?] → morphoSupplyCollateral → [reallocateTo × 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) | `[reallocateTo × N] → morphoWithdraw` | - -`BundlerAction.encodeBundle` derives `tx.value` from native wrapping calls and reallocation fees. +| `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. 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 new file mode 100644 index 000000000..7a0186399 --- /dev/null +++ b/packages/morpho-sdk/src/actions/blue/borrow.bluePublicAllocator.test.ts @@ -0,0 +1,194 @@ +import { ChainId, getChainAddresses, MarketParams } from "@morpho-org/blue-sdk"; +import { vaultV2BluePublicAllocatorAbi as canonicalVaultV2BluePublicAllocatorAbi } from "@morpho-org/blue-sdk-viem"; +import { decodeFunctionData, erc20Abi } from "viem"; +import { describe, expect, test } from "vitest"; +import { + bundler3Abi, + generalAdapter1Abi, + publicAllocatorAbi, + vaultV2BluePublicAllocatorAbi, +} from "../../abis.js"; +import { + type BlueReallocation, + InconsistentReallocationPenaltyError, +} from "../../types/index.js"; +import { blueBorrow } from "./borrow.js"; + +const allocator = "0x0000000000000000000000000000000000000011"; +const vaultV1 = "0x0000000000000000000000000000000000000012"; +const sourceAdapter = "0x0000000000000000000000000000000000000013"; +const targetAdapter = "0x0000000000000000000000000000000000000014"; +const receiver = "0x0000000000000000000000000000000000000015"; +const vaultV2 = "0x0000000000000000000000000000000000000016"; + +const targetMarket = new MarketParams({ + loanToken: "0x0000000000000000000000000000000000000021", + collateralToken: "0x0000000000000000000000000000000000000022", + oracle: "0x0000000000000000000000000000000000000023", + irm: "0x0000000000000000000000000000000000000024", + lltv: 860_000_000_000_000_000n, +}); + +const sourceMarket = new MarketParams({ + loanToken: targetMarket.loanToken, + collateralToken: "0x0000000000000000000000000000000000000032", + oracle: "0x0000000000000000000000000000000000000033", + irm: targetMarket.irm, + lltv: targetMarket.lltv, +}); + +describe("blueBorrow Blue Public Allocator", () => { + test("default", () => { + const { + bundler3: { bundler3 }, + } = 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", + 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: 5n, + }, + ]; + + const tx = blueBorrow({ + market: { chainId: ChainId.EthMainnet, marketParams: targetMarket }, + args: { + amount: 1n, + minSharePrice: 0n, + receiver, + reallocations, + }, + }); + + 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(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[1]!.data, + }); + expect(publicAllocatorCall.functionName).toBe("reallocateTo"); + expect(publicAllocatorCall.args[0]).toBe(vaultV1); + expect( + decodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + 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[5]!.data, + }); + expect(idleCall.functionName).toBe("allocateFromIdle"); + expect(idleCall.args[0]).toBe(vaultV2); + 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(idleCall.args[4]).toBe(5n); + expect( + 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 8176d6d94..aaa7d20f4 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"; @@ -28,8 +28,8 @@ 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). */ - reallocations?: readonly VaultReallocation[]; + /** Public Allocator V1 or V2 reallocations to execute before borrowing. */ + 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 @@ -47,17 +47,18 @@ 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. 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 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. @@ -65,7 +66,12 @@ export interface BlueBorrowParams { * typed `action` discriminator the simulation layer consumes. * @throws {NonPositiveInputError} when `amount <= 0n` or any reallocation withdrawal amount * is non-positive. - * @throws {NegativeInputError} when `minSharePrice < 0n` or any reallocation fee is negative. + * @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 {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 * `reallocation.withdrawals` is empty. * @throws {ReallocationWithdrawalOnTargetMarketError} from `buildReallocationActions` when any @@ -108,15 +114,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({ @@ -140,6 +152,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 991a1f66a..cd8cb7f7d 100644 --- a/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts +++ b/packages/morpho-sdk/src/actions/blue/buildReallocationActions.ts @@ -1,51 +1,137 @@ -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 { VaultReallocation } from "../../types/index.js"; +import type { BlueReallocation } from "../../types/index.js"; /** - * Builds reallocation bundler actions and computes the total fee. + * Builds Public Allocator V1 and Blue Public Allocator actions and their costs. * - * 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. + * 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 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 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 {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. * @internal */ -export const buildReallocationActions = ( - reallocations: readonly VaultReallocation[], - targetMarketParams: MarketParams, -): { readonly actions: Action[]; readonly fee: bigint } => { +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; + readonly penaltyAssets: bigint; +} => { + // Validate the action descriptors before encoding; the validator returns void. validateReallocations(reallocations, targetMarketParams.id); - const fee = reallocations.reduce((sum, r) => sum + r.fee, 0n); + let fee = 0n; const actions: Action[] = []; + const penaltyAssets = computeVaultV2ReallocationPenaltyAssets(reallocations); + + if (penaltyAssets > 0n) { + const { + bundler3: { bundler3, generalAdapter1 }, + } = getChainAddresses(chainId); + 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) { + if (reallocation.type === "bluePublicAllocator") { + if (reallocation.from.type === "market") { + actions.push({ + type: "vaultV2BluePublicAllocatorReallocate", + args: [ + reallocation.allocator, + reallocation.vault, + reallocation.from.adapter, + reallocation.from.marketParams, + reallocation.to.adapter, + targetMarketParams, + reallocation.assets, + reallocation.penalty, + false, + ], + }); + } else { + actions.push({ + type: "vaultV2BluePublicAllocatorAllocateFromIdle", + args: [ + reallocation.allocator, + reallocation.vault, + reallocation.to.adapter, + targetMarketParams, + reallocation.assets, + reallocation.penalty, + false, + ], + }); + } + continue; + } - for (const r of reallocations) { 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 }; + return { actions, fee, penaltyAssets }; }; 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 287f79555..962e6b316 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"; @@ -44,8 +44,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; - /** PublicAllocator reallocations into the target market, run before the bundle. Fees add to `tx.value`. */ - targetReallocations?: readonly VaultReallocation[]; + /** 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 * prepended to the bundle so GeneralAdapter1 is authorized in-bundle instead of via a @@ -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 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`. @@ -111,8 +112,13 @@ 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` 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 {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`, or any reallocation fee 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. @@ -269,15 +275,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({ @@ -305,6 +317,7 @@ export const blueRefinance = ({ maxRepaySharePrice, user, reallocationFee, + reallocationPenaltyAssets, }, }, }); 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 276a5e304..506ac6aea 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -1,10 +1,11 @@ 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 { 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"; @@ -35,8 +35,8 @@ 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). */ - reallocations?: readonly VaultReallocation[]; + /** Public Allocator V1 or V2 reallocations to execute before borrowing. */ + 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 @@ -50,9 +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 - * `reallocateTo` calls → `morphoBorrow`. When `nativeAmount > 0`, native ETH is wrapped via - * `GeneralAdapter1.wrapNative()` before the supply leg. + * 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. 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. @@ -69,27 +73,32 @@ 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 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 + * 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` 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 {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 * token is not the chain's wNative. * @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 @@ -154,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) { @@ -164,7 +191,7 @@ export const blueSupplyCollateralBorrow = ({ ...buildAssetFundingActions({ chainId, asset: marketParams.collateralToken, - erc20Amount: amount, + erc20Amount: erc20FundingAmount, nativeAmount: nativeAmount ?? 0n, requirementSignature, }), @@ -174,14 +201,7 @@ export const blueSupplyCollateralBorrow = ({ type: "morphoSupplyCollateral", args: [marketParams, totalCollateral, onBehalf, [], false], }); - - let reallocationFee = 0n; - - if (reallocations && reallocations.length > 0) { - const result = buildReallocationActions(reallocations, marketParams); - actions.push(...result.actions); - reallocationFee = result.fee; - } + actions.push(...reallocationResult.actions); actions.push({ type: "morphoBorrow", @@ -206,7 +226,8 @@ export const blueSupplyCollateralBorrow = ({ onBehalf, receiver, nativeAmount, - reallocationFee, + 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..46061054b --- /dev/null +++ b/packages/morpho-sdk/src/actions/blue/vaultV2Reallocations.integration.test.ts @@ -0,0 +1,588 @@ +import { + getChainAddresses, + MarketParams, + MathLib, + marketParamsAbi, +} from "@morpho-org/blue-sdk"; +import { + blueAbi, + fetchAccrualVaultV2, + fetchMarket, + fetchVaultV2PublicAllocatorData, + 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, + keccak256, + 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 { VaultV2ReallocationData } from "../../entities/vaultV2ReallocationData.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); + }); + + 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/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 5245da888..6bb36b47a 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"; @@ -32,11 +32,11 @@ 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 `computeVaultV1Reallocations({ 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 @@ -56,10 +56,11 @@ 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. 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 @@ -73,17 +74,22 @@ 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 + * 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` 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 {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. * @throws {ReallocationWithdrawalOnTargetMarketError} when a reallocation withdrawal references @@ -141,15 +147,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({ @@ -177,6 +189,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 ad10d7f0c..1445c6666 100644 --- a/packages/morpho-sdk/src/bundler/actions.test.ts +++ b/packages/morpho-sdk/src/bundler/actions.test.ts @@ -8,6 +8,7 @@ import { erc2612Abi, permit2Abi, publicAllocatorAbi, + vaultV2BluePublicAllocatorAbi, } from "@morpho-org/blue-sdk-viem"; import fc from "fast-check"; import { @@ -15,6 +16,7 @@ import { bytesToHex, decodeFunctionData, encodeAbiParameters, + erc20Abi, type Hex, isAddressEqual, keccak256, @@ -46,6 +48,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"; @@ -76,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({ @@ -342,6 +351,42 @@ describe("BundlerAction", () => { args, }) satisfies Action, ), + fc + .tuple( + addressArbitrary, + addressArbitrary, + addressArbitrary, + marketArbitrary, + addressArbitrary, + marketArbitrary, + amountArbitrary, + penaltyArbitrary, + fc.constant(false), + ) + .map( + (args) => + ({ + type: "vaultV2BluePublicAllocatorReallocate", + args, + }) satisfies Action, + ), + fc + .tuple( + addressArbitrary, + addressArbitrary, + addressArbitrary, + marketArbitrary, + amountArbitrary, + penaltyArbitrary, + fc.constant(false), + ) + .map( + (args) => + ({ + type: "vaultV2BluePublicAllocatorAllocateFromIdle", + args, + }) satisfies Action, + ), fc.tuple(amountArbitrary, addressArbitrary, skipRevertArbitrary).map( (args) => ({ @@ -570,6 +615,40 @@ describe("BundlerAction", () => { expect(calls[0]?.value).toBe(5n); }); + test("encodeBundle keeps Blue Public Allocator calls nonpayable", () => { + const tx = BundlerAction.encodeBundle(chainId, [ + { + type: "vaultV2BluePublicAllocatorReallocate", + args: [ + allocator, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 1n, + 2n, + false, + ], + }, + { + type: "vaultV2BluePublicAllocatorAllocateFromIdle", + args: [allocator, vault, allocateAdapter, market, 3n, 4n, false], + }, + ]); + + 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([ + 0n, + 0n, + 0n, + 0n, + ]); + }); + test("encodeBundle includes callback action values in transaction value", () => { const tx = BundlerAction.encodeBundle(chainId, [ { @@ -972,6 +1051,50 @@ describe("BundlerAction", () => { false, ), ], + [ + "vaultV2BluePublicAllocatorReallocate", + { + type: "vaultV2BluePublicAllocatorReallocate", + args: [ + allocator, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 20n, + 21n, + false, + ], + }, + BundlerAction.vaultV2BluePublicAllocatorReallocate( + allocator, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 20n, + 21n, + false, + ), + ], + [ + "vaultV2BluePublicAllocatorAllocateFromIdle", + { + type: "vaultV2BluePublicAllocatorAllocateFromIdle", + args: [allocator, vault, allocateAdapter, market, 22n, 23n, false], + }, + BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + allocator, + vault, + allocateAdapter, + market, + 22n, + 23n, + false, + ), + ], [ "wrapNative", { @@ -1445,6 +1568,181 @@ describe("BundlerAction", () => { expect(decoded.args).toEqual([vault, withdrawals, market]); }); + 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, + ); + 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: false, + }); + const decoded = decodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + data: call!.data, + }); + + expect(call!.to).toBe(allocator); + expect(call!.value).toBe(0n); + expect(call!.skipRevert).toBe(false); + 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, + vault, + deallocateAdapter, + market, + allocateAdapter, + market, + 1n, + 0n, + 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, + 1n, + 0n, + ]); + }); + + 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] = + BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + allocator, + vault, + allocateAdapter, + market, + 1_000_000n, + penalty, + ); + 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: false, + }); + const decoded = decodeFunctionData({ + abi: vaultV2BluePublicAllocatorAbi, + data: call!.data, + }); + + expect(call!.to).toBe(allocator); + expect(call!.value).toBe(0n); + expect(call!.skipRevert).toBe(false); + 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, + vault, + allocateAdapter, + market, + 1n, + 0n, + 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, 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 6d27997ac..9801eeff7 100644 --- a/packages/morpho-sdk/src/bundler/actions.ts +++ b/packages/morpho-sdk/src/bundler/actions.ts @@ -7,11 +7,13 @@ import { erc2612Abi, permit2Abi, publicAllocatorAbi, + vaultV2BluePublicAllocatorAbi, } from "@morpho-org/blue-sdk-viem"; import { type Address, encodeAbiParameters, encodeFunctionData, + erc20Abi, type Hex, isAddressEqual, keccak256, @@ -21,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, @@ -342,6 +345,14 @@ export namespace BundlerAction { case "reallocateTo": { return BundlerAction.publicAllocatorReallocateTo(chainId, ...args); } + case "vaultV2BluePublicAllocatorReallocate": { + return BundlerAction.vaultV2BluePublicAllocatorReallocate(...args); + } + case "vaultV2BluePublicAllocatorAllocateFromIdle": { + return BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + ...args, + ); + } case "wrapNative": { return BundlerAction.wrapNative(chainId, ...args); } @@ -1441,6 +1452,229 @@ 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. + * @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 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, + * 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 = + * "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: usdc, + * collateralToken: weth, + * oracle: ethUsdOracle, + * irm: adaptiveCurveIrm, + * lltv: 860_000_000_000_000_000n, + * } satisfies InputMarketParams; + * const targetMarket = { + * ...sourceMarket, + * collateralToken: wbtc, + * } satisfies InputMarketParams; + * + * const calls: BundlerCall[] = BundlerAction.vaultV2BluePublicAllocatorReallocate( + * allocatorFixture, + * keyrockUsdcVault, + * sourceAdapterFixture, + * sourceMarket, + * targetAdapterFixture, + * targetMarket, + * 1_000_000n, + * 1_000_000_000_000_000n, + * ); + * // Bundler3 approves 1_000 USDC units, then calls `reallocate` with zero native value. + * ``` + */ + // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call + export function vaultV2BluePublicAllocatorReallocate( + allocator: Address, + vault: Address, + deallocateAdapter: Address, + deallocateMarket: InputMarketParams, + allocateAdapter: Address, + allocateMarket: InputMarketParams, + assets: bigint, + penalty: bigint, + skipRevert = false, + ): BundlerCall[] { + const calls: BundlerCall[] = []; + const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( + assets, + penalty, + ); + if (skipRevert && penaltyAssets > 0n) { + throw new BundlerErrors.SkippableAllocatorPenalty(penaltyAssets); + } + + if (penaltyAssets > 0n) { + calls.push({ + to: allocateMarket.loanToken, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [allocator, penaltyAssets], + }), + 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 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, + * 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 = + * "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: usdc, + * collateralToken: weth, + * oracle: ethUsdOracle, + * irm: adaptiveCurveIrm, + * lltv: 860_000_000_000_000_000n, + * } satisfies InputMarketParams; + * + * const calls: BundlerCall[] = BundlerAction.vaultV2BluePublicAllocatorAllocateFromIdle( + * allocatorFixture, + * keyrockUsdcVault, + * targetAdapterFixture, + * targetMarket, + * 1_000_000n, + * 1_000_000_000_000_000n, + * ); + * // Bundler3 approves 1_000 USDC units, then calls `allocateFromIdle` with zero native value. + * ``` + */ + // biome-ignore lint/complexity/useMaxParams: mirrors the protocol call + export function vaultV2BluePublicAllocatorAllocateFromIdle( + allocator: Address, + vault: Address, + adapter: Address, + market: InputMarketParams, + assets: bigint, + penalty: bigint, + skipRevert = false, + ): BundlerCall[] { + const calls: BundlerCall[] = []; + const penaltyAssets = computeBluePublicAllocatorPenaltyAssets( + assets, + penalty, + ); + if (skipRevert && penaltyAssets > 0n) { + throw new BundlerErrors.SkippableAllocatorPenalty(penaltyAssets); + } + + if (penaltyAssets > 0n) { + calls.push({ + to: market.loanToken, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [allocator, penaltyAssets], + }), + 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; + } + /** * 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..d39a01098 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, ]; + /** Vault V2 Blue Public Allocator market-to-market reallocation with an explicit allocator address and WAD-scaled penalty. */ + readonly vaultV2BluePublicAllocatorReallocate: [ + allocator: Address, + vault: Address, + deallocateAdapter: Address, + deallocateMarket: InputMarketParams, + allocateAdapter: Address, + allocateMarket: InputMarketParams, + assets: bigint, + penalty: bigint, + skipRevert?: boolean, + ]; + + /** 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, + penalty: 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/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/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..78bffff87 --- /dev/null +++ b/packages/morpho-sdk/src/entities/blue/blue.bluePublicAllocatorRequirements.test.ts @@ -0,0 +1,265 @@ +import { + AccrualPosition, + getChainAddresses, + Market, + MarketParams, + ORACLE_PRICE_SCALE, +} from "@morpho-org/blue-sdk"; +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, vi } from "vitest"; +import { CbbtcUsdcBlue } from "../../../test/fixtures/blue.js"; +import { morphoViemExtension } from "../../client/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); + +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, + }); + }); + + 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.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 5e17fa2e7..e890a0da4 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -31,12 +31,13 @@ import { getBlueAuthorizationRequirement, getGeneralAdapterRequirements, } from "../../actions/index.js"; +import { computeVaultV2ReallocationPenaltyAssets } from "../../helpers/bluePublicAllocator.js"; import { computeMaxRepaySharePrice, computeMaxSupplySharePrice, computeMinBorrowSharePrice, computeMinWithdrawSharePrice, - computeReallocations, + computeVaultV1Reallocations, validateAccrualPosition, validateChainId, validateNativeAsset, @@ -54,6 +55,7 @@ import { type AssetsOrSharesArgs, type BlueAuthorizationAction, type BlueBorrowAction, + type BlueReallocation, type BlueRefinanceAction, type BlueRepayAction, type BlueRepayWithdrawCollateralAction, @@ -84,10 +86,10 @@ import { type RequirementSignature, selectRequirementSignatures, type Transaction, - type VaultReallocation, + type VaultV1BlueReallocation, WithdrawExceedsCollateralError, } from "../../types/index.js"; -import { ReallocationData } from "../reallocationData.js"; +import { VaultV1ReallocationData } from "../vaultV1ReallocationData.js"; export interface BlueActions { /** @@ -187,18 +189,23 @@ 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 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` 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 {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ withdraw: ( params: { @@ -206,14 +213,18 @@ export interface BlueActions { receiver?: Address; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; } & AssetsOrSharesArgs, ) => { buildTx: ( signatures?: readonly RequirementSignature[], ) => Readonly>; getRequirements: () => Promise< - (Readonly> | Requirement)[] + ( + | Readonly> + | Readonly> + | Requirement + )[] >; }; @@ -224,29 +235,39 @@ 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 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` 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 {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: { userAddress: Address; amount: bigint; positionData: AccrualPosition; slippageTolerance?: bigint; - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; }) => { buildTx: ( signatures?: readonly RequirementSignature[], ) => Readonly>; getRequirements: () => Promise< - (Readonly> | Requirement)[] + ( + | Readonly> + | Readonly> + | Requirement + )[] >; }; @@ -365,17 +386,24 @@ 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 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` 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 {InvalidReallocationSourceTypeError} when a V2 source is absent, incomplete, or has an unknown discriminator. + * @throws {InvalidReallocationTypeError} when a top-level reallocation variant is unknown. */ supplyCollateralBorrow: ( params: { @@ -383,7 +411,7 @@ export interface BlueActions { positionData: AccrualPosition; borrowAmount: bigint; slippageTolerance?: bigint; - reallocations?: readonly VaultReallocation[]; + reallocations?: readonly BlueReallocation[]; } & DepositAmountArgs, ) => { buildTx: ( @@ -413,9 +441,12 @@ 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 + * 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. @@ -425,8 +456,13 @@ 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` 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 {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: { userAddress: Address; @@ -439,18 +475,22 @@ export interface BlueActions { borrowAssets?: bigint; borrowShares?: bigint; slippageTolerance?: bigint; - targetReallocations?: readonly VaultReallocation[]; + targetReallocations?: readonly BlueReallocation[]; }) => { buildTx: ( signatures?: readonly RequirementSignature[], ) => Readonly>; getRequirements: () => Promise< - (Readonly> | Requirement)[] + ( + | Readonly> + | Readonly> + | Requirement + )[] >; }; /** - * 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 @@ -460,14 +500,14 @@ 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).** * * @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: { @@ -476,7 +516,7 @@ export interface BlueActions { readonly number: bigint; readonly timestamp: bigint; }; - }) => Promise; + }) => Promise; /** * Computes vault reallocations for a borrow or withdraw on this market. @@ -505,7 +545,7 @@ export interface BlueActions { */ getReallocations: ( params: { - reallocationData: ReallocationData; + reallocationData: VaultV1ReallocationData; options?: ReallocationComputeOptions; } & ( | { @@ -520,7 +560,7 @@ export interface BlueActions { amount?: never; } ), - ) => readonly VaultReallocation[]; + ) => readonly VaultV1BlueReallocation[]; } export class MorphoBlue implements BlueActions { @@ -531,6 +571,23 @@ export class MorphoBlue implements BlueActions { private readonly chainId: number, ) {} + private getReallocationPenaltyRequirements( + userAddress: Address, + reallocations: readonly BlueReallocation[] | undefined, + ) { + const amount = computeVaultV2ReallocationPenaltyAssets(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 + // both amounts into the collateral requirement instead. + 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); @@ -642,7 +699,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); @@ -679,6 +736,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); } @@ -715,13 +773,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[]) => { @@ -812,7 +873,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); @@ -822,6 +883,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); } @@ -850,13 +912,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[]) => { @@ -1306,7 +1371,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); @@ -1329,6 +1394,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); } @@ -1361,24 +1427,45 @@ 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 penaltyAssets = computeVaultV2ReallocationPenaltyAssets( + reallocations ?? [], + ); + const usesSharedFundingToken = isAddressEqual( + this.marketParams.collateralToken, + this.marketParams.loanToken, + ); + 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: amount + (usesSharedFundingToken ? penaltyAssets : 0n), + from: userAddress, + }, + }), + usesSharedFundingToken + ? Promise.resolve([]) + : 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[]) => { @@ -1429,7 +1516,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); @@ -1450,6 +1537,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); } @@ -1607,13 +1695,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[]) => { @@ -1661,7 +1755,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; @@ -1725,7 +1819,7 @@ export class MorphoBlue implements BlueActions { ), ]); - // Assemble records for ReallocationData. + // Assemble records for VaultV1ReallocationData. const marketsRecord: Record = { [targetMarketId]: targetMarket, }; @@ -1754,7 +1848,7 @@ export class MorphoBlue implements BlueActions { (positionsRecord[vault] ??= {})[mid] = position; } - return new ReallocationData({ + return new VaultV1ReallocationData({ chainId: this.chainId, markets: marketsRecord, vaults: vaultsRecord, @@ -1784,7 +1878,7 @@ export class MorphoBlue implements BlueActions { */ getReallocations( params: { - reallocationData: ReallocationData; + reallocationData: VaultV1ReallocationData; options?: ReallocationComputeOptions; } & ( | { @@ -1799,14 +1893,14 @@ export class MorphoBlue implements BlueActions { amount?: never; } ), - ): readonly VaultReallocation[] { + ): readonly VaultV1BlueReallocation[] { validateChainId(params.reallocationData.chainId, this.chainId); const marketId = this.marketParams.id; const options = { enabled: true, ...params.options }; if (params.borrowAmount !== undefined) { - return computeReallocations({ + return computeVaultV1Reallocations({ reallocationData: params.reallocationData, marketId, operation: "borrow", @@ -1815,7 +1909,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 2842fba0b..6520960fd 100644 --- a/packages/morpho-sdk/src/entities/index.ts +++ b/packages/morpho-sdk/src/entities/index.ts @@ -67,9 +67,16 @@ 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 { MorphoVaultV1 } from "./vaultV1/index.js"; + VaultV1ReallocationData, +} 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/reallocationData.metrics.test.ts b/packages/morpho-sdk/src/entities/vaultV1ReallocationData.metrics.test.ts similarity index 94% rename from packages/morpho-sdk/src/entities/reallocationData.metrics.test.ts rename to packages/morpho-sdk/src/entities/vaultV1ReallocationData.metrics.test.ts index 4506378e9..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,25 +37,25 @@ 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 }, }); } /** - * 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. */ function stubReallocations( - data: ReallocationData, + data: VaultV1ReallocationData, withdrawals: readonly PublicReallocation[], ) { return vi - .spyOn(data, "getMarketPublicReallocations") + .spyOn(data, "computeVaultV1Reallocations") .mockImplementation((_marketId, options?: PublicAllocatorOptions) => ({ withdrawals: options?.enabled === false ? [] : withdrawals, data, @@ -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 91% rename from packages/morpho-sdk/src/entities/reallocationData.test.ts rename to packages/morpho-sdk/src/entities/vaultV1ReallocationData.test.ts index acf133906..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,36 @@ 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, + targetBorrow: 500n * MathLib.WAD, + sourceSupply: 1_000n * MathLib.WAD, + sourceBorrow: 500n * MathLib.WAD, + }; + const canonical = new VaultV1ReallocationData( + makeInput(input), + ).computeVaultV1Reallocations(targetParams.id, { timestamp: TIMESTAMP }); + const deprecated = new VaultV1ReallocationData( + 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", @@ -333,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 }, @@ -374,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( @@ -383,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({}); @@ -395,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 }, @@ -465,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 }), @@ -589,7 +624,7 @@ describe("ReallocationData unit coverage", () => { }), }; - const reallocationResult = new ReallocationData( + const reallocationResult = new VaultV1ReallocationData( input, ).getMarketPublicReallocations(targetParams.id, { timestamp: TIMESTAMP, @@ -622,7 +657,7 @@ describe("ReallocationData unit coverage", () => { ).toBe(13n); expect( - new ReallocationData({ + new VaultV1ReallocationData({ ...input, vaultMarketConfigs: { [VAULT]: { @@ -642,7 +677,7 @@ describe("ReallocationData unit coverage", () => { ).toEqual([]); expect( - new ReallocationData({ + new VaultV1ReallocationData({ ...input, vaultMarketConfigs: { [VAULT]: { @@ -669,7 +704,7 @@ describe("ReallocationData unit coverage", () => { ]); expect( - new ReallocationData({ + new VaultV1ReallocationData({ ...input, vaultMarketConfigs: { [VAULT]: { @@ -711,7 +746,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaults: { [VAULT]: makeVault({ withoutPublicAllocatorConfig: true }), @@ -723,7 +758,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaultMarketConfigs: { [VAULT]: { @@ -744,7 +779,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaultMarketConfigs: { [VAULT]: { @@ -765,7 +800,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaultMarketConfigs: { [VAULT]: { @@ -786,7 +821,7 @@ describe("ReallocationData unit coverage", () => { expect(() => applyPublicReallocation( - new ReallocationData({ + new VaultV1ReallocationData({ ...baseInput, vaultMarketConfigs: { [VAULT]: { @@ -806,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 93% rename from packages/morpho-sdk/src/entities/reallocationData.ts rename to packages/morpho-sdk/src/entities/vaultV1ReallocationData.ts index fb92da166..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,18 +367,18 @@ export class ReallocationData implements InputReallocationData { * * const result: { * withdrawals: readonly PublicReallocation[]; - * data: ReallocationData; - * } = reallocationData.getMarketPublicReallocations(marketParams.id, { + * data: VaultV1ReallocationData; + * } = reallocationData.computeVaultV1Reallocations(marketParams.id, { * timestamp: block.timestamp, * }); * ``` */ - public getMarketPublicReallocations( + public computeVaultV1Reallocations( marketId: MarketId, options: PublicAllocatorOptions = {}, ): { readonly withdrawals: readonly PublicReallocation[]; - data: ReallocationData; + data: VaultV1ReallocationData; } { const { enabled = true, @@ -467,6 +474,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 +533,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); } @@ -792,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/vaultV2ReallocationData.test.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts new file mode 100644 index 000000000..3328050c8 --- /dev/null +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.test.ts @@ -0,0 +1,1148 @@ +import { + AccrualPosition, + AccrualVault, + AccrualVaultV2, + AccrualVaultV2MorphoMarketV1Adapter, + AccrualVaultV2MorphoMarketV1AdapterV2, + AccrualVaultV2MorphoVaultV1Adapter, + 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 { + InsufficientSharedLiquidityError, + NonPositiveInputError, + ReallocationWithdrawExceedsMarketSupplyError, + UnknownReallocationMarketError, +} from "../types/index.js"; +import { + computeVaultV2Reallocations, + VaultV2ReallocationData, +} from "./vaultV2ReallocationData.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 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, + 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, + lastUpdate = TIMESTAMP, +}: { + readonly params: MarketParams; + readonly supply: bigint; + readonly borrow: bigint; + readonly lastUpdate?: bigint; +}) => + new Market({ + params, + totalSupplyAssets: supply, + totalBorrowAssets: borrow, + totalSupplyShares: supply * 1_000_000n, + totalBorrowShares: borrow * 1_000_000n, + lastUpdate, + 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 canPullFromIdle?: boolean; + readonly canPullFromMarket?: boolean; + readonly penalty?: bigint; + readonly sourceLastUpdate?: bigint; + readonly targetLastUpdate?: bigint; + readonly vaultLastUpdate?: bigint; + readonly maxRate?: 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, + 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 + : makeMarket({ + params: sourceMarketParams, + supply: sourceSupply, + borrow: sourceBorrow, + lastUpdate: sourceLastUpdate, + }); + 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, + lastUpdate: vaultLastUpdate, + liquidityAdapter: zeroAddress, + liquidityData: "0x", + liquidityAllocations: undefined, + performanceFee: 0n, + managementFee: 0n, + performanceFeeRecipient: zeroAddress, + managementFeeRecipient: zeroAddress, + }, + undefined, + adapters, + idle, + {}, + ); + + return { + data: new VaultV2ReallocationData({ + chainId: ChainId.EthMainnet, + allocator: ALLOCATOR, + markets: { + [targetMarket.id]: targetMarket, + [sourceMarket.id]: sourceMarket, + }, + vaults: { [VAULT]: vault }, + allocations: { [VAULT]: allocations }, + publicAllocatorConfigs: { + [VAULT]: { + allocator: ALLOCATOR, + vault: VAULT, + canPullFromIdle, + penalty, + }, + }, + marketPublicAllocatorConfigs: { + [VAULT]: { + [targetIds[2]]: { + allocator: ALLOCATOR, + vault: VAULT, + adapter: TARGET_ADAPTER, + marketParamsId: targetIds[2], + absoluteCap: allocatorTargetCap, + canPullFromMarket: false, + isActiveAdapter: true, + }, + [sourceIds[2]]: { + allocator: ALLOCATOR, + vault: VAULT, + adapter: sourceAdapterAddress, + marketParamsId: sourceIds[2], + absoluteCap: 0n, + canPullFromMarket, + isActiveAdapter: true, + }, + }, + }, + }), + sourceExpectedAssets, + sourceIds, + targetExpectedAssets, + targetIds, + }; +}; + +describe("VaultV2ReallocationData.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, + penalty: 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: 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 }); + + const result = data.computeVaultV2Reallocations(targetParams.id); + + expect( + result.reallocations.map(({ from, assets, penalty }) => ({ + from: from.type, + assets, + penalty, + })), + ).toStrictEqual([ + { from: "market", assets: sourceExpectedAssets, penalty: 7n }, + { from: "idle", assets: 300n, penalty: 7n }, + ]); + expect(result.data.getVault(VAULT).assetBalance).toBe(2n); + }); + + test("behavior: excludes the target market through a different adapter", () => { + const { data } = makeFixture({ + sourceMarketParams: targetParams, + }); + + expect( + data.computeVaultV2Reallocations(targetParams.id).reallocations, + ).toStrictEqual([]); + }); + + 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(1n); + }); + + 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: 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({ + 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 is not counted as target liquidity", () => { + 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, + ).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", () => { + const { data } = makeFixture(); + + expect( + data.computeVaultV2Reallocations(targetParams.id, { enabled: false }) + .reallocations, + ).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, + penalty: 8n, + }); + + expect( + data.computeVaultV2Reallocations(targetParams.id, { + maxPenalty: 7n, + }).reallocations, + ).toStrictEqual([]); + expect( + data + .computeVaultV2Reallocations(targetParams.id, { + maxPenalty: 8n, + }) + .reallocations.map(({ from, assets }) => ({ + from: from.type, + assets, + })), + ).toStrictEqual([ + { from: "market", assets: sourceExpectedAssets }, + { from: "idle", assets: 300n }, + ]); + }); +}); + +describe("computeVaultV2Reallocations", () => { + 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, + }); + + expect(reallocations).toHaveLength(1); + 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: 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, + targetBorrow: 100n, + sourceSupply: 1_000n, + sourceBorrow: 950n, + }); + + const reallocations = computeVaultV2Reallocations({ + 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 = computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "withdraw", + amount: 10n, + }); + + expect(reallocations[0]?.assets).toBe(10n); + }); + + test("behavior: preserves the configured penalty for every retained flat call", () => { + const { data } = makeFixture({ + targetSupply: 100n, + targetBorrow: 100n, + idle: 300n, + }); + const reallocations = computeVaultV2Reallocations({ + 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(0n); + expect(tx.action.args.reallocationPenaltyAssets).toBe(2n); + }); + + test("behavior: excludes reallocations above the penalty threshold", () => { + const { data } = makeFixture({ + targetSupply: 100n, + targetBorrow: 90n, + penalty: 7n, + }); + + expect( + computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 1n, + options: { maxPenalty: 6n }, + }), + ).toStrictEqual([]); + expect( + computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 1n, + options: { maxPenalty: 7n }, + })[0]?.assets, + ).toBe(2n); + }); + + test("error: InsufficientSharedLiquidityError rejects a partial plan", () => { + const { data } = makeFixture({ + targetSupply: 100n, + targetBorrow: 100n, + sourceSupply: 50n, + }); + + expect(() => + computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 100n, + }), + ).toThrow(InsufficientSharedLiquidityError); + }); + + test("error: ReallocationWithdrawExceedsMarketSupplyError", () => { + const { data } = makeFixture({ targetSupply: 100n }); + + expect(() => + computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "withdraw", + amount: 101n, + }), + ).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(); + + expect( + computeVaultV2Reallocations({ + reallocationData: data, + marketId: targetParams.id, + operation: "borrow", + amount: 0n, + options: { enabled: false }, + }), + ).toStrictEqual([]); + }); +}); + +describe("VaultV2ReallocationData 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/vaultV2ReallocationData.ts b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts new file mode 100644 index 000000000..2d8b13f1b --- /dev/null +++ b/packages/morpho-sdk/src/entities/vaultV2ReallocationData.ts @@ -0,0 +1,1233 @@ +import { + AccrualPosition, + AccrualVault, + AccrualVaultV2, + AccrualVaultV2MorphoMarketV1Adapter, + AccrualVaultV2MorphoMarketV1AdapterV2, + AccrualVaultV2MorphoVaultV1Adapter, + type IAccrualVaultV2Adapter, + 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 { computeBluePublicAllocatorPenaltyAssets } from "../helpers/bluePublicAllocator.js"; +import { + DEFAULT_SUPPLY_TARGET_UTILIZATION, + DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, +} from "../helpers/constant.js"; +import type { + VaultV2BluePublicAllocatorOptions, + VaultV2BlueReallocation, +} from "../types/index.js"; +import { + InsufficientSharedLiquidityError, + NonPositiveInputError, + ReallocationAdapterSupplySharesUnderflowError, + ReallocationAllocationUnderflowError, + ReallocationWithdrawExceedsMarketSupplyError, + UnknownReallocationAdapterError, + UnknownReallocationAllocationError, + UnknownReallocationMarketError, + UnknownReallocationMarketPublicAllocatorConfigError, + UnknownReallocationPublicAllocatorConfigError, + UnknownReallocationVaultError, +} from "../types/index.js"; + +/** Input state required to simulate Vault V2 BluePublicAllocator reallocations. */ +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. */ + 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 + > + >; +} + +const cloneMarket = (market: Market) => new Market({ ...market }); + +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( + { + ...vault, + pendingTimelock: { ...vault.pendingTimelock }, + pendingGuardian: { ...vault.pendingGuardian }, + supplyQueue: [...vault.supplyQueue], + publicAllocatorConfig: + vault.publicAllocatorConfig == null + ? undefined + : { ...vault.publicAllocatorConfig }, + }, + [...vault.allocations.values()].map(({ config, position }) => ({ + config: { + ...config, + pendingCap: { ...config.pendingCap }, + publicAllocatorConfig: + config.publicAllocatorConfig == null + ? undefined + : { ...config.publicAllocatorConfig }, + }, + position: clonePosition(position, markets), + })), + ); + +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) => + cloneAdapter(adapter, markets), + ); + const liquidityAdapter = + vault.accrualLiquidityAdapter == null + ? undefined + : (adapters.find((adapter) => + isAddressEqual( + adapter.address, + vault.accrualLiquidityAdapter!.address, + ), + ) ?? cloneAdapter(vault.accrualLiquidityAdapter, markets)); + + 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. 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 + * import { VaultV2ReallocationData } from "@morpho-org/morpho-sdk/entities"; + * + * const data = new VaultV2ReallocationData(input); + * ``` + */ +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. */ + 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: InputVaultV2ReallocationData) { + this.chainId = input.chainId; + this.allocator = input.allocator; + this.markets = {}; + this.vaults = {}; + this.allocations = {}; + this.publicAllocatorConfigs = {}; + this.marketPublicAllocatorConfigs = {}; + this.donatedPenaltyAssets = + input instanceof VaultV2ReallocationData + ? { ...input.donatedPenaltyAssets } + : {}; + this.firstTotalAssets = + input instanceof VaultV2ReallocationData + ? { ...input.firstTotalAssets } + : {}; + + 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.markets); + this.vaults[address] = clonedVault; + } + + 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 VaultV2ReallocationData(this); + } + + /** + * Gets a market from this snapshot. + * + * @param marketId - Market id to read. + * @returns The market state. + * @throws {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 {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 {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 {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 {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 {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. 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 penalty. + * @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 {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 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 []; + if (amount <= 0n) throw new NonPositiveInputError("amount", amount); + + 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 = {}, + }: { + readonly marketId: MarketId; + readonly maxWithdrawalUtilization: bigint; + readonly options?: VaultV2BluePublicAllocatorOptions; + }): { + readonly reallocations: readonly VaultV2BlueReallocation[]; + readonly data: VaultV2ReallocationData; + } { + if (options.enabled === false) return { reallocations: [], data: this }; + + this.getMarket(marketId); + const timestamp = + options.timestamp == null + ? 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)); + } + 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((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, + ) + .sort(bigIntComparator(({ assets }) => assets, "desc")); + + const largest = candidates[0]; + if (largest == null) return { reallocations, data }; + + reallocations.push(largest); + data = data.cloneWithPublicReallocation({ + 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, vault allowlist, and maximum penalty. + * @returns Reallocatable market and idle assets, or `0n` when none are available. + * @throws {UnknownReallocationMarketError} when the target market is absent. + * @example + * ```ts + * const liquidity = data.getPublicReallocationLiquidityVaultV2(targetMarketId); + * ``` + */ + public getPublicReallocationLiquidityVaultV2( + marketId: MarketId, + options?: VaultV2BluePublicAllocatorOptions, + ) { + return this.computeVaultV2ReallocationsAtUtilization({ + marketId, + maxWithdrawalUtilization: DEFAULT_WITHDRAWAL_TARGET_UTILIZATION, + 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, vault allowlist, and maximum penalty. + * @returns Borrowable assets while remaining at or below `utilization`. + * @throws {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?: VaultV2BluePublicAllocatorOptions, + ) { + 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, timestamp }, + ); + return MarketUtils.getBorrowToUtilization( + { + totalSupplyAssets: market.totalSupplyAssets + availableLiquidity, + totalBorrowAssets: market.totalBorrowAssets, + }, + utilization, + ); + } + + 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 cloneWithPublicReallocation({ + reallocation, + targetMarketId, + timestamp, + }: { + readonly reallocation: VaultV2BlueReallocation; + readonly targetMarketId: MarketId; + readonly timestamp: bigint; + }) { + const data = this.clone(); + let vault = data.getVault(reallocation.vault); + const targetMarket = data.getMarket(targetMarketId); + + 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, + 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.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) { + 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; + } + + 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, + reallocation.to.adapter, + ); + const targetIds = targetAdapter.ids(targetMarket.params); + + 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.setMarket(supply.market); + + const targetChange = + supply.market.toSupplyAssets(targetSupplyShares) - oldTargetAllocation; + for (const id of targetIds) { + 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 setMarket(market: Market) { + this.markets[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)) { + 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, + ); + } + } + } + } +} + +/** + * 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, + * 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 36494d266..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** — `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. 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/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/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 eb4152844..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,10 +87,10 @@ interface MockStateParams { } /** - * Creates a minimal mock ReallocationData. + * Creates a minimal mock VaultV1ReallocationData. * - * Only implements the methods computeReallocations actually calls: - * `getMarket`, `getMarketPublicReallocations`, and `getVault`. + * Only implements the methods computeVaultV1Reallocations actually calls: + * `getMarket`, `computeVaultV1Reallocations`, and `getVault`. */ function makeMockState({ targetMarket: tm = defaultTarget, @@ -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); @@ -109,9 +112,9 @@ function makeMockState({ id === tm.id && friendlyTargetMarket != null ? friendlyTargetMarket : markets.get(id)!, - getMarketPublicReallocations: () => ({ + computeVaultV1Reallocations: () => ({ withdrawals: [...aggressiveWithdrawals], - data: {} as ReallocationData, + data: {} as VaultV1ReallocationData, }), }; @@ -121,7 +124,7 @@ function makeMockState({ if (m == null) throw new Error(`Mock: unknown market ${id}`); return m; }, - getMarketPublicReallocations: () => ({ + computeVaultV1Reallocations: () => ({ withdrawals: [...friendlyWithdrawals], data: friendlyData, }), @@ -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 92% rename from packages/morpho-sdk/src/helpers/computeReallocations.ts rename to packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts index 9603b6440..543c4d335 100644 --- a/packages/morpho-sdk/src/helpers/computeReallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeVaultV1Reallocations.ts @@ -1,13 +1,13 @@ 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, type PublicReallocation, type ReallocationComputeOptions, ReallocationWithdrawExceedsMarketSupplyError, - type VaultReallocation, + type VaultV1BlueReallocation, } from "../types/index.js"; import { getSupplyTargetUtilization } from "./utilization.js"; import { compareMarketIds } from "./validate.js"; @@ -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,22 +148,22 @@ 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; readonly options?: ReallocationComputeOptions; -}): readonly VaultReallocation[] => { +}): 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); @@ -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 }) => ({ @@ -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/index.ts b/packages/morpho-sdk/src/helpers/index.ts index ff11e556e..5388ae1fb 100644 --- a/packages/morpho-sdk/src/helpers/index.ts +++ b/packages/morpho-sdk/src/helpers/index.ts @@ -1,4 +1,7 @@ -export { computeReallocations } from "./computeReallocations.js"; +export { + computeReallocations, + computeVaultV1Reallocations, +} from "./computeVaultV1Reallocations.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 89032b49d..642b1848b 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,17 @@ import { import { AccrualPositionUserMismatchError, AddressMismatchError, + type BlueReallocation, BorrowExceedsSafeLtvError, ChainIdMismatchError, ChainWNativeMissingError, EmptyReallocationWithdrawalsError, ExcessiveSlippageToleranceError, + InconsistentReallocationPenaltyError, + InputExceedsMaxError, + InvalidReallocationAddressError, + InvalidReallocationSourceTypeError, + InvalidReallocationTypeError, MarketIdMismatchError, MissingClientPropertyError, MissingMarketPriceError, @@ -34,6 +40,7 @@ import { RepaySharesExceedDebtError, UnsortedReallocationWithdrawalsError, type VaultReallocation, + type VaultV2BlueReallocation, WithdrawExceedsCollateralError, WithdrawExceedsSupplyError, WithdrawMakesPositionUnhealthyError, @@ -554,12 +561,264 @@ describe("validateReallocations", () => { withdrawals: [{ marketParams: sourceMarketA, amount: 10n ** 18n }], }; + const validBluePublicAllocatorReallocation: VaultV2BlueReallocation = { + type: "bluePublicAllocator", + allocator: USER_A, + vault: USER_B, + from: { type: "idle" }, + to: { adapter: USER_A }, + assets: 1n, + penalty: 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 Blue Public Allocator idle reallocation", () => { + expect(() => + validateReallocations( + [validBluePublicAllocatorReallocation], + targetMarketId, + ), + ).not.toThrow(); + }); + + test.each([ + { + name: "negative penalty", + reallocation: { + ...validBluePublicAllocatorReallocation, + penalty: -1n, + }, + ErrorClass: NegativeInputError, + }, + { + name: "penalty above WAD", + reallocation: { + ...validBluePublicAllocatorReallocation, + penalty: MathLib.WAD + 1n, + }, + ErrorClass: InputExceedsMaxError, + }, + { + name: "zero assets", + reallocation: { ...validBluePublicAllocatorReallocation, assets: 0n }, + ErrorClass: NonPositiveInputError, + }, + { + name: "negative assets", + reallocation: { ...validBluePublicAllocatorReallocation, assets: -1n }, + ErrorClass: NonPositiveInputError, + }, + { + name: "uint128 asset overflow", + reallocation: { + ...validBluePublicAllocatorReallocation, + assets: maxUint128 + 1n, + }, + ErrorClass: InputExceedsMaxError, + }, + ])( + "error: rejects Blue Public Allocator $name", + ({ reallocation, ErrorClass }) => { + expect(() => + validateReallocations([reallocation], targetMarketId), + ).toThrow(ErrorClass); + }, + ); + + test("error: InconsistentReallocationPenaltyError for one allocator-vault pair", () => { + expect(() => + validateReallocations( + [ + { ...validBluePublicAllocatorReallocation, penalty: 5n }, + { ...validBluePublicAllocatorReallocation, penalty: 11n }, + ], + targetMarketId, + ), + ).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( + [ + { ...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( + [ + { + ...validBluePublicAllocatorReallocation, + from: { + type: "market", + adapter: USER_A, + marketParams, + }, + } satisfies VaultV2BlueReallocation, + ], + targetMarketId, + ), + ).toThrow(ReallocationWithdrawalOnTargetMarketError); + }); + + test("error: target market through a different Vault V2 adapter", () => { + expect(() => + validateReallocations( + [ + { + ...validBluePublicAllocatorReallocation, + from: { + type: "market", + adapter: USER_B, + marketParams, + }, + } satisfies VaultV2BlueReallocation, + ], + targetMarketId, + ), + ).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", () => { + const reallocation = { + ...validBluePublicAllocatorReallocation, + from: { type: "marketTypo" }, + } as unknown as BlueReallocation; + + expect(() => validateReallocations([reallocation], targetMarketId)).toThrow( + InvalidReallocationSourceTypeError, + ); + }); + + 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", + 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 4d024de11..302b14f30 100644 --- a/packages/morpho-sdk/src/helpers/validate.ts +++ b/packages/morpho-sdk/src/helpers/validate.ts @@ -2,20 +2,27 @@ 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 } from "viem"; +import { type Address, isAddress, isAddressEqual, maxUint128 } from "viem"; import { AccrualPositionUserMismatchError, AddressMismatchError, + type BlueReallocation, BorrowExceedsSafeLtvError, ChainIdMismatchError, ChainWNativeMissingError, EmptyReallocationWithdrawalsError, ExcessiveSlippageToleranceError, + InconsistentReallocationPenaltyError, + InputExceedsMaxError, + InvalidReallocationAddressError, + InvalidReallocationSourceTypeError, + InvalidReallocationTypeError, MarketIdMismatchError, MissingClientPropertyError, MissingMarketPriceError, @@ -26,7 +33,6 @@ import { RepayExceedsDebtError, RepaySharesExceedDebtError, UnsortedReallocationWithdrawalsError, - type VaultReallocation, WithdrawExceedsCollateralError, WithdrawExceedsSupplyError, WithdrawMakesPositionUnhealthyError, @@ -323,23 +329,32 @@ export const validateRepayShares = (params: { }; /** - * Validates that vault reallocations are well-formed. + * Validates that Public Allocator V1 and Blue Public Allocator 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. + * + * 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 market. 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. 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 amount is non-positive. - * @throws {ReallocationWithdrawalOnTargetMarketError} when a withdrawal references the target market. + * @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 {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 or V2 source references the target market. * @throws {UnsortedReallocationWithdrawalsError} when withdrawals are not strictly market-id sorted. * @example * ```ts @@ -351,10 +366,109 @@ export const validateRepayShares = (params: { * ``` */ export const validateReallocations = ( - reallocations: readonly VaultReallocation[], + reallocations: readonly BlueReallocation[], targetMarketId: MarketId, ): void => { + const penaltyByAllocatorVault = new Map(); + for (const r of reallocations) { + if (r.type === "bluePublicAllocator") { + 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); + } + 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, + }); + } + + 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 ( + sourceMarketId !== undefined && + compareMarketIds(sourceMarketId, targetMarketId) === 0 + ) { + throw new ReallocationWithdrawalOnTargetMarketError( + r.vault, + sourceMarketId, + ); + } + 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/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 b8c41ec5b..398118b46 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -18,18 +18,18 @@ 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. +- `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. +- `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 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 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`, `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 41cb8f0a3..d343306cb 100644 --- a/packages/morpho-sdk/src/types/action.ts +++ b/packages/morpho-sdk/src/types/action.ts @@ -140,7 +140,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. */ + readonly reallocationPenaltyAssets: bigint; } > {} @@ -163,7 +166,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. */ + readonly reallocationPenaltyAssets: bigint; } > {} @@ -178,7 +184,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. */ + readonly reallocationPenaltyAssets: bigint; } > {} @@ -238,7 +247,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 b32efae01..c3b90e556 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. @@ -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; +} + /** @deprecated Use {@link NonPositiveInputError}. */ export const NonPositiveAssetAmountError = NonPositiveInputError; /** @deprecated Use {@link NonPositiveInputError}. */ @@ -235,6 +265,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. */ @@ -530,7 +585,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 Public Allocator source references the target Blue market. */ export class ReallocationWithdrawalOnTargetMarketError extends Error { constructor(vault: string, marketId: string) { super( @@ -539,6 +594,148 @@ 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 "bluePublicAllocator".' + : `Reallocation type must be "publicAllocatorV1" or "bluePublicAllocator", got "${reallocationType}".`, + ); + this.name = "InvalidReallocationTypeError"; + } +} + +/** + * 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 + * 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`, + * 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 | undefined, + public readonly missingField?: "adapter" | "marketParams", + ) { + super( + 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"; + } +} + +/** + * 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) { @@ -660,8 +857,8 @@ export class DisabledReallocationMarketError extends Error { } /** - * Thrown when shared liquidity selected by `computeReallocations` 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. @@ -730,6 +927,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: { @@ -970,7 +1252,7 @@ export class WithdrawSharesExceedSupplyError extends Error { } /** - * Thrown when `computeReallocations` 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/types/sharedLiquidity.ts b/packages/morpho-sdk/src/types/sharedLiquidity.ts index 77d6499fe..1cb7e29a7 100644 --- a/packages/morpho-sdk/src/types/sharedLiquidity.ts +++ b/packages/morpho-sdk/src/types/sharedLiquidity.ts @@ -44,6 +44,25 @@ export interface PublicAllocatorOptions { readonly defaultMaxWithdrawalUtilization?: bigint; } +/** Options controlling Vault V2 BluePublicAllocator reallocation discovery. */ +export interface VaultV2BluePublicAllocatorOptions { + /** 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[]; + + /** + * 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 maxPenalty?: bigint; +} + /** * A computed source-market withdrawal before it is grouped by vault. */ @@ -73,7 +92,9 @@ 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; /** Fee in native token (ETH) paid to the PublicAllocator for this vault. */ readonly fee: bigint; @@ -81,6 +102,61 @@ export interface VaultReallocation { readonly withdrawals: readonly ReallocationWithdrawal[]; } +/** Source of a Blue Public Allocator reallocation. */ +export type BluePublicAllocatorSource = + | { + /** 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 contract call performed before a Blue action. + * + * 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. */ + 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`. */ + readonly assets: bigint; + /** Vault-configured WAD-scaled penalty rate passed to the allocator. */ + readonly penalty: bigint; +} + +/** + * 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"`. + */ +export type BlueReallocation = + | 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. * diff --git a/packages/morpho-sdk/src/utils.ts b/packages/morpho-sdk/src/utils.ts index 0d8f607da..4f38bdfb6 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, @@ -63,7 +64,11 @@ export { transformValue, values, } from "@morpho-org/morpho-ts"; -export { computeReallocations } from "./helpers/computeReallocations.js"; +export { computeVaultV2Reallocations } from "./entities/vaultV2ReallocationData.js"; +export { + computeReallocations, + computeVaultV1Reallocations, +} from "./helpers/computeVaultV1Reallocations.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/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/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 470e5652e..040f0453a 100644 --- a/packages/morpho-ts/src/abis.ts +++ b/packages/morpho-ts/src/abis.ts @@ -4038,6 +4038,18 @@ export const metaMorphoAbi = [ }, ] 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 = [ { @@ -4442,33 +4454,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: marketParamsAbi.components, internalType: "struct MarketParams", name: "marketParams", type: "tuple", @@ -4484,33 +4470,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: marketParamsAbi.components, internalType: "struct MarketParams", name: "supplyMarketParams", type: "tuple", @@ -4619,6 +4579,185 @@ export const publicAllocatorAbi = [ }, ] as const; +/** Vault V2 Blue Public Allocator ABI used for market and idle reallocations. */ +export const vaultV2BluePublicAllocatorAbi = [ + { + 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", + }, + ], + name: "vaultData", + outputs: [ + { + internalType: "bool", + name: "canPullFromIdle", + type: "bool", + }, + { + internalType: "uint64", + name: "penalty", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "vault", + type: "address", + }, + { + internalType: "address", + name: "deallocateAdapter", + type: "address", + }, + { + components: marketParamsAbi.components, + internalType: "struct MarketParams", + name: "deallocateMarketParams", + type: "tuple", + }, + { + internalType: "address", + name: "allocateAdapter", + type: "address", + }, + { + components: marketParamsAbi.components, + 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: marketParamsAbi.components, + 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", + }, +] as const; + /** Wrapped Backed token ABI used to discover permissioning controllers. */ export const wrappedBackedTokenAbi = [ { 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 9d82ccb75..0f89b5f38 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 { @@ -29,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 a4ee6d057..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 @@ -1,6 +1,16 @@ -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"; +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"; @@ -474,6 +484,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, + penalty: 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, { @@ -516,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() @@ -713,7 +792,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 f4d126bba..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 @@ -6,6 +6,7 @@ import { import { fetchMarket } from "@morpho-org/blue-sdk-viem"; import { type BlueAuthorizationAction, + type BlueReallocation, type ERC20ApprovalAction, type Metadata, type MorphoClientType, @@ -156,7 +157,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 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; @@ -164,6 +165,25 @@ export interface MorphoBorrowOptions { 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)"); @@ -667,14 +687,34 @@ 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( + public getBorrowRequirements( options: MorphoBorrowOptions, - ): Promise<(RequirementAuthorization | RequirementSignatureRequest)[]> { + ): Promise<(RequirementAuthorization | RequirementSignatureRequest)[]>; + public getBorrowRequirements( + options: MorphoBorrowWithV2ReallocationsOptions, + ): Promise< + ( + | RequirementApproval + | RequirementAuthorization + | RequirementSignatureRequest + )[] + >; + public async getBorrowRequirements( + options: MorphoBorrowInput, + ): Promise< + ( + | RequirementApproval + | RequirementAuthorization + | RequirementSignatureRequest + )[] + > { const action = await this._getBorrowAction(options); return await action.getRequirements(); @@ -688,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); @@ -702,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); @@ -728,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 b9f0514df..274a39db9 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", @@ -51,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];