diff --git a/.gitmodules b/.gitmodules index ac2a919..631b9eb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,3 +18,11 @@ path = lib/openzeppelin-contracts url = https://github.com/openzeppelin/openzeppelin-contracts branch = v4.8.0 +[submodule "lib/prb-math"] + path = lib/prb-math + url = https://github.com/PaulRBerg/prb-math + branch = v4 +[submodule "lib/prb-math-v3"] + path = lib/prb-math-v3 + url = https://github.com/PaulRBerg/prb-math + branch = v3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7bebae..4ee920a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Bug reports and feature suggestions can be submitted to our issue tracker. For b ## Questions -Questions can be submitted to the issue tracker, but you may get a faster response if you ask in our [chat room](https://empireslacking.herokuapp.com/) (in the #ethereum channel). +Questions can be submitted to the issue tracker, but you may get a faster response if you ask in our [chat room](https://slack.empirehacking.nyc/) (in the #ethereum channel). ## Code diff --git a/PROPERTIES.md b/PROPERTIES.md index c24d8ae..b243f22 100644 --- a/PROPERTIES.md +++ b/PROPERTIES.md @@ -1,6 +1,6 @@ # Introduction -This file lists all the currently implemented Echidna property tests for ERC20, ERC721, ERC4626 and ABDKMath64x64. For each property, there is a permalink to the file implementing it in the repository and a small description of the invariant tested. +This file lists all the currently implemented Echidna property tests for ERC20, ERC4626, ABDKMath64x64, PRBMath SD59x18 and PRBMath UD60x18. For each property, there is a permalink to the file implementing it in the repository and a small description of the invariant tested. ## Table of contents @@ -18,6 +18,8 @@ This file lists all the currently implemented Echidna property tests for ERC20, - [Tests for mintable tokens](#tests-for-mintable-tokens-1) - [ERC4626](#erc4626) - [ABDKMath64x64](#abdkmath64x64) + - [PRBMath SD59x18](#prbmath-sd59x18) + - [PRBMath UD60x18](#prbmath-ud60x18) ## ERC20 @@ -260,3 +262,240 @@ This file lists all the currently implemented Echidna property tests for ERC20, | ABDKMATH-104 | [exp_test_zero](https://github.com/crytic/properties/blob/main/contracts/Math/ABDKMath64x64/ABDKMath64x64PropertyTests.sol#L1818) | Natural exponentiation edge case: exponent zero result should be one. | | ABDKMATH-105 | [exp_test_maximum](https://github.com/crytic/properties/blob/main/contracts/Math/ABDKMath64x64/ABDKMath64x64PropertyTests.sol#L1825) | Natural exponentiation edge case: exponent maximum value should revert. | | ABDKMATH-106 | [exp_test_minimum](https://github.com/crytic/properties/blob/main/contracts/Math/ABDKMath64x64/ABDKMath64x64PropertyTests.sol#L1836) | Natural exponentiation edge case: exponent minimum value result should be zero. | + +## PRBMath SD59x18 + +| ID | Name | Invariant tested | +| ------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------ | +| SD59x18-001 | [add_test_commutative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L243) | Commutative property for addition. | +| SD59x18-002 | [add_test_associative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L252) | Associative property for addition. | +| SD59x18-003 | [add_test_identity](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L263) | Identity operation for addition. | +| SD59x18-004 | [add_test_values](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L272) | Addition result should increase or decrease depending on operands signs. | +| SD59x18-005 | [add_test_range](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L290) | Addition result should be in the valid 59x18-arithmetic range. | +| SD59x18-006 | [add_test_maximum_value](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L301) | Addition edge case: maximum value plus zero should be maximum value. | +| SD59x18-007 | [add_test_maximum_value_plus_one](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L311) | Addition edge case: maximum value plus one should revert (out of range). | +| SD59x18-008 | [add_test_minimum_value](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L321) | Addition edge case: minimum value plus zero should be minimum value. | +| SD59x18-009 | [add_test_minimum_value_plus_negative_one](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L331) | Addition edge case: minimum value plus minus one should revert (out of range). | +| SD59x18-010 | [sub_test_equivalence_to_addition](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L353) | Subtraction should be equal to addition with opposite sign. | +| SD59x18-011 | [sub_test_non_commutative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L363) | Anti-commutative property for subtraction. | +| SD59x18-012 | [sub_test_identity](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L372) | Identity operation for subtraction. | +| SD59x18-013 | [sub_test_neutrality](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L381) | Adding and subtracting the same value should not affect original value. | +| SD59x18-014 | [sub_test_values](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L394) | Subtraction result should increase or decrease depending on operands signs. | +| SD59x18-015 | [sub_test_range](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L412) | Subtraction result should be in the valid 59x18-arithmetic range. | +| SD59x18-016 | [sub_test_maximum_value](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L423) | Subtraction edge case: maximum value minus zero should be maximum value. | +| SD59x18-017 | [sub_test_maximum_value_minus_neg_one](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L434) | Subtraction edge case: maximum value minus negative one should revert (out of range). | +| SD59x18-018 | [sub_test_minimum_value](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L444) | Subtraction edge case: minimum value minus zero should be minimum value. | +| SD59x18-019 | [sub_test_minimum_value_minus_one](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L454) | Subtraction edge case: minimum value minus one should revert (out of range). | +| SD59x18-020 | [mul_test_commutative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L476) | Commutative property for multiplication. | +| SD59x18-021 | [mul_test_associative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L486) | Associative property for multiplication. | +| SD59x18-022 | [mul_test_distributive](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L517) | Distributive property for multiplication. | +| SD59x18-023 | [mul_test_identity](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L539) | Identity operation for multiplication. | +| SD59x18-024 | [mul_test_x_positive](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L550) | Multiplication result should increase or decrease depending on operands signs. | +| SD59x18-025 | [mul_test_x_negative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L564) | Multiplication result should increase or decrease depending on operands signs. | +| SD59x18-026 | [mul_test_range](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L584) | Multiplication result should be in the valid 59x18-arithmetic range. | +| SD59x18-027 | [mul_test_maximum_value](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L597) | Multiplication edge case: maximum value times one should be maximum value | +| SD59x18-028 | [mul_test_minimum_value](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L608) | Multiplication edge case: minimum value times one should be minimum value | +| SD59x18-029 | [div_test_division_identity_x_div_1](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L633) | Identity operation for division. x / 1 == x | +| SD59x18-030 | [div_test_division_identity_x_div_x](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L642) | Identity operation for division. x / x == 1 | +| SD59x18-031 | [div_test_negative_divisor](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L660) | Division result sign should change according to divisor sign. | +| SD59x18-032 | [div_test_division_num_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L672) | Division with zero numerator should be zero. | +| SD59x18-033 | [div_test_values](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L683) | Division result should increase or decrease (in absolute value) depending on divisor's absolute value. | +| SD59x18-034 | [div_test_div_by_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L703) | Division edge case: Divisor zero should revert. | +| SD59x18-035 | [div_test_maximum_denominator](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L714) | Division edge case: Division result by a large number should be less than one. | +| SD59x18-036 | [div_test_maximum_numerator](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L723) | Division edge case: Division result of maximum value should revert if divisor is less than one. | +| SD59x18-037 | [div_test_range](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L737) | Division result should be in the valid 64x64-arithmetic range. | +| SD59x18-038 | [neg_test_double_negation](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L765) | Double sign negation should be equal to the original operand. | +| SD59x18-039 | [neg_test_identity](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L773) | Identity operation for sign negation. | +| SD59x18-040 | [neg_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L787) | Negation edge case: Negation of zero should be zero. | +| SD59x18-041 | [neg_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L795) | Negation edge case: Negation of maximum value minus epsilon should not revert. | +| SD59x18-042 | [neg_test_minimum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L805) | Negation edge case: Negation of minimum value plus epsilon should not revert. | +| SD59x18-043 | [abs_test_positive](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L826) | Absolute value should be always positive. | +| SD59x18-044 | [abs_test_negative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L835) | Absolute value of a number and its negation should be equal. | +| SD59x18-045 | [abs_test_multiplicativeness](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L846) | Multiplicativeness property for absolute value. | +| SD59x18-046 | [abs_test_subadditivity](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L863) | Subadditivity property for absolute value. | +| SD59x18-047 | [abs_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L880) | Absolute value edge case: absolute value of zero is zero. | +| SD59x18-048 | [abs_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L894) | Absolute value edge case: absolute value of maximum value is maximum value. | +| SD59x18-049 | [abs_test_minimum_revert](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L907) | Absolute value edge case: absolute value of minimum value should revert | +| SD59x18-050 | [abs_test_minimum_allowed](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L917) | Absolute value edge case: absolute value of minimum permitted value is the negation of minimum value. | +| SD59x18-051 | [inv_test_double_inverse](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L944) | Result of double inverse should be _close enough_ to the original operand. | +| SD59x18-052 | [inv_test_division](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L957) | Inverse should be equivalent to division. | +| SD59x18-053 | [inv_test_division_noncommutativity](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L968) | Anticommutative property for inverse operation. | +| SD59x18-054 | [inv_test_multiplication](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L979) | Multiplication of inverses should be equal to inverse of multiplication. | +| SD59x18-055 | [inv_test_identity](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L993) | Identity property for inverse. | +| SD59x18-056 | [inv_test_values](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1006) | Inverse result should be in range (0, 1) if operand is greater than one. | +| SD59x18-057 | [inv_test_sign](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1020) | Inverse result should keep operand's sign. | +| SD59x18-058 | [inv_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1039) | Inverse edge case: Inverse of zero should revert. | +| SD59x18-059 | [inv_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1047) | Inverse edge case: Inverse of maximum value should be close to zero. | +| SD59x18-060 | [inv_test_minimum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1060) | Inverse edge case: Inverse of minimum value should be close to zero. | +| SD59x18-061 | [avg_test_values_in_range](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1086) | Average result should be between operands. | +| SD59x18-062 | [avg_test_one_value](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1100) | Average of the same number twice is the number itself. | +| SD59x18-063 | [avg_test_operand_order](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1108) | Average result does not depend on the order of operands. | +| SD59x18-064 | [avg_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1122) | Average edge case: Average of maximum value twice is the maximum value. | +| SD59x18-065 | [avg_test_minimum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1136) | Average edge case: Average of minimum value twice is the minimum value. | +| SD59x18-066 | [pow_test_zero_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1163) | Power of zero should be one. | +| SD59x18-067 | [pow_test_zero_base_non_zero_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1172) | Zero to the power of any number should be zero. | +| SD59x18-068 | [pow_test_zero_base_zero_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1181) | Zero to the power of zero should be one | +| SD59x18-069 | [pow_test_one_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1189) | Power of one should be equal to the operand. | +| SD59x18-070 | [pow_test_base_one](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1198) | One to the power of any number should be one. | +| SD59x18-071 | [pow_test_product_same_base](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1206) | Product of powers of the same base property | +| SD59x18-072 | [pow_test_power_of_an_exponentiation](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1223) | Power of an exponentiation property | +| SD59x18-073 | [pow_test_product_power](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1242) | Distributive property for power of a product | +| SD59x18-074 | [pow_test_positive_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1259) | Power result should increase or decrease (in absolute value) depending on exponent's absolute value. | +| SD59x18-075 | [pow_test_negative_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1275) | Power result should increase or decrease (in absolute value) depending on exponent's absolute value. | +| SD59x18-076 | [pow_test_sign](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1291) | Power result sign should change according to the exponent sign. | +| SD59x18-077 | [pow_test_exp2_equivalence](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1314) | Base-2 exponentiation should be equal to power. | +| SD59x18-078 | [pow_test_maximum_base](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1344) | Power edge case: Power of the maximum value should revert if exponent > 1. | +| SD59x18-079 | [pow_test_high_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1356) | Power edge case: Result should be zero if base is small and exponent is large. | +| SD59x18-080 | [sqrt_test_inverse_mul](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1378) | Square root inverse as multiplication. | +| SD59x18-081 | [sqrt_test_inverse_pow](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1394) | Square root inverse as power. | +| SD59x18-082 | [sqrt_test_distributive](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1410) | Square root distributive property respect to multiplication. | +| SD59x18-083 | [sqrt_test_is_increasing](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1433) | Square root should be strictly increasing for any x | +| SD59x18-084 | [sqrt_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1463) | Square root edge case: square root of zero should be zero. | +| SD59x18-085 | [sqrt_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1468) | Square root edge case: square root of maximum value should not revert. | +| SD59x18-086 | [sqrt_test_minimum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1479) | Square root edge case: square root of minimum value should revert (negative). | +| SD59x18-087 | [sqrt_test_negative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1489) | Square root edge case: square root of a negative value should revert. | +| SD59x18-088 | [log2_test_distributive_mul](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1514) | Base-2 logarithm distributive property respect to multiplication. | +| SD59x18-089 | [log2_test_power](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1537) | Base-2 logarithm of a power property. | +| SD59x18-090 | [log2_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1565) | Base-2 logarithm edge case: Logarithm of zero should revert. | +| SD59x18-091 | [log2_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1575) | Base-2 logarithm edge case: Logarithm of maximum value should not revert. | +| SD59x18-092 | [log2_test_negative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1589) | Base-2 logarithm edge case: Logarithm of a negative value should revert. | +| SD59x18-093 | [ln_test_distributive_mul](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1614) | Natural logarithm distributive property respect to multiplication. | +| SD59x18-094 | [ln_test_power](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1637) | Natural logarithm of a power property. | +| SD59x18-095 | [ln_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1670) | Natural logarithm edge case: Logarithm of zero should revert. | +| SD59x18-096 | [ln_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1680) | Natural logarithm edge case: Logarithm of maximum value should not revert. | +| SD59x18-097 | [ln_test_negative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1694) | Natural logarithm edge case: Logarithm of a negative value should revert. | +| SD59x18-098 | [exp2_test_equivalence_pow](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1719) | Base-2 exponentiation should be equal to power. | +| SD59x18-099 | [exp2_test_inverse](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1729) | Base-2 exponentiation inverse function. | +| SD59x18-100 | [exp2_test_negative_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1739) | Base-2 exponentiation with negative exponent should equal the inverse. | +| SD59x18-101 | [exp2_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1756) | Base-2 exponentiation edge case: exponent zero result should be one. | +| SD59x18-102 | [exp2_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1763) | Base-2 exponentiation edge case: exponent maximum value should revert. | +| SD59x18-103 | [exp2_test_maximum_permitted](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1774) | Base-2 exponentiation edge case: exponent maximum permitted value should not revert. | +| SD59x18-104 | [exp2_test_minimum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1785) | Base-2 exponentiation edge case: exponent minimum value result should be zero. | +| SD59x18-105 | [exp_test_inverse](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1812) | Natural exponentiation inverse function. | +| SD59x18-106 | [exp_test_negative_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1823) | Natural exponentiation with negative exponent should equal the inverse. | +| SD59x18-107 | [exp_test_strictly_increasing](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1834) | Natural exponentiation should be strictly increasing for any x | +| SD59x18-108 | [exp_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1852) | Natural exponentiation edge case: exponent zero result should be one. | +| SD59x18-109 | [exp_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1859) | Natural exponentiation edge case: exponent maximum value should revert. | +| SD59x18-110 | [exp_test_maximum_permitted](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1870) | Natural exponentiation edge case: exponent maximum value should revert. | +| SD59x18-111 | [exp_test_minimum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1881) | Natural exponentiation edge case: exponent minimum value result should be zero. | +| SD59x18-112 | [powu_test_zero_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1908) | Power of zero should be one. | +| SD59x18-113 | [powu_test_zero_base](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1918) | Zero to the power of any number should be zero. | +| SD59x18-114 | [powu_test_one_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1928) | Power of one should be equal to the operand. | +| SD59x18-115 | [powu_test_base_one](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1938) | One to the power of any number should be one. | +| SD59x18-116 | [powu_test_product_same_base](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1946) | Product of powers of the same base property | +| SD59x18-117 | [powu_test_power_of_an_exponentiation](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1963) | Power of an exponentiation property | +| SD59x18-118 | [powu_test_product_power](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1980) | Distributive property for power of a product | +| SD59x18-119 | [powu_test_values](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L1997) | Power result should increase or decrease (in absolute value) depending on exponent's absolute value. | +| SD59x18-120 | [powu_test_sign](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2014) | Power result sign should change according to the exponent sign. | +| SD59x18-121 | [powu_test_maximum_base](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2060) | Power edge case: Power of the maximum value should revert if exponent > 1. | +| SD59x18-122 | [powu_test_high_exponent](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2072) | Power edge case: Result should be zero if base is small and exponent is large. | +| SD59x18-123 | [log10_test_distributive_mul](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2094) | Base-10 logarithm distributive property respect to multiplication. | +| SD59x18-124 | [log10_test_power](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2113) | Base-10 logarithm of a power property. | +| SD59x18-125 | [log10_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2140) | Base-10 logarithm edge case: Logarithm of zero should revert. | +| SD59x18-126 | [log10_test_maximum](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2150) | Base-10 logarithm edge case: Logarithm of maximum value should not revert. | +| SD59x18-127 | [log10_test_negative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2164) | Base-10 logarithm edge case: Logarithm of a negative value should revert. | +| SD59x18-128 | [gm_test_product](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2189) | Product of the values should be equal to the geometric mean raised to the power of N | +| SD59x18-129 | [gm_test_positive_set_avg](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2202) | The geometric mean of a set of positive values should be less than the arithmetic mean | +| SD59x18-130 | [gm_test_positive_equal_set_avg](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2213) | The geometric mean of a set of equal positive values should be equal to the arithmetic mean | +| SD59x18-131 | [gm_test_zero](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2229) | GM edge case: if a set contains zero, the result is zero | +| SD59x18-132 | [gm_test_negative](./contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol#L2242) | GM edge case: The geometric mean is not defined when the set contains an odd number of negative values | + +## PRBMath UD60x18 + +| ID | Name | Invariant tested | +| ------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------ | +| UD60x18-001 | [add_test_commutative](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L175) | Commutative property for addition. | +| UD60x18-002 | [add_test_associative](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L184) | Associative property for addition. | +| UD60x18-003 | [add_test_identity](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L195) | Identity operation for addition. | +| UD60x18-004 | [add_test_values](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L204) | Addition result should increase or decrease depending on operands signs. | +| UD60x18-005 | [add_test_range](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L218) | Addition result should be in the valid 60x18-arithmetic range. | +| UD60x18-006 | [add_test_maximum_value](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L228) | Addition edge case: maximum value plus zero should be maximum value. | +| UD60x18-007 | [add_test_maximum_value_plus_one](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L238) | Addition edge case: maximum value plus one should revert (out of range). | +| UD60x18-008 | [add_test_minimum_value](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L248) | Addition edge case: minimum value plus zero should be minimum value. | +| UD60x18-009 | [sub_test_identity](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L271) | Identity operation for subtraction. | +| UD60x18-010 | [sub_test_neutrality](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L280) | Adding and subtracting the same value should not affect original value. | +| UD60x18-011 | [sub_test_values](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L292) | Subtraction result should increase or decrease depending on operands signs. | +| UD60x18-012 | [sub_test_range](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L306) | Subtraction result should be in the valid 60x18-arithmetic range. | +| UD60x18-013 | [sub_test_maximum_value](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L316) | Subtraction edge case: maximum value minus zero should be maximum value. | +| UD60x18-014 | [sub_test_minimum_value](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L327) | Subtraction edge case: minimum value minus zero should be minimum value. | +| UD60x18-015 | [sub_test_minimum_value_minus_one](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L337) | Subtraction edge case: minimum value minus one should revert (out of range). | +| UD60x18-016 | [mul_test_commutative](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L359) | Commutative property for multiplication. | +| UD60x18-017 | [mul_test_associative](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L368) | Associative property for multiplication. | +| UD60x18-018 | [mul_test_distributive](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L380) | Distributive property for multiplication. | +| UD60x18-019 | [mul_test_identity](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L397) | Identity operation for multiplication. | +| UD60x18-020 | [mul_test_values](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L407) | Multiplication result should increase or decrease depending on operands signs. | +| UD60x18-021 | [mul_test_range](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L425) | Multiplication result should be in the valid 59x18-arithmetic range. | +| UD60x18-022 | [mul_test_maximum_value](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L435) | Multiplication edge case: maximum value times one should be maximum value | +| UD60x18-023 | [div_test_division_identity_x_div_1](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L458) | Identity operation for division. x / 1 == x | +| UD60x18-024 | [div_test_division_identity_x_div_x](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L466) | Identity operation for division. x / x == 1 | +| UD60x18-025 | [div_test_division_num_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L481) | Division with zero numerator should be zero. | +| UD60x18-026 | [div_test_values](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L491) | Division result should increase or decrease (in absolute value) depending on divisor's absolute value. | +| UD60x18-027 | [div_test_div_by_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L510) | Division edge case: Divisor zero should revert. | +| UD60x18-028 | [div_test_maximum_denominator](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L520) | Division edge case: Division result by a large number should be less than one. | +| UD60x18-029 | [div_test_maximum_numerator](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L528) | Division edge case: Division result of maximum value should revert if divisor is less than one. | +| UD60x18-030 | [div_test_range](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L543) | Division result should be in the valid 60x18-arithmetic range. | +| UD60x18-031 | [inv_test_double_inverse](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L570) | Result of double inverse should be _close enough_ to the original operand. | +| UD60x18-032 | [inv_test_division](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L579) | Inverse should be equivalent to division. | +| UD60x18-033 | [inv_test_division_noncommutativity](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L590) | Anticommutative property for inverse operation. | +| UD60x18-034 | [inv_test_multiplication](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L601) | Multiplication of inverses should be equal to inverse of multiplication. | +| UD60x18-035 | [inv_test_identity](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L615) | Identity property for inverse. | +| UD60x18-036 | [inv_test_values](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L630) | Inverse result should be in range (0, 1) if operand is greater than one. | +| UD60x18-037 | [inv_test_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L649) | Inverse edge case: Inverse of zero should revert. | +| UD60x18-038 | [inv_test_maximum](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L657) | Inverse edge case: Inverse of maximum value should be close to zero. | +| UD60x18-039 | [inv_test_minimum](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L670) | Inverse edge case: Inverse of minimum value should be close to zero. | +| UD60x18-040 | [avg_test_values_in_range](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L696) | Average result should be between operands. | +| UD60x18-041 | [avg_test_one_value](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L710) | Average of the same number twice is the number itself. | +| UD60x18-042 | [avg_test_operand_order](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L718) | Average result does not depend on the order of operands. | +| UD60x18-043 | [avg_test_maximum](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L732) | Average edge case: Average of maximum value twice is the maximum value. | +| UD60x18-044 | [pow_test_zero_exponent](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L759) | Power of zero should be one. | +| UD60x18-045 | [pow_test_zero_base](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L768) | Zero to the power of any number should be zero. | +| UD60x18-046 | [pow_test_one_exponent](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L779) | Power of one should be equal to the operand. | +| UD60x18-047 | [pow_test_base_one](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L788) | One to the power of any number should be one. | +| UD60x18-048 | [pow_test_product_same_base](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L796) | Product of powers of the same base property | +| UD60x18-049 | [pow_test_power_of_an_exponentiation](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L813) | Power of an exponentiation property | +| UD60x18-050 | [pow_test_product_power](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L830) | Distributive property for power of a product | +| UD60x18-051 | [pow_test_values](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L846) | Power result should increase or decrease (in absolute value) depending on exponent's absolute value. | +| UD60x18-052 | [pow_test_maximum_base](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L884) | Power edge case: Power of the maximum value should revert if exponent > 1. | +| UD60x18-053 | [pow_test_maximum_exponent](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L896) | Power edge case: Power of the maximum value should revert if exponent > 1. | +| UD60x18-054 | [pow_test_high_exponent](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L909) | Power edge case: Result should be zero if base is small and exponent is large. | +| UD60x18-055 | [sqrt_test_inverse_mul](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L931) | Square root inverse as multiplication. | +| UD60x18-056 | [sqrt_test_inverse_pow](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L946) | Square root inverse as power. | +| UD60x18-057 | [sqrt_test_distributive](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L961) | Square root distributive property respect to multiplication. | +| UD60x18-058 | [sqrt_test_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1001) | Square root edge case: square root of zero should be zero. | +| UD60x18-059 | [sqrt_test_maximum](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1006) | Square root edge case: square root of maximum value should not revert. | +| UD60x18-060 | [log2_test_distributive_mul](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1030) | Base-2 logarithm distributive property respect to multiplication. | +| UD60x18-061 | [log2_test_power](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1045) | Base-2 logarithm of a power property. | +| UD60x18-062 | [log2_test_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1073) | Base-2 logarithm edge case: Logarithm of zero should revert. | +| UD60x18-063 | [log2_test_maximum](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1083) | Base-2 logarithm edge case: Logarithm of maximum value should not revert. | +| UD60x18-064 | [log2_test_less_than_unit](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1097) | Base-2 logarithm edge case: Logarithm of a negative value should revert. | +| UD60x18-065 | [ln_test_distributive_mul](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1122) | Natural logarithm distributive property respect to multiplication. | +| UD60x18-066 | [ln_test_power](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1137) | Natural logarithm of a power property. | +| UD60x18-067 | [ln_test_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1164) | Natural logarithm edge case: Logarithm of zero should revert. | +| UD60x18-068 | [ln_test_maximum](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1174) | Natural logarithm edge case: Logarithm of maximum value should not revert. | +| UD60x18-069 | [ln_test_less_than_unit](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1188) | Natural logarithm edge case: Logarithm of a negative value should revert. | +| UD60x18-070 | [exp2_test_equivalence_pow](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1213) | Base-2 exponentiation should be equal to power. | +| UD60x18-071 | [exp2_test_inverse](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1223) | Base-2 exponentiation inverse function. | +| UD60x18-072 | [exp2_test_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1251) | Base-2 exponentiation edge case: exponent zero result should be one. | +| UD60x18-073 | [exp2_test_maximum](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1258) | Base-2 exponentiation edge case: exponent maximum value should revert. | +| UD60x18-075 | [exp_test_inverse](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1281) | Natural exponentiation inverse function. | +| UD60x18-076 | [exp_test_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1310) | Natural exponentiation edge case: exponent zero result should be one. | +| UD60x18-077 | [exp_test_maximum](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1317) | Natural exponentiation edge case: exponent maximum value should revert. | +| UD60x18-078 | [powu_test_zero_exponent](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1340) | Power of zero should be one. | +| UD60x18-079 | [powu_test_zero_base](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1348) | Zero to the power of any number should be zero. | +| UD60x18-080 | [powu_test_one_exponent](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1358) | Power of one should be equal to the operand. | +| UD60x18-081 | [powu_test_base_one](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1366) | One to the power of any number should be one. | +| UD60x18-082 | [powu_test_product_same_base](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1374) | Product of powers of the same base property | +| UD60x18-083 | [powu_test_power_of_an_exponentiation](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1390) | Power of an exponentiation property | +| UD60x18-084 | [powu_test_product_power](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1406) | Distributive property for power of a product | +| UD60x18-085 | [powu_test_values](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1422) | Power result should increase or decrease (in absolute value) depending on exponent's absolute value. | +| UD60x18-086 | [powu_test_maximum_base](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1460) | Power edge case: Power of the maximum value should revert if exponent > 1. | +| UD60x18-087 | [powu_test_high_exponent](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1472) | Power edge case: Result should be zero if base is small and exponent is large. | +| UD60x18-088 | [log10_test_distributive_mul](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1494) | Base-10 logarithm distributive property respect to multiplication. | +| UD60x18-089 | [log10_test_power](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1508) | Base-10 logarithm of a power property. | +| UD60x18-090 | [log10_test_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1535) | Base-10 logarithm edge case: Logarithm of zero should revert. | +| UD60x18-091 | [log10_test_maximum](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1545) | Base-10 logarithm edge case: Logarithm of maximum value should not revert. | +| UD60x18-092 | [log10_test_less_than_unit](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1559) | Base-10 logarithm edge case: Logarithm of a negative value should revert. | +| UD60x18-093 | [gm_test_product](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1584) | Product of the values should be equal to the geometric mean raised to the power of N | +| UD60x18-094 | [gm_test_positive_set_avg](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1593) | The geometric mean of a set of positive values should be less than the arithmetic mean | +| UD60x18-095 | [gm_test_positive_equal_set_avg](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1604) | The geometric mean of a set of equal positive values should be equal to the arithmetic mean | +| UD60x18-096 | [gm_test_zero](./contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol#L1618) | GM edge case: if a set contains zero, the result is zero | \ No newline at end of file diff --git a/README.md b/README.md index d81e866..c371577 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - [ERC721 Tests](#erc721-tests) - [ERC4626 Tests](#erc4626-tests) - [ABDKMath64x64 tests](#abdkmath64x64-tests) + - [PRBMath tests](#prbmath-tests) - [Additional resources](#additional-resources) - [Helper functions](#helper-functions) - [Usage examples](#usage-examples) @@ -26,6 +27,7 @@ This repository contains 168 code properties for: - [ERC721](https://ethereum.org/en/developers/docs/standards/tokens/erc-721/) token: mintable, burnable, and transferable invariants ([19 properties](PROPERTIES.md#erc721)). - [ERC4626](https://ethereum.org/en/developers/docs/standards/tokens/erc-4626/) vaults: strict specification and additional security invariants ([37 properties](PROPERTIES.md#erc4626)). - [ABDKMath64x64](https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.md) fixed-point library invariants ([106 properties](PROPERTIES.md#abdkmath64x64)). +- [PRBMath](https://github.com/PaulRBerg/prb-math/blob/main/README.md) fixed-point library invariants ([132 properties](PROPERTIES.md#prbmath-sd59x18)) for SD59x18, and ([96 properties](PROPERTIES.md#prbmath-ud60x18)) for UD60x18. The goals of these properties are to: @@ -47,6 +49,7 @@ The properties can be used through unit tests or through fuzzing with [Echidna]( - [ERC20 tests](#erc20-tests) - [ERC4626 test](#erc4626-tests) - [ABDKMath64x64 tests](#abdkmath64x64-tests) + - [PRBMath tests](#prbmath-tests) ### ERC20 tests @@ -414,10 +417,48 @@ contract CryticABDKMath64x64Harness is CryticABDKMath64x64PropertyTests { Run the test suite using `echidna . --contract CryticABDKMath64x64Harness --seq-len 1 --test-mode assertion --corpus-dir tests/echidna-corpus` and inspect the coverage report in `tests/echidna-corpus` when it finishes. +### PRBMath tests + +The Solidity smart contract programming language does not have any inbuilt feature for working with decimal numbers, so for contracts dealing with non-integer values, a third party solution is needed. [PRBMath](https://github.com/PaulRBerg/prb-math) is Solidity library for advanced fixed-point math that operates with signed 59.18-decimal fixed-point and unsigned 60.18-decimal fixed-point numbers + +SD59x18 library implements [19 arithmetic operations](https://github.com/PaulRBerg/prb-math/blob/main/README.md#mathematical-functions "19 arithmetic operations") using fixed-point numbers and [11 conversion functions](https://github.com/PaulRBerg/prb-math/blob/main/README.md#mathematical-functions "6 conversion functions") between integer types and fixed-point types. + +We provide a number of tests related with fundamental mathematical properties of the floating point numbers. To include these tests into your repository, follow these steps: + +1. [Integration](#integration-4) +2. [Run](#run-4) + + +#### Integration + +Create a new Solidity file containing the `PRBMathSD59x18Harness` or `PRBMath60x18Harness` contract: + +```Solidity +pragma solidity ^0.8.0; +import "@crytic/properties/contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol; + +contract CryticPRBMath59x18Harness is CryticPRBMath59x18Propertiesv3 { + /* Any additional test can be added here */ +} +``` + +```Solidity +pragma solidity ^0.8.0; +import "@crytic/properties/contracts/Math/PRBMath/v3/PRBMathSD60x18PropertyTests.sol; + +contract CryticPRBMath60x18Harness is CryticPRBMath60x18Propertiesv3 { + /* Any additional test can be added here */ +} +``` + +#### Run + +Run the test suite using `echidna-test . --contract CryticPRBMath59x18Harness --seq-len 1 --test-mode assertion --corpus-dir tests/echidna-corpus` and inspect the coverage report in `tests/echidna-corpus` when it finishes. + ## Additional resources - [Building secure contracts](https://secure-contracts.com/program-analysis/index.html) -- Our [EmpireSlacking](https://empireslacking.herokuapp.com/) slack server, channel #ethereum +- Our [EmpireSlacking](https://slack.empirehacking.nyc/) slack server, channel #ethereum - Watch our [fuzzing workshop](https://www.youtube.com/watch?v=QofNQxW_K08&list=PLciHOL_J7Iwqdja9UH4ZzE8dP1IxtsBXI) # Helper functions diff --git a/contracts/Math/PRBMath/README.md b/contracts/Math/PRBMath/README.md new file mode 100644 index 0000000..45b43d8 --- /dev/null +++ b/contracts/Math/PRBMath/README.md @@ -0,0 +1,36 @@ +# PRBMath test suite for Echidna + +## What is PRBMath? + +The Solidity smart contract programming language does not have any inbuilt feature for working with decimal numbers, so for contracts dealing with non-integer values, a third party solution is needed. PRBMath is a fixed-point arithmetic Solidity library that operates on signed 59x18-decimal fixed-point and unsigned 60.18-decimal fixed-point numbers. This library was developed by [Paul Razvan Berg](https://github.com/PaulRBerg "Paul Razvan Berg") and is [open source](https://github.com/PaulRBerg/prb-math "open source") under the MIT License. + +## Why are tests needed? + +Solidity libraries are used in smart contracts that at some point in time can hold important value in tokens or other assets. The security of those assets is directly related to the robustness and reliability of the smart contract source code. + +While testing does not guarantee the absence of errors, it helps the developers in assessing what the risky operations are, how they work, and ultimately how can they fail. Furthermore, having a working test suite with common and edge cases is useful to ensure that code does not behave unexpectedly, and that future versions of the library do not break compatibility. + +Echidna testing can be integrated into CI/CD pipelines, so bugs are caught early and the developers are notified about security risks in their contracts. + +## Who are these tests designed for? + +In principle, these tests are meant to be an entry level practice to learn how to use Echidna for assertion-based fuzz tests, targeting a stand-alone library. It is a self contained exercise that shows how to determine the contract invariants, create proper tests, and configure the relevant Echidna parameters for the campaign. + +Determining the invariants is a process that involves an intermediate-level comprehension of the library and the math properties behind the operations implemented. For example, the addition function has the `x+y == y+x` commutative property. This statement should always be true, no matter the values of `x` and `y`, therefore it should be a good invariant for the system. More complex operations can demand more complex invariants. + +The next step, creating the tests, means to implement Solidity functions that verify the previously defined invariants. Echidna is a fuzz tester, so it can quickly test different values for the arguments of a function. For example, the commutative property can be tested using a function that takes two parameters and performs the additions, as shown below: + +```solidity +// Test for commutative property +// x + y == y + x +function add_test_commutative(SD59x18 x, SD59x18 y) public pure { + SD59x18 x_y = x.add(y); + SD59x18 y_x = y.add(x); + + assert(x_y.eq(y_x)); +} +``` + +Finally, the fuzzer has to be instructed to perform the correct type of test, the number of test runs to be made, among other configuration parameters. Since the invariant is checked using an assertion, Echidna must be configured to try to find assertion violations. In this mode, different argument values are passed to `add_test_commutative()`, and the result of the `assert(x_y == y_x)` expression is evaluated for each call: if the assertion is false, the invariant was broken, and it is a sign that there can be an issue with the library implementation. + +However, even if this particular test suite is meant as an exercise, it can be used as a template to create tests for other fixed-point arithmetic libraries implementations. diff --git a/contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol b/contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol new file mode 100644 index 0000000..298eb21 --- /dev/null +++ b/contracts/Math/PRBMath/v3/PRBMathSD59x18PropertyTests.sol @@ -0,0 +1,2253 @@ +pragma solidity ^0.8.19; + +import {SD59x18} from "@prb-math-v3/SD59x18.sol"; +import {add, sub, eq, gt, gte, lt, lte, lshift, rshift} from "@prb-math-v3/sd59x18/Helpers.sol"; +import {convert} from "@prb-math-v3/sd59x18/Conversions.sol"; +import {msb} from "@prb-math-v3/Common.sol"; +import {intoUint128, intoUint256} from "@prb-math-v3/sd59x18/Casting.sol"; +import {mul, div, abs, ln, exp, exp2, log2, sqrt, pow, avg, inv, log10, floor, powu, gm} from "@prb-math-v3/sd59x18/Math.sol"; +import "./utils/AssertionHelperSD.sol"; + +contract CryticPRBMath59x18Propertiesv3 is AssertionHelperSD { + /* ================================================================ + 59x18 fixed-point constants used for testing specific values. + This assumes that PRBMath library's convert(x) works as expected. + ================================================================ */ + SD59x18 internal ZERO_FP = convert(0); + SD59x18 internal ONE_FP = convert(1); + SD59x18 internal MINUS_ONE_FP = convert(-1); + SD59x18 internal TWO_FP = convert(2); + SD59x18 internal THREE_FP = convert(3); + SD59x18 internal EIGHT_FP = convert(8); + SD59x18 internal THOUSAND_FP = convert(1000); + SD59x18 internal MINUS_SIXTY_FOUR_FP = convert(-64); + SD59x18 internal EPSILON = SD59x18.wrap(1); + SD59x18 internal ONE_TENTH_FP = convert(1).div(convert(10)); + + /* ================================================================ + Constants used for precision loss calculations + ================================================================ */ + uint256 internal REQUIRED_SIGNIFICANT_DIGITS = 9; + SD59x18 internal LOG2_PRECISION_LOSS = SD59x18.wrap(1); + + /* ================================================================ + Integer representations maximum values. + These constants are used for testing edge cases or limits for + possible values. + ================================================================ */ + /// @dev The unit number, which gives the decimal precision of SD59x18. + int256 constant uUNIT = 1e18; + SD59x18 constant UNIT = SD59x18.wrap(1e18); + + /// @dev The minimum value an SD59x18 number can have. + int256 constant uMIN_SD59x18 = + -57896044618658097711785492504343953926634992332820282019728_792003956564819968; + SD59x18 constant MIN_SD59x18 = SD59x18.wrap(uMIN_SD59x18); + + /// @dev The maximum value an SD59x18 number can have. + int256 constant uMAX_SD59x18 = + 57896044618658097711785492504343953926634992332820282019728_792003956564819967; + SD59x18 constant MAX_SD59x18 = SD59x18.wrap(uMAX_SD59x18); + + /// @dev The maximum input permitted in {exp2}. + int256 constant uEXP2_MAX_INPUT = 192e18 - 1; + SD59x18 constant EXP2_MAX_INPUT = SD59x18.wrap(uEXP2_MAX_INPUT); + + /// @dev The maximum input permitted in {exp}. + int256 constant uEXP_MAX_INPUT = 133_084258667509499440; + SD59x18 constant EXP_MAX_INPUT = SD59x18.wrap(uEXP_MAX_INPUT); + + /// @dev Euler's number as an SD59x18 number. + SD59x18 constant E = SD59x18.wrap(2_718281828459045235); + + int256 constant uMAX_SQRT = uMAX_SD59x18 / uUNIT; + SD59x18 constant MAX_SQRT = SD59x18.wrap(uMAX_SQRT); + + SD59x18 internal constant MAX_PERMITTED_EXP2 = SD59x18.wrap(192e18 - 1); + SD59x18 internal constant MIN_PERMITTED_EXP2 = + SD59x18.wrap(-59_794705707972522261); + + SD59x18 internal constant MAX_PERMITTED_EXP = + SD59x18.wrap(133_084258667509499440); + SD59x18 internal constant MIN_PERMITTED_EXP = + SD59x18.wrap(-41_446531673892822322); + + SD59x18 internal constant MAX_PERMITTED_POW = + SD59x18.wrap(2 ** 192 * 10 ** 18 - 1); + /// @dev Half the UNIT number. + int256 constant uHALF_UNIT = 0.5e18; + SD59x18 constant HALF_UNIT = SD59x18.wrap(uHALF_UNIT); + + /// @dev log2(10) as an SD59x18 number. + int256 constant uLOG2_10 = 3_321928094887362347; + SD59x18 constant LOG2_10 = SD59x18.wrap(uLOG2_10); + + /// @dev log2(e) as an SD59x18 number. + int256 constant uLOG2_E = 1_442695040888963407; + SD59x18 constant LOG2_E = SD59x18.wrap(uLOG2_E); + + /* ================================================================ + Events used for debugging or showing information. + ================================================================ */ + event Value(string reason, SD59x18 val); + event LogErr(bytes error); + + /* ================================================================ + Helper functions. + ================================================================ */ + + // Check that there are remaining significant digits after a multiplication + // Uses functions from the library under test! + function significant_digits_are_lost_in_mult( + SD59x18 a, + SD59x18 b + ) public pure returns (bool) { + int256 la = convert(floor(log10(abs(a)))); + int256 lb = convert(floor(log10(abs(b)))); + + return (la + lb < -18); + } + + // Return how many significant digits will remain after multiplying a and b + // Uses functions from the library under test! + function significant_digits_after_mult( + SD59x18 a, + SD59x18 b + ) public pure returns (uint256) { + int256 la = convert(floor(log10(abs(a)))); + int256 lb = convert(floor(log10(abs(b)))); + int256 prec = la + lb; + + if (prec < -18) return 0; + else return (18 + absInt(prec)); + } + + // Return how many significant digits will be lost after multiplying a and b + // Uses functions from the library under test! + function significant_digits_lost_in_mult( + SD59x18 a, + SD59x18 b + ) public pure returns (uint256) { + int256 la = convert(floor(log10(abs(a)))); + int256 lb = convert(floor(log10(abs(b)))); + + if (la > 0 && lb > 0) { + return 0; + } else { + return absInt(la) < absInt(lb) ? uint256(-la) : uint256(-lb); + } + } + + // Return the absolute value of the input + function absInt(int256 a) public pure returns (uint256) { + return a >= 0 ? uint256(a) : uint256(-a); + } + + /* ================================================================ + Library wrappers. + These functions allow calling the PRBMathSD59x18 library. + ================================================================ */ + function debug(string calldata x, SD59x18 y) public { + emit Value(x, y); + } + + // Wrapper for external try/catch calls + function helpersAdd(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return add(x, y); + } + + // Wrapper for external try/catch calls + function helpersSub(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return sub(x, y); + } + + // Wrapper for external try/catch calls + function helpersMul(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return mul(x, y); + } + + function helpersDiv(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return div(x, y); + } + + function neg(SD59x18 x) public pure returns (SD59x18) { + return SD59x18.wrap(-SD59x18.unwrap(x)); + } + + function helpersAbs(SD59x18 x) public pure returns (SD59x18) { + return abs(x); + } + + function helpersLn(SD59x18 x) public pure returns (SD59x18) { + return ln(x); + } + + function helpersExp(SD59x18 x) public pure returns (SD59x18) { + return exp(x); + } + + function helpersExp2(SD59x18 x) public pure returns (SD59x18) { + return exp2(x); + } + + function helpersLog2(SD59x18 x) public pure returns (SD59x18) { + return log2(x); + } + + function helpersSqrt(SD59x18 x) public pure returns (SD59x18) { + return sqrt(x); + } + + function helpersPow(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return pow(x, y); + } + + function helpersPowu(SD59x18 x, uint256 y) public pure returns (SD59x18) { + return powu(x, y); + } + + function helpersAvg(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return avg(x, y); + } + + function helpersInv(SD59x18 x) public pure returns (SD59x18) { + return inv(x); + } + + function helpersLog10(SD59x18 x) public pure returns (SD59x18) { + return log10(x); + } + + function helpersFloor(SD59x18 x) public pure returns (SD59x18) { + return floor(x); + } + + function helpersGm(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return gm(x, y); + } + + /* ================================================================ + + TESTS FOR FUNCTION add() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for commutative property + // x + y == y + x + function add_test_commutative(SD59x18 x, SD59x18 y) public { + SD59x18 x_y = x.add(y); + SD59x18 y_x = y.add(x); + + assertEq(x_y, y_x); + } + + // Test for associative property + // (x + y) + z == x + (y + z) + function add_test_associative(SD59x18 x, SD59x18 y, SD59x18 z) public { + SD59x18 x_y = x.add(y); + SD59x18 y_z = y.add(z); + SD59x18 xy_z = x_y.add(z); + SD59x18 x_yz = x.add(y_z); + + assertEq(xy_z, x_yz); + } + + // Test for identity operation + // x + 0 == x (equivalent to x + (-x) == 0) + function add_test_identity(SD59x18 x) public { + SD59x18 x_0 = x.add(ZERO_FP); + + assertEq(x, x_0); + assertEq(x.sub(x), ZERO_FP); + } + + // Test that the result increases or decreases depending + // on the value to be added + function add_test_values(SD59x18 x, SD59x18 y) public { + SD59x18 x_y = x.add(y); + + if (y.gte(ZERO_FP)) { + assertGte(x_y, x); + } else { + assertLt(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These should make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the addition must be between the maximum + // and minimum allowed values for SD59x18 + function add_test_range(SD59x18 x, SD59x18 y) public { + try this.helpersAdd(x, y) returns (SD59x18 result) { + assertLte(result, MAX_SD59x18); + assertGte(result, MIN_SD59x18); + } catch { + // If it reverts, just ignore + } + } + + // Adding zero to the maximum value shouldn't revert, as it is valid + // Moreover, the result must be MAX_SD59x18 + function add_test_maximum_value() public { + try this.helpersAdd(MAX_SD59x18, ZERO_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Adding one to the maximum value should revert, as it is out of range + function add_test_maximum_value_plus_one() public { + try this.helpersAdd(MAX_SD59x18, ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + // Adding zero to the minimum value shouldn't revert, as it is valid + // Moreover, the result must be MIN_SD59x18 + function add_test_minimum_value() public { + try this.helpersAdd(MIN_SD59x18, ZERO_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MIN_SD59x18); + } catch { + assert(false); + } + } + + // Adding minus one to the maximum value should revert, as it is out of range + function add_test_minimum_value_plus_negative_one() public { + try this.helpersAdd(MIN_SD59x18, MINUS_ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + /* ================================================================ + + TESTS FOR FUNCTION sub() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test equivalence to addition + // x - y == x + (-y) + function sub_test_equivalence_to_addition(SD59x18 x, SD59x18 y) public { + SD59x18 minus_y = neg(y); + SD59x18 addition = x.add(minus_y); + SD59x18 subtraction = x.sub(y); + + assertEq(addition, subtraction); + } + + // Test for non-commutative property + // x - y == -(y - x) + function sub_test_non_commutative(SD59x18 x, SD59x18 y) public { + SD59x18 x_y = x.sub(y); + SD59x18 y_x = y.sub(x); + + assertEq(x_y, neg(y_x)); + } + + // Test for identity operation + // x - 0 == x (equivalent to x - x == 0) + function sub_test_identity(SD59x18 x) public { + SD59x18 x_0 = x.sub(ZERO_FP); + + assertEq(x_0, x); + assertEq(x.sub(x), ZERO_FP); + } + + // Test for neutrality over addition and subtraction + // (x - y) + y == (x + y) - y == x + function sub_test_neutrality(SD59x18 x, SD59x18 y) public { + SD59x18 x_minus_y = x.sub(y); + SD59x18 x_plus_y = x.add(y); + + SD59x18 x_minus_y_plus_y = x_minus_y.add(y); + SD59x18 x_plus_y_minus_y = x_plus_y.sub(y); + + assertEq(x_minus_y_plus_y, x_plus_y_minus_y); + assertEq(x_minus_y_plus_y, x); + } + + // Test that the result increases or decreases depending + // on the value to be subtracted + function sub_test_values(SD59x18 x, SD59x18 y) public { + SD59x18 x_y = x.sub(y); + + if (y.gte(ZERO_FP)) { + assertLte(x_y, x); + } else { + assertGt(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the subtraction must be between the maximum + // and minimum allowed values for SD59x18 + function sub_test_range(SD59x18 x, SD59x18 y) public { + try this.helpersSub(x, y) returns (SD59x18 result) { + assertLte(result, MAX_SD59x18); + assertGte(result, MIN_SD59x18); + } catch { + // If it reverts, just ignore + } + } + + // Subtracting zero from the maximum value shouldn't revert, as it is valid + // Moreover, the result must be MAX_SD59x18 + function sub_test_maximum_value() public { + try this.helpersSub(MAX_SD59x18, ZERO_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Subtracting minus one from the maximum value should revert, + // as it is out of range + function sub_test_maximum_value_minus_neg_one() public { + try this.helpersSub(MAX_SD59x18, MINUS_ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + // Subtracting zero from the minimum value shouldn't revert, as it is valid + // Moreover, the result must be MIN_SD59x18 + function sub_test_minimum_value() public { + try this.helpersSub(MIN_SD59x18, ZERO_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MIN_SD59x18); + } catch { + assert(false); + } + } + + // Subtracting one from the minimum value should revert, as it is out of range + function sub_test_minimum_value_minus_one() public { + try this.helpersSub(MIN_SD59x18, ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + /* ================================================================ + + TESTS FOR FUNCTION mul() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for commutative property + // x * y == y * x + function mul_test_commutative(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + SD59x18 x_y = x.mul(y); + SD59x18 y_x = y.mul(x); + + assertEq(x_y, y_x); + } + + // Test for associative property + // (x * y) * z == x * (y * z) + function mul_test_associative(SD59x18 x, SD59x18 y, SD59x18 z) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18) && z.gt(MIN_SD59x18)); + SD59x18 x_y = x.mul(y); + SD59x18 y_z = y.mul(z); + SD59x18 xy_z = x_y.mul(z); + SD59x18 x_yz = x.mul(y_z); + + require(xy_z.neq(ZERO_FP) && x_yz.neq(ZERO_FP)); + + // Checks that at least 9 digits of precision are left after multiplication + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + require( + significant_digits_after_mult(y, z) > REQUIRED_SIGNIFICANT_DIGITS + ); + require( + significant_digits_after_mult(x_y, z) > REQUIRED_SIGNIFICANT_DIGITS + ); + require( + significant_digits_after_mult(x, y_z) > REQUIRED_SIGNIFICANT_DIGITS + ); + + uint256 digitsLost = significant_digits_lost_in_mult(x, y); + digitsLost += significant_digits_lost_in_mult(x, z); + digitsLost += significant_digits_lost_in_mult(y, z); + + assertEqWithinDecimalPrecision(xy_z, x_yz, digitsLost); + } + + // Test for distributive property + // x * (y + z) == x * y + x * z + function mul_test_distributive(SD59x18 x, SD59x18 y, SD59x18 z) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18) && z.gt(MIN_SD59x18)); + SD59x18 y_plus_z = y.add(z); + SD59x18 x_times_y_plus_z = x.mul(y_plus_z); + + SD59x18 x_times_y = x.mul(y); + SD59x18 x_times_z = x.mul(z); + + require( + add(x_times_y, x_times_z).neq(ZERO_FP) && + x_times_y_plus_z.neq(ZERO_FP) + ); + assertEqWithinTolerance( + add(x_times_y, x_times_z), + x_times_y_plus_z, + ONE_TENTH_FP, + "0.1%" + ); + } + + // Test for identity operation + // x * 1 == x (also check that x * 0 == 0) + function mul_test_identity(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + SD59x18 x_1 = x.mul(ONE_FP); + SD59x18 x_0 = x.mul(ZERO_FP); + + assertEq(x_0, ZERO_FP); + assertEq(x_1, x); + } + + // If x is positive and y is >= 1, the result should be larger than or equal to x + // If x is positive and y is < 1, the result should be smaller than x + function mul_test_x_positive(SD59x18 x, SD59x18 y) public { + require(x.gte(ZERO_FP)); + + SD59x18 x_y = x.mul(y); + + if (y.gte(ONE_FP)) { + assertGte(x_y, x); + } else { + assertLte(x_y, x); + } + } + + // If x is negative and y is >= 1, the result should be smaller than or equal to x + // If x is negative and y is < 1, the result should be larger than or equal to x + function mul_test_x_negative(SD59x18 x, SD59x18 y) public { + require(x.lte(ZERO_FP) && y.neq(ZERO_FP)); + + SD59x18 x_y = x.mul(y); + + if (y.gte(ONE_FP)) { + assertLte(x_y, x); + } else { + assertGte(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the multiplication must be between the maximum + // and minimum allowed values for SD59x18 + function mul_test_range(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + + try this.helpersMul(x, y) returns (SD59x18 result) { + assertLte(result, MAX_SD59x18); + assertGte(result, MIN_SD59x18); + } catch { + // If it reverts, just ignore + } + } + + // Multiplying the maximum value times one shouldn't revert, as it is valid + // Moreover, the result must be MAX_SD59x18 + function mul_test_maximum_value() public { + try this.helpersMul(MAX_SD59x18, ONE_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Multiplying the minimum value times one shouldn't revert, as it is valid + // Moreover, the result must be MIN_SD59x18 + function mul_test_minimum_value() public { + try this.helpersMul(MIN_SD59x18.add(ONE_FP), ONE_FP) returns ( + SD59x18 result + ) { + // Expected behaviour, does not revert + assertEq(result, MIN_SD59x18.add(ONE_FP)); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION div() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for identity property + // x / 1 == x (equivalent to x / x == 1) + function div_test_division_identity_x_div_1(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + SD59x18 div_1 = div(x, ONE_FP); + + assertEq(x, div_1); + } + + // Test for identity property + // x/x should not revert unless x == 0 || x == MIN_SD59x18 + function div_test_division_identity_x_div_x(SD59x18 x) public { + SD59x18 div_x; + + try this.helpersDiv(x, x) { + // This should always equal one + div_x = div(x, x); + assertEq(div_x, ONE_FP); + } catch { + // There are a couple of allowed cases for a revert: + // 1. x == 0 + // 2. x == MIN_SD59x18 + // 3. when the result overflows + assert(x.eq(ZERO_FP) || x.eq(MIN_SD59x18)); + } + } + + // Test for negative divisor + // x / -y == -(x / y) + function div_test_negative_divisor(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + require(y.lt(ZERO_FP)); + + SD59x18 x_y = div(x, y); + SD59x18 x_minus_y = div(x, neg(y)); + + assertEq(x_y, neg(x_minus_y)); + } + + // Test for division with 0 as numerator + // 0 / x = 0 + function div_test_division_num_zero(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + require(x.neq(ZERO_FP)); + + SD59x18 div_0 = div(ZERO_FP, x); + + assertEq(ZERO_FP, div_0); + } + + // Test that the absolute value of the result increases or + // decreases depending on the denominator's absolute value + function div_test_values(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + require(y.neq(ZERO_FP)); + + SD59x18 x_y = abs(div(x, y)); + + if (abs(y).gte(ONE_FP)) { + assertLte(x_y, abs(x)); + } else { + assertGte(x_y, abs(x)); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for division by zero + function div_test_div_by_zero(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + try this.helpersDiv(x, ZERO_FP) { + // Unexpected, this should revert + assert(false); + } catch { + // Expected revert + } + } + + // Test for division by a large value, the result should be less than one + function div_test_maximum_denominator(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + SD59x18 div_large = div(x, MAX_SD59x18); + + assertLte(abs(div_large), ONE_FP); + } + + // Test for division of a large value + // This should revert if |x| < 1 as it would return a value higher than max + function div_test_maximum_numerator(SD59x18 y) public { + SD59x18 div_large; + + try this.helpersDiv(MAX_SD59x18, y) { + // If it didn't revert, then |x| >= 1 + div_large = div(MAX_SD59x18, y); + + assertGte(abs(y), ONE_FP); + } catch { + // Expected revert as result is higher than max + } + } + + // Test for values in range + function div_test_range(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + SD59x18 result; + + try this.helpersDiv(x, y) { + // If it returns a value, it must be in range + result = div(x, y); + assertLte(result, MAX_SD59x18); + assertGte(result, MIN_SD59x18); + } catch { + // Otherwise, it should revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION neg() + + ================================================================ */ + + /* ================================================================ + Tests for mathematical properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for the double negation + // -(-x) == x + function neg_test_double_negation(SD59x18 x) public { + SD59x18 double_neg = neg(neg(x)); + + assertEq(x, double_neg); + } + + // Test for the identity operation + // x + (-x) == 0 + function neg_test_identity(SD59x18 x) public { + SD59x18 neg_x = neg(x); + + assertEq(add(x, neg_x), ZERO_FP); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for the zero-case + // -0 == 0 + function neg_test_zero() public { + SD59x18 neg_x = neg(ZERO_FP); + + assertEq(neg_x, ZERO_FP); + } + + // Test for the maximum value case + // Since this is implementation-dependant, we will actually test with MAX_SD59x18-EPS + function neg_test_maximum() public { + try this.neg(sub(MAX_SD59x18, EPSILON)) { + // Expected behaviour, does not revert + } catch { + assert(false); + } + } + + // Test for the minimum value case + // Since this is implementation-dependant, we will actually test with MIN_SD59x18+EPS + function neg_test_minimum() public { + try this.neg(add(MIN_SD59x18, EPSILON)) { + // Expected behaviour, does not revert + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION abs() + + ================================================================ */ + + /* ================================================================ + Tests for mathematical properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the absolute value is always positive + function abs_test_positive(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + SD59x18 abs_x = abs(x); + + assertGte(abs_x, ZERO_FP); + } + + // Test that the absolute value of a number equals the + // absolute value of the negative of the same number + function abs_test_negative(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + + SD59x18 abs_x = abs(x); + SD59x18 abs_minus_x = abs(neg(x)); + + assertEq(abs_x, abs_minus_x); + } + + // Test the multiplicativeness property + // | x * y | == |x| * |y| + function abs_test_multiplicativeness(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + + SD59x18 abs_x = abs(x); + SD59x18 abs_y = abs(y); + SD59x18 abs_xy = abs(mul(x, y)); + SD59x18 abs_x_abs_y = mul(abs_x, abs_y); + + // Failure if all significant digits are lost + require(significant_digits_are_lost_in_mult(abs_x, abs_y) == false); + + // Assume a tolerance of two bits of precision + assertEqWithinBitPrecision(abs_xy, abs_x_abs_y, 2); + } + + // Test the subadditivity property + // | x + y | <= |x| + |y| + function abs_test_subadditivity(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + + SD59x18 abs_x = abs(x); + SD59x18 abs_y = abs(y); + SD59x18 abs_xy = abs(add(x, y)); + + assertLte(abs_xy, add(abs_x, abs_y)); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test the zero-case | 0 | = 0 + function abs_test_zero() public { + SD59x18 abs_zero; + + try this.helpersAbs(ZERO_FP) { + // If it doesn't revert, the value must be zero + abs_zero = this.helpersAbs(ZERO_FP); + assertEq(abs_zero, ZERO_FP); + } catch { + // Unexpected, the function must not revert here + assert(false); + } + } + + // Test the maximum value + function abs_test_maximum() public { + SD59x18 abs_max; + + try this.helpersAbs(MAX_SD59x18) { + // If it doesn't revert, the value must be MAX_SD59x18 + abs_max = this.helpersAbs(MAX_SD59x18); + assertEq(abs_max, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Test the minimum value + function abs_test_minimum_revert() public { + SD59x18 abs_min; + + try this.helpersAbs(MIN_SD59x18) { + // It should always revert for MIN_SD59x18 + assert(false); + } catch {} + } + + // Test the minimum value + function abs_test_minimum_allowed() public { + SD59x18 abs_min; + SD59x18 input = MIN_SD59x18.add(SD59x18.wrap(1)); + + try this.helpersAbs(input) { + // If it doesn't revert, the value must be the negative of MIN_SD59x18 + 1 + abs_min = this.helpersAbs(input); + assertEq(abs_min, neg(input)); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION inv() + + ================================================================ */ + + /* ================================================================ + Tests for mathematical properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the inverse of the inverse is close enough to the + // original number + function inv_test_double_inverse(SD59x18 x) public { + require(x.neq(ZERO_FP)); + require(inv(x).neq(ZERO_FP)); + + SD59x18 double_inv_x = inv(inv(x)); + + // The maximum loss of precision will be 2 * log10(x) digits rounded up + uint256 loss = 2 * significant_digits_lost_in_mult(x, inv(x)) + 2; + + assertEqWithinDecimalPrecision(x, double_inv_x, loss); + } + + // Test equivalence with division + function inv_test_division(SD59x18 x) public { + require(x.neq(ZERO_FP)); + + SD59x18 inv_x = inv(x); + SD59x18 div_1_x = div(ONE_FP, x); + + assertEq(inv_x, div_1_x); + } + + // Test the anticommutativity of the division + // x / y == 1 / (y / x) + function inv_test_division_noncommutativity(SD59x18 x, SD59x18 y) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + SD59x18 x_y = div(x, y); + SD59x18 y_x = div(y, x); + + assertEqWithinTolerance(x_y, inv(y_x), ONE_FP, "1%"); + } + + // Test the multiplication of inverses + // 1/(x * y) == 1/x * 1/y + function inv_test_multiplication(SD59x18 x, SD59x18 y) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + SD59x18 inv_x = inv(x); + SD59x18 inv_y = inv(y); + SD59x18 inv_x_times_inv_y = mul(inv_x, inv_y); + + SD59x18 x_y = mul(x, y); + SD59x18 inv_x_y = inv(x_y); + + assertEqWithinTolerance(inv_x_y, inv_x_times_inv_y, ONE_FP, "1%"); + } + + // Test identity property + function inv_test_identity(SD59x18 x) public { + require(x.neq(ZERO_FP)); + + SD59x18 inv_x = inv(x); + SD59x18 identity = mul(inv_x, x); + + // They should agree with a tolerance of one percent + assertEqWithinTolerance(identity, ONE_FP, ONE_FP, "1%"); + } + + // Test that the absolute value of the result is in range zero-one + // if x is greater than one, else, the absolute value of the result + // must be greater than one + function inv_test_values(SD59x18 x) public { + require(x.neq(ZERO_FP)); + + SD59x18 abs_inv_x = abs(inv(x)); + + if (abs(x).gte(ONE_FP)) { + assertLte(abs_inv_x, ONE_FP); + } else { + assertGt(abs_inv_x, ONE_FP); + } + } + + // Test that the result has the same sign as the argument. + // Since inv() rounds towards zero, we are checking the zero case as well + function inv_test_sign(SD59x18 x) public { + require(x.neq(ZERO_FP)); + + SD59x18 inv_x = inv(x); + + if (x.gt(ZERO_FP)) { + assertGte(inv_x, ZERO_FP); + } else { + assertLte(inv_x, ZERO_FP); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test the zero-case, should revert + function inv_test_zero() public { + try this.helpersInv(ZERO_FP) { + // Unexpected, the function must revert + assert(false); + } catch {} + } + + // Test the maximum value case, should not revert, and be close to zero + function inv_test_maximum() public { + SD59x18 inv_maximum; + + try this.helpersInv(MAX_SD59x18) { + inv_maximum = this.helpersInv(MAX_SD59x18); + assertEqWithinBitPrecision(inv_maximum, ZERO_FP, 10); + } catch { + // Unexpected, the function must not revert + assert(false); + } + } + + // Test the minimum value case, should not revert, and be close to zero + function inv_test_minimum() public { + SD59x18 inv_minimum; + + try this.helpersInv(MIN_SD59x18) { + inv_minimum = this.helpersInv(MIN_SD59x18); + assertEqWithinBitPrecision(abs(inv_minimum), ZERO_FP, 10); + } catch { + // Unexpected, the function must not revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION avg() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the result is between the two operands + // avg(x, y) >= min(x, y) && avg(x, y) <= max(x, y) + function avg_test_values_in_range(SD59x18 x, SD59x18 y) public { + SD59x18 avg_xy = avg(x, y); + + if (x.gte(y)) { + assertGte(avg_xy, y); + assertLte(avg_xy, x); + } else { + assertGte(avg_xy, x); + assertLte(avg_xy, y); + } + } + + // Test that the average of the same number is itself + // avg(x, x) == x + function avg_test_one_value(SD59x18 x) public { + SD59x18 avg_x = avg(x, x); + + assertEq(avg_x, x); + } + + // Test that the order of operands is irrelevant + // avg(x, y) == avg(y, x) + function avg_test_operand_order(SD59x18 x, SD59x18 y) public { + SD59x18 avg_xy = avg(x, y); + SD59x18 avg_yx = avg(y, x); + + assertEq(avg_xy, avg_yx); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for the maximum value + function avg_test_maximum() public { + SD59x18 result; + + // This may revert due to overflow depending on implementation + // If it doesn't revert, the result must be MAX_SD59x18 + try this.helpersAvg(MAX_SD59x18, MAX_SD59x18) { + result = this.helpersAvg(MAX_SD59x18, MAX_SD59x18); + assertEq(result, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Test for the minimum value + function avg_test_minimum() public { + SD59x18 result; + + // This may revert due to overflow depending on implementation + // If it doesn't revert, the result must be MIN_SD59x18 + try this.helpersAvg(MIN_SD59x18, MIN_SD59x18) { + result = this.helpersAvg(MIN_SD59x18, MIN_SD59x18); + assertEq(result, MIN_SD59x18); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION pow() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for zero exponent + // x ** 0 == 1 + function pow_test_zero_exponent(SD59x18 x) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + SD59x18 x_pow_0 = pow(x, ZERO_FP); + + assertEq(x_pow_0, ONE_FP); + } + + // Test for zero base + // 0 ** a == 0 (for positive a) + function pow_test_zero_base_non_zero_exponent(SD59x18 a) public { + require(a.gt(ZERO_FP)); + SD59x18 zero_pow_a = pow(ZERO_FP, a); + + assertEq(zero_pow_a, ZERO_FP); + } + + // Test for zero base + // 0 ** 0 == 1 + function pow_test_zero_base_zero_exponent() public { + SD59x18 zero_pow_a = pow(ZERO_FP, ZERO_FP); + + assertEq(zero_pow_a, ONE_FP); + } + + // Test for exponent one + // x ** 1 == x + function pow_test_one_exponent(SD59x18 x) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + SD59x18 x_pow_1 = pow(x, ONE_FP); + + assertEq(x_pow_1, x); + } + + // Test for base one + // 1 ** a == 1 + function pow_test_base_one(SD59x18 a) public { + SD59x18 one_pow_a = pow(ONE_FP, a); + + assertEq(one_pow_a, ONE_FP); + } + + // Test for product of powers of the same base + // x ** a * x ** b == x ** (a + b) + function pow_test_product_same_base( + SD59x18 x, + SD59x18 a, + SD59x18 b + ) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + require(a.gte(MIN_PERMITTED_EXP2) && b.gte(MIN_PERMITTED_EXP2)); + + SD59x18 x_a = pow(x, a); + SD59x18 x_b = pow(x, b); + SD59x18 x_ab = pow(x, a.add(b)); + + assertEqWithinDecimalPrecision(mul(x_a, x_b), x_ab, 9); + } + + // Test for power of an exponentiation + // (x ** a) ** b == x ** (a * b) + function pow_test_power_of_an_exponentiation( + SD59x18 x, + SD59x18 a, + SD59x18 b + ) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + require(a.mul(b).neq(ZERO_FP)); + require(a.gte(MIN_PERMITTED_EXP2) && b.gte(MIN_PERMITTED_EXP2)); + + SD59x18 x_a = pow(x, a); + SD59x18 x_a_b = pow(x_a, b); + SD59x18 x_ab = pow(x, a.mul(b)); + require(x_a_b.neq(ZERO_FP) && x_ab.neq(ZERO_FP)); + + assertEqWithinDecimalPrecision(x_a_b, x_ab, 9); + } + + // Test for power of a product + // (x * y) ** a == x ** a * y ** a + function pow_test_product_power(SD59x18 x, SD59x18 y, SD59x18 a) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + require(y.gte(ZERO_FP) && y.lte(MAX_PERMITTED_POW)); + + require(a.gt(convert(2 ** 32))); // to avoid massive loss of precision + + SD59x18 x_y = mul(x, y); + SD59x18 xy_a = pow(x_y, a); + + SD59x18 x_a = pow(x, a); + SD59x18 y_a = pow(y, a); + + assertEqWithinBitPrecision(mul(x_a, y_a), xy_a, 10); + } + + // Test for result being greater than or lower than the argument, depending on + // its absolute value and the value of the exponent + function pow_test_positive_exponent(SD59x18 x, SD59x18 a) public { + require(x.gte(ZERO_FP) && a.gte(ZERO_FP)); + + SD59x18 x_a = pow(x, a); + + if (abs(x).gte(ONE_FP)) { + assertGte(abs(x_a), ONE_FP); + } + + if (abs(x).lte(ONE_FP)) { + assertLte(abs(x_a), ONE_FP); + } + } + + // Test for result being greater than or lower than the argument, depending on + // its absolute value and the value of the exponent + function pow_test_negative_exponent(SD59x18 x, SD59x18 a) public { + require(x.gte(ZERO_FP) && a.lte(ZERO_FP)); + + SD59x18 x_a = pow(x, a); + + if (abs(x).gte(ONE_FP)) { + assertLte(abs(x_a), ONE_FP); + } + + if (abs(x).lte(ONE_FP)) { + assertGte(abs(x_a), ONE_FP); + } + } + + // Test for result sign: if the exponent is even, sign is positive + // if the exponent is odd, preserves the sign of the base + function pow_test_sign(SD59x18 x, SD59x18 a) public { + require(x.gte(ZERO_FP)); + + SD59x18 x_a = pow(x, a); + + // This prevents the case where a small negative number gets + // rounded down to zero and thus changes sign + require(x_a.neq(ZERO_FP)); + + // If the exponent is even + if (a.mod(convert(2)).eq(ZERO_FP)) { + assertEq(x_a, abs(x_a)); + } else { + // x_a preserves x sign + if (x.lt(ZERO_FP)) { + assertLt(x_a, ZERO_FP); + } else { + assertGt(x_a, ZERO_FP); + } + } + } + + // pow(2, a) == exp2(a) + function pow_test_exp2_equivalence(SD59x18 a) public { + SD59x18 pow_result = pow(convert(2), a); + SD59x18 exp2_result = exp2(a); + + assertEq(pow_result, exp2_result); + } + + // Power is strictly increasing + // x > y && a >= 0 --> pow(x, a) >= pow(y, a) + function pow_test_strictly_increasing( + SD59x18 x, + SD59x18 y, + SD59x18 a + ) public { + require(x.gt(y) && x.lte(MAX_PERMITTED_POW)); + require(a.gte(ZERO_FP)); + + SD59x18 x_a = pow(x, a); + SD59x18 y_a = pow(y, a); + + assertGte(x_a, y_a); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for maximum base and exponent > 1 + function pow_test_maximum_base(SD59x18 a) public { + require(a.gt(ONE_FP)); + + try this.helpersPow(MAX_SD59x18, a) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for abs(base) < 1 and high exponent + function pow_test_high_exponent(SD59x18 x, SD59x18 a) public { + require(abs(x).lt(ONE_FP) && a.gt(convert(2 ** 32))); + + SD59x18 result = pow(x, a); + + assertEq(result, ZERO_FP); + } + + /* ================================================================ + + TESTS FOR FUNCTION sqrt() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for the inverse operation + // sqrt(x) * sqrt(x) == x + function sqrt_test_inverse_mul(SD59x18 x) public { + require(x.gte(ZERO_FP)); + + SD59x18 sqrt_x = sqrt(x); + SD59x18 sqrt_x_squared = mul(sqrt_x, sqrt_x); + + // Precision loss is at most half the bits of the operand + assertEqWithinBitPrecision( + sqrt_x_squared, + x, + (intoUint256(log2(x)) >> 1) + 2 + ); + } + + // Test for the inverse operation + // sqrt(x) ** 2 == x + function sqrt_test_inverse_pow(SD59x18 x) public { + require(x.gte(ZERO_FP)); + + SD59x18 sqrt_x = sqrt(x); + SD59x18 sqrt_x_squared = pow(sqrt_x, convert(2)); + + // Precision loss is at most half the bits of the operand + assertEqWithinBitPrecision( + sqrt_x_squared, + x, + (intoUint256(log2(x)) >> 1) + 2 + ); + } + + // Test for distributive property respect to the multiplication + // sqrt(x) * sqrt(y) == sqrt(x * y) + function sqrt_test_distributive(SD59x18 x, SD59x18 y) public { + require(x.gte(ZERO_FP) && y.gte(ZERO_FP)); + + SD59x18 sqrt_x = sqrt(x); + SD59x18 sqrt_y = sqrt(y); + SD59x18 sqrt_x_sqrt_y = mul(sqrt_x, sqrt_y); + SD59x18 sqrt_xy = sqrt(mul(x, y)); + + // Ensure we have enough significant digits for the result to be meaningful + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + require( + significant_digits_after_mult(sqrt_x, sqrt_y) > + REQUIRED_SIGNIFICANT_DIGITS + ); + + // Allow an error of up to one tenth of a percent + assertEqWithinTolerance(sqrt_x_sqrt_y, sqrt_xy, ONE_TENTH_FP, "0.1%"); + } + + // Test that sqrt is strictly increasing + // x >= 0 && y > x --> sqrt(y) >= sqrt(x) + function sqrt_test_strictly_increasing(SD59x18 x, SD59x18 y) public { + require(x.gte(ZERO_FP) && x.lte(MAX_SQRT.sub(SD59x18.wrap(1)))); + require(y.gt(x) && y.lte(MAX_SQRT)); + + SD59x18 sqrt_x = sqrt(x); + SD59x18 sqrt_y = sqrt(y); + + assertGte(sqrt_y, sqrt_x); + } + + // Square root of perfect square should be equal to x + // sqrt(x * x) == x + function sqrt_test_square(SD59x18 x) public { + require(x.gt(ONE_FP)); + + SD59x18 square_x = x.mul(x); + SD59x18 sqrt_square_x = sqrt(square_x); + + uint256 loss = significant_digits_lost_in_mult(x, x); + + assertEqWithinDecimalPrecision(sqrt_square_x, x, loss); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + function sqrt_test_zero() public { + assertEq(sqrt(ZERO_FP), ZERO_FP); + } + + // Test for maximum value + function sqrt_test_maximum() public { + try this.helpersSqrt(MAX_SQRT) { + // Expected behaviour, MAX_SQRT is positive, and operation + // should not revert as the result is in range + } catch { + // Unexpected, should not revert + assert(false); + } + } + + // Test for minimum value + function sqrt_test_minimum() public { + try this.helpersSqrt(MIN_SD59x18) { + // Unexpected, should revert. MIN_SD59x18 is negative. + assert(false); + } catch { + // Expected behaviour, revert + } + } + + // Test for negative operands + function sqrt_test_negative(SD59x18 x) public { + require(x.lt(ZERO_FP)); + + try this.helpersSqrt(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected behaviour, revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION log2() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // log2(x * y) = log2(x) + log2(y) + function log2_test_distributive_mul(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.gt(ZERO_FP)); + SD59x18 log2_x = log2(x); + SD59x18 log2_y = log2(y); + SD59x18 log2_x_log2_y = add(log2_x, log2_y); + + SD59x18 xy = mul(x, y); + SD59x18 log2_xy = log2(xy); + + // Ensure we have enough significant digits for the result to be meaningful + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + + // The maximum loss of precision is given by the formula: + // | log10(x) + log10(y) | + uint256 loss = significant_digits_lost_in_mult(x, y) + 2; + + assertEqWithinDecimalPrecision(log2_x_log2_y, log2_xy, loss); + } + + // Test for logarithm of a power + // log2(x ** y) = y * log2(x) + function log2_test_power(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP)); + SD59x18 x_y = pow(x, y); + require(x_y.gt(ZERO_FP)); + SD59x18 log2_x_y = log2(x_y); + SD59x18 y_log2_x = mul(log2(x), y); + + assertEqWithinTolerance(y_log2_x, log2_x_y, ONE_FP, "1%"); + } + + // Base 2 logarithm is strictly increasing + // y > 0 && x > y --> log2(x) > log2(y) + function log2_test_strictly_increasing(SD59x18 x, SD59x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + SD59x18 log2_x = log2(x); + SD59x18 log2_y = log2(y); + + assertGte(log2_x, log2_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function log2_test_zero() public { + try this.helpersLog2(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function log2_test_maximum() public { + SD59x18 result; + + try this.helpersLog2(MAX_SD59x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLog2(MAX_SD59x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for negative values, should revert as log2 is not defined + function log2_test_negative(SD59x18 x) public { + require(x.lt(ZERO_FP)); + + try this.helpersLog2(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION ln() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // ln(x * y) = ln(x) + ln(y) + function ln_test_distributive_mul(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.gt(ZERO_FP)); + + SD59x18 ln_x = ln(x); + SD59x18 ln_y = ln(y); + SD59x18 ln_x_ln_y = add(ln_x, ln_y); + + SD59x18 xy = mul(x, y); + SD59x18 ln_xy = ln(xy); + + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + + // The maximum loss of precision is given by the formula: + // | log2(x) + log2(y) | + uint256 loss = significant_digits_lost_in_mult(x, y) + 2; + + assertEqWithinDecimalPrecision(ln_x_ln_y, ln_xy, loss); + } + + // Test for logarithm of a power + // ln(x ** y) = y * ln(x) + function ln_test_power(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP)); + SD59x18 x_y = pow(x, y); + SD59x18 ln_x_y = ln(x_y); + + SD59x18 y_ln_x = mul(ln(x), y); + + require( + significant_digits_after_mult(ln(x), y) > + REQUIRED_SIGNIFICANT_DIGITS + ); + + assertEqWithinTolerance(ln_x_y, y_ln_x, ONE_FP, "1%"); + } + + // Natural logarithm is strictly increasing + // y > 0 && x > y --> log2(x) > log2(y) + function ln_test_strictly_increasing(SD59x18 x, SD59x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + SD59x18 log2_x = ln(x); + SD59x18 log2_y = ln(y); + + assertGte(log2_x, log2_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function ln_test_zero() public { + try this.helpersLn(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function ln_test_maximum() public { + SD59x18 result; + + try this.helpersLn(MAX_SD59x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLn(MAX_SD59x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for negative values, should revert as ln is not defined + function ln_test_negative(SD59x18 x) public { + require(x.lt(ZERO_FP)); + + try this.helpersLn(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION exp2() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for equality with pow(2, x) for integer x + // pow(2, x) == exp2(x) + function exp2_test_equivalence_pow(SD59x18 x) public { + require(x.lte(MAX_PERMITTED_EXP2)); + SD59x18 exp2_x = exp2(x); + SD59x18 pow_2_x = pow(TWO_FP, x); + + assertEq(exp2_x, pow_2_x); + } + + // Test for inverse function + // If y = log2(x) then exp2(y) == x + function exp2_test_inverse(SD59x18 x) public { + require(x.lte(MAX_PERMITTED_EXP2) && x.gt(ZERO_FP)); + SD59x18 log2_x = log2(x); + SD59x18 exp2_x = exp2(log2_x); + + assertEqWithinDecimalPrecision(x, exp2_x, 9); + } + + // Test for negative exponent + // exp2(-x) == inv( exp2(x) ) + function exp2_test_negative_exponent(SD59x18 x) public { + require(x.lt(ZERO_FP) && x.neq(MIN_SD59x18)); + + SD59x18 exp2_x = exp2(x); + SD59x18 exp2_minus_x = exp2(neg(x)); + + assertEqWithinDecimalPrecision(exp2_x, inv(exp2_minus_x), 2); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + // exp2(0) == 1 + function exp2_test_zero() public { + SD59x18 exp_zero = exp2(ZERO_FP); + assertEq(exp_zero, ONE_FP); + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp2_test_maximum() public { + try this.helpersExp2(MAX_SD59x18) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert + } + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp2_test_maximum_permitted() public { + try this.helpersExp2(MAX_PERMITTED_EXP2) { + // Should always pass + } catch { + // Should never revert + assert(false); + } + } + + // Test for minimum value. This should return zero since + // 2 ** -x == 1 / 2 ** x that tends to zero as x increases + function exp2_test_minimum() public { + SD59x18 result; + + try this.helpersExp2(MIN_SD59x18) { + // Expected, should not revert, check that value is zero + result = exp2(MIN_SD59x18); + assertEq(result, ZERO_FP); + } catch { + // Unexpected revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION exp() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for inverse function + // If y = ln(x) then exp(y) == x + function exp_test_inverse(SD59x18 x) public { + require(x.lte(MAX_PERMITTED_EXP)); + SD59x18 ln_x = ln(x); + SD59x18 exp_x = exp(ln_x); + SD59x18 log10_x = log10(x); + + assertEqWithinDecimalPrecision(x, exp_x, 9); + } + + // Test for negative exponent + // exp(-x) == inv( exp(x) ) + function exp_test_negative_exponent(SD59x18 x) public { + require(x.lt(ZERO_FP) && x.neq(MIN_SD59x18)); + + SD59x18 exp_x = exp(x); + SD59x18 exp_minus_x = exp(neg(x)); + + // Result should be within 4 bits precision for the worst case + assertEqWithinBitPrecision(exp_x, inv(exp_minus_x), 4); + } + + // Test that exp strictly increases + function exp_test_strictly_increasing(SD59x18 x, SD59x18 y) public { + require(x.lte(MAX_PERMITTED_EXP)); + require(y.gt(x) && y.lte(MAX_PERMITTED_EXP)); + + SD59x18 exp_x = exp(x); + SD59x18 exp_y = exp(y); + + assertGte(exp_y, exp_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + // exp(0) == 1 + function exp_test_zero() public { + SD59x18 exp_zero = exp(ZERO_FP); + assertEq(exp_zero, ONE_FP); + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp_test_maximum() public { + try this.helpersExp(MAX_SD59x18) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert + } + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp_test_maximum_permitted() public { + try this.helpersExp(MAX_PERMITTED_EXP) { + // Expected to always succeed + } catch { + // Unexpected, should revert + assert(false); + } + } + + // Test for minimum value. This should return zero since + // e ** -x == 1 / e ** x that tends to zero as x increases + function exp_test_minimum() public { + SD59x18 result; + + try this.helpersExp(MIN_SD59x18) { + // Expected, should not revert, check that value is zero + result = exp(MIN_SD59x18); + assertEq(result, ZERO_FP); + } catch { + // Unexpected revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION powu() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for zero exponent + // x ** 0 == 1 + function powu_test_zero_exponent(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18)); + SD59x18 x_pow_0 = powu(x, 0); + + assertEq(x_pow_0, ONE_FP); + } + + // Test for zero base + // 0 ** y == 0 (for positive y) + function powu_test_zero_base(uint256 y) public { + require(y != 0); + + SD59x18 zero_pow_y = powu(ZERO_FP, y); + + assertEq(zero_pow_y, ZERO_FP); + } + + // Test for exponent one + // x ** 1 == x + function powu_test_one_exponent(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18)); + SD59x18 x_pow_1 = powu(x, 1); + + assertEq(x_pow_1, x); + } + + // Test for base one + // 1 ** x == 1 + function powu_test_base_one(uint256 y) public { + SD59x18 one_pow_y = powu(ONE_FP, y); + + assertEq(one_pow_y, ONE_FP); + } + + // Test for product of powers of the same base + // x ** a * x ** b == x ** (a + b) + function powu_test_product_same_base( + SD59x18 x, + uint256 a, + uint256 b + ) public { + require(x.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18)); + + SD59x18 x_a = powu(x, a); + SD59x18 x_b = powu(x, b); + SD59x18 x_ab = powu(x, a + b); + + assertEqWithinDecimalPrecision(mul(x_a, x_b), x_ab, 10); + } + + // Test for power of an exponentiation + // (x ** a) ** b == x ** (a * b) + function powu_test_power_of_an_exponentiation( + SD59x18 x, + uint256 a, + uint256 b + ) public { + require(x.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18)); + + SD59x18 x_a = powu(x, a); + SD59x18 x_a_b = powu(x_a, b); + SD59x18 x_ab = powu(x, a * b); + + assertEqWithinDecimalPrecision(x_a_b, x_ab, 10); + } + + // Test for power of a product + // (x * y) ** a == x ** a * y ** a + function powu_test_product_power(SD59x18 x, SD59x18 y, uint256 a) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18) && y.lte(MAX_SD59x18)); + + require(a > 2 ** 32); // to avoid massive loss of precision + + SD59x18 x_y = mul(x, y); + SD59x18 xy_a = powu(x_y, a); + + SD59x18 x_a = powu(x, a); + SD59x18 y_a = powu(y, a); + + assertEqWithinDecimalPrecision(mul(x_a, y_a), xy_a, 10); + } + + // Test for result being greater than or lower than the argument, depending on + // its absolute value and the value of the exponent + function powu_test_values(SD59x18 x, uint256 a) public { + require(x.neq(ZERO_FP)); + require(x.neq(MIN_SD59x18)); + + SD59x18 x_a = powu(x, a); + + if (abs(x).gte(ONE_FP)) { + assertGte(abs(x_a), ONE_FP); + } + + if (abs(x).lte(ONE_FP)) { + assertLte(abs(x_a), ONE_FP); + } + } + + // Test for result sign: if the exponent is even, sign is positive + // if the exponent is odd, preserves the sign of the base + function powu_test_sign(SD59x18 x, uint256 a) public { + require(x.neq(ZERO_FP) && a != 0); + + SD59x18 x_a = powu(x, a); + + // This prevents the case where a small negative number gets + // rounded down to zero and thus changes sign + require(x_a.neq(ZERO_FP)); + + // If the exponent is even + if (a % 2 == 0) { + assertEq(x_a, abs(x_a)); + } else { + // x_a preserves x sign + if (x.lt(ZERO_FP)) { + assertLt(x_a, ZERO_FP); + } else { + assertGt(x_a, ZERO_FP); + } + } + } + + // Unsigned power is strictly increasing + // y > MIN && x > y && a > 0 && y != 0 --> powu(x, a) >= powu(y, a) + function powu_test_strictly_increasing( + SD59x18 x, + SD59x18 y, + uint256 a + ) public { + require(x.gt(y) && y.gt(MIN_SD59x18) && y.neq(ZERO_FP)); + require(x.lte(MAX_SD59x18)); + require(a > 0); + + SD59x18 x_a = powu(x, a); + SD59x18 y_a = powu(y, a); + + assertGte(x_a, y_a); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for maximum base and exponent > 1 + function powu_test_maximum_base(uint256 a) public { + require(a > 1); + + try this.helpersPowu(MAX_SD59x18, a) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for abs(base) < 1 and high exponent + function powu_test_high_exponent(SD59x18 x, uint256 a) public { + require(abs(x).lt(ONE_FP) && a > 2 ** 32); + + SD59x18 result = powu(x, a); + + assertEq(result, ZERO_FP); + } + + /* ================================================================ + + TESTS FOR FUNCTION log10() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // log10(x * y) = log10(x) + log10(y) + function log10_test_distributive_mul(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.gt(ZERO_FP)); + SD59x18 log10_x = log10(x); + SD59x18 log10_y = log10(y); + SD59x18 log10_x_log10_y = add(log10_x, log10_y); + + SD59x18 xy = mul(x, y); + SD59x18 log10_xy = log10(xy); + + // Ensure we have enough significant digits for the result to be meaningful + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + + assertEqWithinTolerance(log10_x_log10_y, log10_xy, ONE_FP, "1%"); + } + + // Test for logarithm of a power + // log10(x ** y) = y * log10(x) + function log10_test_power(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.gt(ZERO_FP)); + SD59x18 x_y = pow(x, y); + SD59x18 log10_x_y = log10(x_y); + SD59x18 y_log10_x = mul(log10(x), y); + + assertEqWithinTolerance(log10_x_y, y_log10_x, ONE_FP, "1%"); + } + + // Base 10 logarithm is strictly increasing + // x > y && y > 0 --> log10(x) > log10(y) + function log10_is_increasing(SD59x18 x, SD59x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + SD59x18 log2_x = log10(x); + SD59x18 log2_y = log10(y); + + assertGt(log2_x, log2_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function log10_test_zero() public { + try this.helpersLog10(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function log10_test_maximum() public { + SD59x18 result; + + try this.helpersLog10(MAX_SD59x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLog10(MAX_SD59x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for negative values, should revert as log10 is not defined + function log10_test_negative(SD59x18 x) public { + require(x.lt(ZERO_FP)); + + try this.helpersLog10(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION gm() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // The product of the values should be equal to the geometric mean + // raised to the power of N (numbers in the set) + function gm_test_product(SD59x18 x, SD59x18 y) public { + bool x_sign = x.gt(ZERO_FP); + bool y_sign = y.gt(ZERO_FP); + require(x_sign = y_sign); + + SD59x18 x_mul_y = x.mul(y); + SD59x18 gm_squared = pow(gm(x, y), TWO_FP); + + assertEqWithinTolerance(x_mul_y, gm_squared, ONE_TENTH_FP, "0.1%"); + } + + // The geometric mean for a set of positive numbers is less than the + // arithmetic mean of that set, as long as the values of the set are not equal + function gm_test_positive_set_avg(SD59x18 x, SD59x18 y) public { + require(x.gte(ZERO_FP) && y.gte(ZERO_FP) && x.neq(y)); + + SD59x18 gm_x_y = gm(x, y); + SD59x18 avg_x_y = avg(x, y); + + assertLte(gm_x_y, avg_x_y); + } + + // The geometric mean of a set of positive equal numbers should be + // equal to the arithmetic mean + function gm_test_positive_equal_set_avg(SD59x18 x) public { + require(x.gte(ZERO_FP)); + + SD59x18 gm_x = gm(x, x); + SD59x18 avg_x = avg(x, x); + + assertEq(gm_x, avg_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should return 0 + function gm_test_zero(SD59x18 x) public { + require(x.gte(ZERO_FP)); + + try this.helpersGm(x, ZERO_FP) { + SD59x18 result = gm(x, ZERO_FP); + assertEq(result, ZERO_FP); + } catch { + // Unexpected, should not revert + assert(false); + } + } + + // Test for single negative input + function gm_test_negative(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.lt(ZERO_FP)); + + try this.helpersGm(x, y) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, gm of a negative product is not defined + } + } +} diff --git a/contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol b/contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol new file mode 100644 index 0000000..47d7377 --- /dev/null +++ b/contracts/Math/PRBMath/v3/PRBMathUD60x18PropertyTests.sol @@ -0,0 +1,1629 @@ +pragma solidity ^0.8.19; + +import {UD60x18} from "@prb-math-v3/UD60x18.sol"; +import {add, sub, eq, gt, gte, lt, lte, lshift, rshift} from "@prb-math-v3/ud60x18/Helpers.sol"; +import {convert} from "@prb-math-v3/ud60x18/Conversions.sol"; +import {msb} from "@prb-math-v3/Common.sol"; +import {intoUint128, intoUint256, intoSD59x18} from "@prb-math-v3/ud60x18/Casting.sol"; +import {mul, div, ln, exp, exp2, log2, sqrt, pow, avg, inv, log10, floor, powu, gm} from "@prb-math-v3/ud60x18/Math.sol"; +import "./utils/AssertionHelperUD.sol"; + +contract CryticPRBMath60x18Propertiesv3 is AssertionHelperUD { + /* ================================================================ + 59x18 fixed-point constants used for testing specific values. + This assumes that PRBMath library's convert(x) works as expected. + ================================================================ */ + UD60x18 internal ZERO_FP = convert(0); + UD60x18 internal ONE_FP = convert(1); + UD60x18 internal TWO_FP = convert(2); + UD60x18 internal THREE_FP = convert(3); + UD60x18 internal EIGHT_FP = convert(8); + UD60x18 internal THOUSAND_FP = convert(1000); + UD60x18 internal EPSILON = UD60x18.wrap(1); + UD60x18 internal ONE_TENTH_FP = convert(1).div(convert(10)); + + /* ================================================================ + Constants used for precision loss calculations + ================================================================ */ + uint256 internal REQUIRED_SIGNIFICANT_DIGITS = 10; + + /* ================================================================ + Integer representations maximum values. + These constants are used for testing edge cases or limits for + possible values. + ================================================================ */ + /// @dev Euler's number as an UD60x18 number. + UD60x18 constant E = UD60x18.wrap(2_718281828459045235); + + /// @dev Half the UNIT number. + uint256 constant uHALF_UNIT = 0.5e18; + UD60x18 constant HALF_UNIT = UD60x18.wrap(uHALF_UNIT); + + /// @dev log2(10) as an UD60x18 number. + uint256 constant uLOG2_10 = 3_321928094887362347; + UD60x18 constant LOG2_10 = UD60x18.wrap(uLOG2_10); + + /// @dev log2(e) as an UD60x18 number. + uint256 constant uLOG2_E = 1_442695040888963407; + UD60x18 constant LOG2_E = UD60x18.wrap(uLOG2_E); + + /// @dev The maximum value an UD60x18 number can have. + uint256 constant uMAX_UD60x18 = + 115792089237316195423570985008687907853269984665640564039457_584007913129639935; + UD60x18 constant MAX_UD60x18 = UD60x18.wrap(uMAX_UD60x18); + + /// @dev The maximum whole value an UD60x18 number can have. + uint256 constant uMAX_WHOLE_UD60x18 = + 115792089237316195423570985008687907853269984665640564039457_000000000000000000; + UD60x18 constant MAX_WHOLE_UD60x18 = UD60x18.wrap(uMAX_WHOLE_UD60x18); + + /// @dev PI as an UD60x18 number. + UD60x18 constant PI = UD60x18.wrap(3_141592653589793238); + + /// @dev The unit amount that implies how many trailing decimals can be represented. + uint256 constant uUNIT = 1e18; + UD60x18 constant UNIT = UD60x18.wrap(uUNIT); + + /// @dev Zero as an UD60x18 number. + UD60x18 constant ZERO = UD60x18.wrap(0); + + UD60x18 internal constant MAX_PERMITTED_EXP2 = UD60x18.wrap(192e18 - 1); + UD60x18 internal constant MAX_PERMITTED_EXP = + UD60x18.wrap(133_084258667509499440); + UD60x18 internal constant MAX_PERMITTED_POW = + UD60x18.wrap(2 ** 192 * 10 ** 18 - 1); + UD60x18 internal constant MAX_PERMITTED_SQRT = + UD60x18.wrap( + 115792089237316195423570985008687907853269_984665640564039457 + ); + + /* ================================================================ + Events used for debugging or showing information. + ================================================================ */ + event Value(string reason, UD60x18 val); + event LogErr(bytes error); + + /* ================================================================ + Library wrappers. + These functions allow calling the PRBMathUD60x18 library. + ================================================================ */ + function debug(string calldata x, UD60x18 y) public { + emit Value(x, y); + } + + // Wrapper for external try/catch calls + function helpersAdd(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return add(x, y); + } + + // Wrapper for external try/catch calls + function helpersSub(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return sub(x, y); + } + + // Wrapper for external try/catch calls + function helpersMul(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return mul(x, y); + } + + function helpersDiv(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return div(x, y); + } + + function helpersLn(UD60x18 x) public pure returns (UD60x18) { + return ln(x); + } + + function helpersExp(UD60x18 x) public pure returns (UD60x18) { + return exp(x); + } + + function helpersExp2(UD60x18 x) public pure returns (UD60x18) { + return exp2(x); + } + + function helpersLog2(UD60x18 x) public pure returns (UD60x18) { + return log2(x); + } + + function helpersSqrt(UD60x18 x) public pure returns (UD60x18) { + return sqrt(x); + } + + function helpersPow(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return pow(x, y); + } + + function helpersPowu(UD60x18 x, uint256 y) public pure returns (UD60x18) { + return powu(x, y); + } + + function helpersAvg(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return avg(x, y); + } + + function helpersInv(UD60x18 x) public pure returns (UD60x18) { + return inv(x); + } + + function helpersLog10(UD60x18 x) public pure returns (UD60x18) { + return log10(x); + } + + function helpersFloor(UD60x18 x) public pure returns (UD60x18) { + return floor(x); + } + + function helpersGm(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return gm(x, y); + } + + /* ================================================================ + + TESTS FOR FUNCTION add() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for commutative property + // x + y == y + x + function add_test_commutative(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.add(y); + UD60x18 y_x = y.add(x); + + assertEq(x_y, y_x); + } + + // Test for associative property + // (x + y) + z == x + (y + z) + function add_test_associative(UD60x18 x, UD60x18 y, UD60x18 z) public { + UD60x18 x_y = x.add(y); + UD60x18 y_z = y.add(z); + UD60x18 xy_z = x_y.add(z); + UD60x18 x_yz = x.add(y_z); + + assertEq(xy_z, x_yz); + } + + // Test for identity operation + // x + 0 == x (equivalent to x + (-x) == 0) + function add_test_identity(UD60x18 x) public { + UD60x18 x_0 = x.add(ZERO_FP); + + assertEq(x, x_0); + assertEq(x.sub(x), ZERO_FP); + } + + // Test that the result increases or decreases depending + // on the value to be added + function add_test_values(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.add(y); + + assertGte(x_y, x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These should make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the addition must be between the maximum + // and minimum allowed values for UD60x18 + function add_test_range(UD60x18 x, UD60x18 y) public { + try this.helpersAdd(x, y) returns (UD60x18 result) { + assert(result.lte(MAX_UD60x18) && result.gte(ZERO_FP)); + } catch { + // If it reverts, just ignore + } + } + + // Adding zero to the maximum value shouldn't revert, as it is valid + // Moreover, the result must be MAX_UD60x18 + function add_test_maximum_value() public { + try this.helpersAdd(MAX_UD60x18, ZERO_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_UD60x18); + } catch { + assert(false); + } + } + + // Adding one to the maximum value should revert, as it is out of range + function add_test_maximum_value_plus_one() public { + try this.helpersAdd(MAX_UD60x18, ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + // Adding zero to the minimum value shouldn't revert, as it is valid + // Moreover, the result must be MIN_UD60x18 + function add_test_minimum_value() public { + try this.helpersAdd(ZERO_FP, ZERO_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, ZERO_FP); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION sub() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for identity operation + // x - 0 == x (equivalent to x - x == 0) + function sub_test_identity(UD60x18 x) public { + UD60x18 x_0 = x.sub(ZERO_FP); + + assertEq(x_0, x); + assertEq(x.sub(x), ZERO_FP); + } + + // Test for neutrality over addition and subtraction + // (x - y) + y == (x + y) - y == x + function sub_test_neutrality(UD60x18 x, UD60x18 y) public { + UD60x18 x_minus_y = x.sub(y); + UD60x18 x_plus_y = x.add(y); + + UD60x18 x_minus_y_plus_y = x_minus_y.add(y); + UD60x18 x_plus_y_minus_y = x_plus_y.sub(y); + + assertEq(x_minus_y_plus_y, x_plus_y_minus_y); + assertEq(x_minus_y_plus_y, x); + } + + // Test that the result always decreases + function sub_test_values(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.sub(y); + + assertLte(x_y, x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the subtraction must be between the maximum + // and minimum allowed values for UD60x18 + function sub_test_range(UD60x18 x, UD60x18 y) public { + try this.helpersSub(x, y) returns (UD60x18 result) { + assert(result.lte(MAX_UD60x18) && result.gte(ZERO_FP)); + } catch { + // If it reverts, just ignore + } + } + + // Subtracting zero from the maximum value shouldn't revert, as it is valid + // Moreover, the result must be MAX_UD60x18 + function sub_test_maximum_value() public { + try this.helpersSub(MAX_UD60x18, ZERO_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_UD60x18); + } catch { + assert(false); + } + } + + // Subtracting zero from the minimum value shouldn't revert, as it is valid + // Moreover, the result must be MIN_UD60x18 + function sub_test_minimum_value() public { + try this.helpersSub(ZERO_FP, ZERO_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, ZERO_FP); + } catch { + assert(false); + } + } + + // Subtracting one from the minimum value should revert, as it is out of range + function sub_test_minimum_value_minus_one() public { + try this.helpersSub(ZERO_FP, ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + /* ================================================================ + + TESTS FOR FUNCTION mul() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for commutative property + // x * y == y * x + function mul_test_commutative(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.mul(y); + UD60x18 y_x = y.mul(x); + + assertEq(x_y, y_x); + } + + // Test for associative property + // (x * y) * z == x * (y * z) + function mul_test_associative(UD60x18 x, UD60x18 y, UD60x18 z) public { + UD60x18 x_y = x.mul(y); + UD60x18 y_z = y.mul(z); + UD60x18 xy_z = x_y.mul(z); + UD60x18 x_yz = x.mul(y_z); + + require(xy_z.neq(ZERO_FP) && x_yz.neq(ZERO_FP)); + assertEqWithinTolerance(xy_z, x_yz, ONE_TENTH_FP, "0.1%"); + } + + // Test for distributive property + // x * (y + z) == x * y + x * z + function mul_test_distributive(UD60x18 x, UD60x18 y, UD60x18 z) public { + UD60x18 y_plus_z = y.add(z); + UD60x18 x_times_y_plus_z = x.mul(y_plus_z); + + UD60x18 x_times_y = x.mul(y); + UD60x18 x_times_z = x.mul(z); + + assertEqWithinTolerance( + add(x_times_y, x_times_z), + x_times_y_plus_z, + ONE_TENTH_FP, + "0.1%" + ); + } + + // Test for identity operation + // x * 1 == x (also check that x * 0 == 0) + function mul_test_identity(UD60x18 x) public { + UD60x18 x_1 = x.mul(ONE_FP); + UD60x18 x_0 = x.mul(ZERO_FP); + + assertEq(x_0, ZERO_FP); + assertEq(x_1, x); + } + + // Test that the result increases or decreases depending + // on the value to be added + function mul_test_values(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.mul(y); + + if (y.gte(ONE_FP)) { + assertGte(x_y, x); + } else { + assertLte(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the multiplication must be between the maximum + // and minimum allowed values for UD60x18 + function mul_test_range(UD60x18 x, UD60x18 y) public { + try this.helpersMul(x, y) returns (UD60x18 result) { + assertLte(result, MAX_UD60x18); + } catch { + // If it reverts, just ignore + } + } + + // Multiplying the maximum value times one shouldn't revert, as it is valid + // Moreover, the result must be MAX_UD60x18 + function mul_test_maximum_value() public { + try this.helpersMul(MAX_UD60x18, ONE_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_UD60x18); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION div() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for identity property + // x / 1 == x (equivalent to x / x == 1) + function div_test_division_identity_x_div_1(UD60x18 x) public { + UD60x18 div_1 = div(x, ONE_FP); + + assertEq(x, div_1); + } + + // Test for identity property + // x/x should not revert unless x == 0 + function div_test_division_identity_x_div_x(UD60x18 x) public { + UD60x18 div_x; + + try this.helpersDiv(x, x) { + // This should always equal one + div_x = div(x, x); + assertEq(div_x, ONE_FP); + } catch { + // Only valid case for revert is x == 0 + assertEq(x, ZERO_FP); + } + } + + // Test for division with 0 as numerator + // 0 / x = 0 + function div_test_division_num_zero(UD60x18 y) public { + require(y.neq(ZERO_FP)); + + UD60x18 div_0 = div(ZERO_FP, y); + + assertEq(ZERO_FP, div_0); + } + + // Test that the value of the result increases or + // decreases depending on the denominator's value + function div_test_values(UD60x18 x, UD60x18 y) public { + require(y.neq(ZERO_FP)); + + UD60x18 x_y = div(x, y); + + if (y.gte(ONE_FP)) { + assertLte(x_y, x); + } else { + assertGte(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for division by zero + function div_test_div_by_zero(UD60x18 x) public { + try this.helpersDiv(x, ZERO_FP) { + // Unexpected, this should revert + assert(false); + } catch { + // Expected revert + } + } + + // Test for division by a large value, the result should be less than one + function div_test_maximum_denominator(UD60x18 x) public { + UD60x18 div_large = div(x, MAX_UD60x18); + + assertLte(div_large, ONE_FP); + } + + // Test for division of a large value + // This should revert if |y| < 1 as it would return a value higher than max + function div_test_maximum_numerator(UD60x18 y) public { + require(y.neq(ZERO_FP)); + UD60x18 div_large; + + try this.helpersDiv(MAX_UD60x18, y) { + // If it didn't revert, then |y| >= 1 + div_large = div(MAX_UD60x18, y); + + assertGte(y, ONE_FP); + } catch { + // Expected revert as result is higher than max + } + } + + // Test for values in range + function div_test_range(UD60x18 x, UD60x18 y) public { + require(y.neq(ZERO_FP)); + UD60x18 result; + + try this.helpersDiv(x, y) { + // If it returns a value, it must be in range + result = div(x, y); + assertLte(result, MAX_UD60x18); + } catch { + // Otherwise, it should revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION inv() + + ================================================================ */ + + /* ================================================================ + Tests for mathematical properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the inverse of the inverse is close enough to the + // original number + function inv_test_double_inverse(UD60x18 x) public { + require(x.neq(ZERO_FP)); + + UD60x18 double_inv_x = inv(inv(x)); + + assertEqWithinTolerance(x, double_inv_x, ONE_FP, "1%"); + } + + // Test equivalence with division + function inv_test_division(UD60x18 x) public { + require(x.neq(ZERO_FP)); + + UD60x18 inv_x = inv(x); + UD60x18 div_1_x = div(ONE_FP, x); + + assertEq(inv_x, div_1_x); + } + + // Test the anticommutativity of the division + // x / y == 1 / (y / x) + function inv_test_division_noncommutativity(UD60x18 x, UD60x18 y) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + UD60x18 x_y = div(x, y); + UD60x18 y_x = div(y, x); + + assertEqWithinTolerance(x_y, inv(y_x), ONE_FP, "1%"); + } + + // Test the multiplication of inverses + // 1/(x * y) == 1/x * 1/y + function inv_test_multiplication(UD60x18 x, UD60x18 y) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + UD60x18 inv_x = inv(x); + UD60x18 inv_y = inv(y); + UD60x18 inv_x_times_inv_y = mul(inv_x, inv_y); + + UD60x18 x_y = mul(x, y); + UD60x18 inv_x_y = inv(x_y); + + assertEqWithinTolerance(inv_x_y, inv_x_times_inv_y, ONE_FP, "1%"); + } + + // Test multiplicative identity property + function inv_test_identity(UD60x18 x) public { + require(x.neq(ZERO_FP)); + + UD60x18 inv_x = inv(x); + UD60x18 identity = mul(inv_x, x); + + require(inv_x.neq(ZERO_FP) && identity.neq(ZERO_FP)); + + // They should agree with a tolerance of one percent + assertEqWithinTolerance(identity, ONE_FP, ONE_FP, "1%"); + } + + // Test that the value of the result is in range zero-one + // if x is greater than one, else, the value of the result + // must be greater than one + function inv_test_values(UD60x18 x) public { + require(x.neq(ZERO_FP)); + + UD60x18 inv_x = inv(x); + + if (x.gte(ONE_FP)) { + assertLte(inv_x, ONE_FP); + } else { + assertGt(inv_x, ONE_FP); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test the zero-case, should revert + function inv_test_zero() public { + try this.helpersInv(ZERO_FP) { + // Unexpected, the function must revert + assert(false); + } catch {} + } + + // Test the maximum value case, should not revert, and be close to zero + function inv_test_maximum() public { + UD60x18 inv_maximum; + + try this.helpersInv(MAX_UD60x18) { + inv_maximum = this.helpersInv(MAX_UD60x18); + assertEqWithinBitPrecision(inv_maximum, ZERO_FP, 10); + } catch { + // Unexpected, the function must not revert + assert(false); + } + } + + // Test the minimum value case, should not revert, and be close to 1e36 + function inv_test_minimum() public { + UD60x18 inv_minimum; + + try this.helpersInv(UD60x18.wrap(1)) { + inv_minimum = this.helpersInv(UD60x18.wrap(1)); + assertEqWithinBitPrecision(inv_minimum, UD60x18.wrap(1e36), 10); + } catch { + // Unexpected, the function must not revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION avg() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the result is between the two operands + // avg(x, y) >= min(x, y) && avg(x, y) <= max(x, y) + function avg_test_values_in_range(UD60x18 x, UD60x18 y) public { + UD60x18 avg_xy = avg(x, y); + + if (x.gte(y)) { + assertGte(avg_xy, y); + assertLte(avg_xy, x); + } else { + assertGte(avg_xy, x); + assertLte(avg_xy, y); + } + } + + // Test that the average of the same number is itself + // avg(x, x) == x + function avg_test_one_value(UD60x18 x) public { + UD60x18 avg_x = avg(x, x); + + assertEq(avg_x, x); + } + + // Test that the order of operands is irrelevant + // avg(x, y) == avg(y, x) + function avg_test_operand_order(UD60x18 x, UD60x18 y) public { + UD60x18 avg_xy = avg(x, y); + UD60x18 avg_yx = avg(y, x); + + assertEq(avg_xy, avg_yx); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for the maximum value + function avg_test_maximum() public { + UD60x18 result; + + // This may revert due to overflow depending on implementation + // If it doesn't revert, the result must be MAX_UD60x18 + try this.helpersAvg(MAX_UD60x18, MAX_UD60x18) { + result = this.helpersAvg(MAX_UD60x18, MAX_UD60x18); + assertEq(result, MAX_UD60x18); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION pow() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for zero exponent + // x ** 0 == 1 + function pow_test_zero_exponent(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_POW)); + UD60x18 x_pow_0 = pow(x, ZERO_FP); + + assertEq(x_pow_0, ONE_FP); + } + + // Test for zero base + // 0 ** y == 0 (for positive y) + function pow_test_zero_base(UD60x18 y) public { + require(y.lte(MAX_PERMITTED_POW)); + require(y.neq(ZERO_FP)); + + UD60x18 zero_pow_y = pow(ZERO_FP, y); + + assertEq(zero_pow_y, ZERO_FP); + } + + // Test for exponent one + // x ** 1 == x + function pow_test_one_exponent(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_POW)); + UD60x18 x_pow_1 = pow(x, ONE_FP); + + assertEq(x_pow_1, x); + } + + // Test for base one + // 1 ** y == 1 + function pow_test_base_one(UD60x18 y) public { + UD60x18 one_pow_y = pow(ONE_FP, y); + + assertEq(one_pow_y, ONE_FP); + } + + // Test for product of powers of the same base + // x ** a * x ** b == x ** (a + b) + function pow_test_product_same_base( + UD60x18 x, + UD60x18 a, + UD60x18 b + ) public { + require(x.neq(ZERO_FP)); + require(x.lte(MAX_PERMITTED_POW)); + + UD60x18 x_a = pow(x, a); + UD60x18 x_b = pow(x, b); + UD60x18 x_ab = pow(x, a.add(b)); + + assertEqWithinDecimalPrecision(mul(x_a, x_b), x_ab, 9); + } + + // Test for power of an exponentiation + // (x ** a) ** b == x ** (a * b) + function pow_test_power_of_an_exponentiation( + UD60x18 x, + UD60x18 a, + UD60x18 b + ) public { + require(x.neq(ZERO_FP)); + require(x.lte(MAX_PERMITTED_POW)); + + UD60x18 x_a = pow(x, a); + UD60x18 x_a_b = pow(x_a, b); + UD60x18 x_ab = pow(x, a.mul(b)); + + assertEqWithinDecimalPrecision(x_a_b, x_ab, 9); + } + + // Test for power of a product + // (x * y) ** a == x ** a * y ** a + function pow_test_product_power(UD60x18 x, UD60x18 y, UD60x18 a) public { + require(x.lte(MAX_PERMITTED_POW)); + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + require(a.gt(UD60x18.wrap(1e9))); // to avoid massive loss of precision + + UD60x18 x_y = mul(x, y); + UD60x18 xy_a = pow(x_y, a); + + UD60x18 x_a = pow(x, a); + UD60x18 y_a = pow(y, a); + + assertEqWithinDecimalPrecision(mul(x_a, y_a), xy_a, 9); + } + + // Test for result being greater than or lower than the argument, depending on + // its value and the value of the exponent + function pow_test_values(UD60x18 x, UD60x18 a) public { + require(x.neq(ZERO_FP)); + require(x.lte(MAX_PERMITTED_POW)); + + UD60x18 x_a = pow(x, a); + + if (x.gte(ONE_FP)) { + assertGte(x_a, ONE_FP); + } + + if (x.lte(ONE_FP)) { + assertLte(x_a, ONE_FP); + } + } + + // Power is strictly increasing + // x > y && a >= 0 --> pow(x) >= pow(y) + function pow_test_strictly_increasing( + UD60x18 x, + UD60x18 y, + UD60x18 a + ) public { + require(x.gt(y) && x.lte(MAX_PERMITTED_POW)); + require(a.gte(ZERO_FP)); + + UD60x18 x_a = pow(x, a); + UD60x18 y_a = pow(y, a); + + assertGte(x_a, y_a); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for maximum base and exponent > 1 + function pow_test_maximum_base(UD60x18 a) public { + require(a.gt(ONE_FP)); + + try this.helpersPow(MAX_UD60x18, a) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for maximum exponent and base > 1 + function pow_test_maximum_exponent(UD60x18 x) public { + require(x.gt(ONE_FP)); + require(x.lte(MAX_PERMITTED_POW)); + + try this.helpersPow(x, MAX_PERMITTED_POW) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for base < 1 and high exponent + function pow_test_high_exponent(UD60x18 x, UD60x18 a) public { + require(x.lt(ONE_FP) && a.gt(convert(2 ** 64))); + + UD60x18 result = pow(x, a); + + assertEq(result, ZERO_FP); + } + + /* ================================================================ + + TESTS FOR FUNCTION sqrt() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for the inverse operation + // sqrt(x) * sqrt(x) == x + function sqrt_test_inverse_mul(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_SQRT)); + UD60x18 sqrt_x = sqrt(x); + UD60x18 sqrt_x_squared = mul(sqrt_x, sqrt_x); + + // Precision loss is at most half the bits of the operand + assertEqWithinBitPrecision( + sqrt_x_squared, + x, + (intoUint256(log2(x)) >> 1) + 2 + ); + } + + // Test for the inverse operation + // sqrt(x) ** 2 == x + function sqrt_test_inverse_pow(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_SQRT)); + UD60x18 sqrt_x = sqrt(x); + UD60x18 sqrt_x_squared = pow(sqrt_x, convert(2)); + + // Precision loss is at most half the bits of the operand + assertEqWithinBitPrecision( + sqrt_x_squared, + x, + (intoUint256(log2(x)) >> 1) + 2 + ); + } + + // Test for distributive property respect to the multiplication + // sqrt(x) * sqrt(y) == sqrt(x * y) + function sqrt_test_distributive(UD60x18 x, UD60x18 y) public { + require(x.lte(MAX_PERMITTED_SQRT) && y.lte(MAX_PERMITTED_SQRT)); + UD60x18 sqrt_x = sqrt(x); + UD60x18 sqrt_y = sqrt(y); + UD60x18 sqrt_x_sqrt_y = mul(sqrt_x, sqrt_y); + UD60x18 sqrt_xy = sqrt(mul(x, y)); + + // Allow an error of up to one tenth of a percent + assertEqWithinTolerance(sqrt_x_sqrt_y, sqrt_xy, ONE_TENTH_FP, "0.1%"); + } + + // Test that sqrt is strictly increasing + // x >= 0 && y > x --> sqrt(y) >= sqrt(x) + function sqrt_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(x)); + + UD60x18 sqrt_x = sqrt(x); + UD60x18 sqrt_y = sqrt(y); + + assertGte(sqrt_y, sqrt_x); + } + + // Square root of perfect square should be equal to x + // sqrt(x * x) == x + function sqrt_test_square(UD60x18 x) public { + require(x.gt(ONE_FP)); + + UD60x18 square_x = x.mul(x); + UD60x18 sqrt_square_x = sqrt(square_x); + + assertEqWithinDecimalPrecision(sqrt_square_x, x, 2); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + function sqrt_test_zero() public { + assertEq(sqrt(ZERO_FP), ZERO_FP); + } + + // Test for maximum value + function sqrt_test_maximum() public { + try this.helpersSqrt(MAX_PERMITTED_SQRT) { + // Expected behaviour, MAX_SQRT is positive, and operation + // should not revert as the result is in range + } catch { + // Unexpected, should not revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION log2() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // log2(x * y) = log2(x) + log2(y) + function log2_test_distributive_mul(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + + UD60x18 log2_x = log2(x); + UD60x18 log2_y = log2(y); + UD60x18 log2_x_log2_y = add(log2_x, log2_y); + + UD60x18 xy = mul(x, y); + UD60x18 log2_xy = log2(xy); + + assertEqWithinTolerance(log2_x_log2_y, log2_xy, ONE_FP, "1%"); + } + + // Test for logarithm of a power + // log2(x ** y) = y * log2(x) + function log2_test_power(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + + UD60x18 x_y = pow(x, y); + UD60x18 log2_x_y = log2(x_y); + UD60x18 y_log2_x = mul(log2(x), y); + + assertEqWithinTolerance(y_log2_x, log2_x_y, ONE_FP, "1%"); + } + + // Base 2 logarithm is strictly increasing + // y > 0 && x > y --> log2(x) > log2(y) + function log2_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + UD60x18 log2_x = log2(x); + UD60x18 log2_y = log2(y); + + assertGte(log2_x, log2_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function log2_test_zero() public { + try this.helpersLog2(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function log2_test_maximum() public { + UD60x18 result; + + try this.helpersLog2(MAX_UD60x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLog2(MAX_UD60x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for values less than UNIT, should revert as result would be negative + function log2_test_less_than_unit(UD60x18 x) public { + require(x.lt(UNIT)); + + try this.helpersLog2(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION ln() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // ln(x * y) = ln(x) + ln(y) + function ln_test_distributive_mul(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + + UD60x18 ln_x = ln(x); + UD60x18 ln_y = ln(y); + UD60x18 ln_x_ln_y = add(ln_x, ln_y); + + UD60x18 xy = mul(x, y); + UD60x18 ln_xy = ln(xy); + + assertEqWithinTolerance(ln_xy, ln_x_ln_y, ONE_FP, "1%"); + } + + // Test for logarithm of a power + // ln(x ** y) = y * ln(x) + function ln_test_power(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT)); + UD60x18 x_y = pow(x, y); + UD60x18 ln_x_y = ln(x_y); + UD60x18 y_ln_x = mul(ln(x), y); + + assertEqWithinTolerance(ln_x_y, y_ln_x, ONE_FP, "1%"); + } + + // Natural logarithm is strictly increasing + // y > 0 && x > y --> log2(x) > log2(y) + function ln_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + UD60x18 ln_x = ln(x); + UD60x18 ln_y = ln(y); + + assertGte(ln_x, ln_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function ln_test_zero() public { + try this.helpersLn(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function ln_test_maximum() public { + UD60x18 result; + + try this.helpersLn(MAX_UD60x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLn(MAX_UD60x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for values less than UNIT, should revert since result would be negative + function ln_test_less_than_unit(UD60x18 x) public { + require(x.lt(UNIT)); + + try this.helpersLn(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION exp2() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for equality with pow(2, x) for integer x + // pow(2, x) == exp2(x) + function exp2_test_equivalence_pow(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_EXP2)); + UD60x18 exp2_x = exp2(x); + UD60x18 pow_2_x = pow(TWO_FP, x); + + assertEq(exp2_x, pow_2_x); + } + + // Test for inverse function + // If y = log2(x) then exp2(y) == x + function exp2_test_inverse(UD60x18 x) public { + require(x.gte(UNIT)); + UD60x18 log2_x = log2(x); + require(log2_x.lte(MAX_PERMITTED_EXP2)); + UD60x18 exp2_x = exp2(log2_x); + + assertEqWithinTolerance(x, exp2_x, ONE_TENTH_FP, "0.1%"); + } + + // Test that exp2 strictly increases + // y > x && y < MAX --> exp2(y) > exp2(x) + function exp2_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(x) && y.lte(MAX_PERMITTED_EXP2)); + + UD60x18 exp_x = exp(x); + UD60x18 exp_y = exp(y); + + assertGte(exp_y, exp_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + // exp2(0) == 1 + function exp2_test_zero() public { + UD60x18 exp_zero = exp2(ZERO_FP); + assertEq(exp_zero, ONE_FP); + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp2_test_maximum() public { + try this.helpersExp2(convert(192)) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION exp() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for inverse function + // If y = ln(x) then exp(y) == x + function exp_test_inverse(UD60x18 x) public { + require(x.gte(UNIT)); + UD60x18 ln_x = ln(x); + UD60x18 exp_x = exp(ln_x); + require(exp_x.lte(MAX_PERMITTED_EXP)); + UD60x18 log2_x = log2(x); + + assertEqWithinDecimalPrecision(x, exp_x, 9); + } + + // Test that exp strictly increases + // y <= MAX && y.gt(x) --> exp(y) >= exp(x) + function exp_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(x) && y.lte(MAX_PERMITTED_EXP)); + + UD60x18 exp_x = exp(x); + UD60x18 exp_y = exp(y); + + assertGte(exp_y, exp_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + // exp(0) == 1 + function exp_test_zero() public { + UD60x18 exp_zero = exp(ZERO_FP); + assertEq(exp_zero, ONE_FP); + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp_test_maximum() public { + try this.helpersExp(convert(192)) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION powu() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for zero exponent + // x ** 0 == 1 + function powu_test_zero_exponent(UD60x18 x) public { + UD60x18 x_pow_0 = powu(x, 0); + + assertEq(x_pow_0, ONE_FP); + } + + // Test for zero base + // 0 ** x == 0 + function powu_test_zero_base(uint256 a) public { + require(a != 0); + + UD60x18 zero_pow_a = powu(ZERO_FP, a); + + assertEq(zero_pow_a, ZERO_FP); + } + + // Test for exponent one + // x ** 1 == x + function powu_test_one_exponent(UD60x18 x) public { + UD60x18 x_pow_1 = powu(x, 1); + + assertEq(x_pow_1, x); + } + + // Test for base one + // 1 ** x == 1 + function powu_test_base_one(uint256 a) public { + UD60x18 one_pow_a = powu(ONE_FP, a); + + assertEq(one_pow_a, ONE_FP); + } + + // Test for product of powers of the same base + // x ** a * x ** b == x ** (a + b) + function powu_test_product_same_base( + UD60x18 x, + uint256 a, + uint256 b + ) public { + require(x.neq(ZERO_FP)); + + UD60x18 x_a = powu(x, a); + UD60x18 x_b = powu(x, b); + UD60x18 x_ab = powu(x, a + b); + + assertEqWithinBitPrecision(mul(x_a, x_b), x_ab, 10); + } + + // Test for power of an exponentiation + // (x ** a) ** b == x ** (a * b) + function powu_test_power_of_an_exponentiation( + UD60x18 x, + uint256 a, + uint256 b + ) public { + require(x.neq(ZERO_FP)); + + UD60x18 x_a = powu(x, a); + UD60x18 x_a_b = powu(x_a, b); + UD60x18 x_ab = powu(x, a * b); + + assertEqWithinBitPrecision(x_a_b, x_ab, 10); + } + + // Test for power of a product + // (x * y) ** a == x ** a * y ** a + function powu_test_product_power(UD60x18 x, UD60x18 y, uint256 a) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + require(a > 1e9); // to avoid massive loss of precision + + UD60x18 x_y = mul(x, y); + UD60x18 xy_a = powu(x_y, a); + + UD60x18 x_a = powu(x, a); + UD60x18 y_a = powu(y, a); + + assertEqWithinBitPrecision(mul(x_a, y_a), xy_a, 10); + } + + // Test for result being greater than or lower than the argument, depending on + // its value and the value of the exponent + function powu_test_values(UD60x18 x, uint256 a) public { + require(x.neq(ZERO_FP)); + + UD60x18 x_a = powu(x, a); + + if (x.gte(ONE_FP)) { + assertGte(x_a, ONE_FP); + } + + if (x.lte(ONE_FP)) { + assertLte(x_a, ONE_FP); + } + } + + // Unsigned power is strictly increasing + // y > MIN && x > y && a > 0 --> powu(x, a) > powu(y, a) + function powu_test_strictly_increasing( + UD60x18 x, + UD60x18 y, + uint256 a + ) public { + require(x.gt(y) && y.gt(ZERO_FP)); + require(x.lte(MAX_UD60x18)); + require(a > 0); + + UD60x18 x_a = powu(x, a); + UD60x18 y_a = powu(y, a); + + assertGte(x_a, y_a); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for maximum base and exponent > 1 + function powu_test_maximum_base(uint256 a) public { + require(a > 1); + + try this.helpersPowu(MAX_UD60x18, a) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for base < 1 and high exponent + function powu_test_high_exponent(UD60x18 x, uint256 a) public { + require(x.lt(ONE_FP) && a > 2 ** 64); + + UD60x18 result = powu(x, a); + + assertEq(result, ZERO_FP); + } + + /* ================================================================ + + TESTS FOR FUNCTION log10() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // log10(x * y) = log10(x) + log10(y) + function log10_test_distributive_mul(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + UD60x18 log10_x = log10(x); + UD60x18 log10_y = log10(y); + UD60x18 log10_x_log10_y = add(log10_x, log10_y); + + UD60x18 xy = mul(x, y); + UD60x18 log10_xy = log10(xy); + + assertEqWithinTolerance(log10_x_log10_y, log10_xy, ONE_FP, "1%"); + } + + // Test for logarithm of a power + // log10(x ** y) = y * log10(x) + function log10_test_power(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + UD60x18 x_y = pow(x, y); + UD60x18 log10_x_y = log10(x_y); + UD60x18 y_log10_x = mul(log10(x), y); + + assertEqWithinTolerance(log10_x_y, y_log10_x, ONE_FP, "1%"); + } + + // Base 10 logarithm is strictly increasing + // x > y && y > 0 --> log10(x) > log10(y) + function log10_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + UD60x18 log10_x = log10(x); + UD60x18 log10_y = log10(y); + + assertGt(log10_x, log10_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function log10_test_zero() public { + try this.helpersLog10(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function log10_test_maximum() public { + UD60x18 result; + + try this.helpersLog10(MAX_UD60x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLog10(MAX_UD60x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for values less than UNIT, should revert as result would be negative + function log10_test_less_than_unit(UD60x18 x) public { + require(x.lt(UNIT)); + + try this.helpersLog10(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION gm() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // The product of the values should be equal to the geometric mean + // raised to the power of N (numbers in the set) + function gm_test_product(UD60x18 x, UD60x18 y) public { + UD60x18 x_mul_y = x.mul(y); + UD60x18 gm_squared = pow(gm(x, y), TWO_FP); + + assertEqWithinTolerance(x_mul_y, gm_squared, ONE_TENTH_FP, "0.1%"); + } + + // The geometric mean for a set of positive numbers is less than the + // arithmetic mean of that set, as long as the values of the set are not equal + function gm_test_positive_set_avg(UD60x18 x, UD60x18 y) public { + require(x.neq(y)); + + UD60x18 gm_x_y = gm(x, y); + UD60x18 avg_x_y = avg(x, y); + + assertLte(gm_x_y, avg_x_y); + } + + // The geometric mean of a set of positive equal numbers should be + // equal to the arithmetic mean + function gm_test_positive_equal_set_avg(UD60x18 x) public { + UD60x18 gm_x = gm(x, x); + UD60x18 avg_x = avg(x, x); + + assertEq(gm_x, avg_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should return 0 + function gm_test_zero(UD60x18 x) public { + require(x.gte(ZERO_FP)); + + try this.helpersGm(x, ZERO_FP) { + UD60x18 result = gm(x, ZERO_FP); + assertEq(result, ZERO_FP); + } catch { + // Unexpected, should not revert + assert(false); + } + } +} diff --git a/contracts/Math/PRBMath/v3/utils/AssertionHelperSD.sol b/contracts/Math/PRBMath/v3/utils/AssertionHelperSD.sol new file mode 100644 index 0000000..3c542d0 --- /dev/null +++ b/contracts/Math/PRBMath/v3/utils/AssertionHelperSD.sol @@ -0,0 +1,333 @@ +pragma solidity ^0.8.0; + +import {SD59x18} from "@prb-math-v3/SD59x18.sol"; + +import {convert} from "@prb-math-v3/sd59x18/Conversions.sol"; +import {add, sub, eq, gt, gte, lt, lte, rshift} from "@prb-math-v3/sd59x18/Helpers.sol"; +import {mul, div, abs} from "@prb-math-v3/sd59x18/Math.sol"; + +abstract contract AssertionHelperSD { + event AssertEqFailure(string); + event AssertGtFailure(string); + event AssertGteFailure(string); + event AssertLtFailure(string); + event AssertLteFailure(string); + + function assertEq(SD59x18 a, SD59x18 b) internal { + if (!a.eq(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + ". No precision loss allowed." + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertEqWithinBitPrecision( + SD59x18 a, + SD59x18 b, + uint256 precision_bits + ) internal { + SD59x18 max = gt(a, b) ? a : b; + SD59x18 min = gt(a, b) ? b : a; + SD59x18 r = rshift(sub(max, min), precision_bits); + + if (!eq(r, convert(0))) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + string memory str_bits = toString(precision_bits); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_bits, + " bits of precision" + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertEqWithinTolerance( + SD59x18 a, + SD59x18 b, + SD59x18 error_percent, + string memory str_percent + ) internal { + SD59x18 tol_value = mul(a, div(error_percent, convert(100))); + + require(tol_value.neq(convert(0))); + + if (!lte(sub(b, a), abs(tol_value))) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + string memory tolerance = toString(SD59x18.unwrap(abs(tol_value))); + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_percent, + " tolerance: ", + tolerance + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + // Returns true if the n most significant bits of a and b are almost equal + // Uses functions from the library under test! + function assertEqWithinDecimalPrecision( + SD59x18 a, + SD59x18 b, + uint256 digits + ) internal { + // Divide both number by digits to truncate the unimportant digits + int256 a_int = SD59x18.unwrap(a); + int256 b_int = SD59x18.unwrap(b); + + int256 denominator = int256(10 ** digits); + + int256 a_significant = a_int / denominator; + int256 b_significant = b_int / denominator; + + int256 larger = a_significant > b_significant + ? a_significant + : b_significant; + int256 smaller = a_significant > b_significant + ? b_significant + : a_significant; + + if (!((larger - smaller) <= 1)) { + string memory str_a = toString(a_int); + string memory str_b = toString(b_int); + string memory str_larger = toString(larger); + string memory str_smaller = toString(smaller); + string memory str_digits = toString(digits); + string memory difference = toString(larger - smaller); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_digits, + " digits of precision. Difference: ", + difference, + ", truncated input:", + str_larger, + " != ", + str_smaller + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertGt(SD59x18 a, SD59x18 b, string memory reason) internal { + if (!a.gt(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " <= ", + str_b, + ", reason: ", + reason + ); + emit AssertGtFailure(string(assertMsg)); + assert(false); + } + } + + function assertGt(SD59x18 a, SD59x18 b) internal { + if (!a.gt(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " <= ", + str_b + ); + emit AssertGtFailure(string(assertMsg)); + assert(false); + } + } + + function assertGte(SD59x18 a, SD59x18 b, string memory reason) internal { + if (!a.gte(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " < ", + str_b, + ", reason: ", + reason + ); + emit AssertGteFailure(string(assertMsg)); + assert(false); + } + } + + function assertGte(SD59x18 a, SD59x18 b) internal { + if (!a.gte(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " < ", + str_b + ); + emit AssertGteFailure(string(assertMsg)); + assert(false); + } + } + + function assertLt(SD59x18 a, SD59x18 b, string memory reason) internal { + if (!a.lt(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " >= ", + str_b, + ", reason: ", + reason + ); + emit AssertLtFailure(string(assertMsg)); + assert(false); + } + } + + function assertLt(SD59x18 a, SD59x18 b) internal { + if (!a.lt(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " >= ", + str_b + ); + emit AssertLtFailure(string(assertMsg)); + assert(false); + } + } + + function assertLte(SD59x18 a, SD59x18 b, string memory reason) internal { + if (!a.lte(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " > ", + str_b, + ", reason: ", + reason + ); + emit AssertLteFailure(string(assertMsg)); + assert(false); + } + } + + function assertLte(SD59x18 a, SD59x18 b) internal { + if (!a.lte(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " > ", + str_b + ); + emit AssertLteFailure(string(assertMsg)); + assert(false); + } + } + + function toString(int256 value) internal pure returns (string memory str) { + uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); + str = toString(absValue); + + if (value < 0) { + str = string(abi.encodePacked("-", str)); + } + } + + function toString(uint256 value) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes + // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the + // trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. + let newFreeMemoryPointer := add(mload(0x40), 160) + + // Update the free memory pointer to avoid overriding our string. + mstore(0x40, newFreeMemoryPointer) + + // Assign str to the end of the zone of newly allocated memory. + str := sub(newFreeMemoryPointer, 32) + + // Clean the last word of memory it may not be overwritten. + mstore(str, 0) + + // Cache the end of the memory to calculate the length later. + let end := str + + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + // prettier-ignore + for { let temp := value } 1 {} { + // Move the pointer 1 byte to the left. + str := sub(str, 1) + + // Write the character to the pointer. + // The ASCII index of the '0' character is 48. + mstore8(str, add(48, mod(temp, 10))) + + // Keep dividing temp until zero. + temp := div(temp, 10) + + // prettier-ignore + if iszero(temp) { break } + } + + // Compute and cache the final total length of the string. + let length := sub(end, str) + + // Move the pointer 32 bytes leftwards to make room for the length. + str := sub(str, 32) + + // Store the string's length at the start of memory allocated for our string. + mstore(str, length) + } + } +} diff --git a/contracts/Math/PRBMath/v3/utils/AssertionHelperUD.sol b/contracts/Math/PRBMath/v3/utils/AssertionHelperUD.sol new file mode 100644 index 0000000..ad68cb5 --- /dev/null +++ b/contracts/Math/PRBMath/v3/utils/AssertionHelperUD.sol @@ -0,0 +1,331 @@ +pragma solidity ^0.8.0; + +import {UD60x18} from "@prb-math-v3/UD60x18.sol"; + +import {convert} from "@prb-math-v3/ud60x18/Conversions.sol"; +import {add, sub, eq, gt, gte, lt, lte, rshift} from "@prb-math-v3/ud60x18/Helpers.sol"; +import {mul, div, ln, exp, exp2, log2, sqrt, pow, avg, inv, log10, floor, powu, gm} from "@prb-math-v3/ud60x18/Math.sol"; + +abstract contract AssertionHelperUD { + event AssertEqFailure(string); + event AssertGtFailure(string); + event AssertGteFailure(string); + event AssertLtFailure(string); + event AssertLteFailure(string); + + function assertEq(UD60x18 a, UD60x18 b) internal { + if (!a.eq(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + ". No precision loss allowed." + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertEqWithinBitPrecision( + UD60x18 a, + UD60x18 b, + uint256 precision_bits + ) internal { + UD60x18 max = gt(a, b) ? a : b; + UD60x18 min = gt(a, b) ? b : a; + UD60x18 r = rshift(sub(max, min), precision_bits); + + if (!eq(r, convert(0))) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + string memory str_bits = toString(precision_bits); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_bits, + " bits of precision" + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertEqWithinTolerance( + UD60x18 a, + UD60x18 b, + UD60x18 error_percent, + string memory str_percent + ) internal { + UD60x18 tol_value = mul(a, div(error_percent, convert(100))); + + require(tol_value.neq(convert(0))); + + if (!lte(sub(b, a), tol_value)) { + string memory ua = toString(UD60x18.unwrap(a)); + string memory ub = toString(UD60x18.unwrap(b)); + string memory tolerance = toString(UD60x18.unwrap(tol_value)); + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + ua, + " != ", + ub, + " within ", + str_percent, + " tolerance: ", + tolerance + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + // Returns true if the n most significant bits of a and b are almost equal + // Uses functions from the library under test! + function assertEqWithinDecimalPrecision( + UD60x18 a, + UD60x18 b, + uint256 digits + ) internal { + // Divide both number by digits to truncate the unimportant digits + uint256 a_uint = UD60x18.unwrap(a); + uint256 b_uint = UD60x18.unwrap(b); + + uint256 a_significant = a_uint / 10 ** digits; + uint256 b_significant = b_uint / 10 ** digits; + + uint256 larger = a_significant > b_significant + ? a_significant + : b_significant; + uint256 smaller = a_significant > b_significant + ? b_significant + : a_significant; + + if (!((larger - smaller) <= 1)) { + string memory str_a = toString(a_uint); + string memory str_b = toString(b_uint); + string memory str_digits = toString(digits); + string memory str_larger = toString(larger); + string memory str_smaller = toString(smaller); + string memory difference = toString(larger - smaller); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_digits, + " digits of precision. Difference: ", + difference, + ", truncated input:", + str_larger, + " != ", + str_smaller + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertGt(UD60x18 a, UD60x18 b, string memory reason) internal { + if (!a.gt(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " <= ", + str_b, + ", reason: ", + reason + ); + emit AssertGtFailure(string(assertMsg)); + assert(false); + } + } + + function assertGt(UD60x18 a, UD60x18 b) internal { + if (!a.gt(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " <= ", + str_b + ); + emit AssertGtFailure(string(assertMsg)); + assert(false); + } + } + + function assertGte(UD60x18 a, UD60x18 b, string memory reason) internal { + if (!a.gte(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " < ", + str_b, + ", reason: ", + reason + ); + emit AssertGteFailure(string(assertMsg)); + assert(false); + } + } + + function assertGte(UD60x18 a, UD60x18 b) internal { + if (!a.gte(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " < ", + str_b + ); + emit AssertGteFailure(string(assertMsg)); + assert(false); + } + } + + function assertLt(UD60x18 a, UD60x18 b, string memory reason) internal { + if (!a.lt(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " >= ", + str_b, + ", reason: ", + reason + ); + emit AssertLtFailure(string(assertMsg)); + assert(false); + } + } + + function assertLt(UD60x18 a, UD60x18 b) internal { + if (!a.lt(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " >= ", + str_b + ); + emit AssertLtFailure(string(assertMsg)); + assert(false); + } + } + + function assertLte(UD60x18 a, UD60x18 b, string memory reason) internal { + if (!a.lte(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " > ", + str_b, + ", reason: ", + reason + ); + emit AssertLteFailure(string(assertMsg)); + assert(false); + } + } + + function assertLte(UD60x18 a, UD60x18 b) internal { + if (!a.lte(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " > ", + str_b + ); + emit AssertLteFailure(string(assertMsg)); + assert(false); + } + } + + function toString(int256 value) internal pure returns (string memory str) { + uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); + str = toString(absValue); + + if (value < 0) { + str = string(abi.encodePacked("-", str)); + } + } + + function toString(uint256 value) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes + // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the + // trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. + let newFreeMemoryPointer := add(mload(0x40), 160) + + // Update the free memory pointer to avoid overriding our string. + mstore(0x40, newFreeMemoryPointer) + + // Assign str to the end of the zone of newly allocated memory. + str := sub(newFreeMemoryPointer, 32) + + // Clean the last word of memory it may not be overwritten. + mstore(str, 0) + + // Cache the end of the memory to calculate the length later. + let end := str + + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + // prettier-ignore + for { let temp := value } 1 {} { + // Move the pointer 1 byte to the left. + str := sub(str, 1) + + // Write the character to the pointer. + // The ASCII index of the '0' character is 48. + mstore8(str, add(48, mod(temp, 10))) + + // Keep dividing temp until zero. + temp := div(temp, 10) + + // prettier-ignore + if iszero(temp) { break } + } + + // Compute and cache the final total length of the string. + let length := sub(end, str) + + // Move the pointer 32 bytes leftwards to make room for the length. + str := sub(str, 32) + + // Store the string's length at the start of memory allocated for our string. + mstore(str, length) + } + } +} diff --git a/contracts/Math/PRBMath/v4/PRBMathSD59x18PropertyTests.sol b/contracts/Math/PRBMath/v4/PRBMathSD59x18PropertyTests.sol new file mode 100644 index 0000000..2cf59e7 --- /dev/null +++ b/contracts/Math/PRBMath/v4/PRBMathSD59x18PropertyTests.sol @@ -0,0 +1,2253 @@ +pragma solidity ^0.8.19; + +import {SD59x18} from "@prb/math/SD59x18.sol"; +import {add, sub, eq, gt, gte, lt, lte, lshift, rshift} from "@prb/math/sd59x18/Helpers.sol"; +import {convert} from "@prb/math/sd59x18/Conversions.sol"; +import {msb} from "@prb/math/Common.sol"; +import {intoUint128, intoUint256} from "@prb/math/sd59x18/Casting.sol"; +import {mul, div, abs, ln, exp, exp2, log2, sqrt, pow, avg, inv, log10, floor, powu, gm} from "@prb/math/sd59x18/Math.sol"; +import "./utils/AssertionHelperSD.sol"; + +contract CryticPRBMath59x18Propertiesv4 is AssertionHelperSD { + /* ================================================================ + 59x18 fixed-point constants used for testing specific values. + This assumes that PRBMath library's convert(x) works as expected. + ================================================================ */ + SD59x18 internal ZERO_FP = convert(0); + SD59x18 internal ONE_FP = convert(1); + SD59x18 internal MINUS_ONE_FP = convert(-1); + SD59x18 internal TWO_FP = convert(2); + SD59x18 internal THREE_FP = convert(3); + SD59x18 internal EIGHT_FP = convert(8); + SD59x18 internal THOUSAND_FP = convert(1000); + SD59x18 internal MINUS_SIXTY_FOUR_FP = convert(-64); + SD59x18 internal EPSILON = SD59x18.wrap(1); + SD59x18 internal ONE_TENTH_FP = convert(1).div(convert(10)); + + /* ================================================================ + Constants used for precision loss calculations + ================================================================ */ + uint256 internal REQUIRED_SIGNIFICANT_DIGITS = 9; + SD59x18 internal LOG2_PRECISION_LOSS = SD59x18.wrap(1); + + /* ================================================================ + Integer representations maximum values. + These constants are used for testing edge cases or limits for + possible values. + ================================================================ */ + /// @dev The unit number, which gives the decimal precision of SD59x18. + int256 constant uUNIT = 1e18; + SD59x18 constant UNIT = SD59x18.wrap(1e18); + + /// @dev The minimum value an SD59x18 number can have. + int256 constant uMIN_SD59x18 = + -57896044618658097711785492504343953926634992332820282019728_792003956564819968; + SD59x18 constant MIN_SD59x18 = SD59x18.wrap(uMIN_SD59x18); + + /// @dev The maximum value an SD59x18 number can have. + int256 constant uMAX_SD59x18 = + 57896044618658097711785492504343953926634992332820282019728_792003956564819967; + SD59x18 constant MAX_SD59x18 = SD59x18.wrap(uMAX_SD59x18); + + /// @dev The maximum input permitted in {exp2}. + int256 constant uEXP2_MAX_INPUT = 192e18 - 1; + SD59x18 constant EXP2_MAX_INPUT = SD59x18.wrap(uEXP2_MAX_INPUT); + + /// @dev The maximum input permitted in {exp}. + int256 constant uEXP_MAX_INPUT = 133_084258667509499440; + SD59x18 constant EXP_MAX_INPUT = SD59x18.wrap(uEXP_MAX_INPUT); + + /// @dev Euler's number as an SD59x18 number. + SD59x18 constant E = SD59x18.wrap(2_718281828459045235); + + int256 constant uMAX_SQRT = uMAX_SD59x18 / uUNIT; + SD59x18 constant MAX_SQRT = SD59x18.wrap(uMAX_SQRT); + + SD59x18 internal constant MAX_PERMITTED_EXP2 = SD59x18.wrap(192e18 - 1); + SD59x18 internal constant MIN_PERMITTED_EXP2 = + SD59x18.wrap(-59_794705707972522261); + + SD59x18 internal constant MAX_PERMITTED_EXP = + SD59x18.wrap(133_084258667509499440); + SD59x18 internal constant MIN_PERMITTED_EXP = + SD59x18.wrap(-41_446531673892822322); + + SD59x18 internal constant MAX_PERMITTED_POW = + SD59x18.wrap(2 ** 192 * 10 ** 18 - 1); + /// @dev Half the UNIT number. + int256 constant uHALF_UNIT = 0.5e18; + SD59x18 constant HALF_UNIT = SD59x18.wrap(uHALF_UNIT); + + /// @dev log2(10) as an SD59x18 number. + int256 constant uLOG2_10 = 3_321928094887362347; + SD59x18 constant LOG2_10 = SD59x18.wrap(uLOG2_10); + + /// @dev log2(e) as an SD59x18 number. + int256 constant uLOG2_E = 1_442695040888963407; + SD59x18 constant LOG2_E = SD59x18.wrap(uLOG2_E); + + /* ================================================================ + Events used for debugging or showing information. + ================================================================ */ + event Value(string reason, SD59x18 val); + event LogErr(bytes error); + + /* ================================================================ + Helper functions. + ================================================================ */ + + // Check that there are remaining significant digits after a multiplication + // Uses functions from the library under test! + function significant_digits_are_lost_in_mult( + SD59x18 a, + SD59x18 b + ) public pure returns (bool) { + int256 la = convert(floor(log10(abs(a)))); + int256 lb = convert(floor(log10(abs(b)))); + + return (la + lb < -18); + } + + // Return how many significant digits will remain after multiplying a and b + // Uses functions from the library under test! + function significant_digits_after_mult( + SD59x18 a, + SD59x18 b + ) public pure returns (uint256) { + int256 la = convert(floor(log10(abs(a)))); + int256 lb = convert(floor(log10(abs(b)))); + int256 prec = la + lb; + + if (prec < -18) return 0; + else return (18 + absInt(prec)); + } + + // Return how many significant digits will be lost after multiplying a and b + // Uses functions from the library under test! + function significant_digits_lost_in_mult( + SD59x18 a, + SD59x18 b + ) public pure returns (uint256) { + int256 la = convert(floor(log10(abs(a)))); + int256 lb = convert(floor(log10(abs(b)))); + + if (la > 0 && lb > 0) { + return 0; + } else { + return absInt(la) < absInt(lb) ? uint256(-la) : uint256(-lb); + } + } + + // Return the absolute value of the input + function absInt(int256 a) public pure returns (uint256) { + return a >= 0 ? uint256(a) : uint256(-a); + } + + /* ================================================================ + Library wrappers. + These functions allow calling the PRBMathSD59x18 library. + ================================================================ */ + function debug(string calldata x, SD59x18 y) public { + emit Value(x, y); + } + + // Wrapper for external try/catch calls + function helpersAdd(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return add(x, y); + } + + // Wrapper for external try/catch calls + function helpersSub(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return sub(x, y); + } + + // Wrapper for external try/catch calls + function helpersMul(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return mul(x, y); + } + + function helpersDiv(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return div(x, y); + } + + function neg(SD59x18 x) public pure returns (SD59x18) { + return SD59x18.wrap(-SD59x18.unwrap(x)); + } + + function helpersAbs(SD59x18 x) public pure returns (SD59x18) { + return abs(x); + } + + function helpersLn(SD59x18 x) public pure returns (SD59x18) { + return ln(x); + } + + function helpersExp(SD59x18 x) public pure returns (SD59x18) { + return exp(x); + } + + function helpersExp2(SD59x18 x) public pure returns (SD59x18) { + return exp2(x); + } + + function helpersLog2(SD59x18 x) public pure returns (SD59x18) { + return log2(x); + } + + function helpersSqrt(SD59x18 x) public pure returns (SD59x18) { + return sqrt(x); + } + + function helpersPow(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return pow(x, y); + } + + function helpersPowu(SD59x18 x, uint256 y) public pure returns (SD59x18) { + return powu(x, y); + } + + function helpersAvg(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return avg(x, y); + } + + function helpersInv(SD59x18 x) public pure returns (SD59x18) { + return inv(x); + } + + function helpersLog10(SD59x18 x) public pure returns (SD59x18) { + return log10(x); + } + + function helpersFloor(SD59x18 x) public pure returns (SD59x18) { + return floor(x); + } + + function helpersGm(SD59x18 x, SD59x18 y) public pure returns (SD59x18) { + return gm(x, y); + } + + /* ================================================================ + + TESTS FOR FUNCTION add() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for commutative property + // x + y == y + x + function add_test_commutative(SD59x18 x, SD59x18 y) public { + SD59x18 x_y = x.add(y); + SD59x18 y_x = y.add(x); + + assertEq(x_y, y_x); + } + + // Test for associative property + // (x + y) + z == x + (y + z) + function add_test_associative(SD59x18 x, SD59x18 y, SD59x18 z) public { + SD59x18 x_y = x.add(y); + SD59x18 y_z = y.add(z); + SD59x18 xy_z = x_y.add(z); + SD59x18 x_yz = x.add(y_z); + + assertEq(xy_z, x_yz); + } + + // Test for identity operation + // x + 0 == x (equivalent to x + (-x) == 0) + function add_test_identity(SD59x18 x) public { + SD59x18 x_0 = x.add(ZERO_FP); + + assertEq(x, x_0); + assertEq(x.sub(x), ZERO_FP); + } + + // Test that the result increases or decreases depending + // on the value to be added + function add_test_values(SD59x18 x, SD59x18 y) public { + SD59x18 x_y = x.add(y); + + if (y.gte(ZERO_FP)) { + assertGte(x_y, x); + } else { + assertLt(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These should make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the addition must be between the maximum + // and minimum allowed values for SD59x18 + function add_test_range(SD59x18 x, SD59x18 y) public { + try this.helpersAdd(x, y) returns (SD59x18 result) { + assertLte(result, MAX_SD59x18); + assertGte(result, MIN_SD59x18); + } catch { + // If it reverts, just ignore + } + } + + // Adding zero to the maximum value shouldn't revert, as it is valid + // Moreover, the result must be MAX_SD59x18 + function add_test_maximum_value() public { + try this.helpersAdd(MAX_SD59x18, ZERO_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Adding one to the maximum value should revert, as it is out of range + function add_test_maximum_value_plus_one() public { + try this.helpersAdd(MAX_SD59x18, ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + // Adding zero to the minimum value shouldn't revert, as it is valid + // Moreover, the result must be MIN_SD59x18 + function add_test_minimum_value() public { + try this.helpersAdd(MIN_SD59x18, ZERO_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MIN_SD59x18); + } catch { + assert(false); + } + } + + // Adding minus one to the maximum value should revert, as it is out of range + function add_test_minimum_value_plus_negative_one() public { + try this.helpersAdd(MIN_SD59x18, MINUS_ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + /* ================================================================ + + TESTS FOR FUNCTION sub() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test equivalence to addition + // x - y == x + (-y) + function sub_test_equivalence_to_addition(SD59x18 x, SD59x18 y) public { + SD59x18 minus_y = neg(y); + SD59x18 addition = x.add(minus_y); + SD59x18 subtraction = x.sub(y); + + assertEq(addition, subtraction); + } + + // Test for non-commutative property + // x - y == -(y - x) + function sub_test_non_commutative(SD59x18 x, SD59x18 y) public { + SD59x18 x_y = x.sub(y); + SD59x18 y_x = y.sub(x); + + assertEq(x_y, neg(y_x)); + } + + // Test for identity operation + // x - 0 == x (equivalent to x - x == 0) + function sub_test_identity(SD59x18 x) public { + SD59x18 x_0 = x.sub(ZERO_FP); + + assertEq(x_0, x); + assertEq(x.sub(x), ZERO_FP); + } + + // Test for neutrality over addition and subtraction + // (x - y) + y == (x + y) - y == x + function sub_test_neutrality(SD59x18 x, SD59x18 y) public { + SD59x18 x_minus_y = x.sub(y); + SD59x18 x_plus_y = x.add(y); + + SD59x18 x_minus_y_plus_y = x_minus_y.add(y); + SD59x18 x_plus_y_minus_y = x_plus_y.sub(y); + + assertEq(x_minus_y_plus_y, x_plus_y_minus_y); + assertEq(x_minus_y_plus_y, x); + } + + // Test that the result increases or decreases depending + // on the value to be subtracted + function sub_test_values(SD59x18 x, SD59x18 y) public { + SD59x18 x_y = x.sub(y); + + if (y.gte(ZERO_FP)) { + assertLte(x_y, x); + } else { + assertGt(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the subtraction must be between the maximum + // and minimum allowed values for SD59x18 + function sub_test_range(SD59x18 x, SD59x18 y) public { + try this.helpersSub(x, y) returns (SD59x18 result) { + assertLte(result, MAX_SD59x18); + assertGte(result, MIN_SD59x18); + } catch { + // If it reverts, just ignore + } + } + + // Subtracting zero from the maximum value shouldn't revert, as it is valid + // Moreover, the result must be MAX_SD59x18 + function sub_test_maximum_value() public { + try this.helpersSub(MAX_SD59x18, ZERO_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Subtracting minus one from the maximum value should revert, + // as it is out of range + function sub_test_maximum_value_minus_neg_one() public { + try this.helpersSub(MAX_SD59x18, MINUS_ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + // Subtracting zero from the minimum value shouldn't revert, as it is valid + // Moreover, the result must be MIN_SD59x18 + function sub_test_minimum_value() public { + try this.helpersSub(MIN_SD59x18, ZERO_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MIN_SD59x18); + } catch { + assert(false); + } + } + + // Subtracting one from the minimum value should revert, as it is out of range + function sub_test_minimum_value_minus_one() public { + try this.helpersSub(MIN_SD59x18, ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + /* ================================================================ + + TESTS FOR FUNCTION mul() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for commutative property + // x * y == y * x + function mul_test_commutative(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + SD59x18 x_y = x.mul(y); + SD59x18 y_x = y.mul(x); + + assertEq(x_y, y_x); + } + + // Test for associative property + // (x * y) * z == x * (y * z) + function mul_test_associative(SD59x18 x, SD59x18 y, SD59x18 z) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18) && z.gt(MIN_SD59x18)); + SD59x18 x_y = x.mul(y); + SD59x18 y_z = y.mul(z); + SD59x18 xy_z = x_y.mul(z); + SD59x18 x_yz = x.mul(y_z); + + require(xy_z.neq(ZERO_FP) && x_yz.neq(ZERO_FP)); + + // Checks that at least 9 digits of precision are left after multiplication + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + require( + significant_digits_after_mult(y, z) > REQUIRED_SIGNIFICANT_DIGITS + ); + require( + significant_digits_after_mult(x_y, z) > REQUIRED_SIGNIFICANT_DIGITS + ); + require( + significant_digits_after_mult(x, y_z) > REQUIRED_SIGNIFICANT_DIGITS + ); + + uint256 digitsLost = significant_digits_lost_in_mult(x, y); + digitsLost += significant_digits_lost_in_mult(x, z); + digitsLost += significant_digits_lost_in_mult(y, z); + + assertEqWithinDecimalPrecision(xy_z, x_yz, digitsLost); + } + + // Test for distributive property + // x * (y + z) == x * y + x * z + function mul_test_distributive(SD59x18 x, SD59x18 y, SD59x18 z) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18) && z.gt(MIN_SD59x18)); + SD59x18 y_plus_z = y.add(z); + SD59x18 x_times_y_plus_z = x.mul(y_plus_z); + + SD59x18 x_times_y = x.mul(y); + SD59x18 x_times_z = x.mul(z); + + require( + add(x_times_y, x_times_z).neq(ZERO_FP) && + x_times_y_plus_z.neq(ZERO_FP) + ); + assertEqWithinTolerance( + add(x_times_y, x_times_z), + x_times_y_plus_z, + ONE_TENTH_FP, + "0.1%" + ); + } + + // Test for identity operation + // x * 1 == x (also check that x * 0 == 0) + function mul_test_identity(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + SD59x18 x_1 = x.mul(ONE_FP); + SD59x18 x_0 = x.mul(ZERO_FP); + + assertEq(x_0, ZERO_FP); + assertEq(x_1, x); + } + + // If x is positive and y is >= 1, the result should be larger than or equal to x + // If x is positive and y is < 1, the result should be smaller than x + function mul_test_x_positive(SD59x18 x, SD59x18 y) public { + require(x.gte(ZERO_FP)); + + SD59x18 x_y = x.mul(y); + + if (y.gte(ONE_FP)) { + assertGte(x_y, x); + } else { + assertLte(x_y, x); + } + } + + // If x is negative and y is >= 1, the result should be smaller than or equal to x + // If x is negative and y is < 1, the result should be larger than or equal to x + function mul_test_x_negative(SD59x18 x, SD59x18 y) public { + require(x.lte(ZERO_FP) && y.neq(ZERO_FP)); + + SD59x18 x_y = x.mul(y); + + if (y.gte(ONE_FP)) { + assertLte(x_y, x); + } else { + assertGte(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the multiplication must be between the maximum + // and minimum allowed values for SD59x18 + function mul_test_range(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + + try this.helpersMul(x, y) returns (SD59x18 result) { + assertLte(result, MAX_SD59x18); + assertGte(result, MIN_SD59x18); + } catch { + // If it reverts, just ignore + } + } + + // Multiplying the maximum value times one shouldn't revert, as it is valid + // Moreover, the result must be MAX_SD59x18 + function mul_test_maximum_value() public { + try this.helpersMul(MAX_SD59x18, ONE_FP) returns (SD59x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Multiplying the minimum value times one shouldn't revert, as it is valid + // Moreover, the result must be MIN_SD59x18 + function mul_test_minimum_value() public { + try this.helpersMul(MIN_SD59x18.add(ONE_FP), ONE_FP) returns ( + SD59x18 result + ) { + // Expected behaviour, does not revert + assertEq(result, MIN_SD59x18.add(ONE_FP)); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION div() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for identity property + // x / 1 == x (equivalent to x / x == 1) + function div_test_division_identity_x_div_1(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + SD59x18 div_1 = div(x, ONE_FP); + + assertEq(x, div_1); + } + + // Test for identity property + // x/x should not revert unless x == 0 || x == MIN_SD59x18 + function div_test_division_identity_x_div_x(SD59x18 x) public { + SD59x18 div_x; + + try this.helpersDiv(x, x) { + // This should always equal one + div_x = div(x, x); + assertEq(div_x, ONE_FP); + } catch { + // There are a couple of allowed cases for a revert: + // 1. x == 0 + // 2. x == MIN_SD59x18 + // 3. when the result overflows + assert(x.eq(ZERO_FP) || x.eq(MIN_SD59x18)); + } + } + + // Test for negative divisor + // x / -y == -(x / y) + function div_test_negative_divisor(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + require(y.lt(ZERO_FP)); + + SD59x18 x_y = div(x, y); + SD59x18 x_minus_y = div(x, neg(y)); + + assertEq(x_y, neg(x_minus_y)); + } + + // Test for division with 0 as numerator + // 0 / x = 0 + function div_test_division_num_zero(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + require(x.neq(ZERO_FP)); + + SD59x18 div_0 = div(ZERO_FP, x); + + assertEq(ZERO_FP, div_0); + } + + // Test that the absolute value of the result increases or + // decreases depending on the denominator's absolute value + function div_test_values(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + require(y.neq(ZERO_FP)); + + SD59x18 x_y = abs(div(x, y)); + + if (abs(y).gte(ONE_FP)) { + assertLte(x_y, abs(x)); + } else { + assertGte(x_y, abs(x)); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for division by zero + function div_test_div_by_zero(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + try this.helpersDiv(x, ZERO_FP) { + // Unexpected, this should revert + assert(false); + } catch { + // Expected revert + } + } + + // Test for division by a large value, the result should be less than one + function div_test_maximum_denominator(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + SD59x18 div_large = div(x, MAX_SD59x18); + + assertLte(abs(div_large), ONE_FP); + } + + // Test for division of a large value + // This should revert if |x| < 1 as it would return a value higher than max + function div_test_maximum_numerator(SD59x18 y) public { + SD59x18 div_large; + + try this.helpersDiv(MAX_SD59x18, y) { + // If it didn't revert, then |x| >= 1 + div_large = div(MAX_SD59x18, y); + + assertGte(abs(y), ONE_FP); + } catch { + // Expected revert as result is higher than max + } + } + + // Test for values in range + function div_test_range(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + SD59x18 result; + + try this.helpersDiv(x, y) { + // If it returns a value, it must be in range + result = div(x, y); + assertLte(result, MAX_SD59x18); + assertGte(result, MIN_SD59x18); + } catch { + // Otherwise, it should revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION neg() + + ================================================================ */ + + /* ================================================================ + Tests for mathematical properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for the double negation + // -(-x) == x + function neg_test_double_negation(SD59x18 x) public { + SD59x18 double_neg = neg(neg(x)); + + assertEq(x, double_neg); + } + + // Test for the identity operation + // x + (-x) == 0 + function neg_test_identity(SD59x18 x) public { + SD59x18 neg_x = neg(x); + + assertEq(add(x, neg_x), ZERO_FP); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for the zero-case + // -0 == 0 + function neg_test_zero() public { + SD59x18 neg_x = neg(ZERO_FP); + + assertEq(neg_x, ZERO_FP); + } + + // Test for the maximum value case + // Since this is implementation-dependant, we will actually test with MAX_SD59x18-EPS + function neg_test_maximum() public { + try this.neg(sub(MAX_SD59x18, EPSILON)) { + // Expected behaviour, does not revert + } catch { + assert(false); + } + } + + // Test for the minimum value case + // Since this is implementation-dependant, we will actually test with MIN_SD59x18+EPS + function neg_test_minimum() public { + try this.neg(add(MIN_SD59x18, EPSILON)) { + // Expected behaviour, does not revert + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION abs() + + ================================================================ */ + + /* ================================================================ + Tests for mathematical properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the absolute value is always positive + function abs_test_positive(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + SD59x18 abs_x = abs(x); + + assertGte(abs_x, ZERO_FP); + } + + // Test that the absolute value of a number equals the + // absolute value of the negative of the same number + function abs_test_negative(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + + SD59x18 abs_x = abs(x); + SD59x18 abs_minus_x = abs(neg(x)); + + assertEq(abs_x, abs_minus_x); + } + + // Test the multiplicativeness property + // | x * y | == |x| * |y| + function abs_test_multiplicativeness(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + + SD59x18 abs_x = abs(x); + SD59x18 abs_y = abs(y); + SD59x18 abs_xy = abs(mul(x, y)); + SD59x18 abs_x_abs_y = mul(abs_x, abs_y); + + // Failure if all significant digits are lost + require(significant_digits_are_lost_in_mult(abs_x, abs_y) == false); + + // Assume a tolerance of two bits of precision + assertEqWithinBitPrecision(abs_xy, abs_x_abs_y, 2); + } + + // Test the subadditivity property + // | x + y | <= |x| + |y| + function abs_test_subadditivity(SD59x18 x, SD59x18 y) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + + SD59x18 abs_x = abs(x); + SD59x18 abs_y = abs(y); + SD59x18 abs_xy = abs(add(x, y)); + + assertLte(abs_xy, add(abs_x, abs_y)); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test the zero-case | 0 | = 0 + function abs_test_zero() public { + SD59x18 abs_zero; + + try this.helpersAbs(ZERO_FP) { + // If it doesn't revert, the value must be zero + abs_zero = this.helpersAbs(ZERO_FP); + assertEq(abs_zero, ZERO_FP); + } catch { + // Unexpected, the function must not revert here + assert(false); + } + } + + // Test the maximum value + function abs_test_maximum() public { + SD59x18 abs_max; + + try this.helpersAbs(MAX_SD59x18) { + // If it doesn't revert, the value must be MAX_SD59x18 + abs_max = this.helpersAbs(MAX_SD59x18); + assertEq(abs_max, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Test the minimum value + function abs_test_minimum_revert() public { + SD59x18 abs_min; + + try this.helpersAbs(MIN_SD59x18) { + // It should always revert for MIN_SD59x18 + assert(false); + } catch {} + } + + // Test the minimum value + function abs_test_minimum_allowed() public { + SD59x18 abs_min; + SD59x18 input = MIN_SD59x18.add(SD59x18.wrap(1)); + + try this.helpersAbs(input) { + // If it doesn't revert, the value must be the negative of MIN_SD59x18 + 1 + abs_min = this.helpersAbs(input); + assertEq(abs_min, neg(input)); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION inv() + + ================================================================ */ + + /* ================================================================ + Tests for mathematical properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the inverse of the inverse is close enough to the + // original number + function inv_test_double_inverse(SD59x18 x) public { + require(x.neq(ZERO_FP)); + require(inv(x).neq(ZERO_FP)); + + SD59x18 double_inv_x = inv(inv(x)); + + // The maximum loss of precision will be 2 * log10(x) digits rounded up + uint256 loss = 2 * significant_digits_lost_in_mult(x, inv(x)) + 2; + + assertEqWithinDecimalPrecision(x, double_inv_x, loss); + } + + // Test equivalence with division + function inv_test_division(SD59x18 x) public { + require(x.neq(ZERO_FP)); + + SD59x18 inv_x = inv(x); + SD59x18 div_1_x = div(ONE_FP, x); + + assertEq(inv_x, div_1_x); + } + + // Test the anticommutativity of the division + // x / y == 1 / (y / x) + function inv_test_division_noncommutativity(SD59x18 x, SD59x18 y) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + SD59x18 x_y = div(x, y); + SD59x18 y_x = div(y, x); + + assertEqWithinTolerance(x_y, inv(y_x), ONE_FP, "1%"); + } + + // Test the multiplication of inverses + // 1/(x * y) == 1/x * 1/y + function inv_test_multiplication(SD59x18 x, SD59x18 y) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + SD59x18 inv_x = inv(x); + SD59x18 inv_y = inv(y); + SD59x18 inv_x_times_inv_y = mul(inv_x, inv_y); + + SD59x18 x_y = mul(x, y); + SD59x18 inv_x_y = inv(x_y); + + assertEqWithinTolerance(inv_x_y, inv_x_times_inv_y, ONE_FP, "1%"); + } + + // Test identity property + function inv_test_identity(SD59x18 x) public { + require(x.neq(ZERO_FP)); + + SD59x18 inv_x = inv(x); + SD59x18 identity = mul(inv_x, x); + + // They should agree with a tolerance of one percent + assertEqWithinTolerance(identity, ONE_FP, ONE_FP, "1%"); + } + + // Test that the absolute value of the result is in range zero-one + // if x is greater than one, else, the absolute value of the result + // must be greater than one + function inv_test_values(SD59x18 x) public { + require(x.neq(ZERO_FP)); + + SD59x18 abs_inv_x = abs(inv(x)); + + if (abs(x).gte(ONE_FP)) { + assertLte(abs_inv_x, ONE_FP); + } else { + assertGt(abs_inv_x, ONE_FP); + } + } + + // Test that the result has the same sign as the argument. + // Since inv() rounds towards zero, we are checking the zero case as well + function inv_test_sign(SD59x18 x) public { + require(x.neq(ZERO_FP)); + + SD59x18 inv_x = inv(x); + + if (x.gt(ZERO_FP)) { + assertGte(inv_x, ZERO_FP); + } else { + assertLte(inv_x, ZERO_FP); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test the zero-case, should revert + function inv_test_zero() public { + try this.helpersInv(ZERO_FP) { + // Unexpected, the function must revert + assert(false); + } catch {} + } + + // Test the maximum value case, should not revert, and be close to zero + function inv_test_maximum() public { + SD59x18 inv_maximum; + + try this.helpersInv(MAX_SD59x18) { + inv_maximum = this.helpersInv(MAX_SD59x18); + assertEqWithinBitPrecision(inv_maximum, ZERO_FP, 10); + } catch { + // Unexpected, the function must not revert + assert(false); + } + } + + // Test the minimum value case, should not revert, and be close to zero + function inv_test_minimum() public { + SD59x18 inv_minimum; + + try this.helpersInv(MIN_SD59x18) { + inv_minimum = this.helpersInv(MIN_SD59x18); + assertEqWithinBitPrecision(abs(inv_minimum), ZERO_FP, 10); + } catch { + // Unexpected, the function must not revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION avg() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the result is between the two operands + // avg(x, y) >= min(x, y) && avg(x, y) <= max(x, y) + function avg_test_values_in_range(SD59x18 x, SD59x18 y) public { + SD59x18 avg_xy = avg(x, y); + + if (x.gte(y)) { + assertGte(avg_xy, y); + assertLte(avg_xy, x); + } else { + assertGte(avg_xy, x); + assertLte(avg_xy, y); + } + } + + // Test that the average of the same number is itself + // avg(x, x) == x + function avg_test_one_value(SD59x18 x) public { + SD59x18 avg_x = avg(x, x); + + assertEq(avg_x, x); + } + + // Test that the order of operands is irrelevant + // avg(x, y) == avg(y, x) + function avg_test_operand_order(SD59x18 x, SD59x18 y) public { + SD59x18 avg_xy = avg(x, y); + SD59x18 avg_yx = avg(y, x); + + assertEq(avg_xy, avg_yx); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for the maximum value + function avg_test_maximum() public { + SD59x18 result; + + // This may revert due to overflow depending on implementation + // If it doesn't revert, the result must be MAX_SD59x18 + try this.helpersAvg(MAX_SD59x18, MAX_SD59x18) { + result = this.helpersAvg(MAX_SD59x18, MAX_SD59x18); + assertEq(result, MAX_SD59x18); + } catch { + assert(false); + } + } + + // Test for the minimum value + function avg_test_minimum() public { + SD59x18 result; + + // This may revert due to overflow depending on implementation + // If it doesn't revert, the result must be MIN_SD59x18 + try this.helpersAvg(MIN_SD59x18, MIN_SD59x18) { + result = this.helpersAvg(MIN_SD59x18, MIN_SD59x18); + assertEq(result, MIN_SD59x18); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION pow() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for zero exponent + // x ** 0 == 1 + function pow_test_zero_exponent(SD59x18 x) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + SD59x18 x_pow_0 = pow(x, ZERO_FP); + + assertEq(x_pow_0, ONE_FP); + } + + // Test for zero base + // 0 ** a == 0 (for positive a) + function pow_test_zero_base_non_zero_exponent(SD59x18 a) public { + require(a.gt(ZERO_FP)); + SD59x18 zero_pow_a = pow(ZERO_FP, a); + + assertEq(zero_pow_a, ZERO_FP); + } + + // Test for zero base + // 0 ** 0 == 1 + function pow_test_zero_base_zero_exponent() public { + SD59x18 zero_pow_a = pow(ZERO_FP, ZERO_FP); + + assertEq(zero_pow_a, ONE_FP); + } + + // Test for exponent one + // x ** 1 == x + function pow_test_one_exponent(SD59x18 x) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + SD59x18 x_pow_1 = pow(x, ONE_FP); + + assertEq(x_pow_1, x); + } + + // Test for base one + // 1 ** a == 1 + function pow_test_base_one(SD59x18 a) public { + SD59x18 one_pow_a = pow(ONE_FP, a); + + assertEq(one_pow_a, ONE_FP); + } + + // Test for product of powers of the same base + // x ** a * x ** b == x ** (a + b) + function pow_test_product_same_base( + SD59x18 x, + SD59x18 a, + SD59x18 b + ) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + require(a.gte(MIN_PERMITTED_EXP2) && b.gte(MIN_PERMITTED_EXP2)); + + SD59x18 x_a = pow(x, a); + SD59x18 x_b = pow(x, b); + SD59x18 x_ab = pow(x, a.add(b)); + + assertEqWithinDecimalPrecision(mul(x_a, x_b), x_ab, 9); + } + + // Test for power of an exponentiation + // (x ** a) ** b == x ** (a * b) + function pow_test_power_of_an_exponentiation( + SD59x18 x, + SD59x18 a, + SD59x18 b + ) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + require(a.mul(b).neq(ZERO_FP)); + require(a.gte(MIN_PERMITTED_EXP2) && b.gte(MIN_PERMITTED_EXP2)); + + SD59x18 x_a = pow(x, a); + SD59x18 x_a_b = pow(x_a, b); + SD59x18 x_ab = pow(x, a.mul(b)); + require(x_a_b.neq(ZERO_FP) && x_ab.neq(ZERO_FP)); + + assertEqWithinDecimalPrecision(x_a_b, x_ab, 9); + } + + // Test for power of a product + // (x * y) ** a == x ** a * y ** a + function pow_test_product_power(SD59x18 x, SD59x18 y, SD59x18 a) public { + require(x.gte(ZERO_FP) && x.lte(MAX_PERMITTED_POW)); + require(y.gte(ZERO_FP) && y.lte(MAX_PERMITTED_POW)); + + require(a.gt(convert(2 ** 32))); // to avoid massive loss of precision + + SD59x18 x_y = mul(x, y); + SD59x18 xy_a = pow(x_y, a); + + SD59x18 x_a = pow(x, a); + SD59x18 y_a = pow(y, a); + + assertEqWithinBitPrecision(mul(x_a, y_a), xy_a, 10); + } + + // Test for result being greater than or lower than the argument, depending on + // its absolute value and the value of the exponent + function pow_test_positive_exponent(SD59x18 x, SD59x18 a) public { + require(x.gte(ZERO_FP) && a.gte(ZERO_FP)); + + SD59x18 x_a = pow(x, a); + + if (abs(x).gte(ONE_FP)) { + assertGte(abs(x_a), ONE_FP); + } + + if (abs(x).lte(ONE_FP)) { + assertLte(abs(x_a), ONE_FP); + } + } + + // Test for result being greater than or lower than the argument, depending on + // its absolute value and the value of the exponent + function pow_test_negative_exponent(SD59x18 x, SD59x18 a) public { + require(x.gte(ZERO_FP) && a.lte(ZERO_FP)); + + SD59x18 x_a = pow(x, a); + + if (abs(x).gte(ONE_FP)) { + assertLte(abs(x_a), ONE_FP); + } + + if (abs(x).lte(ONE_FP)) { + assertGte(abs(x_a), ONE_FP); + } + } + + // Test for result sign: if the exponent is even, sign is positive + // if the exponent is odd, preserves the sign of the base + function pow_test_sign(SD59x18 x, SD59x18 a) public { + require(x.gte(ZERO_FP)); + + SD59x18 x_a = pow(x, a); + + // This prevents the case where a small negative number gets + // rounded down to zero and thus changes sign + require(x_a.neq(ZERO_FP)); + + // If the exponent is even + if (a.mod(convert(2)).eq(ZERO_FP)) { + assertEq(x_a, abs(x_a)); + } else { + // x_a preserves x sign + if (x.lt(ZERO_FP)) { + assertLt(x_a, ZERO_FP); + } else { + assertGt(x_a, ZERO_FP); + } + } + } + + // pow(2, a) == exp2(a) + function pow_test_exp2_equivalence(SD59x18 a) public { + SD59x18 pow_result = pow(convert(2), a); + SD59x18 exp2_result = exp2(a); + + assertEq(pow_result, exp2_result); + } + + // Power is strictly increasing + // x > y && a >= 0 --> pow(x, a) >= pow(y, a) + function pow_test_strictly_increasing( + SD59x18 x, + SD59x18 y, + SD59x18 a + ) public { + require(x.gt(y) && x.lte(MAX_PERMITTED_POW)); + require(a.gte(ZERO_FP)); + + SD59x18 x_a = pow(x, a); + SD59x18 y_a = pow(y, a); + + assertGte(x_a, y_a); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for maximum base and exponent > 1 + function pow_test_maximum_base(SD59x18 a) public { + require(a.gt(ONE_FP)); + + try this.helpersPow(MAX_SD59x18, a) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for abs(base) < 1 and high exponent + function pow_test_high_exponent(SD59x18 x, SD59x18 a) public { + require(abs(x).lt(ONE_FP) && a.gt(convert(2 ** 32))); + + SD59x18 result = pow(x, a); + + assertEq(result, ZERO_FP); + } + + /* ================================================================ + + TESTS FOR FUNCTION sqrt() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for the inverse operation + // sqrt(x) * sqrt(x) == x + function sqrt_test_inverse_mul(SD59x18 x) public { + require(x.gte(ZERO_FP)); + + SD59x18 sqrt_x = sqrt(x); + SD59x18 sqrt_x_squared = mul(sqrt_x, sqrt_x); + + // Precision loss is at most half the bits of the operand + assertEqWithinBitPrecision( + sqrt_x_squared, + x, + (intoUint256(log2(x)) >> 1) + 2 + ); + } + + // Test for the inverse operation + // sqrt(x) ** 2 == x + function sqrt_test_inverse_pow(SD59x18 x) public { + require(x.gte(ZERO_FP)); + + SD59x18 sqrt_x = sqrt(x); + SD59x18 sqrt_x_squared = pow(sqrt_x, convert(2)); + + // Precision loss is at most half the bits of the operand + assertEqWithinBitPrecision( + sqrt_x_squared, + x, + (intoUint256(log2(x)) >> 1) + 2 + ); + } + + // Test for distributive property respect to the multiplication + // sqrt(x) * sqrt(y) == sqrt(x * y) + function sqrt_test_distributive(SD59x18 x, SD59x18 y) public { + require(x.gte(ZERO_FP) && y.gte(ZERO_FP)); + + SD59x18 sqrt_x = sqrt(x); + SD59x18 sqrt_y = sqrt(y); + SD59x18 sqrt_x_sqrt_y = mul(sqrt_x, sqrt_y); + SD59x18 sqrt_xy = sqrt(mul(x, y)); + + // Ensure we have enough significant digits for the result to be meaningful + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + require( + significant_digits_after_mult(sqrt_x, sqrt_y) > + REQUIRED_SIGNIFICANT_DIGITS + ); + + // Allow an error of up to one tenth of a percent + assertEqWithinTolerance(sqrt_x_sqrt_y, sqrt_xy, ONE_TENTH_FP, "0.1%"); + } + + // Test that sqrt is strictly increasing + // x >= 0 && y > x --> sqrt(y) >= sqrt(x) + function sqrt_test_strictly_increasing(SD59x18 x, SD59x18 y) public { + require(x.gte(ZERO_FP) && x.lte(MAX_SQRT.sub(SD59x18.wrap(1)))); + require(y.gt(x) && y.lte(MAX_SQRT)); + + SD59x18 sqrt_x = sqrt(x); + SD59x18 sqrt_y = sqrt(y); + + assertGte(sqrt_y, sqrt_x); + } + + // Square root of perfect square should be equal to x + // sqrt(x * x) == x + function sqrt_test_square(SD59x18 x) public { + require(x.gt(ONE_FP)); + + SD59x18 square_x = x.mul(x); + SD59x18 sqrt_square_x = sqrt(square_x); + + uint256 loss = significant_digits_lost_in_mult(x, x); + + assertEqWithinDecimalPrecision(sqrt_square_x, x, loss); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + function sqrt_test_zero() public { + assertEq(sqrt(ZERO_FP), ZERO_FP); + } + + // Test for maximum value + function sqrt_test_maximum() public { + try this.helpersSqrt(MAX_SQRT) { + // Expected behaviour, MAX_SQRT is positive, and operation + // should not revert as the result is in range + } catch { + // Unexpected, should not revert + assert(false); + } + } + + // Test for minimum value + function sqrt_test_minimum() public { + try this.helpersSqrt(MIN_SD59x18) { + // Unexpected, should revert. MIN_SD59x18 is negative. + assert(false); + } catch { + // Expected behaviour, revert + } + } + + // Test for negative operands + function sqrt_test_negative(SD59x18 x) public { + require(x.lt(ZERO_FP)); + + try this.helpersSqrt(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected behaviour, revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION log2() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // log2(x * y) = log2(x) + log2(y) + function log2_test_distributive_mul(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.gt(ZERO_FP)); + SD59x18 log2_x = log2(x); + SD59x18 log2_y = log2(y); + SD59x18 log2_x_log2_y = add(log2_x, log2_y); + + SD59x18 xy = mul(x, y); + SD59x18 log2_xy = log2(xy); + + // Ensure we have enough significant digits for the result to be meaningful + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + + // The maximum loss of precision is given by the formula: + // | log10(x) + log10(y) | + uint256 loss = significant_digits_lost_in_mult(x, y) + 2; + + assertEqWithinDecimalPrecision(log2_x_log2_y, log2_xy, loss); + } + + // Test for logarithm of a power + // log2(x ** y) = y * log2(x) + function log2_test_power(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP)); + SD59x18 x_y = pow(x, y); + require(x_y.gt(ZERO_FP)); + SD59x18 log2_x_y = log2(x_y); + SD59x18 y_log2_x = mul(log2(x), y); + + assertEqWithinTolerance(y_log2_x, log2_x_y, ONE_FP, "1%"); + } + + // Base 2 logarithm is strictly increasing + // y > 0 && x > y --> log2(x) > log2(y) + function log2_test_strictly_increasing(SD59x18 x, SD59x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + SD59x18 log2_x = log2(x); + SD59x18 log2_y = log2(y); + + assertGte(log2_x, log2_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function log2_test_zero() public { + try this.helpersLog2(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function log2_test_maximum() public { + SD59x18 result; + + try this.helpersLog2(MAX_SD59x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLog2(MAX_SD59x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for negative values, should revert as log2 is not defined + function log2_test_negative(SD59x18 x) public { + require(x.lt(ZERO_FP)); + + try this.helpersLog2(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION ln() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // ln(x * y) = ln(x) + ln(y) + function ln_test_distributive_mul(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.gt(ZERO_FP)); + + SD59x18 ln_x = ln(x); + SD59x18 ln_y = ln(y); + SD59x18 ln_x_ln_y = add(ln_x, ln_y); + + SD59x18 xy = mul(x, y); + SD59x18 ln_xy = ln(xy); + + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + + // The maximum loss of precision is given by the formula: + // | log2(x) + log2(y) | + uint256 loss = significant_digits_lost_in_mult(x, y) + 2; + + assertEqWithinDecimalPrecision(ln_x_ln_y, ln_xy, loss); + } + + // Test for logarithm of a power + // ln(x ** y) = y * ln(x) + function ln_test_power(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP)); + SD59x18 x_y = pow(x, y); + SD59x18 ln_x_y = ln(x_y); + + SD59x18 y_ln_x = mul(ln(x), y); + + require( + significant_digits_after_mult(ln(x), y) > + REQUIRED_SIGNIFICANT_DIGITS + ); + + assertEqWithinTolerance(ln_x_y, y_ln_x, ONE_FP, "1%"); + } + + // Natural logarithm is strictly increasing + // y > 0 && x > y --> log2(x) > log2(y) + function ln_test_strictly_increasing(SD59x18 x, SD59x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + SD59x18 log2_x = ln(x); + SD59x18 log2_y = ln(y); + + assertGte(log2_x, log2_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function ln_test_zero() public { + try this.helpersLn(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function ln_test_maximum() public { + SD59x18 result; + + try this.helpersLn(MAX_SD59x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLn(MAX_SD59x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for negative values, should revert as ln is not defined + function ln_test_negative(SD59x18 x) public { + require(x.lt(ZERO_FP)); + + try this.helpersLn(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION exp2() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for equality with pow(2, x) for integer x + // pow(2, x) == exp2(x) + function exp2_test_equivalence_pow(SD59x18 x) public { + require(x.lte(MAX_PERMITTED_EXP2)); + SD59x18 exp2_x = exp2(x); + SD59x18 pow_2_x = pow(TWO_FP, x); + + assertEq(exp2_x, pow_2_x); + } + + // Test for inverse function + // If y = log2(x) then exp2(y) == x + function exp2_test_inverse(SD59x18 x) public { + require(x.lte(MAX_PERMITTED_EXP2) && x.gt(ZERO_FP)); + SD59x18 log2_x = log2(x); + SD59x18 exp2_x = exp2(log2_x); + + assertEqWithinDecimalPrecision(x, exp2_x, 9); + } + + // Test for negative exponent + // exp2(-x) == inv( exp2(x) ) + function exp2_test_negative_exponent(SD59x18 x) public { + require(x.lt(ZERO_FP) && x.neq(MIN_SD59x18)); + + SD59x18 exp2_x = exp2(x); + SD59x18 exp2_minus_x = exp2(neg(x)); + + assertEqWithinDecimalPrecision(exp2_x, inv(exp2_minus_x), 2); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + // exp2(0) == 1 + function exp2_test_zero() public { + SD59x18 exp_zero = exp2(ZERO_FP); + assertEq(exp_zero, ONE_FP); + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp2_test_maximum() public { + try this.helpersExp2(MAX_SD59x18) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert + } + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp2_test_maximum_permitted() public { + try this.helpersExp2(MAX_PERMITTED_EXP2) { + // Should always pass + } catch { + // Should never revert + assert(false); + } + } + + // Test for minimum value. This should return zero since + // 2 ** -x == 1 / 2 ** x that tends to zero as x increases + function exp2_test_minimum() public { + SD59x18 result; + + try this.helpersExp2(MIN_SD59x18) { + // Expected, should not revert, check that value is zero + result = exp2(MIN_SD59x18); + assertEq(result, ZERO_FP); + } catch { + // Unexpected revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION exp() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for inverse function + // If y = ln(x) then exp(y) == x + function exp_test_inverse(SD59x18 x) public { + require(x.lte(MAX_PERMITTED_EXP)); + SD59x18 ln_x = ln(x); + SD59x18 exp_x = exp(ln_x); + SD59x18 log10_x = log10(x); + + assertEqWithinDecimalPrecision(x, exp_x, 9); + } + + // Test for negative exponent + // exp(-x) == inv( exp(x) ) + function exp_test_negative_exponent(SD59x18 x) public { + require(x.lt(ZERO_FP) && x.neq(MIN_SD59x18)); + + SD59x18 exp_x = exp(x); + SD59x18 exp_minus_x = exp(neg(x)); + + // Result should be within 4 bits precision for the worst case + assertEqWithinBitPrecision(exp_x, inv(exp_minus_x), 4); + } + + // Test that exp strictly increases + function exp_test_strictly_increasing(SD59x18 x, SD59x18 y) public { + require(x.lte(MAX_PERMITTED_EXP)); + require(y.gt(x) && y.lte(MAX_PERMITTED_EXP)); + + SD59x18 exp_x = exp(x); + SD59x18 exp_y = exp(y); + + assertGte(exp_y, exp_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + // exp(0) == 1 + function exp_test_zero() public { + SD59x18 exp_zero = exp(ZERO_FP); + assertEq(exp_zero, ONE_FP); + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp_test_maximum() public { + try this.helpersExp(MAX_SD59x18) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert + } + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp_test_maximum_permitted() public { + try this.helpersExp(MAX_PERMITTED_EXP) { + // Expected to always succeed + } catch { + // Unexpected, should revert + assert(false); + } + } + + // Test for minimum value. This should return zero since + // e ** -x == 1 / e ** x that tends to zero as x increases + function exp_test_minimum() public { + SD59x18 result; + + try this.helpersExp(MIN_SD59x18) { + // Expected, should not revert, check that value is zero + result = exp(MIN_SD59x18); + assertEq(result, ZERO_FP); + } catch { + // Unexpected revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION powu() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for zero exponent + // x ** 0 == 1 + function powu_test_zero_exponent(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18)); + SD59x18 x_pow_0 = powu(x, 0); + + assertEq(x_pow_0, ONE_FP); + } + + // Test for zero base + // 0 ** y == 0 (for positive y) + function powu_test_zero_base(uint256 y) public { + require(y != 0); + + SD59x18 zero_pow_y = powu(ZERO_FP, y); + + assertEq(zero_pow_y, ZERO_FP); + } + + // Test for exponent one + // x ** 1 == x + function powu_test_one_exponent(SD59x18 x) public { + require(x.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18)); + SD59x18 x_pow_1 = powu(x, 1); + + assertEq(x_pow_1, x); + } + + // Test for base one + // 1 ** x == 1 + function powu_test_base_one(uint256 y) public { + SD59x18 one_pow_y = powu(ONE_FP, y); + + assertEq(one_pow_y, ONE_FP); + } + + // Test for product of powers of the same base + // x ** a * x ** b == x ** (a + b) + function powu_test_product_same_base( + SD59x18 x, + uint256 a, + uint256 b + ) public { + require(x.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18)); + + SD59x18 x_a = powu(x, a); + SD59x18 x_b = powu(x, b); + SD59x18 x_ab = powu(x, a + b); + + assertEqWithinDecimalPrecision(mul(x_a, x_b), x_ab, 10); + } + + // Test for power of an exponentiation + // (x ** a) ** b == x ** (a * b) + function powu_test_power_of_an_exponentiation( + SD59x18 x, + uint256 a, + uint256 b + ) public { + require(x.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18)); + + SD59x18 x_a = powu(x, a); + SD59x18 x_a_b = powu(x_a, b); + SD59x18 x_ab = powu(x, a * b); + + assertEqWithinDecimalPrecision(x_a_b, x_ab, 10); + } + + // Test for power of a product + // (x * y) ** a == x ** a * y ** a + function powu_test_product_power(SD59x18 x, SD59x18 y, uint256 a) public { + require(x.gt(MIN_SD59x18) && y.gt(MIN_SD59x18)); + require(x.lte(MAX_SD59x18) && y.lte(MAX_SD59x18)); + + require(a > 2 ** 32); // to avoid massive loss of precision + + SD59x18 x_y = mul(x, y); + SD59x18 xy_a = powu(x_y, a); + + SD59x18 x_a = powu(x, a); + SD59x18 y_a = powu(y, a); + + assertEqWithinDecimalPrecision(mul(x_a, y_a), xy_a, 10); + } + + // Test for result being greater than or lower than the argument, depending on + // its absolute value and the value of the exponent + function powu_test_values(SD59x18 x, uint256 a) public { + require(x.neq(ZERO_FP)); + require(x.neq(MIN_SD59x18)); + + SD59x18 x_a = powu(x, a); + + if (abs(x).gte(ONE_FP)) { + assertGte(abs(x_a), ONE_FP); + } + + if (abs(x).lte(ONE_FP)) { + assertLte(abs(x_a), ONE_FP); + } + } + + // Test for result sign: if the exponent is even, sign is positive + // if the exponent is odd, preserves the sign of the base + function powu_test_sign(SD59x18 x, uint256 a) public { + require(x.neq(ZERO_FP) && a != 0); + + SD59x18 x_a = powu(x, a); + + // This prevents the case where a small negative number gets + // rounded down to zero and thus changes sign + require(x_a.neq(ZERO_FP)); + + // If the exponent is even + if (a % 2 == 0) { + assertEq(x_a, abs(x_a)); + } else { + // x_a preserves x sign + if (x.lt(ZERO_FP)) { + assertLt(x_a, ZERO_FP); + } else { + assertGt(x_a, ZERO_FP); + } + } + } + + // Unsigned power is strictly increasing + // y > MIN && x > y && a > 0 && y != 0 --> powu(x, a) >= powu(y, a) + function powu_test_strictly_increasing( + SD59x18 x, + SD59x18 y, + uint256 a + ) public { + require(x.gt(y) && y.gt(MIN_SD59x18) && y.neq(ZERO_FP)); + require(x.lte(MAX_SD59x18)); + require(a > 0); + + SD59x18 x_a = powu(x, a); + SD59x18 y_a = powu(y, a); + + assertGte(x_a, y_a); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for maximum base and exponent > 1 + function powu_test_maximum_base(uint256 a) public { + require(a > 1); + + try this.helpersPowu(MAX_SD59x18, a) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for abs(base) < 1 and high exponent + function powu_test_high_exponent(SD59x18 x, uint256 a) public { + require(abs(x).lt(ONE_FP) && a > 2 ** 32); + + SD59x18 result = powu(x, a); + + assertEq(result, ZERO_FP); + } + + /* ================================================================ + + TESTS FOR FUNCTION log10() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // log10(x * y) = log10(x) + log10(y) + function log10_test_distributive_mul(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.gt(ZERO_FP)); + SD59x18 log10_x = log10(x); + SD59x18 log10_y = log10(y); + SD59x18 log10_x_log10_y = add(log10_x, log10_y); + + SD59x18 xy = mul(x, y); + SD59x18 log10_xy = log10(xy); + + // Ensure we have enough significant digits for the result to be meaningful + require( + significant_digits_after_mult(x, y) > REQUIRED_SIGNIFICANT_DIGITS + ); + + assertEqWithinTolerance(log10_x_log10_y, log10_xy, ONE_FP, "1%"); + } + + // Test for logarithm of a power + // log10(x ** y) = y * log10(x) + function log10_test_power(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.gt(ZERO_FP)); + SD59x18 x_y = pow(x, y); + SD59x18 log10_x_y = log10(x_y); + SD59x18 y_log10_x = mul(log10(x), y); + + assertEqWithinTolerance(log10_x_y, y_log10_x, ONE_FP, "1%"); + } + + // Base 10 logarithm is strictly increasing + // x > y && y > 0 --> log10(x) > log10(y) + function log10_is_increasing(SD59x18 x, SD59x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + SD59x18 log2_x = log10(x); + SD59x18 log2_y = log10(y); + + assertGt(log2_x, log2_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function log10_test_zero() public { + try this.helpersLog10(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function log10_test_maximum() public { + SD59x18 result; + + try this.helpersLog10(MAX_SD59x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLog10(MAX_SD59x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for negative values, should revert as log10 is not defined + function log10_test_negative(SD59x18 x) public { + require(x.lt(ZERO_FP)); + + try this.helpersLog10(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION gm() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // The product of the values should be equal to the geometric mean + // raised to the power of N (numbers in the set) + function gm_test_product(SD59x18 x, SD59x18 y) public { + bool x_sign = x.gt(ZERO_FP); + bool y_sign = y.gt(ZERO_FP); + require(x_sign = y_sign); + + SD59x18 x_mul_y = x.mul(y); + SD59x18 gm_squared = pow(gm(x, y), TWO_FP); + + assertEqWithinTolerance(x_mul_y, gm_squared, ONE_TENTH_FP, "0.1%"); + } + + // The geometric mean for a set of positive numbers is less than the + // arithmetic mean of that set, as long as the values of the set are not equal + function gm_test_positive_set_avg(SD59x18 x, SD59x18 y) public { + require(x.gte(ZERO_FP) && y.gte(ZERO_FP) && x.neq(y)); + + SD59x18 gm_x_y = gm(x, y); + SD59x18 avg_x_y = avg(x, y); + + assertLte(gm_x_y, avg_x_y); + } + + // The geometric mean of a set of positive equal numbers should be + // equal to the arithmetic mean + function gm_test_positive_equal_set_avg(SD59x18 x) public { + require(x.gte(ZERO_FP)); + + SD59x18 gm_x = gm(x, x); + SD59x18 avg_x = avg(x, x); + + assertEq(gm_x, avg_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should return 0 + function gm_test_zero(SD59x18 x) public { + require(x.gte(ZERO_FP)); + + try this.helpersGm(x, ZERO_FP) { + SD59x18 result = gm(x, ZERO_FP); + assertEq(result, ZERO_FP); + } catch { + // Unexpected, should not revert + assert(false); + } + } + + // Test for single negative input + function gm_test_negative(SD59x18 x, SD59x18 y) public { + require(x.gt(ZERO_FP) && y.lt(ZERO_FP)); + + try this.helpersGm(x, y) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, gm of a negative product is not defined + } + } +} diff --git a/contracts/Math/PRBMath/v4/PRBMathUD60x18PropertyTests.sol b/contracts/Math/PRBMath/v4/PRBMathUD60x18PropertyTests.sol new file mode 100644 index 0000000..a6f9c8d --- /dev/null +++ b/contracts/Math/PRBMath/v4/PRBMathUD60x18PropertyTests.sol @@ -0,0 +1,1629 @@ +pragma solidity ^0.8.19; + +import {UD60x18} from "@prb/math/UD60x18.sol"; +import {add, sub, eq, gt, gte, lt, lte, lshift, rshift} from "@prb/math/ud60x18/Helpers.sol"; +import {convert} from "@prb/math/ud60x18/Conversions.sol"; +import {msb} from "@prb/math/Common.sol"; +import {intoUint128, intoUint256} from "@prb/math/ud60x18/Casting.sol"; +import {mul, div, ln, exp, exp2, log2, sqrt, pow, avg, inv, log10, floor, powu, gm} from "@prb/math/ud60x18/Math.sol"; +import "./utils/AssertionHelperUD.sol"; + +contract CryticPRBMath60x18Propertiesv4 is AssertionHelperUD { + /* ================================================================ + 59x18 fixed-point constants used for testing specific values. + This assumes that PRBMath library's convert(x) works as expected. + ================================================================ */ + UD60x18 internal ZERO_FP = convert(0); + UD60x18 internal ONE_FP = convert(1); + UD60x18 internal TWO_FP = convert(2); + UD60x18 internal THREE_FP = convert(3); + UD60x18 internal EIGHT_FP = convert(8); + UD60x18 internal THOUSAND_FP = convert(1000); + UD60x18 internal EPSILON = UD60x18.wrap(1); + UD60x18 internal ONE_TENTH_FP = convert(1).div(convert(10)); + + /* ================================================================ + Constants used for precision loss calculations + ================================================================ */ + uint256 internal REQUIRED_SIGNIFICANT_DIGITS = 10; + + /* ================================================================ + Integer representations maximum values. + These constants are used for testing edge cases or limits for + possible values. + ================================================================ */ + /// @dev Euler's number as an UD60x18 number. + UD60x18 constant E = UD60x18.wrap(2_718281828459045235); + + /// @dev Half the UNIT number. + uint256 constant uHALF_UNIT = 0.5e18; + UD60x18 constant HALF_UNIT = UD60x18.wrap(uHALF_UNIT); + + /// @dev log2(10) as an UD60x18 number. + uint256 constant uLOG2_10 = 3_321928094887362347; + UD60x18 constant LOG2_10 = UD60x18.wrap(uLOG2_10); + + /// @dev log2(e) as an UD60x18 number. + uint256 constant uLOG2_E = 1_442695040888963407; + UD60x18 constant LOG2_E = UD60x18.wrap(uLOG2_E); + + /// @dev The maximum value an UD60x18 number can have. + uint256 constant uMAX_UD60x18 = + 115792089237316195423570985008687907853269984665640564039457_584007913129639935; + UD60x18 constant MAX_UD60x18 = UD60x18.wrap(uMAX_UD60x18); + + /// @dev The maximum whole value an UD60x18 number can have. + uint256 constant uMAX_WHOLE_UD60x18 = + 115792089237316195423570985008687907853269984665640564039457_000000000000000000; + UD60x18 constant MAX_WHOLE_UD60x18 = UD60x18.wrap(uMAX_WHOLE_UD60x18); + + /// @dev PI as an UD60x18 number. + UD60x18 constant PI = UD60x18.wrap(3_141592653589793238); + + /// @dev The unit amount that implies how many trailing decimals can be represented. + uint256 constant uUNIT = 1e18; + UD60x18 constant UNIT = UD60x18.wrap(uUNIT); + + /// @dev Zero as an UD60x18 number. + UD60x18 constant ZERO = UD60x18.wrap(0); + + UD60x18 internal constant MAX_PERMITTED_EXP2 = UD60x18.wrap(192e18 - 1); + UD60x18 internal constant MAX_PERMITTED_EXP = + UD60x18.wrap(133_084258667509499440); + UD60x18 internal constant MAX_PERMITTED_POW = + UD60x18.wrap(2 ** 192 * 10 ** 18 - 1); + UD60x18 internal constant MAX_PERMITTED_SQRT = + UD60x18.wrap( + 115792089237316195423570985008687907853269_984665640564039457 + ); + + /* ================================================================ + Events used for debugging or showing information. + ================================================================ */ + event Value(string reason, UD60x18 val); + event LogErr(bytes error); + + /* ================================================================ + Library wrappers. + These functions allow calling the PRBMathUD60x18 library. + ================================================================ */ + function debug(string calldata x, UD60x18 y) public { + emit Value(x, y); + } + + // Wrapper for external try/catch calls + function helpersAdd(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return add(x, y); + } + + // Wrapper for external try/catch calls + function helpersSub(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return sub(x, y); + } + + // Wrapper for external try/catch calls + function helpersMul(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return mul(x, y); + } + + function helpersDiv(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return div(x, y); + } + + function helpersLn(UD60x18 x) public pure returns (UD60x18) { + return ln(x); + } + + function helpersExp(UD60x18 x) public pure returns (UD60x18) { + return exp(x); + } + + function helpersExp2(UD60x18 x) public pure returns (UD60x18) { + return exp2(x); + } + + function helpersLog2(UD60x18 x) public pure returns (UD60x18) { + return log2(x); + } + + function helpersSqrt(UD60x18 x) public pure returns (UD60x18) { + return sqrt(x); + } + + function helpersPow(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return pow(x, y); + } + + function helpersPowu(UD60x18 x, uint256 y) public pure returns (UD60x18) { + return powu(x, y); + } + + function helpersAvg(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return avg(x, y); + } + + function helpersInv(UD60x18 x) public pure returns (UD60x18) { + return inv(x); + } + + function helpersLog10(UD60x18 x) public pure returns (UD60x18) { + return log10(x); + } + + function helpersFloor(UD60x18 x) public pure returns (UD60x18) { + return floor(x); + } + + function helpersGm(UD60x18 x, UD60x18 y) public pure returns (UD60x18) { + return gm(x, y); + } + + /* ================================================================ + + TESTS FOR FUNCTION add() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for commutative property + // x + y == y + x + function add_test_commutative(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.add(y); + UD60x18 y_x = y.add(x); + + assertEq(x_y, y_x); + } + + // Test for associative property + // (x + y) + z == x + (y + z) + function add_test_associative(UD60x18 x, UD60x18 y, UD60x18 z) public { + UD60x18 x_y = x.add(y); + UD60x18 y_z = y.add(z); + UD60x18 xy_z = x_y.add(z); + UD60x18 x_yz = x.add(y_z); + + assertEq(xy_z, x_yz); + } + + // Test for identity operation + // x + 0 == x (equivalent to x + (-x) == 0) + function add_test_identity(UD60x18 x) public { + UD60x18 x_0 = x.add(ZERO_FP); + + assertEq(x, x_0); + assertEq(x.sub(x), ZERO_FP); + } + + // Test that the result increases or decreases depending + // on the value to be added + function add_test_values(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.add(y); + + assertGte(x_y, x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These should make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the addition must be between the maximum + // and minimum allowed values for UD60x18 + function add_test_range(UD60x18 x, UD60x18 y) public { + try this.helpersAdd(x, y) returns (UD60x18 result) { + assert(result.lte(MAX_UD60x18) && result.gte(ZERO_FP)); + } catch { + // If it reverts, just ignore + } + } + + // Adding zero to the maximum value shouldn't revert, as it is valid + // Moreover, the result must be MAX_UD60x18 + function add_test_maximum_value() public { + try this.helpersAdd(MAX_UD60x18, ZERO_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_UD60x18); + } catch { + assert(false); + } + } + + // Adding one to the maximum value should revert, as it is out of range + function add_test_maximum_value_plus_one() public { + try this.helpersAdd(MAX_UD60x18, ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + // Adding zero to the minimum value shouldn't revert, as it is valid + // Moreover, the result must be MIN_UD60x18 + function add_test_minimum_value() public { + try this.helpersAdd(ZERO_FP, ZERO_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, ZERO_FP); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION sub() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for identity operation + // x - 0 == x (equivalent to x - x == 0) + function sub_test_identity(UD60x18 x) public { + UD60x18 x_0 = x.sub(ZERO_FP); + + assertEq(x_0, x); + assertEq(x.sub(x), ZERO_FP); + } + + // Test for neutrality over addition and subtraction + // (x - y) + y == (x + y) - y == x + function sub_test_neutrality(UD60x18 x, UD60x18 y) public { + UD60x18 x_minus_y = x.sub(y); + UD60x18 x_plus_y = x.add(y); + + UD60x18 x_minus_y_plus_y = x_minus_y.add(y); + UD60x18 x_plus_y_minus_y = x_plus_y.sub(y); + + assertEq(x_minus_y_plus_y, x_plus_y_minus_y); + assertEq(x_minus_y_plus_y, x); + } + + // Test that the result always decreases + function sub_test_values(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.sub(y); + + assertLte(x_y, x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the subtraction must be between the maximum + // and minimum allowed values for UD60x18 + function sub_test_range(UD60x18 x, UD60x18 y) public { + try this.helpersSub(x, y) returns (UD60x18 result) { + assert(result.lte(MAX_UD60x18) && result.gte(ZERO_FP)); + } catch { + // If it reverts, just ignore + } + } + + // Subtracting zero from the maximum value shouldn't revert, as it is valid + // Moreover, the result must be MAX_UD60x18 + function sub_test_maximum_value() public { + try this.helpersSub(MAX_UD60x18, ZERO_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_UD60x18); + } catch { + assert(false); + } + } + + // Subtracting zero from the minimum value shouldn't revert, as it is valid + // Moreover, the result must be MIN_UD60x18 + function sub_test_minimum_value() public { + try this.helpersSub(ZERO_FP, ZERO_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, ZERO_FP); + } catch { + assert(false); + } + } + + // Subtracting one from the minimum value should revert, as it is out of range + function sub_test_minimum_value_minus_one() public { + try this.helpersSub(ZERO_FP, ONE_FP) { + assert(false); + } catch { + // Expected behaviour, reverts + } + } + + /* ================================================================ + + TESTS FOR FUNCTION mul() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for commutative property + // x * y == y * x + function mul_test_commutative(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.mul(y); + UD60x18 y_x = y.mul(x); + + assertEq(x_y, y_x); + } + + // Test for associative property + // (x * y) * z == x * (y * z) + function mul_test_associative(UD60x18 x, UD60x18 y, UD60x18 z) public { + UD60x18 x_y = x.mul(y); + UD60x18 y_z = y.mul(z); + UD60x18 xy_z = x_y.mul(z); + UD60x18 x_yz = x.mul(y_z); + + require(xy_z.neq(ZERO_FP) && x_yz.neq(ZERO_FP)); + assertEqWithinTolerance(xy_z, x_yz, ONE_TENTH_FP, "0.1%"); + } + + // Test for distributive property + // x * (y + z) == x * y + x * z + function mul_test_distributive(UD60x18 x, UD60x18 y, UD60x18 z) public { + UD60x18 y_plus_z = y.add(z); + UD60x18 x_times_y_plus_z = x.mul(y_plus_z); + + UD60x18 x_times_y = x.mul(y); + UD60x18 x_times_z = x.mul(z); + + assertEqWithinTolerance( + add(x_times_y, x_times_z), + x_times_y_plus_z, + ONE_TENTH_FP, + "0.1%" + ); + } + + // Test for identity operation + // x * 1 == x (also check that x * 0 == 0) + function mul_test_identity(UD60x18 x) public { + UD60x18 x_1 = x.mul(ONE_FP); + UD60x18 x_0 = x.mul(ZERO_FP); + + assertEq(x_0, ZERO_FP); + assertEq(x_1, x); + } + + // Test that the result increases or decreases depending + // on the value to be added + function mul_test_values(UD60x18 x, UD60x18 y) public { + UD60x18 x_y = x.mul(y); + + if (y.gte(ONE_FP)) { + assertGte(x_y, x); + } else { + assertLte(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // The result of the multiplication must be between the maximum + // and minimum allowed values for UD60x18 + function mul_test_range(UD60x18 x, UD60x18 y) public { + try this.helpersMul(x, y) returns (UD60x18 result) { + assertLte(result, MAX_UD60x18); + } catch { + // If it reverts, just ignore + } + } + + // Multiplying the maximum value times one shouldn't revert, as it is valid + // Moreover, the result must be MAX_UD60x18 + function mul_test_maximum_value() public { + try this.helpersMul(MAX_UD60x18, ONE_FP) returns (UD60x18 result) { + // Expected behaviour, does not revert + assertEq(result, MAX_UD60x18); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION div() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for identity property + // x / 1 == x (equivalent to x / x == 1) + function div_test_division_identity_x_div_1(UD60x18 x) public { + UD60x18 div_1 = div(x, ONE_FP); + + assertEq(x, div_1); + } + + // Test for identity property + // x/x should not revert unless x == 0 + function div_test_division_identity_x_div_x(UD60x18 x) public { + UD60x18 div_x; + + try this.helpersDiv(x, x) { + // This should always equal one + div_x = div(x, x); + assertEq(div_x, ONE_FP); + } catch { + // Only valid case for revert is x == 0 + assertEq(x, ZERO_FP); + } + } + + // Test for division with 0 as numerator + // 0 / x = 0 + function div_test_division_num_zero(UD60x18 y) public { + require(y.neq(ZERO_FP)); + + UD60x18 div_0 = div(ZERO_FP, y); + + assertEq(ZERO_FP, div_0); + } + + // Test that the value of the result increases or + // decreases depending on the denominator's value + function div_test_values(UD60x18 x, UD60x18 y) public { + require(y.neq(ZERO_FP)); + + UD60x18 x_y = div(x, y); + + if (y.gte(ONE_FP)) { + assertLte(x_y, x); + } else { + assertGte(x_y, x); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for division by zero + function div_test_div_by_zero(UD60x18 x) public { + try this.helpersDiv(x, ZERO_FP) { + // Unexpected, this should revert + assert(false); + } catch { + // Expected revert + } + } + + // Test for division by a large value, the result should be less than one + function div_test_maximum_denominator(UD60x18 x) public { + UD60x18 div_large = div(x, MAX_UD60x18); + + assertLte(div_large, ONE_FP); + } + + // Test for division of a large value + // This should revert if |y| < 1 as it would return a value higher than max + function div_test_maximum_numerator(UD60x18 y) public { + require(y.neq(ZERO_FP)); + UD60x18 div_large; + + try this.helpersDiv(MAX_UD60x18, y) { + // If it didn't revert, then |y| >= 1 + div_large = div(MAX_UD60x18, y); + + assertGte(y, ONE_FP); + } catch { + // Expected revert as result is higher than max + } + } + + // Test for values in range + function div_test_range(UD60x18 x, UD60x18 y) public { + require(y.neq(ZERO_FP)); + UD60x18 result; + + try this.helpersDiv(x, y) { + // If it returns a value, it must be in range + result = div(x, y); + assertLte(result, MAX_UD60x18); + } catch { + // Otherwise, it should revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION inv() + + ================================================================ */ + + /* ================================================================ + Tests for mathematical properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the inverse of the inverse is close enough to the + // original number + function inv_test_double_inverse(UD60x18 x) public { + require(x.neq(ZERO_FP)); + + UD60x18 double_inv_x = inv(inv(x)); + + assertEqWithinTolerance(x, double_inv_x, ONE_FP, "1%"); + } + + // Test equivalence with division + function inv_test_division(UD60x18 x) public { + require(x.neq(ZERO_FP)); + + UD60x18 inv_x = inv(x); + UD60x18 div_1_x = div(ONE_FP, x); + + assertEq(inv_x, div_1_x); + } + + // Test the anticommutativity of the division + // x / y == 1 / (y / x) + function inv_test_division_noncommutativity(UD60x18 x, UD60x18 y) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + UD60x18 x_y = div(x, y); + UD60x18 y_x = div(y, x); + + assertEqWithinTolerance(x_y, inv(y_x), ONE_FP, "1%"); + } + + // Test the multiplication of inverses + // 1/(x * y) == 1/x * 1/y + function inv_test_multiplication(UD60x18 x, UD60x18 y) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + UD60x18 inv_x = inv(x); + UD60x18 inv_y = inv(y); + UD60x18 inv_x_times_inv_y = mul(inv_x, inv_y); + + UD60x18 x_y = mul(x, y); + UD60x18 inv_x_y = inv(x_y); + + assertEqWithinTolerance(inv_x_y, inv_x_times_inv_y, ONE_FP, "1%"); + } + + // Test multiplicative identity property + function inv_test_identity(UD60x18 x) public { + require(x.neq(ZERO_FP)); + + UD60x18 inv_x = inv(x); + UD60x18 identity = mul(inv_x, x); + + require(inv_x.neq(ZERO_FP) && identity.neq(ZERO_FP)); + + // They should agree with a tolerance of one percent + assertEqWithinTolerance(identity, ONE_FP, ONE_FP, "1%"); + } + + // Test that the value of the result is in range zero-one + // if x is greater than one, else, the value of the result + // must be greater than one + function inv_test_values(UD60x18 x) public { + require(x.neq(ZERO_FP)); + + UD60x18 inv_x = inv(x); + + if (x.gte(ONE_FP)) { + assertLte(inv_x, ONE_FP); + } else { + assertGt(inv_x, ONE_FP); + } + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test the zero-case, should revert + function inv_test_zero() public { + try this.helpersInv(ZERO_FP) { + // Unexpected, the function must revert + assert(false); + } catch {} + } + + // Test the maximum value case, should not revert, and be close to zero + function inv_test_maximum() public { + UD60x18 inv_maximum; + + try this.helpersInv(MAX_UD60x18) { + inv_maximum = this.helpersInv(MAX_UD60x18); + assertEqWithinBitPrecision(inv_maximum, ZERO_FP, 10); + } catch { + // Unexpected, the function must not revert + assert(false); + } + } + + // Test the minimum value case, should not revert, and be close to 1e36 + function inv_test_minimum() public { + UD60x18 inv_minimum; + + try this.helpersInv(UD60x18.wrap(1)) { + inv_minimum = this.helpersInv(UD60x18.wrap(1)); + assertEqWithinBitPrecision(inv_minimum, UD60x18.wrap(1e36), 10); + } catch { + // Unexpected, the function must not revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION avg() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test that the result is between the two operands + // avg(x, y) >= min(x, y) && avg(x, y) <= max(x, y) + function avg_test_values_in_range(UD60x18 x, UD60x18 y) public { + UD60x18 avg_xy = avg(x, y); + + if (x.gte(y)) { + assertGte(avg_xy, y); + assertLte(avg_xy, x); + } else { + assertGte(avg_xy, x); + assertLte(avg_xy, y); + } + } + + // Test that the average of the same number is itself + // avg(x, x) == x + function avg_test_one_value(UD60x18 x) public { + UD60x18 avg_x = avg(x, x); + + assertEq(avg_x, x); + } + + // Test that the order of operands is irrelevant + // avg(x, y) == avg(y, x) + function avg_test_operand_order(UD60x18 x, UD60x18 y) public { + UD60x18 avg_xy = avg(x, y); + UD60x18 avg_yx = avg(y, x); + + assertEq(avg_xy, avg_yx); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for the maximum value + function avg_test_maximum() public { + UD60x18 result; + + // This may revert due to overflow depending on implementation + // If it doesn't revert, the result must be MAX_UD60x18 + try this.helpersAvg(MAX_UD60x18, MAX_UD60x18) { + result = this.helpersAvg(MAX_UD60x18, MAX_UD60x18); + assertEq(result, MAX_UD60x18); + } catch { + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION pow() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for zero exponent + // x ** 0 == 1 + function pow_test_zero_exponent(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_POW)); + UD60x18 x_pow_0 = pow(x, ZERO_FP); + + assertEq(x_pow_0, ONE_FP); + } + + // Test for zero base + // 0 ** y == 0 (for positive y) + function pow_test_zero_base(UD60x18 y) public { + require(y.lte(MAX_PERMITTED_POW)); + require(y.neq(ZERO_FP)); + + UD60x18 zero_pow_y = pow(ZERO_FP, y); + + assertEq(zero_pow_y, ZERO_FP); + } + + // Test for exponent one + // x ** 1 == x + function pow_test_one_exponent(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_POW)); + UD60x18 x_pow_1 = pow(x, ONE_FP); + + assertEq(x_pow_1, x); + } + + // Test for base one + // 1 ** y == 1 + function pow_test_base_one(UD60x18 y) public { + UD60x18 one_pow_y = pow(ONE_FP, y); + + assertEq(one_pow_y, ONE_FP); + } + + // Test for product of powers of the same base + // x ** a * x ** b == x ** (a + b) + function pow_test_product_same_base( + UD60x18 x, + UD60x18 a, + UD60x18 b + ) public { + require(x.neq(ZERO_FP)); + require(x.lte(MAX_PERMITTED_POW)); + + UD60x18 x_a = pow(x, a); + UD60x18 x_b = pow(x, b); + UD60x18 x_ab = pow(x, a.add(b)); + + assertEqWithinDecimalPrecision(mul(x_a, x_b), x_ab, 9); + } + + // Test for power of an exponentiation + // (x ** a) ** b == x ** (a * b) + function pow_test_power_of_an_exponentiation( + UD60x18 x, + UD60x18 a, + UD60x18 b + ) public { + require(x.neq(ZERO_FP)); + require(x.lte(MAX_PERMITTED_POW)); + + UD60x18 x_a = pow(x, a); + UD60x18 x_a_b = pow(x_a, b); + UD60x18 x_ab = pow(x, a.mul(b)); + + assertEqWithinDecimalPrecision(x_a_b, x_ab, 9); + } + + // Test for power of a product + // (x * y) ** a == x ** a * y ** a + function pow_test_product_power(UD60x18 x, UD60x18 y, UD60x18 a) public { + require(x.lte(MAX_PERMITTED_POW)); + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + require(a.gt(UD60x18.wrap(1e9))); // to avoid massive loss of precision + + UD60x18 x_y = mul(x, y); + UD60x18 xy_a = pow(x_y, a); + + UD60x18 x_a = pow(x, a); + UD60x18 y_a = pow(y, a); + + assertEqWithinDecimalPrecision(mul(x_a, y_a), xy_a, 9); + } + + // Test for result being greater than or lower than the argument, depending on + // its value and the value of the exponent + function pow_test_values(UD60x18 x, UD60x18 a) public { + require(x.neq(ZERO_FP)); + require(x.lte(MAX_PERMITTED_POW)); + + UD60x18 x_a = pow(x, a); + + if (x.gte(ONE_FP)) { + assertGte(x_a, ONE_FP); + } + + if (x.lte(ONE_FP)) { + assertLte(x_a, ONE_FP); + } + } + + // Power is strictly increasing + // x > y && a >= 0 --> pow(x) >= pow(y) + function pow_test_strictly_increasing( + UD60x18 x, + UD60x18 y, + UD60x18 a + ) public { + require(x.gt(y) && x.lte(MAX_PERMITTED_POW)); + require(a.gte(ZERO_FP)); + + UD60x18 x_a = pow(x, a); + UD60x18 y_a = pow(y, a); + + assertGte(x_a, y_a); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for maximum base and exponent > 1 + function pow_test_maximum_base(UD60x18 a) public { + require(a.gt(ONE_FP)); + + try this.helpersPow(MAX_UD60x18, a) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for maximum exponent and base > 1 + function pow_test_maximum_exponent(UD60x18 x) public { + require(x.gt(ONE_FP)); + require(x.lte(MAX_PERMITTED_POW)); + + try this.helpersPow(x, MAX_PERMITTED_POW) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for base < 1 and high exponent + function pow_test_high_exponent(UD60x18 x, UD60x18 a) public { + require(x.lt(ONE_FP) && a.gt(convert(2 ** 64))); + + UD60x18 result = pow(x, a); + + assertEq(result, ZERO_FP); + } + + /* ================================================================ + + TESTS FOR FUNCTION sqrt() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for the inverse operation + // sqrt(x) * sqrt(x) == x + function sqrt_test_inverse_mul(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_SQRT)); + UD60x18 sqrt_x = sqrt(x); + UD60x18 sqrt_x_squared = mul(sqrt_x, sqrt_x); + + // Precision loss is at most half the bits of the operand + assertEqWithinBitPrecision( + sqrt_x_squared, + x, + (intoUint256(log2(x)) >> 1) + 2 + ); + } + + // Test for the inverse operation + // sqrt(x) ** 2 == x + function sqrt_test_inverse_pow(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_SQRT)); + UD60x18 sqrt_x = sqrt(x); + UD60x18 sqrt_x_squared = pow(sqrt_x, convert(2)); + + // Precision loss is at most half the bits of the operand + assertEqWithinBitPrecision( + sqrt_x_squared, + x, + (intoUint256(log2(x)) >> 1) + 2 + ); + } + + // Test for distributive property respect to the multiplication + // sqrt(x) * sqrt(y) == sqrt(x * y) + function sqrt_test_distributive(UD60x18 x, UD60x18 y) public { + require(x.lte(MAX_PERMITTED_SQRT) && y.lte(MAX_PERMITTED_SQRT)); + UD60x18 sqrt_x = sqrt(x); + UD60x18 sqrt_y = sqrt(y); + UD60x18 sqrt_x_sqrt_y = mul(sqrt_x, sqrt_y); + UD60x18 sqrt_xy = sqrt(mul(x, y)); + + // Allow an error of up to one tenth of a percent + assertEqWithinTolerance(sqrt_x_sqrt_y, sqrt_xy, ONE_TENTH_FP, "0.1%"); + } + + // Test that sqrt is strictly increasing + // x >= 0 && y > x --> sqrt(y) >= sqrt(x) + function sqrt_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(x)); + + UD60x18 sqrt_x = sqrt(x); + UD60x18 sqrt_y = sqrt(y); + + assertGte(sqrt_y, sqrt_x); + } + + // Square root of perfect square should be equal to x + // sqrt(x * x) == x + function sqrt_test_square(UD60x18 x) public { + require(x.gt(ONE_FP)); + + UD60x18 square_x = x.mul(x); + UD60x18 sqrt_square_x = sqrt(square_x); + + assertEqWithinDecimalPrecision(sqrt_square_x, x, 2); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + function sqrt_test_zero() public { + assertEq(sqrt(ZERO_FP), ZERO_FP); + } + + // Test for maximum value + function sqrt_test_maximum() public { + try this.helpersSqrt(MAX_PERMITTED_SQRT) { + // Expected behaviour, MAX_SQRT is positive, and operation + // should not revert as the result is in range + } catch { + // Unexpected, should not revert + assert(false); + } + } + + /* ================================================================ + + TESTS FOR FUNCTION log2() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // log2(x * y) = log2(x) + log2(y) + function log2_test_distributive_mul(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + + UD60x18 log2_x = log2(x); + UD60x18 log2_y = log2(y); + UD60x18 log2_x_log2_y = add(log2_x, log2_y); + + UD60x18 xy = mul(x, y); + UD60x18 log2_xy = log2(xy); + + assertEqWithinTolerance(log2_x_log2_y, log2_xy, ONE_FP, "1%"); + } + + // Test for logarithm of a power + // log2(x ** y) = y * log2(x) + function log2_test_power(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + + UD60x18 x_y = pow(x, y); + UD60x18 log2_x_y = log2(x_y); + UD60x18 y_log2_x = mul(log2(x), y); + + assertEqWithinTolerance(y_log2_x, log2_x_y, ONE_FP, "1%"); + } + + // Base 2 logarithm is strictly increasing + // y > 0 && x > y --> log2(x) > log2(y) + function log2_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + UD60x18 log2_x = log2(x); + UD60x18 log2_y = log2(y); + + assertGte(log2_x, log2_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function log2_test_zero() public { + try this.helpersLog2(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function log2_test_maximum() public { + UD60x18 result; + + try this.helpersLog2(MAX_UD60x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLog2(MAX_UD60x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for values less than UNIT, should revert as result would be negative + function log2_test_less_than_unit(UD60x18 x) public { + require(x.lt(UNIT)); + + try this.helpersLog2(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION ln() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // ln(x * y) = ln(x) + ln(y) + function ln_test_distributive_mul(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + + UD60x18 ln_x = ln(x); + UD60x18 ln_y = ln(y); + UD60x18 ln_x_ln_y = add(ln_x, ln_y); + + UD60x18 xy = mul(x, y); + UD60x18 ln_xy = ln(xy); + + assertEqWithinTolerance(ln_xy, ln_x_ln_y, ONE_FP, "1%"); + } + + // Test for logarithm of a power + // ln(x ** y) = y * ln(x) + function ln_test_power(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT)); + UD60x18 x_y = pow(x, y); + UD60x18 ln_x_y = ln(x_y); + UD60x18 y_ln_x = mul(ln(x), y); + + assertEqWithinTolerance(ln_x_y, y_ln_x, ONE_FP, "1%"); + } + + // Natural logarithm is strictly increasing + // y > 0 && x > y --> log2(x) > log2(y) + function ln_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + UD60x18 ln_x = ln(x); + UD60x18 ln_y = ln(y); + + assertGte(ln_x, ln_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function ln_test_zero() public { + try this.helpersLn(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function ln_test_maximum() public { + UD60x18 result; + + try this.helpersLn(MAX_UD60x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLn(MAX_UD60x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for values less than UNIT, should revert since result would be negative + function ln_test_less_than_unit(UD60x18 x) public { + require(x.lt(UNIT)); + + try this.helpersLn(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION exp2() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for equality with pow(2, x) for integer x + // pow(2, x) == exp2(x) + function exp2_test_equivalence_pow(UD60x18 x) public { + require(x.lte(MAX_PERMITTED_EXP2)); + UD60x18 exp2_x = exp2(x); + UD60x18 pow_2_x = pow(TWO_FP, x); + + assertEq(exp2_x, pow_2_x); + } + + // Test for inverse function + // If y = log2(x) then exp2(y) == x + function exp2_test_inverse(UD60x18 x) public { + require(x.gte(UNIT)); + UD60x18 log2_x = log2(x); + require(log2_x.lte(MAX_PERMITTED_EXP2)); + UD60x18 exp2_x = exp2(log2_x); + + assertEqWithinTolerance(x, exp2_x, ONE_TENTH_FP, "0.1%"); + } + + // Test that exp2 strictly increases + // y > x && y < MAX --> exp2(y) > exp2(x) + function exp2_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(x) && y.lte(MAX_PERMITTED_EXP2)); + + UD60x18 exp_x = exp(x); + UD60x18 exp_y = exp(y); + + assertGte(exp_y, exp_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + // exp2(0) == 1 + function exp2_test_zero() public { + UD60x18 exp_zero = exp2(ZERO_FP); + assertEq(exp_zero, ONE_FP); + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp2_test_maximum() public { + try this.helpersExp2(convert(192)) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION exp() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for inverse function + // If y = ln(x) then exp(y) == x + function exp_test_inverse(UD60x18 x) public { + require(x.gte(UNIT)); + UD60x18 ln_x = ln(x); + UD60x18 exp_x = exp(ln_x); + require(exp_x.lte(MAX_PERMITTED_EXP)); + UD60x18 log2_x = log2(x); + + assertEqWithinDecimalPrecision(x, exp_x, 9); + } + + // Test that exp strictly increases + // y <= MAX && y.gt(x) --> exp(y) >= exp(x) + function exp_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(x) && y.lte(MAX_PERMITTED_EXP)); + + UD60x18 exp_x = exp(x); + UD60x18 exp_y = exp(y); + + assertGte(exp_y, exp_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case + // exp(0) == 1 + function exp_test_zero() public { + UD60x18 exp_zero = exp(ZERO_FP); + assertEq(exp_zero, ONE_FP); + } + + // Test for maximum value. This should overflow as it won't fit + // in the data type + function exp_test_maximum() public { + try this.helpersExp(convert(192)) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert + } + } + + /* ================================================================ + + TESTS FOR FUNCTION powu() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for zero exponent + // x ** 0 == 1 + function powu_test_zero_exponent(UD60x18 x) public { + UD60x18 x_pow_0 = powu(x, 0); + + assertEq(x_pow_0, ONE_FP); + } + + // Test for zero base + // 0 ** x == 0 + function powu_test_zero_base(uint256 a) public { + require(a != 0); + + UD60x18 zero_pow_a = powu(ZERO_FP, a); + + assertEq(zero_pow_a, ZERO_FP); + } + + // Test for exponent one + // x ** 1 == x + function powu_test_one_exponent(UD60x18 x) public { + UD60x18 x_pow_1 = powu(x, 1); + + assertEq(x_pow_1, x); + } + + // Test for base one + // 1 ** x == 1 + function powu_test_base_one(uint256 a) public { + UD60x18 one_pow_a = powu(ONE_FP, a); + + assertEq(one_pow_a, ONE_FP); + } + + // Test for product of powers of the same base + // x ** a * x ** b == x ** (a + b) + function powu_test_product_same_base( + UD60x18 x, + uint256 a, + uint256 b + ) public { + require(x.neq(ZERO_FP)); + + UD60x18 x_a = powu(x, a); + UD60x18 x_b = powu(x, b); + UD60x18 x_ab = powu(x, a + b); + + assertEqWithinBitPrecision(mul(x_a, x_b), x_ab, 10); + } + + // Test for power of an exponentiation + // (x ** a) ** b == x ** (a * b) + function powu_test_power_of_an_exponentiation( + UD60x18 x, + uint256 a, + uint256 b + ) public { + require(x.neq(ZERO_FP)); + + UD60x18 x_a = powu(x, a); + UD60x18 x_a_b = powu(x_a, b); + UD60x18 x_ab = powu(x, a * b); + + assertEqWithinBitPrecision(x_a_b, x_ab, 10); + } + + // Test for power of a product + // (x * y) ** a == x ** a * y ** a + function powu_test_product_power(UD60x18 x, UD60x18 y, uint256 a) public { + require(x.neq(ZERO_FP) && y.neq(ZERO_FP)); + + require(a > 1e9); // to avoid massive loss of precision + + UD60x18 x_y = mul(x, y); + UD60x18 xy_a = powu(x_y, a); + + UD60x18 x_a = powu(x, a); + UD60x18 y_a = powu(y, a); + + assertEqWithinBitPrecision(mul(x_a, y_a), xy_a, 10); + } + + // Test for result being greater than or lower than the argument, depending on + // its value and the value of the exponent + function powu_test_values(UD60x18 x, uint256 a) public { + require(x.neq(ZERO_FP)); + + UD60x18 x_a = powu(x, a); + + if (x.gte(ONE_FP)) { + assertGte(x_a, ONE_FP); + } + + if (x.lte(ONE_FP)) { + assertLte(x_a, ONE_FP); + } + } + + // Unsigned power is strictly increasing + // y > MIN && x > y && a > 0 --> powu(x, a) > powu(y, a) + function powu_test_strictly_increasing( + UD60x18 x, + UD60x18 y, + uint256 a + ) public { + require(x.gt(y) && y.gt(ZERO_FP)); + require(x.lte(MAX_UD60x18)); + require(a > 0); + + UD60x18 x_a = powu(x, a); + UD60x18 y_a = powu(y, a); + + assertGte(x_a, y_a); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for maximum base and exponent > 1 + function powu_test_maximum_base(uint256 a) public { + require(a > 1); + + try this.helpersPowu(MAX_UD60x18, a) { + // Unexpected, should revert because of overflow + assert(false); + } catch { + // Expected revert + } + } + + // Test for base < 1 and high exponent + function powu_test_high_exponent(UD60x18 x, uint256 a) public { + require(x.lt(ONE_FP) && a > 2 ** 64); + + UD60x18 result = powu(x, a); + + assertEq(result, ZERO_FP); + } + + /* ================================================================ + + TESTS FOR FUNCTION log10() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // Test for distributive property respect to multiplication + // log10(x * y) = log10(x) + log10(y) + function log10_test_distributive_mul(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + UD60x18 log10_x = log10(x); + UD60x18 log10_y = log10(y); + UD60x18 log10_x_log10_y = add(log10_x, log10_y); + + UD60x18 xy = mul(x, y); + UD60x18 log10_xy = log10(xy); + + assertEqWithinTolerance(log10_x_log10_y, log10_xy, ONE_FP, "1%"); + } + + // Test for logarithm of a power + // log10(x ** y) = y * log10(x) + function log10_test_power(UD60x18 x, UD60x18 y) public { + require(x.gte(UNIT) && y.gte(UNIT)); + UD60x18 x_y = pow(x, y); + UD60x18 log10_x_y = log10(x_y); + UD60x18 y_log10_x = mul(log10(x), y); + + assertEqWithinTolerance(log10_x_y, y_log10_x, ONE_FP, "1%"); + } + + // Base 10 logarithm is strictly increasing + // x > y && y > 0 --> log10(x) > log10(y) + function log10_test_strictly_increasing(UD60x18 x, UD60x18 y) public { + require(y.gt(ZERO_FP) && x.gt(y)); + + UD60x18 log10_x = log10(x); + UD60x18 log10_y = log10(y); + + assertGt(log10_x, log10_y); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should revert + function log10_test_zero() public { + try this.helpersLog10(ZERO_FP) { + // Unexpected, should revert + assert(false); + } catch { + // Expected revert, log(0) is not defined + } + } + + // Test for maximum value case, should return a positive number + function log10_test_maximum() public { + UD60x18 result; + + try this.helpersLog10(MAX_UD60x18) { + // Expected, should not revert and the result must be > 0 + result = this.helpersLog10(MAX_UD60x18); + assertGt(result, ZERO_FP); + } catch { + // Unexpected + assert(false); + } + } + + // Test for values less than UNIT, should revert as result would be negative + function log10_test_less_than_unit(UD60x18 x) public { + require(x.lt(UNIT)); + + try this.helpersLog10(x) { + // Unexpected, should revert + assert(false); + } catch { + // Expected + } + } + + /* ================================================================ + + TESTS FOR FUNCTION gm() + + ================================================================ */ + + /* ================================================================ + Tests for arithmetic properties. + These should make sure that the implemented function complies + with math rules and expected behaviour. + ================================================================ */ + + // The product of the values should be equal to the geometric mean + // raised to the power of N (numbers in the set) + function gm_test_product(UD60x18 x, UD60x18 y) public { + UD60x18 x_mul_y = x.mul(y); + UD60x18 gm_squared = pow(gm(x, y), TWO_FP); + + assertEqWithinTolerance(x_mul_y, gm_squared, ONE_TENTH_FP, "0.1%"); + } + + // The geometric mean for a set of positive numbers is less than the + // arithmetic mean of that set, as long as the values of the set are not equal + function gm_test_positive_set_avg(UD60x18 x, UD60x18 y) public { + require(x.neq(y)); + + UD60x18 gm_x_y = gm(x, y); + UD60x18 avg_x_y = avg(x, y); + + assertLte(gm_x_y, avg_x_y); + } + + // The geometric mean of a set of positive equal numbers should be + // equal to the arithmetic mean + function gm_test_positive_equal_set_avg(UD60x18 x) public { + UD60x18 gm_x = gm(x, x); + UD60x18 avg_x = avg(x, x); + + assertEq(gm_x, avg_x); + } + + /* ================================================================ + Tests for overflow and edge cases. + These will make sure that the function reverts on overflow and + behaves correctly on edge cases + ================================================================ */ + + // Test for zero case, should return 0 + function gm_test_zero(UD60x18 x) public { + require(x.gte(ZERO_FP)); + + try this.helpersGm(x, ZERO_FP) { + UD60x18 result = gm(x, ZERO_FP); + assertEq(result, ZERO_FP); + } catch { + // Unexpected, should not revert + assert(false); + } + } +} diff --git a/contracts/Math/PRBMath/v4/utils/AssertionHelperSD.sol b/contracts/Math/PRBMath/v4/utils/AssertionHelperSD.sol new file mode 100644 index 0000000..3fdf947 --- /dev/null +++ b/contracts/Math/PRBMath/v4/utils/AssertionHelperSD.sol @@ -0,0 +1,333 @@ +pragma solidity ^0.8.0; + +import {SD59x18} from "@prb/math/SD59x18.sol"; + +import {convert} from "@prb/math/sd59x18/Conversions.sol"; +import {add, sub, eq, gt, gte, lt, lte, rshift} from "@prb/math/sd59x18/Helpers.sol"; +import {mul, div, abs} from "@prb/math/sd59x18/Math.sol"; + +abstract contract AssertionHelperSD { + event AssertEqFailure(string); + event AssertGtFailure(string); + event AssertGteFailure(string); + event AssertLtFailure(string); + event AssertLteFailure(string); + + function assertEq(SD59x18 a, SD59x18 b) internal { + if (!a.eq(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + ". No precision loss allowed." + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertEqWithinBitPrecision( + SD59x18 a, + SD59x18 b, + uint256 precision_bits + ) internal { + SD59x18 max = gt(a, b) ? a : b; + SD59x18 min = gt(a, b) ? b : a; + SD59x18 r = rshift(sub(max, min), precision_bits); + + if (!eq(r, convert(0))) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + string memory str_bits = toString(precision_bits); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_bits, + " bits of precision" + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertEqWithinTolerance( + SD59x18 a, + SD59x18 b, + SD59x18 error_percent, + string memory str_percent + ) internal { + SD59x18 tol_value = mul(a, div(error_percent, convert(100))); + + require(tol_value.neq(convert(0))); + + if (!lte(sub(b, a), abs(tol_value))) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + string memory tolerance = toString(SD59x18.unwrap(abs(tol_value))); + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_percent, + " tolerance: ", + tolerance + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + // Returns true if the n most significant bits of a and b are almost equal + // Uses functions from the library under test! + function assertEqWithinDecimalPrecision( + SD59x18 a, + SD59x18 b, + uint256 digits + ) internal { + // Divide both number by digits to truncate the unimportant digits + int256 a_int = SD59x18.unwrap(a); + int256 b_int = SD59x18.unwrap(b); + + int256 denominator = int256(10 ** digits); + + int256 a_significant = a_int / denominator; + int256 b_significant = b_int / denominator; + + int256 larger = a_significant > b_significant + ? a_significant + : b_significant; + int256 smaller = a_significant > b_significant + ? b_significant + : a_significant; + + if (!((larger - smaller) <= 1)) { + string memory str_a = toString(a_int); + string memory str_b = toString(b_int); + string memory str_larger = toString(larger); + string memory str_smaller = toString(smaller); + string memory str_digits = toString(digits); + string memory difference = toString(larger - smaller); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_digits, + " digits of precision. Difference: ", + difference, + ", truncated input:", + str_larger, + " != ", + str_smaller + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertGt(SD59x18 a, SD59x18 b, string memory reason) internal { + if (!a.gt(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " <= ", + str_b, + ", reason: ", + reason + ); + emit AssertGtFailure(string(assertMsg)); + assert(false); + } + } + + function assertGt(SD59x18 a, SD59x18 b) internal { + if (!a.gt(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " <= ", + str_b + ); + emit AssertGtFailure(string(assertMsg)); + assert(false); + } + } + + function assertGte(SD59x18 a, SD59x18 b, string memory reason) internal { + if (!a.gte(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " < ", + str_b, + ", reason: ", + reason + ); + emit AssertGteFailure(string(assertMsg)); + assert(false); + } + } + + function assertGte(SD59x18 a, SD59x18 b) internal { + if (!a.gte(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " < ", + str_b + ); + emit AssertGteFailure(string(assertMsg)); + assert(false); + } + } + + function assertLt(SD59x18 a, SD59x18 b, string memory reason) internal { + if (!a.lt(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " >= ", + str_b, + ", reason: ", + reason + ); + emit AssertLtFailure(string(assertMsg)); + assert(false); + } + } + + function assertLt(SD59x18 a, SD59x18 b) internal { + if (!a.lt(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " >= ", + str_b + ); + emit AssertLtFailure(string(assertMsg)); + assert(false); + } + } + + function assertLte(SD59x18 a, SD59x18 b, string memory reason) internal { + if (!a.lte(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " > ", + str_b, + ", reason: ", + reason + ); + emit AssertLteFailure(string(assertMsg)); + assert(false); + } + } + + function assertLte(SD59x18 a, SD59x18 b) internal { + if (!a.lte(b)) { + string memory str_a = toString(SD59x18.unwrap(a)); + string memory str_b = toString(SD59x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " > ", + str_b + ); + emit AssertLteFailure(string(assertMsg)); + assert(false); + } + } + + function toString(int256 value) internal pure returns (string memory str) { + uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); + str = toString(absValue); + + if (value < 0) { + str = string(abi.encodePacked("-", str)); + } + } + + function toString(uint256 value) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes + // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the + // trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. + let newFreeMemoryPointer := add(mload(0x40), 160) + + // Update the free memory pointer to avoid overriding our string. + mstore(0x40, newFreeMemoryPointer) + + // Assign str to the end of the zone of newly allocated memory. + str := sub(newFreeMemoryPointer, 32) + + // Clean the last word of memory it may not be overwritten. + mstore(str, 0) + + // Cache the end of the memory to calculate the length later. + let end := str + + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + // prettier-ignore + for { let temp := value } 1 {} { + // Move the pointer 1 byte to the left. + str := sub(str, 1) + + // Write the character to the pointer. + // The ASCII index of the '0' character is 48. + mstore8(str, add(48, mod(temp, 10))) + + // Keep dividing temp until zero. + temp := div(temp, 10) + + // prettier-ignore + if iszero(temp) { break } + } + + // Compute and cache the final total length of the string. + let length := sub(end, str) + + // Move the pointer 32 bytes leftwards to make room for the length. + str := sub(str, 32) + + // Store the string's length at the start of memory allocated for our string. + mstore(str, length) + } + } +} diff --git a/contracts/Math/PRBMath/v4/utils/AssertionHelperUD.sol b/contracts/Math/PRBMath/v4/utils/AssertionHelperUD.sol new file mode 100644 index 0000000..799d391 --- /dev/null +++ b/contracts/Math/PRBMath/v4/utils/AssertionHelperUD.sol @@ -0,0 +1,331 @@ +pragma solidity ^0.8.0; + +import {UD60x18} from "@prb/math/UD60x18.sol"; + +import {convert} from "@prb/math/ud60x18/Conversions.sol"; +import {add, sub, eq, gt, gte, lt, lte, rshift} from "@prb/math/ud60x18/Helpers.sol"; +import {mul, div, ln, exp, exp2, log2, sqrt, pow, avg, inv, log10, floor, powu, gm} from "@prb/math/ud60x18/Math.sol"; + +abstract contract AssertionHelperUD { + event AssertEqFailure(string); + event AssertGtFailure(string); + event AssertGteFailure(string); + event AssertLtFailure(string); + event AssertLteFailure(string); + + function assertEq(UD60x18 a, UD60x18 b) internal { + if (!a.eq(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + ". No precision loss allowed." + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertEqWithinBitPrecision( + UD60x18 a, + UD60x18 b, + uint256 precision_bits + ) internal { + UD60x18 max = gt(a, b) ? a : b; + UD60x18 min = gt(a, b) ? b : a; + UD60x18 r = rshift(sub(max, min), precision_bits); + + if (!eq(r, convert(0))) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + string memory str_bits = toString(precision_bits); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_bits, + " bits of precision" + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertEqWithinTolerance( + UD60x18 a, + UD60x18 b, + UD60x18 error_percent, + string memory str_percent + ) internal { + UD60x18 tol_value = mul(a, div(error_percent, convert(100))); + + require(tol_value.neq(convert(0))); + + if (!lte(sub(b, a), tol_value)) { + string memory ua = toString(UD60x18.unwrap(a)); + string memory ub = toString(UD60x18.unwrap(b)); + string memory tolerance = toString(UD60x18.unwrap(tol_value)); + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + ua, + " != ", + ub, + " within ", + str_percent, + " tolerance: ", + tolerance + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + // Returns true if the n most significant bits of a and b are almost equal + // Uses functions from the library under test! + function assertEqWithinDecimalPrecision( + UD60x18 a, + UD60x18 b, + uint256 digits + ) internal { + // Divide both number by digits to truncate the unimportant digits + uint256 a_uint = UD60x18.unwrap(a); + uint256 b_uint = UD60x18.unwrap(b); + + uint256 a_significant = a_uint / 10 ** digits; + uint256 b_significant = b_uint / 10 ** digits; + + uint256 larger = a_significant > b_significant + ? a_significant + : b_significant; + uint256 smaller = a_significant > b_significant + ? b_significant + : a_significant; + + if (!((larger - smaller) <= 1)) { + string memory str_a = toString(a_uint); + string memory str_b = toString(b_uint); + string memory str_digits = toString(digits); + string memory str_larger = toString(larger); + string memory str_smaller = toString(smaller); + string memory difference = toString(larger - smaller); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " != ", + str_b, + " within ", + str_digits, + " digits of precision. Difference: ", + difference, + ", truncated input:", + str_larger, + " != ", + str_smaller + ); + emit AssertEqFailure(string(assertMsg)); + assert(false); + } + } + + function assertGt(UD60x18 a, UD60x18 b, string memory reason) internal { + if (!a.gt(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " <= ", + str_b, + ", reason: ", + reason + ); + emit AssertGtFailure(string(assertMsg)); + assert(false); + } + } + + function assertGt(UD60x18 a, UD60x18 b) internal { + if (!a.gt(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " <= ", + str_b + ); + emit AssertGtFailure(string(assertMsg)); + assert(false); + } + } + + function assertGte(UD60x18 a, UD60x18 b, string memory reason) internal { + if (!a.gte(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " < ", + str_b, + ", reason: ", + reason + ); + emit AssertGteFailure(string(assertMsg)); + assert(false); + } + } + + function assertGte(UD60x18 a, UD60x18 b) internal { + if (!a.gte(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " < ", + str_b + ); + emit AssertGteFailure(string(assertMsg)); + assert(false); + } + } + + function assertLt(UD60x18 a, UD60x18 b, string memory reason) internal { + if (!a.lt(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " >= ", + str_b, + ", reason: ", + reason + ); + emit AssertLtFailure(string(assertMsg)); + assert(false); + } + } + + function assertLt(UD60x18 a, UD60x18 b) internal { + if (!a.lt(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " >= ", + str_b + ); + emit AssertLtFailure(string(assertMsg)); + assert(false); + } + } + + function assertLte(UD60x18 a, UD60x18 b, string memory reason) internal { + if (!a.lte(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " > ", + str_b, + ", reason: ", + reason + ); + emit AssertLteFailure(string(assertMsg)); + assert(false); + } + } + + function assertLte(UD60x18 a, UD60x18 b) internal { + if (!a.lte(b)) { + string memory str_a = toString(UD60x18.unwrap(a)); + string memory str_b = toString(UD60x18.unwrap(b)); + + bytes memory assertMsg = abi.encodePacked( + "Invalid: ", + str_a, + " > ", + str_b + ); + emit AssertLteFailure(string(assertMsg)); + assert(false); + } + } + + function toString(int256 value) internal pure returns (string memory str) { + uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); + str = toString(absValue); + + if (value < 0) { + str = string(abi.encodePacked("-", str)); + } + } + + function toString(uint256 value) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes + // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the + // trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. + let newFreeMemoryPointer := add(mload(0x40), 160) + + // Update the free memory pointer to avoid overriding our string. + mstore(0x40, newFreeMemoryPointer) + + // Assign str to the end of the zone of newly allocated memory. + str := sub(newFreeMemoryPointer, 32) + + // Clean the last word of memory it may not be overwritten. + mstore(str, 0) + + // Cache the end of the memory to calculate the length later. + let end := str + + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + // prettier-ignore + for { let temp := value } 1 {} { + // Move the pointer 1 byte to the left. + str := sub(str, 1) + + // Write the character to the pointer. + // The ASCII index of the '0' character is 48. + mstore8(str, add(48, mod(temp, 10))) + + // Keep dividing temp until zero. + temp := div(temp, 10) + + // prettier-ignore + if iszero(temp) { break } + } + + // Compute and cache the final total length of the string. + let length := sub(end, str) + + // Move the pointer 32 bytes leftwards to make room for the length. + str := sub(str, 32) + + // Store the string's length at the start of memory allocated for our string. + mstore(str, length) + } + } +} diff --git a/contracts/Math/echidna-config.yaml b/contracts/Math/echidna-config.yaml new file mode 100644 index 0000000..31f2af4 --- /dev/null +++ b/contracts/Math/echidna-config.yaml @@ -0,0 +1,6 @@ +corpusDir: "echidna-corpus" +testMode: assertion +testLimit: 100000 +deployer: "0x10000" +sender: ["0x10000", "0x20000", "0x30000"] +cryticArgs: ["--compile-force-framework", "foundry"] diff --git a/hardhat.config.js b/hardhat.config.js index a443bc8..8244d9d 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -26,6 +26,15 @@ module.exports = { runs: 200, }, }, + }, + { + version: "0.8.19", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, }, ], }, diff --git a/lib/prb-math b/lib/prb-math new file mode 160000 index 0000000..7ce3009 --- /dev/null +++ b/lib/prb-math @@ -0,0 +1 @@ +Subproject commit 7ce3009bbfa0d8e2d430b7a1a9ca46b6e706d90d diff --git a/lib/prb-math-v3 b/lib/prb-math-v3 new file mode 160000 index 0000000..1edf08d --- /dev/null +++ b/lib/prb-math-v3 @@ -0,0 +1 @@ +Subproject commit 1edf08dd73eb1ace0042459ba719b8ea4a55c0e0 diff --git a/package-lock.json b/package-lock.json index 836906c..e59ca87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@openzeppelin/contracts": "^4.7.3", + "@prb/math": "^4.0.0", "markdown-link-check": "^3.11.0", "prettier": "^2.8.7", "prettier-plugin-solidity": "^1.1.3", @@ -1027,6 +1028,11 @@ "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.0.tgz", "integrity": "sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw==" }, + "node_modules/@prb/math": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@prb/math/-/math-4.0.0.tgz", + "integrity": "sha512-zGOMIn3EMSxh2yo64uKly4xtwVvbUvsKAand4JFzDRFAPEzqBWJe1/Qf23o992ZR7xeA502L17rSTcOscfRO2Q==" + }, "node_modules/@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -5081,6 +5087,11 @@ "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.0.tgz", "integrity": "sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw==" }, + "@prb/math": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@prb/math/-/math-4.0.0.tgz", + "integrity": "sha512-zGOMIn3EMSxh2yo64uKly4xtwVvbUvsKAand4JFzDRFAPEzqBWJe1/Qf23o992ZR7xeA502L17rSTcOscfRO2Q==" + }, "@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", diff --git a/package.json b/package.json index 58777b9..c1c454a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,12 @@ "format-embedded-solidity": "prettier --write \"**/*.md\" --embedded-language-formatting=auto --plugin prettier-plugin-solidity --tab-width 4 --print-width 120 && prettier --write \"**/*.md\"", "lint": "npm run lint-check-format && npm run lint-check-links", "lint-check-format": "prettier --check .", - "lint-check-links": "find . -name '*.md' -not -path './node_modules/*' -print0 | xargs -0 -n1 markdown-link-check" + "lint-check-links": "find . -name '*.md' -not -path './node_modules/*' -print0 | xargs -0 -n1 markdown-link-check -r", + "echidna-prb-ud-v3": "echidna . --contract CryticPRBMath60x18Propertiesv3 --config ./contracts/Math/echidna-config.yaml --corpus-dir corpus-prb-ud-v3", + "echidna-prb-sd-v3": "echidna . --contract CryticPRBMath59x18Propertiesv3 --config ./contracts/Math/echidna-config.yaml --corpus-dir corpus-prb-sd-v3", + "echidna-prb-ud-v4": "echidna . --contract CryticPRBMath60x18Propertiesv4 --config ./contracts/Math/echidna-config.yaml --corpus-dir corpus-prb-ud-v4", + "echidna-prb-sd-v4": "echidna . --contract CryticPRBMath59x18Propertiesv4 --config ./contracts/Math/echidna-config.yaml --corpus-dir corpus-prb-sd-v4", + "echidna-abdk": "echidna . --contract CryticABDKMath64x64Properties --config ./contracts/Math/echidna-config.yaml --corpus-dir corpus-abdk-64" }, "repository": { "type": "git", @@ -32,4 +37,4 @@ "devDependencies": { "hardhat": "^2.9.3" } -} +} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..b6fa815 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,11 @@ +@prb/test/=lib/prb-math/lib/prb-test/src/ +ERC4626/=lib/ERC4626/contracts/ +ds-test/=lib/forge-std/lib/ds-test/src/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +forge-std/=lib/forge-std/src/ +@openzeppelin/=lib/openzeppelin-contracts/ +@prb-math-v3/=lib/prb-math-v3/src/ +@prb/math/=lib/prb-math/src/ +prb-test/=lib/prb-math/lib/prb-test/src/ +solmate/=lib/solmate/ +src/=lib/prb-math-v3/src/ diff --git a/slither.config.json b/slither.config.json new file mode 100644 index 0000000..ed7a465 --- /dev/null +++ b/slither.config.json @@ -0,0 +1 @@ +{ "no_fail": true } \ No newline at end of file diff --git a/tests/ERC20/foundry/remappings.txt b/tests/ERC20/foundry/remappings.txt index 473e7d7..ccc1909 100644 --- a/tests/ERC20/foundry/remappings.txt +++ b/tests/ERC20/foundry/remappings.txt @@ -1,4 +1,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ properties/=../../../contracts/ -@openzeppelin/=../../../lib/openzeppelin-contracts/ \ No newline at end of file +@openzeppelin/=../../../lib/openzeppelin-contracts/ +src/=src/ \ No newline at end of file diff --git a/tests/ERC4626/foundry/remappings.txt b/tests/ERC4626/foundry/remappings.txt index 25aeca5..c3ae406 100644 --- a/tests/ERC4626/foundry/remappings.txt +++ b/tests/ERC4626/foundry/remappings.txt @@ -1,3 +1,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ -properties/=../../../contracts/ \ No newline at end of file +properties/=../../../contracts/ +solmate/=../../../lib/solmate/src/ +src/=src/ \ No newline at end of file diff --git a/tests/ERC721/foundry/remappings.txt b/tests/ERC721/foundry/remappings.txt index 473e7d7..ccc1909 100644 --- a/tests/ERC721/foundry/remappings.txt +++ b/tests/ERC721/foundry/remappings.txt @@ -1,4 +1,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ properties/=../../../contracts/ -@openzeppelin/=../../../lib/openzeppelin-contracts/ \ No newline at end of file +@openzeppelin/=../../../lib/openzeppelin-contracts/ +src/=src/ \ No newline at end of file