Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Valid

Adjusted profit will not use the current state of the system hence a use can withdraw all margins to bypass the profit adjustment

Summary

The vault's current state is not updated before price adjustments hence the present state of the contract is not track and the first user will successfully bypass the price/margin adjustment.

Vulnerability Details

Lack of an update call to update the state of the marketed before statement adjusts the price

// if trader's old position had positive pnl then credit that to the trader
if (ctx.pnlUsdX18.gt(SD59x18_ZERO)) {
IMarketMakingEngine marketMakingEngine = IMarketMakingEngine(perpsEngineConfiguration.marketMakingEngine);
@audit>> ctx.marginToAddX18 =
@audit>> marketMakingEngine.getAdjustedProfitForMarketId(marketId, ctx.pnlUsdX18.intoUD60x18().intoUint256());
tradingAccount.deposit(perpsEngineConfiguration.usdToken, ctx.marginToAddX18);
// mint settlement tokens credited to trader; tokens are minted to
// address(this) since they have been credited to the trader's margin
marketMakingEngine.withdrawUsdTokenFromMarket(marketId, ctx.marginToAddX18.intoUint256());
}

This update cannot be done in price adjustment because it is a view function

/// @notice Returns the adjusted profit of an active position at the given market id, considering the market's ADL
/// state.
/// @dev If the market is in its default state, it will simply return the provided profit. Otherwise, it will
/// adjust based on the configured ADL parameters.
/// @dev Invariants:
/// The Market of `marketId` MUST exist.
/// The Market of `marketId` MUST be live.
/// @param marketId The engine's market id.
/// @param profitUsd The position's profit in USD.
/// @return adjustedProfitUsdX18 The adjusted profit in USD Token, according to the market's health.
function getAdjustedProfitForMarketId( //Bug called during settlement without updating
uint128 marketId,
uint256 profitUsd
)
public
view
returns (UD60x18 adjustedProfitUsdX18)
{
// load the market's data storage pointer & cache total debt
Market.Data storage market = Market.loadLive(marketId);
@audit >>. SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt(); // @audit note we obtained debt without updating and ensuring it is recent
// caches the market's delegated credit & credit capacity
@audit >>. UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd(); //@audit same with this
@audit>> SD59x18 creditCapacityUsdX18 = Market.getCreditCapacityUsd(delegatedCreditUsdX18, marketTotalDebtUsdX18);
// if the credit capacity is less than or equal to zero then
// the total debt has already taken all the delegated credit
@audit>> if (creditCapacityUsdX18.lte(SD59x18_ZERO)) {
revert Errors.InsufficientCreditCapacity(marketId, creditCapacityUsdX18.intoInt256());
}
// uint256 -> UD60x18; output default case when market not in Auto Deleverage state
adjustedProfitUsdX18 = ud60x18(profitUsd);
// we don't need to add `profitUsd` as it's assumed to be part of the total debt
// NOTE: If we don't return the adjusted profit in this if branch, we assume marketTotalDebtUsdX18 is positive
@audit>> bypassed>>> if (market.isAutoDeleverageTriggered(delegatedCreditUsdX18, marketTotalDebtUsdX18)) { // TRIGGER EVEN WHEN POSITION IS SAFE bug BUG
// if the market's auto deleverage system is triggered, it assumes marketTotalDebtUsdX18 > 0
adjustedProfitUsdX18 =
market.getAutoDeleverageFactor(delegatedCreditUsdX18, marketTotalDebtUsdX18).mul(adjustedProfitUsdX18);
}
}

Diving into the getcreditcapacityusd call

/// @notice Returns the credit capacity of the given market id.
@audit>>>. /// @dev `CreditDelegationBranch::updateMarketCreditDelegations` must be called before calling this function in
/// order to
@audit>>> /// retrieve the latest state. //BUG BUG
/// @dev Each engine can implement its own debt accounting schema according to its business logic, thus, this
/// function will simply return the credit capacity in USD for the given market id.
/// @dev Invariants:
/// The Market MUST exist.
/// @param marketId The engine's market id.
/// @return creditCapacityUsdX18 The current credit capacity of the given market id in USD.
audit>>> function getCreditCapacityForMarketId(uint128 marketId) public view returns (SD59x18) {
Market.Data storage market = Market.loadExisting(marketId);
@audit>>> return Market.getCreditCapacityUsd(market.getTotalDelegatedCreditUsd(), market.getTotalDebt());
}

Missing check

/// @notice Updates the credit delegations from ZLP Vaults to the given market id.
@audit>>> /// @dev Must be called whenever an engine needs to know the current credit capacity of a given market id.
function updateMarketCreditDelegations(uint128 marketId) public {
Vault.recalculateVaultsCreditCapacity(Market.loadLive(marketId).getConnectedVaultsIds());
}

Impact

Price adjustment can be bypassed and the system is placed in a more imbalanced state.

Tools Used

Manual review

Recommendations

call the update market credit delegation before adjusting all deposits.

// if trader's old position had positive pnl then credit that to the trader
if (ctx.pnlUsdX18.gt(SD59x18_ZERO)) {
IMarketMakingEngine marketMakingEngine = IMarketMakingEngine(perpsEngineConfiguration.marketMakingEngine);
++ marketMakingEngine.updateMarketCreditDelegations(marketId);
ctx.marginToAddX18 =
marketMakingEngine.getAdjustedProfitForMarketId(marketId, ctx.pnlUsdX18.intoUD60x18().intoUint256());
tradingAccount.deposit(perpsEngineConfiguration.usdToken, ctx.marginToAddX18);
// mint settlement tokens credited to trader; tokens are minted to
// address(this) since they have been credited to the trader's margin
marketMakingEngine.withdrawUsdTokenFromMarket(marketId, ctx.marginToAddX18.intoUint256());
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

bigsam Submitter
4 months ago
bigsam Submitter
4 months ago
bigsam Submitter
4 months ago
inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Should call `recalculateVaultsCreditCapacity` prior to previewing the `adjustedProfitUsdX18` in `_fillOrder`

Support

FAQs

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