Skip to content

Commit efd3fba

Browse files
authored
fix(validate-open): liquidate the position if start price is below liqPriceWithoutPenaltyNorFunding (#766)
* fix(validate-open): liquidate the position if start price is below liqPriceWithoutPenaltyNorFunding * fix(validate-open): use the right liq price for the effective price of the liq position event * docs(validate-open): add comment to explain new condition * test: fix typo in NatSpecs of new test * test: fix incorrect NatSpecs in tests * test: fix incorrect comment in new test * docs(actions-long): remove innacurate comment in prepare validate open function
1 parent 70a38c2 commit efd3fba

File tree

2 files changed

+96
-17
lines changed

2 files changed

+96
-17
lines changed

src/UsdnProtocol/libraries/UsdnProtocolActionsLongLibrary.sol

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -590,12 +590,24 @@ library UsdnProtocolActionsLongLibrary {
590590
data_.oldPosValue =
591591
Utils._positionValueOptimized(data_.pos.totalExpo, data_.lastPrice, data_.liqPriceWithoutPenalty);
592592

593-
// if lastPrice > liqPriceWithPenalty but startPrice <= liqPriceWithPenalty then the user dodged liquidations
594-
// we still can't let the position open, because we can't calculate the leverage with a start price that is
595-
// lower than a liquidation price, and we also can't liquidate the whole tick because other users could have
596-
// opened positions in this tick after the user of the current position
593+
data_.liqPriceWithoutPenaltyNorFunding = Utils._getEffectivePriceForTick(
594+
Utils._calcTickWithoutPenalty(data_.action.tick, data_.liquidationPenalty), data_.action.liqMultiplier
595+
);
596+
597+
// if lastPrice > liqPriceWithPenalty
598+
// but startPrice <= liqPriceWithPenalty OR startPrice <= liqPriceWithoutPenaltyNorFunding,
599+
// then the user dodged liquidations. We still can't let the position open, because we can't calculate the
600+
// leverage with a start price that is lower than a liquidation price, and we also can't liquidate the whole
601+
// tick because other users could have opened positions in this tick after the user of the current position,
597602
// our only choice is to liquidate this position only
598-
if (data_.startPrice <= liqPriceWithPenalty) {
603+
if (data_.startPrice <= liqPriceWithPenalty || data_.startPrice <= data_.liqPriceWithoutPenaltyNorFunding) {
604+
uint256 liquidationPrice = liqPriceWithPenalty;
605+
// if the liquidation occurs because of liqPriceWithoutPenaltyNorFunding, use it as the effective price for
606+
// the liquidation event
607+
if (data_.startPrice > liqPriceWithPenalty && data_.startPrice <= data_.liqPriceWithoutPenaltyNorFunding) {
608+
liquidationPrice = data_.liqPriceWithoutPenaltyNorFunding;
609+
}
610+
599611
s._balanceLong -= data_.oldPosValue;
600612
s._balanceVault += data_.oldPosValue;
601613

@@ -611,18 +623,14 @@ library UsdnProtocolActionsLongLibrary {
611623
index: data_.action.index
612624
}),
613625
data_.startPrice,
614-
liqPriceWithPenalty
626+
liquidationPrice
615627
);
616628

617629
return (data_, true);
618630
}
619631

620-
data_.liqPriceWithoutPenaltyNorFunding = Utils._getEffectivePriceForTick(
621-
Utils._calcTickWithoutPenalty(data_.action.tick, data_.liquidationPenalty), data_.action.liqMultiplier
622-
);
623632
// calculate the leverage of the position without considering the penalty nor the funding by using the
624633
// multiplier state at T+24
625-
// reverts if liqPriceWithoutPenaltyNorFunding >= startPrice
626634
data_.leverage = Utils._getLeverage(data_.startPrice, data_.liqPriceWithoutPenaltyNorFunding);
627635
}
628636

test/unit/UsdnProtocol/Actions/_PrepareValidateOpenPositionData.t.sol

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,7 @@ contract TestUsdnProtocolActionsPrepareValidateOpenPositionData is UsdnProtocolB
187187
* below its position's liquidation price
188188
* @custom:given Partial liquidations occurred that left the user's tick un-liquidated
189189
* @custom:when The user tries to validate its position
190-
* @custom:then Nothing happens
191-
* @custom:when The lastPrice value is updated and the user tries to validate again
192-
* @custom:then The position was not liquidated and matching data is returned
190+
* @custom:then The position is liquidated
193191
*/
194192
function test_prepareValidateOpenPositionDataWithStartPriceLowerThanLiquidationPrice() public {
195193
skip(1 hours);
@@ -211,8 +209,9 @@ contract TestUsdnProtocolActionsPrepareValidateOpenPositionData is UsdnProtocolB
211209
protocol.i_prepareValidateOpenPositionData(pendingAction, currentPriceData);
212210

213211
uint24 liquidationPenalty = protocol.getLiquidationPenalty();
214-
uint256 positionTotalExpo =
215-
protocol.i_calcPositionTotalExpo(POSITION_AMOUNT, params.initialPrice, liqPriceWithoutPenalty);
212+
uint256 positionTotalExpo = protocol.i_calcPositionTotalExpo(
213+
POSITION_AMOUNT, params.initialPrice, data.liqPriceWithoutPenaltyNorFunding
214+
);
216215

217216
/* ------------------------ checking returned values ------------------------ */
218217
assertTrue(liquidated, "The position should have been liquidated");
@@ -259,12 +258,84 @@ contract TestUsdnProtocolActionsPrepareValidateOpenPositionData is UsdnProtocolB
259258
);
260259
}
261260

