# Yield withdrawal blocked by zero reward early return

Distributor.distributeRewards early-returns when period rewards are zero, so lastUnderlyingBalance never updates and post-maxEndTime yield is permanently stuck.

- Payout: Contest reward
- Severity: medium
- Program: Morpheus
- Credited: KlosMitSoss
- Disclosed: 2025-09-11
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2025-08-morpheus#m-04
- Library page: https://bugbountylibrary.com/r/c4%3A2025-08-morpheus%3AM-04
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:medium tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** Distributor.sol yield accounting (distributeRewards/withdrawYield)
**Root cause:** distributeRewards() returns early when getPeriodRewards()==0, skipping the lastUnderlyingBalance update that withdrawYield() relies on to compute accrued yield.
**Steps:**
1. Deploy protocol; set rewardPoolLastCalculatedTimestamp to oneDay*7 and minRewardsDistributePeriod=1.
2. Alice stakes 100 tokens at t=oneDay*9; deposited=lastUnderlyingBalance=100.
3. Mint 100 tokens to Distributor to imitate yield.
4. Advance to maxEndTime (oneDay*11); call `distributor.distributeRewards(0)` — rewards nonzero, lastUnderlyingBalance becomes 200; `withdrawYield` sweeps 100 to L1Sender.
5. Advance to oneDay*14; mint another 100 to Distributor as post-maxEndTime yield.
6. `rewardPool.getPeriodRewards(0, oneDay*11, oneDay*14)` returns 0, so `distributeRewards` early-returns and lastUnderlyingBalance stays 100.
7. Alice withdraws 100; deposited=lastUnderlyingBalance=0 but Distributor still holds 100.
8. `distributor.withdrawYield(0, depositPool)` is a no-op; L1Sender balance unchanged — yield permanently stuck.
**How found:** Code reading of distributeRewards: noticed the `if (rewards_ == 0) return;` guard precedes the lastUnderlyingBalance update, then reasoned that getPeriodRewards is permanently zero after maxEndTime, breaking yield accounting.
**Exploitation:** No attacker profit; yield accruing after reward distribution ends becomes permanently unwithdrawable because distributeRewards never updates lastUnderlyingBalance once getPeriodRewards returns zero.
**Insight:** When an accounting variable is only refreshed inside a function gated by an unrelated condition (here reward accrual), check whether that condition can become permanently false — post-emission periods are a classic trigger for stuck accounting.
