# Calls to liquidate don't write down totalBorrows which breaks exchange rate

D3Vault liquidate() decrements per-record borrows but not totalBorrows, inflating pToken exchange rate so last withdrawer cannot redeem underlying.

- Payout: Contest reward
- Severity: high
- Program: Dodo
- Credited: dirk_y
- Disclosed: 2023-06-14
- Write-up quality: 6/10
- Original: https://github.com/sherlock-audit/2023-06-dodo-judging/issues/46
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-06-dodo%3AH-2
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:high tech:evm tech:solidity technique:source-review vuln:accounting-error vuln:liquidation

**Target:** D3VaultLiquidation.sol liquidate()
**Root cause:** liquidate() updates record.amount = borrows - debtToCover but never decrements info.totalBorrows, unlike finishLiquidation which does. totalBorrows feeds the pToken exchange rate, so it stays inflated after liquidation.
**Steps:**
1. Call liquidate() on a pool with outstanding debt, specifying debtToCover.
2. Observe record.amount is reduced by debtToCover but info.totalBorrows is unchanged.
3. pToken exchange rate remains computed off the un-decremented totalBorrows, overstating interest owed.
4. LPs withdraw at inflated rate; last withdrawer cannot redeem pTokens for underlying.
**How found:** Code reading comparing liquidate() against finishLiquidation(): noticed finishLiquidation correctly writes down totalBorrows while liquidate() only writes down record.amount, then traced totalBorrows into the exchange-rate calculation.
**Exploitation:** LPs monitor liquidations and withdraw early at the inflated exchange rate, extracting more underlying than their fair share; the final withdrawer is left unable to redeem due to insufficient underlying.
**Insight:** When auditing liquidation paths, diff every state variable written in the normal repayment/finish path against the liquidation path; totalBorrows/totalDebt style aggregates that feed share price are common omissions.
