Skip to content

Commit

Permalink
Refactor Borrower.liquidate and add Uniswap withdrawal sanity check
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenshively committed Dec 11, 2023
1 parent 00339aa commit 2c2a9b9
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 142 deletions.
24 changes: 12 additions & 12 deletions core/.gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
BorrowerGasTest:test_addMargin() (gas: 16225)
BorrowerGasTest:test_borrow() (gas: 116572)
BorrowerGasTest:test_borrow() (gas: 116602)
BorrowerGasTest:test_getUniswapPositions() (gas: 5376)
BorrowerGasTest:test_modify() (gas: 89947)
BorrowerGasTest:test_modifyWithAnte() (gas: 96424)
BorrowerGasTest:test_modify() (gas: 89977)
BorrowerGasTest:test_modifyWithAnte() (gas: 96454)
BorrowerGasTest:test_repay() (gas: 68583)
BorrowerGasTest:test_uniswapDepositInBorrower() (gas: 248168)
BorrowerGasTest:test_uniswapDepositInBorrower() (gas: 248198)
BorrowerGasTest:test_uniswapDepositStandard() (gas: 167581)
BorrowerGasTest:test_uniswapWithdraw() (gas: 154949)
BorrowerGasTest:test_withdraw() (gas: 113530)
BorrowerGasTest:test_uniswapWithdraw() (gas: 154979)
BorrowerGasTest:test_withdraw() (gas: 113560)
FactoryGasTest:test_createBorrower() (gas: 137539)
FactoryGasTest:test_createMarket() (gas: 4082512)
FactoryGasTest:test_createMarket() (gas: 4151486)
LenderGasTest:test_accrueInterest() (gas: 47320)
LenderGasTest:test_borrow() (gas: 41010)
LenderGasTest:test_deposit() (gas: 53576)
LenderGasTest:test_depositWithCourier() (gas: 53722)
LenderGasTest:test_redeem() (gas: 53238)
LenderGasTest:test_redeemWithCourier() (gas: 86384)
LenderGasTest:test_repay() (gas: 44918)
LiquidatorGasTest:test_noCallbackOneAsset() (gas: 125211)
LiquidatorGasTest:test_noCallbackTwoAssets() (gas: 184806)
LiquidatorGasTest:test_noCallbackTwoAssetsAndUniswapPosition() (gas: 228925)
LiquidatorGasTest:test_warn() (gas: 40910)
LiquidatorGasTest:test_withCallbackAndSwap() (gas: 196894)
LiquidatorGasTest:test_noCallbackOneAsset() (gas: 125132)
LiquidatorGasTest:test_noCallbackTwoAssets() (gas: 184775)
LiquidatorGasTest:test_noCallbackTwoAssetsAndUniswapPosition() (gas: 228892)
LiquidatorGasTest:test_warn() (gas: 40940)
LiquidatorGasTest:test_withCallbackAndSwap() (gas: 197147)
VolatilityGasTest:test_consult() (gas: 43957)
VolatilityGasTest:test_updateNoBinarySearch() (gas: 196352)
43 changes: 30 additions & 13 deletions core/src/Borrower.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {ERC20, SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";
import {IUniswapV3MintCallback} from "v3-core/contracts/interfaces/callback/IUniswapV3MintCallback.sol";
import {IUniswapV3Pool} from "v3-core/contracts/interfaces/IUniswapV3Pool.sol";

import {TERMINATING_CLOSE_FACTOR} from "./libraries/constants/Constants.sol";
import {BalanceSheet, AuctionAmounts, Assets, Prices} from "./libraries/BalanceSheet.sol";
import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol";
import {extract} from "./libraries/Positions.sol";
Expand Down Expand Up @@ -191,6 +192,7 @@ contract Borrower is IUniswapV3MintCallback {
slot0 = slot0_ & ~SLOT0_MASK_AUCTION;
}

/* solhint-disable code-complexity */
/**
* @notice Liquidates the borrower, using all available assets to pay down liabilities. `callee` must
* transfer at least `amounts.repay0` and `amounts.repay1` to `LENDER0` and `LENDER1`, respectively.
Expand All @@ -206,7 +208,7 @@ contract Borrower is IUniswapV3MintCallback {
* TWAPs. If any of the highest 8 bits are set, we fallback to onchain binary search.
*/
function liquidate(ILiquidator callee, bytes calldata data, uint256 closeFactor, uint40 oracleSeed) external {
require(closeFactor <= 10000, "Aloe: close");
require(0 < closeFactor && closeFactor <= 10000, "Aloe: close");

uint256 slot0_ = slot0;
// Essentially `slot0.state == State.Ready && slot0.warnTime > 0`
Expand All @@ -222,22 +224,22 @@ contract Borrower is IUniswapV3MintCallback {
(uint256 assets0, uint256 assets1) = (TOKEN0.balanceOf(address(this)), TOKEN1.balanceOf(address(this)));
// Fetch liabilities from lenders
(uint256 liabilities0, uint256 liabilities1) = getLiabilities();
// Sanity check
{
(uint160 sqrtPriceX96, , , , , , ) = UNISWAP_POOL.slot0();
require(prices.a < sqrtPriceX96 && sqrtPriceX96 < prices.b);
}

(AuctionAmounts memory amounts, bool willBeHealthy) = BalanceSheet.computeAuctionAmounts(
prices,
AuctionAmounts memory amounts = BalanceSheet.computeAuctionAmounts(
prices.c,
assets0,
assets1,
liabilities0,
liabilities1,
(slot0_ & SLOT0_MASK_AUCTION) >> 208,
BalanceSheet.auctionTime((slot0_ & SLOT0_MASK_AUCTION) >> 208),
closeFactor
);

// End auction if healthy and `closeFactor` is at least 50%
if (willBeHealthy && closeFactor >= 5000) slot0_ &= SLOT0_MASK_USERSPACE;
// Make sure at least one of the repay values didn't floor to 0
require(amounts.repay0 | amounts.repay1 > 0, "Aloe: zero impact");

if (amounts.out0 > 0) TOKEN0.safeTransfer(address(callee), amounts.out0);
if (amounts.out1 > 0) TOKEN1.safeTransfer(address(callee), amounts.out1);

Expand All @@ -246,15 +248,30 @@ contract Borrower is IUniswapV3MintCallback {
if (amounts.repay0 > 0) LENDER0.repay(amounts.repay0, address(this));
if (amounts.repay1 > 0) LENDER1.repay(amounts.repay1, address(this));

slot0 = (slot0_ & (SLOT0_MASK_USERSPACE | SLOT0_MASK_AUCTION)) | SLOT0_DIRT;
emit Liquidate();

// Pay out remaining ante if `closeFactor` is 100% (otherwise keep it, since we may need to `warn` again)
if (closeFactor == 10000) {
// Everything was repaid → no need to `warn` again → can pay out remaining ETH as incentive
SafeTransferLib.safeTransferETH(payable(callee), address(this).balance);
// End auction since account is definitely healthy
slot0_ &= ~SLOT0_MASK_AUCTION;
} else if (closeFactor > TERMINATING_CLOSE_FACTOR) {
// End auction if account is healthy
if (
BalanceSheet.isHealthy(
prices,
assets0 - amounts.out0,
assets1 - amounts.out1,
liabilities0 - amounts.repay0,
liabilities1 - amounts.repay1
)
) slot0_ &= ~SLOT0_MASK_AUCTION;
}

slot0 = (slot0_ & (SLOT0_MASK_USERSPACE | SLOT0_MASK_AUCTION)) | SLOT0_DIRT;
emit Liquidate();
}

/* solhint-enable code-complexity */

/**
* @notice Allows the owner to manage their account by handing control to some `callee`. Inside the
* callback `callee` has access to all sub-commands (`uniswapDeposit`, `uniswapWithdraw`, `transfer`,
Expand Down
72 changes: 28 additions & 44 deletions core/src/libraries/BalanceSheet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,57 +77,48 @@ library BalanceSheet {
uint256 private constant _M = 20.405429e6;
uint256 private constant _N = 7 days - LIQUIDATION_GRACE_PERIOD;

function auctionTime(uint256 warnTime) internal view returns (uint256) {
unchecked {
require(warnTime > 0 && block.timestamp >= warnTime + LIQUIDATION_GRACE_PERIOD, "Aloe: grace");
return block.timestamp - (warnTime + LIQUIDATION_GRACE_PERIOD);
}
}

function auctionCurve(uint256 t) internal pure returns (uint256) {
unchecked {
return _S + _R.rawDiv(_N - t) - _Q.rawDiv(_M + 1e6 * t);
}
}

function computeAuctionAmounts(
Prices memory prices,
uint160 sqrtPriceX96,
uint256 assets0,
uint256 assets1,
uint256 liabilities0,
uint256 liabilities1,
uint256 warnTime,
uint256 t,
uint256 closeFactor
) internal view returns (AuctionAmounts memory amounts, bool willBeHealthy) {
// Compute `assets` and `liabilities` like in `BalanceSheet.isSolvent`, except we round up `assets`
uint256 priceX128 = square(prices.c);
) internal pure returns (AuctionAmounts memory amounts) {
uint256 priceX128 = square(sqrtPriceX96);
uint256 liabilities = liabilities1 + mulDiv128Up(liabilities0, priceX128);
uint256 assets = assets1 + mulDiv128Up(assets0, priceX128);

unchecked {
uint256 t = _auctionTime(warnTime);
// If it's been less than 7 days since the `Warn`ing, the available incentives (`out0` and `out1`)
// scale with `closeFactor` and increase over time according to `auctionCurve`.
if (t < _N) {
liabilities *= auctionCurve(t) * closeFactor;
assets *= 1e16;
liabilities *= auctionCurve(t);
assets *= 1e12;

amounts.out0 = liabilities.fullMulDiv(assets0, assets).min(assets0);
amounts.out1 = liabilities.fullMulDiv(assets1, assets).min(assets1);
if (liabilities < assets) {
assets0 = assets0.fullMulDiv(liabilities, assets);
assets1 = assets1.fullMulDiv(liabilities, assets);
}
}
// After 7 days, `auctionCurve` is essentially infinite. Assuming `closeFactor != 0`, their product
// would _also_ be infinite, so incentives are set to their maximum values. NOTE: The caller should
// validate this assumption.
else {
amounts.out0 = assets0;
amounts.out1 = assets1;
}

// Expected repay amounts always scale with `closeFactor`
amounts.repay0 = (liabilities0 * closeFactor) / 10000;
amounts.repay1 = (liabilities1 * closeFactor) / 10000;

// Check if the account will end up healthy, assuming transfers/repays are successful
willBeHealthy = isHealthy(
prices,
assets0 - amounts.out0,
assets1 - amounts.out1,
liabilities0 - amounts.repay0,
liabilities1 - amounts.repay1
);
}
}

function auctionCurve(uint256 t) internal pure returns (uint256) {
unchecked {
return _S + (_R / (_N - t)) - (_Q / (_M + 1e6 * t));
// All amounts scale with `closeFactor`
amounts.out0 = (assets0 * closeFactor) / 10000;
amounts.out1 = (assets1 * closeFactor) / 10000;
amounts.repay0 = (liabilities0 * closeFactor).divUp(10000);
amounts.repay1 = (liabilities1 * closeFactor).divUp(10000);
}
}

Expand Down Expand Up @@ -244,13 +235,6 @@ library BalanceSheet {
}
}

function _auctionTime(uint256 warnTime) private view returns (uint256 auctionTime) {
unchecked {
require(warnTime > 0 && block.timestamp >= warnTime + LIQUIDATION_GRACE_PERIOD, "Aloe: grace");
return block.timestamp - (warnTime + LIQUIDATION_GRACE_PERIOD);
}
}

/// @dev Equivalent to \\( \frac{log_{1.0001} \left( \frac{10^{12}}{ltv} \right)}{\text{MANIPULATION_THRESHOLD_DIVISOR}} \\)
function _manipulationThreshold(uint160 ltv, uint8 manipulationThresholdDivisor) private pure returns (uint24) {
unchecked {
Expand Down
17 changes: 13 additions & 4 deletions core/src/libraries/constants/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ uint256 constant MAX_RATE = 706354;
//////////////////////////////////////////////////////////////*/

/// @dev The default amount of Ether required to take on debt in a `Borrower`. The `Factory` can override this value
/// on a per-market basis.
/// on a per-market basis. Incentivizes calls to `Borrower.warn`.
uint208 constant DEFAULT_ANTE = 0.01 ether;

/// @dev The default number of standard deviations of price movement used to determine probe prices for `Borrower`
Expand Down Expand Up @@ -84,13 +84,22 @@ uint216 constant CONSTRAINT_ANTE_MAX = 0.5 ether;
/// `accrualFactor` so that liquidators have time to respond to interest updates
uint256 constant MAX_LEVERAGE = 200;

/// @dev The discount that liquidators receive when swapping assets. Expressed as reciprocal, e.g. 20 → 5%
/// @dev The minimum discount that a healthy `Borrower` should be able to offer a liquidator when swapping
/// assets. Expressed as reciprocal, e.g. 20 → 5%
uint256 constant LIQUIDATION_INCENTIVE = 20;

/// @dev The minimum time that must pass between `Borrower.warn` and `Borrower.liquidate` for any liquidation that
/// involves the swap callbacks (`swap1For0` and `swap0For1`). There is no grace period for in-kind liquidations.
/// @dev The minimum time that must pass between calls to `Borrower.warn` and `Borrower.liquidate`.
uint256 constant LIQUIDATION_GRACE_PERIOD = 5 minutes;

/// @dev The minimum `closeFactor` necessary to conclude a liquidation auction. To actually conclude the auction,
/// `Borrower.liquidate` must result in a healthy balance sheet (in addition to this `closeFactor` requirement).
/// Expressed in basis points.
/// NOTE: The ante is depleted after just 4 `Borrower.warn`ings. By requiring that each auction repay at least
/// 68%, we ensure that after 4 auctions, no more than 1% of debt remains ((1 - 0.6838)^4). Increasing the threshold
/// would reduce that further, but we don't want to prolong individual auctions unnecessarily since the incentive
/// (and loss to `Borrower`s) increases with time.
uint256 constant TERMINATING_CLOSE_FACTOR = 6837;

/// @dev The minimum scaling factor by which `sqrtMeanPriceX96` is multiplied or divided to get probe prices
uint256 constant PROBE_SQRT_SCALER_MIN = 1.026248453011e12;

Expand Down
Loading

0 comments on commit 2c2a9b9

Please sign in to comment.