During the TipRun audit on HackenProof, I found a flaw in the perpetual trading liquidation flow that was easy to misread as a signature issue.
TYPE_LIQUIDATE contained a signed LimitOrder, but that signature belonged to the liquidator. The account being liquidated did not sign the forced terms, which can be completely valid in a liquidation design.
The actual security requirement was different.
If the victim does not consent, the contracts must prove that the account is eligible for liquidation and that the forced terms stay inside an economically safe range.
TipRun did not enforce those conditions before resizing the target account.
I originally submitted the finding as High. HackenProof later validated it as Critical.
The signature protected only one side
The liquidation payload was:
struct Liquidate {
LimitOrder liquidatorOrder;
uint64 liquidatedUid;
int256 actualCollateral;
int256 actualSynthetic;
uint256 actualLiquidatorFee;
}
The only signed object is liquidatorOrder.
That signature can authenticate the liquidator side of the transaction. It does not establish that liquidatedUid is below maintenance, that the target has an exposure that should be reduced, or that the forced collateral amount is consistent with the oracle price.
Those properties require separate on chain checks.
Without them, a valid liquidator signature does not make the forced transition safe for the account being affected.
The liquidation request had no eligibility gate
PerpPlugin decoded TYPE_LIQUIDATE and forwarded it to LiquidateTransLib:
if (txType == TYPE_LIQUIDATE) {
Liquidate memory liquidate = abi.decode(payload, (Liquidate));
CollateralAssetInfo memory collateralInfo = generalConfig.getCollateralAssetInfo();
liquidate.process(
position,
funding,
generalConfig,
order,
signer,
collateralInfo.assetId,
generalConfig.getFeeAccountId(),
transactionProcessor
);
emit LiquidateProcessed(liquidate);
return true;
}
There was no check at this point to determine whether the target account was actually liquidatable.
Inside the library, the victim deltas were derived directly from the submitted values:
int256 liquidatedCollateralDelta = 0;
int256 liquidatedSyntheticDelta = 0;
if (liquidate.liquidatorOrder.isBuyingSynthetic) {
liquidatedCollateralDelta = liquidate.actualCollateral;
liquidatedSyntheticDelta = -liquidate.actualSynthetic;
} else {
liquidatedCollateralDelta = -liquidate.actualCollateral;
liquidatedSyntheticDelta = liquidate.actualSynthetic;
}
Those deltas were then applied to the target account:
position.updatePosition(
liquidatedPosition,
liquidatedUid,
liquidatedCollateralDelta,
syntheticAssetId,
liquidatedSyntheticDelta
);
The path checked basic conditions such as a nonzero target account, collateral asset identity, and whether the synthetic asset was tradable.
The missing checks were the ones that defined whether a forced liquidation was legitimate.
There was no explicit requirement that the original account was below maintenance, no requirement that an existing exposure was being reduced, and no oracle bound on actualCollateral.
Why the generic margin check did not solve it
TipRun already performed generic risk validation around position transitions, so I checked whether that logic indirectly enforced liquidation eligibility.
It did not.
The relevant risk path returned when the updated position was healthy:
(int256 updatedTotalValue, int256 updatedTotalRisk) =
getAccountPositionStatusWithBoundaryCheck(fullUpdated);
if (updatedTotalRisk <= updatedTotalValue * FXP_32_ONE) {
return;
}
That check asks whether the resulting state satisfies a generic margin condition.
Liquidation eligibility asks a different question: was the original account below maintenance before the forced transition?
A healthy final state cannot answer that.
This distinction allowed the liquidation path to accept a forced change against an account that should never have entered liquidation in the first place.
Reproducing the issue through real protocol paths
I built the proof through the real TYPE_TRADE, TYPE_LIQUIDATE, and TYPE_WITHDRAWAL flows.
The victim deposited 1000 collateral and opened a small short through signed orderbook trades.
Immediately before liquidation, the account state was:
Collateral: 1001
Synthetic: -1
Normalized value: 1000
Normalized risk: 1
The account was far above maintenance.
The liquidator then signed its own order, and the authorized batch path submitted a liquidation with:
Synthetic amount: 1
Submitted collateral: 100
Oracle fair collateral: 1
The oracle fair value for one synthetic unit was 1, while the submitted liquidation used 100.
The transaction succeeded.
After the forced transition, the victim had:
Collateral: 901
Synthetic: 0
The liquidator had:
Collateral: 100
Synthetic: -1
Relative to oracle fair value, the victim lost 99 units of normalized value and the liquidator gained 99.
This was the central impact demonstrated by the proof.
A healthy account was forced through liquidation using economic terms that were not bounded by the oracle.
Proving that the value was realizable
I also tested whether the liquidator could externalize the resulting collateral without relying on another vulnerability.
After liquidation, the protocol reported 98 collateral as safely removable from the liquidator account.
The liquidator submitted a valid signed TYPE_WITHDRAWAL for exactly 98.
The withdrawal succeeded, 98 ERC20 units reached the external recipient, and the LoadingZone system balance decreased by the same amount.
The liquidator remained at maintenance afterward:
Collateral: 2
Synthetic: -1
Normalized value: 1
Normalized risk: 1
This demonstrated realizable economic impact while keeping the withdrawal inside the protocol’s normal margin rules.
Liquidation could create exposure instead of reducing it
The missing eligibility logic was not limited to the first scenario.
I also tested a healthy account with collateral and no synthetic exposure:
Collateral: 1000
Synthetic: 0
TYPE_LIQUIDATE still executed.
The resulting state was:
Collateral: 900
Synthetic: 1
Instead of reducing an existing risky position, the liquidation path created a new synthetic exposure.
That behavior showed why a forced liquidation mechanism needs an explicit exposure reduction rule.
Control cases
The proof included controls around the neighboring security boundaries.
A zero liquidatedUid reverted.
A collateral asset mismatch reverted.
A nontradable synthetic asset reverted.
A mutated liquidator order reverted.
A direct caller outside the authorized batch path reverted.
A withdrawal beyond the safely removable amount reverted.
I also tested a genuinely undercollateralized account after a large oracle price movement. A bounded liquidation still succeeded.
That final control was important because the intended fix should preserve valid liquidation while rejecting forced transitions against accounts that are not eligible.
Separate from the earlier deleverage finding
I had previously reported a different vulnerability in TYPE_DELEVERAGE, so I isolated this proof from that issue.
This test never executed TYPE_DELEVERAGE.
It did not depend on the deleverage nonce problem or on deleverage replay.
The affected transaction here was TYPE_LIQUIDATE, the main affected library was LiquidateTransLib, and the liquidator order was validly signed.
The failure was the absence of protections for the third party account receiving the forced state change.
Test results
The full Foundry suite completed successfully:
12 passed
0 failed
0 skipped
The tests covered the healthy account path, oracle price bounds, realizable withdrawal, zero exposure behavior, valid liquidation, guard conditions, arithmetic, and isolation from the deleverage flow.
The proof used local execution through real protocol components. It did not depend on RPC access, governance action, direct mutation of vulnerable state, or external oracle manipulation.
Why the final classification was Critical
I submitted the report as High, while HackenProof ultimately accepted it as Critical.
The final impact went beyond a missing precondition.
The liquidation mechanism could apply unfavorable forced terms to a healthy account, move collateral to the opposite side, and produce value that could be withdrawn through a legitimate protocol path.
It could also create exposure in an account that had no position to liquidate.
That made the issue a realizable collateral loss path inside an authorized protocol operation.
The contest economics
The financial outcome was very different from the technical severity.
The issue was independently reported by 31 researchers, so the Critical reward was shared across the valid submissions.
My payout for this report was $4.84.

