DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: medium
Valid

An Uninitialized Variable In The `MarketConfiguration::update` Function Causes The `PrepMarket::getIndexPrice` Function To Revert

Summary:

The update function within the MarketConfiguration library fails to update the priceFeedHeartbeatSeconds variable, which is essential for the getIndexPrice function to operate correctly. This oversight causes the getIndexPrice function to always revert due to an uninitialized heartbeat check.

Vulnerability Detail:

In the MarketConfiguration library, the update function is designed to update various market configuration parameters. However, it neglects to update the priceFeedHeartbeatSeconds variable. Consequently, this variable remains uninitialized, defaulting to zero.

The getIndexPrice function relies on the priceFeedHeartbeatSeconds variable to validate the timeliness of the price feed data from Chainlink oracles. When it checks if (block.timestamp - updatedAt > priceFeedHeartbeatSeconds), the comparison always results in true (since priceFeedHeartbeatSeconds is zero), causing the function to revert every time it is called.

Impact:

This vulnerability significantly impacts the functionality of the protocol by making the price checking mechanism always revert. As a result, it effectively halts the correct operation of any process relying on the getIndexPrice function. Specifically, it renders the entire price validation mechanism inoperative, potentially disrupting market operations.

These are the list of functions that got affected by this vulnerability includes:

  1. PerpMarket::getIndexPrice

  2. TradingAccountBranch::getAccountEquityUsd

  3. TradingAccountBranch::getAccountMarginBreakdown

  4. TradingAccountBranch::getAccountTotalUnrealizedPnl

  5. TradingAccountBranch::getAccountLeverage

  6. TradingAccountBranch::withdrawMargin

  7. SettlementBranch::fillMarketOrder

  8. SettlementBranch::fillOffchainOrders

  9. OrderBranch::simulateTrade

  10. OrderBranch::getMarginRequirementForTrade

  11. LiquidationBranch::liquidateAccount

  12. LiquidationBranch::checkLiquidatableAccounts

Code Snippet:

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/MarketConfiguration.sol#L37-L47

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;
}
/// @notice Updates the given market configuration.
/// @dev See {MarketConfiguration.Data} for parameter details.
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;
@> // Missing update for priceFeedHeartbeatSeconds
}

Proof Of Concept:

  1. While creating a new market configuration, the priceFeedHeartbeatSeconds param is given a non-zero value.

  2. The update function is called to update the market configuration.

  3. The update function never initializes the priceFeedHeartbeatSeconds variable, causing it to default to zero.

  4. When the getIndexPrice function is called, that invokes the Chainlink oracle to fetch the price data.

  5. The function getPrice reverts due to the uninitialized priceFeedHeartbeatSeconds variable.

Here is the commands to test:

# Add the following to the foundry.toml with Alchemy API key
[rpc_endpoints]
arbitrum_sepolia = "https://arb-sepolia.g.alchemy.com/v2/${API_KEY_ALCHEMY}

To test the POC, run the following Forge test:

forge test --mt test_FortisAudits_IndexPriceGetsDOS -vvvv

Proof Of Code:

Add the following code to the Base.t.sol file:

// Creating the prepmarket suitable for the fork test
function createPerpMarketsFork() internal {
createPerpMarkets(
users.owner.account,
perpsEngine,
INITIAL_MARKET_ID,
FINAL_MARKET_ID,
IVerifierProxy(mockChainlinkVerifier),
false
);
for (uint256 i = INITIAL_MARKET_ID; i <= FINAL_MARKET_ID; i++) {
vm.label({ account: marketOrderKeepers[i], newLabel: "Market Order Keeper Fork" });
}
}

POC:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { Base_Test } from "./Base.t.sol";
import { UD60x18, ud60x18 } from "@prb-math/UD60x18.sol";
import { SD59x18 } from "@prb-math/SD59x18.sol";
import { Errors } from "@zaros/utils/Errors.sol";
// Forked arb-sepolia test
contract BugTest is Base_Test {
function setUp() public override {
uint256 forkId = vm.createFork("https://sepolia-rollup.arbitrum.io/rpc");
vm.selectFork(forkId);
Base_Test.setUp();
changePrank({ msgSender: users.owner.account });
configureSystemParameters();
createPerpMarketsFork();
}
function test_FortisAudits_IndexPriceGetsDOS() public {
address ETH_USD = address(0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165);
deal({ token: address(usdc), to: users.naruto.account, give: 1000e6 });
changePrank({ msgSender: users.naruto.account });
createAccountAndDeposit(1000e6, address(usdc));
vm.expectRevert(abi.encodeWithSelector(Errors.OraclePriceFeedHeartbeat.selector, ETH_USD));
// Calling the index price for the ETH_USD marketid
perpsEngine.exposed_getIndexPrice(2);
}
}

Forge Test stack trace:

├─ [36319] Perps Engine::exposed_getIndexPrice(2) [staticcall]
│ ├─ [31069] PerpMarketHarness::exposed_getIndexPrice(2) [delegatecall]
│ │ ├─ [27534] PerpMarket::a75e8fff(f2a828f98ebacfcc9ead142282f1b781df57bae404a8d1fa83ab68dfbae58f25) [delegatecall]
│ │ │ ├─ [5595] 0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165::decimals() [staticcall]
│ │ │ │ ├─ [253] 0xf3138B59cAcbA1a4d7d24fA7b184c20B3941433e::decimals() [staticcall]
│ │ │ │ │ └─ ← [Return] 8
│ │ │ │ └─ ← [Return] 8
│ │ │ ├─ [11235] 0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165::latestRoundData() [staticcall]
│ │ │ │ ├─ [7502] 0xf3138B59cAcbA1a4d7d24fA7b184c20B3941433e::latestRoundData() [staticcall]
│ │ │ │ │ └─ ← [Return] 330001 [3.3e5], 314332890000 [3.143e11], 1721921867 [1.721e9], 1721921867 [1.721e9], 330001 [3.3e5]
│ │ │ │ └─ ← [Return] 18446744073709881617 [1.844e19], 314332890000 [3.143e11], 1721921867 [1.721e9], 1721921867 [1.721e9], 18446744073709881617 [1.844e19]
│ │ │ └─ ← [Revert] OraclePriceFeedHeartbeat(0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165)
│ │ └─ ← [Revert] OraclePriceFeedHeartbeat(0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165)
│ └─ ← [Revert] OraclePriceFeedHeartbeat(0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165)
└─ ← [Stop]

Recommendations

To resolve this issue, ensure that the priceFeedHeartbeatSeconds variable is appropriately updated within the update function. Here is the recommended mitigation:

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;
}
/// @notice Updates the given market configuration.
/// @dev See {MarketConfiguration.Data} for parameter details.
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;
}

By including this update, the priceFeedHeartbeatSeconds variable will hold the correct value, allowing the getIndexPrice function to perform as intended without unnecessary reverts.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`MarketConfiguration::update` function lacks `priceFeedHeartbeatSeconds` argument

Appeal created

bluedragon Submitter
9 months ago
inallhonesty Lead Judge
9 months ago
bluedragon Submitter
9 months ago
inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`MarketConfiguration::update` function lacks `priceFeedHeartbeatSeconds` argument

Support

FAQs

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