Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · October 2024

Perps V3

for Synthetix

Synthetix engaged Guardian to review the security of its synthetix. From the 2nd of July to the 29th of July, a team of 6 auditors reviewed the source code in scope.

Published
Review window
July 2 to 29, 2024
Language
Solidity
Chains
Ethereum, Base, Optimism
Sector
Perpetuals
  • 6 Critical
  • 7 High
  • 22 Medium
  • 22 Low
  • 0 Informational

23 resolved · 32 acknowledged · 2 declined

Scope

Overview

Synthetix engaged Guardian to review the security of its synthetix. From the 2nd of July to the 29th of July, a team of 6 auditors reviewed the source code in scope.

Issues Detected Throughout the engagement 13 High/Critical issues were uncovered and promptly remediated by the Synthetix team. Several issues impacted the fundamental behavior of the protocol, following their remediation Guardian believes the protocol to uphold the functionality described for the lending protocol product.

Findings 57

  1. C-01 Critical All Debt Is Forgiven Logical Error Resolved
    Location
    PerpsAccount.sol: 266

    Description

    Proof of concept: PoC

    If payDebt is called with an amount that is larger than the existing debt, the function wipes all existing debt and then attempts to add the remaining amount as collateral to the account.

    if (self.debt < amount) {self.debt = 0; updateCollateralAmount(self, SNX_USD_MARKET_ID, (amount - self.debt).toInt());}

    The problem lies with setting self.debt to 0 before performing the collateral update. This results in the full amount being updated as collateral, and the previous debt was essentially repaid at no cost.

    By just repaying 1 additional wei of debt, a user will have his entire debt repaid and his collateral increased by the debt amount + 1 wei.

    Recommendation

    Update collateral amount first before setting debt to 0.

    Resolution

    Synthetix Team: The issue was resolved in PR#2202.

  2. C-02 Critical Global Debt Is Not Changed On Debt Payment Logical Error Resolved
    Location
    PerpsAccount.sol: 252-270

    Description

    Proof of concept: PoC

    PerpsAccount.payDebt() updates the debt value of individual users directly instead of calling updateAccountDebt.

    The user's debt will be updated, but this change will not affect the global market debt and as a result reportedDebt will report higher debt.

    Recommendation

    Don't update the debt directly and instead call updateAccountDebt.

    Resolution

    Synthetix Team: The issue was resolved in PR#2202.

  3. C-03 Critical Utilization Rate Manipulated Through Deposits Logical Error Resolved
    Location
    GlobalPerpsMarket.sol: 85

    Description

    Proof of concept: PoC

    Utilization rate is calculated in GlobalPerpsMarket.sol by lockedCredit.divDecimal(delegatedCollateralValue).

    The issue lies with lockedCredit which is taken from minimumCredit which includes deposited sUSD collateral. Intuitively, this deposited collateral should be excluded as it is not part of the market's positions.

    By including it, an attacker can deposit collateral into the market to increase the utilization rate and result in higher interest charged to all traders.

    Recommendation

    Exclude deposited sUSD from minimum credit when calculating the utilization rate.

    Resolution

    Synthetix Team: The issue was resolved in PR#2198.

  4. C-04 Critical Existing Debt Can Be Erased When Settling Order Logical Error Resolved
    Location
    PerpsAccount.sol: 146

    Description

    Proof of concept: PoC

    The function charge is called when orders are settled to perform collateral and debt accounting.

    The issue lies with the scenario where: 1) trader makes a loss, 2) leftover sUSD credit > 0 and 3) existing debt > 0. In that scenario, due to not accounting for existing debt, all debt is reset to 0.

    Consider this scenario:

    • Trader A deposits ETH collateral, makes a losing trade and incurs debt
    • Trader A deposits sUSD collateral, makes another losing trade and has previous debt erased

    Recommendation

    In PerpsAccount.charge, account for existing debt as shown below:

    if (leftoverCredit > 0) { updateCollateralAmount(self, SNX_USD_MARKET_ID, amount); newDebt = self.debt; // insert this }

    Also, consider reducing any existing debt when traders deposit sUSD collateral.

    Resolution

    Synthetix Team: The issue was resolved in PR#2202.

  5. C-05 Critical Wrong Collateral Discount Logical Error Resolved
    Location
    PerpsCollateralConfiguration

    Description

    Proof of concept: PoC

    The discounted collateral is computed in valueInUsd. A discount is a value between lowerLimitDiscount and upperLimitDiscount that depends on the impact on the spot market.

    The calculation is wrong as it has reversed the places of the min and max functions, which will result in wrong calculation. The discount will always be computed using the upperLimitDiscount, no matter the impact on the market.

    In result, the users' total collateral value will be less than it has to be, which can lead to earlier liquidations.

    Recommendation

    Switch the ordering of the min and max functions.

    Resolution

    Synthetix Team: The issue was resolved in PR#2211.

  6. C-06 Critical RewardDistributor Incompatible With Collaterals Logical Error Resolved
    Location
    RewardDistributor

    Description

    RewardsDistributor.distributeRewards() supports only one collateral type. However, the Perps system loops over multiple collaterals and calls distributeCollateral for each one of them.

    This operation will fail and will cause the whole transaction to revert which will DOS the perps market liquidation functionality.

    Recommendation

    Remove the checks from the RewardDistributor contract

    Resolution

    Synthetix Team: The issue was resolved in PR#2165.

  7. H-01 High validateRequest Errant Price Impact Logical Error Resolved
    Location
    AsyncOrder.sol: 316

    Description

    Proof of concept: PoC

    In the validateRequest function the calculateStartingPnl is computed as if the fill price affected the entire position, when in fact the fillPrice only causes a net change in the position’s margin for the newly added size from the current order.

    For example:

    • Current WETH price is $5,000
    • Bob has a long position with size 10 WETH at an entry price of $5,000
    • Bob opens an increase long for a size of 1 WETH and is negatively impacted to receive $5,100 as a

    fill price

    • The computed starting pnl is 11 * -100 = -$1,100
    • However Bob only received a negative impact of $100 on his order

    As a result orders which build on top of existing positions will have their negative impact errantly accounted for the existing position size in the currentAvailableMargin validation, preventing valid orders from being executed.

    Additionally, orders which flip the side of the position will not be validated for the entire negative price impact that they experience, as only the net position on the opposite side remains.

    Therefore positions can be opened where they are in fact below the necessary margin since the entire negative price impact has not been accounted for.

    Recommendation

    Compute calculateStartingPnl based upon the size delta of the order which is currently being executed instead of the entire size of the new position.

    Resolution

    Synthetix Team: The issue was resolved in PR#2198.

  8. H-02 High Lacking minimumCredit Reserves Validation Logical Error Resolved
    Location
    AsyncOrder.sol

    Description

    Traders are allowed to open and settle orders even when the existing market positions cannot be adequately supported by the liquidity backing the Perps market.

    Even if the existing minimumCredit is too large for the backing creditCapacity, traders can continue to increase the market size and therefore increase the uncovered gap between the minimumCredit and the lacking creditCapacity.

    As a result the Perps market can easily become insolvent in the event that traders continue to open positions without consideration for the backing liquidity.

    This leads to a market state where sUSD collateral withdrawals are DoS'd as well as any settlement for positions in a profit.

    Recommendation

    Validate that orders that would create new positions or increase existing positions do not invalidate the minimumCredit validation for the market.

    Resolution

    Synthetix Team: The issue was resolved in PR#2198.

  9. H-03 High Bad Debt Incurred During Liquidate Margin Logical Error Resolved
    Location
    LiquidationModule.sol: 88

    Description

    Proof of concept: PoC

    Accounts are determined to be eligible for liquidateMarginOnly when their availableMargin < 0, which implies that the cost of their debt has exceed the value of their collateral.

    The collateral discount acts as a buffer which would prevent bad debt from immediate price changes.

    However, there is no requirement that margin is able to cover liquidation fees. These fees will always result in bad debt which will build up in the system over time.

    Recommendation

    When checking eligibility for margin liquidation in isEligibleForMarginLiquidation, subtract liquidation fees from availableMargin.

    Resolution

    Synthetix Team: The issue was resolved in PR#2211.

  10. H-04 High Risk-Free Trades With payDebt Gaming Resolved
    Location
    PerpsAccountModule.sol: 98-105

    Description

    1. When a trader's collateral and positions are slightly above the threshold to allow creating the

    wished order, the order can be created, but trying to settle it a few blocks later will revert because interest is accrued every second.

    1. Users are not allowed to deposit funds while they have a pending order waiting to be settled,

    because of the checkPendingOrder check. This can be bypassed by calling the payDebt function instead.

    1. Orders can be executed after commit time + delay and till the expiry of the order. The price at

    commit time + delay is used even if the order is executed later than that (somewhere between this timestamp and the expiry timestamp)

    A malicious actor can abuse these conditions to create a risk-free trade:

    • The attacker owns an account that has a big amount of collateral, some debt, and a small position

    that accrues interest every second

    • Attacker creates an order that requires exactly the available margin to be created
    • A small amount of interest accumulates till the order can be settled
    • Keepers are not able to settle the order because the attacker does not have enough available

    margin now

    • The attacker waits if the price changes so that the order would be instant profit and if so increases

    the available margin by calling payDebt

    Recommendation

    Add the AsyncOrder.checkPendingOrder(account.id); check to the payDebt function.

    Resolution

    Synthetix Team: The issue was resolved in PR#2202.

  11. H-05 High LPs Can Game Liquidations Via mintUsd Logical Error Acknowledged
    Location
    Global

    Description

    Proof of concept: PoC

    During liquidation, an account is flagged, and its positions are closed. However, if the liquidation window is small, some positions may remain open to be subsequently liquidated.

    The issue lies with these 'ghost' open positions, which may incur PnL before they are closed. If the positions incur a loss, they temporarily reduce reported debt, and if they incur a profit, the reported debt is temporarily increased.

    Additionally, if a position is not liquidated at all due to the capacity window validation, the position’s pending PnL at the time of liquidation is not erased to offset the seizure of collateral.

    SIP-366, Asynchronous Delegation, aims to address a scenario where LPs can target this intermediate liquidation mis-accounting to undelegate while their position's debt is deflated. LPs can specifically target this intermediate liquidation mis-accounting.

    This addresses the vector where LPs would undelegate from the pool. However, LPs may still mint sUSD while they have artificially lowered debt. In this way, an LP could mint sUSD up to the c-ratio while their debt is suppressed.

    Once the Perps-V3 account is liquidated, the LP's position would be immediately under the c-ratio by a stepwise and potentially significant amount.

    In some cases, LPs may be able to mint more sUSD than they would lose from liquidation. As a result, the LP forces others in the vault to take on socialized debt and can even potentially make keeper fee profits from triggering the Perps-V3 liquidation and their own V3 liquidation in the same block.

    Recommendation

    Consider re-working the partial liquidation process, to avoid having these 'ghost' positions incur PnL before they are closed. These positions are technically liquidated and should not remain open.

    Alternatively, ensure that c-ratios in the V3 core system are assigned such that it would not be possible for an LP to gain a net profit from this intermediate state.

    Resolution

    Synthetix Team: Acknowledged.

  12. H-06 High Liquidations Errantly Adjust DebtCorrection Logical Error Declined
    Location
    LiquidationModule.sol

    Description

    When there is not enough liquidation capacity left, position liquidations will be done in multiple transactions while the account liquidation will occur in the first transaction.

    In every subsequent liquidation for the position, the debtCorrectionAccumulator will be updated with the latest funding accrued for the fundingDelta and fundingPnl, however as the position does not realize the funding changes from the period [flagPosition, liquidatePosition], this debtCorrectionAccumulator adjustment is invalid.

    As a result the reportedDebt of the market is perturbed.

    Recommendation

    In the case of a liquidation of a position which was previously flagged, do not use the latest funding changes that took place after the position’s flagging to adjust the fundingPnl value as this amount will not be realized to the account’s margin.

    Resolution

    Synthetix Team: When an account gets partially liquidated, LP’s take on the position and funding repercussions of that position (as well as the margin). So if an account has 1 ETH long and gets partially liquidated to 0.9 ETH long remaining. It’s not like at that point of time that funding gets suspended on that 0.9 ETH, but it simply shift from the trader to LP’s who take on that position.

  13. H-07 High Minimum Delegate Time Not Set Logical Error Resolved
    Location
    Global

    Description

    The core system has setMarketMinDelegateTime() function which has to be called by integrating markets to set their minimum waiting time for the LPs to withdraw their collateral after they have delegated it.

    The PerpsMarket doesn't call this function which lets LPs delegate and undelegate collateral in the same block.

    This can lead to some unexpected behaviors such as interest rate manipulation or risk-free yield opportunity by sandwiching a settlement to gain from fees by depositing a large amount of collateral and the withdrawing it.

    Recommendation

    Set some minimum delegate time.

    Resolution

    Synthetix Team: The issue was resolved in PR#2276.

  14. M-01 Medium MintUSD() Does Not Verify Market Capacities Gaming Acknowledged
    Location
    IssueUSDModule.sol: 52

    Description

    The mintUsd() function decreases the creditCapacity of the markets connected to the pool. However, it does not verify if there is enough creditCapacity available to support the markets. As a result, by using mintUsd, LPs can potentially push the markets below their minimumCredit.

    Recommendation

    Similar to the practice in delegateCollateral(), the addition of the _verifyNotCapacityLocked() check to mintUsd() is suggested. This measure will prevent the minting of USD if there will be locked markets as a consequence of minting.

    Resolution

    Synthetix Team: If credit capacity is restricted by minting, then traders will get an incentive to unwind their positions via the assymetric funding rate is mainly the reason.

  15. M-02 Medium Price Impact Sandwich Attack Logical Error Acknowledged
    Location
    AsyncOrder.sol: 295-300

    Description

    Users who balance the skew can be sandwiched by an attacker who front runs and back runs the call:

    • The skew is at 1000
    • The user tries to balance the skew by shorting -1000
    • Attacker front runs the call and shorts -1000 (receives a positive price impact)
    • User call goes through and the user imbalances the skew by -1000 (receives a negative price

    impact)

    • Attacker back runs the call and longs 1000 (receives a positive price impact)

    Recommendation

    Inform users about the risk of this attack so the slippage checks are set accordingly.

    Resolution

    Synthetix Team: Inform users that they can and should in many cases assign the priceLimit ahead of the market price when they would experience positive impact, thus not having that value extracted from them.

  16. M-03 Medium newRequiredMargin Uses fillPrice Logical Error Resolved
    Location
    AsyncOrder.sol: 547

    Description

    In getRequiredMarginWithNewPosition, newRequiredMargin is calculated with fillPrice. This is inaccurate as fillPrice includes a premium/discount, and therefore should only be used for PnL calculations.

    Otherwise, a new position might be within the required margin (based on fillPrice) but immediately after settling it could be eligible for liquidation (which calculates margin based on oracle price).

    This is also inconsistent with how oldRequiredMargin is calculated in the next step, which uses currentPrice.

    Recommendation

    Use currentPrice to calculate newRequiredMargin.

    Resolution

    Synthetix Team: The issue was resolved in PR#2310.

  17. M-04 Medium Margin Liquidations Should Remove Pending Order Logical Error Resolved
    Location
    LiquidationModule.sol

    Description

    During liquidateMarginOnly, the liquidated account may have a pending order which is not removed. The order cannot be settled and if market price were to go outside of the priceLimit range, the order can now be cancelled.

    However, as the account has been liquidated, the cancel order settlement reward is charged as debt to the account but may never be repaid. This debt will exist in the system as permanent bad debt.

    Recommendation

    Remove any pending order during margin liquidations.

    Resolution

    Synthetix Team: The issue was resolved in PR#2304.

  18. M-05 Medium Order Fee Calculation Uses The Wrong Price Logical Error Acknowledged
    Location
    AsyncOrder.sol: 409

    Description

    • The fillPrice is used in the calculateOrderFee function to calculate the notional.
    • As the orderFee is a percentage taken from the notional a higher fillPrice will lead to a higher

    orderFee and a lower fillPrice will lead to a lower orderFee.

    This impacts long trades correctly, but short trades wrong:

    • Long Trade:
    • With a positive price impact the fillPrice decreases
    • With a negative price impact the fillPrice increases
    • Short Trade:
    • With a positive price impact the fillPrice increases
    • With a negative price impact the fillPrice decreases

    Therefore a positive price impact on a short (trader balances the OI) increases the fillPrice and therefore also the orderFee of the trader and vice versa.

    Recommendation

    Use the orderPrice instead of the fillPrice.

    Resolution

    Synthetix Team: Given that limited repercussions of this imperfection in order fee, we will opt to acknowledge finding and accept it as is.

  19. M-06 Medium Allow Liquidations While Market Is Disabled Logical Error Acknowledged
    Location
    Global

    Description

    All state-modifying functions validate that the feature flag PERPS_SYSTEM is enabled before allowing transactions to proceed. This includes liquidations, which implies that if the feature flag is disabled, liquidations cannot be processed.

    This is undesirable because open positions that become liquidatable while the feature flag is disabled can lead to significant bad debt in the system.

    Additionally, traders should be allowed to close their open positions even while the market is disabled to avoid liquidations.

    Recommendation

    Consider allowing liquidations and the closing of positions while the market is disabled. This could be achieved with more finely-tuned feature flags that apply to different functions, similar to how the BFP market operates.

    Resolution

    Synthetix Team: Acknowledged.

  20. M-07 Medium Liquidation Rewards Should Not Use Fill Price Logical Error Acknowledged
    Location
    AsyncOrder.sol: 566

    Description

    getRequiredMarginWithNewPosition calculates the required margin for a new position by adding possible liquidation rewards to the required margin for the position.

    The issue lies with how the liquidation rewards are calculated using fillPrice instead of an oracle price:

    runtime.accumulatedLiquidationRewards = marketConfig.calculateFlagReward( MathUtil.abs(newPositionSize).mulDecimal(fillPrice));

    1. This in contradiction with how isEligibleForLiquidation is calculated which uses an oracle price.
    2. fillPrice includes a premium/discount according to how the trade affects the market skew. This

    would unfairly make shorts require a higher margin when they balance the market and less when they imbalance the market.

    Recommendation

    Use oracle price to calculate the liquidation rewards.

    Resolution

    Synthetix Team: Given that limited repercussions of this imperfection in order fee, we will opt to acknowledge finding and accept it as is.

  21. M-08 Medium Liquidations May Liquidate LPs Logical Error Acknowledged
    Location
    Global

    Description

    • When a trader is liquidated all of the trader's collateral is distributed to the LPs by reallocating it to

    the distributor where the funds do not contribute to the LP's health.

    • After that the positions of the trader (with negative PnL) are closed.

    The totalDebt is calculated with the following formula and can be negative if the LPs currently make a profit through trader losses: totalDebt = reportedDebt - marketDepositedCollateral

    Here is an example of how a liquidation could influence this calculation:

    • A trader owns $1000 collateral and has a position with a $700 loss
    • Before the liquidation totalDebt = 300 - 1000 = -700 (LPs make profit)
    • After the liquidation totalDebt = 0 - 0 = 0 (position is neutral)

    As pools delegate to multiple markets it could be that other markets have a positive totalDebt and this negative totalDebt is needed to balance the LP's health.

    Therefore this reallocation of debt to the distributor may cause some LP positions to become immediately unhealthy as they no longer are credited with the negative debt of the position (that was liquidated now).

    Their debt and collateral would then be socialized amongst the other LP positions, however these positions would not receive the collateral rewards that would have gone to the liquidated account through the distributor this way.

    Recommendation

    Consider restructuring the method by which market deposited collateral is distributed to LPs upon liquidation.

    Resolution

    Synthetix Team: In future iterations, we have ideas that would help lean exactly on this risk, but for the current version, it will be just accepted and properly documented.

  22. M-09 Medium Liquidation Fees Not Always Covered Logical Error Acknowledged
    Location
    LiquidationModule.sol: 340

    Description

    • Traders can provide collateral in the sUSD token and also in other tokens.
    • When a user is liquidated all of the users non sUSD collateral tokens are distributed among the LPs.
    • The liquidator (keeper) receives the liquidation reward in sUSD

    Therefore if the liquidated user did not deposit sUSD tokens and only used other tokens as collateral the liquidator will still be paid out in sUSD.

    This will cause accrual of negative credit capacity. Once the credit capacity decreases large enough, withdrawMarketUsd will revert and traders will not get their collateral back.

    Recommendation

    Pay out the keeper in the given collateral assets.

    Resolution

    Synthetix Team: Centralized backup keepers will hopefully lean on this very very edge case.

  23. M-10 Medium payDebt Should Update The interestRate Logical Error Resolved
    Location
    PerpsAccountModule.sol: 98-105

    Description

    1. The interestRate is calculated based on the utilizationRate. This rate is calculated by comparing

    the delegated funds from the LPs to the given market, with the open interest of the market. 2. The payDebt function pays the LPs funds back that are automatically counted as delegated to the given market.

    Therefore the interestRate after calling payDebt changes, but is not updated in the current implementation.

    Recommendation

    Update the interestRate at the end of the payDebt function.

    Resolution

    Synthetix Team: The issue was resolved in PR#2202.

  24. M-11 Medium Interest Rates Not Accurate To Liquidity Updates Logical Error Acknowledged
    Location
    Global

    Description

    The utilization fees in the Perps-V3 market are dependent on the amount of credit capacity delegated to the market by the V3 core system, however the utilization rate does not update when the amount of backing liquidity changes.

    For instance, a delegator can undelegate from a vault which provides credit capacity to the Perps-V3 market and increase the utilization ratio. However this utilization update is not reflected in the utilization rate until a updateInterestRate is triggered on the Perps-V3 side.

    Therefore the interest rate that is charged can be misrepresentative of the actual amount of liquidity utilized during these periods before an interest rate update is triggered.

    A malicious LP could abuse this by minting sUSD directly before a interest rate update in the Perps-V3 market, this way increasing the utilization ratio and forcing the the interest rate for all traders to be higher over the next period.

    The LP may then backrun the interest rate update and burn their sUSD that was minted.

    Recommendation

    Consider calling GlobalPerpsMarketModule.updateInterestRate each time backing liquidity in the V3 core system changes.

    Resolution

    Synthetix Team: Acknowledged.

  25. M-12 Medium Stepwise Jump After Update Logical Error Acknowledged
    Location
    MarketConfigurationModule.sol, GlobalPerpsMarketModule.sol

    Description

    A trader's pending funding and interest are calculated based on an accrued value which is updated each time positions are updated based on the current rate and elapsed time.

    The issue lies when admin updates funding or interest rate parameters without first realizing the accumulated funding/interest with the old parameters.

    Any rate increase/decrease would directly affect the funding/interest that a user would have to pay (positively or negatively).

    For example: T0: User opens position, market caches interestRate = 2% T5: Admin adjusts interest rate gradient, market interest rate should increase to 3% but InterestRate.update not performed so new rate is not stored. T10: User closes position

    Interest owed calculated using 2% rate -- (calculateNextInterest uses the last cached rate) Instead user should have been charged 2% from T0-T5, and 3% from T5-T10.

    Recommendation

    Call recomputeFunding and updateInterestRate before updating funding or interest rate parameters respectively.

    Resolution

    Synthetix Team: Acknowledged.

  26. M-13 Medium Partial Liquidators Not Fairly Rewarded Logical Error Acknowledged
    Location
    LiquidationModule.sol

    Description

    Keepers that perform partial liquidations on flagged accounts may be responsible for liquidating multiple open positions.

    However, they only receive a static liquidateKeeperCost, as opposed to flagKeeperCost (received by keeper that flags the account) which increases based on the number of open positions.

    If the gas cost of liquidating multiple positions is greater than the reward, these partial liquidation keepers may not be incentivized to perform the liquidations at all which is detrimental to the system.

    On the other hand, the keeper that flags the account for liquidation receives flagKeeperCost regardless of number of positions that were actually liquidated.

    If the liquidation window happened to be small and only one out of many positions were liquidated, the keeper still receives the cost of liquidating all open positions.

    Recommendation

    Consider limiting the flagKeeperCost and increasing liquidateKeeperCost based on the number of open positions actually liquidated.

    Resolution

    Synthetix Team: Acknowledged

  27. M-14 Medium validDistributorExists Not Checked Validation Resolved
    Location
    PerpsAccountModule.sol: 46

    Description

    The modifyCollateral function calls validDistributorExists which returns a boolean that indicates if a distributor of the given collateral is set or not.

    The modifyCollateral does not check if the returned boolean is true or false. Therefore if no distributor is set, the flow will continue.

    Recommendation

    Revert if the distributor was not set.

    Resolution

    Synthetix Team: The issue was resolved in PR#files.

  28. M-15 Medium Fill Price Funding And Interest Discrepancy Logical Error Acknowledged
    Location
    AsyncOrderSettlementPythModule.sol

    Description

    In _settleOrder and updatePositionData, oldPosition's PnL is called with fillPrice, which computes funding and interest based on fillPrice.

    This is problematic for two reasons: a) it causes a discrepancy with recomputeFunding which uses oraclePrice (see AsyncOrder.sol: 289). b) Funding and interest should not be based upon fillPrice, as this price includes a premium/discount according to how the trade affects the market skew.

    As a result, shorts pay more fees when they balance the market and less when they imbalance the market. Consider this scenario:

    • order price = 1000
    • short trade (positive price impact), fill price = 1010 => more fees are paid
    • short trade (negative price impact), fill price = 990 => less fees are paid

    Recommendation

    Consider changing to use oraclePrice in these two areas: 1. _settleOrder: oldPosition.getPnl(runtime.fillPrice) 2. updatePositionData: oldPosition.getPnl(runtime.currentPrice)

    Resolution

    Synthetix Team: Acknowledged.

  29. M-16 Medium Missing Access Control In payDebt Validation Resolved
    Location
    PerpsAccountModule.sol

    Description

    Except for the payDebt function, all other external, state-modifying functions in the Perps-V3 system have the access control: FeatureFlag.ensureAccessToFeature(Flags.PERPS_SYSTEM).

    This could lead to unexpected behavior in the event that the system is paused through the removal of access to flag feature.

    Also, the payDebt function also does not check that the msg.sender is indeed the account owner, allowing anyone to reduce debt and increase collateral for another account. It is unclear if this is intended behavior.

    Recommendation

    Consider adding to the payDebt function:

    FeatureFlag.ensureAccessToFeature(Flags.PERPS_SYSTEM) and Account.loadAccountAndValidatePermission(accountId, AccountRBAC._PERPS_MODIFY_COLLATERAL_PERMISSION)

    Resolution

    Synthetix Team: Resolved.

  30. M-17 Medium Max Collateral Can Be Exceeded Through payDebt Logical Error Resolved
    Location
    PerpsAccountModule.sol

    Description

    In the modifyCollateral function, the added collateral amount is validated through globalPerpsMarket.validateCollateralAmount to ensure that the maximum collateral limit is not exceeded.

    However, in the payDebt function, this check is not performed, even though the function can increase an account's sUSD collateral (as any excess over the amount used to pay debt is added to the collateral).

    Additionally, sUSD collateral may exceed the maximum amount through the realization of profits. Moreover, the payDebt function lacks the validation to ensure that the maximum number of collateral types per account is not exceeded, which is done through PerpsAccount.validateMaxCollaterals.

    Recommendation

    In the payDebt function, call: globalPerpsMarket.validateCollateralAmount and PerpsAccount.validateMaxCollaterals to ensure both max collateral amount and types are not exceeded.

    Resolution

    Synthetix Team: Resolved.

  31. M-18 Medium Account Can Be Made Liquidateable By Cancelling Unexpected Behavior Acknowledged
    Location
    AsyncOrderCancelModule.sol

    Description

    Cancellation charges account with keeper fee. Which this can put the account into a liquidatable state. This will result in immediate loss for user who could have withdrawn their funds otherwise.

    Order’s cancel-ability can be controlled by manipulating the skew. In addition to the fee for cancellation, the liquidation fees provide an added incentive to cancel the order which could even cover the costs of the skew manipulation.

    Recommendation

    Validate that account won’t be liquidatable after cancellation.

    Resolution

    Synthetix Team: Acknowledged.

  32. M-19 Medium Usage Of DEFAULT Price Tolerance Gaming Acknowledged
    Location
    Global

    Description

    getCurrentPrice from PerpsPrice.sol has a different staleness tolerances for different cases. In the current cannon deployment, node's DEFAULT tolerance is settled as 1 hour.

    Using a 1 hour price tolerance might lead to some attack vectors because user can maliciously choose best price for their purpose from the last one hour to use.

    Followings are some functions that uses DEFAULT tolerance and possible attack vectors that can be used with them: 1. minimumCredit : This can be used to bypass checks (withdrawing collateral from market as an LP when the minimumCredit is in the border) 2. isEligibleForLiquidation : Accounts that are liquidatable can settle order via bypassing this check. 3. reportedDebt : Can return not accurate values 4. View functions: Might be problematic for integrators.

    The reason for decreased severity is because it requires no recent update in Pyth price within the time period and because in order to create a large enough impact it requires good amount of price change within one hour window.

    Recommendation

    Be more strict for DEFAULT tolerance to decrease the possibility of mentioned issues and if possible use STRICT tolerance for specified functions. Also be sure to inform integrators about possible deviations if STRICT tolerance is not gonna be used for view functions.

    Resolution

    Synthetix Team: Acknowledged.

  33. M-20 Medium Unsafe Collateral Amount Because Of Fees Logical Error Acknowledged
    Location
    AsyncOrder

    Description

    When an order is settled, the account the order is committed for is charged orderFees. There are validations in AsyncOrder.validateRequest that the collateral value will not drop below the needed margin after paying the fees.

    The following must be true: currentAvailableMargin = getRequiredMarginWithNewPosition() + orderFees.

    However, getRequiredMarginWithNewPosition returns 0 when a position is being reduced. This makes the above expression equivalent to: currentAvailableMargin = orderFees

    This check is not sufficient. In the following scenario:

    • required margin = $500
    • currentAvailableMargin = $550
    • orderFees = 100

    The currentAvailableMargin = orderFees will pass, but after the account is charged the fees, its margin will fall below the required margin and their positions will instantly become liquidatable.

    Recommendation

    Validate the user is not liquidatable after settlement

    Resolution

    Synthetix Team: Acknowledged.

  34. M-21 Medium Vaults With Zero Delegation Prevent Liquidations DoS Resolved
    Location
    LiquidationModule.sol

    Description

    The LiquidationAssetmanager contract contains an array of addresses known as poolDelegatedCollateralTypes.

    When an account undergoes liquidation, the distribution of its collateral takes place within the distributeCollateral function of this contract.

    This particular function iterates over all pool collateral types, or vaults, and executes distributeRewards for each of them in proportion to the amount held by the vault.

    By tracing the sequence of calls, we eventually arrive at the distribute function within RewardDistributon.sol. In this function, an attempt to distribute a reward to 0 vault delegators will result in an error and cause a revert.

    Recommendation

    To address this issue, we recommend implementing a check to skip the distribution process if either the vault's collateral amount or the reward amount is equal to 0. This way, the liquidation process will not be hindered by attempting to distribute rewards to vaults with zero collateral.

    Resolution

    Synthetix Team: The issue was resolved in PR#2213.

  35. L-01 Low Unused Function Optimization Resolved
    Location
    PerpsMarketFactory

    Description

    The PerpsMarketFactory.depositMarketUsd() is not used.

    Recommendation

    Consider removing the function.

    Resolution

    Synthetix Team: The issue was resolved in PR#2305.

  36. L-02 Low Interest Is Updated Differently Unexpected Behavior Declined
    Location
    PerpsMarket, GlobalPerpsMarket

    Description

    The interest is updated using STRICT price tolerance in GobalPerpsMarket, and using ONE_MONTH price tolerance in GlobalPerpsMarket. This can lead to unexpected differences between the results of the two updates.

    Recommendation

    Consider if ONE_MONTH is not a too big period.

    Resolution

    Synthetix Team: Declined.

  37. L-03 Low Wrong Event Emission Events Resolved
    Location
    PerpsMarketFactoryModule

    Description

    PerpsMarketFactoryModule.initializeFactory() can be called multiple times, but the factory will be initialized only once. However, the FactoryInitialized event is emitted on each call.

    Recommendation

    Emit the FactoryInitialized event only when initializing the factory.

    Resolution

    Synthetix Team: The issue was resolved in PR#2305.

  38. L-04 Low OrderFees May Change Unexpected Behavior Acknowledged
    Location
    AsyncOrder

    Description

    The moment an order is committed, order fees are calculated. However, these fees are recalculated when the order is actually settled. If the difference between the fees is too much, the user may end up paying more than they wanted to.

    Recommendation

    Consider adding a parameter showing how much is the submitter of the request ready to pay for fees.

    Resolution

    Synthetix Team: Acknowledged.

  39. L-05 Low Incorrect NatSpec Documentation Resolved
    Location
    ICollateralConfigurationModule.sol

    Description

    The NatSpec of the registerDistributor function says the distributor parameter is the previous distributor and that it can be set to address(0). This is wrong - the distributor is the new one and it cannot be set to address(0).

    Recommendation

    Correct the NatSpec

    Resolution

    Synthetix Team: The issue was resolved in PR#2305.

  40. L-06 Low Incorrect Array Lengths Optimization Acknowledged
    Location
    KeeperCosts

    Description

    _processWithRuntime() initializes the runtimeKeys and runtimeValues arrays with length of 4 when 2 is enough.

    Recommendation

    Change the length of the arrays to 2.

    Resolution

    Synthetix Team: Aknowledged.

  41. L-07 Low reportedDebt & minimumCredit Do Not Revert Validation Acknowledged
    Location
    PerpsMarketFactoryModule.sol: 138 & 149

    Description

    The reportedDebt and minimumCredit functions receive the perpsMarketId as a parameter and check if the given id equals the own one to ensure the correct contract/market was called.

    If the check fails, the functions return 0 debt/credit instead of reverting. This is a dangerous practice as calling the wrong market will not revert the call, instead it goes on with the wrong data.

    Recommendation

    Revert instead of returning 0.

    Resolution

    Synthetix Team: Acknowledged.

  42. L-08 Low Precision Loss In L1 Gas Price Calculations Precision Acknowledged
    Location
    OpGasPriceOracle.sol

    Description

    In the function getCostOfExecutionEth, if isEcotone is true, the L1 gas cost for execution is calculated with a division before multiplication, which leads to precision loss:

    uint256 l1GasPrice = (baseFeeScalar * l1BaseFee * 16 + blobBaseFeeScalar * blobBaseFee) / (16 * 10 ** decimals);

    costOfExecutionGrossEth = ((gasUnitsL1 * l1GasPrice) + (gasUnitsL2 * gasPriceL2));

    Recommendation

    Consider moving the division of (16 * 10 ** decimals) to after (gasUnitsL1 * l1GasPrice) has been performed.

    Resolution

    Synthetix Team: Acknowledged.

  43. L-09 Low Unused Variable In validateRequest Optimization Resolved
    Location
    AsyncOrder.sol: 281

    Description

    In the function validateRequest, the variable runtime.currentLiquidationReward is obtained from account.isEligibleForLiquidation but is never used.

    Recommendation

    Remove the variable runtime.currentLiquidationReward.

    Resolution

    Synthetix Team: The issue was resolved in PR#2305.

  44. L-10 Low Keeper DoS Griefing Attack DoS Acknowledged
    Location
    LiquidationModule.sol: 70

    Description

    The liquidation functions will revert if the given account can not be liquidated. As users can increase their collateral or repay debt at any time (as long as no other circumstances prevent it) the following attack path is enabled:

    • Keeper tries to liquidate an account that does not have enough available collateral to back its open

    positions

    • The account front runs the transaction and increases its collateral or repays debt
    • The keeper wasted gas

    Recommendation

    Document this so keepers are aware of this griefing attack vector.

    Resolution

    Synthetix Team: Acknowledged.

  45. L-11 Low Liquidate Users By Manipulating Gas Costs Logical Error Acknowledged
    Location
    PerpsAccount.sol: 504-505

    Description

    • To compensate keepers for the gas costs of calling the liquidation functions every account that has

    open positions must hold the necessary amount of funds to pay these gas costs.

    • Therefore if the gas price increases quickly a lot of accounts might be suddenly liquidatable.

    This can be abused by manipulating the block.basefee for example with block stuffing to liquidate users for the reward.

    Recommendation

    Consider increasing the minimumPositionMargin to the point that it can cover the volatility of gas and reduce the risk of liquidations.

    Resolution

    Synthetix Team: Acknowledged.

  46. L-12 Low Max Liquidation Windows May Be Under Estimated Logical Error Acknowledged
    Location
    PerpAccount.sol

    Description

    When calculating the possible liquidation rewards, function getKeeperRewardsAndCosts returns the max number of windows needed to liquidate one position.

    The issue lies with the possibility of multiple positions requiring multiple liquidation windows. Just taking the highest number of liquidation windows for one position may not properly account for the gas needed to liquidate all positions.

    Recommendation

    Consider using the sum of liquidation windows across all positions instead of the max number of windows for one position.

    Resolution

    Synthetix Team: Acknowledged.

  47. L-13 Low System Param Changes May Lead To Liquidations Logical Error Acknowledged
    Location
    GLOBAL

    Description

    Updating system parameters like for example interest rate, funding rate, skew scale, margin requirements, can impact the health of the positions in the market and therefore lead to very fast or even instant liquidations.

    Recommendation

    Consider adding a timelock for important system parameter updates.

    Resolution

    Synthetix Team: Acknowledged.

  48. L-14 Low Utilization Rate Is Unbounded Logical Error Acknowledged
    Location
    GlobalPerpsMarket.sol: 92

    Description

    The function utilizationRate is used to calculate the percentage of delegated collateral is being used in the market, which is then used to calculate the interest rate.

    utilizationRate is unbounded which means that if delegatedCollateral is a very small value, the rate returned and resulting interest could be excessively high.

    Recommendation

    Consider binding utilizationRate to a max value.

    Resolution

    Synthetix Team: Acknowledged.

  49. L-15 Low Precision Loss In valueInUsd Calculations Precision Acknowledged
    Location
    PerpsCollateralConfiguration.sol

    Description

    In the function valueInUsd, impactOnSkew is calculated with a division before multiplication which could lead to precision loss:

    uint256 impactOnSkew = amount.divDecimal(skewScale).mulDecimal(self.discountScalar);

    Recommendation

    Perform the multiplication before division.

    Resolution

    Synthetix Team: Acknowledged.

  50. L-16 Low Risk Reduced Trades By Fill Price Manipulation Logical Error Acknowledged
    Location
    AsyncOrder.sol: 314-347

    Description

    Proof of concept: PoC

    • Orders can be executed after commit time + delay and till the expiry of the order
    • The price at commit time + delay is used even if the order is executed later than that (somewhere

    between this timestamp and the expiry timestamp)

    • Therefore if the attacker can make settles revert during these two timestamps if the price does not

    go in the wished direction and can make it pass again if it goes in the wished direction the attacker can trade risk-free The following attack path allows reduced risk trading:

    • Attacker deposits collateral and creates an order that requires exactly the available margin to be

    created (this order increases the skew = the fill price is against the order)

    • The attacker deposits collateral and creates an order with another account to increase the skew a

    bit further (in the same block)

    • Attacker settles the second order before the first order (by paying more gas than the other keeper)
    • Now the first order reverts because the new fillPrice after increasing the skew further ensures that

    the required margin for the order is bigger than the available one Now the attacker has two options:

    • If the price goes in the wished direction (makes instant profit) the attacker can create a third order

    to decrease the skew again so that the order goes through

    • Else the attacker can wait till the order expires

    Therefore the attacker was able to make one risk-free trade with another non-risk-free trade.

    Recommendation

    Cancel orders that lead to reverts because of price changes.

    Resolution

    Synthetix Team: Acknowledged.

  51. L-18 Low Endorsed Liquidations Lower Liquidation Capacity Unexpected Behavior Acknowledged
    Location
    LiquidationModule

    Description

    Whenever an endorsed liquidation happens, the amount liquidated is being reduced from the liquidation capacity for the relevant windows.

    This creates a discrepancy between the amounts that can be liquidated by a normal user and endorsed user based on the order their transactions were executed.

    Consider the following example:

    • max amount to liquidate in window = 5000
    • user A liquidates 4000
    • endorsed liquidator liquidates 5000 more

    The total liquidated amount is 9000, but if the endorsed liquidator's transaction was executed before the regular user's one, the second transaction would have reverted.

    Recommendation

    Consider not accounting for endorsed liquidations when calculating the current liquidation capacity

    Resolution

    Synthetix Team: Acknowledged.

  52. L-19 Low Funding Rate Is Rounded Down Precision Acknowledged
    Location
    PerpsMarket

    Description

    The calculations for the funding value use division multiple times and each one of them is rounding down the result. In addition, there are hidden divisions before multiplications, for example because of propotionalElapsed(), which cause further loses in the calculations.

    Recommendation

    Consider rounding up and possibly refactoring propotionalElapsed() to not divide before multiplying.

    Resolution

    Synthetix Team: Acknowledged.

  53. L-20 Low Funding Rate Lags Unexpected Behavior Acknowledged
    Location
    PerpsMarket

    Description

    Due to the way the funding rate is calculated, there may be a case where the market skew is inverted from positive to negative or vice versa, but the funding direction doesn't change.

    For example, there are a lot of longs, so the funding rate is positive and the longs pay shorts. Then a user opens a lot of shorts and the skew becomes negative. However, the funding rate still charges longs instead of shorts.

    Recommendation

    Consider different calculation for the funding rate if longs should pay shorts only when the skew is positive and vice versa.

    Resolution

    Synthetix Team: This is intentional in the design, to encourage persistent arbitrage of the funding rate over time frames, rather instantaneous changes in the skew.

  54. L-21 Low Temporary DoS For Commiting Orders DoS Acknowledged
    Location
    AsyncOrder

    Description

    Async orders cannot be committed or settled if the strategy they use is disabled. A problem may arise if the order's strategy is disabled after that order has been committed but not settled.

    The AsyncOrder will be populated and not executable. It won't be cancellable as well (if the price is not exceeded). This will cause a temporary DoS - the user will have to wait until the order expires to commit a new one.

    Recommendation

    Allow users to cancel their order if the used strategy is disabled.

    Resolution

    Synthetix Team: In the extreme situation where the strategy is disabled then all orders should be suspended from action. This is unlikely to happen unless there is a looming situation that calls for it.

  55. L-22 Low Reward Distribution Is Not Future-Proof Logical Error Acknowledged
    Location
    PerpsMarketFactory.sol: 127

    Description

    • The currently implemented reward distribution flow for liquidations only allows to distribute to one

    pool per collateral token.

    • Currently pool creation is done by governance only, but in the future, it will be permissionless.

    New pools in the future will therefore not be able to receive liquidated non-sUSD collateral. This will either disincentivize pool creators to back any other market that they don't own (which is against SNX's vision), or a pool owner that is not aware of this situation will back those markets and LP's will lose funds.

    Recommendation

    Change how rewards are distributed and allow multiple pool IDs. While distributing skim all pool's collaterals and distribute accordingly.

    Resolution

    Synthetix Team: Acknowledged.

  56. L-23 Low Immediately Cancelable Orders Are Permitted Unexpected Behavior Acknowledged
    Location
    AsyncOrderModule.sol: 31-88

    Description

    Users can create orders that can be instantly canceled after creation under the current conditions, by providing a slippage threshold that is already exceeded at the time of order creation. This order will therefore be immediately canceled and the user will lose the keeper gas costs and reward fees.

    Recommendation

    Revert if such a slippage check is given.

    Resolution

    Synthetix Team: Acknowledged.

  57. M-22 Medium Vaults With Zero Delegation Prevent Liquidatons DoS Resolved

    The report lists this finding in its index without a detail page. See the PDF.

