Skip to content

fix(utils): apply the min deposit rate to interest calculations - #38

Open
pucedoteth wants to merge 1 commit into
nadohq:mainfrom
pucedoteth:fix/apply-min-deposit-rate
Open

pucedoteth wants to merge 1 commit into
nadohq:mainfrom
pucedoteth:fix/apply-min-deposit-rate

Conversation

@pucedoteth

Copy link
Copy Markdown

The bug

min_deposit_rate_x18 is parsed into SpotProductConfig:

class SpotProductConfig(NadoBaseModel):
    ...
    min_deposit_rate_x18: str

and then never read anywhere in the SDK. Neither calc_borrow_rate_in_period nor calc_deposit_rate_in_period applies it, so both under-report against what the engine actually accrues.

SpotEngine._updateState converts the rate to a per-second figure, compounds it over dt, and multiplies the result into both multipliers:

int128 minDepositRatePerSecondX18 = minDepositRateX18.div(MathSD21x18.fromInt(31536000));
int128 minDepositRateMultiplierX18 = (ONE + minDepositRatePerSecondX18).pow(int128(dt));

state.cumulativeBorrowsMultiplierX18  = state.cumulativeBorrowsMultiplierX18.mul(minDepositRateMultiplierX18);
state.cumulativeDepositsMultiplierX18 = state.cumulativeDepositsMultiplierX18.mul(minDepositRateMultiplierX18);

With a 5% min deposit rate, a 3.5% borrow curve and 50% utilization:

period SDK engine
1 hour 3.995e-06 9.703e-06
1 day 9.590e-05 2.329e-04
30 days 2.881e-03 7.011e-03

The same problem on the deposit side

calc_deposit_rate_in_period returns 0 when nothing is borrowed:

utilization = calc_utilization_ratio(product)
if utilization == 0:
    return 0

On chain, zero utilization only zeroes the borrower rate — borrowerRateX18 = 0. The minDepositRate block runs regardless and multiplies the deposit multiplier, so depositors still earn the floor while the pool is idle, which is the purpose of a rate floor.

The fix

calc_min_deposit_rate_multiplier reproduces the engine's minDepositRateMultiplier, and both functions apply it multiplicatively the way _updateState does.

The rate is read from product.config, which these functions already receive — the same way calc_borrow_rate_per_second already reads interest_floor_x18 and friends — so there are no signature changes and nothing downstream breaks.

_calc_borrow_multiplier was split out because the deposit rate is derived from the engine's borrowRateMultiplier before the min deposit multiplier is applied; reusing calc_borrow_rate_in_period there would apply the floor twice.

Companion change

The TypeScript SDK has the mirror image of this bug — it adds the annual rate to the period rate instead of omitting it, so it over-reports where this one under-reports. That is nadohq/nado-typescript-sdk#452. After both, the two SDKs agree with the engine and with each other.

Test plan

tests/utils/test_interest.py adds 7 cases pinned to the engine's formula rather than to current output, across an hour, a day, 30 days and a year, plus the idle-pool deposit case.

Reverting only interest.py and keeping the tests fails with the table above:

Obtained: 3.995441563597879e-06     (1 hour, SDK)
Expected: 9.703242916270227e-06     (1 hour, engine)

Obtained: 9.589500359430403e-05     (1 day, SDK)
Expected: 0.0002329038180453047     (1 day, engine)
pytest tests/        # 254 passed
black --check        # 2 files would be left unchanged

ruff check reports I001 (unsorted import block) on nado_protocol/utils/interest.py, but it does so on a clean checkout too — the import block is untouched here, so I left it rather than mixing an unrelated reorder into this diff.

`min_deposit_rate_x18` is parsed into SpotProductConfig and then never read.
Neither calc_borrow_rate_in_period nor calc_deposit_rate_in_period applies it,
so both under-report against the engine.

SpotEngine._updateState converts the rate to a per-second figure, 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, an hour of borrowing came out at 3.995e-06 against
the engine's 9.703e-06, and a day at 9.590e-05 against 2.329e-04.

calc_deposit_rate_in_period also returns 0 when nothing is borrowed. Zero
utilization only zeroes the borrower rate on chain; the min deposit multiplier
is applied either way, so depositors earn the floor on an idle pool.

The rate is read from product.config, which the calculation already receives,
so no signature changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

This branch has not been deployed

No deployments
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