ui and native units in SolOna

Solana program have a 200K computing limit as of now (v1.8.12)

Solana Runtime support float through software emulation, so it’s heavy on the computing usage, see below :

post image

Using Unsigned Integer

So you’r better off using Unsigned Integers (a.k.a u8,16,…).

With Unsigned Integers you have a few options, let’s take u64 for instance :

// unsafe u64 maths, which either crash or overflow
let a = u64::MAX + 1u64;
let b = u64::MAX / u64::ZERO;

// checked u64 maths
let c = u64::MAX.checked_mul(10u64).unwrap_or(math_err!());
let d = u64::MIN.checked_sub(1u64).unwrap_or(math_err!());
//...

That’s already a better way to do calculus, and not much more expensive.

But sometimes you need to do precise computation, as you might need to prevent drifting / precision loss.

Using Fixed Maths (or spl_maths::PreciseNumber)

In order to do precise calculation, there are fixed math library that put your Integer in a higher definition Integer (u64 into a u128 for instance). It then decides that the 80 first bits will code for the Integer part, and that the 48 following bits will code for the Real part. (that’s I80F48 in the fixed crate)

You can now do, still not too computing intensive, “float operations”.

// Here with an u64 as input, we can safely put it inside the 80bits.
let a = I80F48::from_num(u64::MAX)
        .checked_mul(u64::MAX)
        .ok_or(math_err!())?
        .checked_to_num()
        .ok_or(math_err!())?;

// Here it might work, or not
let b = I80F48::checked_from_num(u128::MAX)
        .checked_mul(u64::MAX)
        .ok_or(math_err!())?
        .checked_to_num()
        .ok_or(math_err!())?;

Both cases above don’t have precision loss, but if you start using divisions, you need to be careful:

let a: u32 = I80F48::from_num(100::u64)
        .checked_div(3::u64) 
// 78 zeroes, 33, then 48 threes, representing 33.33333....
        .ok_or(math_err!())?
        .checked_to_num() // 33::u32
        .ok_or(math_err!())?;

In that case, it’s fine, but you need to think a specific strategy if for example you’r minting LP tokens for users liquidities, as you always want to round in the direction at your advantage.

Seems unfair, although the few Lamports imbalance on each trade isn’t much for the user, but it’s a lot for a protocol over time.

Rules of thumb

Front end : deals with UIUnit ONLY

3.12 UIUnits

Front-BackEnd : dual way UIUnits <-> NativeUnits

3.12 UIUnits from the FE
 ... into 3.12 * 10 ** mint.decimals NativeUnits to the BE

// Assuming mint.decimals = 6
312_000_000 NativeUnits from the BE
 ... into 312_000_000 / 10 ** mint.decimals UIUnits to the FE

BackEnd : deals in NativeUnits only


If the value is not an amount, declare a common SOMETHING_BASE (Slippage, fees...) in the FBE and BE.

If you do math in the program, and for easier conversions, use fixed maths (or spl_maths).

Once you've done your calculations, remember to pay attention to the ROUNDING. Even if you did fixed maths, you'll possibly expose yourself to random floor/ceil.

You want that to be deterministic, so depending of your business logic, ensure that it is always handle to your program advantage (for instance, if needed 0.1 and 0.9 should be explicitly floored)

If you use partial result into follow up computation, don’t do back and forth between fixed and UInts, as you’ll drift.

And finally, include some On Chain accounting if you can. I like to have a higher level state account for my programs that track all the ins and outs. After each operations that change this accounting, I do check + update, and only if everything make sense the transaction is allowed to returns Ok(()).

You can do that in the console at first, that's out of my league it the based web dev.
You can do that in the console at first, that's out of my league it the based web dev.