3

I am doing the exercise from the Solana lending protocol video from YouTube, and would like to calculate compound interest, ensuring:

  • Good floating point accuracy
  • My program works within CU limits

Using E versus iterating

  • E is an irrational number and a float and will therefore be less precise. The results below show slightly different results for E versus iterating using Decimals.
  • Iterating seems to be more CU intensive - if I try say 500 periods instead of 120 I'll get a CU error.

Using Decimal versus f64

While the example uses f64, I believe Decimal to be more accurate. But does Decimal math even work on Solana? An LLM (I know, it may have made this up) says:

While Rust is the primary language for Solana programs, rust_decimal isn't directly compatible with Solana's environment [1]. Solana programs typically use the standard Rust types for numerical operations, and integrating a library like rust_decimal would require significant adaptation.

However at least on localhost I was able to get the Decimal crate running.

What's the best way to calculate compound interest on Solana?

I have a working program showing the differences in each approach. It's possible there may be other approaches I should consider.

interest_rates.rs

use std::f64::consts::E;

use rust_decimal::prelude::ToPrimitive;
use rust_decimal::{Decimal, RoundingStrategy};

pub fn get_final_balance_using_decimal(
    initial_balance: u64,
    elapsed_periods: u64,
    interest_rate_basis_points: u64,
) -> u64 {
    let initial_balance_decimal: Decimal = Decimal::from(initial_balance);
    let interest_rate_decimal: Decimal =
        Decimal::from(interest_rate_basis_points) / Decimal::from(10_000);

    let mut final_balance = initial_balance_decimal;
    for _ in 0..elapsed_periods {
        final_balance = final_balance
            .checked_add(
                final_balance
                    .checked_mul(interest_rate_decimal)
                    .unwrap()
                    .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven),
            )
            .unwrap();
    }
    final_balance.to_u64().unwrap()
}

pub fn get_final_balance_using_e(
    initial_balance: u64,
    elapsed_periods: u64,
    interest_rate_basis_points: u64,
) -> u64 {
    let multiplier =
        E.powf(interest_rate_basis_points as f64 * elapsed_periods as f64 / 10_000 as f64);
    (initial_balance as f64 * multiplier) as u64
}

lib.rs

mod interest_rates;
use interest_rates::{get_final_balance_using_decimal, get_final_balance_using_e};

use anchor_lang::prelude::*;
declare_id!("5S8qdNZWGD2wpcg4fEmgutHgvnCjYZezamirN5TdJh9g");

#[program]
pub mod test_decimals {

    use super::*;

    pub fn compare_interest_techniques(_ctx: Context<CompareInterestTechniques>) -> Result<()> {
        const INITIAL_BALANCE: u64 = 100_000;
        // 120 OK, 500 will kill the Decimal technique
        const PERIODS: u64 = 120;
        const INTEREST_RATE_BASIS_POINTS: u64 = 2;
        let final_balance_using_decimal =
            get_final_balance_using_decimal(INITIAL_BALANCE, PERIODS, INTEREST_RATE_BASIS_POINTS);
        let final_balance_using_e =
            get_final_balance_using_e(INITIAL_BALANCE, PERIODS, INTEREST_RATE_BASIS_POINTS);
        msg!(
            "Final balances using decimal: {:?} and using E: {:?}",
            final_balance_using_decimal,
            final_balance_using_e
        );
        Ok(())
    }
}

#[derive(Accounts)]
pub struct CompareInterestTechniques {}

test-decimals.ts

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { TestDecimals } from "../target/types/test_decimals";
import { getLogs } from "@solana-developers/helpers";
import { expect } from "chai";

describe("test_decimals", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const connection = anchor.getProvider().connection;

  const program = anchor.workspace.TestDecimals as Program<TestDecimals>;

  it("Logs the different interest calculations!", async () => {
    const tx = await program.methods.compareInterestTechniques().rpc();
    const logs = await getLogs(connection, tx);
    console.log("Your transaction signature", tx);
    console.log("Your logs", logs);
  });
});

Solana logs

  test_decimals
Your transaction signature dqZMFHam3YxcmYBGh8xcnyBXaWXhuKo9nkjQYa4tuAm4ey89mNEucFHkW9aG6rQCvKyThk1rW9icaiRay3ir5VZ
Your logs [
  'Program 5S8qdNZWGD2wpcg4fEmgutHgvnCjYZezamirN5TdJh9g invoke [1]',
  'Program log: Instruction: CompareInterestTechniques',
  'Program log: Final balances using decimal: 102428 and using E: 102429',
  'Program 5S8qdNZWGD2wpcg4fEmgutHgvnCjYZezamirN5TdJh9g consumed 63666 of 200000 compute units',
  'Program 5S8qdNZWGD2wpcg4fEmgutHgvnCjYZezamirN5TdJh9g success'
]

1 Answer 1

4

The problem with floating point numbers is that they lose precision the bigger or smaller the number. In solana land, I would not use them. Instead, I'd use something like this (copy and paste implementation): https://github.com/solana-labs/solana-program-library/blob/master/libraries/math/src/precise_number.rs

As for using rust_decimal, I would feel very hesitant using a plug and play third party library for calculations in a Solana Program. It's probably fine, but for mission critical code, I personally would not use it and handroll something. Or clone the lib into whatever repo I was working on and leave out the dependancy.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.