Onchain.Aave.Math (onchain_aave v0.2.0)

Copy Markdown View Source

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.

FunctionExponentAave Usage
to_usd/110^8Oracle prices, base currency values
to_ltv/110^4LTV ratios, liquidation thresholds (basis points)
to_health_factor/110^18getUserAccountData health factor
to_ray/110^27Interest rates (variable/stable borrow)
to_wad/110^18Scaled 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.

FunctionSolidity equivalent
ray_mul/2WadRayMath.rayMul
ray_div/2WadRayMath.rayDiv
wad_mul/2WadRayMath.wadMul
wad_div/2WadRayMath.wadDiv
ray_to_wad/1WadRayMath.rayToWad
wad_to_ray/1WadRayMath.wadToRay
calculate_linear_interest/3MathUtils.calculateLinearInterest
calculate_compounded_interest/3MathUtils.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

FunctionArityDescriptionParam Kinds
calculate_compounded_interest3Compute 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_interest3Compute 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_ray1Cast a wad-scaled (10^18) integer up to ray (10^27). Exact, no rounding.a: value
ray_to_wad1Cast a ray-scaled (10^27) integer down to wad (10^18), rounding half-up.a: value
wad_div2Divide two wad-scaled (10^18) integers, rounding half-up.a: value, b: value
wad_mul2Multiply two wad-scaled (10^18) integers, rounding half-up.a: value, b: value
ray_div2Divide two ray-scaled (10^27) integers, rounding half-up.a: value, b: value
ray_mul2Multiply two ray-scaled (10^27) integers, rounding half-up.a: value, b: value
to_wad1Convert wad value (10^18 scale) to Decimal.value: value
to_ray1Convert Aave ray value (10^27 scale) to Decimal.value: value
to_health_factor1Convert Aave health factor (10^18 scale) to Decimal.value: value
to_ltv1Convert Aave basis-point value (10^4 scale) to Decimal ratio.value: value
to_usd1Convert 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

calculate_compounded_interest(rate, last_update_timestamp, current_timestamp)

@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."
  }
}

calculate_linear_interest(rate, last_update_timestamp, current_timestamp)

@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)"
  }
}

ray_div(a, b)

@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)"
  }
}

ray_mul(a, b)

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"
  }
}

ray_to_wad(a)

@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"
  }
}

to_health_factor(value)

@spec to_health_factor(integer()) :: Decimal.t()

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\")"
  }
}

to_ltv(value)

@spec to_ltv(integer()) :: Decimal.t()

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\")"
  }
}

to_ray(value)

@spec to_ray(integer()) :: Decimal.t()

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\")"
  }
}

to_usd(value)

@spec to_usd(integer()) :: Decimal.t()

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\")"
  }
}

to_wad(value)

@spec to_wad(integer()) :: Decimal.t()

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\")"
  }
}

wad_div(a, b)

@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"
  }
}

wad_mul(a, b)

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"
  }
}

wad_to_ray(a)

@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)"
  }
}