Skip to content

Commit

Permalink
Add docs and checkers
Browse files Browse the repository at this point in the history
  • Loading branch information
dulguun-staderlabs committed Dec 6, 2023
1 parent 62d839b commit df0a793
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 31 deletions.
28 changes: 25 additions & 3 deletions contracts/OperatorRewardsCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ contract OperatorRewardsCollector is IOperatorRewardsCollector, AccessControlUpg
return claimFor(msg.sender);
}

/**
* @notice Claims payouts for an operator, repaying any outstanding liquidations and transferring any remaining balance to the operator's rewards address.
* @dev This function first checks for any unpaid liquidations for the operator and repays them if necessary. Then, it transfers any remaining balance to the operator's reward address.
* @param operator The address of the operator for whom the claim is being made.
*/
function claimFor(address operator) public override {
// Retrieve operator liquidation details
ISDUtilityPool sdUtilityPool = ISDUtilityPool(staderConfig.getSDUtilityPool());
Expand All @@ -49,9 +54,7 @@ contract OperatorRewardsCollector is IOperatorRewardsCollector, AccessControlUpg
// If the liquidation is not repaid, check balance and then proceed with repayment
if (!operatorLiquidation.isRepaid) {
// Ensure that the balance is sufficient
if (balances[operator] < operatorLiquidation.totalAmountInEth) {
revert InsufficientBalance();
}
if (balances[operator] < operatorLiquidation.totalAmountInEth) revert InsufficientBalance();

// Repay the liquidation and update the operator's balance
sdUtilityPool.repayLiquidation(operator);
Expand All @@ -70,6 +73,25 @@ contract OperatorRewardsCollector is IOperatorRewardsCollector, AccessControlUpg
}
}

/**
* @notice Distributes the liquidation payout and fee. It sends a specified amount to the liquidator and a fee to the Stader treasury.
* @dev This function should only be called by the SD Utility Pool contract as part of the liquidation process. It uses UtilLib to safely send ETH.
* @param liquidatorAmount The amount of ETH to be sent to the liquidator.
* @param feeAmount The amount of ETH to be sent to the Stader treasury as a fee.
* @param liquidator The address of the liquidator.
*/
function claimLiquidation(
uint256 liquidatorAmount,
uint256 feeAmount,
address liquidator
) external override {
// Ensure only the SD Utility Pool contract can call this function
UtilLib.onlyStaderContract(msg.sender, staderConfig, staderConfig.SD_UTILITY_POOL());

UtilLib.sendValue(liquidator, liquidatorAmount);
UtilLib.sendValue(staderConfig.getStaderTreasury(), feeAmount);
}

function updateStaderConfig(address _staderConfig) external onlyRole(DEFAULT_ADMIN_ROLE) {
UtilLib.checkNonZeroAddress(_staderConfig);
staderConfig = IStaderConfig(_staderConfig);
Expand Down
8 changes: 6 additions & 2 deletions contracts/SDIncentiveController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import './interfaces/ISDIncentiveController.sol';
/// @title SDIncentiveController
/// @notice This contract handles the distribution of reward tokens for the utility pool.
contract SDIncentiveController is ISDIncentiveController, AccessControlUpgradeable {
uint256 public constant DECIMAL = 1e18;

// The emission rate of the reward tokens per block.
uint256 public emissionPerBlock;

Expand Down Expand Up @@ -112,7 +114,8 @@ contract SDIncentiveController is ISDIncentiveController, AccessControlUpgradeab
return rewardPerTokenStored;
}
return
rewardPerTokenStored + (((block.number - lastUpdateBlockNumber) * emissionPerBlock * 1e18) / totalSupply);
rewardPerTokenStored +
(((block.number - lastUpdateBlockNumber) * emissionPerBlock * DECIMAL) / totalSupply);
}

/// @notice Calculates the total accrued reward for an account.
Expand All @@ -122,7 +125,8 @@ contract SDIncentiveController is ISDIncentiveController, AccessControlUpgradeab
uint256 currentBalance = ISDUtilityPool(staderConfig.getSDUtilityPool()).delegatorCTokenBalance(account);
uint256 currentRewardPerToken = rewardPerToken();

return ((currentBalance * (currentRewardPerToken - userRewardPerTokenPaid[account])) / 1e18) + rewards[account];
return
((currentBalance * (currentRewardPerToken - userRewardPerTokenPaid[account])) / DECIMAL) + rewards[account];
}

