Description
During a withdrawal flow we have a call to _withdraw() which calls getPnl():
File: contracts/PerpetualVault.sol
1089: function _withdraw(uint256 depositId, bytes memory metadata, MarketPrices memory prices) internal {
1090: uint256 shares = depositInfo[depositId].shares;
1091: if (shares == 0) {
1092: revert Error.ZeroValue();
1093: }
1094:
1095: if (positionIsClosed) {
1096: _handleReturn(0, true, false);
1097: } else if (_isLongOneLeverage(beenLong)) {
1098: uint256 swapAmount = IERC20(indexToken).balanceOf(address(this)) * shares / totalShares;
1099: nextAction.selector = NextActionSelector.SWAP_ACTION;
1100:
1101: nextAction.data = abi.encode(swapAmount, false);
1102: } else if (curPositionKey == bytes32(0)) {
1103: _handleReturn(0, true, false);
1104: } else {
1105: IVaultReader.PositionData memory positionData = vaultReader.getPositionInfo(curPositionKey, prices);
1106: uint256 collateralDeltaAmount = positionData.collateralAmount * shares / totalShares;
1107: uint256 sizeDeltaInUsd = positionData.sizeInUsd * shares / totalShares;
1108:
1109: uint256 feeAmount = vaultReader.getPositionFeeUsd(market, sizeDeltaInUsd, false) / prices.shortTokenPrice.max;
1110:@---> int256 pnl = vaultReader.getPnl(curPositionKey, prices, sizeDeltaInUsd);
1111: if (pnl < 0) {
1112:@---> collateralDeltaAmount = collateralDeltaAmount - feeAmount - uint256(-pnl) / prices.shortTokenPrice.max;
1113: } else {
1114: collateralDeltaAmount = collateralDeltaAmount - feeAmount;
1115: }
1116: uint256 acceptablePrice = abi.decode(metadata, (uint256));
1117: _createDecreasePosition(collateralDeltaAmount, sizeDeltaInUsd, beenLong, acceptablePrice, prices);
1118: }
1119: }
This pnl is deducted from collateralDeltaAmount on L1112.
If we see getPnl():
File: contracts/VaultReader.sol
171: function getPnl(
172: bytes32 key,
173: MarketPrices memory prices,
174: uint256 sizeDeltaUsd
175: ) external view returns (int256) {
176: uint256 sizeInTokens = getPositionSizeInUsd(key);
177: if (sizeInTokens == 0) return 0;
178:
179: PositionInfo memory positionInfo = gmxReader.getPositionInfo(
180: address(dataStore),
181: referralStorage,
182: key,
183: prices,
184:@---> sizeDeltaUsd,
185: address(0),
186:@---> true
187: );
188:
189: return positionInfo.pnlAfterPriceImpactUsd;
190: }
We see that GMX's gmxReader.getPositionInfo() is called with last param as true which if we see in GMX's implementation, corresponds to usePositionSizeAsSizeDeltaUsd. We further see that when usePositionSizeAsSizeDeltaUsd = true, the passed param sizeDeltaUsd is ignored and the entire position size is considered instead:
File: contracts/reader/ReaderPositionUtils.sol
178: function getPositionInfo(
179: DataStore dataStore,
180: IReferralStorage referralStorage,
181: Position.Props memory position,
182: MarketUtils.MarketPrices memory prices,
183: uint256 sizeDeltaUsd,
184: address uiFeeReceiver,
185:@---> bool usePositionSizeAsSizeDeltaUsd
186: ) internal view returns (PositionInfo memory) {
187: if (position.account() == address(0)) {
188: revert Errors.EmptyPosition();
189: }
190:
191: PositionInfo memory positionInfo;
192: GetPositionInfoCache memory cache;
193:
194: positionInfo.position = position;
195: cache.market = MarketStoreUtils.get(dataStore, positionInfo.position.market());
196: cache.collateralTokenPrice = MarketUtils.getCachedTokenPrice(positionInfo.position.collateralToken(), cache.market, prices);
197:
198:@---> if (usePositionSizeAsSizeDeltaUsd) {
199:@---> sizeDeltaUsd = positionInfo.position.sizeInUsd();
200: }
201:
Hence, the withdrawer is burdened with the entire negative PnL instead of their proportional amount.
Impact
Withdrawer takes a greater than expected loss.
Additionally, a few lines later inside _withdraw() we have calls to _createDecreasePosition() --> vaultReader.willPositionCollateralBeInsufficient(..., collateralDeltaAmount) which will now estimate this incorrectly.
Mitigation
getPnl() seems to have been called only from within _withdraw(), so we can modify the function itself:
function getPnl(
bytes32 key,
MarketPrices memory prices,
uint256 sizeDeltaUsd
) external view returns (int256) {
uint256 sizeInTokens = getPositionSizeInUsd(key);
if (sizeInTokens == 0) return 0;
PositionInfo memory positionInfo = gmxReader.getPositionInfo(
address(dataStore),
referralStorage,
key,
prices,
sizeDeltaUsd,
address(0),
- true
+ false
);
return positionInfo.pnlAfterPriceImpactUsd;
}