diff --git a/contracts/BaseJumpRateModelV3.sol b/contracts/BaseJumpRateModelV3.sol new file mode 100644 index 000000000..8b5adc25a --- /dev/null +++ b/contracts/BaseJumpRateModelV3.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.10; + +import "./InterestRateModel.sol"; + +/** + * @title Logic for Compound's JumpRateModel Contract V2. + * @author Compound (modified by Dharma Labs, refactored by Arr00) + * @notice Version 2 modifies Version 1 by enabling updateable parameters. + */ +abstract contract BaseJumpRateModelV3 is InterestRateModel { + event NewInterestParams(uint baseRatePerBlock, uint multiplierPerBlock, uint jumpMultiplierPerBlock, uint kink); + + uint256 private constant BASE = 1e18; + + /** + * @notice The address of the owner, i.e. the Timelock contract, which can update parameters directly + */ + address public owner; + + /** + * @notice The approximate number of blocks per year that is assumed by the interest rate model + * @dev This was calculated accounting for 12-second block times + */ + uint public constant blocksPerYear = 2629800; + + /** + * @notice The multiplier of utilization rate that gives the slope of the interest rate + */ + uint public multiplierPerBlock; + + /** + * @notice The base interest rate which is the y-intercept when utilization rate is 0 + */ + uint public baseRatePerBlock; + + /** + * @notice The multiplierPerBlock after hitting a specified utilization point + */ + uint public jumpMultiplierPerBlock; + + /** + * @notice The utilization point at which the jump multiplier is applied + */ + uint public kink; + + /** + * @notice Construct an interest rate model + * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by BASE) + * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by BASE) + * @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point + * @param kink_ The utilization point at which the jump multiplier is applied + * @param owner_ The address of the owner, i.e. the Timelock contract (which has the ability to update parameters directly) + */ + constructor(uint baseRatePerYear, uint multiplierPerYear, uint jumpMultiplierPerYear, uint kink_, address owner_) internal { + owner = owner_; + + updateJumpRateModelInternal(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_); + } + + /** + * @notice Update the parameters of the interest rate model (only callable by owner, i.e. Timelock) + * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by BASE) + * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by BASE) + * @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point + * @param kink_ The utilization point at which the jump multiplier is applied + */ + function updateJumpRateModel(uint baseRatePerYear, uint multiplierPerYear, uint jumpMultiplierPerYear, uint kink_) virtual external { + require(msg.sender == owner, "only the owner may call this function."); + + updateJumpRateModelInternal(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_); + } + + /** + * @notice Calculates the utilization rate of the market: `borrows / (cash + borrows - reserves)` + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market (currently unused) + * @return The utilization rate as a mantissa between [0, BASE] + */ + function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) { + // Utilization rate is 0 when there are no borrows + if (borrows == 0) { + return 0; + } + + return borrows * BASE / (cash + borrows - reserves); + } + + /** + * @notice Calculates the current borrow rate per block, with the error code expected by the market + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market + * @return The borrow rate percentage per block as a mantissa (scaled by BASE) + */ + function getBorrowRateInternal(uint cash, uint borrows, uint reserves) internal view returns (uint) { + uint util = utilizationRate(cash, borrows, reserves); + + if (util <= kink) { + return ((util * multiplierPerBlock) / BASE) + baseRatePerBlock; + } else { + uint normalRate = ((kink * multiplierPerBlock) / BASE) + baseRatePerBlock; + uint excessUtil = util - kink; + return ((excessUtil * jumpMultiplierPerBlock) / BASE) + normalRate; + } + } + + /** + * @notice Calculates the current supply rate per block + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market + * @param reserveFactorMantissa The current reserve factor for the market + * @return The supply rate percentage per block as a mantissa (scaled by BASE) + */ + function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) virtual override public view returns (uint) { + uint oneMinusReserveFactor = BASE - reserveFactorMantissa; + uint borrowRate = getBorrowRateInternal(cash, borrows, reserves); + uint rateToPool = borrowRate * oneMinusReserveFactor / BASE; + return utilizationRate(cash, borrows, reserves) * rateToPool / BASE; + } + + /** + * @notice Internal function to update the parameters of the interest rate model + * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by BASE) + * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by BASE) + * @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point + * @param kink_ The utilization point at which the jump multiplier is applied + */ + function updateJumpRateModelInternal(uint baseRatePerYear, uint multiplierPerYear, uint jumpMultiplierPerYear, uint kink_) internal { + baseRatePerBlock = baseRatePerYear / blocksPerYear; + multiplierPerBlock = (multiplierPerYear * BASE) / (blocksPerYear * kink_); + jumpMultiplierPerBlock = jumpMultiplierPerYear / blocksPerYear; + kink = kink_; + + emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock, kink); + } +} diff --git a/contracts/DAIInterestRateModelV3.sol b/contracts/DAIInterestRateModelV4.sol similarity index 85% rename from contracts/DAIInterestRateModelV3.sol rename to contracts/DAIInterestRateModelV4.sol index d78f18d1f..4c93783da 100644 --- a/contracts/DAIInterestRateModelV3.sol +++ b/contracts/DAIInterestRateModelV4.sol @@ -1,20 +1,18 @@ -// SPDX-License-Identifier: BSD-3-Clause pragma solidity ^0.8.10; -import "./JumpRateModelV2.sol"; +import "./JumpRateModelV3.sol"; /** - * @title Compound's DAIInterestRateModel Contract (version 3) - * @author Compound (modified by Dharma Labs) - * @notice The parameterized model described in section 2.4 of the original Compound Protocol whitepaper. - * Version 3 modifies the interest rate model in Version 2 by increasing the initial "gap" or slope of - * the model prior to the "kink" from 2% to 4%, and enabling updateable parameters. + * @title Compound's DAIInterestRateModel Contract (version 4) + * @author Compound, Dharma (modified by Maker Growth) + * @notice Version 4 modifies the number of seconds per block to 12, + * and takes the stability fee of ETH-B as a reference. */ -contract DAIInterestRateModelV3 is JumpRateModelV2 { +contract DAIInterestRateModelV4 is JumpRateModelV3 { uint256 private constant BASE = 1e18; uint256 private constant RAY_BASE = 1e27; uint256 private constant RAY_TO_BASE_SCALE = 1e9; - uint256 private constant SECONDS_PER_BLOCK = 15; + uint256 private constant SECONDS_PER_BLOCK = 12; /** * @notice The additional margin per block separating the base borrow rate from the roof. @@ -22,9 +20,10 @@ contract DAIInterestRateModelV3 is JumpRateModelV2 { uint public gapPerBlock; /** - * @notice The assumed (1 - reserve factor) used to calculate the minimum borrow rate (reserve factor = 0.05) + * @notice The assumed (1 - reserve factor) used to calculate the minimum borrow rate (reserve factor = 0.15) + * @dev This reflects the reserve factor on the DAI market at the time of implementation. */ - uint public constant assumedOneMinusReserveFactorMantissa = 0.95e18; + uint public constant assumedOneMinusReserveFactorMantissa = 0.85e18; PotLike pot; JugLike jug; @@ -84,16 +83,16 @@ contract DAIInterestRateModelV3 is JumpRateModelV2 { */ function dsrPerBlock() public view returns (uint) { return (pot.dsr() - RAY_BASE) // scaled RAY_BASE aka RAY, and includes an extra "ONE" before subtraction - / RAY_TO_BASE_SCALE // descale to BASE - * SECONDS_PER_BLOCK; // seconds per block + * SECONDS_PER_BLOCK // seconds per block + / RAY_TO_BASE_SCALE; // descale to BASE } /** * @notice Resets the baseRate and multiplier per block based on the stability fee and Dai savings rate */ function poke() public { - (uint duty, ) = jug.ilks("ETH-A"); - uint stabilityFeePerBlock = (duty + jug.base() - RAY_BASE) / RAY_TO_BASE_SCALE * SECONDS_PER_BLOCK; + (uint duty, ) = jug.ilks("ETH-B"); + uint stabilityFeePerBlock = (duty + jug.base() - RAY_BASE) * SECONDS_PER_BLOCK / RAY_TO_BASE_SCALE; // We ensure the minimum borrow rate >= DSR / (1 - reserve factor) baseRatePerBlock = dsrPerBlock() * BASE / assumedOneMinusReserveFactorMantissa; diff --git a/contracts/JumpRateModelV3.sol b/contracts/JumpRateModelV3.sol new file mode 100644 index 000000000..b99b3f7ef --- /dev/null +++ b/contracts/JumpRateModelV3.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.10; + +import "./BaseJumpRateModelV3.sol"; +import "./InterestRateModel.sol"; + + +/** + * @title Compound's JumpRateModel Contract V2 for V2 cTokens + * @author Arr00 + * @notice Supports only for V2 cTokens + */ +contract JumpRateModelV2 is InterestRateModel, BaseJumpRateModelV3 { + + /** + * @notice Calculates the current borrow rate per block + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market + * @return The borrow rate percentage per block as a mantissa (scaled by 1e18) + */ + function getBorrowRate(uint cash, uint borrows, uint reserves) override external view returns (uint) { + return getBorrowRateInternal(cash, borrows, reserves); + } + + constructor(uint baseRatePerYear, uint multiplierPerYear, uint jumpMultiplierPerYear, uint kink_, address owner_) + + BaseJumpRateModelV2(baseRatePerYear,multiplierPerYear,jumpMultiplierPerYear,kink_,owner_) public {} +} diff --git a/scenario/src/Builder/InterestRateModelBuilder.ts b/scenario/src/Builder/InterestRateModelBuilder.ts index a1663fd49..3a9152273 100644 --- a/scenario/src/Builder/InterestRateModelBuilder.ts +++ b/scenario/src/Builder/InterestRateModelBuilder.ts @@ -22,7 +22,7 @@ import {getContract, getTestContract} from '../Contract'; const FixedInterestRateModel = getTestContract('InterestRateModelHarness'); const WhitePaperInterestRateModel = getContract('WhitePaperInterestRateModel'); const JumpRateModel = getContract('JumpRateModel'); -const DAIInterestRateModel = getContract('DAIInterestRateModelV3'); +const DAIInterestRateModel = getContract('DAIInterestRateModelV4'); const JumpRateModelV2 = getContract('JumpRateModelV2'); const LegacyJumpRateModelV2 = getContract('LegacyJumpRateModelV2'); diff --git a/tests/Models/DAIInterestRateModelTest.js b/tests/Models/DAIInterestRateModelTest.js index da00535b9..a3575b7ae 100644 --- a/tests/Models/DAIInterestRateModelTest.js +++ b/tests/Models/DAIInterestRateModelTest.js @@ -7,6 +7,7 @@ const { getSupplyRate } = require('../Utils/Compound'); +const assumedSecondsPerBlock = 12; const blocksPerYear = 2102400; const secondsPerYear = 60 * 60 * 24 * 365; @@ -16,8 +17,8 @@ function utilizationRate(cash, borrows, reserves) { function baseRoofRateFn(dsr, duty, mkrBase, jump, kink, cash, borrows, reserves) { const assumedOneMinusReserveFactor = 0.95; - const stabilityFeePerBlock = (duty + mkrBase - 1) * 15; - const dsrPerBlock = (dsr - 1) * 15; + const stabilityFeePerBlock = (duty + mkrBase - 1) * assumedSecondsPerBlock; + const dsrPerBlock = (dsr - 1) * assumedSecondsPerBlock; const gapPerBlock = 0.04 / blocksPerYear; const jumpPerBlock = jump / blocksPerYear; @@ -39,7 +40,7 @@ function baseRoofRateFn(dsr, duty, mkrBase, jump, kink, cash, borrows, reserves) } function daiSupplyRate(dsr, duty, mkrBase, jump, kink, cash, borrows, reserves, reserveFactor = 0.1) { - const dsrPerBlock = (dsr - 1) * 15; + const dsrPerBlock = (dsr - 1) * assumedSecondsPerBlock; const ur = utilizationRate(cash, borrows, reserves); const borrowRate = baseRoofRateFn(dsr, duty, mkrBase, jump, kink, cash, borrows, reserves); const underlying = cash + borrows - reserves; @@ -68,7 +69,7 @@ async function getKovanFork() { return {kovan, root, accounts}; } -describe('DAIInterestRateModelV3', () => { +describe('DAIInterestRateModelV4', () => { describe("constructor", () => { it("sets jug and ilk address and pokes", async () => { // NB: Going back a certain distance requires an archive node, currently that add-on is $250/mo @@ -76,7 +77,7 @@ describe('DAIInterestRateModelV3', () => { const {kovan, root, accounts} = await getKovanFork(); // TODO: Get contract craz - let {contract: model} = await saddle.deployFull('DAIInterestRateModelV3', [ + let {contract: model} = await saddle.deployFull('DAIInterestRateModelV4', [ etherUnsigned(0.8e18), etherUnsigned(0.9e18), "0xea190dbdc7adf265260ec4da6e9675fd4f5a78bb", @@ -152,7 +153,7 @@ describe('DAIInterestRateModelV3', () => { etherUnsigned(perSecondBase) ]); - const daiIRM = await deploy('DAIInterestRateModelV3', [ + const daiIRM = await deploy('DAIInterestRateModelV4', [ etherUnsigned(jump), etherUnsigned(kink), pot._address, @@ -229,7 +230,7 @@ describe('DAIInterestRateModelV3', () => { etherUnsigned(perSecondBase) ]); - const daiIRM = await deploy('DAIInterestRateModelV3', [ + const daiIRM = await deploy('DAIInterestRateModelV4', [ etherUnsigned(jump), etherUnsigned(kink), pot._address,