The trader owed $600 in fees.
He had $250 left to pay them with.
The protocol took the $250, marked the bill settled, then paid him $450 on top.
He should have walked away with $100.
He walked away with $450.
Quick refresh so we’re on the same page.
A perp lets you hold a leveraged long or short with no expiry date.
You post collateral, the protocol marks profit and loss against it, and you sit there forever.
Forever is the problem.
Nothing drags a perp’s price back to spot the way an expiry does.
So perps use funding, a steady payment between the two sides:
• Longs crowded → longs pay shorts.
• Shorts crowded → shorts pay longs.
• It flips whenever the crowd flips.
So funding is sometimes money you owe, and sometimes money you’re owed.
Four words you’ll need:
• Margin - the collateral you posted. Fees come out of it.
• PnL - profit and loss, applied to margin before anything else.
• Funding income - funding owed to you, when you’re on the paid side.
• Liquidation check - the function that decides if you can stay open.
Now the close path.
First pass: you read the settlement code. Every line does what it says.
Second pass: you read the liquidation check.
Also fine. Deliberate, correct, even commented.
Both files pass review. Separately.
You never ask whether the two of them are counting the same money.
“It’s a fee deduction. What could go wrong?”
Read it in order.
The clamp on line 2 is the whole thing.
Fees can only take what’s there.
A $600 bill against $250 collects $250, and the other $350 is never written down anywhere.
Nothing carries it forward. Nothing nets it against what comes next.
And what comes next is that last line. The full funding payment, landing after the books already closed.
Now the liquidation check, same protocol.
Look at that middle branch.
Fees exceed margin, the trader is receiving funding, and the check subtracts the shortfall from funding income and keeps going.
There’s even a comment on it.
Somebody sat down, thought about exactly the case where fees run past collateral while funding is owed to the trader, decided funding should absorb it, and wrote that down.
Then the settlement path was written as if that conversation never happened.
Walk the numbers.
Margin after PnL is $250.
Fees owed are $600.
Funding owed to the trader is $450.
Settlement clamps to $250, takes it, moves on.
The $350 remainder isn’t deferred. It isn’t netted. It’s gone.
Then funding credits on its own line. The full $450.
$450 out, where $100 was correct.
The gap is whichever is smaller: the funding they’re owed, or the fees that went uncollected.
A clamp that stops an underflow also forgives the difference.
The clamp itself is correct. Without it the split runs past the balance and the transaction aborts.
The solvency branch is correct too.
If someone owes you $450, you’re not insolvent over a $350 gap.
And the same ordering repeats across every exit path the protocol has.
Three call sites agreeing reads like a decision.
Consistency isn’t correctness.
Here is the whole thing as one close:
No flash loan. No oracle. No reentrancy.
You close a position at the moment funding is paying you and your fees have already eaten your collateral.
And it repeats. Every close that meets those two conditions.
The money comes out of the LP pool.
Depositors pay the funding, and they eat the fees the protocol never collected.
When you audit a settlement path, check:
Most auditors check that each function is correct on its own.
Few check that two functions share the same model of what money is.
This isn’t a missing check, and that’s exactly why it survives.
The usual version of this finding is “fee not charged” or “wrong account authorised”.
Something is absent, you grep for it, you find it.
Here nothing is absent.
Fees are charged. The cap is deliberate.
The solvency model is deliberate, correct, and commented.
The bug lives between two files that were each reviewed on a different day.
Root cause: settlement clamps then credits, solvency sums then compares.
Two fixes.
Do the arithmetic once. Margin, funding, fees, one number at the end, and clamp that number instead of the fee line.
Make the liquidation check call that same function, so the two can’t drift apart again.
The second one is what stops it coming back.
Arsen