261+
/**
262+
* @custom:scenario A user wants to validate its action but the provided price is not fresh and the lastPrice is
263+
* below its position's liquidation price (without liq penalty nor fundings)
264+
* @custom:given `startPrice` is above the liquidation price but below the liquidation price without fundings
265+
* @custom:and fundings are enabled
266+
* @custom:when The user tries to validate its position
267+
* @custom:then The position is liquidated
268+
*/
269+
function test_prepareValidateOpenPositionDataWithStartPriceLowerThanLiquidationPriceWithoutPenaltyNorFunding()
270+
public
271+
{
272+
skip(1 hours);
273+
vm.startPrank(ADMIN);
274+
protocol.setFundingSF(500);
275+
vm.stopPrank();
276+
277+
// big position to have high fundings
278+
setUpUserPositionInVault(USER_1, ProtocolAction.ValidateDeposit, 20 ether, DEFAULT_PARAMS.initialPrice);
279+
280+
(, posId) = protocol.initiateOpenPosition(
281+
POSITION_AMOUNT,
282+
params.initialPrice * 8 / 10,
283+
type(uint128).max,
284+
protocol.getMaxLeverage(),
285+
address(this),
286+
USER_1,
287+
type(uint256).max,
288+
currentPriceData,
289+
EMPTY_PREVIOUS_DATA
290+
);
291+
timestampAtInitiate = uint40(block.timestamp);
292+
293+
skip(4 hours);
294+
295+
// update lastPrice and lastUpdatedTimestamp
296+
protocol.liquidate(currentPriceData);
297+
298+
liqPriceWithoutPenalty = protocol.getEffectivePriceForTick(protocol.i_calcTickWithoutPenalty(posId.tick));
299+
(pendingAction,) = protocol.i_getPendingAction(USER_1);
300+
uint24 liquidationPenalty = protocol.getLiquidationPenalty();
301+
uint256 liqPriceWithoutPenaltyNorFunding = protocol.i_getEffectivePriceForTick(
302+
protocol.i_calcTickWithoutPenalty(posId.tick, liquidationPenalty), pendingAction.var6
303+
);
304+
305+
// price below the liquidation price of the initiated position
306+
uint256 price = protocol.getEffectivePriceForTick(posId.tick) + 10 ether;
307+
currentPriceData = abi.encode(price);
308+
309+
vm.expectEmit();
310+
emit LiquidatedPosition(USER_1, posId, price, liqPriceWithoutPenaltyNorFunding);
311+
(ValidateOpenPositionData memory data, bool liquidated) =
312+
protocol.i_prepareValidateOpenPositionData(pendingAction, currentPriceData);
313+
314+
/* ------------------------------ sanity checks ----------------------------- */
315+
assertGt(
316+
data.liqPriceWithoutPenaltyNorFunding,
317+
liqPriceWithoutPenalty,
318+
"liqPriceWithoutPenaltyNorFunding should be higher than liqPriceWithoutPenalty"
319+
);
320+
assertLt(data.liqPriceWithoutPenalty, price, "liqPriceWithoutPenalty should be less than the current price");
321+
assertGt(
322+
data.liqPriceWithoutPenaltyNorFunding,
323+
price,
324+
"liqPriceWithoutPenaltyNorFunding should be higher than the current price"
325+
);
326+
327+
/* ------------------------ checking returned values ------------------------ */
328+
assertTrue(liquidated, "The position should have been liquidated");
329+
assertFalse(data.isLiquidationPending, "There should not be any pending liquidations");
330+
}
331+
262332
/// @notice Assert the data in ValidateOpenPositionData depending on `isEarlyReturn`
263333
function _assertData(ValidateOpenPositionData memory data, bool isEarlyReturn) private view {
264334
uint128 currentPrice = abi.decode(currentPriceData, (uint128));
265335
uint24 liquidationPenalty = protocol.getLiquidationPenalty();
266-
uint256 positionTotalExpo =
267-
protocol.i_calcPositionTotalExpo(POSITION_AMOUNT, params.initialPrice, liqPriceWithoutPenalty);
336+
uint256 positionTotalExpo = protocol.i_calcPositionTotalExpo(
337+
POSITION_AMOUNT, params.initialPrice, data.liqPriceWithoutPenaltyNorFunding
338+
);
268339
uint128 liqPriceWithoutPenaltyNorFunding = protocol.i_getEffectivePriceForTick(
269340
protocol.i_calcTickWithoutPenalty(data.action.tick, data.liquidationPenalty), data.action.liqMultiplier
270341
);

0 commit comments

Comments
 (0)