skills/multiversx/mx-ai-skills/multiversx-defi-math

multiversx-defi-math

SKILL.md

MultiversX DeFi Math Patterns

Reusable mathematical patterns for financial safety in MultiversX smart contracts.

Precision Management

Choosing Precision Levels

pub const BPS_PRECISION: usize = 4;       // Basis points: 10,000 = 100%
pub const BPS: u64 = 10_000;

pub const PPM_PRECISION: usize = 6;       // Parts per million: 1,000,000 = 100%
pub const PPM: u64 = 1_000_000;

pub const WAD_PRECISION: usize = 18;      // Standard token decimals: 1e18 = 1.0
pub const WAD: u128 = 1_000_000_000_000_000_000;

pub const RAY_PRECISION: usize = 27;      // High-precision: 1e27 = 1.0
pub const RAY: u128 = 1_000_000_000_000_000_000_000_000_000;
Level Decimals When to Use
BPS (4) 10,000 = 100% Fees, simple percentages, reserve factors
PPM (6) 1,000,000 = 100% Fine-grained percentages, partial withdrawals
WAD (18) 1e18 = 1.0 Token amounts, prices, share ratios
RAY (27) 1e27 = 1.0 Interest indices, compounding rates, any math needing minimal precision loss

Rule of thumb: Use the lowest precision that avoids rounding errors in your domain. For intermediate calculations, always use a higher precision than the final result.

Rounding Strategies

Why Half-Up Rounding?

Standard ManagedDecimal operations truncate (round toward zero). Over many operations, this causes systematic value loss. In DeFi, this means the protocol slowly leaks value, and attackers can exploit it with dust deposits.

Unsigned Half-Up Multiplication

fn mul_half_up(
    &self,
    a: &ManagedDecimal<Self::Api, NumDecimals>,
    b: &ManagedDecimal<Self::Api, NumDecimals>,
    precision: NumDecimals,
) -> ManagedDecimal<Self::Api, NumDecimals> {
    let scaled_a = a.rescale(precision);
    let scaled_b = b.rescale(precision);
    let product = scaled_a.into_raw_units() * scaled_b.into_raw_units();
    let scaled = BigUint::from(10u64).pow(precision as u32);
    let half_scaled = &scaled / &BigUint::from(2u64);
    let rounded_product = (product + half_scaled) / scaled;
    self.to_decimal(rounded_product, precision)
}

Unsigned Half-Up Division

fn div_half_up(
    &self,
    a: &ManagedDecimal<Self::Api, NumDecimals>,
    b: &ManagedDecimal<Self::Api, NumDecimals>,
    precision: NumDecimals,
) -> ManagedDecimal<Self::Api, NumDecimals> {
    let scaled_a = a.rescale(precision);
    let scaled_b = b.rescale(precision);
    let scaled = BigUint::from(10u64).pow(precision as u32);
    let numerator = scaled_a.into_raw_units() * &scaled;
    let denominator = scaled_b.into_raw_units();
    let half_denominator = denominator / &BigUint::from(2u64);
    let rounded_quotient = (numerator + half_denominator) / denominator;
    self.to_decimal(rounded_quotient, precision)
}

Signed Away-From-Zero Rounding

For signed values (e.g., PnL, price deltas), round AWAY from zero to prevent systematic bias:

fn mul_half_up_signed(
    &self,
    a: &ManagedDecimalSigned<Self::Api, NumDecimals>,
    b: &ManagedDecimalSigned<Self::Api, NumDecimals>,
    precision: NumDecimals,
) -> ManagedDecimalSigned<Self::Api, NumDecimals> {
    let scaled_a = a.rescale(precision);
    let scaled_b = b.rescale(precision);
    let product = scaled_a.into_raw_units() * scaled_b.into_raw_units();
    let scaled = BigInt::from(10i64).pow(precision as u32);
    let half_scaled = &scaled / &BigInt::from(2i64);

    let rounded_product = if product.sign() == Sign::Minus {
        (product - half_scaled) / scaled  // More negative
    } else {
        (product + half_scaled) / scaled  // More positive
    };
    ManagedDecimalSigned::from_raw_units(rounded_product, precision)
}

Safe Rescaling

Converting between precision levels with half-up rounding (standard rescale truncates):

fn rescale_half_up(
    &self,
    value: &ManagedDecimal<Self::Api, NumDecimals>,
    new_precision: NumDecimals,
) -> ManagedDecimal<Self::Api, NumDecimals> {
    let old_precision = value.scale();
    match new_precision.cmp(&old_precision) {
        Ordering::Equal => value.clone(),
        Ordering::Less => {
            // Downscaling — rounding matters
            let precision_diff = old_precision - new_precision;
            let factor = BigUint::from(10u64).pow(precision_diff as u32);
            let half_factor = &factor / 2u64;
            let rounded = (value.into_raw_units() + &half_factor) / factor;
            ManagedDecimal::from_raw_units(rounded, new_precision)
        },
        Ordering::Greater => value.rescale(new_precision), // Upscaling — no rounding needed
    }
}

Percentage Calculations

Framework Built-in: proportion()

The MultiversX framework provides BigUint::proportion(part, total) for percentage math. This is the preferred approach:

// BigUint::proportion(numerator, denominator) — built-in framework method
let fee = amount.proportion(fee_percent, PERCENT_BASE_POINTS);

