The function retrieves prices from Chainlink without checking if the data is stale. This means the protocol could be making critical leverage and liquidation decisions based on outdated price information, potentially causing users to get liquidated or lose funds.
Looking at the oracle implementation in StrataxOracle.sol, I noticed the getPrice() function only grabs the answer value from Chainlink and ignores everything else:
The problem is that latestRoundData() actually returns 5 values:
roundId - The round ID
answer - The actual price
startedAt - When the round started
updatedAt - When this price was last updated
answeredInRound - The round where this answer was computed
The code completely ignores updatedAt and answeredInRound, which are critical for detecting stale prices. According to Chainlink's own documentation, you should always check these values to ensure the price is recent enough for your application.
Likelihood:
Chainlink Price Feeds Regularly Experience Staleness Windows
Chainlink oracles update based on two triggers: deviation threshold (typically 0.5-1%) or heartbeat interval (time-based). For major pairs like ETH/USD and BTC/USD on Ethereum mainnet, the heartbeat is 3600 seconds (1 hour). During periods of low volatility - which happens regularly overnight, on weekends, or during ranging markets - the price deviation threshold isn't met. The oracle sits dormant until the full heartbeat interval passes, creating guaranteed staleness windows of 30-60 minutes multiple times per week. Altcoin feeds with 24-hour heartbeats create even longer staleness windows. Every time the protocol uses getPrice() during these periods, it retrieves outdated price data.
User Transactions Naturally Occur During Staleness Windows
Users don't check oracle update timestamps before interacting with the protocol - they simply call createLeveragedPosition() or unwindPosition() based on current market conditions they see on exchanges like Coinbase or Binance. When market prices move 3-5% (common daily volatility in crypto) while the Chainlink feed hasn't updated yet, users unknowingly create positions using prices that are materially different from reality. This happens organically without any malicious intent - a user sees ETH at $3,000 on Binance, calls the protocol expecting calculations based on $3,000, but gets calculations based on $2,700 because that's what Chainlink last reported 45 minutes ago. The timing collision between user transactions and oracle staleness happens routinely in normal operations.
Impact:
Impact 1: Users Get Liquidated Shortly After Opening Positions They Thought Were Safe
Here's what happens to an actual user: Alice checks Coinbase and sees ETH is trading at $3,000. She decides to open a 2x leveraged position with her 10 ETH, thinking she's being conservative - 2x leverage should give her plenty of room before liquidation. She calls createLeveragedPosition() and the transaction goes through successfully. Everything looks fine.
But here's the problem - the Chainlink oracle hasn't updated in 45 minutes and still shows ETH at $2,700. So when the protocol calculates how much Alice can borrow, it thinks her 10 ETH is only worth $27,000 instead of the actual $30,000. The math goes wrong from there: she gets approved for $20,520 in borrowing instead of the $22,800 she should get for proper 2x leverage.
Alice has no idea this happened. Her transaction succeeded, she sees her position is open, and everything appears normal. But her position isn't actually the safe 2x leverage she intended - the parameters are off.
Ten minutes later, the oracle finally updates to $3,000. Now her position shows different numbers than when she opened it. Then ETH drops 6% (totally normal daily movement) to $2,820. Alice isn't worried because she knows 2x leverage should handle a 6% drop easily. But because her position was created with wrong calculations, her health factor drops below 1.0. Liquidation bot immediately swoops in and liquidates her position. Alice just lost $2,500 to liquidators plus gas fees - roughly 5% of her entire position - on what she thought was a conservative trade. She didn't do anything wrong, she just got unlucky with oracle timing.
Impact 2: Users Can't Exit Their Positions When They Want To
Now imagine Bob has been running a profitable leveraged ETH position for a few weeks. ETH has gone up nicely and he decides it's time to take profits and close the position. He calls unwindPosition() to exit.
The transaction reverts. Bob tries again with different parameters. Reverts again. He's confused - his position is healthy, his health factor is 2.0, everything should work. What he doesn't realize is that the oracle price is stale again.
The protocol is trying to withdraw collateral based on outdated prices. It calculates that to repay his debt, it needs to withdraw more ETH than is safe to withdraw. Aave's safety checks prevent withdrawing that much because it would drop his health factor too low. So Bob is just... stuck. He can't exit his position even though he wants to.
This is incredibly frustrating from a user perspective. Bob sees ETH at the perfect price to take profits, but the protocol won't let him exit. He has a few bad options: wait hours for the oracle to update and hope the price hasn't moved against him, try to manually adjust his position (risky and expensive in gas), or just stay in the leveraged position and accept the ongoing risk. If ETH suddenly dumps while he's stuck, he could face liquidation on a position he was actively trying to close. That's a terrible user experience and a direct financial risk that shouldn't exist.
The same issue happens in reverse - sometimes the stale price makes it look like he can withdraw less collateral than he actually should be able to. So Bob closes his position but doesn't get back as much ETH as he deserves. He just unknowingly lost money to the oracle staleness issue. There's no error message, no warning - he just gets less value back and might not even notice until he checks his wallet balance closely.
This vulnerability occurs because StrataxOracle.getPrice() retrieves price data from Chainlink's latestRoundData() function but only validates that the price answer is greater than zero. It completely ignores the timestamp data that indicates when the price was last updated.
The exploit happens naturally without malicious intent - users simply interact with the protocol during periods when Chainlink's price hasn't updated recently. Since Chainlink feeds update based on either price deviation (0.5-1%) or time intervals (heartbeat), there are regular windows where prices can be minutes or even hours old during low volatility periods.
The root cause is that getPrice() doesn't validate price freshness. We need to add three critical checks that Chainlink's documentation explicitly recommends:
Staleness Check: Verify the price was updated recently by comparing updatedAt timestamp against the feed's known heartbeat interval
Round Completeness Check: Ensure answeredInRound >= roundId to confirm the price is from the current round, not carried over from a previous one
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.