-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merging SimpleInterest and Vault from spikes/multi-vault to the devel…
…opment branch (#96) * Adding MultiVault Token, SimpleInterest calculator, and Batch based contract * Adding MultiVault Token, SimpleInterest calculator, and Batch based contract * Add variables into test * Delete playground contracts * Add Vault based on SimpleInterest * Add Vault based on SimpleInterest * Add Vault based on SimpleInterest * Add Calc for Discounted to Principal. Add Tests for SimpleInterestVault * Adding redeem methods to SimpleInterestVault * Fixed Daily Discount Calculation. Add Tests for Daily Vault * Seperate SimpleInterest and Vault from tests * Fix lint warnings * Add a TimeLockVault * Fix lint error * Simplified TimeLock for example purposes * Restrict import to fix lint * Restrict import to fix lint * Update SimpleInterestVault to be a TimelockVault * Update SimpleInterestVault to be a TimelockVault * Refactor name to be numTimePeriodsElapsed * Remove errant comment * Fix Scaling in SimpleInterest * Fix Scaling in SimpleInterest * Add Frequency Type * Add Frequency Type * Renamed Frequencies to Tenor * Change to use 365 Days for Interest Tests * Change to use 365 Days for Interest Tests * Change to use 365 Days for Interest Tests * Calculate Deposit and Redeem Time periods from Tenor. Cycle through the Vault Tenor * Calculate Deposit and Redeem Time periods from Tenor. Cycle through the Vault Tenor * Change SimpleInterest to use uint256 for frequency * Change SimpleInterest to use uint256 for frequency * Add monthly tests to SimpleInterestTest * Start to seperate Frequency from Vault Tenor * Start to seperate Frequency from Vault Tenor * Seperate Frequency from Vault Tenor * Fix unused import linting * Shorten names and cleanup SimpleInterest * Add back in Logs * Refactor names * Add Interfaces for SimpleInterest and Vault * Use Interfaces instead of contract in functions * Update SimpleInterestVault to add a price * Update SimpleInterestVault to add a price * Refactor to move Price into SimpleInterest * Refactor to move Price into SimpleInterest * Change PAR to be 1 * Adding calcPrice to interface * Vault uses SimpleInterest price now * Fix linting - errant imports * Use the discounted in Price calc * Checking about removing the cycle concept * Checking about removing the cycle concept * Add check that early redemption results in financial penalty * Remove Price - it isn't needed and only adds complexity * Update SimpleInterest to leverage inverse property between discounted and principalFromDiscounted * Change SimpleInterest Test cases to be scaled * Add unscaled versions to test case * Add Check to make sure Principal is scaled * Inline with scaled calcuations to simplify interface * Cleanup of method signatures * Fix unused imports * Update Vault Test to be follow SimpleInterest test pattern * Fix Linting issue * Minor update to comment
- Loading branch information
Showing
10 changed files
with
779 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.23; | ||
|
||
library Frequencies { | ||
error InvalidFrequency(Frequency frequency); | ||
|
||
enum Frequency { | ||
ANNUAL, | ||
MONTHLY, | ||
QUARTERLY, | ||
DAYS_360, | ||
DAYS_365 | ||
} | ||
|
||
// Helper function to convert enum value to corresponding uint256 frequency | ||
function toValue(Frequency frequency) external pure returns (uint256) { | ||
if (frequency == Frequency.ANNUAL) return 1; | ||
if (frequency == Frequency.MONTHLY) return 12; | ||
if (frequency == Frequency.QUARTERLY) return 4; | ||
if (frequency == Frequency.DAYS_360) return 360; | ||
if (frequency == Frequency.DAYS_365) return 365; | ||
|
||
revert InvalidFrequency(frequency); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// SPDX-License-Identifier: MIT | ||
// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC4626.sol) | ||
|
||
pragma solidity ^0.8.20; | ||
|
||
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; | ||
import { ISimpleInterest } from "./ISimpleInterest.s.sol"; | ||
|
||
/** | ||
* @dev Extension to Interface Vault Standard | ||
*/ | ||
interface IERC4626Interest is IERC4626, ISimpleInterest { | ||
function convertToSharesAtPeriod(uint256 assets, uint256 numTimePeriodsElapsed) | ||
external | ||
view | ||
returns (uint256 shares); | ||
|
||
function convertToAssetsAtPeriod(uint256 shares, uint256 numTimePeriodsElapsed) | ||
external | ||
view | ||
returns (uint256 assets); | ||
|
||
// TODO - confirm if required on interface | ||
function getCurrentTimePeriodsElapsed() external pure returns (uint256 currentTimePeriodsElapsed); | ||
|
||
// TODO - confirm if required on interface | ||
function setCurrentTimePeriodsElapsed(uint256 currentTimePeriodsElapsed) external; | ||
|
||
// TODO - confirm if required on interface | ||
function getTenor() external view returns (uint256 tenor); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.20; | ||
|
||
/** | ||
* @title Simple Interest Interface | ||
* @dev This interface provides functions to calculate interest and principal amounts over time. | ||
* | ||
* @notice The `calcPrincipalFromDiscounted` and `calcDiscounted` functions are designed to be mathematical inverses of each other. | ||
* This means that applying `calcPrincipalFromDiscounted` to the output of `calcDiscounted` will return the original principal amount. | ||
* | ||
* For example: | ||
* ``` | ||
* uint256 originalPrincipal = 1000; | ||
* uint256 discountedValue = calcDiscounted(originalPrincipal); | ||
* uint256 recoveredPrincipal = calcPrincipalFromDiscounted(discountedValue); | ||
* assert(recoveredPrincipal == originalPrincipal); | ||
* ``` | ||
* | ||
* This property ensures that no information is lost when discounting and then recovering the principal, | ||
* making the system consistent and predictable. | ||
*/ | ||
interface ISimpleInterest { | ||
function calcInterest(uint256 principal, uint256 numTimePeriodsElapsed) external view returns (uint256 interest); | ||
|
||
function calcDiscounted(uint256 principal, uint256 numTimePeriodsElapsed) | ||
external | ||
view | ||
returns (uint256 discounted); | ||
|
||
function calcPrincipalFromDiscounted(uint256 discounted, uint256 numTimePeriodsElapsed) | ||
external | ||
view | ||
returns (uint256 principal); | ||
|
||
function getFrequency() external view returns (uint256 frequency); | ||
|
||
function getInterestInPercentage() external view returns (uint256 interestRateInPercentage); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.23; | ||
|
||
import { ISimpleInterest } from "./ISimpleInterest.s.sol"; | ||
|
||
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; | ||
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; | ||
|
||
import { Test } from "forge-std/Test.sol"; | ||
import { console2 } from "forge-std/console2.sol"; | ||
|
||
abstract contract InterestTest is Test { | ||
uint256 public constant TOLERANCE = 500; // with 18 decimals, means allowed difference of 5E+16 | ||
uint256 public constant NUM_CYCLES_TO_TEST = 2; // number of cycles in test (e.g. 2 years, 24 months, 720 days) | ||
|
||
uint256 public constant SCALE = 1 * 10 ** 18; // number of cycles in test (e.g. 2 years, 24 months, 720 days) | ||
|
||
using Math for uint256; | ||
|
||
function testInterestToMaxPeriods(uint256 principal, ISimpleInterest simpleInterest) internal { | ||
uint256 maxNumPeriods = simpleInterest.getFrequency() * NUM_CYCLES_TO_TEST; // e.g. 2 years, 24 months, 720 days | ||
|
||
// due to small fractional numbers, principal needs to be SCALED to calculate correctly | ||
assertGe(principal, SCALE, "principal not in SCALE"); | ||
|
||
// check all periods for 24 months | ||
for (uint256 numTimePeriods = 0; numTimePeriods <= maxNumPeriods; numTimePeriods++) { | ||
testInterestAtPeriod(principal, simpleInterest, numTimePeriods); | ||
} | ||
} | ||
|
||
function testInterestAtPeriod(uint256 principal, ISimpleInterest simpleInterest, uint256 numTimePeriods) | ||
internal | ||
virtual | ||
{ | ||
console2.log("---------------------- simpleInterestTestHarness ----------------------"); | ||
|
||
// The `calcPrincipalFromDiscounted` and `calcDiscounted` functions are designed to be mathematical inverses of each other. | ||
// This means that applying `calcPrincipalFromDiscounted` to the output of `calcDiscounted` will return the original principal amount. | ||
|
||
uint256 discounted = simpleInterest.calcDiscounted(principal, numTimePeriods); | ||
uint256 principalFromDiscounted = simpleInterest.calcPrincipalFromDiscounted(discounted, numTimePeriods); | ||
|
||
assertApproxEqAbs( | ||
principal, | ||
principalFromDiscounted, | ||
TOLERANCE, | ||
assertMsg("principalFromDiscountW not inverse of principalInWei", simpleInterest, numTimePeriods) | ||
); | ||
|
||
// discountedFactor = principal - interest, therefore interest = principal - discountedFactor | ||
assertApproxEqAbs( | ||
principal - discounted, | ||
simpleInterest.calcInterest(principal, numTimePeriods), | ||
10, // even smaller tolerance here | ||
assertMsg("calcInterest incorrect for ", simpleInterest, numTimePeriods) | ||
); | ||
} | ||
|
||
function assertMsg(string memory prefix, ISimpleInterest simpleInterest, uint256 numTimePeriods) | ||
internal | ||
view | ||
returns (string memory) | ||
{ | ||
return string.concat(prefix, toString(simpleInterest), " timePeriod= ", vm.toString(numTimePeriods)); | ||
} | ||
|
||
function toString(ISimpleInterest simpleInterest) internal view returns (string memory) { | ||
return string.concat( | ||
" ISimpleInterest [ ", | ||
" IR = ", | ||
vm.toString(simpleInterest.getInterestInPercentage()), | ||
" Freq = ", | ||
vm.toString(simpleInterest.getFrequency()), | ||
" ] " | ||
); | ||
} | ||
|
||
function transferAndAssert(IERC20 _token, address fromAddress, address toAddress, uint256 amount) internal { | ||
uint256 beforeBalance = _token.balanceOf(toAddress); | ||
|
||
vm.startPrank(fromAddress); | ||
_token.transfer(toAddress, amount); | ||
vm.stopPrank(); | ||
|
||
assertEq(beforeBalance + amount, _token.balanceOf(toAddress)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.23; | ||
|
||
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; | ||
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; | ||
|
||
import { console2 } from "forge-std/console2.sol"; | ||
import { ISimpleInterest } from "./ISimpleInterest.s.sol"; | ||
|
||
/** | ||
* https://en.wikipedia.org/wiki/Interest | ||
* | ||
* Simple interest is calculated only on the principal amount, or on that portion of the principal amount that remains. | ||
* It excludes the effect of compounding. Simple interest can be applied over a time period other than a year, for example, every month. | ||
* | ||
* Simple interest is calculated according to the following formula: (IR * P * m) / f | ||
* - IR is the simple annual interest rate | ||
* - P is the Principal (aka initial amount) | ||
* - m is the number of time periods elapsed | ||
* - f is the frequency of applying interest (how many interest periods in a year) | ||
* | ||
* | ||
* @notice The `calcPrincipalFromDiscounted` and `calcDiscounted` functions are designed to be mathematical inverses of each other. | ||
* This means that applying `calcPrincipalFromDiscounted` to the output of `calcDiscounted` will return the original principal amount. | ||
* | ||
* For example: | ||
* ``` | ||
* uint256 originalPrincipal = 1000; | ||
* uint256 discountedValue = calcDiscounted(originalPrincipal); | ||
* uint256 recoveredPrincipal = calcPrincipalFromDiscounted(discountedValue); | ||
* assert(recoveredPrincipal == originalPrincipal); | ||
* ``` | ||
* | ||
* This property ensures that no information is lost when discounting and then recovering the principal, | ||
* making the system consistent and predictable. | ||
* | ||
*/ | ||
contract SimpleInterest is ISimpleInterest { | ||
using Math for uint256; | ||
|
||
uint256 public immutable INTEREST_RATE_PERCENTAGE; | ||
uint256 public immutable FREQUENCY; | ||
|
||
uint256 public constant DECIMALS = 18; | ||
uint256 public constant SCALE = 10 ** DECIMALS; | ||
|
||
uint256 public immutable PAR = 1; | ||
|
||
Math.Rounding public constant ROUNDING = Math.Rounding.Floor; | ||
|
||
error PrincipalLessThanScale(uint256 principal, uint256 scale); | ||
|
||
constructor(uint256 interestRatePercentage, uint256 frequency) { | ||
INTEREST_RATE_PERCENTAGE = interestRatePercentage; | ||
FREQUENCY = frequency; | ||
} | ||
|
||
function calcInterest(uint256 principal, uint256 numTimePeriodsElapsed) | ||
public | ||
view | ||
virtual | ||
returns (uint256 interest) | ||
{ | ||
if (principal < SCALE) { | ||
revert PrincipalLessThanScale(principal, SCALE); | ||
} | ||
|
||
uint256 interestScaled = | ||
principal.mulDiv(INTEREST_RATE_PERCENTAGE * numTimePeriodsElapsed * SCALE, FREQUENCY * 100, ROUNDING); | ||
|
||
console2.log( | ||
string.concat( | ||
"Interest = (IR * P * m) / f = ", | ||
Strings.toString(INTEREST_RATE_PERCENTAGE), | ||
"% * ", | ||
Strings.toString(principal), | ||
" * ", | ||
Strings.toString(numTimePeriodsElapsed), | ||
" / ", | ||
Strings.toString(FREQUENCY), | ||
" = ", | ||
Strings.toString(interestScaled) | ||
) | ||
); | ||
|
||
return unscale(interestScaled); | ||
} | ||
|
||
function _calcInterestWithScale(uint256 principal, uint256 numTimePeriodsElapsed) | ||
internal | ||
view | ||
returns (uint256 _interestScaled) | ||
{ | ||
uint256 interestScaled = | ||
principal.mulDiv(INTEREST_RATE_PERCENTAGE * numTimePeriodsElapsed * SCALE, FREQUENCY * 100, ROUNDING); | ||
|
||
console2.log( | ||
string.concat( | ||
"Interest = (IR * P * m) / f = ", | ||
Strings.toString(INTEREST_RATE_PERCENTAGE), | ||
"% * ", | ||
Strings.toString(principal), | ||
" * ", | ||
Strings.toString(numTimePeriodsElapsed), | ||
" / ", | ||
Strings.toString(FREQUENCY), | ||
" = ", | ||
Strings.toString(interestScaled) | ||
) | ||
); | ||
|
||
return interestScaled; | ||
} | ||
|
||
function calcDiscounted(uint256 principal, uint256 numTimePeriodsElapsed) public view returns (uint256) { | ||
if (principal < SCALE) { | ||
revert PrincipalLessThanScale(principal, SCALE); | ||
} | ||
|
||
uint256 discountedScaled = principal * SCALE - _calcInterestWithScale(principal, numTimePeriodsElapsed); | ||
|
||
return unscale(discountedScaled); | ||
} | ||
|
||
function calcPrincipalFromDiscounted(uint256 discounted, uint256 numTimePeriodsElapsed) | ||
public | ||
view | ||
virtual | ||
returns (uint256) | ||
{ | ||
uint256 interestFactor = | ||
INTEREST_RATE_PERCENTAGE.mulDiv(numTimePeriodsElapsed * SCALE, FREQUENCY * 100, ROUNDING); | ||
|
||
uint256 scaledPrincipal = discounted.mulDiv(SCALE * SCALE, SCALE - interestFactor, ROUNDING); | ||
|
||
console2.log( | ||
string.concat( | ||
"Principal = Discounted / (1 - ((IR * m) / f)) = ", | ||
Strings.toString(discounted), | ||
" / (1 - ((", | ||
Strings.toString(INTEREST_RATE_PERCENTAGE), | ||
" * ", | ||
Strings.toString(numTimePeriodsElapsed), | ||
" ) / ", | ||
Strings.toString(FREQUENCY), | ||
" = ", | ||
Strings.toString(scaledPrincipal) | ||
) | ||
); | ||
|
||
return unscale(scaledPrincipal); | ||
} | ||
|
||
function unscale(uint256 amount) internal pure returns (uint256) { | ||
return amount / SCALE; | ||
} | ||
|
||
function getFrequency() public view returns (uint256 frequency) { | ||
return FREQUENCY; | ||
} | ||
|
||
function getInterestInPercentage() public view returns (uint256 interestRateInPercentage) { | ||
return INTEREST_RATE_PERCENTAGE; | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
packages/contracts/test/src/multi/SimpleInterestTest.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.23; | ||
|
||
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; | ||
import { SimpleInterest } from "./SimpleInterest.s.sol"; | ||
import { Frequencies } from "./Frequencies.s.sol"; | ||
|
||
import { ISimpleInterest } from "./ISimpleInterest.s.sol"; | ||
import { InterestTest } from "./InterestTest.t.sol"; | ||
|
||
contract SimpleInterestTest is InterestTest { | ||
using Math for uint256; | ||
|
||
function test__SimpleInterestTest__CheckScale() public { | ||
uint256 apy = 10; // APY in percentage | ||
|
||
ISimpleInterest simpleInterest = new SimpleInterest(apy, Frequencies.toValue(Frequencies.Frequency.DAYS_360)); | ||
|
||
uint256 scaleMinus1 = SCALE - 1; | ||
|
||
// expect revert when principal not scaled | ||
vm.expectRevert(); | ||
simpleInterest.calcInterest(scaleMinus1, 0); | ||
|
||
vm.expectRevert(); | ||
simpleInterest.calcDiscounted(scaleMinus1, 0); | ||
} | ||
|
||
function test__SimpleInterestTest__Monthly() public { | ||
uint256 apy = 12; // APY in percentage | ||
|
||
ISimpleInterest simpleInterest = new SimpleInterest(apy, Frequencies.toValue(Frequencies.Frequency.MONTHLY)); | ||
|
||
testInterestToMaxPeriods(200 * SCALE, simpleInterest); | ||
} | ||
|
||
function test__SimpleInterestTest__Daily360() public { | ||
uint256 apy = 10; // APY in percentage | ||
|
||
ISimpleInterest simpleInterest = new SimpleInterest(apy, Frequencies.toValue(Frequencies.Frequency.DAYS_360)); | ||
|
||
testInterestToMaxPeriods(200 * SCALE, simpleInterest); | ||
} | ||
} |
Oops, something went wrong.