diff --git a/.gitmodules b/.gitmodules index c59f396e6..521039b40 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/solmate"] path = lib/solmate url = https://github.com/transmissions11/solmate +[submodule "lib/morpho-utils"] + path = lib/morpho-utils + url = https://github.com/morpho-dao/morpho-utils diff --git a/lib/morpho-utils b/lib/morpho-utils new file mode 160000 index 000000000..74f194356 --- /dev/null +++ b/lib/morpho-utils @@ -0,0 +1 @@ +Subproject commit 74f194356d1eb131fcd1b35ece16a01e7c0d0386 diff --git a/remappings.txt b/remappings.txt index 2ff87bcb1..f772baf02 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ solmate/=lib/solmate/src/ +morpho-utils/=lib/morpho-utils/src/ diff --git a/src/Blue.sol b/src/Blue.sol index 2a0c956e3..29f7058f4 100644 --- a/src/Blue.sol +++ b/src/Blue.sol @@ -5,7 +5,7 @@ import {IIrm} from "src/interfaces/IIrm.sol"; import {IERC20} from "src/interfaces/IERC20.sol"; import {IOracle} from "src/interfaces/IOracle.sol"; -import {MathLib} from "src/libraries/MathLib.sol"; +import {WadRayMath} from "morpho-utils/math/WadRayMath.sol"; import {SafeTransferLib} from "src/libraries/SafeTransferLib.sol"; uint256 constant WAD = 1e18; @@ -31,7 +31,7 @@ function toId(Market calldata market) pure returns (Id) { } contract Blue { - using MathLib for uint256; + using WadRayMath for uint256; using SafeTransferLib for IERC20; // Storage. @@ -111,7 +111,7 @@ contract Blue { supplyShare[id][msg.sender] = WAD; totalSupplyShares[id] = WAD; } else { - uint256 shares = amount.wMul(totalSupplyShares[id]).wDiv(totalSupply[id]); + uint256 shares = amount.wadDivDown(totalSupply[id]).wadMulDown(totalSupplyShares[id]); supplyShare[id][msg.sender] += shares; totalSupplyShares[id] += shares; } @@ -128,7 +128,7 @@ contract Blue { accrueInterests(market, id); - uint256 shares = amount.wMul(totalSupplyShares[id]).wDiv(totalSupply[id]); + uint256 shares = amount.wadDivUp(totalSupply[id]).wadMulUp(totalSupplyShares[id]); supplyShare[id][msg.sender] -= shares; totalSupplyShares[id] -= shares; @@ -152,7 +152,7 @@ contract Blue { borrowShare[id][msg.sender] = WAD; totalBorrowShares[id] = WAD; } else { - uint256 shares = amount.wMul(totalBorrowShares[id]).wDiv(totalBorrow[id]); + uint256 shares = amount.wadDivUp(totalBorrow[id]).wadMulUp(totalBorrowShares[id]); borrowShare[id][msg.sender] += shares; totalBorrowShares[id] += shares; } @@ -172,7 +172,7 @@ contract Blue { accrueInterests(market, id); - uint256 shares = amount.wMul(totalBorrowShares[id]).wDiv(totalBorrow[id]); + uint256 shares = amount.wadDivDown(totalBorrow[id]).wadMulDown(totalBorrowShares[id]); // TODO: totalBorrow[id] > 0 because ??? borrowShare[id][msg.sender] -= shares; totalBorrowShares[id] -= shares; @@ -222,10 +222,11 @@ contract Blue { require(!isHealthy(market, id, borrower), "cannot liquidate a healthy position"); // The liquidation incentive is 1 + ALPHA * (1 / LLTV - 1). - uint256 incentive = WAD + ALPHA.wMul(WAD.wDiv(market.lltv) - WAD); - uint256 repaid = - seized.wMul(market.collateralOracle.price()).wDiv(incentive).wDiv(market.borrowableOracle.price()); - uint256 repaidShares = repaid.wMul(totalBorrowShares[id]).wDiv(totalBorrow[id]); + uint256 incentive = WAD + ALPHA.wadMulDown(WAD.wadDivDown(market.lltv) - WAD); + uint256 repaid = seized.wadMulUp(market.collateralOracle.price()).wadDivUp(incentive).wadDivUp( + market.borrowableOracle.price() + ); + uint256 repaidShares = repaid.wadDivDown(totalBorrow[id]).wadMulDown(totalBorrowShares[id]); borrowShare[id][borrower] -= repaidShares; totalBorrowShares[id] -= repaidShares; @@ -235,7 +236,7 @@ contract Blue { // Realize the bad debt if needed. if (collateral[id][borrower] == 0) { - totalSupply[id] -= borrowShare[id][borrower].wMul(totalBorrow[id]).wDiv(totalBorrowShares[id]); + totalSupply[id] -= borrowShare[id][borrower].wadDivUp(totalBorrowShares[id]).wadMulUp(totalBorrow[id]); totalBorrowShares[id] -= borrowShare[id][borrower]; borrowShare[id][borrower] = 0; } @@ -251,7 +252,7 @@ contract Blue { if (marketTotalBorrow != 0) { uint256 borrowRate = market.irm.borrowRate(market); - uint256 accruedInterests = marketTotalBorrow.wMul(borrowRate).wMul(block.timestamp - lastUpdate[id]); + uint256 accruedInterests = marketTotalBorrow.wadMulDown(borrowRate * (block.timestamp - lastUpdate[id])); totalBorrow[id] = marketTotalBorrow + accruedInterests; totalSupply[id] += accruedInterests; } @@ -264,10 +265,12 @@ contract Blue { function isHealthy(Market calldata market, Id id, address user) private view returns (bool) { uint256 borrowShares = borrowShare[id][user]; if (borrowShares == 0) return true; + // totalBorrowShares[id] > 0 when borrowShares > 0. - uint256 borrowValue = - borrowShares.wMul(totalBorrow[id]).wDiv(totalBorrowShares[id]).wMul(market.borrowableOracle.price()); - uint256 collateralValue = collateral[id][user].wMul(market.collateralOracle.price()); - return collateralValue.wMul(market.lltv) >= borrowValue; + uint256 borrowValue = borrowShares.wadDivUp(totalBorrowShares[id]).wadMulUp(totalBorrow[id]).wadMulUp( + market.borrowableOracle.price() + ); + uint256 collateralValue = collateral[id][user].wadMulDown(market.collateralOracle.price()); + return collateralValue.wadMulDown(market.lltv) >= borrowValue; } } diff --git a/src/libraries/MathLib.sol b/src/libraries/MathLib.sol deleted file mode 100644 index 0f4f1e8e2..000000000 --- a/src/libraries/MathLib.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -/// @notice Maths utils. -library MathLib { - uint256 internal constant WAD = 1e18; - - function min(uint256 x, uint256 y) internal pure returns (uint256 z) { - z = x < y ? x : y; - } - - /// @dev Rounds towards zero. - function wMul(uint256 x, uint256 y) internal pure returns (uint256 z) { - z = (x * y) / WAD; - } - - /// @dev Rounds towards zero. - function wDiv(uint256 x, uint256 y) internal pure returns (uint256 z) { - z = (x * WAD) / y; - } -} diff --git a/src/mocks/IrmMock.sol b/src/mocks/IrmMock.sol index ae28dbdcd..99241bc7a 100644 --- a/src/mocks/IrmMock.sol +++ b/src/mocks/IrmMock.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.20; -import {MathLib} from "src/libraries/MathLib.sol"; +import {WadRayMath} from "morpho-utils/math/WadRayMath.sol"; import "src/Blue.sol"; contract IrmMock is IIrm { - using MathLib for uint256; + using WadRayMath for uint256; Blue public immutable blue; @@ -16,7 +16,7 @@ contract IrmMock is IIrm { function borrowRate(Market calldata market) external view returns (uint256) { Id id = Id.wrap(keccak256(abi.encode(market))); - uint256 utilization = blue.totalBorrow(id).wDiv(blue.totalSupply(id)); + uint256 utilization = blue.totalBorrow(id).wadDivDown(blue.totalSupply(id)); // Divide by the number of seconds in a year. // This is a very simple model (to refine later) where x% utilization corresponds to x% APR. diff --git a/test/forge/Blue.t.sol b/test/forge/Blue.t.sol index a8638c29d..e20ab7a4c 100644 --- a/test/forge/Blue.t.sol +++ b/test/forge/Blue.t.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.20; import {IERC20} from "src/interfaces/IERC20.sol"; import {IOracle} from "src/interfaces/IOracle.sol"; +import {WadRayMath} from "morpho-utils/math/WadRayMath.sol"; + import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -13,7 +15,7 @@ import {OracleMock as Oracle} from "src/mocks/OracleMock.sol"; import {IrmMock as Irm} from "src/mocks/IrmMock.sol"; contract BlueTest is Test { - using MathLib for uint256; + using WadRayMath for uint256; address private constant BORROWER = address(1234); address private constant LIQUIDATOR = address(5678); @@ -76,25 +78,27 @@ contract BlueTest is Test { // To move to a test utils file later. function netWorth(address user) internal view returns (uint256) { - uint256 collateralAssetValue = collateralAsset.balanceOf(user).wMul(collateralOracle.price()); - uint256 borrowableAssetValue = borrowableAsset.balanceOf(user).wMul(borrowableOracle.price()); + uint256 collateralAssetValue = collateralAsset.balanceOf(user).wadMulDown(collateralOracle.price()); + uint256 borrowableAssetValue = borrowableAsset.balanceOf(user).wadMulDown(borrowableOracle.price()); return collateralAssetValue + borrowableAssetValue; } function supplyBalance(address user) internal view returns (uint256) { uint256 supplyShares = blue.supplyShare(id, user); if (supplyShares == 0) return 0; + uint256 totalShares = blue.totalSupplyShares(id); uint256 totalSupply = blue.totalSupply(id); - return supplyShares.wMul(totalSupply).wDiv(totalShares); + return supplyShares.wadMulDown(totalSupply).wadDivDown(totalShares); } function borrowBalance(address user) internal view returns (uint256) { uint256 borrowerShares = blue.borrowShare(id, user); if (borrowerShares == 0) return 0; + uint256 totalShares = blue.totalBorrowShares(id); uint256 totalBorrow = blue.totalBorrow(id); - return borrowerShares.wMul(totalBorrow).wDiv(totalShares); + return borrowerShares.wadMulUp(totalBorrow).wadDivUp(totalShares); } // Invariants @@ -292,9 +296,9 @@ contract BlueTest is Test { vm.prank(BORROWER); blue.supplyCollateral(market, amountCollateral); - uint256 collateralValue = amountCollateral.wMul(priceCollateral); - uint256 borrowValue = amountBorrowed.wMul(priceBorrowable); - if (borrowValue == 0 || (collateralValue > 0 && borrowValue <= collateralValue.wMul(LLTV))) { + uint256 collateralValue = amountCollateral.wadMulDown(priceCollateral); + uint256 borrowValue = amountBorrowed.wadMulUp(priceBorrowable); + if (borrowValue == 0 || (collateralValue > 0 && borrowValue <= collateralValue.wadMulDown(LLTV))) { vm.prank(BORROWER); blue.borrow(market, amountBorrowed); } else { @@ -360,10 +364,10 @@ contract BlueTest is Test { amountLent = bound(amountLent, 1000, 2 ** 64); uint256 amountCollateral = amountLent; - uint256 borrowingPower = amountCollateral.wMul(LLTV); - uint256 amountBorrowed = borrowingPower.wMul(0.8e18); - uint256 toSeize = amountCollateral.wMul(LLTV); - uint256 incentive = WAD + ALPHA.wMul(WAD.wDiv(LLTV) - WAD); + uint256 borrowingPower = amountCollateral.wadMulDown(LLTV); + uint256 amountBorrowed = borrowingPower.wadMulDown(0.8e18); + uint256 toSeize = amountCollateral.wadMulDown(LLTV); + uint256 incentive = WAD + ALPHA.wadMulDown(WAD.wadDivDown(LLTV) - WAD); borrowableAsset.setBalance(address(this), amountLent); collateralAsset.setBalance(BORROWER, amountCollateral); @@ -389,9 +393,10 @@ contract BlueTest is Test { uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - uint256 expectedRepaid = toSeize.wMul(collateralOracle.price()).wDiv(incentive).wDiv(borrowableOracle.price()); - uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.wMul(collateralOracle.price()) - - expectedRepaid.wMul(borrowableOracle.price()); + uint256 expectedRepaid = + toSeize.wadMulUp(collateralOracle.price()).wadDivUp(incentive).wadDivUp(borrowableOracle.price()); + uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.wadMulDown(collateralOracle.price()) + - expectedRepaid.wadMulDown(borrowableOracle.price()); assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth"); assertApproxEqAbs(borrowBalance(BORROWER), amountBorrowed - expectedRepaid, 100, "BORROWER balance"); assertEq(blue.collateral(id, BORROWER), amountCollateral - toSeize, "BORROWER collateral"); @@ -402,10 +407,10 @@ contract BlueTest is Test { amountLent = bound(amountLent, 1000, 2 ** 64); uint256 amountCollateral = amountLent; - uint256 borrowingPower = amountCollateral.wMul(LLTV); - uint256 amountBorrowed = borrowingPower.wMul(0.8e18); + uint256 borrowingPower = amountCollateral.wadMulDown(LLTV); + uint256 amountBorrowed = borrowingPower.wadMulDown(0.8e18); uint256 toSeize = amountCollateral; - uint256 incentive = WAD + ALPHA.wMul(WAD.wDiv(market.lltv) - WAD); + uint256 incentive = WAD + ALPHA.wadMulDown(WAD.wadDivDown(market.lltv) - WAD); borrowableAsset.setBalance(address(this), amountLent); collateralAsset.setBalance(BORROWER, amountCollateral); @@ -431,9 +436,10 @@ contract BlueTest is Test { uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - uint256 expectedRepaid = toSeize.wMul(collateralOracle.price()).wDiv(incentive).wDiv(borrowableOracle.price()); - uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.wMul(collateralOracle.price()) - - expectedRepaid.wMul(borrowableOracle.price()); + uint256 expectedRepaid = + toSeize.wadMulUp(collateralOracle.price()).wadDivUp(incentive).wadDivUp(borrowableOracle.price()); + uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.wadMulDown(collateralOracle.price()) + - expectedRepaid.wadMulDown(borrowableOracle.price()); assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth"); assertEq(borrowBalance(BORROWER), 0, "BORROWER balance"); assertEq(blue.collateral(id, BORROWER), 0, "BORROWER collateral"); @@ -510,10 +516,10 @@ contract BlueTest is Test { function testEmptyMarket(uint256 amount) public { vm.assume(amount > 0); - vm.expectRevert(stdError.divisionError); + vm.expectRevert(); blue.withdraw(market, amount); - vm.expectRevert(stdError.divisionError); + vm.expectRevert(); blue.repay(market, amount); vm.expectRevert(stdError.arithmeticError);