diff --git a/contracts/OperatorRewardsCollector.sol b/contracts/OperatorRewardsCollector.sol index 3a322ac9..1b97c260 100644 --- a/contracts/OperatorRewardsCollector.sol +++ b/contracts/OperatorRewardsCollector.sol @@ -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()); @@ -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); @@ -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); diff --git a/contracts/SDIncentiveController.sol b/contracts/SDIncentiveController.sol index 3613621f..06df67ff 100644 --- a/contracts/SDIncentiveController.sol +++ b/contracts/SDIncentiveController.sol @@ -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; @@ -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. @@ -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. diff --git a/contracts/SDUtilityPool.sol b/contracts/SDUtilityPool.sol index cbcc0931..cf034f42 100644 --- a/contracts/SDUtilityPool.sol +++ b/contracts/SDUtilityPool.sol @@ -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) { @@ -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); @@ -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); @@ -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, @@ -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); } /** @@ -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; @@ -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); @@ -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]; } /** diff --git a/contracts/interfaces/IOperatorRewardsCollector.sol b/contracts/interfaces/IOperatorRewardsCollector.sol index 8d525a8c..7584ad9a 100644 --- a/contracts/interfaces/IOperatorRewardsCollector.sol +++ b/contracts/interfaces/IOperatorRewardsCollector.sol @@ -16,4 +16,10 @@ interface IOperatorRewardsCollector { function claim() external; function claimFor(address account) external; + + function claimLiquidation( + uint256 liquidatorAmount, + uint256 feeAmount, + address liquidator + ) external; } diff --git a/contracts/interfaces/ISDUtilityPool.sol b/contracts/interfaces/ISDUtilityPool.sol index 6a3b7350..79fffb1b 100644 --- a/contracts/interfaces/ISDUtilityPool.sol +++ b/contracts/interfaces/ISDUtilityPool.sol @@ -40,6 +40,7 @@ interface ISDUtilityPool { error UndelegationPeriodNotPassed(); error MaxLimitOnWithdrawRequestCountReached(); error RequestIdNotFinalized(uint256 requestId); + error AlreadyLiquidated(); event WithdrawnProtocolFee(uint256 amount); event ProtocolFeeFactorUpdated(uint256 protocolFeeFactor); @@ -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);