diff --git a/contracts/views/EulerGeneralView.sol b/contracts/views/EulerGeneralView.sol index 1cf4d1fd..bffd30b0 100644 --- a/contracts/views/EulerGeneralView.sol +++ b/contracts/views/EulerGeneralView.sol @@ -187,6 +187,27 @@ contract EulerGeneralView is Constants { } + // works only for markets with kink IRM configured + function computeSPY(address eulerContract, address underlying, uint totalBorrows, uint totalBalancesUnderlying) public view returns (uint borrowSPY) { + BaseIRMLinearKink irm = BaseIRMLinearKink(getIRMImplementation(eulerContract, underlying)); + + uint32 utilisation; + if (totalBalancesUnderlying == 0) utilisation = 0; // empty pool arbitrarily given utilisation of 0 + else utilisation = uint32(totalBorrows * (uint(type(uint32).max) * 1e18) / totalBalancesUnderlying / 1e18); + + uint kink = irm.kink(); + uint slope1 = irm.slope1(); + uint slope2 = irm.slope2(); + + borrowSPY = irm.baseRate(); + if (utilisation <= kink) { + borrowSPY += utilisation * slope1; + } else { + borrowSPY += kink * slope1; + borrowSPY += slope2 * (utilisation - kink); + } + } + // Interest rate model queries @@ -207,27 +228,66 @@ contract EulerGeneralView is Constants { uint maxSupplyAPY; } - function doQueryIRM(QueryIRM memory q) external view returns (ResponseIRM memory r) { - Euler eulerProxy = Euler(q.eulerContract); - Markets marketsProxy = Markets(eulerProxy.moduleIdToProxy(MODULEID__MARKETS)); - - uint moduleId = marketsProxy.interestRateModel(q.underlying); - address moduleImpl = eulerProxy.moduleIdToImplementation(moduleId); + struct ResponseIRMFull { + uint kink; + DataPointIRM[] dataPoints; + } - BaseIRMLinearKink irm = BaseIRMLinearKink(moduleImpl); + struct DataPointIRM { + uint utilisation; + uint borrowAPY; + uint supplyAPY; + } + function doQueryIRM(QueryIRM memory q) external view returns (ResponseIRM memory r) { + BaseIRMLinearKink irm = BaseIRMLinearKink(getIRMImplementation(q.eulerContract, q.underlying)); uint kink = r.kink = irm.kink(); - 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()); + Markets marketsProxy = Markets(Euler(q.eulerContract).moduleIdToProxy(MODULEID__MARKETS)); + uint32 reserveFee = marketsProxy.reserveFee(q.underlying); + (r.baseAPY, r.baseSupplyAPY) = computeAPYs(baseSPY, 0, type(uint32).max, reserveFee); (r.kinkAPY, r.kinkSupplyAPY) = computeAPYs(kinkSPY, kink, type(uint32).max, reserveFee); (r.maxAPY, r.maxSupplyAPY) = computeAPYs(maxSPY, type(uint32).max, type(uint32).max, reserveFee); } + function doQueryIRMFull(QueryIRM memory q) external view returns (ResponseIRMFull memory r) { + uint preKinkIncrements = 14; + uint postKinkIncrements = 5; + + Markets marketsProxy = Markets(Euler(q.eulerContract).moduleIdToProxy(MODULEID__MARKETS)); + BaseIRMLinearKink irm = BaseIRMLinearKink(getIRMImplementation(q.eulerContract, q.underlying)); + + uint32 reserveFee = marketsProxy.reserveFee(q.underlying); + + r.kink = irm.kink(); + r.dataPoints = new DataPointIRM[](preKinkIncrements + postKinkIncrements + 1); + + uint totalBorrows = 0; + for (uint i = 0; i < r.dataPoints.length; i++) { + uint borrowSPY = computeSPY(q.eulerContract, q.underlying, totalBorrows, type(uint32).max); + (uint borrowAPY, uint supplyAPY) = computeAPYs(borrowSPY, totalBorrows, type(uint32).max, reserveFee); + + r.dataPoints[i] = (DataPointIRM({ + utilisation: totalBorrows * (uint(type(uint32).max) * 1e18) / type(uint32).max / 1e18, + borrowAPY: borrowAPY, + supplyAPY: supplyAPY + })); + + if (i < preKinkIncrements - 1) { + totalBorrows += (type(uint32).max - r.kink) / preKinkIncrements; + } else if (i == preKinkIncrements - 1) { + totalBorrows = r.kink; + } else if (i < preKinkIncrements + postKinkIncrements - 1) { + totalBorrows += r.kink / postKinkIncrements; + } else { + totalBorrows = type(uint32).max; + } + } + } @@ -258,4 +318,12 @@ contract EulerGeneralView is Constants { return result.length == 32 ? string(abi.encodePacked(result)) : abi.decode(result, (string)); } + + function getIRMImplementation(address eulerContract, address underlying) private view returns (address) { + Euler eulerProxy = Euler(eulerContract); + Markets marketsProxy = Markets(eulerProxy.moduleIdToProxy(MODULEID__MARKETS)); + + uint moduleId = marketsProxy.interestRateModel(underlying); + return eulerProxy.moduleIdToImplementation(moduleId); + } } diff --git a/test/view.js b/test/view.js index 032ba01f..27699e7b 100644 --- a/test/view.js +++ b/test/view.js @@ -126,6 +126,58 @@ et.testSet({ }) +.test({ + desc: "compute SPY and APY", + actions: ctx => [ + { action: 'setIRM', underlying: 'TST', irm: 'IRM_DEFAULT', }, + { action: 'setIRM', underlying: 'TST2', irm: 'IRM_DEFAULT', }, + { action: 'setReserveFee', underlying: 'TST', fee: 0.1, }, + { action: 'setReserveFee', underlying: 'TST2', fee: 0.3, }, + { action: 'cb', cb: async () => { + const uint32Max = et.BN(2).pow(32).sub(1) + const keysToCompare = ['base', 'kink', 'max'] + const fixedParams = [ + [ctx.contracts.euler.address, ctx.contracts.tokens.TST.address], + [ctx.contracts.euler.address, ctx.contracts.tokens.TST2.address] + ] + const reserveParams = [0.1 * 4e9, 0.3 * 4e9] + const utilisationParams = [ + [0, uint32Max], + [uint32Max.div(2).add(1), uint32Max], // kink 50% as per IRM_DEFAULT + [uint32Max, uint32Max] + ] + + const doQueryIRMBatchResults = [] + const computeSPYAPYResults = [] + for (const i of [0, 1]) { + doQueryIRMBatchResults.push(await ctx.contracts.eulerGeneralView.doQueryIRM( + { eulerContract: fixedParams[i][0], underlying: fixedParams[i][1], } + )) + + computeSPYAPYResults.push({}) + + for (const [j, key] of keysToCompare.entries()) { + computeSPYAPYResults[i][key] = await ctx.contracts.eulerGeneralView.computeAPYs( + await ctx.contracts.eulerGeneralView.computeSPY( + ...fixedParams[i], ...utilisationParams[j] + ), + ...utilisationParams[j], + reserveParams[i], + ) + } + } + + for (const i of [0, 1]) { + for (const key of keysToCompare) { + et.expect(doQueryIRMBatchResults[i][key + 'APY']).to.equal(computeSPYAPYResults[i][key]['borrowAPY']) + et.expect(doQueryIRMBatchResults[i][key + 'SupplyAPY']).to.equal(computeSPYAPYResults[i][key]['supplyAPY']) + } + } + }}, + ], +}) + + .test({ desc: "handle MKR like tokens returning bytes32 for name and symbol", actions: ctx => [