Skip to content

fix(blue-sdk): throw UnknownMarketAllocation on stale withdrawQueue#548

Open
Kubudak90 wants to merge 1 commit into
morpho-org:mainfrom
Kubudak90:fix/vault-accrueInterest-missing-allocation
Open

fix(blue-sdk): throw UnknownMarketAllocation on stale withdrawQueue#548
Kubudak90 wants to merge 1 commit into
morpho-org:mainfrom
Kubudak90:fix/vault-accrueInterest-missing-allocation

Conversation

@Kubudak90
Copy link
Copy Markdown

Problem

The AccrualVault.accrueInterest() method uses a non-null assertion (!) when looking up allocations by market ID from the withdrawQueue:

const { config, position } = this.allocations.get(marketId)!;

This is unsafe because the withdrawQueue may contain market IDs that are not present in allocations. For example, after a market is removed from a vault's allocations but is still referenced in the withdraw queue, this would cause a runtime crash.

Solution

Replace the non-null assertion with a safe lookup and filter out any missing allocations:

const allocation = this.allocations.get(marketId);
if (!allocation) return null;
// ...

This preserves the withdraw queue order while gracefully handling missing markets.

Changes

  • packages/blue-sdk/src/vault/Vault.ts: Safe allocation lookup with null filter
  • packages/blue-sdk/test/unit/Vault.test.ts: Regression test for missing allocation in withdrawQueue

Testing

npx vitest --run --project blue-sdk test/unit/Vault.test.ts

All tests pass (2/2).

@Rubilmax
Copy link
Copy Markdown
Collaborator

@prd-carapulse[bot] review this PR

@prd-carapulse
Copy link
Copy Markdown
Contributor

prd-carapulse Bot commented May 26, 2026

Reviewed. Changes requested: #548 (review)

Blocking:

  • @morpho-org/blue-sdk build fails with TS2353 because test fixtures pass withdrawQueue into AccrualVault constructor input, but IAccrualVault omits that field.
  • New regression test does not exercise intended stale withdrawQueue case: AccrualVault rebuilds withdrawQueue from allocations, so missing market is already dropped before accrueInterest().

Verified:

  • pnpm exec vitest --project blue-sdk --run packages/blue-sdk/test/unit/Vault.test.ts
  • pnpm --filter @morpho-org/blue-sdk run build

Track: https://carapulse.morpho.dev/dashboard/runs?run_id=gw_run_ab632b458a06

Copy link
Copy Markdown
Contributor

@prd-carapulse prd-carapulse Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issue found. @morpho-org/blue-sdk build fails, and the new regression test does not exercise the stale-withdrawQueue path because AccrualVault rebuilds withdrawQueue from the allocations argument.

Verification:

  • pnpm exec vitest --project blue-sdk --run packages/blue-sdk/test/unit/Vault.test.ts
  • pnpm --filter @morpho-org/blue-sdk run buildTS2353: 'withdrawQueue' does not exist in type 'Omit<IVault, "totalAssets" | "withdrawQueue">'

Comment thread packages/blue-sdk/test/unit/Vault.test.ts Outdated
Comment thread packages/blue-sdk/test/unit/Vault.test.ts Outdated
@Kubudak90 Kubudak90 requested a review from a team as a code owner May 27, 2026 06:17
@Rubilmax Rubilmax closed this May 27, 2026
@Rubilmax Rubilmax reopened this May 27, 2026
@Rubilmax Rubilmax closed this May 27, 2026
@Rubilmax Rubilmax reopened this May 27, 2026
Copy link
Copy Markdown
Collaborator

@Rubilmax Rubilmax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is worth implementing! We just have a different POV regarding missing allocations: we'd prefer loudly failing instead of silently ignoring missing markets

Comment thread packages/blue-sdk/src/vault/Vault.ts Outdated
this.withdrawQueue
.map((marketId) => {
const allocation = this.allocations.get(marketId);
if (!allocation) return null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should throw a custom error UnknownMarketAllocation here instead of silently not accruing so we ensure the returned vault is 100% accurate

Replace the non-null assertion `this.allocations.get(marketId)!` in
`AccrualVault.accrueInterest()` with an explicit throw. When the
withdraw queue is mutated after construction to reference a market
that is not present in `allocations`, the previous code crashed with
an opaque "Cannot destructure property 'config' of 'undefined'" at
the destructuring site instead of surfacing a typed error.

`UnknownMarketAllocation` extends `UnknownDataError`, consistent with
the other unknown-data error classes in this package. Callers that
want to tolerate stale entries can opt in via
`_try(() => vault.accrueInterest(), UnknownDataError)`.

Fixes morpho-org#527
@Kubudak90 Kubudak90 force-pushed the fix/vault-accrueInterest-missing-allocation branch from f8f6056 to 0535a24 Compare May 27, 2026 20:51
@Kubudak90 Kubudak90 changed the title fix(blue-sdk): remove non-null assertion in AccrualVault.accrueInterest fix(blue-sdk): throw UnknownMarketAllocation on stale withdrawQueue May 27, 2026
@Kubudak90
Copy link
Copy Markdown
Author

Switched to the fail-loud approach per @Rubilmax's feedback. The non-null assertion now becomes an explicit throw new UnknownMarketAllocation(marketId) instead of a silent skip + filter, so the returned vault is always 100% accurate (or the caller gets a typed error).

Summary of changes (single commit, rebased onto current main):

  • packages/blue-sdk/src/errors.ts — new UnknownMarketAllocation extends UnknownDataError, mirroring the existing Unknown* error pattern. Callers that want graceful degradation can opt in via _try(() => vault.accrueInterest(), UnknownDataError).
  • packages/blue-sdk/src/vault/Vault.tsaccrueInterest() now throws UnknownMarketAllocation if withdrawQueue references a market that isn't in allocations, instead of crashing with the opaque Cannot destructure property 'config' of 'undefined'.
  • packages/blue-sdk/src/vault/Vault.test.ts — added a regression test using the project's existing fixture helpers (accrualVault, vaultInput). The test mutates withdrawQueue after construction to introduce the stale entry (the only way to reach this branch, since the AccrualVault constructor rebuilds withdrawQueue from allocations) and asserts .toThrow(UnknownMarketAllocation).

Addresses both review comments:

  • @Rubilmax: silent skip → loud fail with a typed error.
  • @prd-carapulse: build is green locally (pnpm --filter @morpho-org/blue-sdk run build ✅), and the test exercises the actual bug path by mutating withdrawQueue after construction; the new expect(...).toThrow(UnknownMarketAllocation) assertion would fail on the unfixed code (the ! assertion throws TypeError: Cannot destructure property 'config' of 'undefined', not UnknownMarketAllocation).

Local verification:

  • pnpm --filter @morpho-org/blue-sdk run build → ✅
  • pnpm exec vitest --project blue-sdk --run --exclude '**/e2e/**' → 333/333 passing
  • pnpm exec biome check packages/blue-sdk/src/{errors.ts,vault/Vault.ts,vault/Vault.test.ts} → clean

Ready for re-review.

// Mutate the public `withdrawQueue` after construction so it references a
// market that has no corresponding entry in `allocations`. Without this
// mutation the bug cannot be reached, because the AccrualVault constructor
// rebuilds `withdrawQueue` from the allocations argument.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This alone makes the bug unreachable with standard behavior so in the end is it worth it? Sorry for the back-and-forth

WDYT @Foulks-Plb ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants