Root + Impact
Description
-
The protocol executes user-supplied 1inch calldata inside _call1InchSwap(). The _swapParams are provided via FlashLoanParams.oneInchSwapData and are not validated before execution.
-
Critically, the protocol does not** **enforce that the swap’s dstReceiver (or equivalent output recipient field) is address(this).
This allows a user to craft swap calldata where the output tokens are sent to an arbitrary address instead of the Stratax contract, breaking core accounting and repayment assumptions.
function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal
returns (uint256 returnAmount)
{
@> (bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);
require(success, "1inch swap failed");
if (result.length > 0) {
(returnAmount,) = abi.decode(result, (uint256, uint256));
} else {
returnAmount = IERC20(_asset).balanceOf(address(this));
}
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
return returnAmount;
}
Risk
Likelihood:
Impact:
-
Swap output may not be received by the contract
-
Flash loan repayment assumptions are broken
The protocol assumes swap output funds repayment:
require(returnAmount >= totalDebt, "Insufficient funds to repay flash loan");
However, no invariant ensures that:
swap output recipient == address(this)
Proof of Concept
Add the following to test/fork/Stratax.t.soland run forge test --match-test test_PoC_SwapReceiverRedirection -vv
function test_PoC_SwapReceiverRedirection() public {
address attacker = address(0xdead);
uint256 swapAmount = 100 * 10**6;
deal(USDC, address(stratax), swapAmount);
bytes memory maliciousSwapData = abi.encodeWithSignature("maliciousSwap()");
uint256 fakeReturnAmount = 0.3 ether;
bytes memory fakeResult = abi.encode(fakeReturnAmount, uint256(0));
vm.mockCall(
INCH_ROUTER,
maliciousSwapData,
fakeResult
);
vm.store(USDC, keccak256(abi.encode(address(stratax), uint256(0))), 0);
deal(WETH, attacker, fakeReturnAmount);
Stratax.FlashLoanParams memory params = Stratax.FlashLoanParams({
collateralToken: WETH,
collateralAmount: 0.4 ether,
borrowToken: USDC,
borrowAmount: swapAmount,
oneInchSwapData: maliciousSwapData,
minReturnAmount: 0.3 ether
});
bytes memory encodedParams = abi.encode(Stratax.OperationType.OPEN, ownerTrader, params);
deal(WETH, address(stratax), 0.5 ether);
uint256 loanAmount = 0.01 ether;
uint256 premium = loanAmount / 1000;
vm.expectRevert("Borrow token left in contract");
vm.prank(AAVE_POOL);
stratax.executeOperation(
WETH,
loanAmount,
premium,
address(stratax),
encodedParams
);
uint256 attackerWETHBalance = IERC20(WETH).balanceOf(attacker);
assertEq(attackerWETHBalance, fakeReturnAmount, "Attacker receives the swapped funds due to malicious receiver");
console.log("PoC Success: _call1InchSwap does not verify receiver, allowing swap redirection");
}
Recommended Mitigation
Decode swap calldata and require:
require(decodedReceiver == address(this), "Invalid swap receiver");
// OR use delta to ensure that swap output was actually received by the contract
function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal
returns (uint256 returnAmount)
{
+ // 1. Snapshot the balance before the swap
+ uint256 balanceBefore = IERC20(_asset).balanceOf(address(this));
// Execute the 1inch swap using low-level call with the calldata from the API
(bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);
require(success, "1inch swap failed");
- // Decode the return amount from the swap
- if (result.length > 0) {
- (returnAmount,) = abi.decode(result, (uint256, uint256));
- } else {
- // If no return data, check balance
- returnAmount = IERC20(_asset).balanceOf(address(this));
- }
+ // 2. Snapshot the balance after the swap
+ uint256 balanceAfter = IERC20(_asset).balanceOf(address(this));
+ // 3. Calculate actual tokens received
+ returnAmount = balanceAfter - balanceBefore;
// Sanity check
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
return returnAmount;
}