Common base point constants used in production:

pub const PERCENT_BASE_POINTS: u64 = 100_000;  // 100% = 100_000 (5-digit precision)
pub const BPS: u64 = 10_000;                     // 100% = 10_000 (basis points)
pub const PPM: u64 = 1_000_000;                  // 100% = 1_000_000 (parts per million)

Using proportion() for Fees

multiversx_sc::imports!();

pub const PERCENT_BASE_POINTS: u64 = 100_000;

/// Apply a percentage fee using framework's proportion()
fn calculate_fee(&self, amount: &BigUint, fee_percent: u64) -> BigUint {
    amount.proportion(fee_percent, PERCENT_BASE_POINTS)
}

/// Amount after deducting fee
fn amount_after_fee(&self, amount: &BigUint, fee_percent: u64) -> BigUint {
    amount - &amount.proportion(fee_percent, PERCENT_BASE_POINTS)
}

BPS (Basis Points) — Manual

When you need explicit control over the calculation:

pub fn apply_bps(amount: &BigUint, bps: u64) -> BigUint {
    require!(bps <= 10_000, "BPS exceeds 100%");
    (amount * bps) / 10_000u64
}

PPM (Parts Per Million) — Manual

pub fn apply_ppm(amount: &BigUint, ppm: u32) -> BigUint {
    require!(ppm <= 1_000_000, "PPM exceeds 100%");
    (amount * ppm) / 1_000_000u64
}

Common Math Module Template

#![no_std]
multiversx_sc::imports!();

#[multiversx_sc::module]
pub trait SharedMathModule {
    fn mul_half_up(
        &self,
        a: &ManagedDecimal<Self::Api, NumDecimals>,
        b: &ManagedDecimal<Self::Api, NumDecimals>,
        precision: NumDecimals,
    ) -> ManagedDecimal<Self::Api, NumDecimals> {
        // ... (implementation above)
    }

    fn div_half_up(
        &self,
        a: &ManagedDecimal<Self::Api, NumDecimals>,
        b: &ManagedDecimal<Self::Api, NumDecimals>,
        precision: NumDecimals,
    ) -> ManagedDecimal<Self::Api, NumDecimals> {
        // ... (implementation above)
    }

    fn to_decimal(
        &self,
        value: BigUint,
        precision: NumDecimals,
    ) -> ManagedDecimal<Self::Api, NumDecimals> {
        ManagedDecimal::from_raw_units(value, precision)
    }

    fn min(
        &self,
        a: ManagedDecimal<Self::Api, NumDecimals>,
        b: ManagedDecimal<Self::Api, NumDecimals>,
    ) -> ManagedDecimal<Self::Api, NumDecimals> {
        if a < b { a } else { b }
    }
}

Rounding Attack Vectors

Attack Mitigation
Dust deposits to steal rounding Half-up rounding on all scaled operations
Repeated small operations to drain value Minimum amounts + half-up on indices
Precision loss across conversions Use highest needed precision for intermediate math
Exploiting truncation in fee calculations Always round fees UP (in protocol's favor)

Bad/Good Examples

Bad

// DON'T: Divide before multiply — loses precision
let shares = (&amount / &total_supply) * &total_shares; // Truncates to 0 for small amounts!

Good

// DO: Multiply first, then divide to preserve precision
let shares = (&amount * &total_shares) / &total_supply;

Bad

// DON'T: Hardcode decimal assumptions — tokens can have 0-18 decimals
let one_token = BigUint::from(10u64).pow(18); // Assumes 18 decimals!

Good

// DO: Fetch decimals from token properties or pass as parameter
let one_token = BigUint::from(10u64).pow(token_decimals as u32);

Anti-Patterns

1. Mixing Precisions Without Rescaling

// WRONG — BPS and RAY have different scales
let result = bps_value + ray_value;

// CORRECT — rescale first
let bps_as_ray = bps_value.rescale(RAY_PRECISION);
let result = bps_as_ray + ray_value;

2. Using Truncating Division for Fees

// WRONG — truncation loses value for the protocol
let fee = amount / 100u64; // Truncates

// CORRECT — round up to favor protocol
let fee = (amount + 99u64) / 100u64; // Ceiling division

3. Intermediate Results at Low Precision

// WRONG — BPS precision loses significant digits in intermediate calc
let ratio = self.div_half_up(&a, &b, BPS_PRECISION);
let result = self.mul_half_up(&ratio, &c, BPS_PRECISION);

// CORRECT — compute at RAY, downscale at the end
let ratio = self.div_half_up(&a, &b, RAY_PRECISION);
let result = self.mul_half_up(&ratio, &c, RAY_PRECISION);
let final_result = self.rescale_half_up(&result, BPS_PRECISION);

Domain Applications

These generic patterns are used differently across DeFi domains:

  • Lending: Interest rate models, compound interest (Taylor expansion), utilization ratios — all built on mul_half_up/div_half_up at RAY precision
  • DEX/AMM: Price impact calculations, LP share math — WAD precision with half-up rounding
  • Staking: Reward distribution, share-to-token ratios — RAY indices with safe rescaling
  • Vaults: Fee calculations, yield accrual — BPS fees with ceiling division
Weekly Installs
7
GitHub Stars
10
First Seen
Feb 8, 2026
Installed on
opencode6
gemini-cli6
github-copilot6
codex6
amp6
kimi-cli6