diff --git a/src/Blue.sol b/src/Blue.sol index beac350a9..d1b7783f8 100644 --- a/src/Blue.sol +++ b/src/Blue.sol @@ -375,7 +375,7 @@ contract Blue is IBlue { if (marketTotalBorrow != 0) { uint256 borrowRate = IIrm(market.irm).borrowRate(market); - uint256 accruedInterests = marketTotalBorrow.mulWadDown(borrowRate * elapsed); + uint256 accruedInterests = marketTotalBorrow.mulWadDown(borrowRate.wTaylorCompounded(elapsed)); totalBorrow[id] = marketTotalBorrow + accruedInterests; totalSupply[id] += accruedInterests; diff --git a/src/libraries/FixedPointMathLib.sol b/src/libraries/FixedPointMathLib.sol index 8afddd034..c8623c040 100644 --- a/src/libraries/FixedPointMathLib.sol +++ b/src/libraries/FixedPointMathLib.sol @@ -29,6 +29,16 @@ library FixedPointMathLib { return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up. } + /// @dev The sum of the last three terms in a four term taylor series expansion + /// to approximate a compound interest rate: (1 + x)^n - 1. + function wTaylorCompounded(uint256 x, uint256 n) internal pure returns (uint256) { + uint256 firstTerm = x * n; + uint256 secondTerm = mulWadDown(firstTerm, x * zeroFloorSub(n, 1)) / 2; + uint256 thirdTerm = mulWadDown(secondTerm, x * zeroFloorSub(n, 2)) / 3; + + return firstTerm + secondTerm + thirdTerm; + } + /*////////////////////////////////////////////////////////////// LOW LEVEL FIXED POINT OPERATIONS //////////////////////////////////////////////////////////////*/ @@ -55,4 +65,15 @@ library FixedPointMathLib { z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator)) } } + + /*////////////////////////////////////////////////////////////// + INTEGER OPERATIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns max(x - y, 0). + function zeroFloorSub(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly { + z := mul(gt(x, y), sub(x, y)) + } + } } diff --git a/test/forge/Math.t.sol b/test/forge/Math.t.sol new file mode 100644 index 000000000..29422dc63 --- /dev/null +++ b/test/forge/Math.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "src/libraries/FixedPointMathLib.sol"; + +contract MathTest is Test { + using FixedPointMathLib for uint256; + + function testWTaylorCompounded(uint256 rate, uint256 timeElapsed) public { + // Assume rate is less than a ~500% APY. (~180% APR) + vm.assume(rate < (FixedPointMathLib.WAD / 20_000_000) && timeElapsed < 365 days); + uint256 result = rate.wTaylorCompounded(timeElapsed) + FixedPointMathLib.WAD; + uint256 toCompare = wPow(FixedPointMathLib.WAD + rate, timeElapsed); + assertLe(result, toCompare, "rate should be less than the compounded rate"); + assertGe( + result, FixedPointMathLib.WAD + timeElapsed * rate, "rate should be greater than the simple interest rate" + ); + assertLe((toCompare - result) * 100_00 / toCompare, 8_00, "The error should be less than or equal to 8%"); + } + + // Exponentiation by squaring with rounding up. + function wPow(uint256 x, uint256 n) private pure returns (uint256 z) { + z = FixedPointMathLib.WAD; + for (; n != 0; n /= 2) { + z = n % 2 != 0 ? z.mulWadUp(x) : z; + x = x.mulWadUp(x); + } + } +}