/// @dev Internal function to update the reward state for an account.
Expand Down
99 changes: 73 additions & 26 deletions contracts/SDUtilityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,14 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr
emit AccruedFees(feeAccumulated, accumulatedProtocolFee, totalUtilizedSD);
}

/**
* @notice Initiates the liquidation process for an account if its health factor is below the required threshold.
* @dev The function checks the health factor, accrues fees, updates utilized indices, and calculates liquidation amounts.
* @param account The address of the account to be liquidated
*/
function liquidationCall(address account) external override {
if (liquidationIndexByOperator[account] != 0) revert AlreadyLiquidated();

UserData memory userData = getUserData(account);

if (userData.healthFactor > 1) {
Expand All @@ -362,7 +369,7 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr

accrueFee();
utilizerData[account].utilizeIndex = utilizeIndex;
totalUtilizedSD = totalUtilizedSD - userData.totalInterestSD;
totalUtilizedSD -= userData.totalInterestSD;

IERC20(staderConfig.getStaderToken()).transferFrom(msg.sender, address(this), userData.totalInterestSD);

Expand All @@ -372,16 +379,16 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr
uint256 liquidationFeeInEth = (totalInterestInEth * riskConfig.liquidationFeePercent) / 100;
uint256 totalLiquidationAmountInEth = totalInterestInEth + liquidationBonusInEth + liquidationFeeInEth;

OperatorLiquidation memory liquidation = OperatorLiquidation(
totalLiquidationAmountInEth,
liquidationBonusInEth,
liquidationFeeInEth,
false,
false,
msg.sender
);
OperatorLiquidation memory liquidation = OperatorLiquidation({
totalAmountInEth: totalLiquidationAmountInEth,
totalBonusInEth: liquidationBonusInEth,
totalFeeInEth: liquidationFeeInEth,
isRepaid: false,
isClaimed: false,
liquidator: msg.sender
});
liquidations.push(liquidation);
liquidationIndexByOperator[account] = liquidations.length - 1;
liquidationIndexByOperator[account] = liquidations.length;

IPoolUtils(staderConfig.getPoolUtils()).processOperatorExit(account, totalLiquidationAmountInEth / 4 + 1);

Expand All @@ -394,23 +401,26 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr
);
}

