diff --git a/package-lock.json b/package-lock.json index d1904ee..0020057 100644 --- a/package-lock.json +++ b/package-lock.json @@ -160,7 +160,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -971,7 +970,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, diff --git a/src/ERC3009Upgradeable.sol b/src/ERC3009Upgradeable.sol index 626218c..6dca9d8 100644 --- a/src/ERC3009Upgradeable.sol +++ b/src/ERC3009Upgradeable.sol @@ -2,7 +2,6 @@ pragma solidity >=0.8.20 <0.9.0; -import { Initializable } from "../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; import { IERC3009 } from "./interfaces/IERC3009.sol"; import { StatefulERC712Upgradeable } from "./StatefulERC712Upgradeable.sol"; diff --git a/src/libs/ContinuousIndexingMath.sol b/src/libs/ContinuousIndexingMath.sol index ff7b4a0..6367405 100644 --- a/src/libs/ContinuousIndexingMath.sol +++ b/src/libs/ContinuousIndexingMath.sol @@ -2,17 +2,15 @@ pragma solidity >=0.8.20 <0.9.0; -import { IndexingMath } from "./IndexingMath.sol"; - /** * @title Arithmetic library with operations for calculating continuous indexing. - * @author M^0 Labs + * @author M0 Labs */ library ContinuousIndexingMath { /* ============ Variables ============ */ /// @notice The scaling of indexes for exponent math. - uint56 internal constant EXP_SCALED_ONE = IndexingMath.EXP_SCALED_ONE; + uint56 internal constant EXP_SCALED_ONE = 1e12; /// @notice The number of seconds in a year. uint32 internal constant SECONDS_PER_YEAR = 31_536_000; @@ -27,9 +25,7 @@ library ContinuousIndexingMath { * @dev Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) */ function multiplyIndicesDown(uint128 index, uint48 deltaIndex) internal pure returns (uint144) { - unchecked { - return uint144((uint256(index) * deltaIndex) / EXP_SCALED_ONE); - } + return uint144((uint256(index) * deltaIndex) / EXP_SCALED_ONE); } /** @@ -37,9 +33,7 @@ library ContinuousIndexingMath { * @dev Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) */ function multiplyIndicesUp(uint128 index, uint48 deltaIndex) internal pure returns (uint144) { - unchecked { - return uint144((uint256(index) * deltaIndex + (EXP_SCALED_ONE - 1)) / EXP_SCALED_ONE); - } + return uint144((uint256(index) * deltaIndex + (EXP_SCALED_ONE - 1)) / EXP_SCALED_ONE); } /** @@ -49,11 +43,7 @@ library ContinuousIndexingMath { * @dev `type(uint64).max * type(uint32).max / SECONDS_PER_YEAR` fits in a `uint72`. */ function getContinuousIndex(uint64 yearlyRate, uint32 time) internal pure returns (uint48) { - unchecked { - // NOTE: Casting `uint256(yearlyRate) * time` to a `uint72` is safe because the largest value is - // `type(uint64).max * type(uint32).max / SECONDS_PER_YEAR`, which is less than `type(uint72).max`. - return exponent(uint72((uint256(yearlyRate) * time) / SECONDS_PER_YEAR)); - } + return exponent(uint72((uint256(yearlyRate) * time) / SECONDS_PER_YEAR)); } /** @@ -66,24 +56,20 @@ library ContinuousIndexingMath { * @dev Output for a `uint72` input `x` will fit in `uint48` */ function exponent(uint72 x) internal pure returns (uint48) { - // NOTE: This can be done unchecked even for `x = type(uint72).max`. - // Verify by removing `unchecked` and running `test_exponent()`. - unchecked { - uint256 x2 = uint256(x) * x; + uint256 x2 = uint256(x) * x; - // `additiveTerms` is `(1 + 3(x^2)/28 + x^4/1680)`, and scaled by `84e27`. - // NOTE: `84e27` the cleanest and largest scalar, given the various intermediate overflow possibilities. - // NOTE: The resulting `(x2 * x2) / 20e21` term has been split to avoid overflow of `x2 * x2`. - uint256 additiveTerms = 84e27 + (9e3 * x2) + ((x2 / 2e11) * (x2 / 1e11)); + // `additiveTerms` is `(1 + 3(x^2)/28 + x^4/1680)`, and scaled by `84e27`. + // NOTE: `84e27` the cleanest and largest scalar, given the various intermediate overflow possibilities. + // NOTE: The resulting `(x2 * x2) / 20e21` term has been split to avoid overflow of `x2 * x2`. + uint256 additiveTerms = 84e27 + (9e3 * x2) + ((x2 / 2e11) * (x2 / 1e11)); - // `differentTerms` is `(- x/2 - x^3/84)`, but positive (will be subtracted later), and scaled by `84e27`. - uint256 differentTerms = uint256(x) * (42e15 + (x2 / 1e9)); + // `differentTerms` is `(- x/2 - x^3/84)`, but positive (will be subtracted later), and scaled by `84e27`. + uint256 differentTerms = uint256(x) * (42e15 + (x2 / 1e9)); - // Result needs to be scaled by `1e12`. - // NOTE: Can cast to `uint48` because contents can never be larger than `type(uint48).max` for any `x`. - // Max `y` is ~200e12, before falling off. See links above for reference. - return uint48(((additiveTerms + differentTerms) * 1e12) / (additiveTerms - differentTerms)); - } + // Result needs to be scaled by `1e12`. + // NOTE: Can cast to `uint48` because contents can never be larger than `type(uint48).max` for any `x`. + // Max `y` is ~200e12, before falling off. See links above for reference. + return uint48(((additiveTerms + differentTerms) * 1e12) / (additiveTerms - differentTerms)); } /** @@ -92,9 +78,7 @@ library ContinuousIndexingMath { * @return The percentage represented as basis points. */ function convertToBasisPoints(uint64 input) internal pure returns (uint40) { - unchecked { - return uint40((uint256(input) * BPS_SCALED_ONE) / EXP_SCALED_ONE); - } + return uint40((uint256(input) * BPS_SCALED_ONE) / EXP_SCALED_ONE); } /** @@ -103,8 +87,6 @@ library ContinuousIndexingMath { * @return The percentage represented as a scaled 12-decimal number. */ function convertFromBasisPoints(uint32 input) internal pure returns (uint64) { - unchecked { - return uint64((uint256(input) * EXP_SCALED_ONE) / BPS_SCALED_ONE); - } + return uint64((uint256(input) * EXP_SCALED_ONE) / BPS_SCALED_ONE); } } diff --git a/src/libs/IndexingMath.sol b/src/libs/IndexingMath.sol index 31cede7..53f2f1b 100644 --- a/src/libs/IndexingMath.sol +++ b/src/libs/IndexingMath.sol @@ -21,68 +21,14 @@ library IndexingMath { /* ============ Exposed Functions ============ */ - /** - * @notice Helper function to calculate `(x * EXP_SCALED_ONE) / y`, rounded down. - * @dev Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) - */ - function divide240By128Down(uint240 x, uint128 y) internal pure returns (uint112) { - if (y == 0) revert DivisionByZero(); - - unchecked { - // NOTE: While `uint256(x) * EXP_SCALED_ONE` can technically overflow, these divide/multiply functions are - // only used for the purpose of principal/present amount calculations for continuous indexing, and - // so for an `x` to be large enough to overflow this, it would have to be a possible result of - // `multiply112By128Down` or `multiply112By128Up`, which would already satisfy - // `uint256(x) * EXP_SCALED_ONE < type(uint240).max`. - return UIntMath.safe112((uint256(x) * EXP_SCALED_ONE) / y); - } - } - - /** - * @notice Helper function to calculate `(x * EXP_SCALED_ONE) / y`, rounded up. - * @dev Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) - */ - function divide240By128Up(uint240 x, uint128 y) internal pure returns (uint112) { - if (y == 0) revert DivisionByZero(); - - unchecked { - // NOTE: While `uint256(x) * EXP_SCALED_ONE` can technically overflow, these divide/multiply functions are - // only used for the purpose of principal/present amount calculations for continuous indexing, and - // so for an `x` to be large enough to overflow this, it would have to be a possible result of - // `multiply112By128Down` or `multiply112By128Up`, which would already satisfy - // `uint256(x) * EXP_SCALED_ONE < type(uint240).max`. - return UIntMath.safe112(((uint256(x) * EXP_SCALED_ONE) + y - 1) / y); - } - } - - /** - * @notice Helper function to calculate `(x * y) / EXP_SCALED_ONE`, rounded down. - * @dev Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) - */ - function multiply112By128Down(uint112 x, uint128 y) internal pure returns (uint240) { - unchecked { - return uint240((uint256(x) * y) / EXP_SCALED_ONE); - } - } - - /** - * @notice Helper function to calculate `(x * index) / EXP_SCALED_ONE`, rounded up. - * @dev Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) - */ - function multiply112By128Up(uint112 x, uint128 index) internal pure returns (uint240 z) { - unchecked { - return uint240(((uint256(x) * index) + (EXP_SCALED_ONE - 1)) / EXP_SCALED_ONE); - } - } - /** * @dev Returns the present amount (rounded down) given the principal amount and an index. * @param principalAmount The principal amount. * @param index An index. * @return The present amount rounded down. */ - function getPresentAmountRoundedDown(uint112 principalAmount, uint128 index) internal pure returns (uint240) { - return multiply112By128Down(principalAmount, index); + function getPresentAmountRoundedDown(uint112 principalAmount, uint128 index) internal pure returns (uint256) { + return (uint256(principalAmount) * index) / EXP_SCALED_ONE; } /** @@ -91,8 +37,8 @@ library IndexingMath { * @param index An index. * @return The present amount rounded up. */ - function getPresentAmountRoundedUp(uint112 principalAmount, uint128 index) internal pure returns (uint240) { - return multiply112By128Up(principalAmount, index); + function getPresentAmountRoundedUp(uint112 principalAmount, uint128 index) internal pure returns (uint256) { + return ((uint256(principalAmount) * index) + (EXP_SCALED_ONE - 1)) / EXP_SCALED_ONE; } /** @@ -101,8 +47,10 @@ library IndexingMath { * @param index An index. * @return The principal amount rounded down. */ - function getPrincipalAmountRoundedDown(uint240 presentAmount, uint128 index) internal pure returns (uint112) { - return divide240By128Down(presentAmount, index); + function getPrincipalAmountRoundedDown(uint256 presentAmount, uint128 index) internal pure returns (uint112) { + if (index == 0) revert DivisionByZero(); + + return UIntMath.safe112((presentAmount * EXP_SCALED_ONE) / index); } /** @@ -111,7 +59,25 @@ library IndexingMath { * @param index An index. * @return The principal amount rounded up. */ - function getPrincipalAmountRoundedUp(uint240 presentAmount, uint128 index) internal pure returns (uint112) { - return divide240By128Up(presentAmount, index); + function getPrincipalAmountRoundedUp(uint256 presentAmount, uint128 index) internal pure returns (uint112) { + if (index == 0) revert DivisionByZero(); + + return UIntMath.safe112(((presentAmount * EXP_SCALED_ONE) + index - 1) / index); + } + + /** + * @dev Returns the safely capped principal amount given the present amount, using the current index. + * @param presentAmount The present amount. + * @param index An index. + * @param maxPrincipalAmount The maximum principal amount. + * @return The principal amount rounded up, capped at maxPrincipalAmount. + */ + function getSafePrincipalAmountRoundedUp( + uint256 presentAmount, + uint128 index, + uint112 maxPrincipalAmount + ) internal pure returns (uint112) { + uint112 principalAmount = getPrincipalAmountRoundedUp(presentAmount, index); + return principalAmount > maxPrincipalAmount ? maxPrincipalAmount : principalAmount; } } diff --git a/test/ContinuousIndexingMath.t.sol b/test/ContinuousIndexingMath.t.sol index 042b08d..441adcd 100644 --- a/test/ContinuousIndexingMath.t.sol +++ b/test/ContinuousIndexingMath.t.sol @@ -80,26 +80,44 @@ contract ContinuousIndexingMathTests is Test { assertEqPrecision((oneDayRate * oneDayRate) / oneInExp, twoDayRate, 1e1); } + /// @dev The tests below feed `ContinuousIndexingMath` indices straight into `IndexingMath`, which is only sound + /// while both libraries scale by the same one. + function test_expScaledOneMatchesIndexingMath() external pure { + assertEq(ContinuousIndexingMath.EXP_SCALED_ONE, IndexingMath.EXP_SCALED_ONE); + } + function test_multiplyThenDivide_100apy() external view { uint112 amount = 1_000e6; uint128 sevenDayRate = continuousIndexingMath.getContinuousIndex(_EXP_SCALED_ONE, 7 days); uint128 thirtyDayRate = continuousIndexingMath.getContinuousIndex(_EXP_SCALED_ONE, 30 days); assertEq( - IndexingMath.divide240By128Down(IndexingMath.multiply112By128Down(amount, sevenDayRate), sevenDayRate), + IndexingMath.getPrincipalAmountRoundedDown( + IndexingMath.getPresentAmountRoundedDown(amount, sevenDayRate), + sevenDayRate + ), amount - 1 ); assertEq( - IndexingMath.multiply112By128Down(IndexingMath.divide240By128Down(amount, sevenDayRate), sevenDayRate), + IndexingMath.getPresentAmountRoundedDown( + IndexingMath.getPrincipalAmountRoundedDown(amount, sevenDayRate), + sevenDayRate + ), amount - 1 ); assertEq( - IndexingMath.divide240By128Down(IndexingMath.multiply112By128Down(amount, thirtyDayRate), thirtyDayRate), + IndexingMath.getPrincipalAmountRoundedDown( + IndexingMath.getPresentAmountRoundedDown(amount, thirtyDayRate), + thirtyDayRate + ), amount - 1 ); assertEq( - IndexingMath.multiply112By128Down(IndexingMath.divide240By128Down(amount, thirtyDayRate), thirtyDayRate), + IndexingMath.getPresentAmountRoundedDown( + IndexingMath.getPrincipalAmountRoundedDown(amount, thirtyDayRate), + thirtyDayRate + ), amount - 1 ); } @@ -110,20 +128,32 @@ contract ContinuousIndexingMathTests is Test { uint128 thirtyDayRate = continuousIndexingMath.getContinuousIndex((_EXP_SCALED_ONE * 6) / 100, 30 days); assertEq( - IndexingMath.divide240By128Down(IndexingMath.multiply112By128Down(amount, sevenDayRate), sevenDayRate), + IndexingMath.getPrincipalAmountRoundedDown( + IndexingMath.getPresentAmountRoundedDown(amount, sevenDayRate), + sevenDayRate + ), amount - 1 ); assertEq( - IndexingMath.multiply112By128Down(IndexingMath.divide240By128Down(amount, sevenDayRate), sevenDayRate), + IndexingMath.getPresentAmountRoundedDown( + IndexingMath.getPrincipalAmountRoundedDown(amount, sevenDayRate), + sevenDayRate + ), amount - 1 ); assertEq( - IndexingMath.divide240By128Down(IndexingMath.multiply112By128Down(amount, thirtyDayRate), thirtyDayRate), + IndexingMath.getPrincipalAmountRoundedDown( + IndexingMath.getPresentAmountRoundedDown(amount, thirtyDayRate), + thirtyDayRate + ), amount - 1 ); assertEq( - IndexingMath.multiply112By128Down(IndexingMath.divide240By128Down(amount, thirtyDayRate), thirtyDayRate), + IndexingMath.getPresentAmountRoundedDown( + IndexingMath.getPrincipalAmountRoundedDown(amount, thirtyDayRate), + thirtyDayRate + ), amount - 1 ); } diff --git a/test/IndexingMath.t.sol b/test/IndexingMath.t.sol index 94d152a..53324a4 100644 --- a/test/IndexingMath.t.sol +++ b/test/IndexingMath.t.sol @@ -2,166 +2,351 @@ pragma solidity >=0.8.20 <0.9.0; -import { Test } from "../lib/forge-std/src/Test.sol"; +import {Test, stdError} from "../lib/forge-std/src/Test.sol"; -import { IndexingMath } from "../src/libs/IndexingMath.sol"; +import {IndexingMath} from "../src/libs/IndexingMath.sol"; +import {UIntMath} from "../src/libs/UIntMath.sol"; -import { IndexingMathHarness } from "./utils/IndexingMathHarness.sol"; +import {IndexingMathHarness} from "./utils/IndexingMathHarness.sol"; -contract ContinuousIndexingMathTests is Test { +contract IndexingMathTests is Test { uint56 internal constant _EXP_SCALED_ONE = IndexingMath.EXP_SCALED_ONE; - IndexingMathHarness public indexingMath; - - function setUp() external { - indexingMath = new IndexingMathHarness(); - } - - function test_divide240By128Down() external view { - // Set 1a - assertEq(indexingMath.divide240By128Down(0, 1), 0); - assertEq(indexingMath.divide240By128Down(1, 1), _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Down(2, 1), 2 * _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Down(3, 1), 3 * _EXP_SCALED_ONE); - - // Set 1b - assertEq(indexingMath.divide240By128Down(1, 1), _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Down(1, 2), _EXP_SCALED_ONE / 2); - assertEq(indexingMath.divide240By128Down(1, 3), _EXP_SCALED_ONE / 3); // Different than divideUp - - // Set 2a - assertEq(indexingMath.divide240By128Down(0, 10), 0); - assertEq(indexingMath.divide240By128Down(5, 10), _EXP_SCALED_ONE / 2); - assertEq(indexingMath.divide240By128Down(10, 10), _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Down(15, 10), _EXP_SCALED_ONE + _EXP_SCALED_ONE / 2); - assertEq(indexingMath.divide240By128Down(20, 10), 2 * _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Down(25, 10), 2 * _EXP_SCALED_ONE + _EXP_SCALED_ONE / 2); - - // Set 2b - assertEq(indexingMath.divide240By128Down(10, 5), 2 * _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Down(10, 10), _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Down(10, 15), (2 * _EXP_SCALED_ONE) / 3); // Different than divideUp - assertEq(indexingMath.divide240By128Down(10, 20), _EXP_SCALED_ONE / 2); - assertEq(indexingMath.divide240By128Down(10, 25), (2 * _EXP_SCALED_ONE) / 5); - - // Set 3 - assertEq(indexingMath.divide240By128Down(1, _EXP_SCALED_ONE + 1), 0); // Different than divideUp - assertEq(indexingMath.divide240By128Down(1, _EXP_SCALED_ONE), 1); - assertEq(indexingMath.divide240By128Down(1, _EXP_SCALED_ONE - 1), 1); // Different than divideUp - assertEq(indexingMath.divide240By128Down(1, (_EXP_SCALED_ONE / 2) + 1), 1); // Different than divideUp - assertEq(indexingMath.divide240By128Down(1, (_EXP_SCALED_ONE / 2)), 2); - assertEq(indexingMath.divide240By128Down(1, (_EXP_SCALED_ONE / 2) - 1), 2); // Different than divideUp - } - - function test_divide240By128Up() external view { - // Set 1a - assertEq(indexingMath.divide240By128Up(0, 1), 0); - assertEq(indexingMath.divide240By128Up(1, 1), _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Up(2, 1), 2 * _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Up(3, 1), 3 * _EXP_SCALED_ONE); - - // Set 1b - assertEq(indexingMath.divide240By128Up(1, 1), _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Up(1, 2), _EXP_SCALED_ONE / 2); - assertEq(indexingMath.divide240By128Up(1, 3), _EXP_SCALED_ONE / 3 + 1); // Different than divideDown - - // Set 2a - assertEq(indexingMath.divide240By128Up(0, 10), 0); - assertEq(indexingMath.divide240By128Up(5, 10), _EXP_SCALED_ONE / 2); - assertEq(indexingMath.divide240By128Up(10, 10), _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Up(15, 10), _EXP_SCALED_ONE + _EXP_SCALED_ONE / 2); - assertEq(indexingMath.divide240By128Up(20, 10), 2 * _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Up(25, 10), 2 * _EXP_SCALED_ONE + _EXP_SCALED_ONE / 2); - - // Set 2b - assertEq(indexingMath.divide240By128Up(10, 5), 2 * _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Up(10, 10), _EXP_SCALED_ONE); - assertEq(indexingMath.divide240By128Up(10, 15), (2 * _EXP_SCALED_ONE) / 3 + 1); // Different than divideDown - assertEq(indexingMath.divide240By128Up(10, 20), _EXP_SCALED_ONE / 2); - assertEq(indexingMath.divide240By128Up(10, 25), (2 * _EXP_SCALED_ONE) / 5); - - // Set 3 - assertEq(indexingMath.divide240By128Up(1, _EXP_SCALED_ONE + 1), 1); // Different than divideDown - assertEq(indexingMath.divide240By128Up(1, _EXP_SCALED_ONE), 1); - assertEq(indexingMath.divide240By128Up(1, _EXP_SCALED_ONE - 1), 2); // Different than divideDown - assertEq(indexingMath.divide240By128Up(1, (_EXP_SCALED_ONE / 2) + 1), 2); // Different than divideDown - assertEq(indexingMath.divide240By128Up(1, (_EXP_SCALED_ONE / 2)), 2); - assertEq(indexingMath.divide240By128Up(1, (_EXP_SCALED_ONE / 2) - 1), 3); // Different than divideDown - } - - function test_multiply112By128Down() external view { - // Set 1a - assertEq(indexingMath.multiply112By128Down(0, 1), 0); - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE, 1), 1); - assertEq(indexingMath.multiply112By128Down(2 * _EXP_SCALED_ONE, 1), 2); - assertEq(indexingMath.multiply112By128Down(3 * _EXP_SCALED_ONE, 1), 3); - - // Set 1b - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE, 1), 1); - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE / 2, 2), 1); - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE / 3, 3), 0); - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE / 3 + 1, 3), 1); - - // Set 2a - assertEq(indexingMath.multiply112By128Down(0, 10), 0); - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE / 2, 10), 5); - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE, 10), 10); - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE + _EXP_SCALED_ONE / 2, 10), 15); - assertEq(indexingMath.multiply112By128Down(2 * _EXP_SCALED_ONE, 10), 20); - assertEq(indexingMath.multiply112By128Down(2 * _EXP_SCALED_ONE + _EXP_SCALED_ONE / 2, 10), 25); - - // Set 2b - assertEq(indexingMath.multiply112By128Down(2 * _EXP_SCALED_ONE, 5), 10); - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE, 10), 10); - assertEq(indexingMath.multiply112By128Down((2 * _EXP_SCALED_ONE) / 3, 15), 9); - assertEq(indexingMath.multiply112By128Down((2 * _EXP_SCALED_ONE) / 3 + 1, 15), 10); - assertEq(indexingMath.multiply112By128Down(_EXP_SCALED_ONE / 2, 20), 10); - assertEq(indexingMath.multiply112By128Down((2 * _EXP_SCALED_ONE) / 5, 25), 10); - - // Set 3 - assertEq(indexingMath.multiply112By128Down(1, _EXP_SCALED_ONE + 1), 1); - assertEq(indexingMath.multiply112By128Down(1, _EXP_SCALED_ONE), 1); - assertEq(indexingMath.multiply112By128Down(1, _EXP_SCALED_ONE - 1), 0); - assertEq(indexingMath.multiply112By128Down(1, (_EXP_SCALED_ONE / 2) + 1), 0); - assertEq(indexingMath.multiply112By128Down(2, (_EXP_SCALED_ONE / 2)), 1); - assertEq(indexingMath.multiply112By128Down(2, (_EXP_SCALED_ONE / 2) - 1), 0); - } - - function test_multiply112By128Up() external view { - // Set 1a - assertEq(indexingMath.multiply112By128Up(0, 1), 0); - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE, 1), 1); - assertEq(indexingMath.multiply112By128Up(2 * _EXP_SCALED_ONE, 1), 2); - assertEq(indexingMath.multiply112By128Up(3 * _EXP_SCALED_ONE, 1), 3); - - // Set 1b - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE, 1), 1); - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE / 2, 2), 1); - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE / 3, 3), 1); // Different than multiplyDown - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE / 3 + 1, 3), 2); // Different than multiplyDown - - // Set 2a - assertEq(indexingMath.multiply112By128Up(0, 10), 0); - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE / 2, 10), 5); - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE, 10), 10); - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE + _EXP_SCALED_ONE / 2, 10), 15); - assertEq(indexingMath.multiply112By128Up(2 * _EXP_SCALED_ONE, 10), 20); - assertEq(indexingMath.multiply112By128Up(2 * _EXP_SCALED_ONE + _EXP_SCALED_ONE / 2, 10), 25); - - // Set 2b - assertEq(indexingMath.multiply112By128Up(2 * _EXP_SCALED_ONE, 5), 10); - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE, 10), 10); - assertEq(indexingMath.multiply112By128Up((2 * _EXP_SCALED_ONE) / 3, 15), 10); // Different than multiplyDown - assertEq(indexingMath.multiply112By128Up((2 * _EXP_SCALED_ONE) / 3 + 1, 15), 11); // Different than multiplyDown - assertEq(indexingMath.multiply112By128Up(_EXP_SCALED_ONE / 2, 20), 10); - assertEq(indexingMath.multiply112By128Up((2 * _EXP_SCALED_ONE) / 5, 25), 10); - - // Set 3 - assertEq(indexingMath.multiply112By128Up(1, _EXP_SCALED_ONE + 1), 2); // Different than multiplyDown - assertEq(indexingMath.multiply112By128Up(1, _EXP_SCALED_ONE), 1); - assertEq(indexingMath.multiply112By128Up(1, _EXP_SCALED_ONE - 1), 1); // Different than multiplyDown - assertEq(indexingMath.multiply112By128Up(1, (_EXP_SCALED_ONE / 2) + 1), 1); // Different than multiplyDown - assertEq(indexingMath.multiply112By128Up(2, (_EXP_SCALED_ONE / 2)), 1); - assertEq(indexingMath.multiply112By128Up(2, (_EXP_SCALED_ONE / 2) - 1), 1); // Different than multiplyDown + /// @dev The largest present amount for which `presentAmount * EXP_SCALED_ONE` still fits a uint256. + uint256 internal constant _MAX_SCALABLE = type(uint256).max / _EXP_SCALED_ONE; + + /// @dev What is left of a uint256 after `_MAX_SCALABLE * EXP_SCALED_ONE`, i.e. the room the `+ index - 1` + /// term of `getPrincipalAmountRoundedUp` has before it overflows. + uint256 internal constant _ROUNDING_HEADROOM = type(uint256).max - (_MAX_SCALABLE * _EXP_SCALED_ONE); + + IndexingMathHarness internal _indexingMath = new IndexingMathHarness(); + + /* ============ EXP_SCALED_ONE ============ */ + + function test_expScaledOne() external pure { + assertEq(_EXP_SCALED_ONE, 1e12); + } + + /* ============ getPresentAmountRoundedDown ============ */ + + function test_getPresentAmountRoundedDown() external view { + // An index of `EXP_SCALED_ONE` is the identity. + assertEq(_indexingMath.getPresentAmountRoundedDown(0, _EXP_SCALED_ONE), 0); + assertEq(_indexingMath.getPresentAmountRoundedDown(1, _EXP_SCALED_ONE), 1); + assertEq(_indexingMath.getPresentAmountRoundedDown(1_000e6, _EXP_SCALED_ONE), 1_000e6); + + // Indexes above and below `EXP_SCALED_ONE`. + assertEq(_indexingMath.getPresentAmountRoundedDown(1_000e6, 2 * _EXP_SCALED_ONE), 2_000e6); + assertEq(_indexingMath.getPresentAmountRoundedDown(1_000e6, (11 * _EXP_SCALED_ONE) / 10), 1_100e6); + assertEq(_indexingMath.getPresentAmountRoundedDown(1_000e6, _EXP_SCALED_ONE / 2), 500e6); + + // Truncation towards zero. + assertEq(_indexingMath.getPresentAmountRoundedDown(1, _EXP_SCALED_ONE - 1), 0); + assertEq(_indexingMath.getPresentAmountRoundedDown(1, _EXP_SCALED_ONE + 1), 1); + assertEq(_indexingMath.getPresentAmountRoundedDown(1, 1), 0); + assertEq(_indexingMath.getPresentAmountRoundedDown(3, _EXP_SCALED_ONE / 3), 0); + assertEq(_indexingMath.getPresentAmountRoundedDown(0, type(uint128).max), 0); + + // A zero index yields a zero present amount. + assertEq(_indexingMath.getPresentAmountRoundedDown(1_000e6, 0), 0); + + // The maximum inputs do not overflow, since `type(uint112).max * type(uint128).max` fits in a `uint256`. + assertEq( + _indexingMath.getPresentAmountRoundedDown(type(uint112).max, type(uint128).max), + (uint256(type(uint112).max) * type(uint128).max) / _EXP_SCALED_ONE + ); + } + + /* ============ getPresentAmountRoundedUp ============ */ + + function test_getPresentAmountRoundedUp() external view { + // An index of `EXP_SCALED_ONE` is the identity. + assertEq(_indexingMath.getPresentAmountRoundedUp(0, _EXP_SCALED_ONE), 0); + assertEq(_indexingMath.getPresentAmountRoundedUp(1, _EXP_SCALED_ONE), 1); + assertEq(_indexingMath.getPresentAmountRoundedUp(1_000e6, _EXP_SCALED_ONE), 1_000e6); + + // Indexes above and below `EXP_SCALED_ONE`. + assertEq(_indexingMath.getPresentAmountRoundedUp(1_000e6, 2 * _EXP_SCALED_ONE), 2_000e6); + assertEq(_indexingMath.getPresentAmountRoundedUp(1_000e6, (11 * _EXP_SCALED_ONE) / 10), 1_100e6); + assertEq(_indexingMath.getPresentAmountRoundedUp(1_000e6, _EXP_SCALED_ONE / 2), 500e6); + + // Truncation away from zero. Different than `getPresentAmountRoundedDown`. + assertEq(_indexingMath.getPresentAmountRoundedUp(1, _EXP_SCALED_ONE - 1), 1); + assertEq(_indexingMath.getPresentAmountRoundedUp(1, _EXP_SCALED_ONE + 1), 2); + assertEq(_indexingMath.getPresentAmountRoundedUp(1, 1), 1); + assertEq(_indexingMath.getPresentAmountRoundedUp(3, _EXP_SCALED_ONE / 3), 1); + + // A zero principal is never rounded up to a non-zero present amount. + assertEq(_indexingMath.getPresentAmountRoundedUp(0, type(uint128).max), 0); + + // A zero index yields a zero present amount. + assertEq(_indexingMath.getPresentAmountRoundedUp(1_000e6, 0), 0); + + // The maximum inputs do not overflow. + assertEq( + _indexingMath.getPresentAmountRoundedUp(type(uint112).max, type(uint128).max), + ((uint256(type(uint112).max) * type(uint128).max) + (_EXP_SCALED_ONE - 1)) / _EXP_SCALED_ONE + ); + } + + /* ============ getPrincipalAmountRoundedDown ============ */ + + function test_getPrincipalAmountRoundedDown() external view { + // An index of `EXP_SCALED_ONE` is the identity. + assertEq(_indexingMath.getPrincipalAmountRoundedDown(0, _EXP_SCALED_ONE), 0); + assertEq(_indexingMath.getPrincipalAmountRoundedDown(1, _EXP_SCALED_ONE), 1); + assertEq(_indexingMath.getPrincipalAmountRoundedDown(1_000e6, _EXP_SCALED_ONE), 1_000e6); + + // Indexes above and below `EXP_SCALED_ONE`. + assertEq(_indexingMath.getPrincipalAmountRoundedDown(2_000e6, 2 * _EXP_SCALED_ONE), 1_000e6); + assertEq(_indexingMath.getPrincipalAmountRoundedDown(1_000e6, (11 * _EXP_SCALED_ONE) / 10), 909_090909); + assertEq(_indexingMath.getPrincipalAmountRoundedDown(500e6, _EXP_SCALED_ONE / 2), 1_000e6); + + // Truncation towards zero. + assertEq(_indexingMath.getPrincipalAmountRoundedDown(1, _EXP_SCALED_ONE + 1), 0); + assertEq(_indexingMath.getPrincipalAmountRoundedDown(1, 2 * _EXP_SCALED_ONE), 0); + assertEq(_indexingMath.getPrincipalAmountRoundedDown(1, 3 * _EXP_SCALED_ONE), 0); + assertEq(_indexingMath.getPrincipalAmountRoundedDown(10, 3 * _EXP_SCALED_ONE), 3); + + // The largest representable principal. + assertEq(_indexingMath.getPrincipalAmountRoundedDown(type(uint112).max, _EXP_SCALED_ONE), type(uint112).max); + } + + function test_getPrincipalAmountRoundedDown_divisionByZero() external { + vm.expectRevert(IndexingMath.DivisionByZero.selector); + _indexingMath.getPrincipalAmountRoundedDown(1_000e6, 0); + + // The zero index is rejected even when the present amount is zero. + vm.expectRevert(IndexingMath.DivisionByZero.selector); + _indexingMath.getPrincipalAmountRoundedDown(0, 0); + } + + function test_getPrincipalAmountRoundedDown_invalidUInt112() external { + vm.expectRevert(UIntMath.InvalidUInt112.selector); + _indexingMath.getPrincipalAmountRoundedDown(uint256(type(uint112).max) + 1, _EXP_SCALED_ONE); + } + + /// @dev The present amount is widened to uint256, so `presentAmount * EXP_SCALED_ONE` is bounded by the scaling + /// limit `type(uint256).max / EXP_SCALED_ONE`. Above it, checked math panics instead of wrapping. + function test_getPrincipalAmountRoundedDown_scalingLimit() external { + // At the limit the multiplication is fine and only the uint112 cap rejects the result. + vm.expectRevert(UIntMath.InvalidUInt112.selector); + _indexingMath.getPrincipalAmountRoundedDown(_MAX_SCALABLE, _EXP_SCALED_ONE); + + vm.expectRevert(stdError.arithmeticError); + _indexingMath.getPrincipalAmountRoundedDown(_MAX_SCALABLE + 1, _EXP_SCALED_ONE); + } + + function test_getPrincipalAmountRoundedDown_overflow() external { + // `presentAmount * EXP_SCALED_ONE` overflows before the division can take place. + vm.expectRevert(stdError.arithmeticError); + _indexingMath.getPrincipalAmountRoundedDown(type(uint256).max, type(uint128).max); + } + + /* ============ getPrincipalAmountRoundedUp ============ */ + + function test_getPrincipalAmountRoundedUp() external view { + // An index of `EXP_SCALED_ONE` is the identity. + assertEq(_indexingMath.getPrincipalAmountRoundedUp(0, _EXP_SCALED_ONE), 0); + assertEq(_indexingMath.getPrincipalAmountRoundedUp(1, _EXP_SCALED_ONE), 1); + assertEq(_indexingMath.getPrincipalAmountRoundedUp(1_000e6, _EXP_SCALED_ONE), 1_000e6); + + // Indexes above and below `EXP_SCALED_ONE`. + assertEq(_indexingMath.getPrincipalAmountRoundedUp(2_000e6, 2 * _EXP_SCALED_ONE), 1_000e6); + assertEq(_indexingMath.getPrincipalAmountRoundedUp(1_000e6, (11 * _EXP_SCALED_ONE) / 10), 909_090910); + assertEq(_indexingMath.getPrincipalAmountRoundedUp(500e6, _EXP_SCALED_ONE / 2), 1_000e6); + + // Truncation away from zero. Different than `getPrincipalAmountRoundedDown`. + assertEq(_indexingMath.getPrincipalAmountRoundedUp(1, _EXP_SCALED_ONE + 1), 1); + assertEq(_indexingMath.getPrincipalAmountRoundedUp(1, 2 * _EXP_SCALED_ONE), 1); + assertEq(_indexingMath.getPrincipalAmountRoundedUp(1, 3 * _EXP_SCALED_ONE), 1); + assertEq(_indexingMath.getPrincipalAmountRoundedUp(10, 3 * _EXP_SCALED_ONE), 4); + + // A zero present amount is never rounded up to a non-zero principal. + assertEq(_indexingMath.getPrincipalAmountRoundedUp(0, type(uint128).max), 0); + + // The largest representable principal. + assertEq(_indexingMath.getPrincipalAmountRoundedUp(type(uint112).max, _EXP_SCALED_ONE), type(uint112).max); + } + + function test_getPrincipalAmountRoundedUp_divisionByZero() external { + vm.expectRevert(IndexingMath.DivisionByZero.selector); + _indexingMath.getPrincipalAmountRoundedUp(1_000e6, 0); + + // The zero index is rejected even when the present amount is zero. + vm.expectRevert(IndexingMath.DivisionByZero.selector); + _indexingMath.getPrincipalAmountRoundedUp(0, 0); + } + + function test_getPrincipalAmountRoundedUp_invalidUInt112() external { + vm.expectRevert(UIntMath.InvalidUInt112.selector); + _indexingMath.getPrincipalAmountRoundedUp(uint256(type(uint112).max) + 1, _EXP_SCALED_ONE); + } + + /// @dev Same scaling limit as the rounded down variant, except that the `+ index - 1` ceiling term consumes the + /// leftover room of the scaled present amount, so any `index > _ROUNDING_HEADROOM` already panics there. + function test_getPrincipalAmountRoundedUp_scalingLimit() external { + // At the limit, with an index small enough for the ceiling term to fit, only the uint112 cap rejects. + vm.expectRevert(UIntMath.InvalidUInt112.selector); + _indexingMath.getPrincipalAmountRoundedUp(_MAX_SCALABLE, uint128(_ROUNDING_HEADROOM)); + + // One unit of index further, the ceiling term itself overflows: `+ index` is applied before `- 1`. + vm.expectRevert(stdError.arithmeticError); + _indexingMath.getPrincipalAmountRoundedUp(_MAX_SCALABLE, uint128(_ROUNDING_HEADROOM + 1)); + + vm.expectRevert(stdError.arithmeticError); + _indexingMath.getPrincipalAmountRoundedUp(_MAX_SCALABLE, type(uint128).max); + + vm.expectRevert(stdError.arithmeticError); + _indexingMath.getPrincipalAmountRoundedUp(_MAX_SCALABLE + 1, _EXP_SCALED_ONE); + } + + function test_getPrincipalAmountRoundedUp_overflow() external { + // `presentAmount * EXP_SCALED_ONE` overflows before the division can take place. + vm.expectRevert(stdError.arithmeticError); + _indexingMath.getPrincipalAmountRoundedUp(type(uint256).max, type(uint128).max); + } + + /* ============ getSafePrincipalAmountRoundedUp ============ */ + + function test_getSafePrincipalAmountRoundedUp() external view { + // Below the cap, the result matches `getPrincipalAmountRoundedUp`. + assertEq(_indexingMath.getSafePrincipalAmountRoundedUp(0, _EXP_SCALED_ONE, 1_000e6), 0); + assertEq(_indexingMath.getSafePrincipalAmountRoundedUp(1, _EXP_SCALED_ONE, 1_000e6), 1); + assertEq(_indexingMath.getSafePrincipalAmountRoundedUp(10, 3 * _EXP_SCALED_ONE, 1_000e6), 4); + + // Exactly at the cap. + assertEq(_indexingMath.getSafePrincipalAmountRoundedUp(1_000e6, _EXP_SCALED_ONE, 1_000e6), 1_000e6); + + // Above the cap. + assertEq(_indexingMath.getSafePrincipalAmountRoundedUp(1_000e6, _EXP_SCALED_ONE, 999e6), 999e6); + assertEq(_indexingMath.getSafePrincipalAmountRoundedUp(1_000e6, _EXP_SCALED_ONE, 0), 0); + + // The cap applies to the rounded up amount, not the rounded down one. + assertEq(_indexingMath.getSafePrincipalAmountRoundedUp(10, 3 * _EXP_SCALED_ONE, 3), 3); + } + + /// @dev The cap only applies within the uint112 window, up to and including `type(uint112).max` itself. + function test_getSafePrincipalAmountRoundedUp_capsUpToMaxUInt112() external view { + uint256 presentAmount_ = uint256(type(uint112).max); + + assertEq( + _indexingMath.getSafePrincipalAmountRoundedUp(presentAmount_, _EXP_SCALED_ONE, type(uint112).max), + type(uint112).max + ); + + assertEq(_indexingMath.getSafePrincipalAmountRoundedUp(presentAmount_, _EXP_SCALED_ONE, 1), 1); + } + + function test_getSafePrincipalAmountRoundedUp_divisionByZero() external { + vm.expectRevert(IndexingMath.DivisionByZero.selector); + _indexingMath.getSafePrincipalAmountRoundedUp(1_000e6, 0, 1_000e6); + } + + function test_getSafePrincipalAmountRoundedUp_invalidUInt112() external { + // NOTE: The cap is applied after the `uint112` cast, so a present amount whose principal does not fit in a + // `uint112` reverts rather than being capped at `maxPrincipalAmount`. + vm.expectRevert(UIntMath.InvalidUInt112.selector); + _indexingMath.getSafePrincipalAmountRoundedUp(uint256(type(uint112).max) + 1, _EXP_SCALED_ONE, 1_000e6); + + vm.expectRevert(UIntMath.InvalidUInt112.selector); + _indexingMath.getSafePrincipalAmountRoundedUp(type(uint112).max, _EXP_SCALED_ONE - 1, 1_000e6); + } + + function test_getSafePrincipalAmountRoundedUp_scalingLimit() external { + vm.expectRevert(stdError.arithmeticError); + _indexingMath.getSafePrincipalAmountRoundedUp(_MAX_SCALABLE + 1, _EXP_SCALED_ONE, type(uint112).max); + } + + /* ============ Fuzz Tests ============ */ + + function testFuzz_presentAmountRounding(uint112 principal, uint128 index) external view { + uint256 roundedDown_ = _indexingMath.getPresentAmountRoundedDown(principal, index); + uint256 roundedUp_ = _indexingMath.getPresentAmountRoundedUp(principal, index); + + assertGe(roundedUp_, roundedDown_); + assertLe(roundedUp_ - roundedDown_, 1); + + // They only differ when the division is inexact. + assertEq(roundedUp_ == roundedDown_, (uint256(principal) * index) % _EXP_SCALED_ONE == 0); + } + + /// @dev An index of `EXP_SCALED_ONE` leaves the principal amount untouched in both directions. + function testFuzz_getPresentAmount_identityAtScaledOne(uint112 principal) external view { + assertEq(_indexingMath.getPresentAmountRoundedDown(principal, _EXP_SCALED_ONE), principal); + assertEq(_indexingMath.getPresentAmountRoundedUp(principal, _EXP_SCALED_ONE), principal); + } + + function testFuzz_principalAmountRounding(uint112 principal, uint128 index) external view { + index = uint128(bound(index, 1, type(uint128).max)); + + // NOTE: Deriving the present amount from a principal amount keeps the inverse within `uint112` bounds. + uint256 presentAmount_ = _indexingMath.getPresentAmountRoundedDown(principal, index); + + uint112 roundedDown_ = _indexingMath.getPrincipalAmountRoundedDown(presentAmount_, index); + uint112 roundedUp_ = _indexingMath.getPrincipalAmountRoundedUp(presentAmount_, index); + + assertGe(roundedUp_, roundedDown_); + assertLe(roundedUp_ - roundedDown_, 1); + + // They only differ when the division is inexact. + assertEq(roundedUp_ == roundedDown_, (presentAmount_ * _EXP_SCALED_ONE) % index == 0); + } + + function testFuzz_getPrincipalAmount_identityAtScaledOne(uint112 principal) external view { + assertEq(_indexingMath.getPrincipalAmountRoundedDown(principal, _EXP_SCALED_ONE), principal); + assertEq(_indexingMath.getPrincipalAmountRoundedUp(principal, _EXP_SCALED_ONE), principal); + } + + function testFuzz_roundTrip(uint112 principal, uint128 index) external view { + index = uint128(bound(index, 1, type(uint128).max)); + + // Rounding the present amount down and back down can never inflate the principal. + assertLe( + _indexingMath.getPrincipalAmountRoundedDown( + _indexingMath.getPresentAmountRoundedDown(principal, index), index + ), + principal + ); + + // NOTE: Rounding up twice can inflate the principal by up to `ceil(EXP_SCALED_ONE / index)`, so reserve the + // full worst-case headroom for the current `index` before the round trip. + uint112 maxRoundTripInflation_ = uint112((_EXP_SCALED_ONE + uint256(index) - 1) / index); + uint112 boundedPrincipal_ = uint112(bound(principal, 0, type(uint112).max - maxRoundTripInflation_)); + + // Rounding the present amount up and back up can never deflate the principal. + assertGe( + _indexingMath.getPrincipalAmountRoundedUp( + _indexingMath.getPresentAmountRoundedUp(boundedPrincipal_, index), index + ), + boundedPrincipal_ + ); + } + + function testFuzz_getSafePrincipalAmountRoundedUp(uint112 principal, uint128 index, uint112 maxPrincipalAmount) + external + view + { + index = uint128(bound(index, 1, type(uint128).max)); + + // NOTE: Deriving the present amount from a principal amount keeps the inverse within `uint112` bounds. + uint256 presentAmount_ = _indexingMath.getPresentAmountRoundedDown(principal, index); + + uint112 uncapped_ = _indexingMath.getPrincipalAmountRoundedUp(presentAmount_, index); + uint112 capped_ = _indexingMath.getSafePrincipalAmountRoundedUp(presentAmount_, index, maxPrincipalAmount); + + assertEq(capped_, uncapped_ > maxPrincipalAmount ? maxPrincipalAmount : uncapped_); + assertLe(capped_, maxPrincipalAmount); + } + + /// @dev The largest reachable present amount is `(2^112 - 1) * (2^128 - 1) / 1e12`, roughly 1.77e60, which is + /// twelve orders of magnitude below `type(uint240).max`. The uint256 return type is therefore never needed. + function test_getPresentAmount_alwaysFitsUInt240() external view { + assertLt(_indexingMath.getPresentAmountRoundedUp(type(uint112).max, type(uint128).max), type(uint240).max); + } + + function testFuzz_getPresentAmount_alwaysFitsUInt240(uint112 principal, uint128 index) external view { + assertLe(_indexingMath.getPresentAmountRoundedDown(principal, index), type(uint240).max); + assertLe(_indexingMath.getPresentAmountRoundedUp(principal, index), type(uint240).max); } } diff --git a/test/utils/IndexingMathHarness.sol b/test/utils/IndexingMathHarness.sol index e62bfc8..0f973ab 100644 --- a/test/utils/IndexingMathHarness.sol +++ b/test/utils/IndexingMathHarness.sol @@ -2,25 +2,33 @@ pragma solidity >=0.8.20 <0.9.0; -import { IndexingMath } from "../../src/libs/IndexingMath.sol"; +import {IndexingMath} from "../../src/libs/IndexingMath.sol"; -// Note: This harness contract is needed cause internal library functions can be inlined by the compiler +// Note: This harness contract is needed because internal library functions can be inlined by the compiler // and won't be picked up by forge coverage // See: https://github.com/foundry-rs/foundry/issues/6308#issuecomment-1866878768 contract IndexingMathHarness { - function divide240By128Down(uint240 x, uint128 index) external pure returns (uint112 z) { - return IndexingMath.divide240By128Down(x, index); + function getPresentAmountRoundedDown(uint112 principal, uint128 index) external pure returns (uint256) { + return IndexingMath.getPresentAmountRoundedDown(principal, index); } - function divide240By128Up(uint240 x, uint128 index) external pure returns (uint112 z) { - return IndexingMath.divide240By128Up(x, index); + function getPresentAmountRoundedUp(uint112 principal, uint128 index) external pure returns (uint256) { + return IndexingMath.getPresentAmountRoundedUp(principal, index); } - function multiply112By128Down(uint112 x, uint128 index) external pure returns (uint240 z) { - return IndexingMath.multiply112By128Down(x, index); + function getPrincipalAmountRoundedDown(uint256 presentAmount, uint128 index) external pure returns (uint112) { + return IndexingMath.getPrincipalAmountRoundedDown(presentAmount, index); } - function multiply112By128Up(uint112 x, uint128 index) external pure returns (uint240 z) { - return IndexingMath.multiply112By128Up(x, index); + function getPrincipalAmountRoundedUp(uint256 presentAmount, uint128 index) external pure returns (uint112) { + return IndexingMath.getPrincipalAmountRoundedUp(presentAmount, index); + } + + function getSafePrincipalAmountRoundedUp(uint256 presentAmount, uint128 index, uint112 maxPrincipalAmount) + external + pure + returns (uint112) + { + return IndexingMath.getSafePrincipalAmountRoundedUp(presentAmount, index, maxPrincipalAmount); } }