Skip to content

Commit

Permalink
feat: Reduce liquidation penalty
Browse files Browse the repository at this point in the history
  • Loading branch information
bingen committed May 3, 2024
1 parent 9311b08 commit 3fee998
Show file tree
Hide file tree
Showing 17 changed files with 691 additions and 71 deletions.
24 changes: 15 additions & 9 deletions contracts/src/BorrowerOperations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
// --- Connected contract declarations ---

IERC20 public immutable ETH;
ITroveManager public troveManager;
ITroveManager public immutable troveManager;
address stabilityPoolAddress;
address gasPoolAddress;
ICollSurplusPool collSurplusPool;
IBoldToken public boldToken;
// A doubly linked list of Troves, sorted by their collateral ratios
ISortedTroves public sortedTroves;

// Minimum collateral ratio for individual troves
uint256 public immutable MCR;

/* --- Variable container structs ---
Used to hold, return and assign variables inside a function, in order to avoid the error:
Expand Down Expand Up @@ -101,15 +104,21 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
);
event BoldBorrowingFeePaid(uint256 indexed _troveId, uint256 _boldFee);

constructor(address _ETHAddress) {
checkContract(_ETHAddress);
ETH = IERC20(_ETHAddress);
constructor(IERC20 _ETH, ITroveManager _troveManager) {
checkContract(address(_ETH));
checkContract(address(_troveManager));

ETH = _ETH;
troveManager = _troveManager;

MCR = _troveManager.MCR();

emit TroveManagerAddressChanged(address(_troveManager));
}

// --- Dependency setters ---

function setAddresses(
address _troveManagerAddress,
address _activePoolAddress,
address _defaultPoolAddress,
address _stabilityPoolAddress,
Expand All @@ -122,7 +131,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
// This makes impossible to open a trove with zero withdrawn Bold
assert(MIN_NET_DEBT > 0);

checkContract(_troveManagerAddress);
checkContract(_activePoolAddress);
checkContract(_defaultPoolAddress);
checkContract(_stabilityPoolAddress);
Expand All @@ -132,7 +140,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
checkContract(_sortedTrovesAddress);
checkContract(_boldTokenAddress);

troveManager = ITroveManager(_troveManagerAddress);
activePool = IActivePool(_activePoolAddress);
defaultPool = IDefaultPool(_defaultPoolAddress);
stabilityPoolAddress = _stabilityPoolAddress;
Expand All @@ -142,7 +149,6 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
sortedTroves = ISortedTroves(_sortedTrovesAddress);
boldToken = IBoldToken(_boldTokenAddress);

emit TroveManagerAddressChanged(_troveManagerAddress);
emit ActivePoolAddressChanged(_activePoolAddress);
emit DefaultPoolAddressChanged(_defaultPoolAddress);
emit StabilityPoolAddressChanged(_stabilityPoolAddress);
Expand Down Expand Up @@ -770,7 +776,7 @@ contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOpe
}
}

function _requireICRisAboveMCR(uint256 _newICR) internal pure {
function _requireICRisAboveMCR(uint256 _newICR) internal view {
require(_newICR >= MCR, "BorrowerOps: An operation that would result in ICR < MCR is not permitted");
}

Expand Down
11 changes: 6 additions & 5 deletions contracts/src/CollateralRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry {
uint256 redeemedAmount;
}

function redeemCollateral(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _maxFeePercentage) external {
function redeemCollateral(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _maxFeePercentage)
external
{
_requireValidMaxFeePercentage(_maxFeePercentage);
_requireAmountGreaterThanZero(_boldAmount);
_requireBoldBalanceCoversRedemption(boldToken, msg.sender, _boldAmount);
Expand All @@ -135,7 +137,8 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry {
// We only compute it here, and update it at the end,
// because the final redeemed amount may be less than the requested amount
// Redeemers should take this into account in order to request the optimal amount to not overpay
uint256 redemptionRate = _calcRedemptionRate(_getUpdatedBaseRateFromRedemption(_boldAmount, totals.boldSupplyAtStart));
uint256 redemptionRate =
_calcRedemptionRate(_getUpdatedBaseRateFromRedemption(_boldAmount, totals.boldSupplyAtStart));
require(redemptionRate <= _maxFeePercentage, "CR: Fee exceeded provided maximum");
// Implicit by the above and the _requireValidMaxFeePercentage checks
//require(newBaseRate < DECIMAL_PRECISION, "CR: Fee would eat up all collateral");
Expand Down Expand Up @@ -195,9 +198,7 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry {
}

// Updates the `baseRate` state with math from `_getUpdatedBaseRateFromRedemption`
function _updateBaseRateAndGetRedemptionRate(uint256 _boldAmount, uint256 _totalBoldSupplyAtStart)
internal
{
function _updateBaseRateAndGetRedemptionRate(uint256 _boldAmount, uint256 _totalBoldSupplyAtStart) internal {
uint256 newBaseRate = _getUpdatedBaseRateFromRedemption(_boldAmount, _totalBoldSupplyAtStart);

//assert(newBaseRate <= DECIMAL_PRECISION); // This is already enforced in `_getUpdatedBaseRateFromRedemption`
Expand Down
3 changes: 0 additions & 3 deletions contracts/src/Dependencies/LiquityBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ contract LiquityBase is BaseMath, ILiquityBase {

uint256 public constant _100pct = 1000000000000000000; // 1e18 == 100%

// Minimum collateral ratio for individual troves
uint256 public constant MCR = 1100000000000000000; // 110%

// Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, Recovery Mode is triggered.
uint256 public constant CCR = 1500000000000000000; // 150%

Expand Down
4 changes: 4 additions & 0 deletions contracts/src/Dependencies/LiquityMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ library LiquityMath {
return (_a >= _b) ? _a : _b;
}

function _sub_min_0(uint256 _a, uint256 _b) internal pure returns (uint256) {
return (_a > _b) ? _a - _b : 0;
}

/*
* Multiply two decimal numbers and use normal rounding rules:
* -round product up if 19'th mantissa digit >= 5
Expand Down
1 change: 0 additions & 1 deletion contracts/src/Interfaces/IBorrowerOperations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ interface IBorrowerOperations is ILiquityBase {
function sortedTroves() external view returns (ISortedTroves);

function setAddresses(
address _troveManagerAddress,
address _activePoolAddress,
address _defaultPoolAddress,
address _stabilityPoolAddress,
Expand Down
1 change: 0 additions & 1 deletion contracts/src/Interfaces/ILiquityBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@ interface ILiquityBase {
function priceFeed() external view returns (IPriceFeed);
function BOLD_GAS_COMPENSATION() external view returns (uint256);
function MIN_NET_DEBT() external view returns (uint256);
function MCR() external view returns (uint256);
function getEntireSystemDebt() external view returns (uint256);
}
2 changes: 2 additions & 0 deletions contracts/src/Interfaces/ITroveManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import "./ISortedTroves.sol";

// Common interface for the Trove Manager.
interface ITroveManager is IERC721, ILiquityBase {
function MCR() external view returns (uint256);

function setAddresses(
address _borrowerOperationsAddress,
address _activePoolAddress,
Expand Down
139 changes: 105 additions & 34 deletions contracts/src/TroveManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {

// --- Data structures ---

// Minimum collateral ratio for individual troves
uint256 public immutable MCR;
// Liquidation penalty for troves offset to the SP
uint256 public immutable LIQUIDATION_PENALTY_SP;
// Liquidation penalty for troves redistributed
uint256 public immutable LIQUIDATION_PENALTY_REDISTRIBUTION;

uint256 public constant SECONDS_IN_ONE_YEAR = 31536000; // 60 * 60 * 24 * 365,
uint256 public constant STALE_TROVE_DURATION = 7776000; // 90 days: 60*60*24*90 = 7776000

Expand Down Expand Up @@ -242,7 +249,18 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {
redeemCollateral
}

constructor() ERC721(NAME, SYMBOL) {}
constructor(uint256 _mcr, uint256 _liquidationPenaltySP, uint256 _liquidationPenaltyRedistribution)
ERC721(NAME, SYMBOL)
{
require(_mcr > 1e18 && _mcr < 2e18, "Invalid MCR");
require(_liquidationPenaltySP >= 5e16, "SP penalty too low");
require(_liquidationPenaltySP <= _liquidationPenaltyRedistribution, "SP penalty cannot be > redist");
require(_liquidationPenaltyRedistribution <= 10e16, "Redistribution penalty too high");

MCR = _mcr;
LIQUIDATION_PENALTY_SP = _liquidationPenaltySP;
LIQUIDATION_PENALTY_REDISTRIBUTION = _liquidationPenaltyRedistribution;
}

// --- Dependency setter ---

Expand Down Expand Up @@ -313,7 +331,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {
IActivePool _activePool,
IDefaultPool _defaultPool,
uint256 _troveId,
uint256 _boldInStabPool
uint256 _boldInStabPool,
uint256 _price
) internal returns (LiquidationValues memory singleLiquidation) {
LocalVariables_InnerSingleLiquidateFunction memory vars;
(
Expand Down Expand Up @@ -342,10 +361,17 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {
singleLiquidation.debtToOffset,
singleLiquidation.collToSendToSP,
singleLiquidation.debtToRedistribute,
singleLiquidation.collToRedistribute
) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, collToLiquidate, _boldInStabPool);
singleLiquidation.collToRedistribute,
singleLiquidation.collSurplus
) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, collToLiquidate, _boldInStabPool, _price);

_closeTrove(_troveId, Status.closedByLiquidation);

// Differencen between liquidation penalty and liquidation threshold
if (singleLiquidation.collSurplus > 0) {
collSurplusPool.accountSurplus(_troveId, singleLiquidation.collSurplus);
}

emit TroveLiquidated(
_troveId,
singleLiquidation.entireTroveDebt,
Expand Down Expand Up @@ -416,12 +442,19 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {
singleLiquidation.debtToOffset,
singleLiquidation.collToSendToSP,
singleLiquidation.debtToRedistribute,
singleLiquidation.collToRedistribute
singleLiquidation.collToRedistribute,
singleLiquidation.collSurplus
) = _getOffsetAndRedistributionVals(
singleLiquidation.entireTroveDebt, vars.collToLiquidate, _boldInStabPool
singleLiquidation.entireTroveDebt, vars.collToLiquidate, _boldInStabPool, _price
);

_closeTrove(_troveId, Status.closedByLiquidation);

// Differencen between liquidation penalty and liquidation threshold
if (singleLiquidation.collSurplus > 0) {
collSurplusPool.accountSurplus(_troveId, singleLiquidation.collSurplus);
}

emit TroveLiquidated(
_troveId,
singleLiquidation.entireTroveDebt,
Expand Down Expand Up @@ -475,33 +508,67 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {
*/
function _getOffsetAndRedistributionVals(
uint256 _entireTroveDebt,
uint256 _collToLiquidate,
uint256 _boldInStabPool
uint256 _collToLiquidate, // gas compensation is already subtracted
uint256 _boldInStabPool,
uint256 _price
)
internal
pure
returns (uint256 debtToOffset, uint256 collToSendToSP, uint256 debtToRedistribute, uint256 collToRedistribute)
view
returns (
uint256 debtToOffset,
uint256 collToSendToSP,
uint256 debtToRedistribute,
uint256 collToRedistribute,
uint256 collSurplus
)
{
uint256 collSPPortion;
/*
* Offset as much debt & collateral as possible against the Stability Pool, and redistribute the remainder
* between all active troves.
*
* If the trove's debt is larger than the deposited Bold in the Stability Pool:
*
* - Offset an amount of the trove's debt equal to the Bold in the Stability Pool
* - Send a fraction of the trove's collateral to the Stability Pool, equal to the fraction of its offset debt
*
*/
if (_boldInStabPool > 0) {
/*
* Offset as much debt & collateral as possible against the Stability Pool, and redistribute the remainder
* between all active troves.
*
* If the trove's debt is larger than the deposited Bold in the Stability Pool:
*
* - Offset an amount of the trove's debt equal to the Bold in the Stability Pool
* - Send a fraction of the trove's collateral to the Stability Pool, equal to the fraction of its offset debt
*
*/
debtToOffset = LiquityMath._min(_entireTroveDebt, _boldInStabPool);
collToSendToSP = _collToLiquidate * debtToOffset / _entireTroveDebt;
debtToRedistribute = _entireTroveDebt - debtToOffset;
collToRedistribute = _collToLiquidate - collToSendToSP;
collSPPortion = _collToLiquidate * debtToOffset / _entireTroveDebt;
(collToSendToSP, collSurplus) =
_getCollPenaltyAndSurplus(collSPPortion, debtToOffset, LIQUIDATION_PENALTY_SP, _price);
}
// TODO: this fails if debt in gwei is less than price (rounding coll to zero)
//assert(debtToOffset == 0 || collToSendToSP > 0);

// Redistribution
debtToRedistribute = _entireTroveDebt - debtToOffset;
if (debtToRedistribute > 0) {
uint256 collRedistributionPortion = _collToLiquidate - collSPPortion;
if (collRedistributionPortion > 0) {
(collToRedistribute, collSurplus) = _getCollPenaltyAndSurplus(
collRedistributionPortion + collSurplus, // Coll surplus from offset can be eaten up by red. penalty
debtToRedistribute, LIQUIDATION_PENALTY_REDISTRIBUTION, _price
);
}
}
assert(_collToLiquidate == collToSendToSP + collToRedistribute + collSurplus);
}

