Skip to content

nova's degen pair is 500.001x, and two decimals is what hid it - #3171

Closed
Flotapponnier wants to merge 4 commits into
devfrom
fix/nova-degen-500001
Closed

Flotapponnier wants to merge 4 commits into
devfrom
fix/nova-degen-500001

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

Follow-up to #3168, found by reading the deployed gauge rather than by re-reading my own code.

perp_leverage_max published 500.001 for Nova where Gains reads 500. Raw integers at 1e3:

min max
gains 300/313 500000 500000 min == max, genuinely fixed
nova 300/313 500000 500001 a band one thousandth of a multiple wide

My own comparison script printed both as 500.00 because it formatted to two decimals. The difference was in the data from the very first read and the display hid it, so three things shipped wrong in #3168:

  • the model carried maxLeverage: 500 for a row whose gauge says 500.001, which had the perp-cost tool and bench 295 disagreeing about one venue
  • the row called itself "x500 degen, fixed, same terms as Gains". It is neither: its bounds do not coincide, so it is not fixed, and its ceiling is not Gains'. Now "x500 degen, Gains' terms at 500.001x"
  • the bench formula claimed "leverage bounds identical to Gains". The standard pair's are; the degen pair's maximum is not

The test was the problem too

It asserted the two maxima were equal, and passed, because both rows carried the same number I had written by hand. That is the failure mode worth naming: a test comparing two values I typed confirms I was consistent, not that I was right.

It now pins 500 and 500.001 separately, asserts that Gains' bounds coincide while Nova's do not, and checks the row no longer calls itself fixed. On the harness side the same two figures are checked against the contract under OCB_LIVE=1, so the source of truth is the chain rather than my transcription.

The live test's log prints leverage to three decimals now, because the contract stores it at 1e3 and rounding to two is exactly what this cost.

Does it matter?

Nobody trades the difference between 500x and 500.001x. It is carried because the published gauge carries it, so a row saying 500 makes two of our own pages contradict each other, and because a fork that edited one digit of one parameter edited it deliberately.

Gate: validate 253 specs, typecheck, lint at its 9 pre-existing warnings, test 712 pass, go build/vet/test green, both live tests run.

… copied

I said Nova could not go on bench 295 or the perp-cost tool because it
lists neither BTC nor ETH. That was wrong, and wrong for a reason worth
recording: I had read app.nova.markets/api/markets, which is the
Hyperliquid-routed FRONTEND list. The gTrade deployment is a different
thing and its pair 0 is BTC/USD, pair 1 ETH/USD.

All five selectors the leverage source uses answer on Nova's diamond, so
the reader takes the venue and the diamond as arguments instead of
hardcoding Gains', the same shape as the volume source. Measured on
chain 2026-10-22, both deployments, both markets:

  standard pair   1.1x-200x, threshold 90/75 between 25x and 60x
                  gains 0.035% a leg   nova 0.050% a leg
  x500 degen      500x fixed, threshold 80% flat
                  gains 0.020% a leg   nova 0.020% a leg

So it is one contract priced twice: identical thresholds and bounds, a
dearer standard pair. The fee sits inside the liquidation trigger, so
Nova leaves strictly less room at every leverage (10x: 8.90% against
8.93%; 200x: 0.275% against 0.305%).

THE NEAR MISS. perp-cost-model carries a spreadBps per row, and Gains'
is 1.0. Copying it would have looked right beside the identical
thresholds and been wrong: word 7 of pairs() is the pair spread at 1e10
and reads 100000000 on Gains but 0 on Nova. Nova is 5.0 bps a leg, not
5.5, and the earlier "43% dearer" I quoted from the taker fee alone is
really 25% once the spread is counted on both sides. The file's own
header says two venues were left out rather than guessed at; this is why.

Nova's figures were read a day after everything else in that file, so
rows can now carry their own measuredAt and the footer no longer claims
one date for figures gathered on two.

Found while editing bench 295, both pre-existing:

