Skip to content

fix(open-max-leverage): fix inaccuracies in the new pos value and total expo calculations #905

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions src/UsdnProtocol/libraries/UsdnProtocolActionsLongLibrary.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -253,35 +253,42 @@ 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

// 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
Expand All @@ -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
Expand Down
55 changes: 39 additions & 16 deletions test/unit/UsdnProtocol/Actions/ValidateOpenPosition.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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"
);
}

/**
Expand Down