fix(shared): compound the min deposit rate over the period - #452
Open
pucedoteth wants to merge 1 commit into
Open
fix(shared): compound the min deposit rate over the period#452pucedoteth wants to merge 1 commit into
pucedoteth wants to merge 1 commit into
Conversation
calcBorrowRateForTimeRange compounds the borrow rate over `seconds` and then
adds `minDepositRate` unchanged. minDepositRate is an annual figure —
SpotProduct.minDepositRate comes from removeDecimals(config.min_deposit_rate_x18)
— so a period rate and an annual rate were being summed.
SpotEngine._updateState does not add it. It converts to a per-second rate,
compounds it over dt, and multiplies the result into both the borrow and the
deposit multiplier:
minDepositRatePerSecondX18 = minDepositRateX18 / 31536000
minDepositRateMultiplierX18 = (ONE + minDepositRatePerSecondX18).pow(dt)
cumulativeBorrowsMultiplierX18 *= minDepositRateMultiplierX18
cumulativeDepositsMultiplierX18 *= minDepositRateMultiplierX18
With a 5% min deposit rate the SDK reported a full year of it no matter how
short the window — 5.0004% for one hour against 0.0010% on chain, and 5.0096%
for one day against 0.0233%. The error disappears at exactly one year, which
is the shape of a missing per-period conversion.
calcRealizedDepositRateForTimeRange had the second half of the same problem:
it returns 0 when nothing is borrowed, but the engine applies the min deposit
multiplier whether or not there are borrows, so depositors earn the floor in
that state rather than nothing.
Both now mirror the engine, and minDepositRate is documented on both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
calcBorrowRateForTimeRangecompounds the borrow rate overseconds, then addsminDepositRateunchanged:minDepositRateis an annual figure —SpotProduct.minDepositRateis populated fromremoveDecimals(product.config.min_deposit_rate_x18)inqueryDataMappers.ts, and it is a field on the sameproductalready being passed in, so it is the natural thing for a caller to hand to this parameter. The two terms are in different units: one is a rate forseconds, the other is a rate for a year.SpotEngine._updateStatedoes not add it. It converts to a per-second rate, compounds it overdt, and multiplies the result into both multipliers:With a 5% min deposit rate, a 3.5% borrow curve and 50% utilization, the SDK reports a full year of the floor no matter how short the window:
The error vanishing at exactly one year is the shape of a missing per-period conversion — a year is the only window where an annual rate added unscaled is nearly right.
The same problem on the deposit side
calcRealizedDepositRateForTimeRangereturns0when nothing is borrowed:On chain, zero utilization only zeroes the borrower rate. The
minDepositRateblock runs regardless and multiplies the deposit multiplier, so depositors still earn the floor when the pool is idle — which is what a rate floor is for. It also added the annual rate to the period rate, the same way the borrow path did.The fix
A shared helper produces the engine's
minDepositRateMultiplier, and both functions apply it multiplicatively the way_updateStatedoes.minDepositRateis now documented with@paramon both — it was the only undocumented parameter on either.calcRealizedDepositRateForTimeRangecallscalcBorrowRateForTimeRange(product, seconds, 0)internally; a zero rate gives a multiplier of exactly 1, so that composition still yields the engine'sborrowRateMultiplier - ONE.Test plan
packages/shared/src/utils/interest.test.tsadds 8 cases pinned to the engine's formula rather than to the current output, covering an hour, a day, 30 days and a year, plus the idle-pool deposit case.Reverting only
interest.tsand keeping the tests fails with exactly the table above:The repo's pre-commit gate (
typecheck,eslint,depcruise,prettier) passes on the commit. Note that a fullbun typecheckonly resolves afterbun run build, since the workspace packages resolve@nadohq/sharedthroughdist/;packages/mobile-client/src/signing/signing.test.tsfails to load for the same reason on a clean checkout, unrelated to this change.Note for the other SDKs
nado-python-sdk'scalc_borrow_rate_in_periodtakes no min deposit rate at all, andcalc_deposit_rate_in_perioddoes not apply one, so it under-reports wherever this SDK over-reported. Happy to send the matching Python change if useful.