Description
https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/branches/GlobalConfigurationBranch.sol#L475-L538
https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/MarketConfiguration.sol#L37-L49
Updating Market Configuration lacks a key sanity check that may lead to an unstable liquidation market wherein nearly all user accounts are liquidatable. Specifically, self.maintenanceMarginRateX18
could potentially be updated to a very high value, bricking tradingAccount::isLiquidatable
functionality.
Impact
While this described condition is rare and may only arise from some form of input error from the owner of GlobalConfigurationBranch, the potential impact is high. Considered the low probability of such conditions arising, It would be fair to categorize as a Medium.
Vulnerability Details
Below are all the sanity checks for update within GlobalConfigurationBranch::updatePerpMarketConfiguration
, where we only check for zero input for params.maintenanceMarginRateX18
.
if (abi.encodePacked(params.name).length == 0) {
revert Errors.ZeroInput("name");
}
if (abi.encodePacked(params.symbol).length == 0) {
revert Errors.ZeroInput("symbol");
}
if (params.priceAdapter == address(0)) {
revert Errors.ZeroInput("priceAdapter");
}
if (params.maintenanceMarginRateX18 == 0) {
revert Errors.ZeroInput("maintenanceMarginRateX18");
}
if (params.maxOpenInterest == 0) {
revert Errors.ZeroInput("maxOpenInterest");
}
if (params.maxSkew == 0) {
revert Errors.ZeroInput("maxSkew");
}
if (params.initialMarginRateX18 == 0) {
revert Errors.ZeroInput("initialMarginRateX18");
}
if (params.initialMarginRateX18 <= params.maintenanceMarginRateX18) {
revert Errors.InitialMarginRateLessOrEqualThanMaintenanceMarginRate();
}
if (params.skewScale == 0) {
revert Errors.ZeroInput("skewScale");
}
if (params.minTradeSizeX18 == 0) {
revert Errors.ZeroInput("minTradeSizeX18");
}
if (params.maxFundingVelocity == 0) {
revert Errors.ZeroInput("maxFundingVelocity");
}
if (params.priceFeedHeartbeatSeconds == 0) {
revert Errors.ZeroInput("priceFeedHeartbeatSeconds");
}
Note that we are not checking for extreme values of params.maintenanceMarginRateX18.
Within tradingAccount::isLiquidatable
the below is used to evaluate if an account is liquidatable.
return requiredMaintenanceMarginUsdX18.intoSD59x18().gt(marginBalanceUsdX18);
https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/TradingAccount.sol#L243-L256
requiredMaintenanceMarginUsdX18
is calculated by summing all of the positionalMaintenanceMarginUsdX18
within TradingAccount.sol::getAccountMarginRequirementUsdAndUnrealizedPnlUsd
.
function getAccountMarginRequirementUsdAndUnrealizedPnlUsd(
Data storage self,
uint128 targetMarketId,
SD59x18 sizeDeltaX18
)
internal
view
returns (
UD60x18 requiredInitialMarginUsdX18,
UD60x18 requiredMaintenanceMarginUsdX18,
SD59x18 accountTotalUnrealizedPnlUsdX18
)
{
if (targetMarketId != 0) {
...
(UD60x18 positionInitialMarginUsdX18, UD60x18 positionMaintenanceMarginUsdX18) = Position
.getMarginRequirement(
notionalValueX18,
ud60x18(perpMarket.configuration.initialMarginRateX18),
ud60x18(perpMarket.configuration.maintenanceMarginRateX18)
);
...
requiredMaintenanceMarginUsdX18 = requiredMaintenanceMarginUsdX18.add(positionMaintenanceMarginUsdX18);
}
}
Within Position.sol::getMarginRequirement
each positionMaintenanceMarginUsdX18
is calculated by multiplying the notionalValueX18
by maintenanceMarginRateX18
. It is returned as maintenanceMarginRateX18
in getMarginRequirement
, but is assigned to positionalMaintenanceMarginUsdX18
within tradingAccount.sol::getAccountMarginRequirementUsdAndUnrealizedPnlUsd
.
function getMarginRequirement(
UD60x18 notionalValueX18,
UD60x18 initialMarginRateX18,
UD60x18 maintenanceMarginRateX18
)
internal
pure
returns (UD60x18 initialMarginUsdX18, UD60x18 maintenanceMarginUsdX18)
{
initialMarginUsdX18 = notionalValueX18.mul(initialMarginRateX18);
maintenanceMarginUsdX18 = notionalValueX18.mul(maintenanceMarginRateX18);
}
Note that if perpMarket.configuration.maintenanceMarginRateX18
is initialized to a high value within MarketConfiguration.sol::update
, all positions would have a very high coefficient to their notionalValueX18
. This would lead totradingAccount::isLiquidatable
returning true for nearly all participating users due to a very high value of requiredMaintenanceMarginUsdX18
.
Tools Used
Manual Review
Mitigation
Implement a revert for when params.maintenanceMarginRateX18
is a nonesensically high number alongside the other revert sanity checks in updatePerpMarketConfiguration