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
at commit 1e3d70c4151a94166ebc59e2eaa4aff6e6ba6978 (src/contracts/protocol/libraries/math/{WadRayMath,MathUtils}.sol).
API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
calculate_compounded_interest | 3 | Compute the compounded-interest factor (in ray) accumulated between two timestamps at a given ray-scaled rate via Aave's polynomial approximation. | rate: value, last_update_timestamp: value, current_timestamp: value |
calculate_linear_interest | 3 | Compute the linear-interest factor (in ray) accumulated between two timestamps at a given ray-scaled rate. | rate: value, last_update_timestamp: value, current_timestamp: value |
wad_to_ray | 1 | Cast a wad-scaled (10^18) integer up to ray (10^27). Exact, no rounding. | a: value |
ray_to_wad | 1 | Cast a ray-scaled (10^27) integer down to wad (10^18), rounding half-up. | a: value |
wad_div | 2 | Divide two wad-scaled (10^18) integers, rounding half-up. | a: value, b: value |
wad_mul | 2 | Multiply two wad-scaled (10^18) integers, rounding half-up. | a: value, b: value |
ray_div | 2 | Divide two ray-scaled (10^27) integers, rounding half-up. | a: value, b: value |
ray_mul | 2 | Multiply two ray-scaled (10^27) integers, rounding half-up. | a: value, b: value |
to_wad | 1 | Convert wad value (10^18 scale) to Decimal. | value: value |
to_ray | 1 | Convert Aave ray value (10^27 scale) to Decimal. | value: value |
to_health_factor | 1 | Convert Aave health factor (10^18 scale) to Decimal. | value: value |
to_ltv | 1 | Convert Aave basis-point value (10^4 scale) to Decimal ratio. | value: value |
to_usd | 1 | Convert Aave oracle price or base currency value (10^8 scale) to Decimal. | value: value |
Summary
Functions
Compute the compounded-interest factor (in ray) accumulated between two timestamps at a given ray-scaled rate via Aave's polynomial approximation.
Compute the linear-interest factor (in ray) accumulated between two timestamps at a given ray-scaled rate.
Divide two ray-scaled (10^27) integers, rounding half-up.
Multiply two ray-scaled (10^27) integers, rounding half-up.
Cast a ray-scaled (10^27) integer down to wad (10^18), rounding half-up.
Convert Aave health factor (10^18 scale) to Decimal.
Convert Aave basis-point value (10^4 scale) to Decimal ratio.
Convert Aave ray value (10^27 scale) to Decimal.
Convert Aave oracle price or base currency value (10^8 scale) to Decimal.
Convert wad value (10^18 scale) to Decimal.
Divide two wad-scaled (10^18) integers, rounding half-up.
Multiply two wad-scaled (10^18) integers, rounding half-up.
Cast a wad-scaled (10^18) integer up to ray (10^27). Exact, no rounding.
Functions
@spec calculate_compounded_interest( non_neg_integer(), non_neg_integer(), non_neg_integer() ) :: non_neg_integer()
Compute the compounded-interest factor (in ray) accumulated between two timestamps at a given ray-scaled rate via Aave's polynomial approximation.
Parameters
rate- Annual interest rate in ray (10^27 scale) (value)last_update_timestamp- Unix-second timestamp of the last accrual (value)current_timestamp- Unix-second timestamp to accrue to (>= last_update_timestamp) (value)
Returns
Ray-scaled compounded interest factor. Slightly undercharges borrowers / underpays LPs vs. the ideal e^x formula — matches deployed protocol exactly. (non_neg_integer())
# descripex:contract
%{
params: %{
rate: %{
description: "Annual interest rate in ray (10^27 scale)",
kind: :value
},
last_update_timestamp: %{
description: "Unix-second timestamp of the last accrual",
kind: :value
},
current_timestamp: %{
description: "Unix-second timestamp to accrue to (>= last_update_timestamp)",
kind: :value
}
},
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_linear_interest( non_neg_integer(), non_neg_integer(), non_neg_integer() ) :: non_neg_integer()
Compute the linear-interest factor (in ray) accumulated between two timestamps at a given ray-scaled rate.
Parameters
rate- Annual interest rate in ray (10^27 scale) (value)last_update_timestamp- Unix-second timestamp of the last accrual (value)current_timestamp- Unix-second timestamp to accrue to (>= last_update_timestamp) (value)
Returns
Ray-scaled linear interest factor: RAY + rate * (current - last) / SECONDS_PER_YEAR (non_neg_integer())
# descripex:contract
%{
params: %{
rate: %{
description: "Annual interest rate in ray (10^27 scale)",
kind: :value
},
last_update_timestamp: %{
description: "Unix-second timestamp of the last accrual",
kind: :value
},
current_timestamp: %{
description: "Unix-second timestamp to accrue to (>= last_update_timestamp)",
kind: :value
}
},
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 ray_div(non_neg_integer(), pos_integer()) :: non_neg_integer()
Divide two ray-scaled (10^27) integers, rounding half-up.
Parameters
a- Ray-scaled uint256 dividend (value)b- Ray-scaled uint256 divisor (non-zero) (value)
Returns
a / b in ray, rounded half-up via add-half-divisor-then-floor-divide (non_neg_integer())
# descripex:contract
%{
params: %{
a: %{description: "Ray-scaled uint256 dividend", kind: :value},
b: %{description: "Ray-scaled uint256 divisor (non-zero)", kind: :value}
},
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_mul(non_neg_integer(), non_neg_integer()) :: non_neg_integer()
Multiply two ray-scaled (10^27) integers, rounding half-up.
Parameters
a- Ray-scaled uint256 integer (value)b- Ray-scaled uint256 integer (value)
Returns
a * b in ray, rounded half-up via add-half-then-floor-divide (non_neg_integer())
# descripex:contract
%{
params: %{
a: %{description: "Ray-scaled uint256 integer", kind: :value},
b: %{description: "Ray-scaled uint256 integer", kind: :value}
},
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_to_wad(non_neg_integer()) :: non_neg_integer()
Cast a ray-scaled (10^27) integer down to wad (10^18), rounding half-up.
Parameters
a- Ray-scaled uint256 integer (value)
Returns
a rescaled to wad, rounded half-up at the wad_ray_ratio (10^9) midpoint (non_neg_integer())
# descripex:contract
%{
params: %{a: %{description: "Ray-scaled uint256 integer", kind: :value}},
returns: %{
type: "non_neg_integer()",
description: "a rescaled to wad, rounded half-up at the wad_ray_ratio (10^9) midpoint"
}
}
Convert Aave health factor (10^18 scale) to Decimal.
Parameters
value- Raw integer health factor from getUserAccountData (value)
Returns
Health factor (> 1 means not liquidatable) (Decimal.t())
# descripex:contract
%{
params: %{
value: %{
description: "Raw integer health factor from getUserAccountData",
kind: :value
}
},
returns: %{
type: "Decimal.t()",
description: "Health factor (> 1 means not liquidatable)",
example: "1_500_000_000_000_000_000 → Decimal.new(\"1.5\")"
}
}
Convert Aave basis-point value (10^4 scale) to Decimal ratio.
Parameters
value- Raw integer LTV ratio or liquidation threshold (basis points) (value)
Returns
Ratio between 0 and 1 (Decimal.t())
# descripex:contract
%{
params: %{
value: %{
description: "Raw integer LTV ratio or liquidation threshold (basis points)",
kind: :value
}
},
returns: %{
type: "Decimal.t()",
description: "Ratio between 0 and 1",
example: "8000 → Decimal.new(\"0.8\")"
}
}
Convert Aave ray value (10^27 scale) to Decimal.
Parameters
value- Raw integer interest rate in ray units (value)
Returns
Decimal interest rate (Decimal.t())
# descripex:contract
%{
params: %{
value: %{
description: "Raw integer interest rate in ray units",
kind: :value
}
},
returns: %{
type: "Decimal.t()",
description: "Decimal interest rate",
example: "100_000_000_000_000_000_000_000_000 (10^26) → Decimal.new(\"0.1\")"
}
}
Convert Aave oracle price or base currency value (10^8 scale) to Decimal.
Parameters
value- Raw integer from Aave oracle or base currency field (value)
Returns
USD value (Decimal.t())
# descripex:contract
%{
params: %{
value: %{
description: "Raw integer from Aave oracle or base currency field",
kind: :value
}
},
returns: %{
type: "Decimal.t()",
description: "USD value",
example: "123_456_789 → Decimal.new(\"1.23456789\")"
}
}
Convert wad value (10^18 scale) to Decimal.
Parameters
value- Raw integer scaled token amount in wad units (value)
Returns
Decimal token amount (Decimal.t())
# descripex:contract
%{
params: %{
value: %{
description: "Raw integer scaled token amount in wad units",
kind: :value
}
},
returns: %{
type: "Decimal.t()",
description: "Decimal token amount",
example: "1_000_000_000_000_000_000 (10^18) → Decimal.new(\"1.0\")"
}
}
@spec wad_div(non_neg_integer(), pos_integer()) :: non_neg_integer()
Divide two wad-scaled (10^18) integers, rounding half-up.
Parameters
a- Wad-scaled uint256 dividend (value)b- Wad-scaled uint256 divisor (non-zero) (value)
Returns
a / b in wad, rounded half-up via add-half-divisor-then-floor-divide (non_neg_integer())
# descripex:contract
%{
params: %{
a: %{description: "Wad-scaled uint256 dividend", kind: :value},
b: %{description: "Wad-scaled uint256 divisor (non-zero)", kind: :value}
},
returns: %{
type: "non_neg_integer()",
description: "a / b in wad, rounded half-up via add-half-divisor-then-floor-divide"
}
}
@spec wad_mul(non_neg_integer(), non_neg_integer()) :: non_neg_integer()
Multiply two wad-scaled (10^18) integers, rounding half-up.
Parameters
a- Wad-scaled uint256 integer (value)b- Wad-scaled uint256 integer (value)
Returns
a * b in wad, rounded half-up via add-half-then-floor-divide (non_neg_integer())
# descripex:contract
%{
params: %{
a: %{description: "Wad-scaled uint256 integer", kind: :value},
b: %{description: "Wad-scaled uint256 integer", kind: :value}
},
returns: %{
type: "non_neg_integer()",
description: "a * b in wad, rounded half-up via add-half-then-floor-divide"
}
}
@spec wad_to_ray(non_neg_integer()) :: non_neg_integer()
Cast a wad-scaled (10^18) integer up to ray (10^27). Exact, no rounding.
Parameters
a- Wad-scaled uint256 integer (value)
Returns
a rescaled to ray (a * 10^9) (non_neg_integer())
# descripex:contract
%{
params: %{a: %{description: "Wad-scaled uint256 integer", kind: :value}},
returns: %{
type: "non_neg_integer()",
description: "a rescaled to ray (a * 10^9)"
}
}