Skip to content

Commit c5caca7

Browse files
authored
Merge pull request #99 from OpenZeppelin/stellar-wad
stellar-wad
2 parents b586384 + 1b85e29 commit c5caca7

File tree

2 files changed

+392
-0
lines changed

2 files changed

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

src/navigation/stellar.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@
111111
"name": "Upgradeable",
112112
"url": "/stellar-contracts/utils/upgradeable"
113113
},
114+
{
115+
"type": "folder",
116+
"name": "Math",
117+
"children": [
118+
{
119+
"type": "page",
120+
"name": "WAD",
121+
"url": "/stellar-contracts/utils/math/wad"
122+
}
123+
]
124+
},
114125
{
115126
"type": "folder",
116127
"name": "Cryptography",

0 commit comments

Comments
 (0)