function _getCollPenaltyAndSurplus(
uint256 _collToLiquidate,
uint256 _debtToLiquidate,
uint256 _penaltyRatio,
uint256 _price
) internal pure returns (uint256 collPenalty, uint256 collSurplus) {
uint256 maxCollWithPenalty = _debtToLiquidate * (DECIMAL_PRECISION + _penaltyRatio) / _price;
if (_collToLiquidate > maxCollWithPenalty) {
collPenalty = maxCollWithPenalty;
collSurplus = _collToLiquidate - maxCollWithPenalty;
} else {
debtToOffset = 0;
collToSendToSP = 0;
debtToRedistribute = _entireTroveDebt;
collToRedistribute = _collToLiquidate;
collPenalty = _collToLiquidate;
collSurplus = 0;
}
}

Expand All @@ -514,11 +581,12 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {
uint256 _recordedTroveDebt,
uint256 _weightedRecordedTroveDebt,
uint256 _price
) internal pure returns (LiquidationValues memory singleLiquidation) {
) internal view returns (LiquidationValues memory singleLiquidation) {
singleLiquidation.entireTroveDebt = _entireTroveDebt;
singleLiquidation.entireTroveColl = _entireTroveColl;
singleLiquidation.recordedTroveDebt = _recordedTroveDebt;
singleLiquidation.weightedRecordedTroveDebt = _weightedRecordedTroveDebt;
// TODO: We don’t bother updating this because we are removing RM
uint256 cappedCollPortion = _entireTroveDebt * MCR / _price;

singleLiquidation.collGasCompensation = _getCollGasCompensation(cappedCollPortion);
Expand Down Expand Up @@ -575,7 +643,10 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {
);

// Move liquidated ETH and Bold to the appropriate pools
stabilityPoolCached.offset(totals.totalDebtToOffset, totals.totalCollToSendToSP);
if (totals.totalDebtToOffset > 0 || totals.totalCollToSendToSP > 0) {
stabilityPoolCached.offset(totals.totalDebtToOffset, totals.totalCollToSendToSP);
}
// we check amount is not zero inside
_redistributeDebtAndColl(
activePoolCached, defaultPoolCached, totals.totalDebtToRedistribute, totals.totalCollToRedistribute
);
Expand Down Expand Up @@ -646,7 +717,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {
!_checkPotentialRecoveryMode(vars.entireSystemColl, vars.entireSystemDebt, _price);
} else if (vars.backToNormalMode && vars.ICR < MCR) {
singleLiquidation =
_liquidateNormalMode(_activePool, _defaultPool, vars.troveId, vars.remainingBoldInStabPool);
_liquidateNormalMode(_activePool, _defaultPool, vars.troveId, vars.remainingBoldInStabPool, _price);
vars.remainingBoldInStabPool = vars.remainingBoldInStabPool - singleLiquidation.debtToOffset;

// Add liquidation values to their respective running totals
Expand Down Expand Up @@ -675,7 +746,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {

if (vars.ICR < MCR) {
singleLiquidation =
_liquidateNormalMode(_activePool, _defaultPool, vars.troveId, vars.remainingBoldInStabPool);
_liquidateNormalMode(_activePool, _defaultPool, vars.troveId, vars.remainingBoldInStabPool, _price);
vars.remainingBoldInStabPool = vars.remainingBoldInStabPool - singleLiquidation.debtToOffset;

// Add liquidation values to their respective running totals
Expand Down Expand Up @@ -772,8 +843,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {
if (_getNetDebt(singleRedemption.newRecordedTroveDebt) < MIN_NET_DEBT) {
Troves[_troveId].status = Status.unredeemable;
sortedTroves.remove(_troveId);
// TODO: should we also remove from the Troves array? Seems unneccessary as it's only used for off-chain hints.
// We save borrowers gas by not removing
// TODO: should we also remove from the Troves array? Seems unneccessary as it's only used for off-chain hints.
// We save borrowers gas by not removing
}
Troves[_troveId].debt = singleRedemption.newRecordedTroveDebt;
Troves[_troveId].coll = newColl;
Expand Down Expand Up @@ -1233,7 +1304,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager {

return TCR < CCR;
}

function checkTroveIsOpen(uint256 _troveId) public view returns (bool) {
Status status = Troves[_troveId].status;
return status == Status.active || status == Status.unredeemable;
Expand Down
Loading

0 comments on commit 3fee998

Please sign in to comment.