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

refactor(blue): use single oracle #202

Merged
merged 9 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 9 additions & 12 deletions src/Blue.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions src/interfaces/IBlue.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ type Id is bytes32;
struct Market {
address borrowableAsset;
address collateralAsset;
address borrowableOracle;
address collateralOracle;
address oracle;
address irm;
uint256 lltv;
}
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/IOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
}
10 changes: 8 additions & 2 deletions src/mocks/OracleMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
139 changes: 72 additions & 67 deletions test/forge/Blue.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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) {
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
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;
}

Expand Down Expand Up @@ -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"");
MathisGD marked this conversation as resolved.
Show resolved Hide resolved

vm.prank(BORROWER);
blue.borrow(market, amountBorrowed, BORROWER, BORROWER);

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -596,7 +597,7 @@ contract BlueTest is
vm.stopPrank();

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

uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR);

Expand All @@ -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;
Expand All @@ -640,7 +641,7 @@ contract BlueTest is
vm.stopPrank();

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

uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR);

Expand All @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Loading