Skip to content

Commit

Permalink
[Release Candidate] Improve DSR Integration and Jump Rate Model
Browse files Browse the repository at this point in the history
This patch makes a few important changes. We first fix an optimization of CDaiDelegate that simplifies a variety of logic in certain calculations. Secondly, we fix the jump rate model to have a fixed slope past the kink, as opposed to a certain multiplier.
  • Loading branch information
hayesgm committed Dec 6, 2019
1 parent 8e40ba1 commit 9ea64dd
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 85 deletions.
9 changes: 2 additions & 7 deletions contracts/CDaiDelegate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -157,18 +157,13 @@ contract CDaiDelegate is CErc20Delegate {
function doTransferOut(address payable to, uint amount) internal {
DaiJoinLike daiJoin = DaiJoinLike(daiJoinAddress);
PotLike pot = PotLike(potAddress);
VatLike vat = VatLike(vatAddress);

// Calculate the percentage decrease from the pot, and move that much out
// Note: Use a slightly larger pie size to ensure that we get at least amount in the vat
uint pie = mul(add(amount, 1), RAY) / pot.chi();
uint pie = add(mul(amount, RAY) / pot.chi(), 1);
pot.exit(pie);

// Checks the actual balance of DAI in the vat after the pot exit
uint bal = vat.dai(address(this));

// Remove our whole balance if rounding would lead us to remove more than we have
daiJoin.exit(to, bal >= mul(amount, RAY) ? amount : bal / RAY);
daiJoin.exit(to, amount);
}

/*** Maker Internals ***/
Expand Down
40 changes: 28 additions & 12 deletions contracts/DAIInterestRateModel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,29 @@ import "./SafeMath.sol";
contract DAIInterestRateModel is JumpRateModel {
using SafeMath for uint;

/**
* @notice The additional margin per block separating the base borrow rate from the roof (0.05% / block)
*/
uint public constant gapPerBlock = 0.05e16 / blocksPerYear;

/**
* @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 _pot The approximate target base APR, as a mantissa (scaled by 1e18)
* @param _jug The rate of increase in interest rate wrt utilization (scaled by 1e18)
* @param _kink The utilization point at which an additional multiplier is applied
* @param _jump The additional multiplier to be applied to multiplierPerBlock after hitting a specified utilization point
* @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)
*/
constructor(address _pot, address _jug, uint _kink, uint _jump) JumpRateModel(0, 0, _kink, _jump) public {
pot = PotLike(_pot);
jug = JugLike(_jug);
constructor(uint jumpMultiplierPerYear, uint kink_, address pot_, address jug_) JumpRateModel(0, 0, jumpMultiplierPerYear, kink_) public {
pot = PotLike(pot_);
jug = JugLike(jug_);
poke();
}

Expand Down Expand Up @@ -58,18 +68,24 @@ contract DAIInterestRateModel is JumpRateModel {
.mul(15); // 15 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 stabilityFee = duty.add(jug.base()).sub(1e27).mul(1e18).div(1e27).mul(15);
uint stabilityFeePerBlock = duty.add(jug.base()).sub(1e27).mul(1e18).div(1e27).mul(15);

// We ensure the minimum borrow rate >= DSR / (1 - reserve factor)
baseRatePerBlock = dsrPerBlock().mul(1e18).div(assumedOneMinusReserveFactorMantissa);

baseRatePerBlock = dsrPerBlock().mul(1e18).div(0.9e18); // ensure borrow rate is higher than savings rate
multiplierPerBlock = stabilityFee.sub(baseRatePerBlock).mul(1e18).div(kink);
// The roof borrow rate is max(base rate, stability fee) + gap, from which we derive the slope
if (baseRatePerBlock < stabilityFeePerBlock) {
multiplierPerBlock = stabilityFeePerBlock.sub(baseRatePerBlock).add(gapPerBlock).mul(1e18).div(kink);
} else {
multiplierPerBlock = gapPerBlock.mul(1e18).div(kink);
}

emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, kink, jump);
emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock, kink);
}
}

