Asymmetry in profit and loss (PnL) calculations
An asymmetry arises in profit and loss (PnL) calculations due to relative price changes. This discrepancy emerges when adjustments to a position lead to differing PnL outcomes despite equivalent absolute price shifts in rETH, leading to loss of assets.\\nScenario 1\\nAssume at $T0$, the price of rETH is $1000. Bob opened a long position with the following state:\\nPosition Size = 40 ETH\\nMargin = $x$ ETH\\nAt $T2$, the price of rETH increased to $2000. Thus, Bob's PnL is as follows: he gains 20 rETH.\\n```\\nPnL = Position Size * Price Shift / Current Price\\nPnL = Position Size * (Current Price - Last Price) / Current Price\\nPnL = 40 rETH * ($2000 - $1000) / $2000\\nPnL = $40000 / $2000 = 20 rETH\\n```\\n\\nImportant Note: In terms of dollars, each ETH earns $1000. Since the position held 40 ETH, the position gained $40000.\\nScenario 2\\nAssume at $T0$, the price of rETH is $1000. Bob opened a long position with the following state:\\nPosition Size = 40 ETH\\nMargin = $x$ ETH\\nAt $T1$, the price of rETH dropped to $500. An adjustment is executed against Bob's long position, and a `newMargin` is computed to account for the PnL accrued till now, as shown in Line 191 below. Thus, Bob's PnL is as follows: he lost 40 rETH.\\n```\\nPnL = Position Size * Price Shift / Current Price\\nPnL = Position Size * (Current Price - Last Price) / Current Price\\nPnL = 40 rETH * ($500 - $1000) / $500\\nPnL = -$20000 / $500 = -40 rETH\\n```\\n\\nAt this point, the position's `marginDeposited` will be $(x - 40)\\ rETH$ and `lastPrice` set to $500.\\nImportant Note 1: In terms of dollars, each ETH lost $500. Since the position held 40 ETH, the position lost $20000\\n```\\nFile: LeverageModule.sol\\n        // This accounts for the profit loss and funding fees accrued till now.\\n        uint256 newMargin = (marginAdjustment +\\n            PerpMath\\n                ._getPositionSummary({position: position, nextFundingEntry: cumulativeFunding, price: adjustPrice})\\n                .marginAfterSettlement).toUint256();\\n..SNIP..\\n        vault.setPosition(\\n            FlatcoinStructs.Position({\\n                lastPrice: adjustPrice,\\n                marginDeposited: newMargin,\\n                additionalSize: newAdditionalSize,\\n                entryCumulativeFunding: cumulativeFunding\\n            }),\\n            announcedAdjust.tokenId\\n        );\\n```\\n\\nAt $T2$, the price of rETH increases from $500 to $2000. Thus, Bob's PnL is as follows:\\n```\\nPnL = Position Size * Price Shift / Current Price\\nPnL = Position Size * (Current Price - Last Price) / Current Price\\nPnL = 40 rETH * ($2000 - $500) / $500\\nPnL = $60000 / $2000 = 30 rETH\\n```\\n\\nAt this point, the position's `marginDeposited` will be $(x - 40 + 30)\\ rETH$, which is equal to $(x - 10)\\ rETH$. This effectively means that Bob has lost 10 rETH of the total margin he deposited.\\nImportant Note 2: In terms of dollars, each ETH gains $1500. Since the position held 40 ETH, the position gained $60000.\\nImportant Note 3: If we add up the loss of $20000 at 𝑇1 and the gain of $60000 at 𝑇2, the overall PnL is a gain of $40000 at the end.\\nAnalysis\\nThe final PnL of a position should be equivalent regardless of the number of adjustments/position updates made between $T0$ and $T2$. However, the current implementation does not conform to this property. Bob gains 20 rETH in the first scenario, while Bob loses 10 rETH in the second scenario.\\nThere are several reasons that lead to this issue:\\nThe PnL calculation emphasizes relative price changes (percentage) rather than absolute price changes (dollar value). This leads to asymmetric rETH outcomes for the same absolute dollar gains/losses. If we have used the dollar to compute the PnL, both scenarios will return the same correct result, with a gain of $40000 at the end, as shown in the examples above. (Refer to the important note above)\\nThe formula for PnL calculation is sensitive to the proportion of the price change relative to the current price. This causes the rETH gains/losses to be non-linear even when the absolute dollar gains/losses are the same.\\nExtra Example\\nThe current approach to computing the PnL will also cause issues in another area besides the one shown above. The following example aims to demonstrate that it can cause a desync between the PnL accumulated by the global positions AND the PnL of all the individual open positions in the system.\\nThe following shows the two open positions owned by Alice and Bob. The current price of ETH is $1000 and the current time is $T0$\\nAlice's Long Position Bob's Long Position\\nPosition Size = 100 ETH\\nEntry Price = $1000 Position Size = 50 ETH\\nEntry Price = $1000\\nAt $T1$, the price of ETH drops from $1000 to $750, and the `updateGlobalPositionData` function is executed. The `profitLossTotal` is computed as below. Thus, the `marginDepositedTotal` decreased by 50 ETH.\\n```\\npriceShift = $750 - $1000 = -$250\\nprofitLossTotal = (globalPosition.sizeOpenedTotal * priceShift) / price\\nprofitLossTotal = (150 ETH * -$250) / $750 = -50 ETH\\n```\\n\\nAt $T2$, the price of ETH drops from $750 to $500, and the `updateGlobalPositionData` function is executed. The `profitLossTotal` is computed as below. Thus, the `marginDepositedTotal` decreased by 75 ETH.\\n```\\npriceShift = $500 - $750 = -$250\\nprofitLossTotal = (globalPosition.sizeOpenedTotal * priceShift) / price\\nprofitLossTotal = (150 ETH * -$250) / $500 = -75 ETH\\n```\\n\\nIn total, the `marginDepositedTotal` decreased by 125 ETH (50 + 75), which means that the long traders lost 125 ETH from $T0$ to $T2$.\\nHowever, when we compute the loss of Alice and Bob's positions at $T2$, they lost a total of 150 ETH, which deviated from the loss of 125 ETH in the global position data.\\n```\\nAlice's PNL\\npriceShift = current price - entry price = $500 - $1000 = -$500\\nPnL = (position size * priceShift) / current price\\nPnL = (100 ETH * -$500) / $500 = -100 ETH\\n\\nBob's PNL\\npriceShift = current price - entry price = $500 - $1000 = -$500\\nPnL = (position size * priceShift) / current price\\nPnL = (50 ETH * -$500) / $500 = -50 ETH\\n```\\n
Consider tracking the PnL in dollar value/term to ensure consistency between the rETH and dollar representations of gains and losses.\\nAppendix\\nCompared to SNX V2, it is not vulnerable to this issue. The reason is that in SNX V2 when it computes the PnL, it does not "scale" down the result by the price. The PnL in SNXv2 is simply computed in dollar value ($positionSize \\times priceShift$), while FlatCoin protocol computes in collateral (rETH) term ($\\frac{positionSize \\times priceShift}{price}$).\\n```\\nfunction _profitLoss(Position memory position, uint price) internal pure returns (int pnl) {\\n    int priceShift = int(price).sub(int(position.lastPrice));\\n    return int(position.size).multiplyDecimal(priceShift);\\n}\\n```\\n\\n```\\n/*\\n * The initial margin of a position, plus any PnL and funding it has accrued. The resulting value may be negative.\\n */\\nfunction _marginPlusProfitFunding(Position memory position, uint price) internal view returns (int) {\\n    int funding = _accruedFunding(position, price);\\n    return int(position.margin).add(_profitLoss(position, price)).add(funding);\\n}\\n```\\n
Loss of assets, as demonstrated in the second scenario in the first example above. The tracking of profit and loss, which is the key component within the protocol, both on the position level and global level, is broken.
```\\nPnL = Position Size * Price Shift / Current Price\\nPnL = Position Size * (Current Price - Last Price) / Current Price\\nPnL = 40 rETH * ($2000 - $1000) / $2000\\nPnL = $40000 / $2000 = 20 rETH\\n```\\n