Skip to content

Commit 66e5b6c

Browse files
committed
stellar-wad
1 parent d2a50bc commit 66e5b6c

2 files changed

Lines changed: 387 additions & 0 deletions

File tree

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
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+
```

src/navigation/stellar.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@
100100
"name": "Upgradeable",
101101
"url": "/stellar-contracts/utils/upgradeable"
102102
},
103+
{
104+
"type": "folder",
105+
"name": "Math",
106+
"children": [
107+
{
108+
"type": "page",
109+
"name": "WAD",
110+
"url": "/stellar-contracts/utils/math/wad"
111+
}
112+
]
113+
},
103114
{
104115
"type": "folder",
105116
"name": "Cryptography",

0 commit comments

Comments
 (0)