fix(payment): prevent Int64 overflow panic in settle-timestamp arithmetic#287
Merged
Conversation
cd5219e to
5bb7e8d
Compare
…etic UpdateStreamRecord and TryResumeStreamRecord computed settle timestamps by calling Int.Int64() on an unbounded sdkmath.Int. When a stream account's pay-duration exceeded MaxInt64 seconds (~2.9e11 years) the call panicked with "Int64() out of bound". Because both functions run inside the storage EndBlocker with no recover(), the panic was deterministic across every validator and halted the chain. Fix: perform the full settle-timestamp expression in sdkmath.Int, then handle an out-of-range result by intent rather than by call site: - A user-initiated deposit (positive static-balance change, no rate change) that would overflow is rejected with ErrSettleTimestampOverflow — the depositor is over-funding past what we can represent and can deposit less. - Every other path saturates the settle timestamp to math.MaxInt64: forced EndBlocker updates (where an error would re-trigger the same halt via abci.go "should not happen"), rate decreases from object deletion or discontinue, and lazy re-pricing when the SP price falls. The settle timestamp is only a scheduling hint for the auto-settle queue and is recomputed on the next balance change or bucket touch, so capping it is loss-free and never blocks a legitimate object deletion. Closes MOCA-385
The previous commit guarded only the high side of the settle-timestamp arithmetic. Int.Int64() panics on either bound, and UpdateStreamRecord has a route to a hugely negative value: when payDuration <= ForcedSettleTime the code calls ForceSettle and then falls through to compute the settle timestamp from the same (stale, negative) payDuration. A deeply indebted account whose netflow rate collapses while its balance is negative makes that value underflow int64, and the else-branch Int64() panics. The route is reachable only when forced (a non-forced update returns early once payDuration drops below ForcedSettleTime), i.e. inside the no-recover EndBlocker -> chain halt. Replace the high-only check with a 3-way bound: reject a user deposit on overflow (unchanged), saturate to MaxInt64 on any other overflow, and saturate to MinInt64 on underflow (never a deposit; the account is already force-settled, so "overdue" is the correct hint). TryResumeStreamRecord is unaffected: after its resume-floor check the pay duration is provably positive. Relates to MOCA-385
04f3efb to
8886161
Compare
- Hoist the int64 settle-timestamp bounds to package vars (settleTimestampMax/Min) so the UpdateStreamRecord/TryResumeStreamRecord range checks don't re-allocate per call. - TryResumeStreamRecord: clamp the MinInt64 side too — unreachable (the resume guard bounds StaticBalance) but kept for parity with UpdateStreamRecord and a provably in-range Int64() cast; correct the comment to the actual invariant. - Clarify the isUserDeposit classification (forced is set on every EndBlocker path). MOCA-385 Signed-off-by: puneetmahajan <59960662+puneet2019@users.noreply.github.com>
phenix3443
approved these changes
Jul 2, 2026
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.
Linear
https://linear.app/mocanetwork/issue/MOCA-385/prevent-int64-overflow-panic-in-stream-record-updates
What
UpdateStreamRecordandTryResumeStreamRecordcalledInt.Int64()on an unboundedsdkmath.Intto compute the settle timestamp. When a stream account's pay-duration exceededMaxInt64seconds (~2.9e11 years), the SDK panicked with"Int64() out of bound". Both functions run inside the storage EndBlocker with norecover(), so the panic was deterministic across every validator and halted the chain.Why it happens
The settle-timestamp formula is:
payDurationblows pastMaxInt64when|netflowRate|is tiny relative to the balance — reachable when SP prices are low (governance/devnet) and the account is heavily funded, or when a rate decrease (object deletion / discontinue) shrinks the denominator. The storage EndBlocker (ForceDeleteObject → UnChargeObjectStoreFee → UpdateStreamRecord) then panics, and every validator hits aCONSENSUS FAILUREat the same height.Fix
Compute the full expression in arbitrary-precision
sdkmath.Int, then handle an out-of-range result by intent, not by call site:ErrSettleTimestampOverflow. The depositor is over-funding past what we can represent and can simply deposit less. This is the only path where the degenerate state is genuinely user-created.MaxInt64:abci.gopanic("should not happen"));The settle timestamp is only a scheduling hint for the auto-settle queue — recomputed on the next balance change or bucket touch — so capping it is loss-free and never blocks a legitimate object deletion. This avoids the asymmetry of rejecting a user's own
MsgDeleteObject.Why not cap/refund the deposit?
Storage prices are SP-set (median-aggregated, time-versioned, recomputed monthly), and buckets are re-priced on every touch. Prepayment does not lock a price —
staticBalancedrains at the current rate and is withdrawable anytime. So "N years of runway" isn't a stable quantity, and a time-based deposit cap would fight the floating-price model while only ever triggering at the absurd ~2.9e11-year boundary. The overflow is a representation limit, so it gets a representation fix.ErrSettleTimestampOverflow(code 1214) is added tox/payment/types/errors.go.Tests
In the existing
x/payment/keeper/stream_record_test.go(no new files):..._ForcedSaturatessettle = MaxInt64..._UserDepositRejectedErrSettleTimestampOverflow..._RateDecreaseSaturatesMsgDeleteObject/ re-price)settle = MaxInt64(delete never blocked)TestTryResumeStreamRecord_SettleTimestampInt64OverflowErrSettleTimestampOverflow..._SilentWrapsettle = MaxInt64, not negativeChecklist