← All posts

Splitting a range down the middle was always wrong

Splitting a Uniswap range used to happen at the geometric midpoint and nowhere else. Some ranges could not be split at all, and the failure said nothing.

Architecture3 min readDeFiLoops

Splitting a liquidity position means turning one range into two that meet somewhere in the middle. It is how you put more liquidity near the price and less out at the edges without closing anything.

Our first version of it always cut at the geometric midpoint. You named the position, and the split happened exactly halfway. That looked like a sensible default and it was two bugs wearing a convenience.

Bug one: the default was a strategy decision

Cutting halfway produces two equal children. That is a fine thing to want and a strange thing to be given automatically, because the reason to split a range is usually that you do not want the two halves to be equal.

You are concentrating liquidity where the price is and thinning it where it is not. An equal cut is the one case where the split has changed nothing about the shape of your exposure — it has only doubled your position count.

Bug two: some ranges could not be split, and said nothing

Uniswap ranges are not continuous. They are built from ticks, and legal ticks are multiples of the pool’s spacing — which differs per fee tier, from 1 at the lowest through 10, 60 and 200 at the highest.

So the midpoint of a range is only a legal place to cut when the range spans an even number of spacings. Span an odd number and the exact middle lands between two ticks, where no position can begin or end.

Even number of spacings
The midpoint is a real tick. The split works
Odd number of spacings
The midpoint falls between two ticks. The position manager refuses it from inside the pool — with EMPTY returndata

That second row is the part that made it a bad failure rather than a limitation. Empty returndata means no error code, no message, no reason: the call comes back having refused, with nothing to read. From the outside it is indistinguishable from a dozen other problems, and nothing in the message tells you the fix is pick a different tick.

What changed

The tick is now a required parameter with no default at all. You name where the two children meet, and the constraint is stated rather than assumed: it must be a multiple of the pool’s spacing, and it must lie strictly inside the range you are splitting.

Two consequences worth knowing:

  • Odd-span ranges are splittable now. There was never anything wrong with them. The midpoint was the wrong question to ask.
  • A one-spacing range can never be split. Not a bug, arithmetic: no tick lies strictly inside it. This matters because repositioning a range around the current price leaves it exactly one spacing wide, so a position that has just been recentred is not a candidate.

The levered version, and the same lesson

There are two split operations: one for a position you hold outright, and one for a position that has been borrowed against. The levered one does the same work inside a flash loan — each child re-borrows after it is minted, so the loan can be repaid in the same transaction — and it carried the identical midpoint assumption, so it needed the identical fix.

Both also refuse a zero on the unwind minimums, because a burn settles at whatever the pool’s price is at that instant and those minimums are the only bound on it. That one is not about convenience either.

Why write this up

Because the interesting failure is not that a default was wrong. It is that a default made a decision nobody was asked about, and then, in the cases where it could not be honoured, declined to say so.