Skip to content

Liquidations #4

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

Merged
merged 18 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from 14 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
79 changes: 64 additions & 15 deletions src/Market.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {IOracle} from "src/interfaces/IOracle.sol";
import {MathLib} from "src/libraries/MathLib.sol";
import {SafeTransferLib} from "src/libraries/SafeTransferLib.sol";

uint constant WAD = 1e18;

uint constant alpha = 0.5e18;

// Market id.
type Id is bytes32;

Expand Down Expand Up @@ -67,8 +71,8 @@ contract Blue {
accrueInterests(id);

if (totalSupply[id] == 0) {
supplyShare[id][msg.sender] = 1e18;
totalSupplyShares[id] = 1e18;
supplyShare[id][msg.sender] = WAD;
totalSupplyShares[id] = WAD;
} else {
uint shares = amount.wMul(totalSupplyShares[id]).wDiv(totalSupply[id]);
supplyShare[id][msg.sender] += shares;
Expand Down Expand Up @@ -108,8 +112,8 @@ contract Blue {
accrueInterests(id);

if (totalBorrow[id] == 0) {
borrowShare[id][msg.sender] = 1e18;
totalBorrowShares[id] = 1e18;
borrowShare[id][msg.sender] = WAD;
totalBorrowShares[id] = WAD;
} else {
uint shares = amount.wMul(totalBorrowShares[id]).wDiv(totalBorrow[id]);
borrowShare[id][msg.sender] += shares;
Expand All @@ -118,7 +122,7 @@ contract Blue {

totalBorrow[id] += amount;

checkHealth(market, id, msg.sender);
require(isHealthy(market, id, msg.sender), "not enough collateral");
require(totalBorrow[id] <= totalSupply[id], "not enough liquidity");

market.borrowableAsset.safeTransfer(msg.sender, amount);
Expand Down Expand Up @@ -163,11 +167,57 @@ contract Blue {

collateral[id][msg.sender] -= amount;

checkHealth(market, id, msg.sender);
require(isHealthy(market, id, msg.sender), "not enough collateral");

market.collateralAsset.transfer(msg.sender, amount);
}

// Liquidation.

function liquidate(Market calldata market, address borrower, uint maxSeized)
external
returns (uint seized, uint repaid)
{
Id id = Id.wrap(keccak256(abi.encode(market)));
require(lastUpdate[id] != 0, "unknown market");
require(maxSeized > 0, "zero amount");

accrueInterests(id);

require(!isHealthy(market, id, borrower), "cannot liquidate a healthy position");

// The size of the bonus is the proportion alpha of 1 / LLTV - 1
uint incentive = WAD + alpha.wMul(WAD.wDiv(market.lLTV) - WAD);
uint borrowablePrice = market.borrowableOracle.price();
uint collateralPrice = market.collateralOracle.price();
seized = maxSeized.min(collateral[id][borrower]);
repaid = seized.wMul(collateralPrice).wDiv(incentive).wDiv(borrowablePrice);
uint priorBorrowShares = borrowShare[id][borrower];
uint priorBorrow = priorBorrowShares.wMul(totalBorrow[id]).wDiv(totalBorrowShares[id]);
if (repaid > priorBorrow) {
repaid = priorBorrow;
seized = repaid.wDiv(collateralPrice).wMul(incentive).wMul(borrowablePrice);
}

uint newCollateral = collateral[id][borrower] - seized;
if (newCollateral == 0) {
totalBorrow[id] -= priorBorrow;
totalBorrowShares[id] -= priorBorrowShares;
borrowShare[id][borrower] = 0;
// Realize the bad debt.
totalSupply[id] -= priorBorrow - repaid;
} else {
uint repaidShares = repaid.wMul(totalBorrowShares[id]).wDiv(totalBorrow[id]);
totalBorrow[id] -= repaid;
totalBorrowShares[id] -= repaidShares;
borrowShare[id][borrower] -= repaidShares;
}
collateral[id][borrower] = newCollateral;

market.collateralAsset.safeTransfer(msg.sender, seized);
market.borrowableAsset.safeTransferFrom(msg.sender, address(this), repaid);
}

// Interests management.

function accrueInterests(Id id) private {
Expand All @@ -187,14 +237,13 @@ contract Blue {

// Health check.

function checkHealth(Market calldata market, Id id, address user) private view {
if (borrowShare[id][user] > 0) {
// totalBorrowShares[id] > 0 because borrowShare[id][user] > 0.
uint borrowValue = borrowShare[id][user].wMul(totalBorrow[id]).wDiv(totalBorrowShares[id]).wMul(
IOracle(market.borrowableOracle).price()
);
uint collateralValue = collateral[id][user].wMul(IOracle(market.collateralOracle).price());
require(collateralValue.wMul(market.lLTV) >= borrowValue, "not enough collateral");
}
function isHealthy(Market calldata market, Id id, address user) private view returns (bool) {
uint borrowShares = borrowShare[id][user];
// totalBorrowShares[id] > 0 when borrowShares > 0.
uint borrowValue = borrowShares > 0
? borrowShares.wMul(totalBorrow[id]).wDiv(totalBorrowShares[id]).wMul(market.borrowableOracle.price())
: 0;
uint collateralValue = collateral[id][user].wMul(market.collateralOracle.price());
return collateralValue.wMul(market.lLTV) >= borrowValue;
}
}
4 changes: 4 additions & 0 deletions src/libraries/MathLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ pragma solidity ^0.8.0;
library MathLib {
uint internal constant WAD = 1e18;

function min(uint x, uint y) internal pure returns (uint z) {
z = x < y ? x : y;
}

/// @dev Rounds towards zero.
function wMul(uint x, uint y) internal pure returns (uint z) {
z = (x * y) / WAD;
Expand Down
122 changes: 120 additions & 2 deletions test/forge/Market.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ contract BlueTest is Test {
using MathLib for uint;

address private constant borrower = address(1234);
address private constant liquidator = address(5678);
uint private constant lLTV = 0.8 ether;

Blue private blue;
Expand Down Expand Up @@ -54,10 +55,38 @@ contract BlueTest is Test {
vm.stopPrank();
}

// To move to a test utils file later.

function networth(address user) internal view returns (uint) {
uint collateralAssetValue = collateralAsset.balanceOf(user).wMul(collateralOracle.price());
uint borrowableAssetValue = borrowableAsset.balanceOf(user).wMul(borrowableOracle.price());
return collateralAssetValue + borrowableAssetValue;
}

function supplyBalance(address user) internal view returns (uint) {
uint supplyShares = blue.supplyShare(id, user);
if (supplyShares == 0) return 0;
uint totalShares = blue.totalSupplyShares(id);
uint totalSupply = blue.totalSupply(id);
return supplyShares.wMul(totalSupply).wDiv(totalShares);
}

function borrowBalance(address user) internal view returns (uint) {
uint borrowerShares = blue.borrowShare(id, user);
if (borrowerShares == 0) return 0;
uint totalShares = blue.totalBorrowShares(id);
uint totalBorrow = blue.totalBorrow(id);
return borrowerShares.wMul(totalBorrow).wDiv(totalShares);
}

// Invariants

function invariantLiquidity() public {
assertLe(blue.totalBorrow(id), blue.totalSupply(id));
}

// Tests

function testSupply(uint amount) public {
amount = bound(amount, 1, 2 ** 64);

Expand Down Expand Up @@ -208,6 +237,93 @@ contract BlueTest is Test {
assertEq(collateralAsset.balanceOf(address(blue)), amountDeposited - amountWithdrawn);
}

function testLiquidate(uint amountLent) public {
borrowableOracle.setPrice(1e18);
amountLent = bound(amountLent, 1000, 2 ** 64);

uint amountCollateral = amountLent;
uint borrowingPower = amountCollateral.wMul(lLTV);
uint amountBorrowed = borrowingPower.wMul(0.8e18);
uint maxCollat = amountCollateral.wMul(lLTV);

borrowableAsset.setBalance(address(this), amountLent);
collateralAsset.setBalance(borrower, amountCollateral);
borrowableAsset.setBalance(liquidator, amountBorrowed);

// Supply
blue.supply(market, amountLent);

// Borrow
vm.startPrank(borrower);
blue.supplyCollateral(market, amountCollateral);
blue.borrow(market, amountBorrowed);
vm.stopPrank();

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

uint liquidatorNetworthBefore = networth(liquidator);

// Liquidate
vm.startPrank(liquidator);
borrowableAsset.approve(address(blue), type(uint).max);
(uint seized, uint repaid) = blue.liquidate(market, borrower, maxCollat);
vm.stopPrank();

uint liquidatorNetworthAfter = networth(liquidator);

assertGt(liquidatorNetworthAfter, liquidatorNetworthBefore, "liquidator's networth");
assertGt(seized, 0, "collateral seized");
assertGt(repaid, 0, "borrow repaid");
assertApproxEqAbs(borrowBalance(borrower), amountBorrowed - repaid, 100, "collateral balance borrower");
assertApproxEqAbs(blue.collateral(id, borrower), amountCollateral - seized, 100, "collateral balance borrower");
}

function testRealizeBadDebt(uint amountLent) public {
borrowableOracle.setPrice(1e18);
amountLent = bound(amountLent, 1000, 2 ** 64);

uint amountCollateral = amountLent;
uint borrowingPower = amountCollateral.wMul(lLTV);
uint amountBorrowed = borrowingPower.wMul(0.8e18);
uint maxCollat = type(uint).max;

borrowableAsset.setBalance(address(this), amountLent);
collateralAsset.setBalance(borrower, amountCollateral);
borrowableAsset.setBalance(liquidator, amountBorrowed);

// Supply
blue.supply(market, amountLent);

// Borrow
vm.startPrank(borrower);
blue.supplyCollateral(market, amountCollateral);
blue.borrow(market, amountBorrowed);
vm.stopPrank();

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

uint liquidatorNetworthBefore = networth(liquidator);

// Liquidate
vm.startPrank(liquidator);
borrowableAsset.approve(address(blue), type(uint).max);
(uint seized, uint repaid) = blue.liquidate(market, borrower, maxCollat);
vm.stopPrank();

uint liquidatorNetworthAfter = networth(liquidator);

assertGt(liquidatorNetworthAfter, liquidatorNetworthBefore, "liquidator's networth");
assertEq(seized, amountCollateral, "collateral seized");
assertGt(repaid, 0, "borrow repaid");
assertEq(borrowBalance(borrower), 0, "collateral balance borrower");
assertEq(blue.collateral(id, borrower), 0, "collateral balance borrower");
uint expectedBadDebt = amountBorrowed - repaid;
assertGt(expectedBadDebt, 0, "positive bad debt");
assertApproxEqAbs(supplyBalance(address(this)), amountLent - expectedBadDebt, 10, "realized bad debt");
}

function testTwoUsersSupply(uint firstAmount, uint secondAmount) public {
firstAmount = bound(firstAmount, 1, 2 ** 64);
secondAmount = bound(secondAmount, 1, 2 ** 64);
Expand All @@ -219,8 +335,10 @@ contract BlueTest is Test {
vm.prank(borrower);
blue.supply(market, secondAmount);

assertEq(blue.supplyShare(id, address(this)), 1e18);
assertEq(blue.supplyShare(id, borrower), secondAmount * 1e18 / firstAmount);
assertApproxEqAbs(supplyBalance(address(this)), firstAmount, 100, "same balance first user");
assertEq(blue.supplyShare(id, address(this)), 1e18, "expected shares first user");
assertApproxEqAbs(supplyBalance(borrower), secondAmount, 100, "same balance second user");
assertEq(blue.supplyShare(id, borrower), secondAmount * 1e18 / firstAmount, "expected shares second user");
}

function testSupplyUnknownMarket(Market memory marketFuzz) public {
Expand Down