Stratax Contracts

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

Contains the 5 high-severity vulnerabilities... check:

Author Revealed upon completion

Arbitrary External Call Injection via 1inch Swap Data

Description

The _call1InchSwap function performs a low-level call to the oneInchRouter with arbitrary calldata provided by the user. While the router address is set during initialization, the calldata is completely user-controlled and not validated. This allows the owner to craft malicious calldata that could call any function on the 1inch router, potentially including functions that transfer tokens to arbitrary addresses or interact with other contracts in unintended ways. The lack of function selector validation means the contract trusts that the provided calldata will always call the swap function.

Risk

Likelihood:

  • An attacker who gains control of the owner account or a malicious owner could exploit this vulnerability.

Impact:

  • An attacker could drain all approved tokens from the contract by calling unintended functions on the 1inch router (e.g., transferFrom or similar logic exposed by the router).

  • Manipulation of the swap process to benefit the attacker at the expense of the protocol's positions.

Proof of Concept

// Attacker crafts malicious calldata that doesn't actually perform a swap
// but instead calls a different function that benefits them
bytes memory maliciousCalldata = abi.encodeWithSignature(
"unoswapTo(address,address,uint256,uint256,address)",
collateralToken,
attackerAddress, // recipient is attacker instead of contract
amount,
minReturn,
debtToken
);
// Call createLeveragedPosition with the malicious calldata
stratax.createLeveragedPosition(
flashLoanToken,
flashLoanAmount,
collateralAmount,
borrowToken,
borrowAmount,
maliciousCalldata, // malicious swap data
minReturnAmount
);

Recommended Mitigation

function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal
returns (uint256 returnAmount)
{
+ // Validate the function selector
+ bytes4 selector;
+ assembly {
+ selector := mload(add(_swapParams, 32))
+ }
+ // Only allow specific swap functions
+ require(
+ selector == IAggregationRouter.swap.selector ||
+ selector == bytes4(keccak256("unoswap(address,uint256,uint256,uint256[])")) ||
+ selector == bytes4(keccak256("unoswapTo(address,uint256,uint256,uint256[],address)")),
+ "Invalid swap function"
+ );
// 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));
}
// Sanity check
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
return returnAmount;
}

Missing Staleness Check in Oracle Price Feeds

Description

The StrataxOracle contract fetches prices from Chainlink oracles but does not validate the freshness of the price data. The latestRoundData() function returns an updatedAt timestamp that should be checked to ensure the price is not stale. During periods of high volatility or oracle issues, stale prices could lead to incorrect leverage calculations, potentially allowing positions to be created with excessive leverage or unwound at unfavorable rates.

Risk

Likelihood:

  • During periods of high market volatility or if the Chainlink oracle stops updating (e.g., due to network congestion or maintenance), the price feed may become stale.

Impact:

  • Creation of overleveraged positions that immediately become liquidatable.

  • Incorrect calculation of collateral requirements leading to bad debt.

  • Manipulation opportunities where attackers wait for stale prices to execute favorable trades.

  • Unwinding positions at incorrect exchange rates causing loss of funds.

Proof of Concept

// Scenario: Oracle hasn't updated for 3 hours during a market crash
// ETH price dropped from $2000 to $1500 but oracle still shows $2000
// Attacker creates a leveraged position using stale high price
// This allows them to borrow more than they should
TradeDetails memory details = TradeDetails({
collateralToken: ETH,
borrowToken: USDC,
desiredLeverage: 30000, // 3x leverage
collateralAmount: 10 ether,
collateralTokenPrice: 0, // Will fetch stale $2000 price
borrowTokenPrice: 0,
collateralTokenDec: 18,
borrowTokenDec: 6
});
// Calculate params using stale price
(uint256 flashLoan, uint256 borrowAmount) = stratax.calculateOpenParams(details);
// borrowAmount calculated as if ETH is $2000, but real value is $1500
// Position is immediately underwater by 25%

Recommended Mitigation

