diff --git a/contracts/modules/interest-rate-models/IRMClassLido.sol b/contracts/modules/interest-rate-models/IRMClassLido.sol index 64815d88..11f7aeca 100644 --- a/contracts/modules/interest-rate-models/IRMClassLido.sol +++ b/contracts/modules/interest-rate-models/IRMClassLido.sol @@ -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) { @@ -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); } } @@ -85,7 +70,7 @@ contract IRMClassLido is BaseIRM { ir = MAX_ALLOWED_LIDO_INTEREST_RATE; } } - + if (utilisation <= kink) { ir += utilisation * slope1; } else { @@ -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); + } } diff --git a/contracts/views/EulerGeneralView.sol b/contracts/views/EulerGeneralView.sol index 1cf4d1fd..0cb41579 100644 --- a/contracts/views/EulerGeneralView.sol +++ b/contracts/views/EulerGeneralView.sol @@ -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); diff --git a/hardhat.config.js b/hardhat.config.js index 5a4c90e4..725b0ef6 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -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"); diff --git a/package-lock.json b/package-lock.json index b8d6c85e..499a530b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "seedrandom": "^3.0.5" }, "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", @@ -23,7 +23,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", @@ -743,9 +743,9 @@ } }, "node_modules/@ethersproject/providers": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.1.tgz", - "integrity": "sha512-vZveG/DLyo+wk4Ga1yx6jSEHrLPgmTt+dFv0dv8URpVCRf0jVhalps1jq/emN/oXnMRsC7cQgAF32DcXLL7BPQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", + "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", "dev": true, "funding": [ { @@ -1474,9 +1474,9 @@ } }, "node_modules/@nomiclabs/hardhat-ethers": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.6.tgz", - "integrity": "sha512-q2Cjp20IB48rEn2NPjR1qxsIQBvFVYW9rFRCFq+bC4RUrn1Ljz3g4wM8uSlgIBZYBi2JMXxmOzFqHraczxq4Ng==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.2.1.tgz", + "integrity": "sha512-RHWYwnxryWR8hzRmU4Jm/q4gzvXpetUOJ4OPlwH2YARcDB+j79+yAYCwO0lN1SUOb4++oOTJEe6AWLEc42LIvg==", "dev": true, "peerDependencies": { "ethers": "^5.0.0", @@ -4861,9 +4861,9 @@ } }, "node_modules/ethers": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.1.tgz", - "integrity": "sha512-5krze4dRLITX7FpU8J4WscXqADiKmyeNlylmmDLbS95DaZpBhDe2YSwRQwKXWNyXcox7a3gBgm/MkGXV1O1S/Q==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", "dev": true, "funding": [ { @@ -4894,7 +4894,7 @@ "@ethersproject/networks": "5.7.1", "@ethersproject/pbkdf2": "5.7.0", "@ethersproject/properties": "5.7.0", - "@ethersproject/providers": "5.7.1", + "@ethersproject/providers": "5.7.2", "@ethersproject/random": "5.7.0", "@ethersproject/rlp": "5.7.0", "@ethersproject/sha2": "5.7.0", @@ -22301,9 +22301,9 @@ } }, "@ethersproject/providers": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.1.tgz", - "integrity": "sha512-vZveG/DLyo+wk4Ga1yx6jSEHrLPgmTt+dFv0dv8URpVCRf0jVhalps1jq/emN/oXnMRsC7cQgAF32DcXLL7BPQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", + "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", "dev": true, "requires": { "@ethersproject/abstract-provider": "^5.7.0", @@ -22767,9 +22767,9 @@ "optional": true }, "@nomiclabs/hardhat-ethers": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.6.tgz", - "integrity": "sha512-q2Cjp20IB48rEn2NPjR1qxsIQBvFVYW9rFRCFq+bC4RUrn1Ljz3g4wM8uSlgIBZYBi2JMXxmOzFqHraczxq4Ng==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.2.1.tgz", + "integrity": "sha512-RHWYwnxryWR8hzRmU4Jm/q4gzvXpetUOJ4OPlwH2YARcDB+j79+yAYCwO0lN1SUOb4++oOTJEe6AWLEc42LIvg==", "dev": true, "requires": {} }, @@ -25575,9 +25575,9 @@ } }, "ethers": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.1.tgz", - "integrity": "sha512-5krze4dRLITX7FpU8J4WscXqADiKmyeNlylmmDLbS95DaZpBhDe2YSwRQwKXWNyXcox7a3gBgm/MkGXV1O1S/Q==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", "dev": true, "requires": { "@ethersproject/abi": "5.7.0", @@ -25598,7 +25598,7 @@ "@ethersproject/networks": "5.7.1", "@ethersproject/pbkdf2": "5.7.0", "@ethersproject/properties": "5.7.0", - "@ethersproject/providers": "5.7.1", + "@ethersproject/providers": "5.7.2", "@ethersproject/random": "5.7.0", "@ethersproject/rlp": "5.7.0", "@ethersproject/sha2": "5.7.0", diff --git a/package.json b/package.json index 620aa48c..b497f018 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/test/irmClassLido-integration.js b/test/irmClassLido-integration.js index 1f1cd857..f4219a15 100644 --- a/test/irmClassLido-integration.js +++ b/test/irmClassLido-integration.js @@ -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(); diff --git a/test/view.js b/test/view.js index 032ba01f..878d2abb 100644 --- a/test/view.js +++ b/test/view.js @@ -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]) }, }, ], })