Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

view - Add IRM params. Add base rate to Lido IRM #176

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 45 additions & 28 deletions contracts/modules/interest-rate-models/IRMClassLido.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ contract IRMClassLido is BaseIRM {
kink = 3435973836;
}

/// @notice Getter for compatibility with linear kink models. Meant to be called on implementation contract directly, without access to IRMLidoStorage.
function baseRate() external view returns (uint) {
(bool success, uint lidoBaseRate) = getLidoBaseRate();
require (success, "e/irmclasslido/get-lido-base-rate");

return lidoBaseRate;
}

function computeInterestRateImpl(address, uint32 utilisation) internal override returns (int96) {
uint ir = 0;
if (utilisation > 0) {
Expand All @@ -47,33 +55,10 @@ contract IRMClassLido is BaseIRM {
}

if (block.timestamp - irmLido.lastCalled > SECONDS_PER_DAY) {
(bool successReport, bytes memory dataReport) = lidoOracle.staticcall(abi.encodeWithSelector(ILidoOracle.getLastCompletedReportDelta.selector));
(bool successFee, bytes memory dataFee) = stETH.staticcall(abi.encodeWithSelector(IStETH.getFee.selector));

// if the external contract calls unsuccessful, the base rate will be set to the last stored value
if (successReport && successFee && dataReport.length >= (3 * 32) && dataFee.length >= 32) {
(uint postTotalPooledEther, uint preTotalPooledEther, uint timeElapsed) = abi.decode(dataReport, (uint, uint, uint));
uint16 lidoFee = abi.decode(dataFee, (uint16));

// do not support negative rebases
// assure Lido reward fee is not greater than LIDO_BASIS_POINT
uint baseRate = 0;
if (
preTotalPooledEther != 0 &&
timeElapsed != 0 &&
preTotalPooledEther < postTotalPooledEther &&
lidoFee < LIDO_BASIS_POINT
) {
unchecked {
baseRate = 1e27 * (postTotalPooledEther - preTotalPooledEther) / (preTotalPooledEther * timeElapsed);

// reflect Lido reward fee
baseRate = baseRate * (LIDO_BASIS_POINT - lidoFee) / LIDO_BASIS_POINT;
}
}

// update the storage only if the Lido oracle call was successful
irmLido.baseRate = int96(int(baseRate));
(bool success, uint lidoBaseRate) = getLidoBaseRate();
// update the storage only if the Lido oracle call was successful
if (success) {
irmLido.baseRate = int96(int(lidoBaseRate));
irmLido.lastCalled = uint64(block.timestamp);
}
}
Expand All @@ -85,7 +70,7 @@ contract IRMClassLido is BaseIRM {
ir = MAX_ALLOWED_LIDO_INTEREST_RATE;
}
}

if (utilisation <= kink) {
ir += utilisation * slope1;
} else {
Expand All @@ -95,4 +80,36 @@ contract IRMClassLido is BaseIRM {

return int96(int(ir));
}

function getLidoBaseRate() private view returns (bool, uint) {
(bool successReport, bytes memory dataReport) = lidoOracle.staticcall(abi.encodeWithSelector(ILidoOracle.getLastCompletedReportDelta.selector));
(bool successFee, bytes memory dataFee) = stETH.staticcall(abi.encodeWithSelector(IStETH.getFee.selector));

// if the external contract calls unsuccessful, the base rate will be set to the last stored value
if (successReport && successFee && dataReport.length >= (3 * 32) && dataFee.length >= 32) {
(uint postTotalPooledEther, uint preTotalPooledEther, uint timeElapsed) = abi.decode(dataReport, (uint, uint, uint));
uint16 lidoFee = abi.decode(dataFee, (uint16));

// do not support negative rebases
// assure Lido reward fee is not greater than LIDO_BASIS_POINT
uint lidoBaseRate = 0;
if (
preTotalPooledEther != 0 &&
timeElapsed != 0 &&
preTotalPooledEther < postTotalPooledEther &&
lidoFee < LIDO_BASIS_POINT
) {
unchecked {
lidoBaseRate = 1e27 * (postTotalPooledEther - preTotalPooledEther) / (preTotalPooledEther * timeElapsed);

// reflect Lido reward fee
lidoBaseRate = lidoBaseRate * (LIDO_BASIS_POINT - lidoFee) / LIDO_BASIS_POINT;
}
}

return (true, lidoBaseRate);
}

return (false, 0);
}
}
26 changes: 21 additions & 5 deletions contracts/views/EulerGeneralView.sol
Original file line number Diff line number Diff line change
Expand Up @@ -205,23 +205,39 @@ contract EulerGeneralView is Constants {
uint baseSupplyAPY;
uint kinkSupplyAPY;
uint maxSupplyAPY;

uint baseRate;
uint slope1;
uint slope2;
uint moduleId;
}

function doQueryIRM(QueryIRM memory q) external view returns (ResponseIRM memory r) {
function doQueryIRMBatch(QueryIRM[] memory qs) external view returns (ResponseIRM[] memory r) {
r = new ResponseIRM[](qs.length);

for (uint i = 0; i < qs.length; ++i) {
r[i] = doQueryIRM(qs[i]);
}
}

function doQueryIRM(QueryIRM memory q) public view returns (ResponseIRM memory r) {
Euler eulerProxy = Euler(q.eulerContract);
Markets marketsProxy = Markets(eulerProxy.moduleIdToProxy(MODULEID__MARKETS));

uint moduleId = marketsProxy.interestRateModel(q.underlying);
uint moduleId = r.moduleId = marketsProxy.interestRateModel(q.underlying);
address moduleImpl = eulerProxy.moduleIdToImplementation(moduleId);

BaseIRMLinearKink irm = BaseIRMLinearKink(moduleImpl);

uint kink = r.kink = irm.kink();
uint slope1 = r.slope1 = irm.slope1();
uint slope2 = r.slope2 = irm.slope2();

uint32 reserveFee = marketsProxy.reserveFee(q.underlying);

uint baseSPY = irm.baseRate();
uint kinkSPY = baseSPY + (kink * irm.slope1());
uint maxSPY = kinkSPY + ((type(uint32).max - kink) * irm.slope2());
uint baseSPY = r.baseRate = irm.baseRate();
uint kinkSPY = baseSPY + (kink * slope1);
uint maxSPY = kinkSPY + ((type(uint32).max - kink) * slope2);

(r.baseAPY, r.baseSupplyAPY) = computeAPYs(baseSPY, 0, type(uint32).max, reserveFee);
(r.kinkAPY, r.kinkSupplyAPY) = computeAPYs(kinkSPY, kink, type(uint32).max, reserveFee);
Expand Down
1 change: 1 addition & 0 deletions hardhat.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const fs = require("fs");
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require("@nomiclabs/hardhat-ethers");
require("hardhat-contract-sizer");
require('hardhat-gas-reporter');
require("solidity-coverage");
Expand Down
44 changes: 22 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"compile": "npx hardhat compile"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.0",
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@nomiclabs/hardhat-etherscan": "^3.1.2",
"@nomiclabs/hardhat-waffle": "^2.0.0",
"@uniswap/sdk-core": "^3.0.1",
Expand All @@ -21,7 +21,7 @@
"cross-fetch": "^3.1.4",
"ethereum-block-by-date": "^1.4.2",
"ethereum-waffle": "^3.3.0",
"ethers": "^5.7.0",
"ethers": "^5.7.2",
"ganache-cli": "^6.12.1",
"hardhat": "^2.10.2",
"hardhat-contract-sizer": "^2.0.3",
Expand Down
10 changes: 10 additions & 0 deletions test/irmClassLido-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,14 @@ et.testSet({
],
})

.test({
desc: "baseRate",
actions: ctx => [
// very small non-zero utilisation
{ send: 'dTokens.dUSDT.borrow', args: [0, 25], },
{ call: 'markets.interestRate', args: [ctx.contracts.tokens.USDT.address],equals: [LIDO_SPY_AT_14707000.mul(9).div(10), 1e-5], },
{ call: 'modules.irmClassLido.baseRate', args: [], equals: [LIDO_SPY_AT_14707000.mul(9).div(10), 1e-5], },
],
})

.run();
24 changes: 24 additions & 0 deletions test/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,30 @@ et.testSet({

let kink = r.kink.toNumber();
et.assert(kink > 0 && kink < 2**32);

let slope1 = r.slope1.toNumber();
let slope2 = r.slope2.toNumber();

et.assert(slope1 > 0);
et.assert(slope2 > slope1);

et.assert(r.moduleId.toNumber() === 2_000_000)
}, },
],
})



.test({
desc: "batch query IRM",
actions: ctx => [
{ action: 'setIRM', underlying: 'TST', irm: 'IRM_DEFAULT', },
{ action: 'setIRM', underlying: 'TST2', irm: 'IRM_DEFAULT', },
{ call: 'eulerGeneralView.doQueryIRMBatch', args: [[
{ eulerContract: ctx.contracts.euler.address, underlying: ctx.contracts.tokens.TST.address, },
{ eulerContract: ctx.contracts.euler.address, underlying: ctx.contracts.tokens.TST2.address, },
]], assertResult: r => {
et.expect(r[0]).to.deep.equal(r[1])
}, },
],
})
Expand Down