Skip to content

[Bug] Vault withdraw() sizes payout from ADPT_SH, which never appreciates, stranding all yield #486

Closed

Description

@collinsezedike

Description

MeridianVault::withdraw sizes the adapter redemption as a proportional share of ADPT_SH, a counter incremented only by adapter.deposit()'s return value. BlendAdapter::deposit returns the raw USDC amount it was given (packages/contracts/blend-adapter/src/lib.rs), so ADPT_SH only ever tracks deposited principal and never reflects accrued yield. That undersized figure is passed straight to Blend as the withdrawal request amount.

A withdrawer therefore receives back only their principal. Any real yield Blend has credited on the position stays behind in the adapter permanently — there is no sweep function and no upgrade path to recover it.

Confirmed via a full-exit simulation against the live testnet vault: withdrawing 100% of the vault's shares returns exactly the deposited principal in USDC, while the adapter's actual bToken collateral balance (which includes accrued interest) is left with a small non-zero remainder that becomes permanently unreachable once ADPT_SH resets to zero.

This is a critical correctness bug in a yield aggregator: the product currently behaves as a custody vault, not a yield-paying one, at the withdrawal level.

Steps to Reproduce

  1. Deposit into the live meridian-usdc vault.
  2. Wait for real yield to accrue in the underlying Blend pool (or call accrue()/refresh() after a rate increase).
  3. Withdraw 100% of your position.
  4. Compare the USDC received against the adapter's total_assets() immediately before the withdrawal.

Expected Behavior

A full withdrawal should return the depositor's proportional share of the adapter's live total_assets(), including accrued yield.

Actual Behavior

Only principal is returned; accrued yield remains stranded in the adapter with no recovery path.

Environment

Field Value
Network testnet
Wallet N/A
Protocol affected Blend
Browser (if frontend) N/A
Node.js version N/A
pnpm version N/A

Possible Cause / Fix

  • Stop deriving withdrawal amounts from ADPT_SH. After a refresh() call, compute assets_out = shares * adapter.total_assets() / total_shares and pass that to the adapter's withdraw().
  • Have BlendAdapter::withdraw return the measured USDC delta it actually redeemed, rather than echoing its input unconditionally (see also the adapter-return-verification gap, a separate but related issue).
  • Add a test whose mock adapter burns bTokens at a rate that is not 1:1 with USDC deposited. The current MockAdapter (vault/src/lib.rs) redeems proportionally to its own USDC balance, which is exactly the behavior the real BlendAdapter does not have, so the existing yield-related tests pass against behavior that doesn't exist in production.

Additional Context

This issue was deliberately not filed alongside #418 (deposit-side pricing refresh) because #418's own premise incorrectly assumed withdrawal payout was already computed independently of the vault's cache. #418's fix (see its PR) correctly refreshes deposit-side pricing but does not touch this withdrawal-sizing bug — landing it does not resolve this issue, and in fact makes the dashboard's displayed "earned" figure accurate while the actual withdrawal still underpays, which is worth being aware of when reviewing that PR.

Activity

  1. added
    bugSomething isn't working
    hardComplex implementation spanning multiple packages or involving Soroban contracts
    contractsInvolves writing or testing Rust/Soroban contracts in packages/contracts
    securitySecurity hardening, vulnerability fixes, or audit-related work
    on Aug 9, 2026
  2. added
    Stellar WaveIssues in the Stellar wave program
    and removed
    Stellar WaveIssues in the Stellar wave program
    on Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcontractsInvolves writing or testing Rust/Soroban contracts in packages/contractshardComplex implementation spanning multiple packages or involving Soroban contractssecuritySecurity hardening, vulnerability fixes, or audit-related work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions