DEV Community

Daniel
Daniel

Posted on

How Rujira’s Slippage Guard Could Become a Hidden Liquidation Bonus

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
Enter fullscreen mode Exit fullscreen mode

It then treated the difference as slippage:

slippage =
(spent_usd - repaid_usd)
÷
spent_usd
Enter fullscreen mode Exit fullscreen mode

and accepted the liquidation when:

slippage
<=
liquidation_max_slip
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and:

repaid =
old debt value
-
new debt value
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This made the validation source-blind.

Arbitrary execution created the collateral outflow

The liquidation caller could provide:

LiquidateMsg::Execute {
    contract_addr,
    msg,
    funds,
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The liquidation passes when:

repaid_usd
>=
(1 - s) × spent_usd
Enter fullscreen mode Exit fullscreen mode

For a fixed repayment amount, the maximum collateral outflow is:

spent_usd
=
repaid_usd
÷
(1 - s)
Enter fullscreen mode Exit fullscreen mode

The extra value removed beyond the debt reduction is:

hidden_bonus =
spent_usd - repaid_usd
Enter fullscreen mode Exit fullscreen mode

Substituting the maximum allowed outflow:

hidden_bonus
=
repaid_usd × s
÷
(1 - s)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Ten percent max slip

With:

liquidation_max_slip = 10%
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The extra extraction can therefore approach:

11.11%
Enter fullscreen mode Exit fullscreen mode

of the repaid amount.

The proof used integer values:

spent_usd = 11

repaid_usd = 10
Enter fullscreen mode Exit fullscreen mode

The validator calculated:

slippage =
(11 - 10)
÷
11

approximately 9.09%
Enter fullscreen mode Exit fullscreen mode

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%
Enter fullscreen mode Exit fullscreen mode

the extra extraction relative to repayment can reach:

20%
÷
80%

=
25%
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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%
Enter fullscreen mode Exit fullscreen mode

The initial account was:

Collateral
100

Debt
81

LTV
81%
Enter fullscreen mode Exit fullscreen mode

The account therefore started above the 80 percent liquidation threshold.

The final account was:

Collateral
89

Debt
71

LTV
approximately 79.78%
Enter fullscreen mode Exit fullscreen mode

The final LTV was:

Below the 80% liquidation threshold

At or above the 70% adjustment threshold
Enter fullscreen mode Exit fullscreen mode

The post-liquidation band checks passed.

PoC result

The state transition produced:

spent_usd
100 - 89
=
11

repaid_usd
81 - 71
=
10
Enter fullscreen mode Exit fullscreen mode

Using integer basis-point arithmetic, the slippage was:

slippage_bp
=
909
Enter fullscreen mode Exit fullscreen mode

or approximately:

9.09%
Enter fullscreen mode Exit fullscreen mode

The configured maximum was:

1,000 basis points
=
10%
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then enforce:

Unauthorized outflow
=
0

Verified execution loss
<=
liquidation_max_slip

Explicit rewards
<=
configured fee schedule
Enter fullscreen mode Exit fullscreen mode

That validates economic causation rather than only final deltas.

Regression tests

A complete patch should verify:

  1. Collateral sent to an arbitrary destination is rejected

  2. Approved swap adapters remain usable

  3. Swap output must return to the credit account

  4. External debt-token funding is not credited as swap output

  5. Repayment is linked to verified liquidation proceeds

  6. Explicit fees are accounted for separately

  7. Slippage applies only to approved swap execution

  8. Raising liquidation_max_slip does not increase arbitrary extraction

  9. Final LTV checks continue to hold

  10. Multiple collateral and debt denoms remain correctly attributed

  11. Pre-existing debt-token balances follow an explicit policy

  12. 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)