Description
-
When consuming Chainlink feeds, clients should validate that the returned answer is recent and from a valid round. Typical checks include:
-
answer > 0
-
updatedAt is within an acceptable freshness window (e.g., not older than X seconds)
-
answeredInRound >= roundId (to avoid stale results carried over to a later round)
-
StrataxOracle.getPrice only checks that answer > 0 and does not validate updatedAt or the round linkage. Consequently, stale or frozen price data is accepted and used in leverage/open and unwind parameter calculations. This can lead to opening unhealthy positions or failing unwinds under real market conditions.
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");
price = uint256(answer);
}
Risk
Likelihood: High
-
Chainlink feeds can become temporarily stale during network issues or maintenance windows; when that happens, updatedAt will be old, yet the current code will still accept it.
-
Integration or deployment on alt‑L2s/sidechains with less reliable infrastructure increases the frequency of stale updates. In live operations, this will occur.
Impact: High
-
Bad positions / reverts later: Users can open with an outdated price, making calculated borrow amounts unrealistic; swaps may later fail to cover flash‑loan debts or bring the HF below 1, causing reverts or liquidation risk.
-
Inaccurate unwinds: Unwind math derived from stale data can under‑ or over‑withdraw collateral, again breaking the flash‑loan repayment or leaving capital stuck.
Proof of Concept
-
Copy tests test_OracleAllowsStalePricesInOpenParams() and test_OracleAllowsRoundMismatchAnsweredInRoundLTRoundId() to test/fork/Stratax.t.sol: inside the StrataxForkTest contract.
-
Copy mock contract MockStaleAggregator to test/fork/Stratax.t.sol: after the StrataxForkTest contract.
-
Run command forge test --mt test_OracleAllowsStalePricesInOpenParams -vvvv.
-
Run command forge test --mt test_OracleAllowsRoundMismatchAnsweredInRoundLTRoundId -vvvv.
function test_OracleAllowsStalePricesInOpenParams() public {
uint256 veryOld = 1;
MockStaleAggregator usdcAgg = new MockStaleAggregator(
int256(1e8), 10, 10, veryOld, veryOld
);
MockStaleAggregator wethAgg = new MockStaleAggregator(
int256(2000e8), 20, 20, veryOld, veryOld
);
strataxOracle.setPriceFeed(USDC, address(usdcAgg));
strataxOracle.setPriceFeed(WETH, address(wethAgg));
(uint256 flashLoanAmount, uint256 borrowAmount) = stratax.calculateOpenParams(
Stratax.TradeDetails({
collateralToken: USDC,
borrowToken: WETH,
desiredLeverage: 30_000,
collateralAmount: 1_000 * 1e6,
collateralTokenPrice: 0,
borrowTokenPrice: 0,
collateralTokenDec: 6,
borrowTokenDec: 18
})
);
assertTrue(flashLoanAmount > 0, "Expected flashLoanAmount > 0 with stale price");
assertTrue(borrowAmount > 0, "Expected borrowAmount > 0 with stale price");
}
function test_OracleAllowsRoundMismatchAnsweredInRoundLTRoundId() public {
uint256 nowTs = block.timestamp;
MockStaleAggregator badRoundUSDC = new MockStaleAggregator(
int256(1e8), 100, 99, nowTs - 1 days, nowTs - 1 days
);
MockStaleAggregator okWETH = new MockStaleAggregator(
int256(2000e8), 200, 200, nowTs - 1 days, nowTs - 1 days
);
strataxOracle.setPriceFeed(USDC, address(badRoundUSDC));
strataxOracle.setPriceFeed(WETH, address(okWETH));
(uint256 flashLoanAmount, uint256 borrowAmount) = stratax.calculateOpenParams(
Stratax.TradeDetails({
collateralToken: USDC,
borrowToken: WETH,
desiredLeverage: 30_000,
collateralAmount: 500 * 1e6,
collateralTokenPrice: 0,
borrowTokenPrice: 0,
collateralTokenDec: 6,
borrowTokenDec: 18
})
);
assertTrue(flashLoanAmount > 0, "Expected positive result despite round mismatch");
assertTrue(borrowAmount > 0, "Expected positive result despite round mismatch");
}
contract MockStaleAggregator {
uint8 private constant DEC = 8;
int256 public answer;
uint80 public roundId;
uint80 public answeredInRound;
uint256 public startedAt;
uint256 public updatedAt;
constructor(
int256 _answer,
uint80 _roundId,
uint80 _answeredInRound,
uint256 _startedAt,
uint256 _updatedAt
) {
answer = _answer;
roundId = _roundId;
answeredInRound = _answeredInRound;
startedAt = _startedAt;
updatedAt = _updatedAt;
}
function decimals() external pure returns (uint8) { return DEC; }
function latestRoundData()
external
view
returns (uint80, int256, uint256, uint256, uint80)
{
return (roundId, answer, startedAt, updatedAt, answeredInRound);
}
Output 1:
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/fork/Stratax.t.sol:StrataxForkTest
[PASS] test_OracleAllowsStalePricesInOpenParams() (gas: 671133)
Logs:
Available swap data files: 3
Randomly selected block: 24329390
Current fork block number is: 24329390
Traces:
[671133] StrataxForkTest::test_OracleAllowsStalePricesInOpenParams()
├─ [259243] → new MockStaleAggregator@0xc7183455a4C133Ae270771860664b6B7ec320bB1
│ └─ ← [Return] 846 bytes of code
├─ [259243] → new MockStaleAggregator@0xa0Cb889707d426A7A386870A03bc70d1b0697598
│ └─ ← [Return] 846 bytes of code
├─ [10538] StrataxOracle::setPriceFeed(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, MockStaleAggregator: [0xc7183455a4C133Ae270771860664b6B7ec320bB1])
│ ├─ [339] MockStaleAggregator::decimals() [staticcall]
│ │ └─ ← [Return] 8
│ ├─ emit PriceFeedUpdated(token: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, priceFeed: MockStaleAggregator: [0xc7183455a4C133Ae270771860664b6B7ec320bB1])
│ └─ ← [Stop]
├─ [8538] StrataxOracle::setPriceFeed(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, MockStaleAggregator: [0xa0Cb889707d426A7A386870A03bc70d1b0697598])
│ ├─ [339] MockStaleAggregator::decimals() [staticcall]
│ │ └─ ← [Return] 8
│ ├─ emit PriceFeedUpdated(token: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, priceFeed: MockStaleAggregator: [0xa0Cb889707d426A7A386870A03bc70d1b0697598])
│ └─ ← [Stop]
├─ [49919] BeaconProxy::fallback(TradeDetails({ collateralToken: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, borrowToken: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, desiredLeverage: 30000 [3e4], collateralAmount: 1000000000 [1e9], collateralTokenPrice: 0, borrowTokenPrice: 0, collateralTokenDec: 6, borrowTokenDec: 18 })) [staticcall]
│ ├─ [2515] UpgradeableBeacon::implementation() [staticcall]
│ │ └─ ← [Return] Stratax: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]
│ ├─ [41540] Stratax::calculateOpenParams(TradeDetails({ collateralToken: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, borrowToken: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, desiredLeverage: 30000 [3e4], collateralAmount: 1000000000 [1e9], collateralTokenPrice: 0, borrowTokenPrice: 0, collateralTokenDec: 6, borrowTokenDec: 18 })) [delegatecall]
│ │ ├─ [11703] 0x0a16f2FCC0D44FaE41cc54e079281D84A363bECD::getReserveConfigurationData(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
│ │ │ ├─ [7732] 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2::getConfiguration(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
│ │ │ │ ├─ [2680] 0x8147b99DF7672A21809c9093E6F6CE1a60F119Bd::getConfiguration(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [delegatecall]
│ │ │ │ │ └─ ← [Return] 0x100000000000000000000007d01bf08eb001a13b860003e8a50628d21e781d4c
│ │ │ │ └─ ← [Return] 0x100000000000000000000007d01bf08eb001a13b860003e8a50628d21e781d4c
│ │ │ └─ ← [Return] 6, 7500, 7800, 10450 [1.045e4], 1000, true, true, false, true, false
│ │ ├─ [3629] StrataxOracle::getPrice(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
│ │ │ ├─ [1478] MockStaleAggregator::latestRoundData() [staticcall]
│ │ │ │ └─ ← [Return] 10, 100000000 [1e8], 1, 1, 10
│ │ │ └─ ← [Return] 100000000 [1e8]
│ │ ├─ [3629] StrataxOracle::getPrice(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) [staticcall]
│ │ │ ├─ [1478] MockStaleAggregator::latestRoundData() [staticcall]
│ │ │ │ └─ ← [Return] 20, 200000000000 [2e11], 1, 1, 20
│ │ │ └─ ← [Return] 200000000000 [2e11]
│ │ └─ ← [Return] 2000000000 [2e9], 1068750000000000000 [1.068e18]
│ └─ ← [Return] 2000000000 [2e9], 1068750000000000000 [1.068e18]
├─ [0] VM::assertTrue(true, "Expected flashLoanAmount > 0 with stale price") [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertTrue(true, "Expected borrowAmount > 0 with stale price") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.46s (3.87ms CPU time)
Ran 1 test suite in 1.47s (1.46s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Output 2:
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/fork/Stratax.t.sol:StrataxForkTest
[PASS] test_OracleAllowsRoundMismatchAnsweredInRoundLTRoundId() (gas: 671874)
Logs:
Available swap data files: 3
Randomly selected block: 24329390
Current fork block number is: 24329390
Traces:
[671874] StrataxForkTest::test_OracleAllowsRoundMismatchAnsweredInRoundLTRoundId()
├─ [259243] → new MockStaleAggregator@0xc7183455a4C133Ae270771860664b6B7ec320bB1
│ └─ ← [Return] 846 bytes of code
├─ [259243] → new MockStaleAggregator@0xa0Cb889707d426A7A386870A03bc70d1b0697598
│ └─ ← [Return] 846 bytes of code
├─ [10538] StrataxOracle::setPriceFeed(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, MockStaleAggregator: [0xc7183455a4C133Ae270771860664b6B7ec320bB1])
│ ├─ [339] MockStaleAggregator::decimals() [staticcall]
│ │ └─ ← [Return] 8
│ ├─ emit PriceFeedUpdated(token: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, priceFeed: MockStaleAggregator: [0xc7183455a4C133Ae270771860664b6B7ec320bB1])
│ └─ ← [Stop]
├─ [8538] StrataxOracle::setPriceFeed(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, MockStaleAggregator: [0xa0Cb889707d426A7A386870A03bc70d1b0697598])
│ ├─ [339] MockStaleAggregator::decimals() [staticcall]
│ │ └─ ← [Return] 8
│ ├─ emit PriceFeedUpdated(token: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, priceFeed: MockStaleAggregator: [0xa0Cb889707d426A7A386870A03bc70d1b0697598])
│ └─ ← [Stop]
├─ [49919] BeaconProxy::fallback(TradeDetails({ collateralToken: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, borrowToken: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, desiredLeverage: 30000 [3e4], collateralAmount: 500000000 [5e8], collateralTokenPrice: 0, borrowTokenPrice: 0, collateralTokenDec: 6, borrowTokenDec: 18 })) [staticcall]
│ ├─ [2515] UpgradeableBeacon::implementation() [staticcall]
│ │ └─ ← [Return] Stratax: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]
│ ├─ [41540] Stratax::calculateOpenParams(TradeDetails({ collateralToken: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, borrowToken: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, desiredLeverage: 30000 [3e4], collateralAmount: 500000000 [5e8], collateralTokenPrice: 0, borrowTokenPrice: 0, collateralTokenDec: 6, borrowTokenDec: 18 })) [delegatecall]
│ │ ├─ [11703] 0x0a16f2FCC0D44FaE41cc54e079281D84A363bECD::getReserveConfigurationData(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
│ │ │ ├─ [7732] 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2::getConfiguration(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
│ │ │ │ ├─ [2680] 0x8147b99DF7672A21809c9093E6F6CE1a60F119Bd::getConfiguration(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [delegatecall]
│ │ │ │ │ └─ ← [Return] 0x100000000000000000000007d01bf08eb001a13b860003e8a50628d21e781d4c
│ │ │ │ └─ ← [Return] 0x100000000000000000000007d01bf08eb001a13b860003e8a50628d21e781d4c
│ │ │ └─ ← [Return] 6, 7500, 7800, 10450 [1.045e4], 1000, true, true, false, true, false
│ │ ├─ [3629] StrataxOracle::getPrice(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
│ │ │ ├─ [1478] MockStaleAggregator::latestRoundData() [staticcall]
│ │ │ │ └─ ← [Return] 100, 100000000 [1e8], 1769469323 [1.769e9], 1769469323 [1.769e9], 99
│ │ │ └─ ← [Return] 100000000 [1e8]
│ │ ├─ [3629] StrataxOracle::getPrice(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) [staticcall]
│ │ │ ├─ [1478] MockStaleAggregator::latestRoundData() [staticcall]
│ │ │ │ └─ ← [Return] 200, 200000000000 [2e11], 1769469323 [1.769e9], 1769469323 [1.769e9], 200
│ │ │ └─ ← [Return] 200000000000 [2e11]
│ │ └─ ← [Return] 1000000000 [1e9], 534375000000000000 [5.343e17]
│ └─ ← [Return] 1000000000 [1e9], 534375000000000000 [5.343e17]
├─ [0] VM::assertTrue(true, "Expected positive result despite round mismatch") [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertTrue(true, "Expected positive result despite round mismatch") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.06s (3.11ms CPU time)
Ran 1 test suite in 1.06s (1.06s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Recommended Mitigation
Introduce freshness and round integrity checks in StrataxOracle, and consider a configurable staleness threshold:
Reject prices where updatedAt == 0 or updatedAt < block.timestamp - maxStaleTime.
Require answeredInRound >= roundId.
Keep the existing answer > 0 check.
contract StrataxOracle {
address public owner;
+ // Global max staleness in seconds (e.g., 1 hour). Make configurable.
+ uint256 public maxStaleTime = 1 hours;
mapping(address => address) public priceFeeds;
+ event MaxStaleTimeUpdated(uint256 oldValue, uint256 newValue);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
+ function setMaxStaleTime(uint256 _seconds) external onlyOwner {
+ uint256 old = maxStaleTime;
+ maxStaleTime = _seconds;
+ emit MaxStaleTimeUpdated(old, _seconds);
+ }
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");
- price = uint256(answer);
+ (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
+ = priceFeed.latestRoundData();
+ require(answer > 0, "Invalid price from oracle");
+ require(updatedAt != 0 && startedAt != 0, "Oracle: incomplete round");
+ require(answeredInRound >= roundId, "Oracle: stale round");
+ require(updatedAt + maxStaleTime >= block.timestamp, "Oracle: price is stale");
+ price = uint256(answer); // 8 decimals as enforced on setPriceFeed
}
}