diff --git a/contracts/DAIInterestRateModelV4.sol b/contracts/DAIInterestRateModelV4.sol new file mode 100644 index 000000000..46234293e --- /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-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; +} 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,