Stratax Contracts

First Flight #57
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: high
Likelihood: high

`Stratax::_executeUnwindOperation` only withdraws collateral to cover position debt, permanently locking user's original collateral in Aave

Author Revealed upon completion

Stratax::_executeUnwindOperation only withdraws collateral to cover position debt, permanently locking user's original collateral in Aave

Description

When a user calls Stratax::createLeveragedPosition the must specify an amount of collateral they are willing to supply and this must be greater than 0. Then if they decide they would like to unwind the position and call Stratax::unwindPosition, the amount to withdraw is calculated to effectively be only the collateral that backed the repaid debt.

This then leaves the users original supplied collateral in the Aave position with no way to retrieve it, effectively locking the tokens.

function createLeveragedPosition(
address _flashLoanToken,
uint256 _flashLoanAmount,
uint256 _collateralAmount,
address _borrowToken,
uint256 _borrowAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
) public onlyOwner {
@> require(_collateralAmount > 0, "Collateral Cannot be Zero");
@> // Transfer the user's collateral to the contract
@> IERC20(_flashLoanToken).transferFrom(msg.sender, address(this), _collateralAmount);
// ...
}
function _executeUnwindOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
// ...
@> // Step 2: Calculate and withdraw only the collateral that backed the repaid debt
uint256 withdrawnAmount;
{
// Get LTV from Aave for the collateral token
(,, uint256 liqThreshold,,,,,,,) =
aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
// Get prices and decimals
uint256 debtTokenPrice = IStrataxOracle(strataxOracle).getPrice(_asset);
uint256 collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(unwindParams.collateralToken);
require(debtTokenPrice > 0 && collateralTokenPrice > 0, "Invalid prices");
@> // Calculate collateral to withdraw: (debtAmount * debtPrice * collateralDec * LTV_PRECISION) / (collateralPrice * debtDec * ltv)
@> uint256 collateralToWithdraw = (
@> _amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
@> ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);
@> withdrawnAmount = aavePool.withdraw(unwindParams.collateralToken, uint256(unwindParams.collateralToWithdraw), address(this));
}
// ...
}

Risk

Likelihood:

  • Every time a user opens and unwinds a position, and collateral left over after paying the position debt is locked in Aave.

  • There are no special conditions required so this will occur 100% of the time.

Impact:

  • Users will have their funds permenantly locked in Aave postions with no means of retrieving them.

  • This is compounded by the fact that Stratax::_executeOpenOperation and Stratax::_executeUnwindOperation both supply excess tokens from swaps back into Aave.

Proof of Concept

  1. A user opens a new leveraged position

  2. The user then decides to unwind that leveraged position

  3. The user's original supplied collateral is now stuck in the Aave position

Add the following test to test/fork/Stratax.t.sol