Invariants 40

The review's fuzzing suite asserted 40 invariants. 33 held and 7 did not.

Every invariant tested
IDInvariantResult
ORD-01If an account has an unexpired committed order, a subsequent commit order call willHeld
ORD-02always revert The sizeDelta of an order is always 0 after a successful settle order callHeld
ORD-03An order immediately after a successful settle order call is never liquidatableBroken
ORD-04If a user successfully settles an order, their sUSD balance is strictly increasingHeld
ORD-05The sUSD balance of a user that successfully cancels an order for another user is strictlyHeld
ORD-06increasing The minimum credit requirement must be met after increase order settlementBroken
ORD-07Utilization is between 0% and 100% before and after order settlementBroken
ORD-08Non-SUSD collateral should stay the same after profitably settling orderHeld
ORD-09Should always give premium when increasing skew and discount when decreasing skewHeld
ORD-10Market utilization rate is always between 0 and 100%Held
ORD-11An account should not be liquidatable by margin only after order settlementHeld
ORD-12An account should not be liquidatable by margin only after order cancelledHeld
ORD-13Market size should always be the sum of individual position sizesHeld
ORD-14Position should not be liquidatable after committing an orderHeld
ORD-15Position should not be liquidatable after cancelling an orderBroken
ORD-16Open positions should always be added / removed from the openPositionMarketIdsHeld
ORD-17array All tokens in the activeCollateralTypes array from individual accounts should be included inHeld
ORD-18the global activeCollateralTypes array Sum of the debt of all accounts == global debtBroken
ORD-19Debt should not vanish after settle another orderBroken
ORD-20AsyncOrder.calculateFillPrice() should never revert.Held
LIQ-01isPositionLiquidatable never revertsHeld
LIQ-02remainingLiquidatableSizeCapacity is strictly decreasing immediately after a successfulHeld
LIQ-03liquidation A user can be liquidated if minimum credit is not metBroken
LIQ-04All account margin collateral should be removed after full liquidationHeld
LIQ-05Market deposited collateral should decrease after full liquidation by the account collateralHeld
LIQ-06that was liquidated User should not be able to gain more in keeper fees than collateral lost in liquidateMarginOnlyHeld
LIQ-07If an account is flagged for liquidations the account is not allowed to have collateral orHeld
LIQ-08debt. MaxLiquidatableAmount can never return a value greater thanHeld
LIQ-09requestedLiquidationAmount Calling LiquidationModule.liquidate after it has been previously called in the same blockHeld
MGN-01should not increase the balance of the caller Position is never liquidatable after a successful margin withdrawHeld
MGN-02A modify collateral call will always revert for an account that has a pending orderHeld
MGN-03If an account's collateral is 0, then the account's debt must also be 0Held
MGN-04depositedCollaterals array should be adjusted by amount of collateral modified (for WBTC)Held
MGN-05If sUSD collateral modified, minimumCredit should be updated by that amountHeld
MGN-06Sum of collateral token values should be the totalCollateralValueUsd stored in the marketHeld
MGN-07User cannot withdraw more non-susd collateral than they depositedHeld
MGN-08It should never happen that a user has an amount of collateral deposited with a token > 18 decimals precision and withdrawing lead toHeld
MGN-09precision loss After modifying collateral, a trader should not be immediately liquidatable.Held
MGN-10After paying debt, a trader should not be immediately liquidatable.Held
MGN-11The sum of collateral amounts from all accounts should always equal the global collateral amount.Held

More from Synthetix

All 14 reports
  1. Update Reviews

    34 findings2 critical · 4 high 34 findings: 2 critical, 4 high, 13 medium, 10 low, 5 informational
  2. Deposit Contract

    38 findings1 high 38 findings: 1 high, 6 medium, 20 low, 11 informational
  3. Fixed Staking Rewards

    6 findings1 high 6 findings: 1 high, 2 medium, 3 low
  4. Auto-Compounding LP Vault

    80 findings1 critical · 4 high 80 findings: 1 critical, 4 high, 14 medium, 61 low

Put your code through the same review.

This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.

Get a quote