Skip to content
This repository has been archived by the owner on Aug 1, 2023. It is now read-only.

Commit

Permalink
[COMP-1165] fixed precision decimal implementation (wip)
Browse files Browse the repository at this point in the history
As discussed bigint + u8 for decimal representation also avoiding the
use of generics for the most part. For now the only imaginable way
to have errors in my mind is decimal mismatch.

Lots to fill in here and some open questions

Do we want this in a separate crate? I imagine it is possible the
eth-rpc crate may ultimately depend on this functionality.

I just figured out that u128 is natively available in rust on all
targets as LLVM has support for it rust simply exposes that. See
[this pr](rust-lang/rust#38482) from late
2016. This has been [stablized in 1.26](https://blog.rust-lang.org/2018/05/10/Rust-1.26.html).
The representable range here even with a token that is using 18
decimals of precision is 19 9s. If you have 10^19 of something
I think it is alright to say that you need to do two transactions.

I believe that u128 represents the best combination of speed
flexibility and consistency. It is very unlikely to me that
any token balance will natively overflow this representation.
  • Loading branch information
Wayne Nilsen committed Nov 20, 2020
1 parent 2353bc9 commit 1a46e3f
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 1 deletion.
15 changes: 14 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pallets/cash/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ version = '1.3.4'
frame-support = { default-features = false, version = '2.0.0' }
frame-system = { default-features = false, version = '2.0.0' }
runtime-interfaces = { default-features = false, version = '1.0.0', path="../runtime-interfaces"}
num-bigint = { default-features = false, version = "0.3" }
num-traits = { default-features = false, version = "0.2" }

[dev-dependencies]
sp-core = { default-features = false, version = '2.0.0' }
Expand Down
104 changes: 104 additions & 0 deletions pallets/cash/src/fixed_precision.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use num_bigint::BigUint;

/// The type of the decimal field.
pub type DecimalType = u8;

/// The type of the mantissa field
pub type MantissaType = BigUint;

/// Represents a decimal number in base 10 with fixed precision. The number can be as precise as
/// the maximum value of the DecimalType and there is no upper bound on the numbers size.
///
/// For example, if the mantissa is 123456789 and decimals is 4 the number that is represented is
/// 12345.6789.
#[derive(Clone, PartialEq, Debug)]
pub struct FixedPrecision {
mantissa: MantissaType,
decimals: DecimalType,
}

/// Error type for fixed precision math.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum MathError {
PrecisionMismatch,
}

impl FixedPrecision {
/// Create a new FixedPrecision number from parts. The mantissa is used "raw" and not scaled
/// in any way
pub fn new<T: Into<MantissaType>>(mantissa: T, decimals: DecimalType) -> Self {
let mantissa = mantissa.into();
FixedPrecision { mantissa, decimals }
}

/// A helper function for downstream math functions, returns an error if and only if the
/// number of decimals do not match
fn check_decimals(self: &Self, other: &Self) -> Result<(), MathError> {
if self.decimals == other.decimals {
Ok(())
} else {
Err(MathError::PrecisionMismatch)
}
}

/// Add two FixedPrecision numbers together. Note the signature uses borrowed values this is
/// because the underlying storage is arbitrarily large and we do not want to copy the values.
pub fn add(self: &Self, rhs: &Self) -> Result<Self, MathError> {
self.check_decimals(rhs)?;
// note - this cannot fail with BigUint but that will change based on underlying storage
let new_mantissa = &self.mantissa + &rhs.mantissa;

Ok(Self::new(new_mantissa, self.decimals))
}

/// Create the representation of 1 in the number of decimals requested. For example one(3)
/// will return a fixed precision number with 1000 as the mantissa and 3 as the number of decimals
pub fn one<T: Into<DecimalType> + Copy>(decimals: T) -> Self {
let ten: MantissaType = 10u8.into();
let decimals: DecimalType = decimals.into();
let new_mantissa = ten.pow(decimals as u32);
Self::new(new_mantissa, decimals)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_one() {
let expected = FixedPrecision::new(1000u32, 3);
let actual = FixedPrecision::one(3);
assert_eq!(expected, actual);
}

#[test]
fn test_add_happy_path() -> Result<(), MathError> {
let a = FixedPrecision::one(2);
let b = FixedPrecision::one(2);
// note - automatic borrow of `a` here (rust elides the (&a).add for you
let actual = a.add(&b)?;

// make sure nothing has changed
assert_eq!(a, b);
assert_eq!(a, FixedPrecision::one(2));
assert_eq!(b, FixedPrecision::one(2));

let expected = FixedPrecision::new(200u8, 2);
assert_eq!(actual, expected);

Ok(())
}

#[test]
fn test_add_decimal_mismatch() {
let a = FixedPrecision::one(2);
let b = FixedPrecision::one(3);
// note - automatic borrow of `a` here (rust elides the (&a).add for you
let actual = a.add(&b);
assert!(actual.is_err());
let actual = actual.err().unwrap();
let expected = MathError::PrecisionMismatch;
assert_eq!(actual, expected);
}
}
1 change: 1 addition & 0 deletions pallets/cash/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use frame_system::ensure_signed;
#[cfg(test)]
mod mock;

mod fixed_precision;
#[cfg(test)]
mod tests;

Expand Down

0 comments on commit 1a46e3f

Please sign in to comment.