Atomic Wool Tuna
High
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.
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.
- The Leverager contract accepts and decodes swap parameters (e.g.,
swapParams1
andswapParams2
) without overriding or validating therecipient
field. - 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.
- An attacker must have the ability to supply swap parameters when calling functions such as
openLeveragedPosition
,withdraw
, orliquidatePosition
.
- The external swap router must honor the
recipient
field provided in the swap parameters without enforcing additional restrictions. - The tokens involved must ensure that transfers based on swap outputs are executed as per the swap parameters.
- The attacker initiates a leveraged position by calling
openLeveragedPosition
and includes maliciously crafted swap parameters where therecipient
field is set to the address controlled by the attacker. - During the swap execution, the
Leverager
contract decodes the swap parameters without sanitizing therecipient
. - The swap router processes the swap, sending the output tokens directly to the address of the attacker.
- 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.
- Similar exploitation may occur during
withdraw
andliquidatePosition
, allowing further diversion of funds from ongoing operations.
The protocol suffers a loss equal to the stolen tokens. The attacker gains X
tokens without repayment.
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.
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);