Skip to content

Commit

Permalink
SD utility pool (#212)
Browse files Browse the repository at this point in the history
* basic utility pool structure

* sdCollateral intergration with utilityPool

* Rename SDx.sol to SDX.sol

* terminology change

* SD Incentive Controller (#210)

* Initial code

* Change terminologies

* IncentiveController->SDIncentiveController

* Run prettier

* Use block number and read from staderConfig

* utilize flow

* dwlwgator withdraw flow

* seperate function in node registry to utilze SD

* view function to get latest data

* Liquidation (#211)

* User struct

* mapping

* calculation

* liquidationCall initial

* Add riskconfig

* Add back comments

* include pause functionality

* repay flow changes and comments

* Operator reward integration (#213)

* init

* Introduce owedAmount

* Update OperatorRewardsCollector.sol

* Add PoolUtils

* claimLiquidation

* Fix compile

* Fix claimFor

* Add liquidator data

* review fixes

* deposit SD reward as collateral changes

* Fix compilation

* Change function orders

* Fix review

* Add exit operator

* deposit SD reward as collateral flow changes

* Fix review

* Use operatorUtilizedSDBalance

* custom error message

* deploy script for utility pool

* Updates on incentiveController

* Add docs and checkers

* fix claim function time cool down logic

* Add withdrawable

* rename function and variables

* Add non-existent liquidation check

* fix sdCollateral withdraw function

* Fix claim logic

* refactor slash SD

* whenNotPaused modifier

* Claim rewards during requestWithdraw

* review fixes

* change in request withdraw logic

* Review fix

* Introduce weth

* introducing withdrawOnBehalf in SDCollateral

* Transfer back utilized SD

* add getter in interface

* incentiveController and addValidatorKey fix

* test case for utilze and delegate flow

* referral Id for adding validator keys

* sdCollateral test cases

* SD token decimal adjust

* Multiply by DECIMAL

* Liquidation test

* Only manager role for certain functions

* test fix

* Fix weth issue

* Small fix and unit test

* Minimum reward amount

* Claim available amount

* Lint test

* Add start incentive later test

* SDP-06 fix

* SDP-11 fix

* Fix SDP-03

* SDP-02 fix

* push back SDP-03 fix

* SDP-07 fix

* SDP-17 fix

* Take initial _delegate into consideration

* whenNotPaused test cases

* comment fix

* Expand liquidation call tests

* Update SDIncentiveController.t.sol

* Separate claim and claimLiquidation

* Add test

* Quick fix

* Remove claimFor

* Fix test

* Lint and add tests

* small change claim

* Add emit tests

* Refactor test

* add emit test

* claim after liquidation

* change initial delegate amount to 1 SD

* Initialise risk config at initialize

* Internal function prefix with _

* renaming change

* Collateral in ETH and expose liquidationIndexByOperator

* rounding up interest in ETH

* reward collector test fixed

* foundry deploy script

* minimum withdraw and delegate limits

* rounding up cTokenShare in requestWithdrawWithSD call

* fix utilityPool with min delegate amount

* add clearUtilizedPosition function

* add totalUtilizedSD getter in interface

* clearing utilized position using withdrawOnBehalf

* optimize _transferBackUtilizedSD

* handling of edge cases when nonTerminalKeys are 0

* update interface

* add utilityPool deploy script

* introduce claimWithAmount function in rewardCollector

* fix: only call withdraw if balance of nodeELVault greater than 0

* updating operatorRewardsCollector Interface

* adding third party license file

* fix formatting of README

* add title in License file

* formatting the title of License file

---------

Co-authored-by: Sanjay Yadav <[email protected]>
Co-authored-by: Dulguun <[email protected]>
  • Loading branch information
3 people authored and jac18281828 committed Apr 18, 2024
1 parent 34ba126 commit 1939e6c
Show file tree
Hide file tree
Showing 39 changed files with 4,062 additions and 72 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ lcov.info
# Foundry
cache_forge/
out/
broadcast



13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,16 @@ forge coverage
# Integration

Check the Integration guide [here](https://github.com/stader-labs/ethx/blob/mainnet_V0/INTEGRATION.md)

# Dependencies
This project incorporates code from the following external sources:
- Compound Labs, Inc. (licensed under the BSD-3-Clause License)

- Ben Hauser (licensed under the MIT License)

The borrowed code contributes to 'SDUtilityPool.sol for computing fee, utilizer balance and exchange rate for a C-token based model' and 'SDIncentiveController.sol for computing incentivize rewards to the delegator of UtilityPool' respectively. For further details on the specific code sections and their respective licenses, please refer to the Third-Party Licenses file.

Link to Third-Party Licenses:

[THIRD-PARTY-LICENSES.md](https://github.com/stader-labs/ethx/blob/mainnet_V0/THIRD-PARTY-LICENSES.md)

82 changes: 82 additions & 0 deletions THIRD-PARTY-LICENSES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
THIRD PARTY LICENSES

Compound Code (BSD-3-Clause License)

This project incorporates portions of code originally developed by Compound Labs, Inc. and
licensed under the BSD-3-Clause License ("BSD-3-Clause"). The full text of the BSD-3-Clause
License is included below for your reference:

Copyright (c) 2020 Compound Labs, Inc.

Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.

- Neither the name of the copyright holder nor the names of its contributors may be used
to endorse or promote products derived from this software without specific prior written
permission.


DISCLAIMER:

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

The following sections of this project codebase are derived from the Compound code:

contracts/SDUtilityPool.sol - accrueFee(), utilizerBalanceCurrent(address account),
utilizerBalanceStored(address account), _utilizerBalanceStoredInternal(address account),
exchangeRateCurrent(),exchangeRateStored(), _exchangeRateStored().

These are the logic to compute the rewards, compute utilizer balance and
exchangeRate computation for C-token based model.

Curvefi Code (MIT License)

This project incorporates portions of code originally developed by Ben Hauser and licensed
under the MIT License. The full text of the MIT License is included below for your reference:

MIT License

Copyright (c) 2020 Ben Hauser

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to
do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Specific Code Attribution:

The following sections of this project codebase are derived from the Curvefi code:

contracts/SDIncentiveController.sol - updateRewardForAccount(address account),
rewardPerToken() and earned(address account).

These are mainly the logic for storing and computing user incentivize rewards.
172 changes: 166 additions & 6 deletions contracts/OperatorRewardsCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ pragma solidity 0.8.16;

import './library/UtilLib.sol';

import './interfaces/INodeRegistry.sol';
import './interfaces/INodeELRewardVault.sol';
import './interfaces/IPermissionlessNodeRegistry.sol';
import './interfaces/IOperatorRewardsCollector.sol';
import './interfaces/IStaderConfig.sol';
import './interfaces/ISDUtilityPool.sol';
import './interfaces/SDCollateral/ISDCollateral.sol';
import './interfaces/IWETH.sol';
import '../contracts/interfaces/IStaderOracle.sol';

import '@openzeppelin/contracts/utils/math/Math.sol';
import '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol';

contract OperatorRewardsCollector is IOperatorRewardsCollector, AccessControlUpgradeable {
IStaderConfig public staderConfig;

mapping(address => uint256) public balances;

IWETH public weth;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
Expand All @@ -36,19 +46,169 @@ contract OperatorRewardsCollector is IOperatorRewardsCollector, AccessControlUpg
emit DepositedFor(msg.sender, _receiver, msg.value);
}

/**
* @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.
*/
function claim() external {
address operator = msg.sender;
uint256 amount = balances[operator];
balances[operator] -= amount;
claimLiquidation(msg.sender);

address operatorRewardsAddr = UtilLib.getOperatorRewardAddress(msg.sender, staderConfig);
UtilLib.sendValue(operatorRewardsAddr, amount);
emit Claimed(operatorRewardsAddr, amount);
uint256 amount = balances[msg.sender] > withdrawableInEth(msg.sender)
? withdrawableInEth(msg.sender)
: balances[msg.sender];
_claim(msg.sender, amount);
}

/**
* @notice function to claim a given amount of ETH, this is done to make sure operator can claim
* any desired amount such that their health factor remains above 1
* @dev amount should not be more than the minimum of operator balance and withdrawableInEth after completing liquidation if any
* @param _amount amount of ETH to claim
*/
function claimWithAmount(uint256 _amount) external {
claimLiquidation(msg.sender);
_claim(msg.sender, _amount);
}

function claimLiquidation(address operator) public override {
_transferBackUtilizedSD(operator);
_completeLiquidationIfExists(operator);
}

function updateStaderConfig(address _staderConfig) external onlyRole(DEFAULT_ADMIN_ROLE) {
UtilLib.checkNonZeroAddress(_staderConfig);
staderConfig = IStaderConfig(_staderConfig);
emit UpdatedStaderConfig(_staderConfig);
}

function withdrawableInEth(address operator) public view override returns (uint256) {
ISDUtilityPool sdUtilityPool = ISDUtilityPool(staderConfig.getSDUtilityPool());
uint256 liquidationThreshold = sdUtilityPool.getLiquidationThreshold();
UserData memory userData = sdUtilityPool.getUserData(operator);
uint256 totalInterestAdjusted = (userData.totalInterestSD * 100) / liquidationThreshold;
uint256 sdPriceInETH = IStaderOracle(staderConfig.getStaderOracle()).getSDPriceInETH();

uint256 totalInterestAdjustedInEth = Math.ceilDiv(
(totalInterestAdjusted * sdPriceInETH),
staderConfig.getDecimals()
);

if (totalInterestAdjustedInEth > userData.totalCollateralInEth) return 0;
uint256 withdrawableAmountInEth = userData.totalCollateralInEth - totalInterestAdjustedInEth;

OperatorLiquidation memory operatorLiquidation = sdUtilityPool.getOperatorLiquidation(operator);
return
withdrawableAmountInEth > operatorLiquidation.totalAmountInEth
? withdrawableAmountInEth - operatorLiquidation.totalAmountInEth
: 0;
}

function updateWethAddress(address _weth) external onlyRole(DEFAULT_ADMIN_ROLE) {
UtilLib.checkNonZeroAddress(_weth);
weth = IWETH(_weth);
emit UpdatedWethAddress(_weth);
}

function getBalance(address operator) external view override returns (uint256) {
return balances[operator];
}

/**
* @notice Completes any pending liquidation for an operator if exists.
* @dev Internal function to handle liquidation completion.
* @param operator The operator whose liquidation needs to be checked.
*/
function _completeLiquidationIfExists(address operator) internal {
// Retrieve operator liquidation details
ISDUtilityPool sdUtilityPool = ISDUtilityPool(staderConfig.getSDUtilityPool());
OperatorLiquidation memory operatorLiquidation = sdUtilityPool.getOperatorLiquidation(operator);

// If the liquidation is not repaid, check balance and then proceed with repayment
if (!operatorLiquidation.isRepaid && operatorLiquidation.totalAmountInEth > 0) {
(uint8 poolId, uint256 operatorId, uint256 nonTerminalKeys) = ISDCollateral(staderConfig.getSDCollateral())
.getOperatorInfo(operator);
// Ensure that the balance is sufficient
if (balances[operator] < operatorLiquidation.totalAmountInEth && nonTerminalKeys > 0) {
revert InsufficientBalance();
}
address permissionlessNodeRegistry = staderConfig.getPermissionlessNodeRegistry();
if (INodeRegistry(permissionlessNodeRegistry).POOL_ID() == poolId) {
address nodeELVault = IPermissionlessNodeRegistry(permissionlessNodeRegistry)
.nodeELRewardVaultByOperatorId(operatorId);
if (nodeELVault.balance > 0) {
INodeELRewardVault(nodeELVault).withdraw();
}
}
if (balances[operator] < operatorLiquidation.totalAmountInEth) {
uint256 wETHDeposit = Math.min(
balances[operator],
operatorLiquidation.totalAmountInEth - operatorLiquidation.totalFeeInEth
);

weth.deposit{value: wETHDeposit}();
if (weth.transferFrom(address(this), operatorLiquidation.liquidator, wETHDeposit) == false)
revert WethTransferFailed();
balances[operator] -= wETHDeposit;

uint256 protocolFee = Math.min(operatorLiquidation.totalFeeInEth, balances[operator]);
UtilLib.sendValue(staderConfig.getStaderTreasury(), protocolFee);

balances[operator] -= protocolFee;
sdUtilityPool.completeLiquidation(operator);
} else {
// Transfer WETH to liquidator and ETH to treasury
weth.deposit{value: operatorLiquidation.totalAmountInEth - operatorLiquidation.totalFeeInEth}();
if (
weth.transferFrom(
address(this),
operatorLiquidation.liquidator,
operatorLiquidation.totalAmountInEth - operatorLiquidation.totalFeeInEth
) == false
) revert WethTransferFailed();
UtilLib.sendValue(staderConfig.getStaderTreasury(), operatorLiquidation.totalFeeInEth);

sdUtilityPool.completeLiquidation(operator);
balances[operator] -= operatorLiquidation.totalAmountInEth;
}
}
}

/**
* @notice Internal function to claim a specified amount for an operator.
* @dev Deducts the amount from the operator's balance and transfers it to their rewards address.
* It also checks if the claiming amount does not exceed the withdrawable limit or the operator's balance.
* @param operator The address of the operator claiming the amount.
* @param amount The amount to be claimed.
*/
function _claim(address operator, uint256 amount) internal {
uint256 maxWithdrawableInEth = withdrawableInEth(operator);

if (amount > maxWithdrawableInEth || amount > balances[operator]) revert InsufficientBalance();

balances[operator] -= amount;

// If there's an amount to send, transfer it to the operator's rewards address
if (amount > 0) {
address rewardsAddress = UtilLib.getOperatorRewardAddress(operator, staderConfig);
UtilLib.sendValue(rewardsAddress, amount);
emit Claimed(rewardsAddress, amount);
}
}

/**
* When the operator has no remaining active keys, transfer back the utilized SD to the operator.
* @param operator The address of the operator whose utilized SD needs to be transferred back.
*/
function _transferBackUtilizedSD(address operator) internal {
ISDCollateral sdCollateral = ISDCollateral(staderConfig.getSDCollateral());
(, , uint256 nonTerminalKeys) = sdCollateral.getOperatorInfo(operator);

uint256 operatorUtilizedSDBal = sdCollateral.operatorUtilizedSDBalance(operator);

// Only proceed if the operator has no non-terminal (active) keys left and nonZero utilizedSD Balance
if (nonTerminalKeys > 0 || (operatorUtilizedSDBal == 0)) return;

// transfer back the operator's utilized SD balance to SD Utility Pool
sdCollateral.transferBackUtilizedSD(operator);
}
}
Loading

0 comments on commit 1939e6c

Please sign in to comment.