From 74e997eec6581119a97ab238cf9e15a3892de54e Mon Sep 17 00:00:00 2001 From: Felipe Buiras Date: Thu, 22 Dec 2022 12:30:43 -0300 Subject: [PATCH 1/8] Update DAIInterestRateModel to v4 (#230) --- contracts/DAIInterestRateModelV4.sol | 132 ++++++++++++++++++ .../src/Builder/InterestRateModelBuilder.ts | 2 +- tests/Models/DAIInterestRateModelTest.js | 15 +- 3 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 contracts/DAIInterestRateModelV4.sol diff --git a/contracts/DAIInterestRateModelV4.sol b/contracts/DAIInterestRateModelV4.sol new file mode 100644 index 000000000..c8dba03ee --- /dev/null +++ b/contracts/DAIInterestRateModelV4.sol @@ -0,0 +1,132 @@ +pragma solidity ^0.8.10; + +import "./JumpRateModelV2.sol"; + +/** + * @title Compound's DAIInterestRateModel Contract (version 4) + * @author Compound, Dharma (modified by Maker Growth) + * @notice The change from v3 to v4 of this contract was just the `SECONDS_PER_BLOCK` constant, + * as noted in https://github.com/compound-finance/compound-protocol/issues/230 + */ +contract DAIInterestRateModelV4 is JumpRateModelV2 { + 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 = 12; + + /** + * @notice The additional margin per block separating the base borrow rate from the roof. + */ + uint public gapPerBlock; + + /** + * @notice The assumed (1 - reserve factor) used to calculate the minimum borrow rate (reserve factor = 0.05) + */ + uint public constant assumedOneMinusReserveFactorMantissa = 0.95e18; + + PotLike pot; + JugLike jug; + + /** + * @notice Construct an interest rate model + * @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point + * @param kink_ The utilization point at which the jump multiplier is applied + * @param pot_ The address of the Dai pot (where DSR is earned) + * @param jug_ The address of the Dai jug (where SF is kept) + * @param owner_ The address of the owner, i.e. the Timelock contract (which has the ability to update parameters directly) + */ + constructor(uint jumpMultiplierPerYear, uint kink_, address pot_, address jug_, address owner_) JumpRateModelV2(0, 0, jumpMultiplierPerYear, kink_, owner_) public { + gapPerBlock = 4e16 / blocksPerYear; + pot = PotLike(pot_); + jug = JugLike(jug_); + poke(); + } + + /** + * @notice External function to update the parameters of the interest rate model + * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by BASE). For DAI, this is calculated from DSR and SF. Input not used. + * @param gapPerYear The Additional margin per year separating the base borrow rate from the roof. (scaled by BASE) + * @param jumpMultiplierPerYear The jumpMultiplierPerYear after hitting a specified utilization point + * @param kink_ The utilization point at which the jump multiplier is applied + */ + function updateJumpRateModel(uint baseRatePerYear, uint gapPerYear, uint jumpMultiplierPerYear, uint kink_) override external { + require(msg.sender == owner, "only the owner may call this function."); + gapPerBlock = gapPerYear / blocksPerYear; + updateJumpRateModelInternal(0, 0, jumpMultiplierPerYear, kink_); + poke(); + } + + /** + * @notice Calculates the current supply interest rate per block including the Dai savings rate + * @param cash The total amount of cash the market has + * @param borrows The total amount of borrows the market has outstanding + * @param reserves The total amnount of reserves the market has + * @param reserveFactorMantissa The current reserve factor the market has + * @return The supply rate per block (as a percentage, and scaled by BASE) + */ + function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) override public view returns (uint) { + uint protocolRate = super.getSupplyRate(cash, borrows, reserves, reserveFactorMantissa); + + uint underlying = cash + borrows - reserves; + if (underlying == 0) { + return protocolRate; + } else { + uint cashRate = cash * dsrPerBlock() / underlying; + return cashRate + protocolRate; + } + } + + /** + * @notice Calculates the Dai savings rate per block + * @return The Dai savings rate per block (as a percentage, and scaled by BASE) + */ + 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 + } + + /** + * @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-B"); + uint stabilityFeePerBlock = (duty + jug.base() - RAY_BASE) / RAY_TO_BASE_SCALE * SECONDS_PER_BLOCK; + + // We ensure the minimum borrow rate >= DSR / (1 - reserve factor) + baseRatePerBlock = dsrPerBlock() * BASE / assumedOneMinusReserveFactorMantissa; + + // The roof borrow rate is max(base rate, stability fee) + gap, from which we derive the slope + if (baseRatePerBlock < stabilityFeePerBlock) { + multiplierPerBlock = (stabilityFeePerBlock - baseRatePerBlock + gapPerBlock) * BASE / kink; + } else { + multiplierPerBlock = gapPerBlock * BASE / kink; + } + + emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock, kink); + } +} + + +/*** Maker Interfaces ***/ + +interface PotLike { + function chi() external view returns (uint); + function dsr() external view returns (uint); + function rho() external view returns (uint); + function pie(address) external view returns (uint); + function drip() external returns (uint); + function join(uint) external; + function exit(uint) external; +} + +contract JugLike { + // --- Data --- + struct Ilk { + uint256 duty; + uint256 rho; + } + + mapping (bytes32 => Ilk) public ilks; + uint256 public base; +} 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, From 93f75affe02a7fcd8dcc133dcd958a2c653a4d82 Mon Sep 17 00:00:00 2001 From: Felipe Buiras Date: Tue, 17 Jan 2023 12:14:56 -0300 Subject: [PATCH 2/8] chore: remove DAIInterestRateModelV3 --- contracts/DAIInterestRateModelV3.sol | 134 --------------------------- 1 file changed, 134 deletions(-) delete mode 100644 contracts/DAIInterestRateModelV3.sol diff --git a/contracts/DAIInterestRateModelV3.sol b/contracts/DAIInterestRateModelV3.sol deleted file mode 100644 index d78f18d1f..000000000 --- a/contracts/DAIInterestRateModelV3.sol +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.10; - -import "./JumpRateModelV2.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. - */ -contract DAIInterestRateModelV3 is JumpRateModelV2 { - 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; - - /** - * @notice The additional margin per block separating the base borrow rate from the roof. - */ - uint public gapPerBlock; - - /** - * @notice The assumed (1 - reserve factor) used to calculate the minimum borrow rate (reserve factor = 0.05) - */ - uint public constant assumedOneMinusReserveFactorMantissa = 0.95e18; - - PotLike pot; - JugLike jug; - - /** - * @notice Construct an interest rate model - * @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point - * @param kink_ The utilization point at which the jump multiplier is applied - * @param pot_ The address of the Dai pot (where DSR is earned) - * @param jug_ The address of the Dai jug (where SF is kept) - * @param owner_ The address of the owner, i.e. the Timelock contract (which has the ability to update parameters directly) - */ - constructor(uint jumpMultiplierPerYear, uint kink_, address pot_, address jug_, address owner_) JumpRateModelV2(0, 0, jumpMultiplierPerYear, kink_, owner_) public { - gapPerBlock = 4e16 / blocksPerYear; - pot = PotLike(pot_); - jug = JugLike(jug_); - poke(); - } - - /** - * @notice External function to update the parameters of the interest rate model - * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by BASE). For DAI, this is calculated from DSR and SF. Input not used. - * @param gapPerYear The Additional margin per year separating the base borrow rate from the roof. (scaled by BASE) - * @param jumpMultiplierPerYear The jumpMultiplierPerYear after hitting a specified utilization point - * @param kink_ The utilization point at which the jump multiplier is applied - */ - function updateJumpRateModel(uint baseRatePerYear, uint gapPerYear, uint jumpMultiplierPerYear, uint kink_) override external { - require(msg.sender == owner, "only the owner may call this function."); - gapPerBlock = gapPerYear / blocksPerYear; - updateJumpRateModelInternal(0, 0, jumpMultiplierPerYear, kink_); - poke(); - } - - /** - * @notice Calculates the current supply interest rate per block including the Dai savings rate - * @param cash The total amount of cash the market has - * @param borrows The total amount of borrows the market has outstanding - * @param reserves The total amnount of reserves the market has - * @param reserveFactorMantissa The current reserve factor the market has - * @return The supply rate per block (as a percentage, and scaled by BASE) - */ - function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) override public view returns (uint) { - uint protocolRate = super.getSupplyRate(cash, borrows, reserves, reserveFactorMantissa); - - uint underlying = cash + borrows - reserves; - if (underlying == 0) { - return protocolRate; - } else { - uint cashRate = cash * dsrPerBlock() / underlying; - return cashRate + protocolRate; - } - } - - /** - * @notice Calculates the Dai savings rate per block - * @return The Dai savings rate per block (as a percentage, and scaled by BASE) - */ - 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 - } - - /** - * @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; - - // We ensure the minimum borrow rate >= DSR / (1 - reserve factor) - baseRatePerBlock = dsrPerBlock() * BASE / assumedOneMinusReserveFactorMantissa; - - // The roof borrow rate is max(base rate, stability fee) + gap, from which we derive the slope - if (baseRatePerBlock < stabilityFeePerBlock) { - multiplierPerBlock = (stabilityFeePerBlock - baseRatePerBlock + gapPerBlock) * BASE / kink; - } else { - multiplierPerBlock = gapPerBlock * BASE / kink; - } - - emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock, kink); - } -} - - -/*** Maker Interfaces ***/ - -interface PotLike { - function chi() external view returns (uint); - function dsr() external view returns (uint); - function rho() external view returns (uint); - function pie(address) external view returns (uint); - function drip() external returns (uint); - function join(uint) external; - function exit(uint) external; -} - -contract JugLike { - // --- Data --- - struct Ilk { - uint256 duty; - uint256 rho; - } - - mapping (bytes32 => Ilk) public ilks; - uint256 public base; -} From fc712b9fec8a3bea32464b65ed41ae9504b27d6a Mon Sep 17 00:00:00 2001 From: Felipe Buiras Date: Tue, 17 Jan 2023 12:16:18 -0300 Subject: [PATCH 3/8] docs: change wording in DAI IRM v4 notice --- contracts/DAIInterestRateModelV4.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/DAIInterestRateModelV4.sol b/contracts/DAIInterestRateModelV4.sol index c8dba03ee..6e1ff9457 100644 --- a/contracts/DAIInterestRateModelV4.sol +++ b/contracts/DAIInterestRateModelV4.sol @@ -5,8 +5,8 @@ import "./JumpRateModelV2.sol"; /** * @title Compound's DAIInterestRateModel Contract (version 4) * @author Compound, Dharma (modified by Maker Growth) - * @notice The change from v3 to v4 of this contract was just the `SECONDS_PER_BLOCK` constant, - * as noted in https://github.com/compound-finance/compound-protocol/issues/230 + * @notice Version 4 modifies the number of seconds per block to 12, + * and takes the stability fee of ETH-B as a reference. */ contract DAIInterestRateModelV4 is JumpRateModelV2 { uint256 private constant BASE = 1e18; From 9ee3aa32e61f2e1536a6bae67184032ff2a2af3e Mon Sep 17 00:00:00 2001 From: Felipe Buiras Date: Tue, 17 Jan 2023 12:17:19 -0300 Subject: [PATCH 4/8] fix: change DAI reserve factor to reflect current market conditions --- contracts/DAIInterestRateModelV4.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/DAIInterestRateModelV4.sol b/contracts/DAIInterestRateModelV4.sol index 6e1ff9457..c5d33b755 100644 --- a/contracts/DAIInterestRateModelV4.sol +++ b/contracts/DAIInterestRateModelV4.sol @@ -22,7 +22,7 @@ contract DAIInterestRateModelV4 is JumpRateModelV2 { /** * @notice The assumed (1 - reserve factor) used to calculate the minimum borrow rate (reserve factor = 0.05) */ - uint public constant assumedOneMinusReserveFactorMantissa = 0.95e18; + uint public constant assumedOneMinusReserveFactorMantissa = 0.85e18; PotLike pot; JugLike jug; From a4e8d65d0ad1a17aab6715b822f3d3a10f42b41a Mon Sep 17 00:00:00 2001 From: Felipe Buiras Date: Thu, 30 Mar 2023 11:03:03 -0300 Subject: [PATCH 5/8] fix: mul before div to avoid precision loss --- contracts/DAIInterestRateModelV4.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/DAIInterestRateModelV4.sol b/contracts/DAIInterestRateModelV4.sol index c5d33b755..5b38ca374 100644 --- a/contracts/DAIInterestRateModelV4.sol +++ b/contracts/DAIInterestRateModelV4.sol @@ -82,8 +82,8 @@ contract DAIInterestRateModelV4 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 + / RAY_TO_BASE_SCALE // descale to BASE } /** @@ -91,7 +91,7 @@ contract DAIInterestRateModelV4 is JumpRateModelV2 { */ function poke() public { (uint duty, ) = jug.ilks("ETH-B"); - uint stabilityFeePerBlock = (duty + jug.base() - RAY_BASE) / RAY_TO_BASE_SCALE * SECONDS_PER_BLOCK; + 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; From 1ab3f5d288d0bd3b0a10673ac0c5e040af628602 Mon Sep 17 00:00:00 2001 From: Felipe Buiras Date: Thu, 30 Mar 2023 11:25:20 -0300 Subject: [PATCH 6/8] docs: comment on reserve factor mantissa change --- contracts/DAIInterestRateModelV4.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/DAIInterestRateModelV4.sol b/contracts/DAIInterestRateModelV4.sol index 5b38ca374..30ecceefb 100644 --- a/contracts/DAIInterestRateModelV4.sol +++ b/contracts/DAIInterestRateModelV4.sol @@ -20,7 +20,8 @@ contract DAIInterestRateModelV4 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.85e18; From 2ba1450f3fa7aeeb707e3dac1da5bf0f48d7cda3 Mon Sep 17 00:00:00 2001 From: Felipe Buiras Date: Mon, 10 Apr 2023 09:07:29 -0300 Subject: [PATCH 7/8] fix: syntax --- contracts/DAIInterestRateModelV4.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/DAIInterestRateModelV4.sol b/contracts/DAIInterestRateModelV4.sol index 30ecceefb..c275fd01d 100644 --- a/contracts/DAIInterestRateModelV4.sol +++ b/contracts/DAIInterestRateModelV4.sol @@ -83,8 +83,8 @@ contract DAIInterestRateModelV4 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 - * SECONDS_PER_BLOCK; // seconds per block - / RAY_TO_BASE_SCALE // descale to BASE + * SECONDS_PER_BLOCK // seconds per block + / RAY_TO_BASE_SCALE; // descale to BASE } /** From 2a6a050ad3f756920af9a298e70560be53985421 Mon Sep 17 00:00:00 2001 From: Felipe Buiras Date: Tue, 11 Apr 2023 15:53:47 -0300 Subject: [PATCH 8/8] feat: add BaseJumpRateModelV3 , fixing `blocksPerYear` The older version of BaseJumpRateModel has a blocksPerYear constant that reflects the amount of blocks produced in a 15-second block time environment. After the Merge, block times have gone down from 15 seconds to 12 seconds, this being reflected in DAIInterestRateModelV4. This brings the constant in line with the new state of the Ethereum Network. --- contracts/BaseJumpRateModelV3.sol | 139 +++++++++++++++++++++++++++ contracts/DAIInterestRateModelV4.sol | 4 +- contracts/JumpRateModelV3.sol | 29 ++++++ 3 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 contracts/BaseJumpRateModelV3.sol create mode 100644 contracts/JumpRateModelV3.sol 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/DAIInterestRateModelV4.sol b/contracts/DAIInterestRateModelV4.sol index c275fd01d..4c93783da 100644 --- a/contracts/DAIInterestRateModelV4.sol +++ b/contracts/DAIInterestRateModelV4.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.10; -import "./JumpRateModelV2.sol"; +import "./JumpRateModelV3.sol"; /** * @title Compound's DAIInterestRateModel Contract (version 4) @@ -8,7 +8,7 @@ import "./JumpRateModelV2.sol"; * @notice Version 4 modifies the number of seconds per block to 12, * and takes the stability fee of ETH-B as a reference. */ -contract DAIInterestRateModelV4 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; 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 {} +}