|
| 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 | +## Problems with Alternatives |
| 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 | +- **Gas 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. Why 18 Decimals? |
| 46 | + |
| 47 | +**18 decimals was chosen for several reasons:** |
| 48 | + |
| 49 | +- **Battle-Tested Standard**: Most DeFi protocols on use 18 decimals |
| 50 | +- **Sufficient Precision**: Provides precision down to 10^-18, far exceeding practical financial needs |
| 51 | +- **Interoperability**: Makes it easier to port DeFi protocols from other ecosystems to Stellar |
| 52 | +- **Performance**: Fits in `i128` without overflow concerns for typical values |
| 53 | + |
| 54 | +**Example Precision:** |
| 55 | +```rust |
| 56 | +// Interest rate: 5.5% = 0.055 |
| 57 | +let rate = Wad::from_ratio(&e, 55, 1000); // 55/1000 = 0.055 |
| 58 | + |
| 59 | +// Time fraction: 1 day / 365 days ≈ 0.00274 |
| 60 | +let time = Wad::from_ratio(&e, 1, 365); // 1/365 ≈ 0.00274 |
| 61 | + |
| 62 | +// Interest = principal * rate * time |
| 63 | +let interest = principal * rate * time; |
| 64 | +// Precise to 18 decimal places! |
| 65 | +``` |
| 66 | + |
| 67 | +## 2. NewType Pattern |
| 68 | + |
| 69 | +We use a NewType `struct Wad(i128)` instead of a type alias: |
| 70 | + |
| 71 | +```rust |
| 72 | +// ❌ BAD: Type alias |
| 73 | +type Wad = i128; |
| 74 | + |
| 75 | +// ✅ GOOD: NewType |
| 76 | +pub struct Wad(i128); |
| 77 | +``` |
| 78 | + |
| 79 | +**Benefits:** |
| 80 | +- **Type Safety**: Cannot accidentally mix scaled and unscaled values |
| 81 | +- **Operator Overloading**: Can implement `+`, `-`, `*`, `/` with correct semantics |
| 82 | +- **Semantic Clarity**: Makes intent explicit in function signatures |
| 83 | + |
| 84 | +## 3. No `From`/`Into` Traits |
| 85 | + |
| 86 | +We deliberately **do not** implement `From<i128>` or `Into<i128>` because it's ambiguous: |
| 87 | + |
| 88 | +```rust |
| 89 | +// What should this mean? |
| 90 | +let wad = Wad::from(5); |
| 91 | +// Is it 5.0 (scaled to 5 * 10^18)? |
| 92 | +// Or 0.000000000000000005 (raw value 5)? |
| 93 | +``` |
| 94 | + |
| 95 | +Instead, we provide explicit constructors: |
| 96 | +- `Wad::from_integer(e, 5)` - Creates 5.0 (scaled) |
| 97 | +- `Wad::from_raw(5)` - Creates raw value 5 (0.000000000000000005) |
| 98 | + |
| 99 | +## 4. Truncation vs Rounding |
| 100 | + |
| 101 | +All operations truncate toward zero rather than rounding: |
| 102 | + |
| 103 | +**Why truncation?** |
| 104 | +- **Predictable**: Same behavior as integer division |
| 105 | +- **Conservative**: In financial calculations, truncation is often safer (e.g., don't over-calculate interest) |
| 106 | +- **Fast**: No additional logic needed |
| 107 | +- **Standard**: Matches Solidity and most fixed-point libraries |
| 108 | + |
| 109 | +## 5. Operator Overloading |
| 110 | + |
| 111 | +We provide operator overloading (`+`, `-`, `*`, `/`, `-`) for convenience: |
| 112 | + |
| 113 | +```rust |
| 114 | +// Readable arithmetic |
| 115 | +let total = price + fee; |
| 116 | +let cost = quantity * price; |
| 117 | +let ratio = numerator / denominator; |
| 118 | +``` |
| 119 | + |
| 120 | +Operator overloading is supported across WAD and native i128 types where unambiguous: |
| 121 | +`WAD * i128`, `i128 * WAD`, `WAD / i128`. |
| 122 | + |
| 123 | +**Explicit methods are available for safety:** |
| 124 | +- `checked_add()`, `checked_sub()`, etc. return `Option<Wad>` for overflow handling |
| 125 | + |
| 126 | +:::warning[Overflow Behavior] |
| 127 | + |
| 128 | +**Just like regular Rust**, operator overloading does not include overflow checks: |
| 129 | + |
| 130 | +- **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. |
| 131 | +- **Use operator overloads** (`+`, `-`, `*`, `/`) when you want to save gas by skipping overflow checks, or when you're confident the operation cannot overflow. |
| 132 | + |
| 133 | +This design follows Rust's standard library pattern: operators for performance, checked methods for safety. |
| 134 | + |
| 135 | +::: |
| 136 | + |
| 137 | +# How It Works |
| 138 | + |
| 139 | +## Internal Representation |
| 140 | + |
| 141 | +```rust |
| 142 | +pub struct Wad(i128); // Internal representation |
| 143 | +pub const WAD_SCALE: i128 = 1_000_000_000_000_000_000; // 10^18 |
| 144 | +``` |
| 145 | + |
| 146 | +A `Wad` is simply a wrapper around `i128` that interprets the value as having 18 decimal places. |
| 147 | + |
| 148 | +## Arithmetic Operations |
| 149 | + |
| 150 | +**Addition/Subtraction:** Direct on internal values |
| 151 | +```rust |
| 152 | +impl Add for Wad { |
| 153 | + fn add(self, rhs: Wad) -> Wad { |
| 154 | + Wad(self.0 + rhs.0) |
| 155 | + } |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +**Multiplication:** Scale down by WAD_SCALE |
| 160 | +```rust |
| 161 | +impl Mul for Wad { |
| 162 | + fn mul(self, rhs: Wad) -> Wad { |
| 163 | + // (a * b) / 10^18 |
| 164 | + Wad((self.0 * rhs.0) / WAD_SCALE) |
| 165 | + } |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +**Division:** Scale up by WAD_SCALE |
| 170 | +```rust |
| 171 | +impl Div for Wad { |
| 172 | + fn div(self, rhs: Wad) -> Wad { |
| 173 | + // (a * 10^18) / b |
| 174 | + Wad((self.0 * WAD_SCALE) / rhs.0) |
| 175 | + } |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | +## Token Conversions |
| 180 | + |
| 181 | +Different tokens have different decimal places (USDC: 6, ETH: 18, BTC: 8). WAD handles these conversions: |
| 182 | + |
| 183 | +```rust |
| 184 | +// Convert from USDC (6 decimals) to WAD |
| 185 | +let usdc_amount: i128 = 1_500_000; // 1.5 USDC |
| 186 | +let wad = Wad::from_token_amount(&e, usdc_amount, 6); |
| 187 | +// wad.raw() = 1_500_000_000_000_000_000 (1.5 in WAD) |
| 188 | + |
| 189 | +// Convert back to USDC |
| 190 | +let usdc_back: i128 = wad.to_token_amount(&e, 6); |
| 191 | +// usdc_back = 1_500_000 |
| 192 | +``` |
| 193 | + |
| 194 | +# Precision Characteristics |
| 195 | + |
| 196 | +## Understanding Fixed-Point Precision |
| 197 | + |
| 198 | +**WAD is a fixed-point math library.** Like all fixed-point arithmetic systems, |
| 199 | +precision loss is inherent and unavoidable. The goal is not to eliminate precision errors |
| 200 | +—that's impossible— but to reduce them to a degree so minimal |
| 201 | +that they become irrelevant in practical applications. |
| 202 | + |
| 203 | +WAD achieves this goal exceptionally well. With precision loss in the range of **10^-16**, |
| 204 | +the errors are so microscopically small that they have zero practical impact on financial calculations. |
| 205 | +To put this in perspective: if you're calculating with millions of dollars, the error would be |
| 206 | +measured in quadrillionths of a cent. |
| 207 | + |
| 208 | +## How Precision Loss Manifests |
| 209 | + |
| 210 | +Due to truncation in each operation, operation order can produce slightly different results: |
| 211 | + |
| 212 | +```rust |
| 213 | +let a = Wad::from_integer(&e, 1000); |
| 214 | +let b = Wad::from_raw(55_000_000_000_000_000); // 0.055 |
| 215 | +let c = Wad::from_raw(8_333_333_333_333_333); // ~0.00833 |
| 216 | + |
| 217 | +let result1 = a * b * c; // Truncates after first multiplication |
| 218 | +let result2 = a * (b * c); // Truncates after inner multiplication |
| 219 | + |
| 220 | +// result1 and result2 may differ by ~315 WAD units |
| 221 | +// That's 0.000000000000000315 or (3.15 × 10^-16) |
| 222 | +``` |
| 223 | + |
| 224 | +**Why This Doesn't Matter:** |
| 225 | +- Errors are in the **10^-15 to 10^-18** range, far beyond practical significance |
| 226 | +- Token precision (6-8 decimals) completely absorbs these errors when converting back |
| 227 | +- Real-world financial systems round to 2-8 decimal places; WAD's 18 decimals provide a massive safety margin |
| 228 | +- This is **orders of magnitude more precise** than needed for DeFi applications |
| 229 | + |
| 230 | +# Usage Examples |
| 231 | + |
| 232 | +## Basic Arithmetic |
| 233 | + |
| 234 | +```rust |
| 235 | +use soroban_sdk::Env; |
| 236 | +use contract_utils::math::wad::Wad; |
| 237 | + |
| 238 | +fn calculate_interest(e: &Env, principal: i128, rate_bps: u32) -> i128 { |
| 239 | + // Convert principal (assume 6 decimals like USDC) |
| 240 | + let principal_wad = Wad::from_token_amount(e, principal, 6); |
| 241 | + |
| 242 | + // Rate in basis points (e.g., 550 = 5.5%) |
| 243 | + let rate_wad = Wad::from_ratio(e, rate_bps as i128, 10_000); |
| 244 | + |
| 245 | + // Calculate interest |
| 246 | + let interest_wad = principal_wad * rate_wad; |
| 247 | + |
| 248 | + // Convert back to token amount |
| 249 | + interest_wad.to_token_amount(e, 6) |
| 250 | +} |
| 251 | +``` |
| 252 | + |
| 253 | +## Price Calculations |
| 254 | + |
| 255 | +```rust |
| 256 | +fn calculate_swap_output( |
| 257 | + e: &Env, |
| 258 | + amount_in: i128, |
| 259 | + reserve_in: i128, |
| 260 | + reserve_out: i128, |
| 261 | +) -> i128 { |
| 262 | + // Convert to WAD |
| 263 | + let amount_in_wad = Wad::from_token_amount(e, amount_in, 6); |
| 264 | + let reserve_in_wad = Wad::from_token_amount(e, reserve_in, 6); |
| 265 | + let reserve_out_wad = Wad::from_token_amount(e, reserve_out, 6); |
| 266 | + |
| 267 | + // Constant product formula: amount_out = (amount_in * reserve_out) / (reserve_in + amount_in) |
| 268 | + let numerator = amount_in_wad * reserve_out_wad; |
| 269 | + let denominator = reserve_in_wad + amount_in_wad; |
| 270 | + let amount_out_wad = numerator / denominator; |
| 271 | + |
| 272 | + // Convert back |
| 273 | + amount_out_wad.to_token_amount(e, 6) |
| 274 | +} |
| 275 | +``` |
| 276 | + |
| 277 | +## Compound Interest |
| 278 | + |
| 279 | +```rust |
| 280 | +fn calculate_compound_interest( |
| 281 | + e: &Env, |
| 282 | + principal: i128, |
| 283 | + annual_rate_bps: u32, |
| 284 | + days: u32, |
| 285 | +) -> i128 { |
| 286 | + let principal_wad = Wad::from_token_amount(e, principal, 6); |
| 287 | + let rate = Wad::from_ratio(e, annual_rate_bps as i128, 10_000); |
| 288 | + let time_fraction = Wad::from_ratio(e, days as i128, 365); |
| 289 | + |
| 290 | + // Simple interest: principal * rate * time |
| 291 | + let interest = principal_wad * rate * time_fraction; |
| 292 | + |
| 293 | + interest.to_token_amount(e, 6) |
| 294 | +} |
| 295 | +``` |
| 296 | + |
| 297 | +## Safe Arithmetic with Overflow Checks |
| 298 | + |
| 299 | +```rust |
| 300 | +fn safe_multiply(e: &Env, a: i128, b: i128) -> Result<i128, Error> { |
| 301 | + let a_wad = Wad::from_token_amount(e, a, 6); |
| 302 | + let b_wad = Wad::from_token_amount(e, b, 6); |
| 303 | + |
| 304 | + // Use checked variant |
| 305 | + let result_wad = a_wad |
| 306 | + .checked_mul(b_wad) |
| 307 | + .ok_or(Error::Overflow)?; |
| 308 | + |
| 309 | + Ok(result_wad.to_token_amount(e, 6)) |
| 310 | +} |
| 311 | +``` |
| 312 | + |
| 313 | +# API Reference |
| 314 | + |
| 315 | +## Constructors |
| 316 | + |
| 317 | +| Method | Description | Example | |
| 318 | +|--------|-------------|---------| |
| 319 | +| `from_integer(e, n)` | Create from whole number | `Wad::from_integer(&e, 5)` → 5.0 | |
| 320 | +| `from_ratio(e, num, den)` | Create from fraction | `Wad::from_ratio(&e, 1, 2)` → 0.5 | |
| 321 | +| `from_token_amount(e, amount, decimals)` | Create from token amount | `Wad::from_token_amount(&e, 1_500_000, 6)` → 1.5 | |
| 322 | +| `from_price(e, price, decimals)` | Alias for `from_token_amount` | `Wad::from_price(&e, 100_000, 6)` → 0.1 | |
| 323 | +| `from_raw(raw)` | Create from raw i128 value | `Wad::from_raw(10^18)` → 1.0 | |
| 324 | + |
| 325 | +## Converters |
| 326 | + |
| 327 | +| Method | Description | Example | |
| 328 | +|--------|-------------|---------| |
| 329 | +| `to_integer()` | Convert to whole number (truncates) | `Wad(5.7).to_integer()` → 5 | |
| 330 | +| `to_token_amount(e, decimals)` | Convert to token amount | `Wad(1.5).to_token_amount(&e, 6)` → 1_500_000 | |
| 331 | +| `raw()` | Get raw i128 value | `Wad(1.0).raw()` → 10^18 | |
| 332 | + |
| 333 | +## Arithmetic Operators |
| 334 | + |
| 335 | +| Operator | Description | Example | |
| 336 | +|----------|-------------|---------| |
| 337 | +| `a + b` | Addition | `Wad(1.5) + Wad(2.3)` → 3.8 | |
| 338 | +| `a - b` | Subtraction | `Wad(5.0) - Wad(3.0)` → 2.0 | |
| 339 | +| `a * b` | Multiplication (WAD × WAD) | `Wad(2.0) * Wad(3.0)` → 6.0 | |
| 340 | +| `a / b` | Division (WAD ÷ WAD) | `Wad(6.0) / Wad(2.0)` → 3.0 | |
| 341 | +| `a * n` | Multiply WAD by integer | `Wad(2.5) * 3` → 7.5 | |
| 342 | +| `n * a` | Multiply integer by WAD | `3 * Wad(2.5)` → 7.5 | |
| 343 | +| `a / n` | Divide WAD by integer | `Wad(7.5) / 3` → 2.5 | |
| 344 | +| `-a` | Negation | `-Wad(5.0)` → -5.0 | |
| 345 | + |
| 346 | +## Checked Arithmetic |
| 347 | + |
| 348 | +| Method | Returns | Description | |
| 349 | +|--------|---------|-------------| |
| 350 | +| `checked_add(rhs)` | `Option<Wad>` | Addition with overflow check | |
| 351 | +| `checked_sub(rhs)` | `Option<Wad>` | Subtraction with overflow check | |
| 352 | +| `checked_mul(rhs)` | `Option<Wad>` | Multiplication with overflow check | |
| 353 | +| `checked_div(rhs)` | `Option<Wad>` | Division with overflow/zero check | |
| 354 | +| `checked_mul_int(n)` | `Option<Wad>` | Integer multiplication with overflow check | |
| 355 | +| `checked_div_int(n)` | `Option<Wad>` | Integer division with zero check | |
| 356 | + |
| 357 | +## Utility Methods |
| 358 | + |
| 359 | +| Method | Description | |
| 360 | +|--------|-------------| |
| 361 | +| `abs()` | Absolute value | |
| 362 | +| `min(other)` | Minimum of two values | |
| 363 | +| `max(other)` | Maximum of two values | |
| 364 | + |
| 365 | +## Error Handling |
| 366 | + |
| 367 | +WAD uses Soroban's contract error system: |
| 368 | + |
| 369 | +```rust |
| 370 | +#[contracterror] |
| 371 | +pub enum WadError { |
| 372 | + Overflow = 1600, // Arithmetic overflow |
| 373 | + DivisionByZero = 1601, // Division by zero |
| 374 | + InvalidDecimals = 1602, // Invalid decimal conversion |
| 375 | +} |
| 376 | +``` |
0 commit comments