diff --git a/.gas-snapshot b/.gas-snapshot index 3eb4644d..f29bab8e 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,25 +1,31 @@ -DecimalFloatAbsTest:testAbsDeployed(bytes32) (runs: 5096, μ: 2379271, ~: 2379213) -DecimalFloatAddTest:testAddDeployed(bytes32,bytes32) (runs: 5096, μ: 2381906, ~: 2381585) -DecimalFloatConstantsTest:testEDeployed() (gas: 2378346) -DecimalFloatConstantsTest:testMaxValueDeployed() (gas: 2378358) -DecimalFloatConstantsTest:testMinNegativeValueDeployed() (gas: 2378400) -DecimalFloatDivTest:testDivDeployed(bytes32,bytes32) (runs: 5096, μ: 2381775, ~: 2381627) -DecimalFloatEqTest:testEqDeployed(bytes32,bytes32) (runs: 5096, μ: 2379590, ~: 2379516) -DecimalFloatFloorTest:testFloorDeployed(bytes32) (runs: 5096, μ: 2379387, ~: 2379371) -DecimalFloatFormatTest:testFormatDeployed(bytes32) (runs: 5096, μ: 2383231, ~: 2383074) -DecimalFloatFracTest:testFracDeployed(bytes32) (runs: 5096, μ: 2379436, ~: 2379419) -DecimalFloatGtTest:testGtDeployed(bytes32,bytes32) (runs: 5096, μ: 2379510, ~: 2379436) -DecimalFloatGteTest:testGteDeployed(bytes32,bytes32) (runs: 5096, μ: 2379583, ~: 2379509) -DecimalFloatInvTest:testInvDeployed(bytes32) (runs: 5096, μ: 2380819, ~: 2380741) -DecimalFloatIsZeroTest:testIsZeroDeployed(bytes32) (runs: 5096, μ: 2378745, ~: 2378745) -DecimalFloatLtTest:testLtDeployed(bytes32,bytes32) (runs: 5096, μ: 2379531, ~: 2379457) -DecimalFloatLteTest:testLteDeployed(bytes32,bytes32) (runs: 5096, μ: 2379540, ~: 2379466) -DecimalFloatMaxTest:testMaxDeployed(bytes32,bytes32) (runs: 5096, μ: 2379593, ~: 2379532) -DecimalFloatMinTest:testMinDeployed(bytes32,bytes32) (runs: 5096, μ: 2379590, ~: 2379530) -DecimalFloatMinusTest:testMinusDeployed(bytes32) (runs: 5096, μ: 2379305, ~: 2379305) -DecimalFloatMulTest:testMulDeployed(bytes32,bytes32) (runs: 5096, μ: 2381426, ~: 2382488) -DecimalFloatParseTest:testParseDeployed(string) (runs: 5096, μ: 2381982, ~: 2381851) -DecimalFloatSubTest:testSubDeployed(bytes32,bytes32) (runs: 5096, μ: 2382255, ~: 2381946) +DecimalFloatAbsTest:testAbsDeployed(bytes32) (runs: 5096, μ: 2425711, ~: 2425653) +DecimalFloatAddTest:testAddDeployed(bytes32,bytes32) (runs: 5096, μ: 2428475, ~: 2428157) +DecimalFloatConstantsTest:testEDeployed() (gas: 2424851) +DecimalFloatConstantsTest:testMaxValueDeployed() (gas: 2424885) +DecimalFloatConstantsTest:testMinNegativeValueDeployed() (gas: 2424883) +DecimalFloatDivTest:testDivDeployed(bytes32,bytes32) (runs: 5096, μ: 2428214, ~: 2428066) +DecimalFloatEqTest:testEqDeployed(bytes32,bytes32) (runs: 5096, μ: 2426116, ~: 2426043) +DecimalFloatFloorTest:testFloorDeployed(bytes32) (runs: 5096, μ: 2425915, ~: 2425898) +DecimalFloatFormatTest:testFormatDeployed(bytes32) (runs: 5096, μ: 2429752, ~: 2429579) +DecimalFloatFracTest:testFracDeployed(bytes32) (runs: 5096, μ: 2425963, ~: 2425946) +DecimalFloatFromFixedDecimalLosslessTest:testFromFixedDecimalLosslessDeployed(uint256,uint8) (runs: 5096, μ: 2426516, ~: 2426450) +DecimalFloatFromFixedDecimalLossyTest:testFromFixedDecimalLossyDeployed(uint256,uint8) (runs: 5096, μ: 2427004, ~: 2426920) +DecimalFloatGtTest:testGtDeployed(bytes32,bytes32) (runs: 5096, μ: 2426036, ~: 2425963) +DecimalFloatGteTest:testGteDeployed(bytes32,bytes32) (runs: 5096, μ: 2426109, ~: 2426036) +DecimalFloatInvTest:testInvDeployed(bytes32) (runs: 5096, μ: 2427304, ~: 2427226) +DecimalFloatIsZeroTest:testIsZeroDeployed(bytes32) (runs: 5096, μ: 2425228, ~: 2425228) +DecimalFloatLtTest:testLtDeployed(bytes32,bytes32) (runs: 5096, μ: 2426014, ~: 2425940) +DecimalFloatLteTest:testLteDeployed(bytes32,bytes32) (runs: 5096, μ: 2426067, ~: 2425993) +DecimalFloatMaxTest:testMaxDeployed(bytes32,bytes32) (runs: 5096, μ: 2426098, ~: 2426037) +DecimalFloatMinTest:testMinDeployed(bytes32,bytes32) (runs: 5096, μ: 2426096, ~: 2426035) +DecimalFloatMinusTest:testMinusDeployed(bytes32) (runs: 5096, μ: 2425788, ~: 2425788) +DecimalFloatMulTest:testMulDeployed(bytes32,bytes32) (runs: 5096, μ: 2427932, ~: 2428993) +DecimalFloatPackLosslessTest:testPackDeployed(int224,int32) (runs: 5096, μ: 2426066, ~: 2426066) +DecimalFloatParseTest:testParseDeployed(string) (runs: 5096, μ: 2428465, ~: 2428334) +DecimalFloatSubTest:testSubDeployed(bytes32,bytes32) (runs: 5096, μ: 2428809, ~: 2428495) +DecimalFloatToFixedDecimalLosslessTest:testToFixedDecimalLosslessDeployed(bytes32,uint8) (runs: 5096, μ: 2427141, ~: 2427033) +DecimalFloatToFixedDecimalLossyTest:testToFixedDecimalLossyDeployed(bytes32,uint8) (runs: 5096, μ: 2427285, ~: 2427562) +DecimalFloatUnpackTest:testUnpackDeployed(bytes32) (runs: 5096, μ: 2425741, ~: 2425741) LibDecimalFloatAbsTest:testAbsMinValue(int32) (runs: 5096, μ: 5121, ~: 5121) LibDecimalFloatAbsTest:testAbsNegative(int256,int32) (runs: 5096, μ: 10475, ~: 10702) LibDecimalFloatAbsTest:testAbsNonNegative(int256,int32) (runs: 5096, μ: 9641, ~: 9392) diff --git a/crates/float/src/evm.rs b/crates/float/src/evm.rs index aab41886..2867fecf 100644 --- a/crates/float/src/evm.rs +++ b/crates/float/src/evm.rs @@ -12,32 +12,68 @@ use std::cell::RefCell; use crate::{DecimalFloat, FloatError}; +#[cfg(test)] +use crate::TestDecimalFloat; + /// Fixed address where the DecimalFloat contract is deployed in the in-memory EVM. /// This arbitrary address is used consistently across all Calculator instances. pub(crate) const FLOAT_ADDRESS: Address = address!("00000000000000000000000000000000000f10a2"); +#[cfg(test)] +/// Fixed address where the TestDecimalFloat contract is deployed in the in-memory EVM. +pub(crate) const TEST_FLOAT_ADDRESS: Address = address!("00000000000000000000000000000000000f10a3"); + type EvmContext = Context; type LocalEvm = Evm, EthPrecompiles>; thread_local! { pub(crate) static LOCAL_EVM: RefCell = { let mut db = InMemoryDB::default(); + + // Deploy DecimalFloat contract let bytecode = revm::state::Bytecode::new_legacy(DecimalFloat::DEPLOYED_BYTECODE.clone()); let account_info = revm::state::AccountInfo::default().with_code(bytecode); db.insert_account_info(FLOAT_ADDRESS, account_info); + #[cfg(test)] + { + // Deploy TestDecimalFloat contract for testing + let test_bytecode = revm::state::Bytecode::new_legacy(TestDecimalFloat::DEPLOYED_BYTECODE.clone()); + let test_account_info = revm::state::AccountInfo::default().with_code(test_bytecode); + db.insert_account_info(TEST_FLOAT_ADDRESS, test_account_info); + } + let evm = Context::mainnet().with_db(db).build_mainnet(); RefCell::new(evm) }; } pub(crate) fn execute_call(calldata: Bytes, process_output: F) -> Result +where + F: FnOnce(Bytes) -> Result, +{ + execute_call_at_address(FLOAT_ADDRESS, calldata, process_output) +} + +#[cfg(test)] +pub(crate) fn execute_test_call(calldata: Bytes, process_output: F) -> Result +where + F: FnOnce(Bytes) -> Result, +{ + execute_call_at_address(TEST_FLOAT_ADDRESS, calldata, process_output) +} + +fn execute_call_at_address( + address: Address, + calldata: Bytes, + process_output: F, +) -> Result where F: FnOnce(Bytes) -> Result, { let result = LOCAL_EVM.try_with(|evm| { let evm = &mut *evm.borrow_mut(); - let result_and_state = evm.transact_system_call_finalize(FLOAT_ADDRESS, calldata)?; + let result_and_state = evm.transact_system_call_finalize(address, calldata)?; Ok::<_, FloatError>(result_and_state.result) })??; diff --git a/crates/float/src/js_api.rs b/crates/float/src/js_api.rs index efdc0a4c..bc88a3fe 100644 --- a/crates/float/src/js_api.rs +++ b/crates/float/src/js_api.rs @@ -1,5 +1,4 @@ use crate::{Float, FloatError}; -use alloy::primitives::aliases::I224; use revm::primitives::{B256, U256}; use std::{ ops::{Add, Div, Mul, Neg, Sub}, @@ -176,32 +175,65 @@ impl Float { .map_err(|e| FloatError::JsSysError(e.to_string().into())) } - /// Packs a coefficient and exponent into a `Float` in a lossless manner. + /// Converts a fixed-point decimal value to a `Float` using the specified number of decimals lossy. /// /// # Arguments /// - /// * `coefficient` - The coefficient as an `string`. - /// * `exponent` - The exponent as an `number`. + /// * `value` - The fixed-point decimal value as a `string`. + /// * `decimals` - The number of decimals in the fixed-point representation. /// /// # Returns /// - /// * `Ok(Float)` - The packed float. - /// * `Err(FloatError)` - If the packing fails (e.g., overflow). + /// * `Ok(Float)` - The resulting `Float` value. + /// * `Err(FloatError)` - If the conversion fails. /// /// # Example /// /// ```typescript - /// const floatResult = Float.packLossless("314", -2); + /// const floatResult = Float.fromFixedDecimalLossy("12345", 2); /// if (floatResult.error) { /// console.error(floatResult.error); /// } /// const float = floatResult.value; - /// assert(float.format() === "3.14"); + /// assert(float.format() === "123.45"); + /// ``` + #[wasm_export(js_name = "fromFixedDecimalLossy", preserve_js_class)] + pub fn from_fixed_decimal_lossy_js(value: BigInt, decimals: u8) -> Result { + let value_str: String = value.to_string(10)?.into(); + let val = U256::from_str(&value_str)?; + Self::from_fixed_decimal_lossy(val, decimals) + } + + /// Converts a `Float` to a fixed-point decimal value using the specified number of decimals lossy. + /// + /// # Arguments + /// + /// * `decimals` - The number of decimals in the fixed-point representation. + /// + /// # Returns + /// + /// * `Ok(String)` - The resulting fixed-point decimal value as a string. + /// * `Err(FloatError)` - If the conversion fails. + /// + /// # Example + /// + /// ```typescript + /// const float = Float.fromFixedDecimal(12345n, 3).value!; + /// const result = float.toFixedDecimalLossy(2); + /// if (result.error) { + /// console.error(result.error); + /// } + /// assert(result.value === "1234"); /// ``` - #[wasm_export(js_name = "packLossless", preserve_js_class)] - pub fn pack_lossless_js(coefficient: String, exponent: i32) -> Result { - let val = I224::from_str(&coefficient)?; - Self::pack_lossless(val, exponent) + #[wasm_export( + js_name = "toFixedDecimalLossy", + preserve_js_class, + unchecked_return_type = "bigint" + )] + pub fn to_fixed_decimal_lossy_js(&self, decimals: u8) -> Result { + let fixed = self.to_fixed_decimal_lossy(decimals)?; + BigInt::from_str(&fixed.to_string()) + .map_err(|e| FloatError::JsSysError(e.to_string().into())) } /// Parses a decimal string into a `Float`. diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index fccbac87..bc535490 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -1,5 +1,4 @@ use alloy::hex::FromHex; -use alloy::primitives::aliases::I224; use alloy::primitives::{B256, Bytes}; use alloy::{sol, sol_types::SolCall}; use revm::primitives::{U256, fixed_bytes}; @@ -7,6 +6,9 @@ use serde::{Deserialize, Serialize}; use std::ops::{Add, Div, Mul, Neg, Sub}; use wasm_bindgen_utils::prelude::*; +#[cfg(test)] +use alloy::primitives::aliases::I224; + pub mod error; mod evm; pub mod js_api; @@ -14,6 +16,8 @@ pub mod js_api; use error::DecimalFloatErrorSelector; pub use error::FloatError; use evm::execute_call; +#[cfg(test)] +use evm::execute_test_call; sol!( #![sol(all_derives)] @@ -21,6 +25,13 @@ sol!( "../../out/DecimalFloat.sol/DecimalFloat.json" ); +#[cfg(test)] +sol!( + #![sol(all_derives)] + TestDecimalFloat, + "../../out/TestDecimalFloat.sol/TestDecimalFloat.json" +); + #[derive(Debug, Copy, Clone, Default, Serialize, Deserialize, Hash)] #[wasm_bindgen] pub struct Float(B256); @@ -68,13 +79,11 @@ impl Float { /// anyhow::Ok(()) /// ``` pub fn from_fixed_decimal(value: U256, decimals: u8) -> Result { - let calldata = - DecimalFloat::fromFixedDecimalLosslessPackedCall { value, decimals }.abi_encode(); + let calldata = DecimalFloat::fromFixedDecimalLosslessCall { value, decimals }.abi_encode(); execute_call(Bytes::from(calldata), |output| { - let decoded = DecimalFloat::fromFixedDecimalLosslessPackedCall::abi_decode_returns( - output.as_ref(), - )?; + let decoded = + DecimalFloat::fromFixedDecimalLosslessCall::abi_decode_returns(output.as_ref())?; Ok(Float(decoded)) }) } @@ -114,6 +123,77 @@ impl Float { }) } + /// Converts a fixed-point decimal value to a `Float` using the specified number of decimals lossy. + /// + /// # Arguments + /// + /// * `value` - The fixed-point decimal value as a `U256`. + /// * `decimals` - The number of decimals in the fixed-point representation. + /// + /// # Returns + /// + /// * `Ok(Float)` - The resulting `Float` value. + /// * `Err(FloatError)` - If the conversion fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// use alloy::primitives::U256; + /// + /// // 123.45 with 2 decimals is represented as 12345 + /// let value = U256::from(12345u64); + /// let decimals = 2u8; + /// let float = Float::from_fixed_decimal_lossy(value, decimals)?; + /// assert_eq!(float.format()?, "123.45"); + /// + /// anyhow::Ok(()) + /// ``` + pub fn from_fixed_decimal_lossy(value: U256, decimals: u8) -> Result { + let calldata = DecimalFloat::fromFixedDecimalLossyCall { value, decimals }.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = + DecimalFloat::fromFixedDecimalLossyCall::abi_decode_returns(output.as_ref())?; + Ok(Float(decoded._0)) + }) + } + + /// Converts a `Float` to a fixed-point decimal value using the specified number of decimals lossy. + /// + /// # Arguments + /// + /// * `decimals` - The number of decimals in the fixed-point representation. + /// + /// # Returns + /// + /// * `Ok(U256)` - The resulting fixed-point decimal value. + /// * `Err(FloatError)` - If the conversion fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// use alloy::primitives::U256; + /// + /// // 123.45 with 2 decimals becomes 12345 + /// let float = Float::from_fixed_decimal(U256::from(12345), 3)?; + /// let fixed = float.to_fixed_decimal_lossy(2)?; + /// assert_eq!(fixed, U256::from(1234u64)); + /// + /// anyhow::Ok(()) + /// ``` + pub fn to_fixed_decimal_lossy(self, decimals: u8) -> Result { + let Float(float) = self; + let calldata = DecimalFloat::toFixedDecimalLossyCall { float, decimals }.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = + DecimalFloat::toFixedDecimalLossyCall::abi_decode_returns(output.as_ref())?; + Ok(decoded._0) + }) + } + /// Packs a coefficient and exponent into a `Float` in a lossless manner. /// /// # Arguments @@ -140,29 +220,30 @@ impl Float { /// /// anyhow::Ok(()) /// ``` + #[cfg(test)] pub fn pack_lossless(coefficient: I224, exponent: i32) -> Result { - let calldata = DecimalFloat::packLosslessCall { + let calldata = TestDecimalFloat::packLosslessCall { coefficient, exponent, } .abi_encode(); - execute_call(Bytes::from(calldata), |output| { - let decoded = DecimalFloat::packLosslessCall::abi_decode_returns(output.as_ref())?; + execute_test_call(Bytes::from(calldata), |output| { + let decoded = TestDecimalFloat::packLosslessCall::abi_decode_returns(output.as_ref())?; Ok(Float(decoded)) }) } #[cfg(test)] - fn unpack(self) -> Result<(I224, i32), FloatError> { + fn unpack(self) -> Result<(alloy::primitives::I256, alloy::primitives::I256), FloatError> { let Float(float) = self; - let calldata = DecimalFloat::unpackCall { float }.abi_encode(); + let calldata = TestDecimalFloat::unpackCall { float }.abi_encode(); - execute_call(Bytes::from(calldata), |output| { - let DecimalFloat::unpackReturn { + execute_test_call(Bytes::from(calldata), |output| { + let TestDecimalFloat::unpackReturn { _0: coefficient, _1: exponent, - } = DecimalFloat::unpackCall::abi_decode_returns(output.as_ref())?; + } = TestDecimalFloat::unpackCall::abi_decode_returns(output.as_ref())?; Ok((coefficient, exponent)) }) @@ -1602,4 +1683,56 @@ mod tests { prop_assert!(gte); // gt } } + + #[test] + fn test_from_fixed_decimal_lossy() { + let cases = vec![ + (U256::from(0u128), 0u8, "0"), + (U256::from(0u128), 18u8, "0"), + (U256::from(1u128), 18u8, "1e-18"), + (U256::from(123456789u128), 0u8, "123456789"), + (U256::from(123456789u128), 2u8, "123456789e-2"), + (U256::from(1000000000000000000u128), 18u8, "1"), + ]; + + for (amount, decimals, expected) in cases { + let float = Float::from_fixed_decimal_lossy(amount, decimals).expect("should convert"); + let expected = Float::parse(expected.to_string()).unwrap(); + assert!(float.eq(expected).unwrap()); + } + } + + #[test] + fn test_to_fixed_decimal_lossy() { + let cases = vec![ + (U256::from(0), 0u8, 0u128), + (U256::from(0), 18u8, 0u128), + (U256::from(1), 18u8, 0u128), + (U256::from(123456789), 0u8, 12345678u128), + (U256::from(123456789), 2u8, 12345678u128), + ]; + + for (input, decimals, expected) in cases { + let float = Float::from_fixed_decimal(input, decimals + 1).unwrap(); + let fixed = float.to_fixed_decimal_lossy(decimals).unwrap(); + assert_eq!(fixed, U256::from(expected)); + } + } + + proptest! { + #[test] + fn test_from_to_fixed_decimal_lossy_valid_range(coeff in any::(), decimals in 0u8..=66u8) { + prop_assume!(coeff >= I224::ZERO); + + let exponent = -(decimals as i32 + 1); + let value = U256::from(coeff); + + let float = Float::from_fixed_decimal_lossy(value, decimals + 1).unwrap(); + let expected = Float::pack_lossless(coeff, exponent).unwrap(); + prop_assert!(float.eq(expected).unwrap()); + + let fixed = float.to_fixed_decimal_lossy(decimals).unwrap(); + assert_eq!(fixed, value / U256::from(10)); + } + } } diff --git a/src/concrete/DecimalFloat.sol b/src/concrete/DecimalFloat.sol index 7e4d15e6..367026aa 100644 --- a/src/concrete/DecimalFloat.sol +++ b/src/concrete/DecimalFloat.sol @@ -200,23 +200,6 @@ contract DecimalFloat { return a.isZero(); } - /// Exposes `LibDecimalFloat.packLossless` for offchain use. - /// @param coefficient The coefficient to pack. - /// @param exponent The exponent to pack. - /// @return The packed float. - function packLossless(int224 coefficient, int32 exponent) external pure returns (Float) { - return LibDecimalFloat.packLossless(coefficient, exponent); - } - - /// Exposes `LibDecimalFloat.unpack` for offchain use. - /// @param float The float to unpack. - /// @return coefficient The coefficient of the float. - /// @return exponent The exponent of the float. - function unpack(Float float) external pure returns (int224, int32) { - (int256 coefficient, int256 exponent) = LibDecimalFloat.unpack(float); - return (int224(coefficient), int32(exponent)); - } - /// Exposes `LibDecimalFloat.fromFixedDecimalLosslessPacked` for offchain /// use. /// @param value The fixed point decimal value to convert. @@ -224,7 +207,7 @@ contract DecimalFloat { /// representation. e.g. If 1e18 represents 1 this would be 18 decimals. /// @return float The Float struct containing the signed coefficient and /// exponent. - function fromFixedDecimalLosslessPacked(uint256 value, uint8 decimals) external pure returns (Float) { + function fromFixedDecimalLossless(uint256 value, uint8 decimals) external pure returns (Float) { return LibDecimalFloat.fromFixedDecimalLosslessPacked(value, decimals); } @@ -236,4 +219,28 @@ contract DecimalFloat { function toFixedDecimalLossless(Float float, uint8 decimals) external pure returns (uint256) { return LibDecimalFloat.toFixedDecimalLossless(float, decimals); } + + /// Exposes `LibDecimalFloat.fromFixedDecimalLossyPacked` for offchain + /// use. + /// @param value The fixed point decimal value to convert. + /// @param decimals The number of decimals in the fixed point + /// representation. e.g. If 1e18 represents 1 this would be 18 decimals. + /// @return float The Float struct containing the signed coefficient and + /// exponent. + /// @return lossless True if the conversion was lossless, false otherwise. + function fromFixedDecimalLossy(uint256 value, uint8 decimals) external pure returns (Float, bool) { + //slither-disable-next-line unused-return + return LibDecimalFloat.fromFixedDecimalLossyPacked(value, decimals); + } + + /// Exposes `LibDecimalFloat.toFixedDecimalLossy` for offchain use. + /// @param float The Float struct to convert. + /// @param decimals The number of decimals in the fixed point + /// representation. e.g. If 1e18 represents 1 this would be 18 decimals. + /// @return value The fixed point decimal value as a uint256. + /// @return lossless True if the conversion was lossless, false otherwise. + function toFixedDecimalLossy(Float float, uint8 decimals) external pure returns (uint256, bool) { + //slither-disable-next-line unused-return + return LibDecimalFloat.toFixedDecimalLossy(float, decimals); + } } diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index dd80ed33..284d1e85 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -134,6 +134,7 @@ library LibDecimalFloat { /// e.g. If 1e18 represents 1 this would be 18 decimals. /// @return float The Float struct containing the signed coefficient and /// exponent. + /// @return lossless `true` if the conversion is lossless. function fromFixedDecimalLossyPacked(uint256 value, uint8 decimals) internal pure returns (Float, bool) { (int256 signedCoefficient, int256 exponent, bool lossless) = fromFixedDecimalLossy(value, decimals); (Float float, bool losslessPack) = packLossy(signedCoefficient, exponent); diff --git a/test/concrete/DecimalFloat.packLossless.t.sol b/test/concrete/DecimalFloat.packLossless.t.sol new file mode 100644 index 00000000..a26fcf35 --- /dev/null +++ b/test/concrete/DecimalFloat.packLossless.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {TestDecimalFloat} from "./TestDecimalFloat.sol"; +import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol"; + +contract DecimalFloatPackLosslessTest is Test { + function packLosslessExternal(int224 signedCoefficient, int32 exponent) external pure returns (Float) { + return LibDecimalFloat.packLossless(signedCoefficient, exponent); + } + + function testPackDeployed(int224 signedCoefficient, int32 exponent) external { + TestDecimalFloat deployed = new TestDecimalFloat(); + + try this.packLosslessExternal(signedCoefficient, exponent) returns (Float packed) { + Float deployedPacked = deployed.packLossless(signedCoefficient, exponent); + + assertEq(Float.unwrap(packed), Float.unwrap(deployedPacked)); + } catch (bytes memory err) { + vm.expectRevert(err); + deployed.packLossless(signedCoefficient, exponent); + } + } +} diff --git a/test/concrete/TestDecimalFloat.sol b/test/concrete/TestDecimalFloat.sol new file mode 100644 index 00000000..2e709c8f --- /dev/null +++ b/test/concrete/TestDecimalFloat.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol"; + +/// Additional exposed functions for testing the internals of floats +/// from downstream environments, e.g. rust. +contract TestDecimalFloat { + using LibDecimalFloat for Float; + + /// Exposes `LibDecimalFloat.packLossless` for offchain use. + /// @param coefficient The coefficient to pack. + /// @param exponent The exponent to pack. + /// @return The packed float. + function packLossless(int224 coefficient, int32 exponent) external pure returns (Float) { + return LibDecimalFloat.packLossless(coefficient, exponent); + } + + /// Exposes `LibDecimalFloat.unpack` for offchain use. + /// @param float The float to unpack. + /// @return coefficient The coefficient of the float. + /// @return exponent The exponent of the float. + function unpack(Float float) external pure returns (int256, int256) { + return LibDecimalFloat.unpack(float); + } +} diff --git a/test/concrete/TestDecimalFloat.unpack.t.sol b/test/concrete/TestDecimalFloat.unpack.t.sol new file mode 100644 index 00000000..b074adeb --- /dev/null +++ b/test/concrete/TestDecimalFloat.unpack.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {TestDecimalFloat} from "./TestDecimalFloat.sol"; +import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol"; + +contract TestDecimalFloatUnpackTest is Test { + using LibDecimalFloat for Float; + + function unpackExternal(Float packed) external pure returns (int256 signedCoefficient, int256 exponent) { + return LibDecimalFloat.unpack(packed); + } + + function testUnpackDeployed(Float packed) external { + TestDecimalFloat deployed = new TestDecimalFloat(); + + try this.unpackExternal(packed) returns (int256 signedCoefficient, int256 exponent) { + (int256 deployedSignedCoefficient, int256 deployedExponent) = deployed.unpack(packed); + + assertEq(signedCoefficient, deployedSignedCoefficient); + assertEq(exponent, deployedExponent); + } catch (bytes memory err) { + vm.expectRevert(err); + deployed.unpack(packed); + } + } +} diff --git a/test/src/concrete/DecimalFloat.fromFixedDecimalLossless.t.sol b/test/src/concrete/DecimalFloat.fromFixedDecimalLossless.t.sol new file mode 100644 index 00000000..e2d2b6df --- /dev/null +++ b/test/src/concrete/DecimalFloat.fromFixedDecimalLossless.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {DecimalFloat} from "src/concrete/DecimalFloat.sol"; +import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol"; + +contract DecimalFloatFromFixedDecimalLosslessTest is Test { + using LibDecimalFloat for Float; + + function fromFixedDecimalLosslessExternal(uint256 fixedDecimal, uint8 decimals) external pure returns (Float) { + return LibDecimalFloat.fromFixedDecimalLosslessPacked(fixedDecimal, decimals); + } + + function testFromFixedDecimalLosslessDeployed(uint256 fixedDecimal, uint8 decimals) external { + DecimalFloat deployed = new DecimalFloat(); + + try this.fromFixedDecimalLosslessExternal(fixedDecimal, decimals) returns (Float packed) { + Float deployedPacked = deployed.fromFixedDecimalLossless(fixedDecimal, decimals); + + assertEq(Float.unwrap(packed), Float.unwrap(deployedPacked)); + } catch (bytes memory err) { + vm.expectRevert(err); + deployed.fromFixedDecimalLossless(fixedDecimal, decimals); + } + } +} diff --git a/test/src/concrete/DecimalFloat.fromFixedDecimalLossy.t.sol b/test/src/concrete/DecimalFloat.fromFixedDecimalLossy.t.sol new file mode 100644 index 00000000..0f445685 --- /dev/null +++ b/test/src/concrete/DecimalFloat.fromFixedDecimalLossy.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {DecimalFloat} from "src/concrete/DecimalFloat.sol"; +import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol"; + +contract DecimalFloatFromFixedDecimalLossyTest is Test { + using LibDecimalFloat for Float; + + function fromFixedDecimalLossyExternal(uint256 fixedDecimal, uint8 decimals) external pure returns (Float, bool) { + return LibDecimalFloat.fromFixedDecimalLossyPacked(fixedDecimal, decimals); + } + + function testFromFixedDecimalLossyDeployed(uint256 fixedDecimal, uint8 decimals) external { + DecimalFloat deployed = new DecimalFloat(); + + try this.fromFixedDecimalLossyExternal(fixedDecimal, decimals) returns (Float packed, bool lossless) { + (Float deployedPacked, bool deployedLossless) = deployed.fromFixedDecimalLossy(fixedDecimal, decimals); + + assertEq(Float.unwrap(packed), Float.unwrap(deployedPacked)); + assertEq(lossless, deployedLossless); + } catch (bytes memory err) { + vm.expectRevert(err); + deployed.fromFixedDecimalLossy(fixedDecimal, decimals); + } + } +} diff --git a/test/src/concrete/DecimalFloat.toFixedDecimalLossless.t.sol b/test/src/concrete/DecimalFloat.toFixedDecimalLossless.t.sol new file mode 100644 index 00000000..47111352 --- /dev/null +++ b/test/src/concrete/DecimalFloat.toFixedDecimalLossless.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {DecimalFloat} from "src/concrete/DecimalFloat.sol"; +import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol"; + +contract DecimalFloatToFixedDecimalLosslessTest is Test { + using LibDecimalFloat for Float; + + function toFixedDecimalLosslessExternal(Float packed, uint8 decimals) external pure returns (uint256) { + return LibDecimalFloat.toFixedDecimalLossless(packed, decimals); + } + + function testToFixedDecimalLosslessDeployed(Float packed, uint8 decimals) external { + DecimalFloat deployed = new DecimalFloat(); + + try this.toFixedDecimalLosslessExternal(packed, decimals) returns (uint256 fixedDecimal) { + uint256 deployedFixedDecimal = deployed.toFixedDecimalLossless(packed, decimals); + + assertEq(fixedDecimal, deployedFixedDecimal); + } catch (bytes memory err) { + vm.expectRevert(err); + deployed.toFixedDecimalLossless(packed, decimals); + } + } +} diff --git a/test/src/concrete/DecimalFloat.toFixedDecimalLossy.t.sol b/test/src/concrete/DecimalFloat.toFixedDecimalLossy.t.sol new file mode 100644 index 00000000..b5059cbe --- /dev/null +++ b/test/src/concrete/DecimalFloat.toFixedDecimalLossy.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {DecimalFloat} from "src/concrete/DecimalFloat.sol"; +import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol"; + +contract DecimalFloatToFixedDecimalLossyTest is Test { + using LibDecimalFloat for Float; + + function toFixedDecimalLossyExternal(Float packed, uint8 decimals) external pure returns (uint256, bool) { + return LibDecimalFloat.toFixedDecimalLossy(packed, decimals); + } + + function testToFixedDecimalLossyDeployed(Float packed, uint8 decimals) external { + DecimalFloat deployed = new DecimalFloat(); + + try this.toFixedDecimalLossyExternal(packed, decimals) returns (uint256 fixedDecimal, bool lossless) { + (uint256 deployedFixedDecimal, bool deployedLossless) = deployed.toFixedDecimalLossy(packed, decimals); + + assertEq(fixedDecimal, deployedFixedDecimal); + assertEq(lossless, deployedLossless); + } catch (bytes memory err) { + vm.expectRevert(err); + deployed.toFixedDecimalLossy(packed, decimals); + } + } +} diff --git a/test_js/float.test.ts b/test_js/float.test.ts index f1d6cbb1..54a09996 100644 --- a/test_js/float.test.ts +++ b/test_js/float.test.ts @@ -41,10 +41,6 @@ describe('Test Float Bindings', () => { expect(result).toBe(originalValue); }); - it('should test packLossless', () => { - const float = Float.packLossless('314', -2)?.value!; - expect(float.format()?.value!).toBe('3.14'); - }); it('should try from bigint', () => { const result = Float.tryFromBigint(5n);