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

Liquidations #4

Merged
merged 18 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from 11 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
85 changes: 77 additions & 8 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;

uint constant N = 10;

function bucketToLLTV(uint bucket) pure returns (uint) {
Expand Down Expand Up @@ -76,8 +80,8 @@ contract Market {
accrueInterests(bucket);

if (totalSupply[bucket] == 0 && amount > 0) {
supplyShare[msg.sender][bucket] = 1e18;
totalSupplyShares[bucket] = 1e18;
supplyShare[msg.sender][bucket] = WAD;
totalSupplyShares[bucket] = WAD;
} else {
int shares = amount.wMul(totalSupplyShares[bucket]).wDiv(totalSupply[bucket]);
supplyShare[msg.sender][bucket] = (int(supplyShare[msg.sender][bucket]) + shares).safeToUint();
Expand All @@ -102,8 +106,8 @@ contract Market {
accrueInterests(bucket);

if (totalBorrow[bucket] == 0 && amount > 0) {
borrowShare[msg.sender][bucket] = 1e18;
totalBorrowShares[bucket] = 1e18;
borrowShare[msg.sender][bucket] = WAD;
totalBorrowShares[bucket] = WAD;
} else {
int shares = amount.wMul(totalBorrowShares[bucket]).wDiv(totalBorrow[bucket]);
borrowShare[msg.sender][bucket] = (int(borrowShare[msg.sender][bucket]) + shares).safeToUint();
Expand All @@ -114,7 +118,7 @@ contract Market {
totalBorrow[bucket] = uint(int(totalBorrow[bucket]) + amount);

if (amount > 0) {
checkHealth(msg.sender, bucket);
require(isHealthy(msg.sender, bucket), "not enough collateral");
require(totalBorrow[bucket] <= totalSupply[bucket], "not enough liquidity");
}

Expand All @@ -130,11 +134,75 @@ contract Market {

collateral[msg.sender][bucket] = (int(collateral[msg.sender][bucket]) + amount).safeToUint();

if (amount < 0) checkHealth(msg.sender, bucket);
require(amount > 0 || isHealthy(msg.sender, bucket), "not enough collateral");
QGarchery marked this conversation as resolved.
Show resolved Hide resolved

IERC20(collateralAsset).handleTransfer({user: msg.sender, amountIn: amount});
}

// Liquidation.

struct Liquidation {
uint bucket;
address borrower;
uint maxCollat;
}

/// @return sumCollat The negative amount of collateral added.
/// @return sumBorrow The negative amount of borrow added.
function batchLiquidate(Liquidation[] memory liquidationData) external returns (int sumCollat, int sumBorrow) {
for (uint i; i < liquidationData.length; i++) {
Liquidation memory liq = liquidationData[i];
(int collat, int borrow) = liquidate(liq.bucket, liq.borrower, liq.maxCollat);
sumCollat += collat;
sumBorrow += borrow;
}

IERC20(collateralAsset).handleTransfer(msg.sender, sumCollat);
IERC20(borrowableAsset).handleTransfer(msg.sender, -sumBorrow);
}
QGarchery marked this conversation as resolved.
Show resolved Hide resolved

/// @return collat The negative amount of collateral added.
/// @return borrow The negative amount of borrow added.
function liquidate(uint bucket, address borrower, uint maxCollat) internal returns (int collat, int borrow) {
if (maxCollat == 0) return (0, 0);
require(bucket < N, "unknown bucket");

accrueInterests(bucket);

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

uint incentive = WAD + alpha.wMul(WAD.wDiv(bucketToLLTV(bucket)) - WAD);
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
uint borrowPrice = IOracle(borrowableOracle).price();
uint collatPrice = IOracle(collateralOracle).price();
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
// Safe to cast because it's smaller than collateral[borrower][bucket]
collat = -int(maxCollat.min(collateral[borrower][bucket]));
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
borrow = collat.wMul(collatPrice).wDiv(incentive).wDiv(borrowPrice);
uint priorBorrowShares = borrowShare[borrower][bucket];
uint priorBorrow = priorBorrowShares.wMul(totalBorrow[bucket]).wDiv(totalBorrowShares[bucket]);
if (int(priorBorrow) + borrow < 0) {
borrow = -int(priorBorrow);
collat = borrow.wDiv(collatPrice).wMul(incentive).wMul(borrowPrice);
}
int shares = borrow.wMul(totalBorrowShares[bucket]).wDiv(totalBorrow[bucket]);
QGarchery marked this conversation as resolved.
Show resolved Hide resolved

// Keep this next computation outside of the if-then-else.
uint newTotalSupply = (int(totalSupply[bucket]) - int(priorBorrow) - borrow).safeToUint();
QGarchery marked this conversation as resolved.
Show resolved Hide resolved

uint newCollateral = uint(int(collateral[borrower][bucket]) + collat);
if (newCollateral == 0) {
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
totalBorrow[bucket] -= priorBorrow;
totalBorrowShares[bucket] -= priorBorrowShares;
borrowShare[borrower][bucket] = 0;
// Realize the bad debt.
totalSupply[bucket] = newTotalSupply;
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
} else {
totalBorrow[bucket] = (int(totalBorrow[bucket]) + borrow).safeToUint();
totalBorrowShares[bucket] = (int(totalBorrowShares[bucket]) + shares).safeToUint();
borrowShare[borrower][bucket] = (int(priorBorrowShares) + shares).safeToUint();
}
collateral[borrower][bucket] = newCollateral;
}

// Interests management.

function accrueInterests(uint bucket) internal {
Expand All @@ -152,14 +220,15 @@ contract Market {

// Health check.

function checkHealth(address user, uint bucket) public view {
function isHealthy(address user, uint bucket) public view returns (bool) {
if (borrowShare[user][bucket] > 0) {
// totalBorrowShares[bucket] > 0 because borrowShare[user][bucket] > 0.
uint borrowValue = borrowShare[user][bucket].wMul(totalBorrow[bucket]).wDiv(totalBorrowShares[bucket]).wMul(
IOracle(borrowableOracle).price()
);
uint collateralValue = collateral[user][bucket].wMul(IOracle(collateralOracle).price());
require(collateralValue.wMul(bucketToLLTV(bucket)) >= borrowValue, "not enough collateral");
return collateralValue.wMul(bucketToLLTV(bucket)) >= borrowValue;
}
return true;
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
}
}
6 changes: 5 additions & 1 deletion 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 All @@ -25,7 +29,7 @@ library MathLib {
z = (x * int(WAD)) / int(y);
}

/// @dev Reverts if x is negative.
/// @dev Reverts if `x` is negative.
function safeToUint(int x) internal pure returns (uint z) {
require(x >= 0, "negative");
z = uint(x);
Expand Down
143 changes: 141 additions & 2 deletions test/forge/Market.t.sol
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ contract MarketTest is Test {
using MathLib for uint;

address private constant borrower = address(1234);
address private constant liquidator = address(5678);

Market private market;
ERC20 private borrowableAsset;
Expand Down Expand Up @@ -44,6 +45,32 @@ contract MarketTest is Test {
vm.stopPrank();
}

// To move to a test utils file later.

function networth(address user) internal view returns (uint) {
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
uint collateralAssetValue = collateralAsset.balanceOf(user).wMul(collateralOracle.price());
uint borrowableAssetValue = borrowableAsset.balanceOf(user).wMul(borrowableOracle.price());
return collateralAssetValue + borrowableAssetValue;
}

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

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

// Invariants

function invariantParams() public {
assertEq(market.borrowableAsset(), address(borrowableAsset));
assertEq(market.collateralAsset(), address(collateralAsset));
Expand All @@ -57,6 +84,8 @@ contract MarketTest is Test {
}
}

// Tests

function testDeposit(uint amount, uint bucket) public {
amount = bound(amount, 1, 2 ** 64);
vm.assume(bucket < N);
Expand Down Expand Up @@ -218,6 +247,114 @@ contract MarketTest is Test {
assertEq(collateralAsset.balanceOf(address(market)), amountDeposited - amountWithdrawn);
}

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

uint amountCollateral = amountLent;
uint lLTV = bucketToLLTV(bucket);
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);

// Lend
borrowableAsset.approve(address(market), type(uint).max);
market.modifyDeposit(int(amountLent), bucket);

// Borrow
vm.startPrank(borrower);
collateralAsset.approve(address(market), type(uint).max);
market.modifyCollateral(int(amountCollateral), bucket);
market.modifyBorrow(int(amountBorrowed), bucket);
vm.stopPrank();

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

uint liquidatorNetworthBefore = networth(liquidator);

// Liquidate
Market.Liquidation[] memory liquidationData = new Market.Liquidation[](1);
liquidationData[0] = Market.Liquidation(bucket, borrower, maxCollat);
vm.startPrank(liquidator);
borrowableAsset.approve(address(market), type(uint).max);
(int sumCollat, int sumBorrow) = market.batchLiquidate(liquidationData);
vm.stopPrank();

uint liquidatorNetworthAfter = networth(liquidator);

assertGt(liquidatorNetworthAfter, liquidatorNetworthBefore, "liquidator's networth");
assertLt(sumCollat, 0, "collateral seized");
assertLt(sumBorrow, 0, "borrow repaid");
assertApproxEqAbs(
int(borrowBalance(bucket, borrower)), int(amountBorrowed) + sumBorrow, 100, "collateral balance borrower"
);
assertApproxEqAbs(
int(market.collateral(borrower, bucket)),
int(amountCollateral) + sumCollat,
100,
"collateral balance borrower"
);
}

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

uint amountCollateral = amountLent;
uint lLTV = bucketToLLTV(bucket);
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);

// Lend
borrowableAsset.approve(address(market), type(uint).max);
market.modifyDeposit(int(amountLent), bucket);

// Borrow
vm.startPrank(borrower);
collateralAsset.approve(address(market), type(uint).max);
market.modifyCollateral(int(amountCollateral), bucket);
market.modifyBorrow(int(amountBorrowed), bucket);
vm.stopPrank();

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

uint liquidatorNetworthBefore = networth(liquidator);

// Liquidate
Market.Liquidation[] memory liquidationData = new Market.Liquidation[](1);
liquidationData[0] = Market.Liquidation(bucket, borrower, maxCollat);
vm.startPrank(liquidator);
borrowableAsset.approve(address(market), type(uint).max);
(int sumCollat, int sumBorrow) = market.batchLiquidate(liquidationData);
vm.stopPrank();

uint liquidatorNetworthAfter = networth(liquidator);

assertGt(liquidatorNetworthAfter, liquidatorNetworthBefore, "liquidator's networth");
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
assertEq(sumCollat, -int(amountCollateral), "collateral seized");
assertLt(sumBorrow, 0, "borrow repaid");
assertEq(int(borrowBalance(bucket, borrower)), 0, "collateral balance borrower");
assertEq(int(market.collateral(borrower, bucket)), 0, "collateral balance borrower");
int expectedBadDebt = int(amountBorrowed) + sumBorrow;
assertGt(expectedBadDebt, 0, "positive bad debt");
assertApproxEqAbs(
int(supplyBalance(bucket, address(this))), int(amountLent) - expectedBadDebt, 10, "realized bad debt"
);
}

function testTwoUsersSupply(uint firstAmount, uint secondAmount, uint bucket) public {
vm.assume(bucket < N);
firstAmount = bound(firstAmount, 1, 2 ** 64);
Expand All @@ -230,8 +367,10 @@ contract MarketTest is Test {
vm.prank(borrower);
market.modifyDeposit(int(secondAmount), bucket);

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

function testModifyDepositUnknownBucket(uint bucket) public {
Expand Down