diff --git a/contracts/SDIncentiveController.sol b/contracts/SDIncentiveController.sol new file mode 100644 index 00000000..b4b13ef4 --- /dev/null +++ b/contracts/SDIncentiveController.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.16; + +import '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol'; +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import './library/UtilLib.sol'; +import './interfaces/IStaderConfig.sol'; +import './interfaces/ISDIncentiveController.sol'; + +/// @title SDIncentiveController +/// @notice This contract handles the distribution of reward tokens for a lending pool. +contract SDIncentiveController is ISDIncentiveController, AccessControlUpgradeable { + // The emission rate of the reward tokens per block. + uint256 public emissionPerBlock; + + // The block number of the last reward calculation. + uint256 public lastUpdateBlockNumber; + + // The stored value of the reward per token, used to calculate rewards. + uint256 public rewardPerTokenStored; + + // Reference to the Stader configuration contract. + IStaderConfig public staderConfig; + + // A mapping of accounts to their pending reward amounts. + mapping(address => uint256) public rewards; + + // A mapping of accounts to the reward per token value at their last update. + mapping(address => uint256) public userRewardPerTokenPaid; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract with necessary addresses. + /// @param _staderConfig The address of the Stader configuration contract. + function initialize(address _staderConfig) external initializer { + UtilLib.checkNonZeroAddress(_staderConfig); + + staderConfig = IStaderConfig(_staderConfig); + + __AccessControl_init(); + } + + /// @notice Claims the accrued rewards for an account. + /// @param account The address of the account claiming rewards. + function claim(address account) external { + UtilLib.onlyStaderContract(msg.sender, staderConfig, staderConfig.SD_UTILITY_POOL()); + + updateReward(account); + + uint256 reward = rewards[account]; + require(reward > 0, 'No rewards to claim.'); + rewards[account] = 0; + IERC20(staderConfig.getStaderToken()).transfer(account, reward); + + emit RewardClaimed(account, reward); + } + + /// @notice Updates the reward on deposit in the lending pool. + /// @param account The account that made a deposit. + function onDelegate(address account) external override { + UtilLib.onlyStaderContract(msg.sender, staderConfig, staderConfig.SD_UTILITY_POOL()); + + updateReward(account); + } + + /// @notice Calculates the current reward per token. + /// @return The calculated reward per token. + function rewardPerToken() public view returns (uint256) { + if (IERC20(staderConfig.getSDxToken()).totalSupply() == 0) { + return rewardPerTokenStored; + } + return + rewardPerTokenStored + + (((block.number - lastUpdateBlockNumber) * emissionPerBlock * 1e18) / + IERC20(staderConfig.getSDxToken()).totalSupply()); + } + + /// @notice Calculates the total accrued reward for an account. + /// @param account The account to calculate rewards for. + /// @return The total accrued reward for the account. + function earned(address account) public view returns (uint256) { + uint256 currentBalance = IERC20(staderConfig.getSDxToken()).balanceOf(account); + uint256 currentRewardPerToken = rewardPerToken(); + + return ((currentBalance * (currentRewardPerToken - userRewardPerTokenPaid[account])) / 1e18) + rewards[account]; + } + + /// @dev Internal function to update the reward state for an account. + /// @param account The account to update the reward for. + function updateReward(address account) internal { + rewardPerTokenStored = rewardPerToken(); + lastUpdateBlockNumber = block.number; + + if (account != address(0)) { + rewards[account] = earned(account); + userRewardPerTokenPaid[account] = rewardPerTokenStored; + } + } + + /// @dev Emitted when a reward is claimed. + /// @param user The user who claimed the reward. + /// @param reward The amount of reward claimed. + event RewardClaimed(address indexed user, uint256 reward); +} diff --git a/contracts/SDUtilityPool.sol b/contracts/SDUtilityPool.sol index 450e880c..9983002b 100644 --- a/contracts/SDUtilityPool.sol +++ b/contracts/SDUtilityPool.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.16; import './library/UtilLib.sol'; import './SDX.sol'; import './interfaces/IStaderConfig.sol'; +import './interfaces/ISDIncentiveController.sol'; import './interfaces/ISDUtilityPool.sol'; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; @@ -71,6 +72,7 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr */ function delegate(uint256 sdAmount) external { accrueFee(); + ISDIncentiveController(staderConfig.getSDIncentiveController()).onDelegate(msg.sender); _delegate(sdAmount); } diff --git a/contracts/StaderConfig.sol b/contracts/StaderConfig.sol index ba760c63..e93c244e 100644 --- a/contracts/StaderConfig.sol +++ b/contracts/StaderConfig.sol @@ -79,6 +79,7 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable { bytes32 public constant override SDx = keccak256('SDx'); bytes32 public constant override SD_UTILITY_POOL = keccak256('SD_UTILITY_POOL'); + bytes32 public constant override SD_INCENTIVE_CONTROLLER = keccak256('SD_INCENTIVE_CONTROLLER'); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -303,6 +304,10 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable { setContract(SD_UTILITY_POOL, _utilityPool); } + function updateSDIncentiveController(address _sdIncentiveController) external onlyRole(DEFAULT_ADMIN_ROLE) { + setContract(SD_INCENTIVE_CONTROLLER, _sdIncentiveController); + } + //Constants Getters function getStakedEthPerNode() external view override returns (uint256) { @@ -472,6 +477,10 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable { return contractsMap[SD_UTILITY_POOL]; } + function getSDIncentiveController() external view override returns (address) { + return contractsMap[SD_INCENTIVE_CONTROLLER]; + } + //Token Getters function getStaderToken() external view override returns (address) { diff --git a/contracts/interfaces/ISDIncentiveController.sol b/contracts/interfaces/ISDIncentiveController.sol new file mode 100644 index 00000000..dff8b620 --- /dev/null +++ b/contracts/interfaces/ISDIncentiveController.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.16; + +import './IStaderConfig.sol'; + +interface ISDIncentiveController { + // events + event UpdatedStaderConfig(address staderConfig); + + // functions + function claim(address account) external; + + function onDelegate(address account) external; + + function rewardPerToken() external view returns (uint256); + + function earned(address account) external view returns (uint256); +} diff --git a/contracts/interfaces/IStaderConfig.sol b/contracts/interfaces/IStaderConfig.sol index 4587cea8..fd0060f4 100644 --- a/contracts/interfaces/IStaderConfig.sol +++ b/contracts/interfaces/IStaderConfig.sol @@ -63,6 +63,8 @@ interface IStaderConfig { function SD_UTILITY_POOL() external view returns (bytes32); + function SD_INCENTIVE_CONTROLLER() external view returns (bytes32); + //POR Feed Proxy function ETH_BALANCE_POR_FEED() external view returns (bytes32); @@ -157,6 +159,8 @@ interface IStaderConfig { function getSDUtilityPool() external view returns (address); + function getSDIncentiveController() external view returns (address); + // Tokens function getStaderToken() external view returns (address);