Skip to content

Commit d9cd6db

Browse files
committed
convert interest model to per-second and add full accrue flow
1 parent 7adf262 commit d9cd6db

4 files changed

Lines changed: 78 additions & 22 deletions

File tree

src/ZToken.sol

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.30;
33

4+
import { console } from "forge-std/console.sol";
45
import { ERC20 } from "solady/tokens/ERC20.sol";
56
import { IZToken } from "./interfaces/IZToken.sol";
67
import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol";
@@ -13,6 +14,7 @@ abstract contract ZToken is IZToken, ERC20 {
1314
uint256 internal totalBorrow;
1415
uint256 internal totalReserve;
1516
uint256 internal borrowIndex;
17+
uint256 internal accrualTimestamp;
1618

1719
address public immutable UNDERLYING_TOKEN;
1820

@@ -22,6 +24,7 @@ abstract contract ZToken is IZToken, ERC20 {
2224
uint256 public immutable BASE_BORROW_RATE;
2325
uint256 public immutable RESERVE_FACTOR;
2426
uint256 public immutable COLLATERAL_FACTOR;
27+
uint256 public immutable BASE_BORROW_RATE_PER_SEC;
2528

2629
uint256 public constant SECONDS_PER_YEAR = 365 days;
2730
uint256 public constant INITIAL_EXCHANGE_RATE = 0.02e18;
@@ -32,13 +35,37 @@ abstract contract ZToken is IZToken, ERC20 {
3235
constructor(MarketConfig memory _config) {
3336
UNDERLYING_TOKEN = _config.underlyingToken;
3437
UTILIZATION_THRESHOLD = _config.utilizationThreshold;
35-
SLOPE_BEFORE_KINK = _config.slopeBeforeKink;
36-
SLOPE_AFTER_KINK = _config.slopeAfterKink;
38+
SLOPE_BEFORE_KINK = _config.slopeBeforeKink / SECONDS_PER_YEAR;
39+
SLOPE_AFTER_KINK = _config.slopeAfterKink / SECONDS_PER_YEAR;
3740
BASE_BORROW_RATE = _config.baseBorrowRate;
3841
RESERVE_FACTOR = _config.reserveFactor;
3942
COLLATERAL_FACTOR = _config.collateralFactor;
43+
BASE_BORROW_RATE_PER_SEC = BASE_BORROW_RATE / SECONDS_PER_YEAR;
4044

4145
borrowIndex = 1e18;
46+
accrualTimestamp = block.timestamp;
47+
}
48+
49+
function _accureInterest() internal {
50+
uint256 cash = _getCash();
51+
uint256 currentTotalBorrowed = totalBorrow;
52+
uint256 utilization = cash.utilization(currentTotalBorrowed, totalReserve);
53+
54+
uint256 borrowRatePerSecond = utilization.borrowInterestRatePerSecond(
55+
UTILIZATION_THRESHOLD, BASE_BORROW_RATE_PER_SEC, SLOPE_BEFORE_KINK, SLOPE_AFTER_KINK
56+
);
57+
58+
console.log("current borrow rate", borrowRatePerSecond);
59+
uint256 time = block.timestamp - accrualTimestamp;
60+
61+
uint256 simpleInterestFactor = borrowRatePerSecond * time;
62+
63+
uint256 borrowInterestAccrued = currentTotalBorrowed.mulWad(simpleInterestFactor);
64+
65+
borrowIndex = borrowIndex.newBorrowIndex(borrowRatePerSecond, time);
66+
totalReserve += borrowInterestAccrued.mulWad(RESERVE_FACTOR);
67+
totalBorrow = currentTotalBorrowed + borrowInterestAccrued;
68+
accrualTimestamp = block.timestamp;
4269
}
4370

4471
function _getCash() internal view virtual returns (uint256) { }
@@ -50,7 +77,7 @@ abstract contract ZToken is IZToken, ERC20 {
5077
return cash.exchangeRate(totalBorrow, totalReserve, totalSupply());
5178
}
5279

53-
function getUtilization() external view returns (uint256) {
80+
function getUtilization() public view returns (uint256) {
5481
uint256 cash = _getCash();
5582
return cash.utilization(totalBorrow, totalReserve);
5683
}

src/libraries/InterestMath.sol

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.30;
33

4+
import { console } from "forge-std/console.sol";
45
import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol";
56

67
library InterestMath {
@@ -51,19 +52,19 @@ library InterestMath {
5152
function borrowInterestRatePerSecond(
5253
uint256 _utilization,
5354
uint256 _utilizationThreshold,
54-
uint256 _baseRatePerYear,
55+
uint256 baseRatePerSec,
5556
uint256 _slopeBeforeKink,
5657
uint256 _slopeAfterKink
5758
) internal pure returns (uint256) {
58-
uint256 baseRatePerSec = _baseRatePerYear / 365 days;
59+
console.log("rate per sec", baseRatePerSec);
5960
if (_utilization <= _utilizationThreshold) {
6061
// rb = base rate + s1 * Utilization, u <= uopt
6162
return baseRatePerSec + (_slopeBeforeKink.mulWad(_utilization));
6263
}
6364

6465
// rb = base rate + s1 * k + s2(U - K), u > uopt, where k = utilization threshold(_utilizationThreshold)
65-
return baseRatePerSec + _slopeBeforeKink.mulDiv(_utilizationThreshold, WAD)
66-
+ _slopeAfterKink.mulDiv(_utilization - _utilizationThreshold, WAD);
66+
return baseRatePerSec + _slopeBeforeKink.mulWad(_utilizationThreshold)
67+
+ _slopeAfterKink.mulWad(_utilization - _utilizationThreshold);
6768
}
6869

6970
function supplyInterestRatePerSecond(

test/mocks/ZTokenHarness.sol

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.30;
33

4+
import { console } from "forge-std/Test.sol";
45
import { IZToken, ZToken, InterestMath } from "src/ZToken.sol";
56

67
contract ZTokenHarness is ZToken {
@@ -13,27 +14,36 @@ contract ZTokenHarness is ZToken {
1314
constructor(IZToken.MarketConfig memory _config) ZToken(_config) { }
1415

1516
function setTotalBorrow(uint256 _borrow) external {
16-
totalBorrow = _borrow;
17+
totalBorrow += _borrow;
1718
}
1819

1920
function setTotalReserves(uint256 _reserve) external {
20-
totalReserve = _reserve;
21+
totalReserve += _reserve;
2122
}
2223

2324
function setTotalCash(uint256 _cash) external {
24-
cash = _cash;
25+
cash += _cash;
26+
console.log("cash", cash);
2527
}
2628

2729
function setBorrowIndex(uint256 _index) external {
2830
borrowIndex = _index;
2931
}
3032

31-
function setTotalSupply(uint256 _supply) external {
33+
function setAccrualTimestamp() external {
34+
accrualTimestamp = block.timestamp;
35+
}
36+
37+
function setTotalSupply(uint256 _supply) public {
3238
assembly {
3339
sstore(_TOTAL_SUPPLY_SLOT, _supply)
3440
}
3541
}
3642

43+
function accureInterest() external {
44+
_accureInterest();
45+
}
46+
3747
// function borrowInterestRatePerSecond(
3848
// uint256 _utilization,
3949
// uint256 _utilizationThreshold,
@@ -44,7 +54,7 @@ contract ZTokenHarness is ZToken {
4454

4555
function getBorrowInterestRatePerSecond(uint256 _utilization) external view returns (uint256) {
4656
return _utilization.borrowInterestRatePerSecond(
47-
UTILIZATION_THRESHOLD, BASE_BORROW_RATE, SLOPE_BEFORE_KINK, SLOPE_AFTER_KINK
57+
UTILIZATION_THRESHOLD, BASE_BORROW_RATE_PER_SEC, SLOPE_BEFORE_KINK, SLOPE_AFTER_KINK
4858
);
4959
}
5060

test/unit/ZToken.t.sol

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,34 +116,52 @@ contract ZTokenTest is Test {
116116
uint256 borrowInterestRate = zToken.getBorrowInterestRatePerSecond(utilization);
117117
uint256 supplyInterestRate = zToken.getSupplyInterestRatePerSecond(utilization, borrowInterestRate);
118118

119-
assertEq(borrowInterestRate, zToken.BASE_BORROW_RATE() / 365 days);
119+
assertEq(borrowInterestRate, zToken.BASE_BORROW_RATE_PER_SEC());
120120
assertEq(supplyInterestRate, 0);
121121
}
122122

123-
modifier whenThereIsBorrowActivity(uint256 _borrow) {
123+
modifier whenThereIsBorrowActivity(uint256 _cash, uint256 _borrow) {
124+
_cash = bound(_cash, MINIMUM_AMOUNT, type(uint128).max);
125+
_borrow = bound(_borrow, _cash / 10, _cash);
126+
uint256 reserve = _cash / 100;
127+
128+
zToken.setTotalCash(_cash);
129+
zToken.setTotalSupply(_cash.mulDiv(zToken.INITIAL_EXCHANGE_RATE(), 1e18));
124130
zToken.setTotalBorrow(_borrow);
131+
zToken.setTotalReserves(reserve);
132+
133+
zToken.accureInterest();
134+
135+
vm.warp(10);
125136
_;
126137
}
127138

128-
function test_AccrueInterest_WhenThereIsBorrowActivity(uint256 _borrow)
139+
function test_AccrueInterest_WhenThereIsBorrowActivity(uint256 _cash, uint256 _borrow)
129140
external
130-
whenThereIsBorrowActivity(_borrow)
141+
whenThereIsBorrowActivity(_cash, _borrow)
131142
{
132-
// it borrow interest rate should be greater than zero
133-
// it supply interest rate should be greater than zero
143+
uint256 utilization = zToken.getUtilization();
144+
uint256 borrowInterestRate = zToken.getBorrowInterestRatePerSecond(utilization);
145+
uint256 supplyInterestRate = zToken.getSupplyInterestRatePerSecond(utilization, borrowInterestRate);
146+
147+
assertGt(borrowInterestRate, 0);
148+
assertGt(supplyInterestRate, 0);
134149
}
135150

136-
function test_AccrueInterest_WhenItTheFirstBorrowActivity(uint256 _borrow)
151+
function test_AccrueInterest_WhenItTheFirstBorrowActivity(uint256 _cash, uint256 _borrow)
137152
external
138-
whenThereIsBorrowActivity(_borrow)
153+
whenThereIsBorrowActivity(_cash, _borrow)
139154
{
140155
// it borrow index should not change
156+
assertEq(zToken.getBorrowIndex(), 1e18);
141157
}
142158

143-
function test_AccrueInterest_WhenThereIsMultipleBorrowActivity(uint256 _borrow)
159+
function test_AccrueInterest_WhenThereIsMultipleBorrowActivity(uint256 _cash, uint256 _borrow)
144160
external
145-
whenThereIsBorrowActivity(_borrow)
161+
whenThereIsBorrowActivity(_cash, _borrow)
162+
whenThereIsBorrowActivity(_cash, _borrow)
146163
{
147164
// it borrow index should be greater than zero
165+
assertGt(zToken.getBorrowIndex(), 1e18);
148166
}
149167
}

0 commit comments

Comments
 (0)