Skip to content

Latest commit

 

History

History
109 lines (81 loc) · 5.66 KB

045.md

File metadata and controls

109 lines (81 loc) · 5.66 KB

Precise Fossilized Lobster

Medium

The mismatched price oracles in Leverager.sol allow attackers to overvalue their collateral and borrow more than they should

Summary

The Leverager contract’s price calculation mechanism can be misled due to its reliance on both Chainlink feeds and a Uniswap V3-based TWAP with minimal cross-verification. Whenever one token lacks a direct Chainlink price feed, the system uses the TWAP for conversion. Attackers can steadily manipulate the Uniswap TWAP to diverge from the Chainlink price, thereby inflating or deflating the recorded collateral value at the moment a leveraged position is created. This manipulation lets them bypass leverage constraints, creating effectively undercollateralized positions that appear valid under the contract’s checks.

Root Cause

The issue is within openLeveragedPosition in Leverager.sol. The function uses two price sources:

  1. Chainlink (when available).
  2. The Uniswap V3 TWAP (via IVault(lp.vault).twapPrice()), if the token has no Chainlink feed.

Once the position is opened, the collateral’s USD value is locked in using _calculateTokenValues:

function openLeveragedPosition(LeverageParams calldata lp) external nonReentrant returns (uint256 _id) {
    // Simplified excerpt:
    uint256 price = IVault(lp.vault).twapPrice();
    require(IVault(lp.vault).checkPoolActivity(), "market too volatile");
    
    // 1) Gather user tokens + flashloaned tokens
    // 2) Deposit into the Vault
    (uint256 shares, uint256 a0, uint256 a1) = IVault(lp.vault).deposit(...);

    // 3) Compute collateral in USD
    up.initCollateralUsd = _calculateTokenValues(up.token0, up.token1, a0, a1, price);

    // 4) Convert that USD figure to 'denomination' tokens
    uint256 bPrice = IPriceFeed(pricefeed).getPrice(lp.denomination);
    up.initCollateralValue = up.initCollateralUsd * (10 ** ERC20(lp.denomination).decimals()) / bPrice;

    // 5) Borrow from LendingPool and perform swaps
    ILendingPool(lendingPool).borrow(lp.denomination, lp.maxBorrowAmount);
    ...
    ILendingPool(lendingPool).repay(lp.denomination, denomBalance);

    // 6) Validate the final leverage
    _checkWithinlimits(up);
    require(!isLiquidateable(_id = id++), "position can't be liquidateable upon opening");
    ...
}

Inside _calculateTokenValues, a token with no Chainlink feed is converted to USD using the “price” variable, which is the TWAP from IVault(lp.vault).twapPrice():

function _calculateTokenValues(
    address token0,
    address token1,
    uint256 amount0,
    uint256 amount1,
    uint256 price // the TWAP from the pool
) internal view returns (uint256 usdValue) {
    uint256 chPrice0;
    uint256 chPrice1;

    if (IPriceFeed(pricefeed).hasPriceFeed(token0)) {
        chPrice0 = IPriceFeed(pricefeed).getPrice(token0);
        usdValue += amount0 * chPrice0 / decimals0;
    }
    if (IPriceFeed(pricefeed).hasPriceFeed(token1)) {
        chPrice1 = IPriceFeed(pricefeed).getPrice(token1);
        usdValue += amount1 * chPrice1 / decimals1;
    }

    // If token0 lacks Chainlink feed, we rely on TWAP for partial conversion:
    if (chPrice0 == 0) {
        usdValue += (amount0 * price / PRECISION) * chPrice1 / decimals1;
    } else if (chPrice1 == 0) {
        usdValue += amount1 * PRECISION / price * chPrice0 / decimals0;
    }
}

Internal Pre-conditions

  1. A user calls openLeveragedPosition() to create a leveraged position in Leverager.sol.
  2. The vaultParams for the chosen vault are already set to leverageEnabled = true.
  3. One of the tokens in that vault lacks a direct Chainlink feed, triggering TWAP-based valuation in _calculateTokenValues().
  4. checkPoolActivity() is set with a maxObservationDeviation that can tolerate gradual price movements.

External Pre-conditions

  1. The Uniswap V3 pool for the underlying token pair has sufficient liquidity to let an attacker trade incrementally and move the TWAP without triggering abrupt changes.
  2. The Chainlink feed for the other token remains steady (not manipulated externally).

Attack Path

  1. The attacker makes many correctly calculated trades in the Uniswap V3 pool to shift the price incrementally, keeping each observation within maxObservationDeviation, bypassing checkPoolActivity().
  2. The attacker calls openLeveragedPosition(), triggering the system to fetch the manipulated TWAP price for the token lacking a Chainlink feed.
  3. _calculateTokenValues() sees a harmlessly normal Chainlink price for the other token, but a distorted TWAP for the manipulated token, inflating the total collateral USD value.
  4. The attacker passes the leverage checks, borrowing more than what would otherwise be allowed.
  5. The attacker finalizes the position, leaving the protocol exposed to bad debt whenever a liquidation attempt is made at the real market price.

Impact

The protocol suffers a potential severe loss of capital. Attackers can open undercollateralized positions based on an inflated TWAP price and leave behind bad debt when liquidation occurs or when price reverts.

PoC

Bypassing checkPoolActivity() is shown in #4 After bypassing checkPoolActivity(), the attack is straightforward: call openLeveragedPosition() -> _calculateTokenValues()` inflates the total collateral USD value -> attacker borrows more than allowed.

Mitigation

Compare TWAP-based valuations with Chainlink prices whenever both tokens have reliable feeds. Reject positions if the two price sources differ by more than a configured threshold.