Skip to content

Latest commit

 

History

History
167 lines (117 loc) · 9.24 KB

080.md

File metadata and controls

167 lines (117 loc) · 9.24 KB

Atomic Wool Tuna

High

An attacker will steal funds from the protocol by redirecting swap proceeds to their address.

Summary

The failure to validate the recipient field in user-supplied swap parameters will cause a misdirection of swap outputs for leveraged positions.

This results in an attacker being able to redirect borrowed or collateral funds to their own address, thereby stealing funds from the protocol and its users.

Root Cause

Within the Leverager contract, the openLeveragedPosition function decodes user-provided swap parameters swapParams1, swapParams2 without validating the recipient field.

/// @notice Opens up a leveraged position within a certain Vault.
    /// @dev Check the ILeverager contract for comments on all LeverageParams arguments
    /// In order to achieve this, contract first "flashloans" funds from the lending pool to open a position
    /// Then, it borrows the denomination token and performs swaps (if necessary)
    /// Any unused borrowed tokens, as well as flashloaned tokens are then repaid.
    function openLeveragedPosition(LeverageParams calldata lp) external nonReentrant returns (uint256 _id) {
        require(vaultParams[lp.vault].leverageEnabled, "leverage not enabled");

        Position memory up;
        up.token0 = IVault(lp.vault).token0();
        up.token1 = IVault(lp.vault).token1();
        up.vault = lp.vault;

        // we check the activity here, in order to make sure TWAP price is accurate
        // TWAP lags behind, so if we don't check, attacker might utilize old price
        uint256 price = IVault(lp.vault).twapPrice();
        require(IVault(lp.vault).checkPoolActivity(), "market too volatile");

        IERC20(up.token0).safeTransferFrom(msg.sender, address(this), lp.amount0In);
        IERC20(up.token1).safeTransferFrom(msg.sender, address(this), lp.amount1In);

        uint256 delta0 = lp.vault0In - lp.amount0In;
        uint256 delta1 = lp.vault1In - lp.amount1In;
        if (delta0 > 0) ILendingPool(lendingPool).pullFunds(up.token0, delta0); // we flashloan the difference between amounts desired to be deposited in Vault
        if (delta1 > 0) ILendingPool(lendingPool).pullFunds(up.token1, delta1); // and the amount pulled from the user

        // a0 and a1 represent the actual amounts deposited within the vault.
        // We keep track of them as we later make exactOutput swaps based on them
        (uint256 shares, uint256 a0, uint256 a1) =
            IVault(lp.vault).deposit(lp.vault0In, lp.vault1In, lp.min0in, lp.min1in);

        up.initCollateralUsd = _calculateTokenValues(up.token0, up.token1, a0, a1, price); // returns the USD price in 1e18
        uint256 bPrice = IPriceFeed(pricefeed).getPrice(lp.denomination);
        up.initCollateralValue = up.initCollateralUsd * (10 ** ERC20(lp.denomination).decimals()) / bPrice;

        {
            // we first borrow the maximum amount the user is willing to borrow. Any unused within the swaps is later repaid.
            ILendingPool(lendingPool).borrow(lp.denomination, lp.maxBorrowAmount);

            IMainnetRouter.ExactOutputParams memory swapParams;

            // Only important thing to verify here is that the tokenIn is NOT a vault share token.
            // Otherwise, attacker could utilize this to swap out all of the share tokens out of here.
            // We do not verify here that the output tokens is one of the lp tokens. If for some reason it isn't
            // The transaction will later revert within `pushFunds`
            if (a0 > lp.amount0In && up.token0 != lp.denomination) {
                swapParams = abi.decode(lp.swapParams1, (IMainnetRouter.ExactOutputParams));

                address tokenIn = _getTokenIn(swapParams.path);
                require(tokenIn == lp.denomination, "token should be denomination");

                IERC20(tokenIn).forceApprove(swapRouter, swapParams.amountInMaximum);

                swapParams.amountOut = a0 - lp.amount0In;
                IMainnetRouter(swapRouter).exactOutput(swapParams);
                IERC20(tokenIn).forceApprove(swapRouter, 0);
            }

            if (a1 > lp.amount1In && up.token1 != lp.denomination) {
                swapParams = abi.decode(lp.swapParams2, (IMainnetRouter.ExactOutputParams));
                address tokenIn = _getTokenIn(swapParams.path);
                require(tokenIn == lp.denomination, "token should be denomination 2 ");
                IERC20(tokenIn).forceApprove(swapRouter, swapParams.amountInMaximum);

                swapParams.amountOut = a1 - lp.amount1In;
                IMainnetRouter(swapRouter).exactOutput(swapParams);
                IERC20(tokenIn).forceApprove(swapRouter, 0);
            }
        }

        if (delta0 > 0) ILendingPool(lendingPool).pushFunds(up.token0, delta0);
        if (delta1 > 0) ILendingPool(lendingPool).pushFunds(up.token1, delta1);

        uint256 denomBalance = IERC20(lp.denomination).balanceOf(address(this));
        ILendingPool(lendingPool).repay(lp.denomination, denomBalance);

        // if for some reason there have previously been a large amount of tokens "stuck", this could fail
        // this is ok as 1) its unlikely 2) anyone could sweep them 3) likely MEV bot would sweep them within seconds
        // Although opening positions is time-sensitive, please do not report this as a vulnerability, ty.
        up.borrowedAmount = lp.maxBorrowAmount - denomBalance;
        up.initBorrowedUsd = up.borrowedAmount * bPrice / (10 ** ERC20(lp.denomination).decimals());

        up.borrowedIndex = ILendingPool(lendingPool).getCurrentBorrowingIndex(lp.denomination);
        up.denomination = lp.denomination;
        up.shares = shares;
        up.vault = lp.vault;

        vaultParams[lp.vault].currBorrowedUSD += up.initBorrowedUsd;
        _checkWithinlimits(up);

        _id = id++;
        positions[_id] = up;

        // here we check whether the position is liquidateable, as for positions with over 2x leverage
        // user could swap the all of the denom tokens for pretty much nothing in return
        // (sandwiching the tx himself). This way they'd steal the borrowed tokens and create
        // underwater position, which would force governance to liquidate it at a loss.
        require(!isLiquidateable(_id), "position can't be liquidateable upon opening");

        _mint(msg.sender, _id);
        _sweepTokens(up.token0, up.token1);

        return _id;
    }

