diff --git a/src/UsdnProtocol/libraries/UsdnProtocolActionsLongLibrary.sol b/src/UsdnProtocol/libraries/UsdnProtocolActionsLongLibrary.sol index 9be4737e1..1214dafcc 100644 --- a/src/UsdnProtocol/libraries/UsdnProtocolActionsLongLibrary.sol +++ b/src/UsdnProtocol/libraries/UsdnProtocolActionsLongLibrary.sol @@ -243,7 +243,7 @@ library UsdnProtocolActionsLongLibrary { data.liqPriceWithoutPenalty = Utils._getLiquidationPrice(data.startPrice, maxLeverage); // find corresponding tick and actual liq price with current penalty setting maxLeverageData.currentLiqPenalty = s._liquidationPenalty; - (maxLeverageData.newPosId.tick, data.liqPriceWithoutPenalty) = Long._getTickFromDesiredLiqPrice( + (maxLeverageData.newPosId.tick, data.liqPriceWithoutPenaltyNorFunding) = Long._getTickFromDesiredLiqPrice( data.liqPriceWithoutPenalty, data.action.liqMultiplier, s._tickSpacing, @@ -253,12 +253,12 @@ library UsdnProtocolActionsLongLibrary { // retrieve the actual penalty for this tick we want to use maxLeverageData.liquidationPenalty = Long.getTickLiquidationPenalty(maxLeverageData.newPosId.tick); // check if the penalty for that tick is different from the current setting - // if the penalty is the same, then `data.liqPriceWithoutPenalty` is already correct + // if the penalty is the same, then `data.liqPriceWithoutPenaltyNorFunding` is already correct if (maxLeverageData.liquidationPenalty != maxLeverageData.currentLiqPenalty) { - // the tick's imposed penalty is different from the current setting, so the `liqPriceWithoutPenalty` we - // got above can't be used to calculate the leverage - // we must instead use the tick's penalty to find the new `liqPriceWithoutPenalty` and calculate the - // total exposure + // the tick's imposed penalty is different from the current setting, so the + // `liqPriceWithoutPenaltyNorFunding` we got above can't be used to calculate the leverage + // we must instead use the tick's penalty to find the new `liqPriceWithoutPenaltyNorFunding` and + // calculate the total exposure // note: In case the tick liquidation penalty is lower than the current setting, it might lead to a // leverage that exceeds the max leverage slightly. We allow this behavior in this rare occurrence @@ -266,22 +266,29 @@ library UsdnProtocolActionsLongLibrary { // retrieve exact liquidation price without penalty // we consider the liquidation multiplier as it was during the initiation, to ignore any funding // that was due between the initiation and the validation - data.liqPriceWithoutPenalty = Utils._getEffectivePriceForTick( + data.liqPriceWithoutPenaltyNorFunding = Utils._getEffectivePriceForTick( Utils._calcTickWithoutPenalty(maxLeverageData.newPosId.tick, maxLeverageData.liquidationPenalty), data.action.liqMultiplier ); } + // recalculate the liquidation price of the tick + data.liqPriceWithoutPenalty = Utils._getEffectivePriceForTick( + Utils._calcTickWithoutPenalty(maxLeverageData.newPosId.tick, maxLeverageData.liquidationPenalty) + ); + // must calculate before removing the position from the tick which affects the trading expo + uint128 liqPriceWithPenalty = Utils._getEffectivePriceForTick(maxLeverageData.newPosId.tick); + // move the position to its new tick, update its total exposure, and return the new tickVersion and index // remove position from old tick completely Long._removeAmountFromPosition( data.action.tick, data.action.index, data.pos, data.pos.amount, data.pos.totalExpo ); - // if the last price is below the liquidation price without penalty of the new position, we are unable to - // calculate the new position's value - // this is extremely unlikely, but we have no other choice but to liquidate if it happens - if (data.lastPrice <= data.liqPriceWithoutPenalty) { + // if the last price is below the liquidation price with penalty of the new position, the position is + // already underwater and we might be unable to calculate the new position value if we are further below + // `liqPriceWithoutPenalty` + if (data.lastPrice <= liqPriceWithPenalty) { s._balanceLong -= data.oldPosValue; s._balanceVault += data.oldPosValue; // position was already removed from the tick above @@ -294,14 +301,14 @@ library UsdnProtocolActionsLongLibrary { index: data.action.index }), data.lastPrice, - data.liqPriceWithoutPenalty + liqPriceWithPenalty ); return (false, true, Types.PositionId({ tick: Constants.NO_POSITION_TICK, tickVersion: 0, index: 0 })); } // update position total exposure (because of new leverage / liq price) data.pos.totalExpo = - Utils._calcPositionTotalExpo(data.pos.amount, data.startPrice, data.liqPriceWithoutPenalty); + Utils._calcPositionTotalExpo(data.pos.amount, data.startPrice, data.liqPriceWithoutPenaltyNorFunding); // mark the position as validated data.pos.validated = true; // insert position into new tick diff --git a/test/unit/UsdnProtocol/Actions/ValidateOpenPosition.t.sol b/test/unit/UsdnProtocol/Actions/ValidateOpenPosition.t.sol index 7c041b4b4..b0ff8f0df 100644 --- a/test/unit/UsdnProtocol/Actions/ValidateOpenPosition.t.sol +++ b/test/unit/UsdnProtocol/Actions/ValidateOpenPosition.t.sol @@ -343,6 +343,8 @@ contract TestUsdnProtocolActionsValidateOpenPosition is UsdnProtocolBaseFixture * @custom:and That tick has a higher price than after the initiate */ function test_validateOpenPositionMaxLeverageFunding() public { + TestData memory testData; + // set aggressive funding vm.prank(ADMIN); protocol.setFundingSF(10 ** Constants.FUNDING_SF_DECIMALS); @@ -358,32 +360,53 @@ contract TestUsdnProtocolActionsValidateOpenPosition is UsdnProtocolBaseFixture abi.encode(CURRENT_PRICE), EMPTY_PREVIOUS_DATA ); - - (PendingAction memory pendingAction,) = protocol.i_getPendingAction(address(this)); - LongPendingAction memory longPendingAction = protocol.i_toLongPendingAction(pendingAction); - uint128 firstTickPrice = protocol.getEffectivePriceForTick(posId.tick); - uint128 validationPrice = CURRENT_PRICE - 100 ether; - uint128 newLiqPrice = protocol.i_getLiquidationPrice(validationPrice, uint128(protocol.getMaxLeverage())); + testData.validatePrice = CURRENT_PRICE - 100 ether; + testData.originalLiqPenalty = protocol.getLiquidationPenalty(); + uint128 newLiqPrice = protocol.i_getLiquidationPrice(testData.validatePrice, uint128(protocol.getMaxLeverage())); - (int24 validationTick,) = protocol.i_getTickFromDesiredLiqPrice( - newLiqPrice, longPendingAction.liqMultiplier, protocol.getTickSpacing(), protocol.getLiquidationPenalty() - ); + uint128 liqPriceWithoutPenaltyNorFunding; + { + (PendingAction memory pendingAction,) = protocol.i_getPendingAction(address(this)); + uint256 initLiqMultiplier = (protocol.i_toLongPendingAction(pendingAction)).liqMultiplier; + (testData.validateTick, liqPriceWithoutPenaltyNorFunding) = protocol.i_getTickFromDesiredLiqPrice( + newLiqPrice, initLiqMultiplier, protocol.getTickSpacing(), testData.originalLiqPenalty + ); + } + assertLt(testData.validateTick, posId.tick, "tick"); - assertLt(validationTick, posId.tick, "tick"); - PositionId memory newPos = PositionId(validationTick, 0, 0); - uint128 newTickPriceBefore = protocol.getEffectivePriceForTick(validationTick); + // estimate the value of the new position for later comparison + uint256 newPosTotalExpo = protocol.i_calcPositionTotalExpo( + uint128(LONG_AMOUNT), testData.validatePrice, liqPriceWithoutPenaltyNorFunding + ); + uint128 liqPriceWithoutPenalty = protocol.getEffectivePriceForTick( + protocol.i_calcTickWithoutPenalty(testData.validateTick, testData.originalLiqPenalty), + testData.validatePrice, + protocol.longTradingExpoWithFunding(testData.validatePrice, uint128(block.timestamp + 24)), + protocol.getLiqMultiplierAccumulator() + ); + testData.expectedPosValue = + uint256(protocol.i_positionValue(uint128(newPosTotalExpo), testData.validatePrice, liqPriceWithoutPenalty)); // validation _waitDelay(); - + uint128 newTickPrice = protocol.getEffectivePriceForTick(testData.validateTick); + testData.tempPosId = PositionId(testData.validateTick, 0, 0); vm.expectEmit(); - emit LiquidationPriceUpdated(posId, newPos); - protocol.validateOpenPosition(payable(this), abi.encode(validationPrice), EMPTY_PREVIOUS_DATA); + emit LiquidationPriceUpdated(posId, testData.tempPosId); + protocol.validateOpenPosition(payable(this), abi.encode(testData.validatePrice), EMPTY_PREVIOUS_DATA); // check that all ticks have now a higher price - assertGt(protocol.getEffectivePriceForTick(validationTick), newTickPriceBefore, "new tick price"); + assertGt(protocol.getEffectivePriceForTick(testData.validateTick), newTickPrice, "new tick price"); assertGt(protocol.getEffectivePriceForTick(posId.tick), firstTickPrice, "first tick price"); + + // check the value of the tick is as calculated during the max leverage re-calculation + assertEq( + // - 1 because we are at T+25 but calculations were done for T+24 + protocol.getPositionValue(testData.tempPosId, testData.validatePrice, uint128(block.timestamp - 1)), + int256(testData.expectedPosValue), + "new position value" + ); } /**