Fraction: Q68.60 fixed-point numbers for Solana finance #
A fixed-point number with 60 fractional bits, modelled here at the
spec layer with an unbounded Nat underlying representation
(so omega works without ceremony). A future Fraction128 companion
will refine this to the u128-bounded on-chain shape used by Kamino
and other DeFi protocols (Kamino's U68F60).
A Fraction f represents the real number f.bits / 2^60.
This module is Layer 1 in the long-term Solanalib stack: a numeric
foundation that every domain primitive (interest curves, exchange
rates, slippage, vesting fractions, …) sits on top of. The conscious
choice to use Nat rather than u128 at this layer follows the
seL4 / CompCert pattern — prove mathematical properties on the
abstract model, refine to the bounded representation separately.
Main definitions #
Fraction— a fixed-point number, conceptuallybits / 2^60.Fraction.zero,Fraction.one— the additive and multiplicative units.Fraction.fromNat/Fraction.toFloor— integer/fraction conversions.Fraction.add,Fraction.mul— arithmetic at the fixed-point scale.
Main statements (this commit) #
A minimal kit:
add_zero,zero_add,add_comm—Fraction.addis a commutative monoid.mul_one,one_mul—Fraction.oneis the multiplicative identity.
Multiplication is approximately associative — the truncating division
by scale after each multiplication introduces a rounding error of at
most one ULP. A bound on that error is deferred to a follow-up file.
Conventions #
The "scale" exponent (60) is exposed as Fraction.scaleBits so future
refinements that need a different precision (e.g. Q.32, Q.96) can be
parameterised cleanly.
A fixed-point number with scaleBits fractional bits, where
scaleBits = 60 to match the U68F60 convention common in Solana DeFi.
Fraction.mk b represents the real number b / 2^60.
- bits : LamportsUnchecked
The underlying integer encoding:
bits = realValue * 2^60.
Instances For
Equations
- Solanalib.instReprFraction.repr x✝ prec✝ = Std.Format.bracket "{ " (Std.Format.nil ++ Std.Format.text "bits" ++ Std.Format.text " := " ++ (Std.Format.nest 8 (repr x✝.bits)).group) " }"
Instances For
Equations
- Solanalib.instReprFraction = { reprPrec := Solanalib.instReprFraction.repr }
The number of fractional bits in the representation.
Equations
Instances For
The scale factor: 2^scaleBits = 2^60. Every operation that
preserves the fixed-point invariant divides intermediate products by
this constant.
Equations
Instances For
scale > 0 — useful for division-cancellation lemmas.
The multiplicative identity: encoding of 1.0.
Equations
Instances For
Convert an integer n to its fixed-point encoding n * 2^60.
Equations
- Solanalib.Fraction.fromNat n = { bits := n * Solanalib.Fraction.scale }
Instances For
Subtraction with an explicit underflow precondition: b ≤ a (on
the underlying bit encoding). Returns ⟨a.bits − b.bits⟩. The
precondition is mandatory: matches the dependent-type discipline used
throughout Solanalib (compare Account.debit).
Instances For
Division with an explicit non-zero precondition on the divisor.
Returns ⟨a.bits * scale / b.bits⟩ — the multiplication by scale
corrects the lost-scale that would otherwise result from bits / bits.
Like every truncating division in Fraction, this rounds toward zero.
A rounded-up companion (divCeil) is a future refinement.
Instances For
Order #
Fraction inherits its order from the underlying Nat encoding:
a ≤ b ↔ a.bits ≤ b.bits. Comparisons are decidable and omega-friendly
once unfolded via the iff lemmas.
Equations
- Solanalib.Fraction.instLE = { le := fun (a b : Solanalib.Fraction) => a.bits ≤ b.bits }
Equations
- Solanalib.Fraction.instLT = { lt := fun (a b : Solanalib.Fraction) => a.bits < b.bits }
Equations
- a.instDecidableLe b = a.bits.decLe b.bits
Equations
- a.instDecidableLt b = a.bits.decLt b.bits
Theorems #
Order theorems #
Round-trip theorems #
Division by one is identity.
Bridging to Lamports #
Lamports (= UInt64) and Fraction (= Q68.60) are the two numeric
foundations Solana programs juggle: integer lamport counts and fractional
ratios. The lift ofLamports is exact (no precision loss — Lamports fit
comfortably in the 68 integer bits). The reverse projection
toLamports? returns Option because the Fraction's integer floor can
exceed u64::MAX.
Project a Fraction to Lamports by taking the integer floor.
Returns none if the floor exceeds u64::MAX.
Equations
- f.toLamports? = if h : f.toFloor < UInt64.size then some (UInt64.ofNatLT f.toFloor h) else none
Instances For
Round-trip: (ofLamports l).toFloor = l.toNat. The Q68.60 lift
preserves the integer value exactly.