This allows an attacker to craft swap parameters that set the recipient to an arbitrary address (e.g., the address controlled by the attacker) instead of the Leverager contract, thus diverting funds during the swap.

The issue is also prevalent in the withdraw function as well liquidatePosition function of the contract.

Internal Pre-conditions

  1. The Leverager contract accepts and decodes swap parameters (e.g., swapParams1 and swapParams2) without overriding or validating the recipient field.
  2. The swap execution via the router uses the provided swap parameters as-is, meaning the output tokens are sent to the address specified in the swap parameters.
  3. An attacker must have the ability to supply swap parameters when calling functions such as openLeveragedPosition, withdraw, or liquidatePosition.

External Pre-conditions

  1. The external swap router must honor the recipient field provided in the swap parameters without enforcing additional restrictions.
  2. The tokens involved must ensure that transfers based on swap outputs are executed as per the swap parameters.

Attack Path

  1. The attacker initiates a leveraged position by calling openLeveragedPosition and includes maliciously crafted swap parameters where the recipient field is set to the address controlled by the attacker.
  2. During the swap execution, the Leverager contract decodes the swap parameters without sanitizing the recipient.
  3. The swap router processes the swap, sending the output tokens directly to the address of the attacker.
  4. The attacker’s leveraged position now includes an outstanding debt that remains unpaid, while the attacker profits by receiving the diverted tokens, effectively stealing funds from the protocol.
  5. Similar exploitation may occur during withdraw and liquidatePosition, allowing further diversion of funds from ongoing operations.

Impact

The protocol suffers a loss equal to the stolen tokens. The attacker gains X tokens without repayment.

PoC

Step 1: Attacker crafts swap parameters with their own address as recipient. Step 2: Attacker supplies these parameters when opening a leveraged position. Step 3: When the swap is executed, the router sends tokens to the attacker instead of the Leverager.

Mitigation

Modify the Leverager contract to override the recipient field in all swap parameters. This means, immediately after decoding the parameters in functions such as openLeveragedPosition, withdraw, and liquidatePosition, set

swapParams.recipient = address(this);