PariFi engaged Guardian to review the security of its decentralized synthetics perpetuals exchange. From the 18th of August to the 1st of September, a team of 3 auditors reviewed the source code in scope.
- Published
- Review window
- August 18 to September 1, 2023
- Language
- Solidity
- Chains
- Arbitrum
- Sector
- Perpetuals
- 6 Critical
- 6 High
- 19 Medium
- 7 Low
- 0 Informational
Scope
Overview
PariFi engaged Guardian to review the security of its decentralized synthetics perpetuals exchange. From the 18th of August to the 1st of September, a team of 3 auditors reviewed the source code in scope.
Issues Detected Throughout the course of the audit numerous high impact issues were uncovered and promptly remediated by the PariFi team. Several issues impacted the fundamental behavior of the protocol, following their remediation Guardian believes the protocol to uphold the functionality described for the perpetuals exchange.
Code Quality Given the number of high-impact issues detected and the scope of remediations necessary, Guardian supports an independent security review of the protocol at a finalized frozen commit.
Findings 38
-
MKTV-1 Critical Users Prevented From Withdrawing Liquidity Access Control Resolved
Description
Whenever a user deposits into the
MarketVault, thelastDepositedTimestamp[receiver]is updated to the current block's timestamp. This is then used to ensure depositors have been in the vault for aMINIMUM_DEPOSIT_PERIODwhen withdrawing or redeeming.The problem is that a malicious user can deposit 1 wei of assets with another user as the
receiver, updating the receiver'slastDepositedTimestamp. This can be used to prevent users from ever exiting their LP, leading to loss of funds.Recommendation
Do not allow users to deposit for arbitrary receivers.
Resolution
-
FM-1 Critical Fees Unable To Be Distributed Logical Error Resolved
Description
The fee distribution interval early return logic is reversed such that fees can only be distributed inside of the
DELAYwindow. After theDELAYwindow has passed fees can no longer be distributed.// Distribute fees at regular intervals of every 1 hour if (lastTransferTimestamp + DELAY < block.timestamp) return;
Recommendation
Replace the interval early return logic with:
lastTransferTimestamp + DELAY > block.timestamp.Resolution
-
ORDM-1 Critical Users Can Modify Any Position Access Control Resolved
Description
An arbitrary
positionIdcan be provided to themodifyPositionfunction without validation that themsg.senderis the owner. As a result, users may modify or close any arbitrary position, even if it doesn’t belong to them.Recommendation
Validate that the
msg.senderis the owner of the suppliedpositionId.Resolution
-
ORDM-2 Critical Decreasing Position Size Does Not Account For PnL Logical Error Resolved
Description
In the
_decreasePositionfunction users can decrease their position size without realizing any of the positive or negative PnL for their position. This way a user can decrease their size to a trivial amount if they do not immediately start to profit.Recommendation
When users decrease their position size account for a proportional amount of their current PnL being realized.
Resolution
PariFi Team: Fixed https://github.com/Parifi/parifi-contracts-internal/pull/66
-
ORDM-3 Critical Users In Profit Errantly Liquidated Logical Error Resolved
Description
When the position's PnL is obtained in the liquidate function with
(uint256 pnlInCollateral,) = getProfitOrLossInCollateral(_positionId, executionPrice)The protocol assumes that the pnlInCollateral is always a loss,uint256 lossInCollateral =pnlInCollateral + feesInCollateral.As a result, a position with a large profit will be treated as if it is in a large loss and can be liquidated.
Recommendation
Require that the
isProfitreturned from thegetProfitOrLossInCollateralfunction isfalse.Resolution
PariFi Team: Fixed https://github.com/Parifi/parifi-contracts-internal/pull/66
-
ORDM-4 Critical Insolvent Closes Steal Collateral From Other Positions Logical Error Resolved
Description
When the
pnlInCollateralis greater in magnitude than thecollateralAmountandisProfitisfalsethe entirepnlInCollateralamount is transferred to thefeeManagerand distributed to the protocol and the LPers.This steals the delta (
|pnlInCollateral| - collateralAmount)from the collateral of other positions in theOrderManager, otherwise if that collateral amount isn't in theOrderManagercontract from other positions the tx will simply revert with a balance underflow.Recommendation
In the event where the
pnlInCollateralis greater than the available collateral andisProfitisfalse, only send the available collateral from the position to thefeeManager.Resolution
PariFi Team: Fixed https://github.com/Parifi/parifi-contracts-internal/pull/66
-
ORDM-5 High Incorrect OrderType Used For Execution Price Logical Error Resolved
Description
In the
calculateLeveragefunction theexecutionPriceis hardcoded to use theOPEN_NEW_POSITIONorder type, however thecalculateLeveragefunction is used for other order types such asDECREASE_POSITIONwhere the resultingexecutionPriceought to be using the less favorable price for decreases.Recommendation
Allow the
calculateLeveragefunction to take in anorderTypeand supply the appropriateorderTypewhen validating leverage for eachorderType.Resolution
-
ORDM-6 High Price Updated In Wrong Direction Logical Error Resolved
Description
A potential problem arises when price is updated according to the market's
deviationPoints. The update only considers whether the user is long or short, if the user opens a long they receive a superior execution.If the user closes a long position, the user receives a less favorable execution. The liquidity curve is intended to imitate widening or narrowing market spreads. If spreads are tight, execution should be favorable both when buying and selling. However, that is not the behavior displayed.
Recommendation
Take into consideration whether the
orderTypebeing executed is an increase or decreaseorderTypewhen adjusting theupdatedPriceby thedeviationPoints.Resolution
-
ORDM-7 High Small Positions Prevented From Being Closed Validation Resolved
Description
The
_deductFeesFromPositionfunction reverts if the remaining position collateral after deducting fees is less than the configured minimum collateral for the market.However this
_deductFeesFromPositionfunction is called during_closePositionexecution, therefore positions that have accumulated enough fees to be put under the minimum collateral amount cannot be closed.Recommendation
Do not validate the minimum collateral amount in the
_deductFeesFromPositionwhen closing a position.Resolution
-
ORDM-8 High Misconfigured Markets Can Break The Protocol Validation Resolved
Description
The
addNewMarketfunction allows a market to be added with the specified_marketId, however the_newMarket.marketIdis not validated to be the same as the provided_marketId.Additionally, the
Marketstruct should not store amarketIdas it is unnecessary and can always be accessed from an order or position object.Recommendation
Remove the
marketIdattribute on theMarketstruct as it is unnecessary and leads to misconfiguration.Resolution
-
ORDM-9 Medium updatedAvgPrice Always Rounds Down Rounding Resolved
Description
The
updatedAvgPricecomputed in the_increasePositionfunction is always rounded down, therefore it is possible for users with long positions to increase their position at anexecutionPricethat is higher than their average price and see no change in their position’s average price.For example, an increase order with the following characteristics will not change the average price of the position.
positionSize = 1e18avgPrice = 100e8orderSize = 1e10executionPrice = 101e8
Therefore, the
updatedAvgPrice = (1e18 * 100e8 + 1e10 * 101e8) / (1e18 + 1e10) = 100e8.For some tokens this rounding can yield opportunities for traders to manipulate their average price and make risk free profits.
Recommendation
Use roundUp division when calculating the
updatedAvgPricefor longs and round down division when calculating theupdatedAvgPricefor shorts.Resolution
-
ORDM-10 High Lack Of Reserve Validation Validation Acknowledged
Description
Although the OrderManager validates that the position does not increase the open interest past the
maximumOi, LPers are still exposed to great risk as there is no validation on the size of the position relative to the funds available in the market.With a large enough order which is still within OI bounds, a user in profit can drain the entirety of the MarketVault’s reserves and leave LPer’s insolvent. Furthermore, any traders in profit would not be able to claim their profit. This would break a core function of the protocol.
Recommendation
Add validation checks to ensure the position size may be only up to a specific percentage of funds available in the market.
Resolution
PariFi Team: We use fractional reserves and use
maximumOito keep the risks in check. -
ORDM-11 High Open Interest Validation Becomes Meaningless Validation Resolved
Description
Whenever the Aggregate
vaultOpenInterestis increased or decreased, it is done so at the current value of the index token:- User opens a position with size 10 ETH @ $2,000 / ETH
vaultOpenInterestis increased from 0 to $20,000- The user then closes their position after ETH drops to $1,000 / ETH
vaultOpenInterestis decreased from $20,000 → $10,000- $10,000
vaultOpenInterestremains even though there are no open positions
This inaccuracy leads to cases where the
vaultOpenInterestis massively inflated or massively reduced depending on whether user’s close their positions at a higher or lower index token price than when they opened.Over the lifecycle of the vault this validation will become entirely detached from the actual open interest of position’s using the vault as backing, perturbing the purpose of the validation and preventing actions from occurring when the
vaultOpenInterestis inflated.Recommendation
Use the open interest that is tracked on a per market basis and value the open interest of each market in aggregate based on the current price of the each index token during validation.
This solution requires an O(n) approach where n is the number of markets configured for a single vault. This ought to be fine as long as the number of markets for a single vault is limited to a reasonable amount and the aggregate
vaultOpenInterestis computed once per transaction.Resolution
PariFi Team: We removed the open interest validation check, and will stick to the original solution of using
maximumOito control the open interest for markets. -
FWD-1 Medium Relayer May Censor Transactions Validation Acknowledged
Description
The
gasSentis validated to be greater than64/63 * the transaction.minGas, however thegasSentis recorded at the beginning of theexecutefunction which has a non-trivial amount of logic, that will expend gas, before executing the external call.Therefore the remaining gas that is forwarded to the external call may be less than the
transaction.minGas.Recommendation
Add a buffer to the
gasSentvalidation that comfortably covers any expenditure that would occur before the external call.Resolution
PariFi Team: Agreed to explain in documentation about
minGasbeing the gas needed for the entireexecutefunction. -
MKTV-2 Medium Maximum Open Interest Configuration Risk Configuration Resolved
Description
The admin can configure any
maxVaultOpenInterestvalue with thesetMaximumVaultOpenInterestfunction. This poses a risk as the admin may accidentally configure amaxVaultOpenInterestthat is below the currentvaultOpenInterest.Consider the following scenario:
maxVaultOpenInterestis 15vaultOpenInterestis 10maxVaultOpenInterestis set to 5- A liquidation that would reduce the
vaultOpenInterestby 3 cannot execute as it would fail themaxVaultOpenInterestvalidation.
Recommendation
Take care when using the
setMaximumVaultOpenInterestfunction. Consider adding validation such that themaxVaultOpenInterestcannot be set below the currentvaultOpenInterest.Resolution
PariFi Team: We removed the open interest validation check, and will stick to the original solution of using
maximumOito control the open interest for markets. -
ORDM-12 Medium Unexpected Limit Execution Unexpected Behavior Resolved
Description
Limit orders are traditionally defined to execute at or better than the limit price. However, the current limit price check disallows execution when the market price is equivalent to the expected price. This may lead to unexpectedly failed entries for users who expected their order to be executed once price reached their limit price.
if (userOrder.isLimitOrder) { if ( (userOrder.triggerAbove && executionPrice <= expectedPrice) || (!userOrder.triggerAbove && executionPrice >= expectedPrice) ) { revert LibError.PriceMismatch(executionPrice, expectedPrice); } }
Recommendation
Allow limit orders to execute when they are at or better than the configured trigger price.
Resolution
-
ORDM-13 Medium Slippage Applies To Both Sides Logical Error Resolved
Description
For slippage calculations,
_settleOrdercalculates a percentage above theexpectedPriceand a percentage below theexpectedPrice.However, slippage should not apply to both the upper and lower prices. Traditionally, when a user submits a long, slippage is measured against the best offer price. If a user submits a short, slippage is measured against the best bid price. PariFi is comparing against both a
lowerLimitandupperLimitregardless of position direction.A user who is long would like to buy the asset at a price lower than the
lowerLimit. A user who is short would like to sell the asset at a price higher than theupperLimit.Recommendation
Only compare the price against the
upperLimitfor longs and thelowerLimitfor shorts.Resolution
-
ORDM-14 Medium maxLeverage Bypassed Validation Resolved
Description
Upon creating a new position the
maxLeverageis not validated. ThemaxLeveragevalidation will occur when you make a new order, however fees, price, and themaxLeverageitself may change in between the time you create your order and when it gets executed.Therefore it is possible to bypass the configured
maxLeveragefor a market.Recommendation
Validate the
maxLeverageafter the order has been executed.Resolution
-
ORDM-15 Medium Lacking Validation For New Markets Validation Resolved
Description
The
addNewMarketfunction lacks several key validations on new markets being added such as validating the ranges for:openingFeeclosingFeeliquidationFeeminCollateralliquidationThreshold
Recommendation
Add validation on the configured values for each of the above.
Resolution
-
ORDM-16 Medium Lacking Validation When Updating Markets Validation Resolved
Description
The
updateExistingMarketfunction lacks several key validations on new markets being updated such as validating the ranges for:openingFeeclosingFeeliquidationFeeminCollateralliquidationThreshold
Recommendation
Add validation on the configured values for each of the above.
Resolution
-
MKTV-3 Medium _withdrawalFee Not Validated In Constructor Validation Resolved
Description
The
_withdrawalFeeis not validated to be within theWITHDRAWAL_FEE_CAPin theMarketVaultconstructor.Recommendation
Validate that the
_withdrawalFeeis within theWITHDRAWAL_FEE_CAPin theMarketVaultconstructor.Resolution
-
ORDM-17 Medium Pyth Prices Not Updated Before Being Used Logical Error Declined
Description
priceFeed.updatePythPrice(priceUpdateData)is not used to update the pyth price beforecancelPendingOrderorcreateNewPosition.To be entirely accurate prices should be updated before computing the fees in these functions using those pyth prices.
Recommendation
Update Pyth prices with
priceFeed.updatePythPrice(priceUpdateData)for entirely up to date pricing when computing fees in thecancelPendingOrderandcreateNewPositionfunctions.Resolution
PariFi Team: This is by design that we use the TWAP EMA price for non-critical purposes to prevent pushing prices when the action is initiated from the user.
-
ORDM-18 Medium ExecutionFee To Cover The Keeper's Gas Logical Error Resolved
Description
Currently there is no remuneration for the keeper executing orders with the
settleOrderfunction. It may be prudent to cover the gas costs for the keeper by introducing anexecutionFeethat must be sent in when creating an order to cover the keeper’s gas expenditure.Such an
executionFeewould dissuade from potential keeper griefing as currently there are no fees charged upon cancelling decrease orders with thecancelPendingOrderfunction. Malicious actors could see the keeper submit asettleOrdertransaction and front-run it to cancel their order and avoid paying any fees. Though such an attack is dubious at best on the Arbitrum network.Recommendation
Consider implementing an
executionFeeto remunerate the keeper’s gas expenditure when settling orders. Additionally consider charging a percentage of thisexecutionFeeupon the cancellation of an order, especially decrease orders as they currently have no fees applied on cancellation.Resolution
PariFi Team: We are discussing internally to have a mechanism to have the execution fee as a configurable value so that we can subsidize the execution fee for a certain promotion period initially and later turn it on
-
ORDM-19 Medium Fees May Not Be Skipped Due To Misconfiguration Validation Resolved
Description
In the
updateExistingMarketfunction the admin may update the market and provide an_updatedMarketwith a non-paused status e.g.isLive = true, however this would avoid thedataFabric.unpauseMarketcall as the market has been toggled to a live status without calling thetoggleMarketStatusfunction.Recommendation
Either require that markets are created with an
isLiveoffalseor invoke thedataFabric.unpauseMarketfunction in theupdateExistingMarketwhen the_updatedMarket.isLive ==true.Resolution
-
ORDM-20 Medium Leverage Relies On EMA Logical Error Declined
Description
A position's leverage is calculated using an EMA price, however this is a lagging indicator and will not be entirely accurate to current prices.
Therefore users will be able to open positions that, at current prices, would be above the
maxLeverage. However by the EMA are not abovemaxLeverage.Recommendation
Consider if this is the desired behavior, if not, use current prices to calculate the position’s leverage.
Resolution
PariFi Team: This is by design that we use the TWAP EMA price for non-critical purposes to prevent pushing prices when the action is initiated from the user.
-
ORDM-21 Medium User's Leverage Changes With The Index Price Unexpected Behavior Declined
Description
The max leverage check relies on the index token price relative to the collateral token price, if the index token price moves relative to the collateral token price, a position’s leverage changes. This means the leverage of user’s position can change even if the price of their collateral token does not move and they don’t make any orders.
The leverage of a position will change with the price of the index token. However this is unexpected behavior as often the leverage of a perpetual position is a ratio of the collateral deposited and the initial size of the position, which does not change with the index price.
Recommendation
Determine if this ought to be the expected behavior and if so ensure it is well documented for users.
-
ORDM-22 Medium Fees Based On EMA Logical Error Declined
Description
The fees on lines 524 and 651 are calculated based on an EMA price which is a lagging indicator and will not be entirely accurate to the current token price.
Recommendation
Consider if this is desired. If not, use the
convertMarketToTokenfunction to compute the value of these fees. -
ORDM-23 Medium Read-only Reentrancy Potential Reentrancy Resolved
Description
In the
_closePositionfunction the position is deleted at the end of the function call, however for tokens with callbacks it would be safer to delete the position and update the market data before the transfer, it may also be wise to delete the position before thedistributeFeesexternal call as well, even though this is supposedly a trusted external call.Recommendation
Ensure state is modified before initiating token transfers to protect against potential read-only reentrancies with ERC-777 tokens.
Resolution
-
ORDM-24 Medium Inefficient Limit Order Validation Validation Resolved
Description
When executing a limit order the
expectedPriceis validated for the first time, however this validation should be carried out when the order is first created. Otherwise user’s may submit many orders that will always fail this validation in order to waste the keeper’s gas upon execution of thesettleOrderfunction.Recommendation
Validate the
expectedPricefor thelimitOrderwhen first creating the order.Resolution
-
ORDM-25 Medium Fees Can Be Avoided With Small Order Sizes Rounding Resolved
Description
Throughout the
OrderManagercontract, fees are calculated using round down division. Therefore it is possible for users to avoid paying fees by using order sizes small enough such that they round down to 0.With sponsored order creation through the
PariFiForwarderthis can be a viable method to avoid fees.Recommendation
Employing such a strategy would cost a significant amount of gas and is unlikely to ever be profitable, however it may be preferred to use round up division when computing these fees.
Resolution
-
ORDM-26 Medium Risk Of Stale Pricing Validation Declined
Description
When creating a new position, the Pyth price feed is not updated. The fees calculation and the leverage validation are at risk of utilizing a stale price. This is because the
convertMarketToTokenSecondaryfunction is called which uses the unsafe EMA price point without validating publish time.PythStructs.Price memory pythPrice = pyth.getEmaPriceUnsafe(priceId); (priceUsd, priceTimestamp) = _calculatePythPrice(pythPrice, false);
According to Pyth documentation the
getEmaPriceUnsafefunction, “may return a price from arbitrarily far in the past. It is the caller's responsibility to check the returnedpublishTimeto ensure that the update is recent enough for their use case.”Recommendation
Consider validating the publish time or updating the price feed when creating a new position.
Resolution
PariFi Team: This is by design that we use the TWAP EMA price for non-critical purposes to prevent pushing prices when the action is initiated from the user.
-
MKTV-4 Low Dead Address Can Be Constant Optimization Resolved
Description
To reduce bytecode and favor DRY the dead address referenced multiple times can instead be a constant in the
MarketVaultcontract.Recommendation
Declare the dead address as a constant variable in the
MarketVaultcontract.Resolution
-
ORDM-27 Low Unnecessary userPosition Storage Declaration Optimization Resolved
Description
The
userPositionvariable is declared as astoragereference variable, however it would be much more efficient to declare it as amemoryvariable.Recommendation
Declare the
userPositionvariable as amemoryvariable.Resolution
-
ORDM-28 Low Unnecessary userOrder Storage Declaration Optimization Resolved
Description
The
userOrdervariable is declared as astoragereference variable, however it would be much more efficient to declare it as amemoryvariable.Recommendation
Declare the
userOrdervariable as amemoryvariable.Resolution
-
ORDM-29 Low Leverage Rounded Down Rounding Resolved
Description
When computing the leverage of a position in the
calculateLeveragefunction round down division is used. The result of the rounding is that the position appearing as if it has slightly lower leverage than it actually does.Recommendation
Though this rounding will have a minimal impact it may be worthwhile to use
roundUpdivision to avoid position’s being technically above the allowed leverage yet rounded to within the allowed range.Resolution
-
ORDM-30 Low Unnecessary Handling Of 0 Collateral Optimization Acknowledged
Description
In the event that
isProfitisfalseand thepnlInCollateralis greater in magnitude than thecollateralAmountfor the position an alternative leverage calculation is used to avoid divide by zero reverts. However any position where the collateral is exactly equal to thepnlInCollateralor insufficient to cover thepnlInCollateraltechnically has infinite leverage and should therefore always fail themaxLeveragevalidation.Recommendation
Simply revert with a
maxLeveragevalidation failure in the event that thepnlInCollateral >=_collateralAmount.Resolution
PariFi Team: The
calculateLeveragefunction will also be used off-chain by UI and other components, hence we are not reverting inside the function, but reverting later inside the_validateLeveragefunction. -
ORDM-31 Low Loss Amount Is Rounded Down Rounding Resolved
Description
When computing the
profitOrLossround down division is always used whether or not this variable represents profit or loss.This way user’s are able to avoid a fraction of their losses that are lost from truncation. This amount will almost always be negligible, however the loss amount should use `roundUp` division to act in the protocol’s favor.
Recommendation
Use
roundUpdivision when theprofitOrLossvariable represents a loss amount.Resolution
-
ORDM-32 Low Superfluous orderToPositionId Mapping Superfluous Code Acknowledged
Description
The
orderToPositionIdis unnecessary and inefficient as each order can simply have apositionIdas an attribute, for open orders this id can be ignored.Recommendation
Remove the
orderToPositionIdmapping and introduce an attribute on the order struct to store thepositionIdthe order is meant for.Resolution
PariFi Team: Implementing this change would result in a need for additional validations in the contract and other changes in the UI, so we’ve decided to continue using this mapping for now.
No findings match.
More from PariFi
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.