Expand Down
27 changes: 13 additions & 14 deletions contracts/JumpRateModel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import "./SafeMath.sol";
contract JumpRateModel is InterestRateModel {
using SafeMath for uint;

event NewInterestParams(uint baseRatePerBlock, uint multiplierPerBlock, uint kink, uint jump);
event NewInterestParams(uint baseRatePerBlock, uint multiplierPerBlock, uint jumpMultiplierPerBlock, uint kink);

/**
* @notice Indicator that this is an InterestRateModel contract (for inspection)
Expand All @@ -33,29 +33,29 @@ contract JumpRateModel is InterestRateModel {
uint public baseRatePerBlock;

/**
* @notice the utilization point at which an additional multiplier is applied
*/
uint public kink;
* @notice The multiplierPerBlock after hitting a specified utilization point
*/
uint public jumpMultiplierPerBlock;

/**
* @notice the additional multiplier to be applied to multiplierPerBlock after hitting a specified utilization point
*/
uint public jump;
* @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 1e18)
* @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by 1e18)
* @param kink_ The utilization point at which an additional multiplier is applied
* @param jump_ The additional multiplier to be applied to multiplierPerBlock after hitting a specified utilization point
* @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point
* @param kink_ The utilization point at which the jump multiplier is applied
*/
constructor(uint baseRatePerYear, uint multiplierPerYear, uint kink_, uint jump_) public {
constructor(uint baseRatePerYear, uint multiplierPerYear, uint jumpMultiplierPerYear, uint kink_) public {
baseRatePerBlock = baseRatePerYear.div(blocksPerYear);
multiplierPerBlock = multiplierPerYear.div(blocksPerYear);
jumpMultiplierPerBlock = jumpMultiplierPerYear.div(blocksPerYear);
kink = kink_;
jump = jump_;

emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, kink, jump);
emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock, kink);
}

