diff --git a/src/Blue.sol b/src/Blue.sol index 660ffd91c..d87adadc0 100644 --- a/src/Blue.sol +++ b/src/Blue.sol @@ -277,15 +277,14 @@ contract Blue is IBlue { _accrueInterests(market, id); - uint256 collateralPrice = IOracle(market.collateralOracle).price(); - uint256 borrowablePrice = IOracle(market.borrowableOracle).price(); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); - require(!_isHealthy(market, id, borrower, collateralPrice, borrowablePrice), Errors.HEALTHY_POSITION); + require(!_isHealthy(market, id, borrower, collateralPrice, priceScale), Errors.HEALTHY_POSITION); // The liquidation incentive is 1 + ALPHA * (1 / LLTV - 1). uint256 incentive = FixedPointMathLib.WAD + ALPHA.mulWadDown(FixedPointMathLib.WAD.divWadDown(market.lltv) - FixedPointMathLib.WAD); - uint256 repaid = seized.mulWadUp(collateralPrice).divWadUp(incentive).divWadUp(borrowablePrice); + uint256 repaid = seized.mulDivUp(collateralPrice, priceScale).divWadUp(incentive); uint256 repaidShares = repaid.toSharesDown(totalBorrow[id], totalBorrowShares[id]); borrowShares[id][borrower] -= repaidShares; @@ -398,22 +397,20 @@ contract Blue is IBlue { function _isHealthy(Market memory market, Id id, address user) internal view returns (bool) { if (borrowShares[id][user] == 0) return true; - uint256 collateralPrice = IOracle(market.collateralOracle).price(); - uint256 borrowablePrice = IOracle(market.borrowableOracle).price(); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); - return _isHealthy(market, id, user, collateralPrice, borrowablePrice); + return _isHealthy(market, id, user, collateralPrice, priceScale); } - function _isHealthy(Market memory market, Id id, address user, uint256 collateralPrice, uint256 borrowablePrice) + function _isHealthy(Market memory market, Id id, address user, uint256 collateralPrice, uint256 priceScale) internal view returns (bool) { - uint256 borrowValue = - borrowShares[id][user].toAssetsUp(totalBorrow[id], totalBorrowShares[id]).mulWadUp(borrowablePrice); - uint256 collateralValue = collateral[id][user].mulWadDown(collateralPrice); + uint256 borrowed = borrowShares[id][user].toAssetsUp(totalBorrow[id], totalBorrowShares[id]); + uint256 maxBorrow = collateral[id][user].mulDivDown(collateralPrice, priceScale).mulWadDown(market.lltv); - return collateralValue.mulWadDown(market.lltv) >= borrowValue; + return maxBorrow >= borrowed; } // Storage view. diff --git a/src/interfaces/IBlue.sol b/src/interfaces/IBlue.sol index 6ad39e8dd..00e589a27 100644 --- a/src/interfaces/IBlue.sol +++ b/src/interfaces/IBlue.sol @@ -8,8 +8,7 @@ type Id is bytes32; struct Market { address borrowableAsset; address collateralAsset; - address borrowableOracle; - address collateralOracle; + address oracle; address irm; uint256 lltv; } diff --git a/src/interfaces/IOracle.sol b/src/interfaces/IOracle.sol index 6da75e903..1cb7f9daf 100644 --- a/src/interfaces/IOracle.sol +++ b/src/interfaces/IOracle.sol @@ -2,5 +2,6 @@ pragma solidity >=0.5.0; interface IOracle { - function price() external view returns (uint256); + /// @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); } diff --git a/src/mocks/OracleMock.sol b/src/mocks/OracleMock.sol index 40207cc48..8792ffa2e 100644 --- a/src/mocks/OracleMock.sol +++ b/src/mocks/OracleMock.sol @@ -3,10 +3,16 @@ pragma solidity ^0.8.0; import {IOracle} from "../interfaces/IOracle.sol"; +import {FixedPointMathLib} from "src/libraries/FixedPointMathLib.sol"; + contract OracleMock is IOracle { - uint256 public price; + uint256 internal _price; + + function price() external view returns (uint256, uint256) { + return (_price, FixedPointMathLib.WAD); + } function setPrice(uint256 newPrice) external { - price = newPrice; + _price = newPrice; } } diff --git a/test/forge/Blue.t.sol b/test/forge/Blue.t.sol index c0940f99c..cad1f55a6 100644 --- a/test/forge/Blue.t.sol +++ b/test/forge/Blue.t.sol @@ -40,8 +40,7 @@ contract BlueTest is IBlue private blue; ERC20 private borrowableAsset; ERC20 private collateralAsset; - Oracle private borrowableOracle; - Oracle private collateralOracle; + Oracle private oracle; Irm private irm; Market public market; Id public id; @@ -53,19 +52,11 @@ contract BlueTest is // List a market. borrowableAsset = new ERC20("borrowable", "B", 18); collateralAsset = new ERC20("collateral", "C", 18); - borrowableOracle = new Oracle(); - collateralOracle = new Oracle(); + oracle = new Oracle(); irm = new Irm(blue); - market = Market( - address(borrowableAsset), - address(collateralAsset), - address(borrowableOracle), - address(collateralOracle), - address(irm), - LLTV - ); + market = Market(address(borrowableAsset), address(collateralAsset), address(oracle), address(irm), LLTV); id = market.id(); vm.startPrank(OWNER); @@ -74,29 +65,31 @@ contract BlueTest is blue.createMarket(market); vm.stopPrank(); - // We set the price of the borrowable asset to zero so that borrowers - // don't need to deposit any collateral. - borrowableOracle.setPrice(0); - collateralOracle.setPrice(1e18); + oracle.setPrice(FixedPointMathLib.WAD); borrowableAsset.approve(address(blue), type(uint256).max); collateralAsset.approve(address(blue), type(uint256).max); + vm.startPrank(BORROWER); borrowableAsset.approve(address(blue), type(uint256).max); collateralAsset.approve(address(blue), type(uint256).max); blue.setAuthorization(address(this), true); vm.stopPrank(); + vm.startPrank(LIQUIDATOR); borrowableAsset.approve(address(blue), type(uint256).max); collateralAsset.approve(address(blue), type(uint256).max); vm.stopPrank(); } - // To move to a test utils file later. - + /// @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 collateralAssetValue = collateralAsset.balanceOf(user).mulWadDown(collateralOracle.price()); - uint256 borrowableAssetValue = borrowableAsset.balanceOf(user).mulWadDown(borrowableOracle.price()); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); + + uint256 collateralAssetValue = collateralAsset.balanceOf(user).mulDivDown(collateralPrice, priceScale); + uint256 borrowableAssetValue = borrowableAsset.balanceOf(user); + return collateralAssetValue + borrowableAssetValue; } @@ -283,6 +276,10 @@ contract BlueTest is borrowableAsset.setBalance(address(this), amountLent); blue.supply(market, amountLent, address(this), hex""); + uint256 collateralAmount = amountBorrowed.divWadUp(LLTV); + collateralAsset.setBalance(address(this), collateralAmount); + blue.supplyCollateral(market, collateralAmount, BORROWER, hex""); + vm.prank(BORROWER); blue.borrow(market, amountBorrowed, BORROWER, BORROWER); @@ -337,6 +334,10 @@ contract BlueTest is borrowableAsset.setBalance(address(this), amountLent); blue.supply(market, amountLent, address(this), hex""); + uint256 collateralAmount = amountBorrowed.divWadUp(LLTV); + collateralAsset.setBalance(address(this), collateralAmount); + blue.supplyCollateral(market, collateralAmount, BORROWER, hex""); + if (amountBorrowed > amountLent) { vm.prank(BORROWER); vm.expectRevert(bytes(Errors.INSUFFICIENT_LIQUIDITY)); @@ -375,6 +376,11 @@ contract BlueTest is _testWithdrawCommon(amountLent); amountBorrowed = bound(amountBorrowed, 1, blue.totalSupply(id)); + + uint256 collateralAmount = amountBorrowed.divWadUp(LLTV); + collateralAsset.setBalance(address(this), collateralAmount); + blue.supplyCollateral(market, collateralAmount, BORROWER, hex""); + blue.borrow(market, amountBorrowed, BORROWER, BORROWER); uint256 totalSupplyBefore = blue.totalSupply(id); @@ -435,6 +441,11 @@ contract BlueTest is borrowableAsset.setBalance(address(this), 2 ** 66); blue.supply(market, amountBorrowed, address(this), hex""); + + uint256 collateralAmount = amountBorrowed.divWadUp(LLTV); + collateralAsset.setBalance(address(this), collateralAmount); + blue.supplyCollateral(market, collateralAmount, borrower, hex""); + vm.prank(borrower); blue.borrow(market, amountBorrowed, borrower, borrower); @@ -537,19 +548,14 @@ contract BlueTest is assertEq(collateralAsset.balanceOf(address(blue)), 0, "blue balance"); } - function testCollateralRequirements( - uint256 amountCollateral, - uint256 amountBorrowed, - uint256 priceCollateral, - uint256 priceBorrowable - ) public { + function testCollateralRequirements(uint256 amountCollateral, uint256 amountBorrowed, uint256 collateralPrice) + public + { amountBorrowed = bound(amountBorrowed, 1, 2 ** 64); - priceBorrowable = bound(priceBorrowable, 0, 2 ** 64); amountCollateral = bound(amountCollateral, 1, 2 ** 64); - priceCollateral = bound(priceCollateral, 0, 2 ** 64); + collateralPrice = bound(collateralPrice, 0, 2 ** 64); - borrowableOracle.setPrice(priceBorrowable); - collateralOracle.setPrice(priceCollateral); + oracle.setPrice(collateralPrice); borrowableAsset.setBalance(address(this), amountBorrowed); collateralAsset.setBalance(BORROWER, amountCollateral); @@ -559,20 +565,15 @@ contract BlueTest is vm.prank(BORROWER); blue.supplyCollateral(market, amountCollateral, BORROWER, hex""); - uint256 collateralValue = amountCollateral.mulWadDown(priceCollateral); - uint256 borrowValue = amountBorrowed.mulWadUp(priceBorrowable); - if (borrowValue == 0 || (collateralValue > 0 && borrowValue <= collateralValue.mulWadDown(LLTV))) { - vm.prank(BORROWER); - blue.borrow(market, amountBorrowed, BORROWER, BORROWER); - } else { - vm.prank(BORROWER); - vm.expectRevert(bytes(Errors.INSUFFICIENT_COLLATERAL)); - blue.borrow(market, amountBorrowed, BORROWER, BORROWER); - } + uint256 maxBorrow = amountCollateral.mulWadDown(collateralPrice).mulWadDown(LLTV); + + vm.prank(BORROWER); + if (maxBorrow < amountBorrowed) vm.expectRevert(bytes(Errors.INSUFFICIENT_COLLATERAL)); + blue.borrow(market, amountBorrowed, BORROWER, BORROWER); } function testLiquidate(uint256 amountLent) public { - borrowableOracle.setPrice(1e18); + oracle.setPrice(1e18); amountLent = bound(amountLent, 1000, 2 ** 64); uint256 amountCollateral = amountLent; @@ -596,7 +597,7 @@ contract BlueTest is vm.stopPrank(); // Price change - borrowableOracle.setPrice(2e18); + oracle.setPrice(0.5e18); uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR); @@ -605,18 +606,18 @@ contract BlueTest is blue.liquidate(market, BORROWER, toSeize, hex""); uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); - uint256 expectedRepaid = - toSeize.mulWadUp(collateralOracle.price()).divWadUp(incentive).divWadUp(borrowableOracle.price()); - uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.mulWadDown(collateralOracle.price()) - - expectedRepaid.mulWadDown(borrowableOracle.price()); + uint256 expectedRepaid = toSeize.mulDivUp(collateralPrice, priceScale).divWadUp(incentive); + uint256 expectedNetWorthAfter = + liquidatorNetWorthBefore + toSeize.mulDivDown(collateralPrice, priceScale) - expectedRepaid; assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth"); assertApproxEqAbs(borrowBalance(BORROWER), amountBorrowed - expectedRepaid, 100, "BORROWER balance"); assertEq(blue.collateral(id, BORROWER), amountCollateral - toSeize, "BORROWER collateral"); } function testRealizeBadDebt(uint256 amountLent) public { - borrowableOracle.setPrice(1e18); + oracle.setPrice(1e18); amountLent = bound(amountLent, 1000, 2 ** 64); uint256 amountCollateral = amountLent; @@ -640,7 +641,7 @@ contract BlueTest is vm.stopPrank(); // Price change - borrowableOracle.setPrice(100e18); + oracle.setPrice(0.01e18); uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR); @@ -649,11 +650,11 @@ contract BlueTest is blue.liquidate(market, BORROWER, toSeize, hex""); uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); - uint256 expectedRepaid = - toSeize.mulWadUp(collateralOracle.price()).divWadUp(incentive).divWadUp(borrowableOracle.price()); - uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.mulWadDown(collateralOracle.price()) - - expectedRepaid.mulWadDown(borrowableOracle.price()); + uint256 expectedRepaid = toSeize.mulDivUp(collateralPrice, priceScale).divWadUp(incentive); + uint256 expectedNetWorthAfter = + liquidatorNetWorthBefore + toSeize.mulDivDown(collateralPrice, priceScale) - expectedRepaid; assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth"); assertEq(borrowBalance(BORROWER), 0, "BORROWER balance"); assertEq(blue.collateral(id, BORROWER), 0, "BORROWER collateral"); @@ -885,40 +886,45 @@ contract BlueTest is function testRepayCallback(uint256 amount) public { amount = bound(amount, 1, 2 ** 64); + borrowableAsset.setBalance(address(this), amount); blue.supply(market, amount, address(this), hex""); + + uint256 collateralAmount = amount.divWadUp(LLTV); + collateralAsset.setBalance(address(this), collateralAmount); + blue.supplyCollateral(market, collateralAmount, address(this), hex""); blue.borrow(market, amount, address(this), address(this)); borrowableAsset.approve(address(blue), 0); - vm.expectRevert(); + vm.expectRevert("TRANSFER_FROM_FAILED"); blue.repay(market, amount, address(this), hex""); blue.repay(market, amount, address(this), abi.encode(this.testRepayCallback.selector, hex"")); } function testLiquidateCallback(uint256 amount) public { amount = bound(amount, 10, 2 ** 64); - borrowableOracle.setPrice(1e18); + borrowableAsset.setBalance(address(this), amount); - collateralAsset.setBalance(address(this), amount); blue.supply(market, amount, address(this), hex""); - blue.supplyCollateral(market, amount, address(this), hex""); - blue.borrow(market, amount.mulWadDown(LLTV), address(this), address(this)); - borrowableOracle.setPrice(1.01e18); + uint256 collateralAmount = amount.divWadUp(LLTV); + collateralAsset.setBalance(address(this), collateralAmount); + blue.supplyCollateral(market, collateralAmount, address(this), hex""); + blue.borrow(market, amount, address(this), address(this)); - uint256 toSeize = amount.mulWadDown(LLTV); + oracle.setPrice(0.5e18); - borrowableAsset.setBalance(address(this), toSeize); + borrowableAsset.setBalance(address(this), amount); borrowableAsset.approve(address(blue), 0); - vm.expectRevert(); - blue.liquidate(market, address(this), toSeize, hex""); - blue.liquidate(market, address(this), toSeize, abi.encode(this.testLiquidateCallback.selector, hex"")); + vm.expectRevert("TRANSFER_FROM_FAILED"); + blue.liquidate(market, address(this), collateralAmount, hex""); + blue.liquidate(market, address(this), collateralAmount, abi.encode(this.testLiquidateCallback.selector, hex"")); } function testFlashActions(uint256 amount) public { amount = bound(amount, 10, 2 ** 64); - borrowableOracle.setPrice(1e18); + oracle.setPrice(1e18); uint256 toBorrow = amount.mulWadDown(LLTV); borrowableAsset.setBalance(address(this), 2 * toBorrow); @@ -990,7 +996,6 @@ contract BlueTest is } function neq(Market memory a, Market memory b) pure returns (bool) { - return a.borrowableAsset != b.borrowableAsset || a.collateralAsset != b.collateralAsset - || a.borrowableOracle != b.borrowableOracle || a.collateralOracle != b.collateralOracle || a.lltv != b.lltv - || a.irm != b.irm; + return a.borrowableAsset != b.borrowableAsset || a.collateralAsset != b.collateralAsset || a.oracle != b.oracle + || a.lltv != b.lltv || a.irm != b.irm; } diff --git a/test/hardhat/Blue.spec.ts b/test/hardhat/Blue.spec.ts index 981baea6e..1d715be7c 100644 --- a/test/hardhat/Blue.spec.ts +++ b/test/hardhat/Blue.spec.ts @@ -5,6 +5,7 @@ import { expect } from "chai"; import { BigNumber, constants, utils } from "ethers"; import hre from "hardhat"; import { Blue, OracleMock, ERC20Mock, IrmMock } from "types"; +import { MarketStruct } from "types/src/Blue"; import { FlashBorrowerMock } from "types/src/mocks/FlashBorrowerMock"; const closePositions = false; @@ -17,24 +18,15 @@ const random = () => { return (seed - 1) / 2147483646; }; -const identifier = (market: Market) => { +const identifier = (market: MarketStruct) => { const encodedMarket = defaultAbiCoder.encode( - ["address", "address", "address", "address", "address", "uint256"], + ["address", "address", "address", "address", "uint256"], Object.values(market), ); return Buffer.from(utils.keccak256(encodedMarket).slice(2), "hex"); }; -interface Market { - borrowableAsset: string; - collateralAsset: string; - borrowableOracle: string; - collateralOracle: string; - irm: string; - lltv: BigNumber; -} - describe("Blue", () => { let signers: SignerWithAddress[]; let admin: SignerWithAddress; @@ -43,17 +35,16 @@ describe("Blue", () => { let blue: Blue; let borrowable: ERC20Mock; let collateral: ERC20Mock; - let borrowableOracle: OracleMock; - let collateralOracle: OracleMock; + let oracle: OracleMock; let irm: IrmMock; let flashBorrower: FlashBorrowerMock; - let market: Market; + let market: MarketStruct; let id: Buffer; let nbLiquidations: number; - const updateMarket = (newMarket: Partial) => { + const updateMarket = (newMarket: Partial) => { market = { ...market, ...newMarket }; id = identifier(market); }; @@ -73,11 +64,9 @@ describe("Blue", () => { const OracleMockFactory = await hre.ethers.getContractFactory("OracleMock", admin); - borrowableOracle = await OracleMockFactory.deploy(); - collateralOracle = await OracleMockFactory.deploy(); + oracle = await OracleMockFactory.deploy(); - await borrowableOracle.setPrice(BigNumber.WAD); - await collateralOracle.setPrice(BigNumber.WAD); + await oracle.setPrice(BigNumber.WAD); const BlueFactory = await hre.ethers.getContractFactory("Blue", admin); @@ -90,8 +79,7 @@ describe("Blue", () => { updateMarket({ borrowableAsset: borrowable.address, collateralAsset: collateral.address, - borrowableOracle: borrowableOracle.address, - collateralOracle: collateralOracle.address, + oracle: oracle.address, irm: irm.address, lltv: BigNumber.WAD.div(2).add(1), }); @@ -136,7 +124,14 @@ describe("Blue", () => { const totalSupplyShares = await blue.totalSupplyShares(id); Promise.all([ blue.connect(user).supply(market, amount, user.address, []), - blue.connect(user).withdraw(market, amount.mul(totalSupplyShares.add(BigNumber.WAD)).div(totalSupply.add(1)).div(2), user.address, user.address), + blue + .connect(user) + .withdraw( + market, + amount.mul(totalSupplyShares.add(BigNumber.WAD)).div(totalSupply.add(1)).div(2), + user.address, + user.address, + ), ]); } else { const totalSupply = await blue.totalSupply(id); @@ -150,7 +145,14 @@ describe("Blue", () => { Promise.all([ blue.connect(user).supplyCollateral(market, amount, user.address, []), blue.connect(user).borrow(market, amount.div(2), user.address, user.address), - blue.connect(user).repay(market, amount.mul(totalBorrowShares.add(BigNumber.WAD)).div(totalBorrow.add(1)).div(4), user.address, []), + blue + .connect(user) + .repay( + market, + amount.mul(totalBorrowShares.add(BigNumber.WAD)).div(totalBorrow.add(1)).div(4), + user.address, + [], + ), blue.connect(user).withdrawCollateral(market, amount.div(8), user.address, user.address), ]); } @@ -188,7 +190,7 @@ describe("Blue", () => { await blue.connect(borrower).supplyCollateral(market, amount, borrower.address, "0x"); await blue.connect(borrower).borrow(market, borrowedAmount, borrower.address, user.address); - await borrowableOracle.setPrice(BigNumber.WAD.mul(1000)); + await oracle.setPrice(BigNumber.WAD.div(10)); const seized = closePositions ? constants.MaxUint256 : amount.div(2); @@ -200,7 +202,7 @@ describe("Blue", () => { 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 borrowableOracle.setPrice(BigNumber.WAD); + await oracle.setPrice(BigNumber.WAD); } });