|
| 1 | +--- |
| 2 | +title: WAD Fixed-Point Math |
| 3 | +description: High-precision decimal arithmetic |
| 4 | +--- |
| 5 | + |
| 6 | +# Overview |
| 7 | + |
| 8 | +The WAD library provides fixed-point decimal arithmetic for Soroban smart contracts with 18 decimal places of precision. |
| 9 | +It's designed specifically for DeFi applications where precise decimal calculations are critical, |
| 10 | +such as interest rates, exchange rates, and token pricing. |
| 11 | + |
| 12 | +It is a fixed-point representation where: |
| 13 | +- **1.0 is represented as `1_000_000_000_000_000_000` (10^18)** |
| 14 | +- **0.5 is represented as `500_000_000_000_000_000`** |
| 15 | +- **123.456 is represented as `123_456_000_000_000_000_000`** |
| 16 | + |
| 17 | +This allows precise decimal arithmetic using only integer operations, avoiding the pitfalls |
| 18 | +of floating-point arithmetic in smart contracts. |
| 19 | + |
| 20 | +# Why WAD? |
| 21 | + |
| 22 | +## Shortcomings of Integers and Float Numbers |
| 23 | + |
| 24 | +**Native Integers (`i128`, `u64`):** |
| 25 | +- No decimal support - `1/2 = 0` instead of `0.5` |
| 26 | +- Loss of precision in financial calculations |
| 27 | +- Requires manual scaling for each operation |
| 28 | + |
| 29 | +**Floating-Point (`f64`, `f32`):** |
| 30 | +- Non-deterministic behavior across platforms |
| 31 | +- Rounding errors that compound in financial calculations |
| 32 | +- Security vulnerabilities from precision loss |
| 33 | + |
| 34 | +## Why WAD is Better |
| 35 | + |
| 36 | +- **High Precision**: 18 decimals is more than sufficient for financial calculations |
| 37 | +- **Deterministic**: Same inputs always produce same outputs |
| 38 | +- **Efficient**: Uses native `i128` arithmetic under the hood |
| 39 | +- **Battle-Tested**: Used in production by MakerDAO, Uniswap, Aave, and others |
| 40 | +- **Ergonomic**: Operator overloading makes code readable: `a + b * c` |
| 41 | +- **Type Safe**: NewType pattern prevents mixing scaled and unscaled values |
| 42 | + |
| 43 | +# Design Decisions |
| 44 | + |
| 45 | +## 1. NewType Pattern |
| 46 | + |
| 47 | +We use a NewType `struct Wad(i128)` instead of a type alias: |
| 48 | + |
| 49 | +```rust |
| 50 | +// Type alias |
| 51 | +type Wad = i128; |
| 52 | + |
| 53 | +// NewType |
| 54 | +pub struct Wad(i128); |
| 55 | +``` |
| 56 | + |
| 57 | +**Benefits:** |
| 58 | +- **Type Safety**: Cannot accidentally mix scaled and unscaled values |
| 59 | +- **Operator Overloading**: Can implement `+`, `-`, `*`, `/` with correct semantics |
| 60 | +- **Semantic Clarity**: Makes intent explicit in function signatures |
| 61 | + |
| 62 | +## 2. No `From`/`Into` Traits |
| 63 | + |
| 64 | +We deliberately **DID NOT** implement `From<i128>` or `Into<i128>` because it's ambiguous: |
| 65 | + |
| 66 | +```rust |
| 67 | +// What should this mean? |
| 68 | +let wad = Wad::from(5); |
| 69 | +// Is it 5.0 (scaled to 5 * 10^18)? |
| 70 | +// Or 0.000000000000000005 (raw value 5)? |
| 71 | +``` |
| 72 | + |
| 73 | +Instead, we provide explicit constructors: |
| 74 | +- `Wad::from_integer(e, 5)` - Creates 5.0 (scaled) |
| 75 | +- `Wad::from_raw(5)` - Creates raw value 5 (0.000000000000000005) |
| 76 | + |
| 77 | +## 3. Truncation vs Rounding |
| 78 | + |
| 79 | +All operations truncate toward zero rather than rounding: |
| 80 | + |
| 81 | +**Why truncation?** |
| 82 | +- **Predictable**: Same behavior as integer division |
| 83 | +- **Conservative**: In financial calculations, truncation is often safer (e.g., don't over-calculate interest) |
| 84 | +- **Fast**: No additional logic needed |
| 85 | + |
| 86 | +## 4. Operator Overloading |
| 87 | + |
| 88 | +We provide operator overloading (`+`, `-`, `*`, `/`, `-`) for convenience: |
| 89 | + |
| 90 | +```rust |
| 91 | +// Readable arithmetic |
| 92 | +let total = price + fee; |
| 93 | +let cost = quantity * price; |
| 94 | +let ratio = numerator / denominator; |
| 95 | +``` |
| 96 | + |
| 97 | +Operator overloading is supported across WAD and native i128 types where unambiguous: |
| 98 | +`WAD * i128`, `i128 * WAD`, `WAD / i128`. |
| 99 | + |
| 100 | +**Explicit methods are available for safety:** |
| 101 | +- `checked_add()`, `checked_sub()`, etc. return `Option<Wad>` for overflow handling |
| 102 | + |
| 103 | +<Callout type="warning"> |
| 104 | + |
| 105 | +**Overflow Behavior** |
| 106 | + |
| 107 | +Just like regular Rust, operator overloading does not include overflow checks: |
| 108 | + |
| 109 | +- Use `checked_*` methods (`checked_add()`, `checked_sub()`, `checked_mul()`, etc.) when handling user inputs or when overflow is possible. These return `Option<Wad>` for safe error handling. |
| 110 | +- Use operator overloads (`+`, `-`, `*`, `/`) when you want to reduce computational overhead by skipping overflow checks, or when you're confident the operation cannot overflow. |
| 111 | + |
| 112 | +This design follows Rust's standard library pattern: operators for performance, checked methods for safety. |
| 113 | + |
| 114 | +</Callout> |
| 115 | + |
| 116 | +# How It Works |
| 117 | + |
| 118 | +## Internal Representation |
| 119 | + |
| 120 | +```rust |
| 121 | +pub struct Wad(i128); // Internal representation |
| 122 | +pub const WAD_SCALE: i128 = 1_000_000_000_000_000_000; // 10^18 |
| 123 | +``` |
| 124 | + |
| 125 | +A `Wad` is simply a wrapper around `i128` that interprets the value as having 18 decimal places. |
| 126 | + |
| 127 | +## Arithmetic Operations |
| 128 | + |
| 129 | +**Addition/Subtraction:** Direct on internal values |
| 130 | +```rust |
| 131 | +impl Add for Wad { |
| 132 | + fn add(self, rhs: Wad) -> Wad { |
| 133 | + Wad(self.0 + rhs.0) |
| 134 | + } |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +**Multiplication:** Scale down by WAD_SCALE |
| 139 | +```rust |
| 140 | +impl Mul for Wad { |
| 141 | + fn mul(self, rhs: Wad) -> Wad { |
| 142 | + // (a * b) / 10^18 |
| 143 | + Wad((self.0 * rhs.0) / WAD_SCALE) |
| 144 | + } |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +**Division:** Scale up by WAD_SCALE |
| 149 | +```rust |
| 150 | +impl Div for Wad { |
| 151 | + fn div(self, rhs: Wad) -> Wad { |
| 152 | + // (a * 10^18) / b |
| 153 | + Wad((self.0 * WAD_SCALE) / rhs.0) |
| 154 | + } |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +## Exponentiation |
| 159 | + |
| 160 | +WAD supports raising a value to an unsigned integer exponent via `pow`. |
| 161 | + |
| 162 | +- `pow(&e, exponent)` is optimized using exponentiation by squaring (O(log n) multiplications). |
| 163 | +- Each multiplication keeps WAD semantics (fixed-point multiplication and truncation toward zero). |
| 164 | +- Overflow is reported via Soroban errors. |
| 165 | + |
| 166 | +In addition to `pow`, WAD also provides `checked_pow`, which returns `None` on overflow. |
| 167 | + |
| 168 | +```rust |
| 169 | +// Compound interest multiplier: (1.05)^10 |
| 170 | +let rate = Wad::from_ratio(&e, 105, 100); // 1.05 |
| 171 | +let multiplier = rate.pow(&e, 10); |
| 172 | +``` |
| 173 | + |
| 174 | +### Notes on `pow` and Phantom Overflow |
| 175 | + |
| 176 | +`pow` / `checked_pow` are implemented using exponentiation by squaring and rely |
| 177 | +on Soroban fixed-point helpers that can automatically scale intermediate products |
| 178 | +to `I256` when needed. |
| 179 | + |
| 180 | +This avoids **phantom overflow** cases where an intermediate multiplication would |
| 181 | +overflow `i128`, but the final scaled result would still fit in `i128`. |
| 182 | + |
| 183 | + |
| 184 | +## Token Conversions |
| 185 | + |
| 186 | +Different tokens have different decimal places (USDC: 6, XLM: 7, ETH: 18, BTC: 8). WAD handles these conversions: |
| 187 | + |
| 188 | +```rust |
| 189 | +// Convert from USDC (6 decimals) to WAD |
| 190 | +let usdc_amount: i128 = 1_500_000; // 1.5 USDC |
| 191 | +let wad = Wad::from_token_amount(&e, usdc_amount, 6); |
| 192 | +// wad.raw() = 1_500_000_000_000_000_000 (1.5 in WAD) |
| 193 | + |
| 194 | +// Convert back to USDC |
| 195 | +let usdc_back: i128 = wad.to_token_amount(&e, 6); |
| 196 | +// usdc_back = 1_500_000 |
| 197 | +``` |
| 198 | + |
| 199 | +# Precision Characteristics |
| 200 | + |
| 201 | +## Understanding Fixed-Point Precision |
| 202 | + |
| 203 | +WAD is a fixed-point math library. Like all fixed-point arithmetic systems, |
| 204 | +precision loss is inherent and unavoidable. The goal is not to eliminate precision errors |
| 205 | +—that's impossible— but to reduce them to a degree so minimal |
| 206 | +that they become irrelevant in practical applications. |
| 207 | + |
| 208 | +WAD achieves this goal exceptionally well. With precision loss in the range of **10^-16**, |
| 209 | +the errors are so microscopically small that they have zero practical impact on financial calculations. |
| 210 | +To put this in perspective: if you're calculating with millions of dollars, the error would be |
| 211 | +measured in quadrillionths of a cent. |
| 212 | + |
| 213 | +## How Precision Loss Manifests |
| 214 | + |
| 215 | +Due to truncation in each operation, operation order can produce slightly different results: |
| 216 | + |
| 217 | +```rust |
| 218 | +let a = Wad::from_integer(&e, 1000); |
| 219 | +let b = Wad::from_raw(55_000_000_000_000_000); // 0.055 |
| 220 | +let c = Wad::from_raw(8_333_333_333_333_333); // ~0.00833 |
| 221 | + |
| 222 | +let result1 = a * b * c; // Truncates after first multiplication |
| 223 | +let result2 = a * (b * c); // Truncates after inner multiplication |
| 224 | + |
| 225 | +// result1 and result2 may differ by ~315 WAD units |
| 226 | +// That's 0.000000000000000315 or (3.15 × 10^-16) |
| 227 | +``` |
| 228 | + |
| 229 | +**Why This Doesn't Matter:** |
| 230 | +- Errors are in the **10^-15 to 10^-18** range, far beyond practical significance |
| 231 | +- Token precision (6-8 decimals) completely absorbs these errors when converting back |
| 232 | +- Real-world financial systems round to 2-8 decimal places; WAD's 18 decimals provide a massive safety margin |
| 233 | +- This is orders of magnitude more precise than needed for DeFi applications |
| 234 | + |
| 235 | +# Usage Examples |
| 236 | + |
| 237 | +## Basic Arithmetic |
| 238 | + |
| 239 | +```rust |
| 240 | +use soroban_sdk::Env; |
| 241 | +use stellar_contract_utils::math::wad::Wad; |
| 242 | + |
| 243 | +fn calculate_interest(e: &Env, principal: i128, rate_bps: u32) -> i128 { |
| 244 | + // Convert principal (assume 6 decimals like USDC) |
| 245 | + let principal_wad = Wad::from_token_amount(e, principal, 6); |
| 246 | + |
| 247 | + // Rate in basis points (e.g., 550 = 5.5%) |
| 248 | + let rate_wad = Wad::from_ratio(e, rate_bps as i128, 10_000); |
| 249 | + |
| 250 | + // Calculate interest |
| 251 | + let interest_wad = principal_wad * rate_wad; |
| 252 | + |
| 253 | + // Convert back to token amount |
| 254 | + interest_wad.to_token_amount(e, 6) |
| 255 | +} |
| 256 | +``` |
| 257 | + |
| 258 | +## Price Calculations |
| 259 | + |
| 260 | +```rust |
| 261 | +fn calculate_swap_output( |
| 262 | + e: &Env, |
| 263 | + amount_in: i128, |
| 264 | + reserve_in: i128, |
| 265 | + reserve_out: i128, |
| 266 | +) -> i128 { |
| 267 | + // Convert to WAD |
| 268 | + let amount_in_wad = Wad::from_token_amount(e, amount_in, 6); |
| 269 | + let reserve_in_wad = Wad::from_token_amount(e, reserve_in, 6); |
| 270 | + let reserve_out_wad = Wad::from_token_amount(e, reserve_out, 6); |
| 271 | + |
| 272 | + // Constant product formula: amount_out = (amount_in * reserve_out) / (reserve_in + amount_in) |
| 273 | + let numerator = amount_in_wad * reserve_out_wad; |
| 274 | + let denominator = reserve_in_wad + amount_in_wad; |
| 275 | + let amount_out_wad = numerator / denominator; |
| 276 | + |
| 277 | + // Convert back |
| 278 | + amount_out_wad.to_token_amount(e, 6) |
| 279 | +} |
| 280 | +``` |
| 281 | + |
| 282 | +## Compound Interest |
| 283 | + |
| 284 | +```rust |
| 285 | +fn calculate_compound_interest( |
| 286 | + e: &Env, |
| 287 | + principal: i128, |
| 288 | + annual_rate_bps: u32, |
| 289 | + days: u32, |
| 290 | +) -> i128 { |
| 291 | + let principal_wad = Wad::from_token_amount(e, principal, 6); |
| 292 | + let rate = Wad::from_ratio(e, annual_rate_bps as i128, 10_000); |
| 293 | + let time_fraction = Wad::from_ratio(e, days as i128, 365); |
| 294 | + |
| 295 | + // Simple interest: principal * rate * time |
| 296 | + let interest = principal_wad * rate * time_fraction; |
| 297 | + |
| 298 | + interest.to_token_amount(e, 6) |
| 299 | +} |
| 300 | +``` |
| 301 | + |
| 302 | +## Safe Arithmetic with Overflow Checks |
| 303 | + |
| 304 | +```rust |
| 305 | +fn safe_multiply(e: &Env, a: i128, b: i128) -> Result<i128, Error> { |
| 306 | + let a_wad = Wad::from_token_amount(e, a, 6); |
| 307 | + let b_wad = Wad::from_token_amount(e, b, 6); |
| 308 | + |
| 309 | + // Use checked variant |
| 310 | + let result_wad = a_wad |
| 311 | + .checked_mul(e, b_wad) |
| 312 | + .ok_or(Error::Overflow)?; |
| 313 | + |
| 314 | + Ok(result_wad.to_token_amount(e, 6)) |
| 315 | +} |
| 316 | +``` |
| 317 | + |
| 318 | +# API Reference |
| 319 | + |
| 320 | +## Constructors |
| 321 | + |
| 322 | +| Method | Description | Example | |
| 323 | +|--------|-------------|---------| |
| 324 | +| `from_integer(e, n)` | Create from whole number | `Wad::from_integer(&e, 5)` → 5.0 | |
| 325 | +| `from_ratio(e, num, den)` | Create from fraction | `Wad::from_ratio(&e, 1, 2)` → 0.5 | |
| 326 | +| `from_token_amount(e, amount, decimals)` | Create from token amount | `Wad::from_token_amount(&e, 1_500_000, 6)` → 1.5 | |
| 327 | +| `from_price(e, price, decimals)` | Alias for `from_token_amount` | `Wad::from_price(&e, 100_000, 6)` → 0.1 | |
| 328 | +| `from_raw(raw)` | Create from raw i128 value | `Wad::from_raw(10^18)` → 1.0 | |
| 329 | + |
| 330 | +## Converters |
| 331 | + |
| 332 | +| Method | Description | Example | |
| 333 | +|--------|-------------|---------| |
| 334 | +| `to_integer()` | Convert to whole number (truncates) | `Wad(5.7).to_integer()` → 5 | |
| 335 | +| `to_token_amount(e, decimals)` | Convert to token amount | `Wad(1.5).to_token_amount(&e, 6)` → 1_500_000 | |
| 336 | +| `raw()` | Get raw i128 value | `Wad(1.0).raw()` → 10^18 | |
| 337 | + |
| 338 | +## Arithmetic Operators |
| 339 | + |
| 340 | +| Operator | Description | Example | |
| 341 | +|----------|-------------|---------| |
| 342 | +| `a + b` | Addition | `Wad(1.5) + Wad(2.3)` → 3.8 | |
| 343 | +| `a - b` | Subtraction | `Wad(5.0) - Wad(3.0)` → 2.0 | |
| 344 | +| `a * b` | Multiplication (WAD × WAD) | `Wad(2.0) * Wad(3.0)` → 6.0 | |
| 345 | +| `a / b` | Division (WAD ÷ WAD) | `Wad(6.0) / Wad(2.0)` → 3.0 | |
| 346 | +| `a * n` | Multiply WAD by integer | `Wad(2.5) * 3` → 7.5 | |
| 347 | +| `n * a` | Multiply integer by WAD | `3 * Wad(2.5)` → 7.5 | |
| 348 | +| `a / n` | Divide WAD by integer | `Wad(7.5) / 3` → 2.5 | |
| 349 | +| `-a` | Negation | `-Wad(5.0)` → -5.0 | |
| 350 | + |
| 351 | +## Checked Arithmetic |
| 352 | + |
| 353 | +| Method | Returns | Description | |
| 354 | +|--------|---------|-------------| |
| 355 | +| `checked_add(rhs)` | `Option<Wad>` | Addition with overflow check | |
| 356 | +| `checked_sub(rhs)` | `Option<Wad>` | Subtraction with overflow check | |
| 357 | +| `checked_mul(e, rhs)` | `Option<Wad>` | Multiplication with overflow check (handles phantom overflow internally) | |
| 358 | +| `checked_div(e, rhs)` | `Option<Wad>` | Division with overflow/zero check | |
| 359 | +| `checked_mul_int(n)` | `Option<Wad>` | Integer multiplication with overflow check | |
| 360 | +| `checked_div_int(n)` | `Option<Wad>` | Integer division with zero check | |
| 361 | +| `checked_pow(e, exponent)` | `Option<Wad>` | Exponentiation with overflow check | |
| 362 | + |
| 363 | +## Utility Methods |
| 364 | + |
| 365 | +| Method | Description | |
| 366 | +|--------|-------------| |
| 367 | +| `abs()` | Absolute value | |
| 368 | +| `min(other)` | Minimum of two values | |
| 369 | +| `max(other)` | Maximum of two values | |
| 370 | +| `pow(e, exponent)` | Raises WAD to an unsigned integer power (panics with Soroban error on overflow) | |
| 371 | + |
| 372 | +## Error Handling |
| 373 | + |
| 374 | +WAD uses Soroban's contract error system via `SorobanFixedPointError`: |
| 375 | + |
| 376 | +```rust |
| 377 | +pub enum SorobanFixedPointError { |
| 378 | + Overflow = 1500, |
| 379 | + DivisionByZero = 1501, |
| 380 | +} |
| 381 | +``` |
0 commit comments