Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions package-lock.json

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

1 change: 0 additions & 1 deletion src/ERC3009Upgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
54 changes: 18 additions & 36 deletions src/libs/ContinuousIndexingMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,19 +25,15 @@ 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);
}

/**
* @notice Helper function to calculate `(index * deltaIndex) / EXP_SCALED_ONE`, rounded up.
* @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);
}

/**
Expand All @@ -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));
}

/**
Expand All @@ -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));
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}
}
90 changes: 28 additions & 62 deletions src/libs/IndexingMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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(
Comment thread
PierrickGT marked this conversation as resolved.
uint256 presentAmount,
uint128 index,
uint112 maxPrincipalAmount
) internal pure returns (uint112) {
uint112 principalAmount = getPrincipalAmountRoundedUp(presentAmount, index);
return principalAmount > maxPrincipalAmount ? maxPrincipalAmount : principalAmount;
}
}
46 changes: 38 additions & 8 deletions test/ContinuousIndexingMath.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
Expand All @@ -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
);
}
Expand Down
Loading
Loading