Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed oracle price scale #252

Merged
merged 6 commits into from
Aug 13, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
17 changes: 10 additions & 7 deletions src/Morpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {FixedPointMathLib, WAD} from "./libraries/FixedPointMathLib.sol";

/// @dev The maximum fee a market can have (25%).
uint256 constant MAX_FEE = 0.25e18;
/// @dev Oracle price scale.
uint256 constant ORACLE_PRICE_SCALE = 1e36;
/// @dev Liquidation cursor.
uint256 constant LIQUIDATION_CURSOR = 0.3e18;
/// @dev Max liquidation incentive factor.
Expand Down Expand Up @@ -325,11 +327,12 @@ contract Morpho is IMorpho {

_accrueInterests(market, id);

(uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price();
uint256 collateralPrice = IOracle(market.oracle).price();

require(!_isHealthy(market, id, borrower, collateralPrice, priceScale), ErrorsLib.HEALTHY_POSITION);
require(!_isHealthy(market, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION);

uint256 repaid = seized.mulDivUp(collateralPrice, priceScale).wDivUp(liquidationIncentiveFactor(market.lltv));
uint256 repaid =
seized.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor(market.lltv));
uint256 repaidShares = repaid.toSharesDown(totalBorrow[id], totalBorrowShares[id]);

borrowShares[id][borrower] -= repaidShares;
Expand Down Expand Up @@ -455,19 +458,19 @@ contract Morpho is IMorpho {
function _isHealthy(Market memory market, Id id, address user) internal view returns (bool) {
if (borrowShares[id][user] == 0) return true;

(uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price();
uint256 collateralPrice = IOracle(market.oracle).price();

return _isHealthy(market, id, user, collateralPrice, priceScale);
return _isHealthy(market, id, user, collateralPrice);
}

/// @notice Returns whether the position of `user` in the given `market` with the given `collateralPrice` and `priceScale` is healthy.
function _isHealthy(Market memory market, Id id, address user, uint256 collateralPrice, uint256 priceScale)
function _isHealthy(Market memory market, Id id, address user, uint256 collateralPrice)
internal
view
returns (bool)
{
uint256 borrowed = borrowShares[id][user].toAssetsUp(totalBorrow[id], totalBorrowShares[id]);
uint256 maxBorrow = collateral[id][user].mulDivDown(collateralPrice, priceScale).wMulDown(market.lltv);
uint256 maxBorrow = collateral[id][user].mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(market.lltv);

return maxBorrow >= borrowed;
}
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/IOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ pragma solidity >=0.5.0;
/// @custom:contact [email protected]
/// @notice Interface that oracles used by Morpho must implement.
interface IOracle {
/// @notice Returns the price of the collateral asset quoted in the borrowable asset and the price's unit scale.
function price() external view returns (uint256 collateralPrice, uint256 scale);
/// @notice Returns the price of the collateral asset quoted in the borrowable asset, scaled by 1e36.
function price() external view returns (uint256);
}
8 changes: 2 additions & 6 deletions src/mocks/OracleMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ import {IOracle} from "../interfaces/IOracle.sol";
import {FixedPointMathLib, WAD} from "../libraries/FixedPointMathLib.sol";

contract OracleMock is IOracle {
uint256 internal _price;

function price() external view returns (uint256, uint256) {
return (_price, WAD);
}
uint256 public price;

function setPrice(uint256 newPrice) external {
_price = newPrice;
price = newPrice;
}
}
34 changes: 18 additions & 16 deletions test/forge/Morpho.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ contract MorphoTest is
morpho.createMarket(market);
vm.stopPrank();

oracle.setPrice(WAD);
oracle.setPrice(ORACLE_PRICE_SCALE);

borrowableToken.approve(address(morpho), type(uint256).max);
collateralToken.approve(address(morpho), type(uint256).max);
Expand All @@ -83,9 +83,9 @@ contract MorphoTest is
/// @dev Calculates the net worth of the given user quoted in borrowable asset.
// TODO: To move to a test utils file later.
function netWorth(address user) internal view returns (uint256) {
(uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price();
uint256 collateralPrice = IOracle(market.oracle).price();

uint256 collateralAssetValue = collateralToken.balanceOf(user).mulDivDown(collateralPrice, priceScale);
uint256 collateralAssetValue = collateralToken.balanceOf(user).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE);
uint256 borrowableAssetValue = borrowableToken.balanceOf(user);

return collateralAssetValue + borrowableAssetValue;
Expand Down Expand Up @@ -603,15 +603,15 @@ contract MorphoTest is
vm.prank(BORROWER);
morpho.supplyCollateral(market, assetsCollateral, BORROWER, hex"");

uint256 maxBorrow = assetsCollateral.wMulDown(collateralPrice).wMulDown(LLTV);
uint256 maxBorrow = assetsCollateral.mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(LLTV);

vm.prank(BORROWER);
if (maxBorrow < assetsBorrowed) vm.expectRevert(bytes(ErrorsLib.INSUFFICIENT_COLLATERAL));
morpho.borrow(market, assetsBorrowed, 0, BORROWER, BORROWER);
}

function testLiquidate(uint256 assetsLent) public {
oracle.setPrice(1e18);
oracle.setPrice(ORACLE_PRICE_SCALE);
assetsLent = bound(assetsLent, 1000, 2 ** 64);

uint256 assetsCollateral = assetsLent;
Expand All @@ -635,7 +635,7 @@ contract MorphoTest is
vm.stopPrank();

// Price change
oracle.setPrice(0.5e18);
oracle.setPrice(ORACLE_PRICE_SCALE / 2);

uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR);

Expand All @@ -644,18 +644,19 @@ contract MorphoTest is
morpho.liquidate(market, BORROWER, toSeize, hex"");

uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR);
(uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price();
uint256 collateralPrice = IOracle(market.oracle).price();

uint256 expectedRepaid = toSeize.mulDivUp(collateralPrice, priceScale).wDivUp(liquidationIncentiveFactor);
uint256 expectedRepaid =
toSeize.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor);
uint256 expectedNetWorthAfter =
liquidatorNetWorthBefore + toSeize.mulDivDown(collateralPrice, priceScale) - expectedRepaid;
liquidatorNetWorthBefore + toSeize.mulDivDown(collateralPrice, ORACLE_PRICE_SCALE) - expectedRepaid;
assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth");
assertApproxEqAbs(borrowBalance(BORROWER), assetsBorrowed - expectedRepaid, 100, "BORROWER balance");
assertEq(morpho.collateral(id, BORROWER), assetsCollateral - toSeize, "BORROWER collateral");
}

function testRealizeBadDebt(uint256 assetsLent) public {
oracle.setPrice(1e18);
oracle.setPrice(ORACLE_PRICE_SCALE);
assetsLent = bound(assetsLent, 1000, 2 ** 64);

uint256 assetsCollateral = assetsLent;
Expand All @@ -679,7 +680,7 @@ contract MorphoTest is
vm.stopPrank();

// Price change
oracle.setPrice(0.01e18);
oracle.setPrice(ORACLE_PRICE_SCALE / 100);

uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR);

Expand All @@ -688,11 +689,12 @@ contract MorphoTest is
morpho.liquidate(market, BORROWER, toSeize, hex"");

uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR);
(uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price();
uint256 collateralPrice = IOracle(market.oracle).price();

uint256 expectedRepaid = toSeize.mulDivUp(collateralPrice, priceScale).wDivUp(liquidationIncentiveFactor);
uint256 expectedRepaid =
toSeize.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor);
uint256 expectedNetWorthAfter =
liquidatorNetWorthBefore + toSeize.mulDivDown(collateralPrice, priceScale) - expectedRepaid;
liquidatorNetWorthBefore + toSeize.mulDivDown(collateralPrice, ORACLE_PRICE_SCALE) - expectedRepaid;
assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth");
assertEq(borrowBalance(BORROWER), 0, "BORROWER balance");
assertEq(morpho.collateral(id, BORROWER), 0, "BORROWER collateral");
Expand Down Expand Up @@ -961,7 +963,7 @@ contract MorphoTest is
morpho.supplyCollateral(market, collateralAmount, address(this), hex"");
morpho.borrow(market, assets.wMulDown(LLTV), 0, address(this), address(this));

oracle.setPrice(0.5e18);
oracle.setPrice(ORACLE_PRICE_SCALE / 2);

borrowableToken.setBalance(address(this), assets);
borrowableToken.approve(address(morpho), 0);
Expand All @@ -974,7 +976,7 @@ contract MorphoTest is

function testFlashActions(uint256 assets) public {
assets = bound(assets, 10, 2 ** 64);
oracle.setPrice(1e18);
oracle.setPrice(ORACLE_PRICE_SCALE);
uint256 toBorrow = assets.wMulDown(LLTV);

borrowableToken.setBalance(address(this), 2 * toBorrow);
Expand Down
7 changes: 4 additions & 3 deletions test/hardhat/Morpho.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FlashBorrowerMock } from "types/src/mocks/FlashBorrowerMock";

const closePositions = false;
const initBalance = constants.MaxUint256.div(2);
const oraclePriceScale = BigNumber.from("1000000000000000000000000000000000000");
MathisGD marked this conversation as resolved.
Show resolved Hide resolved

let seed = 42;
const random = () => {
Expand Down Expand Up @@ -66,7 +67,7 @@ describe("Morpho", () => {

oracle = await OracleMockFactory.deploy();

await oracle.setPrice(BigNumber.WAD);
await oracle.setPrice(oraclePriceScale);

const MorphoFactory = await hre.ethers.getContractFactory("Morpho", admin);

Expand Down Expand Up @@ -173,7 +174,7 @@ describe("Morpho", () => {
await morpho.connect(borrower).supplyCollateral(market, assets, borrower.address, "0x");
await morpho.connect(borrower).borrow(market, borrowedAmount, 0, borrower.address, user.address);

await oracle.setPrice(BigNumber.WAD.div(100));
await oracle.setPrice(oraclePriceScale.div(100));

const seized = closePositions ? assets : assets.div(2);

Expand All @@ -185,7 +186,7 @@ describe("Morpho", () => {
expect(remainingCollateral.isZero(), "did not take the whole collateral when closing the position").to.be.true;
else expect(!remainingCollateral.isZero(), "unexpectedly closed the position").to.be.true;

await oracle.setPrice(BigNumber.WAD);
await oracle.setPrice(oraclePriceScale);
}
});

Expand Down