DispatchFrom Entry Point
The dispatchFrom function enables third parties to interact with another user's positions for liquidations, force exercises, and long premium settlements. The specific operation is determined by the account's solvency state and the relationship between input position lists.
Overviewβ
function dispatchFrom(
TokenId[] calldata positionIdListFrom,
address account,
TokenId[] calldata positionIdListTo,
TokenId[] calldata positionIdListToFinal,
LeftRightUnsigned usePremiaAsCollateral
) external payable
Parametersβ
| Parameter | Description |
|---|---|
positionIdListFrom | Caller's (msg.sender) current positions |
account | Target account being acted upon |
positionIdListTo | Target account's current positions |
positionIdListToFinal | Expected positions after operation |
usePremiaAsCollateral | Packed flags: leftSlot for caller, rightSlot for target |
Operation Determinationβ
The function determines which operation to execute based on two factors:
- Solvency State: Is the target account solvent at all checked price ticks?
- List Lengths: How do
positionIdListToandpositionIdListToFinalcompare?
Solvency Checkβ
The account is checked at four price points:
int24[] memory atTicks = new int24[](4);
atTicks[0] = spotTick; // 10-minute EMA
atTicks[1] = twapTick; // Weighted TWAP from oracle
atTicks[2] = latestTick; // Most recent observation
atTicks[3] = currentTick; // Current Uniswap tick
solvent = _checkSolvencyAtTicks(
account,
0, // No safe mode override
positionIdListTo,
currentTick,
atTicks,
COMPUTE_PREMIA_AS_COLLATERAL,
NO_BUFFER // No additional margin buffer
);
Decision Matrixβ
| Solvent Count | Final Length | To Length | Operation |
|---|---|---|---|
| 4 (all) | = To Length | N/A | Settle Premium |
| 4 (all) | To Length - 1 | N/A | Force Exercise |
| 0 (none) | 0 | N/A | Liquidation |
| 1-3 (partial) | Any | Any | Revert (NotMarginCalled) |
if (solvent == numberOfTicks) {
// Solvent at all ticks
if (toLength == finalLength) {
_settlePremium(...); // Same length = settle
} else if (toLength == finalLength + 1) {
_forceExercise(...); // One shorter = exercise
} else if (finalLength == 0) {
revert Errors.NotMarginCalled(); // Was meant for liquidation but solvent
}
} else if (solvent == 0) {
// Insolvent at all ticks
if (finalLength != 0) revert Errors.InputListFail();
_liquidate(...);
} else {
// Partially solvent - can't proceed
revert Errors.NotMarginCalled();
}
Price Manipulation Protectionβ
Before any operation, the function validates that the current price hasn't been manipulated:
int256 MAX_TWAP_DELTA_LIQUIDATION = int256(uint256(riskParameters.tickDeltaLiquidation()));
if (Math.abs(currentTick - twapTick) > MAX_TWAP_DELTA_LIQUIDATION)
revert Errors.StaleOracle();
This prevents attackers from:
- Manipulating the current price to trigger unfair liquidations
- Executing force exercises at manipulated prices
Post-Operation Validationβ
Both the target account and caller must remain solvent:
// Validate target account (after operation)
_validateSolvency(
account,
positionIdListToFinal,
NO_BUFFER,
usePremiaAsCollateral.rightSlot() > 0,
0
);
// Validate caller
_validateSolvency(
msg.sender,
positionIdListFrom,
NO_BUFFER,
usePremiaAsCollateral.leftSlot() > 0,
0
);
Liquidation Flow
Liquidation occurs when an account is insolvent at all four checked price ticks. All positions are closed and a bonus is paid to the liquidator.
Function Signatureβ
function _liquidate(
address liquidatee,
TokenId[] calldata positionIdList,
int24 twapTick,
int24 currentTick
) internal
Flow Diagramβ
_liquidate
β
βββΊ Calculate accumulated premia
β ββ _calculateAccumulatedPremia(liquidatee, positionIdList, ...)
β
βββΊ Get margin data from RiskEngine
β ββ riskEngine().getMargin(...)
β
βββΊ Delegate virtual shares to liquidatee
β ββ collateralToken0().delegate(liquidatee)
β ββ collateralToken1().delegate(liquidatee)
β
βββΊ Burn all positions (without committing long premium)
β ββ _burnAllOptionsFrom(liquidatee, ..., DONOT_COMMIT_LONG_SETTLED, ...)
β
βββΊ Calculate liquidation bonus
β ββ riskEngine().getLiquidationBonus(...)
β
βββΊ Process premium haircut (if protocol loss)
β ββ PanopticMath.haircutPremia(...)
β
βββΊ Settle with liquidator
β ββ collateralToken0().settleLiquidation(liquidator, liquidatee, bonus0)
β ββ collateralToken1().settleLiquidation(liquidator, liquidatee, bonus1)
β
βββΊ Emit AccountLiquidated event
Key Stepsβ
1. Premium Calculationβ
(shortPremium, longPremium, positionBalanceArray) = _calculateAccumulatedPremia(
liquidatee,
positionIdList,
COMPUTE_PREMIA_AS_COLLATERAL,
ONLY_AVAILABLE_PREMIUM, // Only settled premium counts
currentTick
);
2. Virtual Share Delegationβ
The protocol delegates virtual shares to ensure the liquidatee has enough balance to settle all position closures:
collateralToken0().delegate(liquidatee); // Adds 2^248 - 1 shares
collateralToken1().delegate(liquidatee);
This is necessary because the liquidatee may not have enough shares to cover the settlement amounts for burning their positions.
3. Position Burningβ
(netPaid, premiasByLeg) = _burnAllOptionsFrom(
liquidatee,
MIN_SWAP_TICK, // No price limits during liquidation
MAX_SWAP_TICK,
DONOT_COMMIT_LONG_SETTLED, // Don't commit long premium yet
positionIdList
);
The DONOT_COMMIT_LONG_SETTLED flag prevents long premium from being committed to storage. This is critical because:
- The premium may need to be haircut if there's protocol loss
- Committing first could allow shorts to withdraw tokens that will later be clawed back
4. Bonus Calculationβ
(bonusAmounts, collateralRemaining) = riskEngine().getLiquidationBonus(
tokenData0,
tokenData1,
Math.getSqrtRatioAtTick(twapTick),
netPaid,
shortPremium
);
The bonus is calculated as:
min(collateralBalance/2, collateralDeficit)- Split proportionally between token0 and token1 based on requirements
- Cross-token substitution applied if one token has surplus
5. Premium Haircutβ
If there's protocol loss, premium owed to the liquidatee is haircut:
LeftRightSigned bonusDeltas = PanopticMath.haircutPremia(
liquidatee,
positionIdList,
premiasByLeg,
collateralRemaining,
collateralToken0(),
collateralToken1(),
Math.getSqrtRatioAtTick(twapTick),
s_settledTokens
);
bonusAmounts = bonusAmounts.add(bonusDeltas);
This ensures PLPs aren't forced to pay out premium to a liquidator who colluded with the liquidatee.
6. Settlementβ
// Native currency support for token0
collateralToken0().settleLiquidation{value: msg.value}(
msg.sender, // liquidator
liquidatee,
bonusAmounts.rightSlot()
);
collateralToken1().settleLiquidation(
msg.sender,
liquidatee,
bonusAmounts.leftSlot()
);
The settleLiquidation function:
- Revokes the delegated virtual shares
- Transfers bonus to liquidator (or from liquidator if negative)
- Handles any protocol loss
Force Exercise Flow
Force exercise allows anyone to close another user's out-of-range long positions in exchange for paying an exercise fee.
Function Signatureβ
function _forceExercise(
address account,
TokenId tokenId,
int24 twapTick,
int24 currentTick
) internal
Prerequisitesβ
// Position must have at least one long leg
if (tokenId.countLongs() == 0) revert Errors.NoLegsExercisable();
Flow Diagramβ
_forceExercise
β
βββΊ Get position data and calculate exercise fee
β ββ positionSize = s_positionBalance[account][tokenId].positionSize()
β ββ exerciseFees = riskEngine().exerciseCost(currentTick, twapTick, tokenId, positionBalance)
β
βββΊ Delegate virtual shares to account
β ββ collateralToken0().delegate(account)
β ββ collateralToken1().delegate(account)
β
βββΊ Burn the position
β ββ _burnOptions(tokenId, positionSize, [MIN_SWAP_TICK, MAX_SWAP_TICK], account, COMMIT_LONG_SETTLED, ...)
β
βββΊ Calculate refund amounts (handle token imbalances)
β ββ riskEngine().getRefundAmounts(account, exerciseFees, twapTick, ct0, ct1)
β
βββΊ Execute refunds between exerciser and account
β ββ collateralToken0().refund(account, msg.sender, refundAmounts.rightSlot())
β ββ collateralToken1().refund(account, msg.sender, refundAmounts.leftSlot())
β
βββΊ Revoke virtual shares
β ββ collateralToken0().revoke(account)
β ββ collateralToken1().revoke(account)
β
βββΊ Emit ForcedExercised event
Key Stepsβ
1. Exercise Fee Calculationβ
exerciseFees = riskEngine().exerciseCost(
currentTick,
twapTick,
tokenId,
positionBalance
);
The exercise cost includes:
- Base fee (higher if position is in-range, lower if far OTM)
- Price differential compensation between current and oracle prices
2. Position Burningβ
int24[2] memory tickLimits;
tickLimits[0] = MIN_SWAP_TICK; // No ITM swapping
tickLimits[1] = MAX_SWAP_TICK;
_burnOptions(
tokenId,
positionSize,
tickLimits,
account,
COMMIT_LONG_SETTLED, // Commit premium (unlike liquidation)
riskParameters
);
Unlike liquidation, force exercise does commit long premium because:
- The account is solvent
- No protocol loss risk
- Normal premium flow should occur
3. Refund Amount Calculationβ
LeftRightSigned refundAmounts = riskEngine().getRefundAmounts(
account,
exerciseFees,
twapTick,
ct0,
ct1
);
If the exercised account lacks sufficient balance in one token:
- The deficit is converted to the other token
- Exerciser receives equivalent value in the surplus token
- Ensures the exercise can complete even with imbalanced holdings
4. Token Transfersβ
// Positive = transfer from account to exerciser
// Negative = transfer from exerciser to account
ct0.refund(account, msg.sender, refundAmounts.rightSlot());
ct1.refund(account, msg.sender, refundAmounts.leftSlot());
Settle Premium Flow
Premium settlement allows third parties to force long position holders to pay their accumulated premium, making it available for short sellers to withdraw.
Function Signatureβ
function _settlePremium(
address owner,
TokenId tokenId,
int24 twapTick,
int24 currentTick
) internal
Flow Diagramβ
_settlePremium
β
βββΊ Delegate virtual shares to owner
β ββ collateralToken0().delegate(owner)
β ββ collateralToken1().delegate(owner)
β
βββΊ Settle options (keep position open)
β ββ _settleOptions(owner, tokenId, positionSize, riskParameters, currentTick)
β
βββΊ Calculate refund amounts
β ββ riskEngine().getRefundAmounts(owner, LeftRightSigned.wrap(0), twapTick, ct0, ct1)
β
βββΊ Execute refunds (caller pays for owner's shortfall)
β ββ collateralToken0().refund(owner, msg.sender, refundAmounts.rightSlot())
β ββ collateralToken1().refund(owner, msg.sender, refundAmounts.leftSlot())
β
βββΊ Revoke virtual shares
ββ collateralToken0().revoke(owner)
ββ collateralToken1().revoke(owner)
Purposeβ
Short sellers need long buyers to pay their accumulated premium before the shorts can withdraw their earnings. If a long holder is neglecting to settle:
- Any third party can call
dispatchFromto force settlement - The long's premium debt is paid
s_settledTokensis increased, making premium available to shorts- The long position remains open
Key Stepsβ
1. Position Validationβ
uint128 positionSize = s_positionBalance[owner][tokenId].positionSize();
if (positionSize == 0) revert Errors.PositionNotOwned();
2. Premium Settlementβ
_settleOptions(owner, tokenId, positionSize, riskParameters, currentTick);
This calls _updateSettlementPostBurn with flags to:
- Commit long premium to
s_settledTokens - Keep the position open
- Update premium accumulator snapshots
3. Balance Redistributionβ
LeftRightSigned refundAmounts = riskEngine().getRefundAmounts(
owner,
LeftRightSigned.wrap(0), // No exercise fees
twapTick,
ct0,
ct1
);
If the owner lacks sufficient collateral in one token:
- The caller covers the shortfall
- Caller receives equivalent value in the other token
- This incentivizes settlement when the owner has imbalanced collateral
Summary
| Operation | Trigger | Account State | Position Effect |
|---|---|---|---|
| Liquidation | Insolvent at all ticks | Insolvent | All positions closed |
| Force Exercise | Final list one shorter | Solvent | Single position closed |
| Settle Premium | Same list lengths | Solvent | Position remains open |
| Operation | Caller Pays | Caller Receives | Account Effect |
|---|---|---|---|
| Liquidation | Nothing (or negative bonus) | Liquidation bonus | Positions closed, collateral claimed |
| Force Exercise | Exercise fee | Position closure | Position closed, receives fee |
| Settle Premium | Token shortfall | Surplus token | Premium debt paid |