function getPrice(address _token) public view returns (uint256 price) {
address priceFeedAddress = priceFeeds[_token];
require(priceFeedAddress != address(0), "Price feed not set for token");
AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeedAddress);
- (, int256 answer,,,) = priceFeed.latestRoundData();
- require(answer > 0, "Invalid price from oracle");
+ (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) =
+ priceFeed.latestRoundData();
+ require(answer > 0, "Invalid price from oracle");
+ require(updatedAt != 0, "Round not complete");
+ require(answeredInRound >= roundId, "Stale price");
+ // Check if price is stale (more than 1 hour old)
+ uint256 stalePriceThreshold = 3600; // 1 hour
+ require(block.timestamp - updatedAt <= stalePriceThreshold, "Price data is stale");
price = uint256(answer);
}

Incorrect Collateral Withdrawal Calculation in Unwind Operation

Description

In the _executeUnwindOperation function, the collateral withdrawal calculation uses liquidationThreshold instead of LTV, and divides by it rather than using it to calculate the necessary collateral. This results in the calculation attempting to withdraw significantly less collateral than necessary to cover the repaid debt (or vice-versa depending on framing, but analysis shows incorrect formula). The formula divides by liqThreshold, which amplifies or reduces the withdrawal amount incorrectly relative to the actual debt value needed.

Risk

Likelihood:

  • This will occur every time a user attempts to unwind a position.

Impact:

  • The contract may attempt to withdraw an incorrect amount of collateral (likely less than needed due to division by a large integer treated as a small fraction if misinterpreted, or just logically wrong formula).

  • If insufficient collateral is withdrawn, the swap to repay the flash loan may fail due to insufficient funds, causing the transaction to revert ("Insufficient funds to repay flash loan").

  • Users may be unable to close positions or may have excess collateral locked.

Proof of Concept

// Example with real numbers:
// Debt to repay: 1000 USDC ($1000)
// Collateral: ETH at $2000
// LTV: 8000 (80%)
// Liquidation Threshold: 8500 (85%)
// Expected collateral needed: 1000 / 0.8 = $1250 worth = 0.625 ETH
// Current calculation:
// collateralToWithdraw = (1000 * 1e8 * 1e18 * 10000) / (2000 * 1e8 * 1e6 * 8500)
// = 1e33 / 1.7e21 = 0.588 ETH (LESS than needed!)
// This means:
// 1. Contract withdraws 0.588 ETH instead of 0.625 ETH
// 2. After swap, may not have enough USDC to repay flash loan + premium
// 3. Transaction reverts with "Insufficient funds to repay flash loan"

Recommended Mitigation

// 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,,,,,,,) =
+ (, uint256 ltv,,,,,,,,) =
aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
+ require(ltv > 0, "Asset not usable as collateral");
// 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);
+ // Calculate value of debt in collateral terms
+ uint256 debtValueInCollateral = (
+ _amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals())
+ ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()));
+ // Add 5% buffer for slippage and fees
+ uint256 collateralToWithdraw = (debtValueInCollateral * 10500) / 10000;
withdrawnAmount = aavePool.withdraw(unwindParams.collateralToken, collateralToWithdraw, address(this));
}

Flash Loan Callback Missing Reentrancy Protection

Description

The executeOperation function, which serves as the flash loan callback, lacks reentrancy protection. While it checks that msg.sender is the Aave pool and initiator is the contract itself, it does not use a nonReentrant modifier. During the swap execution in _call1InchSwap, the 1inch router (or a malicious implementation if swapped via a proxy) could call back into the contract, potentially manipulating state or draining funds. The contract performs external calls without following the checks-effects-interactions pattern.

Risk

Likelihood:

  • An attacker could exploit this if they can control the swap path or if the 1inch router interacts with a token that has transfer hooks (e.g., ERC777 or malicious tokens).

Impact:

  • Manipulation of contract state during flash loan execution.

  • Potential to drain funds by re-entering during the 1inch swap when tokens are approved but not yet swapped.

  • Bypassing health factor checks by manipulating position data mid-execution.

Proof of Concept