/**
Expand Down Expand Up @@ -89,8 +89,7 @@ contract JumpRateModel is InterestRateModel {
} else {
uint normalRate = kink.mul(multiplierPerBlock).div(1e18).add(baseRatePerBlock);
uint excessUtil = util.sub(kink);
uint jumpMultiplier = multiplierPerBlock.mul(jump);
return excessUtil.mul(jumpMultiplier).div(1e18).add(normalRate);
return excessUtil.mul(jumpMultiplierPerBlock).div(1e18).add(normalRate);
}
}

Expand Down
1 change: 0 additions & 1 deletion spec/scenario/MCDai.scen
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ Test "Basic mint and redeem cDAI2 (upgrade to swept DSR)"
UpgradeToDSR
CheckBasicMintRedeemWithDSR


Test "Basic borrow and repay cDAI2 (upgrade to swept DSR)"
DeployCDAI
CheckBasicMintRedeem
Expand Down
54 changes: 31 additions & 23 deletions test/Models/DAIInterestRateModelTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,41 @@ const BigNum = require('bignumber.js')
const { call, etherUnsigned, getContract, getContractDefaults, getTestContract } = require('../Utils/MochaTruffle');
const { getBorrowRate, getSupplyRate } = require('../Utils/Compound');

const blocksPerYear = 2102400;
const secondsPerYear = 60 * 60 * 24 * 365;

function utilizationRate(cash, borrows, reserves) {
return borrows ? borrows / (cash + borrows - reserves) : 0;
}

function baseRoofRateFn(dsr, duty, mkrBase, kink, jump, cash, borrows, reserves) {
const stabilityFee = (duty + mkrBase - 1) * 15;
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 base = dsrPerBlock / 0.9;
const slope = (stabilityFee - base) / kink;
const gapPerBlock = 0.0005 / blocksPerYear;
const jumpPerBlock = jump / blocksPerYear;

let baseRatePerBlock = dsrPerBlock / assumedOneMinusReserveFactor, multiplierPerBlock;
if (baseRatePerBlock < stabilityFeePerBlock) {
multiplierPerBlock = (stabilityFeePerBlock - baseRatePerBlock + gapPerBlock) / kink;
} else {
multiplierPerBlock = gapPerBlock / kink;
}

const ur = utilizationRate(cash, borrows, reserves);

if (ur <= kink) {
return ur * slope + base;
return ur * multiplierPerBlock + baseRatePerBlock;
} else {
const excessUtil = ur - kink;
const jumpMultiplier = jump * slope;
return (excessUtil * jumpMultiplier) + (kink * slope) + base;
return (excessUtil * jumpPerBlock) + (kink * multiplierPerBlock) + baseRatePerBlock;
}
}

function daiSupplyRate(dsr, duty, mkrBase, kink, jump, cash, borrows, reserves, reserveFactor = 0.1) {
function daiSupplyRate(dsr, duty, mkrBase, jump, kink, cash, borrows, reserves, reserveFactor = 0.1) {
const dsrPerBlock = (dsr - 1) * 15;
const ur = utilizationRate(cash, borrows, reserves);
const borrowRate = baseRoofRateFn(dsr, duty, mkrBase, kink, jump, cash, borrows, reserves);
const borrowRate = baseRoofRateFn(dsr, duty, mkrBase, jump, kink, cash, borrows, reserves);
const underlying = cash + borrows - reserves;
const lendingSupplyRate = borrowRate * (1 - reserveFactor) * ur;

Expand Down Expand Up @@ -66,10 +74,10 @@ contract('DAIInterestRateModel', async function (_accounts) {

let model = await contract.deploy({
arguments: [
"0xea190dbdc7adf265260ec4da6e9675fd4f5a78bb",
"0xcbb7718c9f39d05aeede1c472ca8bf804b2f1ead",
etherUnsigned(0.8e18),
etherUnsigned(0.9e18),
etherUnsigned(5)
"0xea190dbdc7adf265260ec4da6e9675fd4f5a78bb",
"0xcbb7718c9f39d05aeede1c472ca8bf804b2f1ead"
]
})
.send({ from: root });
Expand Down Expand Up @@ -129,8 +137,8 @@ contract('DAIInterestRateModel', async function (_accounts) {
[0e27, 0.1e27, 0.005e27, 3e18, 500],

].map(vs => vs.map(Number))
.forEach(([dsr, duty, base, cash, borrows, reserves = 0, kink = 0.9e18, jump = 5]) => {
it(`calculates correct borrow value for dsr=${(dsr / 1e25)}%, duty=${(duty / 1e25)}%, base=${(base / 1e25)}%, cash=${cash}, borrows=${borrows}, reserves=${reserves}`, async () => {
.forEach(([dsr, duty, base, cash, borrows, reserves = 0, jump = 0.8e18, kink = 0.9e18]) => {
it(`calculates correct borrow value for dsr=${(dsr / 1e25)}%, duty=${(duty / 1e25)}%, base=${(base / 1e25)}%, jump=${jump / 1e18}, cash=${cash}, borrows=${borrows}, reserves=${reserves}`, async () => {
const [root] = _accounts;

const onePlusPerSecondDsr = 1e27 + (dsr / secondsPerYear);
Expand All @@ -156,14 +164,14 @@ contract('DAIInterestRateModel', async function (_accounts) {

const daiIRM = await DAIInterestRateModel.deploy({
arguments: [
pot.options.address,
jug.options.address,
etherUnsigned(jump),
etherUnsigned(kink),
etherUnsigned(jump)
pot.options.address,
jug.options.address
]
}).send({ from: root });

const expected = baseRoofRateFn(onePlusPerSecondDsr / 1e27, onePlusPerSecondDuty / 1e27, perSecondBase / 1e27, kink / 1e18, jump, cash, borrows, reserves);
const expected = baseRoofRateFn(onePlusPerSecondDsr / 1e27, onePlusPerSecondDuty / 1e27, perSecondBase / 1e27, jump / 1e18, kink / 1e18, cash, borrows, reserves);
assert.like(
await getBorrowRate(daiIRM, cash, borrows, reserves),
(x) => assert.approximately(Number(x) / 1e18, expected, 1e-8)
Expand Down Expand Up @@ -221,7 +229,7 @@ contract('DAIInterestRateModel', async function (_accounts) {
[0e27, 0.1e27, 0.005e27, 3e18, 500],

].map(vs => vs.map(Number))
.forEach(([dsr, duty, base, cash, borrows, reserves = 0, kink = 0.9e18, jump = 5, reserveFactor = 0.1e18]) => {
.forEach(([dsr, duty, base, cash, borrows, reserves = 0, jump = 0.8e18, kink = 0.9e18, reserveFactor = 0.1e18]) => {
it(`calculates correct supply value for dsr=${(dsr / 1e25)}%, duty=${(duty / 1e25)}%, base=${(base / 1e25)}%, cash=${cash}, borrows=${borrows}, reserves=${reserves}`, async () => {
const [root] = _accounts;

Expand All @@ -248,14 +256,14 @@ contract('DAIInterestRateModel', async function (_accounts) {

const daiIRM = await DAIInterestRateModel.deploy({
arguments: [
pot.options.address,
jug.options.address,
etherUnsigned(jump),
etherUnsigned(kink),
etherUnsigned(jump)
pot.options.address,
jug.options.address
]
}).send({ from: root });

const expected = daiSupplyRate(onePlusPerSecondDsr / 1e27, onePlusPerSecondDuty / 1e27, perSecondBase / 1e27, kink / 1e18, jump, cash, borrows, reserves, reserveFactor / 1e18);
const expected = daiSupplyRate(onePlusPerSecondDsr / 1e27, onePlusPerSecondDuty / 1e27, perSecondBase / 1e27, jump / 1e18, kink / 1e18, cash, borrows, reserves, reserveFactor / 1e18);
assert.like(
await getSupplyRate(daiIRM, cash, borrows, reserves, reserveFactor),
(x) => assert.approximately(Number(x) / 1e18, expected, 1e-8)
Expand Down
Loading

0 comments on commit 9ea64dd

Please sign in to comment.