function test_OpenAndUnwindLocksUserCollateral() public {
if (!hasApiKey && !usesSavedData) {
vm.skip(true);
}
console.log("");
console.log("============================================================");
console.log(" STRATAX OPEN AND UNWIND TEST: Locks User Collateral");
console.log("============================================================");
// ──────────────────────────────────────────────────────────────
// POSITION: USDC Collateral / WETH Borrow (3x Leverage)
// ──────────────────────────────────────────────────────────────
console.log("");
console.log("------------------------------------------------------------");
console.log(" POSITION: USDC Collateral / WETH Borrow");
console.log("------------------------------------------------------------");
uint256 collateralAmount = 1000 * 10 ** 6;
(uint256 flashLoanAmount, uint256 borrowAmount) = stratax.calculateOpenParams(
Stratax.TradeDetails({
collateralToken: address(USDC),
borrowToken: address(WETH),
desiredLeverage: 30_000,
collateralAmount: collateralAmount,
collateralTokenPrice: 0,
borrowTokenPrice: 0,
collateralTokenDec: 6,
borrowTokenDec: 18
})
);
(bytes memory openSwapData,) = get1inchSwapData(WETH, USDC, borrowAmount, address(stratax));
deal(USDC, ownerTrader, collateralAmount);
console.log(" [DEAL] USDC to ownerTrader: $", collateralAmount / 1e6);
console.log("");
console.log(" [OPEN] Parameters:");
console.log(" Collateral (USDC): $", collateralAmount / 1e6);
console.log(" Flash Loan (USDC): $", flashLoanAmount / 1e6);
console.log(" Borrow (WETH): ", borrowAmount / 1e18,".",(borrowAmount % 1e18) / 1e14);
console.log("");
console.log(" [OPEN] User Wallet Before:");
console.log(" USDC: $", IERC20(USDC).balanceOf(ownerTrader) / 1e6);
vm.startPrank(ownerTrader);
IERC20(USDC).approve(address(stratax), collateralAmount);
stratax.createLeveragedPosition(
USDC, flashLoanAmount, collateralAmount, WETH, borrowAmount, openSwapData, (flashLoanAmount * 950) / 1000
);
(uint256 totalCollateralAfterOpen, uint256 totalDebtAfterOpen,,,, uint256 healthFactorAfterOpen) =
IPool(AAVE_POOL).getUserAccountData(address(stratax));
console.log("");
console.log(" [OPEN] Aave Position After:");
console.log(" Total Collateral (USD): $", totalCollateralAfterOpen / 1e8);
console.log(" Total Debt (USD): $", totalDebtAfterOpen / 1e8);
console.log(" Health Factor: ", healthFactorAfterOpen / 1e14);
assertTrue(totalCollateralAfterOpen > 0, "Should have collateral");
assertTrue(totalDebtAfterOpen > 0, "Should have debt");
assertTrue(healthFactorAfterOpen > 1e18, "Health factor should be above 1");
// Unwind position
console.log("");
console.log(" [UNWIND] Calculating unwind params...");
(uint256 collateralToWithdraw, uint256 debtAmount) = stratax.calculateUnwindParams(USDC, WETH);
console.log(" Collateral to Withdraw (USDC): $", collateralToWithdraw / 1e6);
console.log(" Debt to Repay (WETH): ", debtAmount / 1e18,".",(debtAmount % 1e18) / 1e14);
console.log(" [UNWIND] Fetching 1inch swap data...");
(bytes memory unwindSwapData,) = get1inchSwapData(USDC, WETH, collateralToWithdraw, address(stratax));
console.log(" [UNWIND] Executing unwind...");
stratax.unwindPosition(USDC, collateralToWithdraw, WETH, debtAmount, unwindSwapData, (debtAmount * 950) / 1000);
vm.stopPrank();
(uint256 totalCollateralAfterUnwind, uint256 totalDebtAfterUnwind,,,,) =
IPool(AAVE_POOL).getUserAccountData(address(stratax));
uint256 userUsdcAfter = IERC20(USDC).balanceOf(ownerTrader);
uint256 userWethAfter = IERC20(WETH).balanceOf(ownerTrader);
console.log("");
console.log(" [UNWIND] Aave Position After:");
console.log(" Total Collateral (USD): $", totalCollateralAfterUnwind / 1e8);
console.log(" Total Debt (USD): $", totalDebtAfterUnwind / 1e8);
console.log("");
console.log(" [UNWIND] User Wallet After:");
console.log(" USDC: ", userUsdcAfter / 1e6);
console.log(" WETH: ", userWethAfter / 1e18);
console.log("");
console.log("============================================================");
console.log(" RESULT");
console.log("============================================================");
console.log(" User deposited (USDC): $", collateralAmount / 1e6);
console.log(" User received back (USDC): $", userUsdcAfter / 1e6);
console.log(" Locked in Aave (USD): $", totalCollateralAfterUnwind / 1e8);
console.log(" Remaining Aave debt (USD): $", totalDebtAfterUnwind / 1e8);
console.log("============================================================");
console.log("");
assertTrue(totalDebtAfterUnwind == 0, "All debt should be repaid");
assertTrue(totalCollateralAfterUnwind > 0, "Collateral is still locked in Aave");
assertTrue(
userUsdcAfter == 0,
"User received no USDC back - funds permanently locked in Aave position"
);
}
Expected output:
============================================================
RESULT
============================================================
User deposited (USDC): $ 1000
User received back (USDC): $ 0
Locked in Aave (USD): $ 983
Remaining Aave debt (USD): $ 0
============================================================

Recommended Mitigation

Add the following withdrawCollateral function, allowing the owner/user to withdraw a specified amount of collateral from a Stratax contract Aave position

+ function withdrawCollateral(address _collateralToken, uint256 _amount) external onlyOwner {
+ aavePool.withdraw(_collateralToken, _amount, msg.sender);
+ }

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!