Repository navigation
[Bug] Vault withdraw() sizes payout from ADPT_SH, which never appreciates, stranding all yield #486
Description
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
- Deposit into the live
meridian-usdcvault. - Wait for real yield to accrue in the underlying Blend pool (or call
accrue()/refresh()after a rate increase). - Withdraw 100% of your position.
- 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 arefresh()call, computeassets_out = shares * adapter.total_assets() / total_sharesand pass that to the adapter'swithdraw(). - Have
BlendAdapter::withdrawreturn 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 realBlendAdapterdoes 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
fix: correct inaccurate withdraw-payout comment per review
feat(contracts): refresh adapter cache before deposit/withdraw pricing (