The submission fee was $5.
Across the TipRun contest, my total loss was $18.
This individual Critical report therefore did not recover its own submission fee. It is a useful example of how duplicate density and reward sharing can make contest economics very different from technical impact.
How I would fix it
The liquidation flow should remain forced. Requiring the victim to sign would undermine the purpose of liquidation.
Instead, the protocol should validate the forced action before changing the target account.
At minimum, the contracts should require:
The target account is below maintenance before liquidation.
The target has an existing exposure in the direction being reduced.
The liquidation cannot cross the position through zero and create opposite exposure.
actualCollateralstays within an oracle derived range that includes only the configured liquidation penalty.
These conditions should be checked before position.updatePosition() applies the target deltas.
The existing liquidator signature can continue to authenticate the liquidator order. The missing protection is the on chain validation for the account that does not sign.
The broader lesson
Forced protocol actions require explicit invariants.
When user consent is intentionally absent, the contract has to define exactly when the action is allowed and how far it can go.
A valid signature from one side does not authorize arbitrary consequences for another account.
The useful security question is therefore not only:
Who signed this operation?
It is also:
What on chain rule proves that forcing these terms onto the affected account is legitimate?
In TipRun’s liquidation path, that second guarantee was missing.
That gap was enough to turn a liquidation mechanism intended for risk management into a path capable of forcing economically unfair state transitions onto healthy accounts.
Top comments (0)