/**
* @notice Allows a liquidator to claim the ETH amount and fees from a completed liquidation.
* @dev This function requires that the liquidation is marked as repaid, not already claimed, and that the caller is the liquidator.
* @param index The index of the liquidation in the liquidations array
*/
function claimLiquidation(uint256 index) external override {
if (index >= liquidations.length) revert InvalidInput();

OperatorLiquidation storage liquidation = liquidations[index];

if (!liquidation.isRepaid) {
revert NotClaimable();
}
if (liquidation.isClaimed) {
revert AlreadyClaimed();
}
if (liquidation.liquidator != msg.sender) {
revert NotLiquidator();
}
if (!liquidation.isRepaid) revert NotClaimable();
if (liquidation.isClaimed) revert AlreadyClaimed();
if (liquidation.liquidator != msg.sender) revert NotLiquidator();

liquidation.isClaimed = true;

UtilLib.sendValue(msg.sender, liquidation.totalAmountInEth - liquidation.totalFeeInEth);
UtilLib.sendValue(staderConfig.getStaderTreasury(), liquidation.totalFeeInEth);
IOperatorRewardsCollector(staderConfig.getOperatorRewardsCollector()).claimLiquidation(
liquidation.totalAmountInEth - liquidation.totalFeeInEth,
liquidation.totalFeeInEth,
liquidation.liquidator
);

emit ClaimedLiquidation(
msg.sender,
Expand All @@ -432,7 +442,35 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr
function repayLiquidation(address account) external override {
UtilLib.onlyStaderContract(msg.sender, staderConfig, staderConfig.OPERATOR_REWARD_COLLECTOR());

liquidations[liquidationIndexByOperator[account]].isRepaid = true;
liquidations[liquidationIndexByOperator[account] - 1].isRepaid = true;
liquidationIndexByOperator[account] = 0;
}

/**
* @notice Updates the risk configuration
* @param liquidationThreshold The new liquidation threshold percent (1 - 100)
* @param liquidationBonusPercent The new liquidation bonus percent (0 - 100)
* @param liquidationFeePercent The new liquidation fee percent (0 - 100)
* @param ltv The new loan-to-value ratio (1 - 100)
*/
function updateRiskConfig(
uint256 liquidationThreshold,
uint256 liquidationBonusPercent,
uint256 liquidationFeePercent,
uint256 ltv
) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (liquidationThreshold > 100 || liquidationThreshold == 0) revert InvalidInput();
if (liquidationBonusPercent > 100) revert InvalidInput();
if (liquidationFeePercent > 100) revert InvalidInput();
if (ltv > 100 || ltv == 0) revert InvalidInput();

riskConfig = RiskConfig({
liquidationThreshold: liquidationThreshold,
liquidationBonusPercent: liquidationBonusPercent,
liquidationFeePercent: liquidationFeePercent,
ltv: ltv
});
emit RiskConfigUpdated(liquidationThreshold, liquidationBonusPercent, liquidationFeePercent, ltv);
}

/**
Expand Down Expand Up @@ -656,13 +694,17 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr
return (totalUtilizedSD * DECIMAL) / (getPoolAvailableSDBalance() + totalUtilizedSD - accumulatedProtocolFee);
}

/**
* @notice Calculates and returns the user data for a given account
* @param account The address whose utilisation should be calculated
* @return UserData struct containing the user data
*/
function getUserData(address account) public view returns (UserData memory) {
address staderOracle = staderConfig.getStaderOracle();
uint256 sdPriceInEth = IStaderOracle(staderOracle).getSDPriceInETH();
uint256 totalInterestSD = getUtilizerLatestBalance(account) -
ISDCollateral(staderConfig.getSDCollateral()).operatorUtilizedSDBalance(account);

// Multiplying other values by sdPriceInEth to avoid division
uint256 totalCollateralInEth = getOperatorTotalEth(account);
uint256 totalCollateralInSD = totalCollateralInEth / sdPriceInEth;

Expand All @@ -675,10 +717,15 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr
totalInterestSD,
totalCollateralInSD,
healthFactor,
liquidations[liquidationIndexByOperator[account]].totalAmountInEth
liquidations[liquidationIndexByOperator[account] - 1].totalAmountInEth
);
}

/**
* @notice
* @param operator Calculates and returns the conservative estimate of the total Ether (ETH) bonded by a given operator.
* @return totalEth The total ETH bonded by the operator
*/
function getOperatorTotalEth(address operator) public view returns (uint256) {
(, , uint256 totalValidators) = ISDCollateral(staderConfig.getSDCollateral()).getOperatorInfo(operator);

Expand All @@ -688,7 +735,7 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr
}

function getOperatorLiquidation(address account) external view override returns (OperatorLiquidation memory) {
return liquidations[liquidationIndexByOperator[account]];
return liquidations[liquidationIndexByOperator[account] - 1];
}

/**
Expand Down
6 changes: 6 additions & 0 deletions contracts/interfaces/IOperatorRewardsCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ interface IOperatorRewardsCollector {
function claim() external;

function claimFor(address account) external;

function claimLiquidation(
uint256 liquidatorAmount,
uint256 feeAmount,
address liquidator
) external;
}
7 changes: 7 additions & 0 deletions contracts/interfaces/ISDUtilityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface ISDUtilityPool {
error UndelegationPeriodNotPassed();
error MaxLimitOnWithdrawRequestCountReached();
error RequestIdNotFinalized(uint256 requestId);
error AlreadyLiquidated();

event WithdrawnProtocolFee(uint256 amount);
event ProtocolFeeFactorUpdated(uint256 protocolFeeFactor);
Expand All @@ -64,6 +65,12 @@ interface ISDUtilityPool {
address indexed liquidator
);
event ClaimedLiquidation(address indexed liquidator, uint256 liquidationBonusInEth, uint256 liquidationFeeInEth);
event RiskConfigUpdated(
uint256 liquidationThreshold,
uint256 liquidationBonusPercent,
uint256 liquidationFeePercent,
uint256 ltv
);

event AccruedFees(uint256 feeAccumulated, uint256 totalProtocolFee, uint256 totalUtilizedSD);

Expand Down

0 comments on commit df0a793

Please sign in to comment.