// Malicious 1inch router implementation
contract Malicious1inchRouter {
IStratax stratax;
function swap(bytes calldata data) external returns (uint256, uint256) {
// During the swap, tokens are approved to this contract
// Re-enter the Stratax contract
// Try to initiate another flash loan while one is in progress
stratax.createLeveragedPosition(
collateralToken,
flashAmount,
0, // no additional collateral
borrowToken,
borrowAmount,
maliciousSwapData,
0
);
// Or try to unwind a position during creation
stratax.unwindPosition(
collateralToken,
withdrawAmount,
debtToken,
debtAmount,
swapData,
minReturn
);
// Continue with swap to not revert
return (expectedAmount, spentAmount);
}
}

Recommended Mitigation

// Add OpenZeppelin's ReentrancyGuard
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- contract Stratax is Initializable {
+ contract Stratax is Initializable, ReentrancyGuard {
// Add nonReentrant modifier to all external functions
function executeOperation(
address _asset,
uint256 _amount,
uint256 _premium,
address _initiator,
bytes calldata _params
- ) external returns (bool) {
+ ) external nonReentrant returns (bool) {
require(msg.sender == address(aavePool), "Caller must be Aave Pool");
require(_initiator == address(this), "Initiator must be this contract");
// Rest of function...
}
function createLeveragedPosition(
// ... params ...
- ) public onlyOwner {
+ ) public onlyOwner nonReentrant {
// Function implementation...
}
function unwindPosition(
// ... params ...
- ) external onlyOwner {
+ ) external onlyOwner nonReentrant {
// Function implementation...
}
}

No Validation of Oracle Price Feed Addresses

Description

The StrataxOracle contract allows the owner to set any address as a price feed without validating that it is a legitimate Chainlink oracle contract. The only validation present is checking that decimals() returns 8, which is easily spoofable by any malicious contract.

Risk

Likelihood:

  • This relies on a malicious or compromised owner, or human error in setting the wrong address.

Impact:

  • A malicious owner could set fake price feeds that return manipulated prices to create bad debt or steal funds.

  • Accidentally setting the wrong oracle address could lead to incorrect position calculations or transaction failures.

  • Enables price manipulation attacks by controlling the price feed.

Proof of Concept

// Malicious price feed contract
contract FakePriceFeed {
function decimals() external pure returns (uint8) {
return 8; // Pass the validation
}
function latestRoundData() external view returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) {
// Return manipulated price
// Make ETH appear worth $10,000 instead of $2,000
return (1, 1000000000000, block.timestamp, block.timestamp, 1);
}
}
// Owner sets the fake price feed
strataxOracle.setPriceFeed(WETH, address(fakePriceFeed));
// Now all calculations using ETH price are manipulated

Recommended Mitigation

// Maintain a whitelist of valid Chainlink oracles
+ mapping(address => bool) public validPriceFeeds;
+ function addValidPriceFeed(address _priceFeed) external onlyOwner {
+ // Verify it's a Chainlink oracle by checking multiple functions
+ AggregatorV3Interface priceFeed = AggregatorV3Interface(_priceFeed);
+ // Check it implements required functions
+ require(priceFeed.decimals() == 8, "Must have 8 decimals");
+ // Verify it returns valid data
+ (uint80 roundId, int256 answer, , uint256 updatedAt, ) = priceFeed.latestRoundData();
+ require(roundId > 0, "Invalid round ID");
+ require(answer > 0, "Invalid price");
+ require(updatedAt > 0 && updatedAt <= block.timestamp, "Invalid timestamp");
+ // Check description is not empty (Chainlink oracles have descriptions)
+ string memory description = priceFeed.description();
+ require(bytes(description).length > 0, "Invalid description");
+ validPriceFeeds[_priceFeed] = true;
+ emit ValidPriceFeedAdded(_priceFeed);
+ }
function _setPriceFeed(address _token, address _priceFeed) internal {
require(_token != address(0), "Invalid token address");
require(_priceFeed != address(0), "Invalid price feed address");
+ require(validPriceFeeds[_priceFeed], "Price feed not whitelisted");
AggregatorV3Interface priceFeed = AggregatorV3Interface(_priceFeed);
require(priceFeed.decimals() == 8, "Price feed must have 8 decimals");
priceFeeds[_token] = _priceFeed;
}

Support

FAQs

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

Give us feedback!