Root + Impact
Description
-
In _executeOpenOperation, the protocol calls _call1InchSwap with:
_call1InchSwap(
flashParams.oneInchSwapData,
flashParams.borrowToken,
flashParams.minReturnAmount
);
However, the swap being executed converts:
borrowToken → collateralToken (_asset)
The _asset parameter (flash loan asset) represents the collateral token, not the borrow token.
Passing flashParams.borrowToken instead of the collateral token into _call1InchSwap causes swap output accounting to be performed against the wrong token.s
function _executeOpenOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
IERC20(flashParams.borrowToken).approve(address(oneInchRouter), flashParams.borrowAmount);
uint256 returnAmount =
@> _call1InchSwap(flashParams.oneInchSwapData, flashParams.borrowToken, flashParams.minReturnAmount);
uint256 afterSwapBorrowTokenbalance = IERC20(flashParams.borrowToken).balanceOf(address(this));
require(afterSwapBorrowTokenbalance == prevBorrowTokenBalance, "Borrow token left in contract");
return true;
}
Risk
Likelihood:
Impact:
Proof of Concept
Add the following test case to test/fork/Stratax.t.soland run: forge test --match-test test_Exploit_Confirmed_WrongAssetAccounting -vvv
function test_Exploit_Confirmed_WrongAssetAccounting() public {
uint256 massiveUSDC = 200 * 10**18;
deal(USDC, address(stratax), massiveUSDC);
vm.mockCall(AAVE_POOL, abi.encodeWithSelector(IPool.supply.selector), abi.encode(true));
vm.mockCall(AAVE_POOL, abi.encodeWithSelector(IPool.borrow.selector), abi.encode(true));
vm.mockCall(AAVE_POOL, abi.encodeWithSelector(IPool.getUserAccountData.selector),
abi.encode(0, 0, 0, 0, 0, 2e18));
bytes memory swapCalldata = abi.encodeWithSignature("swap()");
vm.mockCall(INCH_ROUTER, swapCalldata, "");
Stratax.FlashLoanParams memory params = Stratax.FlashLoanParams({
collateralToken: WETH,
collateralAmount: 0,
borrowToken: USDC,
borrowAmount: 0,
oneInchSwapData: swapCalldata,
minReturnAmount: 100 ether
});
vm.startPrank(AAVE_POOL);
bool success = stratax.executeOperation(
WETH,
0.1 ether,
0,
address(stratax),
abi.encode(Stratax.OperationType.OPEN, address(this), params)
);
vm.stopPrank();
assertTrue(success, "Vulnerability Confirmed: Accepted USDC balance as WETH return amount!");
console.log("The contract reported success because it looked at USDC balance instead of WETH.");
}
Recommended Mitigation
Change flashParams.borrowToken to flashParams.collateralToken as shown below
function _executeOpenOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
(, address user, FlashLoanParams memory flashParams) =
abi.decode(_params, (OperationType, address, FlashLoanParams));
// Step 1: Supply flash loan amount + user's extra amount to Aave as collateral
uint256 totalCollateral = _amount + flashParams.collateralAmount;
IERC20(_asset).approve(address(aavePool), totalCollateral);
aavePool.supply(_asset, totalCollateral, address(this), 0);
//......
// Step 3: Swap borrowed tokens via 1inch to get back the collateral token
IERC20(flashParams.borrowToken).approve(address(oneInchRouter), flashParams.borrowAmount);
// Execute swap via 1inch
uint256 returnAmount =
- _call1InchSwap(flashParams.oneInchSwapData, flashParams.borrowToken, flashParams.minReturnAmount);
+ _call1InchSwap(flashParams.oneInchSwapData, flashParams.collateralToken, flashParams.minReturnAmount);
// Ensure all borrowed tokens were used in the swap
uint256 afterSwapBorrowTokenbalance = IERC20(flashParams.borrowToken).balanceOf(address(this));
require(afterSwapBorrowTokenbalance == prevBorrowTokenBalance, "Borrow token left in contract");
//.....rest of the code
}