Description
MarketConfiguration::priceFeedHeartbeatSeconds is a variable used in ChainlinkUtil::getPrice to ensure the price is sufficiently recent for the protocol to use the returned price. If this variable is too low or the Oracle has an outdated price, the function reverts:
function getPrice(
IAggregatorV3 priceFeed,
@> uint32 priceFeedHeartbeatSeconds,
IAggregatorV3 sequencerUptimeFeed
)
internal
view
returns (UD60x18 price)
{
...
try priceFeed.latestRoundData() returns (uint80, int256 answer, uint256, uint256 updatedAt, uint80) {
@> if (block.timestamp - updatedAt > priceFeedHeartbeatSeconds) {
@> revert Errors.OraclePriceFeedHeartbeat(address(priceFeed));
}
...
} catch {
revert Errors.InvalidOracleReturn();
}
}
The issue is that this variable is not set due to an omission in MarketConfiguration::update and will therefore always be 0, causing ChainlinkUtil::getPrice to always revert:
struct Data {
string name;
string symbol;
address priceAdapter;
uint128 initialMarginRateX18;
uint128 maintenanceMarginRateX18;
uint128 maxOpenInterest;
uint128 maxSkew;
uint128 maxFundingVelocity;
uint128 minTradeSizeX18;
uint256 skewScale;
OrderFees.Data orderFees;
@> uint32 priceFeedHeartbeatSeconds;
}
function update(Data storage self, Data memory params) internal {
self.name = params.name;
self.symbol = params.symbol;
self.priceAdapter = params.priceAdapter;
self.initialMarginRateX18 = params.initialMarginRateX18;
self.maintenanceMarginRateX18 = params.maintenanceMarginRateX18;
self.maxOpenInterest = params.maxOpenInterest;
self.maxSkew = params.maxSkew;
self.maxFundingVelocity = params.maxFundingVelocity;
self.minTradeSizeX18 = params.minTradeSizeX18;
self.skewScale = params.skewScale;
self.orderFees = params.orderFees;
@>
}
No other place in the codebase allows setting it and even if the update sets the params like in GlobalConfigurationBranch::updatePerpMarketConfiguration, it won't work:
function updatePerpMarketConfiguration(
uint128 marketId,
UpdatePerpMarketConfigurationParams calldata params
)
external
onlyOwner
onlyWhenPerpMarketIsInitialized(marketId)
{
...
perpMarketConfiguration.update(
MarketConfiguration.Data({
...
@> priceFeedHeartbeatSeconds: params.priceFeedHeartbeatSeconds
})
);
emit LogUpdatePerpMarketConfiguration(msg.sender, marketId);
}
This variable and ChainlinkUtil::getPrice is used in PerpMarket::getIndexPrice, which is used in many other functions in the protocol.
Apart from a lot of getters, the most important/impacted ones will be:
OrderBranch::simulateTrade called when a market order is created, causing a DoS of OrderBranch::createMarketOrder.
LiquidationBranch::liquidateAccounts preventing liquidation.
TradingAccount::getAccountMarginRequirementUsdAndUnrealizedPnlUsd and TradingAccount::getAccountUnrealizedPnlUsd which cause a DoS of SettlementBranch::_fillOrder and TradingAccountBranch::withdrawMargin, leading to funds being stuck in the contract.
Risk
Likelyhood: High
Impact: High
DoS of a large number of functionalities.
Withdrawal of margin is impossible.
Creation of market orders and their liquidation is impossible.
Recommended Mitigation
function update(Data storage self, Data memory params) internal {
self.name = params.name;
self.symbol = params.symbol;
self.priceAdapter = params.priceAdapter;
self.initialMarginRateX18 = params.initialMarginRateX18;
self.maintenanceMarginRateX18 = params.maintenanceMarginRateX18;
self.maxOpenInterest = params.maxOpenInterest;
self.maxSkew = params.maxSkew;
self.maxFundingVelocity = params.maxFundingVelocity;
self.minTradeSizeX18 = params.minTradeSizeX18;
self.skewScale = params.skewScale;
self.orderFees = params.orderFees;
+ self.priceFeedHeartbeatSeconds = params.priceFeedHeartbeatSeconds;
}