- Its methodology and Gains' formula said the bench reads "pairs 300 and
  313, the x500 cohort rather than the standard pairs". It reads all
  four: gainsPairs is {BTC: [0, 300], ETH: [1, 313]} and the publisher
  picks per leverage tab. Corrected, and split in two because the
  parameters and the pair list are separate facts.
- The prose said "four venues" in six places and, worse, claimed
  "Papertrade leaves the most room and Gains the least". With Nova that
  ordering is false. Both fixed.

Cache keys: all three move this time. Bench 295 declares dimensions, so
bench-filters matters here where it did not for 041 and 116, and a
provider missing from it is invisible on every tab. The cache-key test
did not know that key existed; it does now.

Tests: the two deployments agree on leverage and disagree on fee, the
degen rows are identical as their label claims, Nova's spread is zero and
not Gains', Nova leaves less room at every shared leverage, and the pair
INDICES are checked against each venue's own registry rather than trusted
to stay aligned, because a wrong index would publish another asset's
terms plausibly. Each verified against a mutation, including the copied
spread.
Review of my own previous commit. fetchGains became fetchGtrade but its
helpers kept the Gains name, so the code read s.gainsCall(novaDiamond,
...) and gtradePairs was still gainsPairs while being the pair map for
both deployments. Half a rename misleads more than either option.

24 references, compiler-verified. gainsArbRPC and gainsDiamondArb keep
their names because they really are about Gains: one is the RPC that
harness has always used, the other is Gains' own diamond address.
Audit of my own change, three things on the perp-cost page.

"Nine of the ten products below charge a share of your notional" is now
eleven of twelve.

The meta description said "ten perp venues". The page listed ten
PRODUCTS across nine venues, so it was already wrong, and Nova makes it
accidentally right at ten venues and twelve products. A count that is
only correct by coincidence is worse than no count, so it names no
number now.

And the footer gives every venue a paragraph saying how its figures were
read, which left Nova's two rows marked "chain" with no account of what
that meant. Nova now has its own, including the thing worth writing
down: word 7 of pairs() is the pair spread at 1e10 and reads zero on
Nova where Gains reads 0.0100%, so the row carries zero rather than the
neighbouring value. Copying it would have put Nova at 5.5 bps a leg
instead of 5.0 and looked right beside the identical thresholds.

The existing Gains paragraph already documented that 0.0100% pair
spread, which is independent confirmation that word 7 is the field I
read rather than my own inference about it.
Found by reading the gauge after deploying: perp_leverage_max published
500.001 for Nova where Gains reads 500. Raw integers at 1e3:

  gains 300/313   min 500000  max 500000    min == max, genuinely fixed
  nova  300/313   min 500000  max 500001    a band one thousandth wide

My own comparison script printed both as "500.00" because it formatted
to two decimals. The difference was in the data from the first read and
the display hid it, so three things shipped wrong in #3168:

- the model carried maxLeverage 500 for a row whose gauge says 500.001,
  which had this page and bench 295 disagreeing about one venue
- the row called itself "fixed, same terms as Gains". It is neither: its
  bounds do not coincide, so it is not fixed, and its ceiling is not
  Gains'. Now "x500 degen, Gains' terms at 500.001x"
- the bench formula claimed "leverage bounds identical to Gains". The
  standard pair's are; the degen pair's maximum is not

And the test asserted the two maxima were EQUAL. It passed because both
rows carried the same wrong number, which is the failure mode worth
naming: a test comparing two values I wrote by hand confirms I was
consistent, not that I was right. It now pins 500 and 500.001
separately, asserts Gains' bounds coincide while Nova's do not, and the
harness checks the same two figures against the contract on OCB_LIVE.

The test log prints leverage to three decimals now, since the contract
stores it at 1e3 and rounding to two is the thing that cost this.

Nobody trades the difference. It is carried because the published gauge
carries it, and because a fork that edited one digit of one parameter
edited it on purpose.
@Flotapponnier

Copy link
Copy Markdown
Collaborator Author

Superseded by a branch off current dev: this one predates the squash merge of #3168, so its diff is inflated and it rebases into conflict. Same three commits, cleanly applied, in the replacement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant