Compound interest in a Solana program: spl-math PreciseNumber beats f64 and E, and per-period loops exhaust CUs
Problem: computing compound interest on-chain with f64 and std::f64::consts::E gives results that drift from true period-by-period compounding (E is irrational and f64 rounds), while looping over periods with a decimal type is precise but expensive — a few hundred iterations can exhaust the compute budget outright.
What actually works on Solana:
- rust_decimal does compile and run inside an on-chain instruction handler, contrary to the common claim that it is incompatible. Crates depending on it (e.g. switchboard-on-demand) ship on mainnet. It is heavy in compute, though.
- The ecosystem-standard tool is spl-math's PreciseNumber, a fixed-point type backed by a U256, with checked arithmetic and a to_imprecise() to get back to u128. Wrap each input, do the math with PreciseNumber methods, unwrap the result. Raydium and many other programs use it.
Guidance that keeps you inside CU limits: express rates in integer basis points, prefer a closed-form power computation over per-period iteration, and cap the number of periods you will process in a single instruction (or checkpoint interest accrual into account state so each call covers a bounded interval). Always use checked math — silent overflow in fixed point is worse than a failed transaction.
Version caveat: spl-math is still published on crates.io but the repository was archived around January 2025 with no direct Anza replacement, so pin an exact version, and consider vendoring it if you need an auditable dependency. Verify current status before adopting it in 2026.
Source: adapted from a Stack Exchange answer by mikemaccana (question by mikemaccana), https://solana.stackexchange.com/questions/17115/how-should-i-calculate-compound-interest-in-solana-programs — CC BY-SA 4.0
The problem statement mixes up two separate errors. Using E means you are modelling continuous compounding, and that differs from period-by-period compounding by model error, not by rounding: 5% APR daily for 10 years is 3.4e-5 relative, monthly for 1 year is 1.0e-4, 20% daily for 30 years is 1.6e-3. Plain f64 (1+r)^n against a 60-digit Decimal reference is 1.5e-16 relative for 12 periods and 1.4e-13 for 3650 daily periods (about n*eps, because r itself is rounded). That is 8+ orders of magnitude smaller. Checked in Python, whose float ** calls the same libm pow as Rust's powf. The real reasons for fixed point on-chain are different: absolute error grows with the balance (284 base units on a 4e14 balance in the 30-year case), u64 amounts above 2^53 cannot be represented exactly in f64, and you want explicit, auditable rounding direction. The advice itself (integer bps, closed-form pow, checked fixed-point maths) is right; only the stated reason is wrong.