From 8aea0dba22467221ba7a40c90dff90d8d3881568 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 7 Aug 2023 10:31:22 +0200 Subject: [PATCH 1/9] refactor(blue): use single oracle --- src/Blue.sol | 18 +++++----- src/interfaces/IBlue.sol | 3 +- test/forge/Blue.t.sol | 72 ++++++++++++++-------------------------- 3 files changed, 34 insertions(+), 59 deletions(-) diff --git a/src/Blue.sol b/src/Blue.sol index 660ffd91c..d0215e478 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 borrowablePrice = IOracle(market.oracle).price(); - require(!_isHealthy(market, id, borrower, collateralPrice, borrowablePrice), Errors.HEALTHY_POSITION); + require(!_isHealthy(market, id, borrower, borrowablePrice), 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.divWadUp(incentive).divWadUp(borrowablePrice); uint256 repaidShares = repaid.toSharesDown(totalBorrow[id], totalBorrowShares[id]); borrowShares[id][borrower] -= repaidShares; @@ -398,22 +397,21 @@ 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 borrowablePrice = IOracle(market.oracle).price(); - return _isHealthy(market, id, user, collateralPrice, borrowablePrice); + return _isHealthy(market, id, user, borrowablePrice); } - function _isHealthy(Market memory market, Id id, address user, uint256 collateralPrice, uint256 borrowablePrice) + function _isHealthy(Market memory market, Id id, address user, uint256 borrowablePrice) internal view returns (bool) { uint256 borrowValue = borrowShares[id][user].toAssetsUp(totalBorrow[id], totalBorrowShares[id]).mulWadUp(borrowablePrice); - uint256 collateralValue = collateral[id][user].mulWadDown(collateralPrice); + uint256 collateralPower = collateral[id][user].mulWadDown(market.lltv); - return collateralValue.mulWadDown(market.lltv) >= borrowValue; + return collateralPower >= borrowValue; } // 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/test/forge/Blue.t.sol b/test/forge/Blue.t.sol index c0940f99c..d197dc6cc 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,10 +65,8 @@ 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); + // We set the price of the borrowable asset to zero so that borrowers don't need to deposit any collateral. + oracle.setPrice(0); borrowableAsset.approve(address(blue), type(uint256).max); collateralAsset.approve(address(blue), type(uint256).max); @@ -95,8 +84,8 @@ contract BlueTest is // 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 collateralAssetValue = collateralAsset.balanceOf(user); + uint256 borrowableAssetValue = borrowableAsset.balanceOf(user).mulWadDown(oracle.price()); return collateralAssetValue + borrowableAssetValue; } @@ -537,19 +526,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 priceBorrowable) + 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); - borrowableOracle.setPrice(priceBorrowable); - collateralOracle.setPrice(priceCollateral); + oracle.setPrice(priceBorrowable); borrowableAsset.setBalance(address(this), amountBorrowed); collateralAsset.setBalance(BORROWER, amountCollateral); @@ -559,9 +543,8 @@ 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))) { + if (borrowValue == 0 || (amountCollateral > 0 && borrowValue <= amountCollateral.mulWadDown(LLTV))) { vm.prank(BORROWER); blue.borrow(market, amountBorrowed, BORROWER, BORROWER); } else { @@ -572,7 +555,7 @@ contract BlueTest is } function testLiquidate(uint256 amountLent) public { - borrowableOracle.setPrice(1e18); + oracle.setPrice(1e18); amountLent = bound(amountLent, 1000, 2 ** 64); uint256 amountCollateral = amountLent; @@ -596,7 +579,7 @@ contract BlueTest is vm.stopPrank(); // Price change - borrowableOracle.setPrice(2e18); + oracle.setPrice(2e18); uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR); @@ -606,17 +589,15 @@ contract BlueTest is uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - 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.divWadUp(incentive).divWadUp(oracle.price()); + uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize - expectedRepaid.mulWadDown(oracle.price()); 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 +621,7 @@ contract BlueTest is vm.stopPrank(); // Price change - borrowableOracle.setPrice(100e18); + oracle.setPrice(100e18); uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR); @@ -650,10 +631,8 @@ contract BlueTest is uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - 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.divWadUp(incentive).divWadUp(oracle.price()); + uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize - expectedRepaid.mulWadDown(oracle.price()); assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth"); assertEq(borrowBalance(BORROWER), 0, "BORROWER balance"); assertEq(blue.collateral(id, BORROWER), 0, "BORROWER collateral"); @@ -898,14 +877,14 @@ contract BlueTest is function testLiquidateCallback(uint256 amount) public { amount = bound(amount, 10, 2 ** 64); - borrowableOracle.setPrice(1e18); + oracle.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); + oracle.setPrice(1.01e18); uint256 toSeize = amount.mulWadDown(LLTV); @@ -918,7 +897,7 @@ contract BlueTest is 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 +969,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; } From 77646d43ed5bca316b1c2f8648808822bb490514 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 31 Jul 2023 14:16:48 +0200 Subject: [PATCH 2/9] test(hardhat): update hardhat tests --- test/hardhat/Blue.spec.ts | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/test/hardhat/Blue.spec.ts b/test/hardhat/Blue.spec.ts index 981baea6e..a5aa88ae7 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,7 +18,7 @@ const random = () => { return (seed - 1) / 2147483646; }; -const identifier = (market: Market) => { +const identifier = (market: MarketStruct) => { const encodedMarket = defaultAbiCoder.encode( ["address", "address", "address", "address", "address", "uint256"], Object.values(market), @@ -26,15 +27,6 @@ const identifier = (market: 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), }); @@ -188,7 +176,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.mul(1000)); const seized = closePositions ? constants.MaxUint256 : amount.div(2); @@ -200,7 +188,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); } }); From eb725733136e7d2ee26f8bd8459daf56147217c7 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 7 Aug 2023 10:40:51 +0200 Subject: [PATCH 3/9] test(forge): refactor to supply collateral before borrow --- src/Blue.sol | 19 +++++----- test/forge/Blue.t.sol | 84 ++++++++++++++++++++++++++----------------- 2 files changed, 60 insertions(+), 43 deletions(-) diff --git a/src/Blue.sol b/src/Blue.sol index d0215e478..18ad20a15 100644 --- a/src/Blue.sol +++ b/src/Blue.sol @@ -277,14 +277,14 @@ contract Blue is IBlue { _accrueInterests(market, id); - uint256 borrowablePrice = IOracle(market.oracle).price(); + uint256 collateralPrice = IOracle(market.oracle).price(); - require(!_isHealthy(market, id, borrower, borrowablePrice), Errors.HEALTHY_POSITION); + require(!_isHealthy(market, id, borrower, collateralPrice), 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.divWadUp(incentive).divWadUp(borrowablePrice); + uint256 repaid = seized.mulWadUp(collateralPrice).divWadUp(incentive); uint256 repaidShares = repaid.toSharesDown(totalBorrow[id], totalBorrowShares[id]); borrowShares[id][borrower] -= repaidShares; @@ -397,21 +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 borrowablePrice = IOracle(market.oracle).price(); + uint256 collateralPrice = IOracle(market.oracle).price(); - return _isHealthy(market, id, user, borrowablePrice); + return _isHealthy(market, id, user, collateralPrice); } - function _isHealthy(Market memory market, Id id, address user, uint256 borrowablePrice) + function _isHealthy(Market memory market, Id id, address user, uint256 collateralPrice) internal view returns (bool) { - uint256 borrowValue = - borrowShares[id][user].toAssetsUp(totalBorrow[id], totalBorrowShares[id]).mulWadUp(borrowablePrice); - uint256 collateralPower = collateral[id][user].mulWadDown(market.lltv); + uint256 borrowed = borrowShares[id][user].toAssetsUp(totalBorrow[id], totalBorrowShares[id]); + uint256 collateralPower = collateral[id][user].mulWadDown(collateralPrice).mulWadDown(market.lltv); - return collateralPower >= borrowValue; + return collateralPower >= borrowed; } // Storage view. diff --git a/test/forge/Blue.t.sol b/test/forge/Blue.t.sol index d197dc6cc..ea34adfd5 100644 --- a/test/forge/Blue.t.sol +++ b/test/forge/Blue.t.sol @@ -65,8 +65,7 @@ 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. - oracle.setPrice(0); + oracle.setPrice(FixedPointMathLib.WAD); borrowableAsset.approve(address(blue), type(uint256).max); collateralAsset.approve(address(blue), type(uint256).max); @@ -84,8 +83,8 @@ contract BlueTest is // To move to a test utils file later. function netWorth(address user) internal view returns (uint256) { - uint256 collateralAssetValue = collateralAsset.balanceOf(user); - uint256 borrowableAssetValue = borrowableAsset.balanceOf(user).mulWadDown(oracle.price()); + uint256 collateralAssetValue = collateralAsset.balanceOf(user).mulWadDown(oracle.price()); + uint256 borrowableAssetValue = borrowableAsset.balanceOf(user); return collateralAssetValue + borrowableAssetValue; } @@ -272,6 +271,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); @@ -326,6 +329,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)); @@ -364,6 +371,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); @@ -424,6 +436,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); @@ -526,14 +543,14 @@ contract BlueTest is assertEq(collateralAsset.balanceOf(address(blue)), 0, "blue balance"); } - function testCollateralRequirements(uint256 amountCollateral, uint256 amountBorrowed, uint256 priceBorrowable) + 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); + collateralPrice = bound(collateralPrice, 0, 2 ** 64); - oracle.setPrice(priceBorrowable); + oracle.setPrice(collateralPrice); borrowableAsset.setBalance(address(this), amountBorrowed); collateralAsset.setBalance(BORROWER, amountCollateral); @@ -543,15 +560,11 @@ contract BlueTest is vm.prank(BORROWER); blue.supplyCollateral(market, amountCollateral, BORROWER, hex""); - uint256 borrowValue = amountBorrowed.mulWadUp(priceBorrowable); - if (borrowValue == 0 || (amountCollateral > 0 && borrowValue <= amountCollateral.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 collateralPower = amountCollateral.mulWadDown(collateralPrice).mulWadDown(LLTV); + + vm.prank(BORROWER); + if (collateralPower < amountBorrowed) vm.expectRevert(bytes(Errors.INSUFFICIENT_COLLATERAL)); + blue.borrow(market, amountBorrowed, BORROWER, BORROWER); } function testLiquidate(uint256 amountLent) public { @@ -579,7 +592,7 @@ contract BlueTest is vm.stopPrank(); // Price change - oracle.setPrice(2e18); + oracle.setPrice(0.5e18); uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR); @@ -589,8 +602,8 @@ contract BlueTest is uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - uint256 expectedRepaid = toSeize.divWadUp(incentive).divWadUp(oracle.price()); - uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize - expectedRepaid.mulWadDown(oracle.price()); + uint256 expectedRepaid = toSeize.mulWadUp(oracle.price()).divWadUp(incentive); + uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.mulWadDown(oracle.price()) - expectedRepaid; assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth"); assertApproxEqAbs(borrowBalance(BORROWER), amountBorrowed - expectedRepaid, 100, "BORROWER balance"); assertEq(blue.collateral(id, BORROWER), amountCollateral - toSeize, "BORROWER collateral"); @@ -621,7 +634,7 @@ contract BlueTest is vm.stopPrank(); // Price change - oracle.setPrice(100e18); + oracle.setPrice(0.01e18); uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR); @@ -631,8 +644,8 @@ contract BlueTest is uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - uint256 expectedRepaid = toSeize.divWadUp(incentive).divWadUp(oracle.price()); - uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize - expectedRepaid.mulWadDown(oracle.price()); + uint256 expectedRepaid = toSeize.mulWadUp(oracle.price()).divWadUp(incentive); + uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.mulWadDown(oracle.price()) - expectedRepaid; assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth"); assertEq(borrowBalance(BORROWER), 0, "BORROWER balance"); assertEq(blue.collateral(id, BORROWER), 0, "BORROWER collateral"); @@ -864,35 +877,40 @@ 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); - oracle.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)); - oracle.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 { From 830dcbfb3b713af0b6531c88f0effad422bf24c8 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 31 Jul 2023 18:20:24 +0200 Subject: [PATCH 4/9] test(hardhat): fix liquidations --- test/hardhat/Blue.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hardhat/Blue.spec.ts b/test/hardhat/Blue.spec.ts index a5aa88ae7..5843ab395 100644 --- a/test/hardhat/Blue.spec.ts +++ b/test/hardhat/Blue.spec.ts @@ -176,7 +176,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 oracle.setPrice(BigNumber.WAD.mul(1000)); + await oracle.setPrice(BigNumber.WAD.div(10)); const seized = closePositions ? constants.MaxUint256 : amount.div(2); From 2e6a532528e0f45998ee1c2cf4d1290da4f21205 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 7 Aug 2023 10:41:16 +0200 Subject: [PATCH 5/9] refactor(oracle): add natspec --- src/Blue.sol | 4 ++-- src/interfaces/IOracle.sol | 1 + test/forge/Blue.t.sol | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Blue.sol b/src/Blue.sol index 18ad20a15..7ca4f9b83 100644 --- a/src/Blue.sol +++ b/src/Blue.sol @@ -408,9 +408,9 @@ contract Blue is IBlue { returns (bool) { uint256 borrowed = borrowShares[id][user].toAssetsUp(totalBorrow[id], totalBorrowShares[id]); - uint256 collateralPower = collateral[id][user].mulWadDown(collateralPrice).mulWadDown(market.lltv); + uint256 maxBorrow = collateral[id][user].mulWadDown(collateralPrice).mulWadDown(market.lltv); - return collateralPower >= borrowed; + return maxBorrow >= borrowed; } // Storage view. diff --git a/src/interfaces/IOracle.sol b/src/interfaces/IOracle.sol index 6da75e903..b3ce029bc 100644 --- a/src/interfaces/IOracle.sol +++ b/src/interfaces/IOracle.sol @@ -2,5 +2,6 @@ pragma solidity >=0.5.0; interface IOracle { + /// @notice Returns the price of the collateral asset quoted in the borrowable asset. function price() external view returns (uint256); } diff --git a/test/forge/Blue.t.sol b/test/forge/Blue.t.sol index ea34adfd5..12ff07ede 100644 --- a/test/forge/Blue.t.sol +++ b/test/forge/Blue.t.sol @@ -560,10 +560,10 @@ contract BlueTest is vm.prank(BORROWER); blue.supplyCollateral(market, amountCollateral, BORROWER, hex""); - uint256 collateralPower = amountCollateral.mulWadDown(collateralPrice).mulWadDown(LLTV); + uint256 maxBorrow = amountCollateral.mulWadDown(collateralPrice).mulWadDown(LLTV); vm.prank(BORROWER); - if (collateralPower < amountBorrowed) vm.expectRevert(bytes(Errors.INSUFFICIENT_COLLATERAL)); + if (maxBorrow < amountBorrowed) vm.expectRevert(bytes(Errors.INSUFFICIENT_COLLATERAL)); blue.borrow(market, amountBorrowed, BORROWER, BORROWER); } From 5cbe214c761a32479608d98b506b67ae8f2856b9 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 7 Aug 2023 10:42:08 +0200 Subject: [PATCH 6/9] feat(oracle): add price scale --- src/Blue.sol | 14 +++++++------- src/interfaces/IOracle.sol | 4 ++-- src/mocks/OracleMock.sol | 10 ++++++++-- test/forge/Blue.t.sol | 23 ++++++++++++++++------- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/Blue.sol b/src/Blue.sol index 7ca4f9b83..d87adadc0 100644 --- a/src/Blue.sol +++ b/src/Blue.sol @@ -277,14 +277,14 @@ contract Blue is IBlue { _accrueInterests(market, id); - uint256 collateralPrice = IOracle(market.oracle).price(); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); - require(!_isHealthy(market, id, borrower, collateralPrice), 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); + uint256 repaid = seized.mulDivUp(collateralPrice, priceScale).divWadUp(incentive); uint256 repaidShares = repaid.toSharesDown(totalBorrow[id], totalBorrowShares[id]); borrowShares[id][borrower] -= repaidShares; @@ -397,18 +397,18 @@ 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.oracle).price(); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); - return _isHealthy(market, id, user, collateralPrice); + return _isHealthy(market, id, user, collateralPrice, priceScale); } - function _isHealthy(Market memory market, Id id, address user, uint256 collateralPrice) + function _isHealthy(Market memory market, Id id, address user, uint256 collateralPrice, uint256 priceScale) internal view returns (bool) { uint256 borrowed = borrowShares[id][user].toAssetsUp(totalBorrow[id], totalBorrowShares[id]); - uint256 maxBorrow = collateral[id][user].mulWadDown(collateralPrice).mulWadDown(market.lltv); + uint256 maxBorrow = collateral[id][user].mulDivDown(collateralPrice, priceScale).mulWadDown(market.lltv); return maxBorrow >= borrowed; } diff --git a/src/interfaces/IOracle.sol b/src/interfaces/IOracle.sol index b3ce029bc..33e447615 100644 --- a/src/interfaces/IOracle.sol +++ b/src/interfaces/IOracle.sol @@ -2,6 +2,6 @@ pragma solidity >=0.5.0; interface IOracle { - /// @notice Returns the price of the collateral asset quoted in the borrowable asset. - function price() external view returns (uint256); + /// @notice Returns the price of the collateral asset quoted in the borrowable asset and its 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 12ff07ede..32abb33c0 100644 --- a/test/forge/Blue.t.sol +++ b/test/forge/Blue.t.sol @@ -69,22 +69,27 @@ contract BlueTest is 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(oracle.price()); + (uint256 collateralPrice, uint256 priceScale) = market.oracle.price(); + + uint256 collateralAssetValue = collateralAsset.balanceOf(user).mulDivDown(collateralPrice, priceScale); uint256 borrowableAssetValue = borrowableAsset.balanceOf(user); + return collateralAssetValue + borrowableAssetValue; } @@ -601,9 +606,11 @@ contract BlueTest is blue.liquidate(market, BORROWER, toSeize, hex""); uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); + (uint256 collateralPrice, uint256 priceScale) = market.oracle.price(); - uint256 expectedRepaid = toSeize.mulWadUp(oracle.price()).divWadUp(incentive); - uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.mulWadDown(oracle.price()) - expectedRepaid; + 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"); @@ -643,9 +650,11 @@ contract BlueTest is blue.liquidate(market, BORROWER, toSeize, hex""); uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); + (uint256 collateralPrice, uint256 priceScale) = market.oracle.price(); - uint256 expectedRepaid = toSeize.mulWadUp(oracle.price()).divWadUp(incentive); - uint256 expectedNetWorthAfter = liquidatorNetWorthBefore + toSeize.mulWadDown(oracle.price()) - expectedRepaid; + 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"); From 550f457d1c2fd17156c23e120549b29893a4f758 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 7 Aug 2023 10:43:57 +0200 Subject: [PATCH 7/9] test(hardhat): fix runtime error --- test/hardhat/Blue.spec.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/hardhat/Blue.spec.ts b/test/hardhat/Blue.spec.ts index 5843ab395..1d715be7c 100644 --- a/test/hardhat/Blue.spec.ts +++ b/test/hardhat/Blue.spec.ts @@ -20,7 +20,7 @@ const random = () => { const identifier = (market: MarketStruct) => { const encodedMarket = defaultAbiCoder.encode( - ["address", "address", "address", "address", "address", "uint256"], + ["address", "address", "address", "address", "uint256"], Object.values(market), ); @@ -124,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); @@ -138,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), ]); } From 5c6c7131d548563bca97acdc4cb6b73874940c77 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 7 Aug 2023 11:00:50 +0200 Subject: [PATCH 8/9] test(forge): refactor oracle price --- test/forge/Blue.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/forge/Blue.t.sol b/test/forge/Blue.t.sol index 32abb33c0..cad1f55a6 100644 --- a/test/forge/Blue.t.sol +++ b/test/forge/Blue.t.sol @@ -85,7 +85,7 @@ contract BlueTest 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) = market.oracle.price(); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); uint256 collateralAssetValue = collateralAsset.balanceOf(user).mulDivDown(collateralPrice, priceScale); uint256 borrowableAssetValue = borrowableAsset.balanceOf(user); @@ -606,7 +606,7 @@ contract BlueTest is blue.liquidate(market, BORROWER, toSeize, hex""); uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - (uint256 collateralPrice, uint256 priceScale) = market.oracle.price(); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); uint256 expectedRepaid = toSeize.mulDivUp(collateralPrice, priceScale).divWadUp(incentive); uint256 expectedNetWorthAfter = @@ -650,7 +650,7 @@ contract BlueTest is blue.liquidate(market, BORROWER, toSeize, hex""); uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - (uint256 collateralPrice, uint256 priceScale) = market.oracle.price(); + (uint256 collateralPrice, uint256 priceScale) = IOracle(market.oracle).price(); uint256 expectedRepaid = toSeize.mulDivUp(collateralPrice, priceScale).divWadUp(incentive); uint256 expectedNetWorthAfter = From 5bbbe776df755b4ac33c646fbf769ce075c124a0 Mon Sep 17 00:00:00 2001 From: Romain Milon Date: Wed, 9 Aug 2023 15:22:15 +0200 Subject: [PATCH 9/9] docs(oracle): update comment Signed-off-by: Romain Milon --- src/interfaces/IOracle.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/IOracle.sol b/src/interfaces/IOracle.sol index 33e447615..1cb7f9daf 100644 --- a/src/interfaces/IOracle.sol +++ b/src/interfaces/IOracle.sol @@ -2,6 +2,6 @@ pragma solidity >=0.5.0; interface IOracle { - /// @notice Returns the price of the collateral asset quoted in the borrowable asset and its unit scale. + /// @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); }