Packages

Aave V3 protocol wrappers for Elixir — pool reads/writes, oracle, math, and type structs. Built on onchain.

Current section

Files

Jump to
onchain_aave lib onchain aave math.ex
Raw

lib/onchain/aave/math.ex

defmodule Onchain.Aave.Math do
@moduledoc """
Aave V3 math — `Decimal.t()` display conversions plus integer-native WadRayMath
and MathUtils ports.
Two layers:
## Layer 1 — Raw integer → `Decimal.t()` (human display)
Aave smart contracts return raw integers at various scales. These functions
centralize the conversions so all consumers use the same math.
| Function | Exponent | Aave Usage |
|----------|----------|------------|
| `to_usd/1` | 10^8 | Oracle prices, base currency values |
| `to_ltv/1` | 10^4 | LTV ratios, liquidation thresholds (basis points) |
| `to_health_factor/1` | 10^18 | `getUserAccountData` health factor |
| `to_ray/1` | 10^27 | Interest rates (variable/stable borrow) |
| `to_wad/1` | 10^18 | Scaled token amounts (18-decimal tokens) |
All `to_*` functions are pure, guard-protected, accept integers, and return
`Decimal.t()`. Each delegates to `Onchain.Decimal.div_pow10/2`.
## Layer 2 — Fixed-point arithmetic (WadRayMath / MathUtils)
Integer-in / integer-out ports of Aave's Solidity libraries, preserving the
exact round-half-up semantics. Inputs and outputs are `non_neg_integer()` values
implicitly at ray (10^27) or wad (10^18) scale — the same representation the
on-chain contracts use. This makes revm cross-validation straightforward:
pass the same uint256 inputs, compare outputs bit-exact.
| Function | Solidity equivalent |
|----------|---------------------|
| `ray_mul/2` | `WadRayMath.rayMul` |
| `ray_div/2` | `WadRayMath.rayDiv` |
| `wad_mul/2` | `WadRayMath.wadMul` |
| `wad_div/2` | `WadRayMath.wadDiv` |
| `ray_to_wad/1` | `WadRayMath.rayToWad` |
| `wad_to_ray/1` | `WadRayMath.wadToRay` |
| `calculate_linear_interest/3` | `MathUtils.calculateLinearInterest` |
| `calculate_compounded_interest/3` | `MathUtils.calculateCompoundedInterest` |
### Rounding semantics
`ray_mul` / `wad_mul` / `ray_div` / `wad_div` / `ray_to_wad` round half-up
via Solidity's "add half the divisor, then floor-divide" idiom — e.g.
`ray_mul(a, b) = div(a * b + HALF_RAY, RAY)`. BEAM's `div/2` truncates toward
zero, which equals floor for non-negative operands (enforced by guards).
`wad_to_ray/1` multiplies exactly (no rounding needed).
BEAM integers are arbitrary-precision, so there is no uint256 overflow revert
to mirror. In realistic Aave inputs the products sit ~20 orders of magnitude
below 2^256 (upstream caps on reserve supply and borrow rate), so divergence
from Solidity's revert path is unreachable for valid callers.
### calculate_compounded_interest
Follows Aave's current polynomial approximation of `e^(rate * exp / seconds_per_year)`:
`x + rayMul(x, x/2 + rayMul(x, x/6))` where `x = rate * exp / seconds_per_year`.
The approximation slightly undercharges borrowers and underpays LPs vs. the
ideal compound-interest formula; the trade-off is bounded error for massive
gas savings, and is what the deployed protocol uses.
### Signature deviation from Solidity
Solidity's `calculateLinearInterest(rate, lastUpdateTimestamp)` and
`calculateCompoundedInterest(rate, lastUpdateTimestamp)` take `block.timestamp`
implicitly. Off-chain we don't have `block.timestamp`, so the ports require
`current_timestamp` as a third argument. Task 41's revm cross-validation
pins `block.timestamp` in the EVM env to match.
### Source
Ported from [aave-dao/aave-v3-origin](https://github.com/aave-dao/aave-v3-origin)
at commit `1e3d70c4151a94166ebc59e2eaa4aff6e6ba6978` (`src/contracts/protocol/libraries/math/{WadRayMath,MathUtils}.sol`).
"""
use Descripex, namespace: "/aave/math"
# --- Decimal display conversions ---
@usd_exponent 8
@ltv_exponent 4
@health_factor_exponent 18
@ray_exponent 27
@wad_exponent 18
# --- WadRayMath constants (uint256 fixed-point scales) ---
@ray 1_000_000_000_000_000_000_000_000_000
@half_ray 500_000_000_000_000_000_000_000_000
@wad 1_000_000_000_000_000_000
@half_wad 500_000_000_000_000_000
@wad_ray_ratio 1_000_000_000
@half_wad_ray_ratio 500_000_000
# --- MathUtils constants ---
@seconds_per_year 31_536_000
# --- to_usd ---
api(:to_usd, "Convert Aave oracle price or base currency value (10^8 scale) to Decimal.",
params: [
value: [kind: :value, description: "Raw integer from Aave oracle or base currency field"]
],
returns: %{
type: "Decimal.t()",
description: "USD value",
example: "123_456_789 → Decimal.new(\"1.23456789\")"
}
)
@spec to_usd(integer()) :: Decimal.t()
def to_usd(value) when is_integer(value) do
Onchain.Decimal.div_pow10(value, @usd_exponent)
end
# --- to_ltv ---
api(:to_ltv, "Convert Aave basis-point value (10^4 scale) to Decimal ratio.",
params: [
value: [kind: :value, description: "Raw integer LTV ratio or liquidation threshold (basis points)"]
],
returns: %{
type: "Decimal.t()",
description: "Ratio between 0 and 1",
example: "8000 → Decimal.new(\"0.8\")"
}
)
@spec to_ltv(integer()) :: Decimal.t()
def to_ltv(value) when is_integer(value) do
Onchain.Decimal.div_pow10(value, @ltv_exponent)
end
# --- to_health_factor ---
api(:to_health_factor, "Convert Aave health factor (10^18 scale) to Decimal.",
params: [
value: [kind: :value, description: "Raw integer health factor from getUserAccountData"]
],
returns: %{
type: "Decimal.t()",
description: "Health factor (> 1 means not liquidatable)",
example: "1_500_000_000_000_000_000 → Decimal.new(\"1.5\")"
}
)
@spec to_health_factor(integer()) :: Decimal.t()
def to_health_factor(value) when is_integer(value) do
Onchain.Decimal.div_pow10(value, @health_factor_exponent)
end
# --- to_ray ---
api(:to_ray, "Convert Aave ray value (10^27 scale) to Decimal.",
params: [
value: [kind: :value, description: "Raw integer interest rate in ray units"]
],
returns: %{
type: "Decimal.t()",
description: "Decimal interest rate",
example: "100_000_000_000_000_000_000_000_000 (10^26) → Decimal.new(\"0.1\")"
}
)
@spec to_ray(integer()) :: Decimal.t()
def to_ray(value) when is_integer(value) do
Onchain.Decimal.div_pow10(value, @ray_exponent)
end
# --- to_wad ---
api(:to_wad, "Convert wad value (10^18 scale) to Decimal.",
params: [
value: [kind: :value, description: "Raw integer scaled token amount in wad units"]
],
returns: %{
type: "Decimal.t()",
description: "Decimal token amount",
example: "1_000_000_000_000_000_000 (10^18) → Decimal.new(\"1.0\")"
}
)
@spec to_wad(integer()) :: Decimal.t()
def to_wad(value) when is_integer(value) do
Onchain.Decimal.div_pow10(value, @wad_exponent)
end
# --- ray_mul ---
api(:ray_mul, "Multiply two ray-scaled (10^27) integers, rounding half-up.",
params: [
a: [kind: :value, description: "Ray-scaled uint256 integer"],
b: [kind: :value, description: "Ray-scaled uint256 integer"]
],
returns: %{
type: "non_neg_integer()",
description: "a * b in ray, rounded half-up via add-half-then-floor-divide",
example:
"ray_mul(1_000_000_000_000_000_000_000_000_000, 2_000_000_000_000_000_000_000_000_000) == 2_000_000_000_000_000_000_000_000_000"
}
)
@spec ray_mul(non_neg_integer(), non_neg_integer()) :: non_neg_integer()
def ray_mul(a, b) when is_integer(a) and is_integer(b) and a >= 0 and b >= 0 do
div(a * b + @half_ray, @ray)
end
# --- ray_div ---
api(:ray_div, "Divide two ray-scaled (10^27) integers, rounding half-up.",
params: [
a: [kind: :value, description: "Ray-scaled uint256 dividend"],
b: [kind: :value, description: "Ray-scaled uint256 divisor (non-zero)"]
],
returns: %{
type: "non_neg_integer()",
description: "a / b in ray, rounded half-up via add-half-divisor-then-floor-divide",
example: "ray_div(ray, 2 * ray) == div(ray, 2)"
}
)
@spec ray_div(non_neg_integer(), pos_integer()) :: non_neg_integer()
def ray_div(a, b) when is_integer(a) and is_integer(b) and a >= 0 and b > 0 do
div(a * @ray + div(b, 2), b)
end
# --- wad_mul ---
api(:wad_mul, "Multiply two wad-scaled (10^18) integers, rounding half-up.",
params: [
a: [kind: :value, description: "Wad-scaled uint256 integer"],
b: [kind: :value, description: "Wad-scaled uint256 integer"]
],
returns: %{
type: "non_neg_integer()",
description: "a * b in wad, rounded half-up via add-half-then-floor-divide"
}
)
@spec wad_mul(non_neg_integer(), non_neg_integer()) :: non_neg_integer()
def wad_mul(a, b) when is_integer(a) and is_integer(b) and a >= 0 and b >= 0 do
div(a * b + @half_wad, @wad)
end
# --- wad_div ---
api(:wad_div, "Divide two wad-scaled (10^18) integers, rounding half-up.",
params: [
a: [kind: :value, description: "Wad-scaled uint256 dividend"],
b: [kind: :value, description: "Wad-scaled uint256 divisor (non-zero)"]
],
returns: %{
type: "non_neg_integer()",
description: "a / b in wad, rounded half-up via add-half-divisor-then-floor-divide"
}
)
@spec wad_div(non_neg_integer(), pos_integer()) :: non_neg_integer()
def wad_div(a, b) when is_integer(a) and is_integer(b) and a >= 0 and b > 0 do
div(a * @wad + div(b, 2), b)
end
# --- ray_to_wad ---
api(:ray_to_wad, "Cast a ray-scaled (10^27) integer down to wad (10^18), rounding half-up.",
params: [
a: [kind: :value, description: "Ray-scaled uint256 integer"]
],
returns: %{
type: "non_neg_integer()",
description: "a rescaled to wad, rounded half-up at the wad_ray_ratio (10^9) midpoint"
}
)
@spec ray_to_wad(non_neg_integer()) :: non_neg_integer()
def ray_to_wad(a) when is_integer(a) and a >= 0 do
quotient = div(a, @wad_ray_ratio)
remainder = rem(a, @wad_ray_ratio)
if remainder < @half_wad_ray_ratio do
quotient
else
quotient + 1
end
end
# --- wad_to_ray ---
api(:wad_to_ray, "Cast a wad-scaled (10^18) integer up to ray (10^27). Exact, no rounding.",
params: [
a: [kind: :value, description: "Wad-scaled uint256 integer"]
],
returns: %{
type: "non_neg_integer()",
description: "a rescaled to ray (a * 10^9)"
}
)
@spec wad_to_ray(non_neg_integer()) :: non_neg_integer()
def wad_to_ray(a) when is_integer(a) and a >= 0 do
a * @wad_ray_ratio
end
# --- calculate_linear_interest ---
api(
:calculate_linear_interest,
"Compute the linear-interest factor (in ray) accumulated between two timestamps at a given ray-scaled rate.",
params: [
rate: [kind: :value, description: "Annual interest rate in ray (10^27 scale)"],
last_update_timestamp: [kind: :value, description: "Unix-second timestamp of the last accrual"],
current_timestamp: [kind: :value, description: "Unix-second timestamp to accrue to (>= last_update_timestamp)"]
],
returns: %{
type: "non_neg_integer()",
description: "Ray-scaled linear interest factor: RAY + rate * (current - last) / SECONDS_PER_YEAR",
example: "calculate_linear_interest(rate, t, t) == ray (zero elapsed => factor = 1)"
}
)
@spec calculate_linear_interest(non_neg_integer(), non_neg_integer(), non_neg_integer()) ::
non_neg_integer()
def calculate_linear_interest(rate, last_update_timestamp, current_timestamp)
when is_integer(rate) and rate >= 0 and is_integer(last_update_timestamp) and last_update_timestamp >= 0 and
is_integer(current_timestamp) and current_timestamp >= last_update_timestamp do
@ray + div(rate * (current_timestamp - last_update_timestamp), @seconds_per_year)
end
# --- calculate_compounded_interest ---
api(
:calculate_compounded_interest,
"Compute the compounded-interest factor (in ray) accumulated between two timestamps at a given ray-scaled rate via Aave's polynomial approximation.",
params: [
rate: [kind: :value, description: "Annual interest rate in ray (10^27 scale)"],
last_update_timestamp: [kind: :value, description: "Unix-second timestamp of the last accrual"],
current_timestamp: [kind: :value, description: "Unix-second timestamp to accrue to (>= last_update_timestamp)"]
],
returns: %{
type: "non_neg_integer()",
description:
"Ray-scaled compounded interest factor. Slightly undercharges borrowers / underpays LPs vs. the ideal e^x formula — matches deployed protocol exactly."
}
)
@spec calculate_compounded_interest(non_neg_integer(), non_neg_integer(), non_neg_integer()) ::
non_neg_integer()
def calculate_compounded_interest(rate, last_update_timestamp, current_timestamp)
when is_integer(rate) and rate >= 0 and is_integer(last_update_timestamp) and last_update_timestamp >= 0 and
is_integer(current_timestamp) and current_timestamp >= last_update_timestamp do
exp = current_timestamp - last_update_timestamp
if exp == 0 do
@ray
else
x = div(rate * exp, @seconds_per_year)
@ray + x + ray_mul(x, div(x, 2) + ray_mul(x, div(x, 6)))
end
end
end