Liquidation slippage should measure unavoidable execution loss.
It should not authorize a liquidator to transfer borrower collateral to themselves and fund the debt repayment from somewhere else.
Rujira Ghost Credit validated liquidation through changes in the account’s total collateral and debt values.
The check compared:
spent_usd
Collateral value that left the account
repaid_usd
Debt value that disappeared
It then treated the difference as slippage:
slippage =
(spent_usd - repaid_usd)
÷
spent_usd
and accepted the liquidation when:
slippage
<=
liquidation_max_slip
The arithmetic bounded the difference between collateral outflow and debt reduction.
It did not prove that the collateral outflow produced the tokens used for repayment.
That missing link allowed two independent value flows to satisfy the same validator:
Borrower collateral could be sent to a liquidator-controlled destination
Debt tokens could be supplied externally and then used by Repay
As long as the final LTV remained inside the required band and the delta stayed within liquidation_max_slip, the transaction could pass.
A parameter intended to tolerate market execution loss therefore became an additional liquidation incentive.
Code4rena classified the finding as High.
It was one of my three High findings in the Rujira contest. Across the contest, I had three High and three Medium findings and earned $252.39 in total.
The intended invariant
A liquidation may legitimately lose some value through:
Swap price impact
Router fees
Market movement
Rounding
Execution slippage
That is why a protocol may need a nonzero liquidation_max_slip.
But the correct invariant is stronger than a final balance comparison:
Value counted as liquidation slippage must come from a verified conversion of borrower collateral into assets used to reduce borrower debt.
Rujira checked the final deltas without validating that causal relationship.
The protocol knew that both collateral and debt had decreased.
It did not know whether one decrease funded the other.
How the validator measured liquidation
CreditAccount::validate_liquidation() compared the account before and after liquidation.
Conceptually, it calculated:
spent =
old collateral value
-
new collateral value
and:
repaid =
old debt value
-
new debt value
When repaid was lower than spent, the difference was divided by spent and treated as slippage.
When repaid was equal to or greater than spent, slippage was treated as zero.
The validator also checked that the account began above the liquidation threshold and ended within the required post-liquidation LTV band.
Those checks constrained the resulting account state.
They did not constrain:
The recipient of collateral outflows
The contract receiving LiquidateMsg::Execute funds
The source of debt tokens used for repayment
Whether any swap actually occurred
Whether swap proceeds returned to the credit account
This made the validation source-blind.
Arbitrary execution created the collateral outflow
The liquidation caller could provide:
LiquidateMsg::Execute {
contract_addr,
msg,
funds,
}
That message allowed the credit account to call a contract and forward funds during liquidation.
Without a destination or semantic restriction, the operation could move collateral to:
A liquidator-controlled contract
A custom adapter controlled by the liquidator
Another arbitrary contract address
For the later slippage check, all of these transfers appeared as collateral that had been “spent.”
The validator did not distinguish a legitimate swap from a direct value transfer.
Repayment could use externally supplied debt tokens
The liquidation Repay step used the account’s balance of the selected debt denom as repayment input after the relevant fee handling.
The code did not track how those tokens entered the account.
This allowed the liquidator to ensure that debt tokens were present from an external source before the repay step executed.
The resulting flow could be:
1. Send borrower collateral out through LiquidateMsg::Execute
2. Ensure debt tokens from an external source are present in the account
3. Execute LiquidateMsg::Repay
4. Reduce borrower debt with those external tokens
5. Keep the collateral difference allowed by the slip check
The collateral outflow and debt repayment were economically independent.
The validator treated them as one liquidation conversion because their net values moved in the expected directions.
The hidden bonus formula
Let:
s =
liquidation_max_slip
spent_usd =
collateral value removed
repaid_usd =
debt value reduced
The liquidation passes when:
repaid_usd
>=
(1 - s) × spent_usd
For a fixed repayment amount, the maximum collateral outflow is:
spent_usd
=
repaid_usd
÷
(1 - s)
The extra value removed beyond the debt reduction is:
hidden_bonus =
spent_usd - repaid_usd
Substituting the maximum allowed outflow:
hidden_bonus
=
repaid_usd × s
÷
(1 - s)
This distinction matters.
The gap is bounded by s relative to collateral outflow.
Relative to the debt repaid, the extra extraction is:
s
÷
(1 - s)
Ten percent max slip
With:
liquidation_max_slip = 10%
the validator permits repayment to cover only 90 percent of the collateral outflow.
For a repayment of 10 USD, the mathematical maximum outflow is approximately:
11.11 USD
The extra extraction can therefore approach:
11.11%
of the repaid amount.
The proof used integer values:
spent_usd = 11
repaid_usd = 10
The validator calculated:
slippage =
(11 - 10)
÷
11
approximately 9.09%
That remained below the configured 10 percent limit.
The liquidation passed while one additional USD unit of borrower collateral was not matched by debt reduction.
Twenty percent max slip
With:
liquidation_max_slip = 20%
the extra extraction relative to repayment can reach:
20%
÷
80%
=
25%
A larger tolerance may be operationally attractive during volatile markets.
Under this accounting model, the same change also raises the amount a liquidator can intentionally extract.
A liveness parameter silently becomes a compensation parameter.
Why this is not normal slippage
A legitimate swap-loss example looks like:
100 USD of collateral enters an approved swap
98 USD of debt token returns
98 USD of debt is repaid
2 USD is lost to execution
The vulnerable model could also accept:
11 USD of collateral is sent to the liquidator
10 USD of debt token comes from an external source
10 USD of debt is repaid
The validator records 9.09% slippage
Both states produce a collateral decrease and a debt decrease.
Only the first represents market execution loss.
The validator could not distinguish them.
What the proof of concept demonstrated
The submitted PoC was a local, deterministic reproduction of the relevant liquidation validation logic.
It modeled:
The initial liquidation-threshold check
The final LTV-band checks
Collateral and debt deltas
The liquidation_max_slip comparison
It did not execute the complete Ghost Credit liquidation stack.
That limitation is important.
The PoC proves that the validator accepts the malicious accounting result.
The separate contract paths described in the report establish the enabling primitives:
LiquidateMsg::Execute can forward account funds
LiquidateMsg::Repay can use debt tokens present in the account
Together, those findings show why the accepted state is reachable.
PoC configuration
The model used:
Adjustment threshold
70%
Liquidation threshold
80%
Liquidation max slip
10%
The initial account was:
Collateral
100
Debt
81
LTV
81%
The account therefore started above the 80 percent liquidation threshold.
The final account was:
Collateral
89
Debt
71
LTV
approximately 79.78%
The final LTV was:
Below the 80% liquidation threshold
At or above the 70% adjustment threshold
The post-liquidation band checks passed.
PoC result
The state transition produced:
spent_usd
100 - 89
=
11
repaid_usd
81 - 71
=
10
Using integer basis-point arithmetic, the slippage was:
slippage_bp
=
909
or approximately:
9.09%
The configured maximum was:
1,000 basis points
=
10%
The result passed.
Expected output:
old: collateral_usd=100, debt_usd=81
new: collateral_usd=89, debt_usd=71
spent_usd=11
repaid_usd=10
slippage_bp=909
hidden_bonus_usd=1
result=PASS
The example was intentionally small.
The security property does not depend on the absolute numbers.
It shows that the validator accepts borrower collateral loss that is not required to produce the debt repayment.
Why the LTV band did not prevent extraction
The final-state checks controlled how far the liquidator could move the account.
They did not validate the path used to reach that state.
A liquidator could choose values that simultaneously satisfied:
The account started liquidatable
The account ended below the liquidation threshold
The account remained above the adjustment threshold
The slippage delta stayed within liquidation_max_slip
The PoC demonstrated one such transition.
A valid final ratio does not prove a fair liquidation.
Impact on borrower economics
Rujira already had explicit liquidation economics.
The contest material documented a 0.5 percent liquidator fee in addition to other protocol fees.
liquidation_max_slip was intended as execution tolerance, not as another reward.
The vulnerable accounting allowed a liquidator to optimize for:
Explicit liquidation fees
plus
The maximum value gap accepted as slippage
Borrower losses could therefore exceed the amount implied by the fee schedule.
The hidden extraction was bounded, but it could be materially larger than the explicit incentive.
Misaligned incentives
A correct liquidation system should reward liquidators for:
Reducing unsafe debt
Executing efficiently
Preserving remaining borrower value
Following an explicit fee model
The vulnerable system rewarded a different strategy.
A liquidator could increase profit by maximizing collateral outflow up to the permitted slip boundary while sourcing repayment elsewhere.
The protocol’s economic check encouraged the behavior it was intended to prevent.
Severity
Code4rena classified the finding as High.
The issue affected liquidation integrity and could cause material borrower loss beyond the intended fee structure.
The impact scaled with:
Debt being repaid
Collateral available
Configured liquidation_max_slip
The final LTV constraints
The liquidator could not remove unlimited collateral.
The flow remained bounded by the account state, protocol fees, debt reduction, and liquidation validation.
The High impact came from converting an execution-tolerance parameter into an intentional extraction mechanism during a forced borrower liquidation.
Primary fix: replace arbitrary execution with a liquidation swap primitive
LiquidateMsg::Execute should not function as an unrestricted collateral-transfer mechanism.
A dedicated liquidation swap message can enforce:
Approved router or adapter
Approved collateral input denom
Required debt-token output denom
Minimum output
Recipient fixed to the credit account
No arbitrary collateral recipient
A narrow operation gives the protocol enough information to validate the value flow.
Bind swap output to repayment
The protocol should measure debt tokens produced by approved liquidation operations.
A practical design can snapshot the account’s debt-token balance before the swap and count only verified increases returned by an approved adapter.
Repayment credit should not automatically include:
Pre-existing debt-token balances
Externally injected tokens
Unrelated transfers
External funding may still be allowed, but it must be accounted for separately and must not justify collateral outflow.
Classify every collateral outflow
Every amount leaving the credit account during liquidation should belong to a recognized category:
Approved swap input
Protocol fee
Liquidator fee
Solver fee
Authorized refund
Any other recipient should be rejected.
The validator must know why collateral left, not merely how much left.
Separate execution loss from liquidation rewards
When the protocol intends to pay an additional solver spread, it should use an explicit parameter:
liquidation_bonus
Intentional and tightly capped
liquidation_max_slip
Applied only to verified swap loss
This makes borrower costs predictable and governance changes understandable.
Increasing swap tolerance should not automatically increase liquidator compensation.
A stronger validation model
Source-aware liquidation accounting should track:
Collateral sent to approved adapters
Debt tokens returned by those adapters
Debt actually repaid
Explicit configured fees
Unauthorized collateral outflows
Then enforce:
Unauthorized outflow
=
0
Verified execution loss
<=
liquidation_max_slip
Explicit rewards
<=
configured fee schedule
That validates economic causation rather than only final deltas.
Regression tests
A complete patch should verify:
Collateral sent to an arbitrary destination is rejected
Approved swap adapters remain usable
Swap output must return to the credit account
External debt-token funding is not credited as swap output
Repayment is linked to verified liquidation proceeds
Explicit fees are accounted for separately
Slippage applies only to approved swap execution
Raising
liquidation_max_slipdoes not increase arbitrary extractionFinal LTV checks continue to hold
Multiple collateral and debt denoms remain correctly attributed
Pre-existing debt-token balances follow an explicit policy
Failed liquidation leaves balances unchanged
The central invariant is:
Borrower collateral may leave during liquidation only when the protocol attributes it to verified debt reduction or an explicit fee.
Broader audit lessons
Net deltas do not prove causation
Balances moving in the expected directions do not prove that one movement funded the other.
Economic validation must link sources and destinations.
Generic execution expands the trust boundary
An arbitrary call primitive can invalidate assumptions made by downstream accounting.
Every destination and fund path becomes part of the security model.
External funding can spoof economic productivity
A debt-token balance does not reveal where the tokens came from.
The protocol must not infer successful collateral conversion from balance presence alone.
Tolerance can become profit
Any permitted loss can become an incentive when the actor controls the destination of that loss.
Execution tolerance and compensation must be separate.
Final-state validation is insufficient
A safe-looking final LTV does not prove that the liquidation treated the borrower fairly.
The path matters.
Conclusion
Rujira Ghost Credit compared collateral outflow with debt reduction to enforce liquidation_max_slip.
The validator did not require the collateral to produce the debt tokens used for repayment.
A liquidator could therefore direct collateral through LiquidateMsg::Execute, provide debt tokens from an external source, repay the debt, and retain the value gap accepted as slippage.
The PoC showed that a transition removing 11 USD units of collateral while reducing debt by 10 USD units produced 909 basis points of slippage.
With a 10 percent limit, every modeled validation check passed.
The tolerance intended for market execution had become a hidden liquidation bonus.
Code4rena classified the finding as High.

The engineering rule is simple:
Slippage must measure verified swap loss, not authorize arbitrary collateral extraction.

Top comments (0)