diff --git a/contracts/StaderConfig.sol b/contracts/StaderConfig.sol index f7ee8992..ad231a8c 100644 --- a/contracts/StaderConfig.sol +++ b/contracts/StaderConfig.sol @@ -59,6 +59,7 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable { keccak256('NODE_EL_REWARD_VAULT_IMPLEMENTATION'); bytes32 public constant override VALIDATOR_WITHDRAWAL_VAULT_IMPLEMENTATION = keccak256('VALIDATOR_WITHDRAWAL_VAULT_IMPLEMENTATION'); + bytes32 public constant override LENDING_POOL_CONTRACT = keccak256('LENDING_POOL_CONTRACT'); //POR Feed Proxy bytes32 public constant override ETH_BALANCE_POR_FEED = keccak256('ETH_BALANCE_POR_FEED'); @@ -284,6 +285,10 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable { setContract(ETHX_SUPPLY_POR_FEED, _ethXSupplyProxy); } + function updateLendingPoolProxy(address _lendingPoolProxy) external onlyRole(DEFAULT_ADMIN_ROLE) { + setContract(LENDING_POOL_CONTRACT, _lendingPoolProxy); + } + function updateStaderToken(address _staderToken) external onlyRole(DEFAULT_ADMIN_ROLE) { setToken(SD, _staderToken); } @@ -448,6 +453,10 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable { return contractsMap[VALIDATOR_WITHDRAWAL_VAULT_IMPLEMENTATION]; } + function getLendingPool() external view override returns (address) { + return contractsMap[LENDING_POOL_CONTRACT]; + } + //POR Feed Proxy Getters function getETHBalancePORFeedProxy() external view override returns (address) { return contractsMap[ETH_BALANCE_POR_FEED]; diff --git a/contracts/interfaces/IIncentiveController.sol b/contracts/interfaces/IIncentiveController.sol new file mode 100644 index 00000000..dbd690f8 --- /dev/null +++ b/contracts/interfaces/IIncentiveController.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.16; + +import './IStaderConfig.sol'; + +interface IIncentiveController { + + // events + event UpdatedStaderConfig(address staderConfig); + + // functions + function claim(address account) external; + + function onDeposit(address account) external; + + function rewardPerToken() external view returns (uint256); + + function earned(address account) external view returns (uint256); +} diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol new file mode 100644 index 00000000..3e7b6257 --- /dev/null +++ b/contracts/interfaces/ILendingPool.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.16; + +import './IStaderConfig.sol'; + +struct UserData { + uint256 totalInterestSD; + uint256 totalCollateralInSD; + uint256 ltv; + uint256 healthFactor; +} + +interface ILendingPool { + + // events + event UpdatedStaderConfig(address staderConfig); + + // functions + function deposit(uint256 amount) external returns (uint256); + + function requestWithdraw(uint256 amount) external returns (uint256); + + function claim(uint256 index) external returns (uint256); + + function borrow(uint256 amount) external returns (uint256); + + function repay(uint256 amount) external returns (uint256); + + function liquidationCall(address account) external returns (uint256); + + function claimLiquidation(uint256 index) external returns (uint256); + + function getUserData(address account) external view returns (UserData memory); +} \ No newline at end of file diff --git a/contracts/interfaces/ILendingPoolToken.sol b/contracts/interfaces/ILendingPoolToken.sol new file mode 100644 index 00000000..46ffcaf8 --- /dev/null +++ b/contracts/interfaces/ILendingPoolToken.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.16; + +interface ILendingPoolToken { + function mint(address account, uint256 amount) external; + + function burn(address account, uint256 amount) external; +} diff --git a/contracts/interfaces/IStaderConfig.sol b/contracts/interfaces/IStaderConfig.sol index 280f0cc9..425a86b7 100644 --- a/contracts/interfaces/IStaderConfig.sol +++ b/contracts/interfaces/IStaderConfig.sol @@ -58,6 +58,8 @@ interface IStaderConfig { function VALIDATOR_WITHDRAWAL_VAULT_IMPLEMENTATION() external view returns (bytes32); + function LENDING_POOL_CONTRACT() external view returns (bytes32); + //POR Feed Proxy function ETH_BALANCE_POR_FEED() external view returns (bytes32); @@ -149,6 +151,8 @@ interface IStaderConfig { function getETHBalancePORFeedProxy() external view returns (address); function getETHXSupplyPORFeedProxy() external view returns (address); + + function getLendingPool() external view returns (address); // Tokens function getStaderToken() external view returns (address); diff --git a/contracts/lendingpool/IncentiveController.sol b/contracts/lendingpool/IncentiveController.sol new file mode 100644 index 00000000..fdea65a7 --- /dev/null +++ b/contracts/lendingpool/IncentiveController.sol @@ -0,0 +1,119 @@ +// 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/IIncentiveController.sol'; + +/// @title IncentiveController +/// @notice This contract handles the distribution of reward tokens for a lending pool. +contract IncentiveController is IIncentiveController, AccessControlUpgradeable { + // The emission rate of the reward tokens per second. + uint256 public emissionPerSecond; + + // The timestamp of the last reward calculation. + uint256 public lastUpdateTimestamp; + + // The stored value of the reward per token, used to calculate rewards. + uint256 public rewardPerTokenStored; + + // Reference to the lending pool token contract. + IERC20 public lendingPoolToken; + + // Reference to the reward token contract. + IERC20 public rewardToken; + + // 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 _lendingPoolToken The address of the lending pool token contract. + /// @param _staderConfig The address of the Stader configuration contract. + /// @param _rewardToken The address of the reward token contract. + function initialize(address _lendingPoolToken, address _staderConfig, address _rewardToken) external initializer { + UtilLib.checkNonZeroAddress(_lendingPoolToken); + UtilLib.checkNonZeroAddress(_staderConfig); + UtilLib.checkNonZeroAddress(_rewardToken); + + lendingPoolToken = IERC20(_lendingPoolToken); + staderConfig = IStaderConfig(_staderConfig); + rewardToken = IERC20(_rewardToken); + + __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.LENDING_POOL_CONTRACT()); + + updateReward(account); + + uint256 reward = rewards[account]; + require(reward > 0, "No rewards to claim."); + rewards[account] = 0; + rewardToken.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 onDeposit(address account) external { + UtilLib.onlyStaderContract(msg.sender, staderConfig, staderConfig.LENDING_POOL_CONTRACT()); + + updateReward(account); + } + + /// @notice Calculates the current reward per token. + /// @return The calculated reward per token. + function rewardPerToken() public view returns (uint256) { + if (lendingPoolToken.totalSupply() == 0) { + return rewardPerTokenStored; + } + return rewardPerTokenStored + ( + (block.timestamp - lastUpdateTimestamp) * emissionPerSecond * 1e18 / lendingPoolToken.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 = lendingPoolToken.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(); + lastUpdateTimestamp = block.timestamp; + + 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/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol new file mode 100644 index 00000000..78dc2c8b --- /dev/null +++ b/contracts/lendingpool/LendingPool.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.16; + +import '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol'; + +import '../interfaces/ILendingPool.sol'; +import '../interfaces/IIncentiveController.sol'; +import '../interfaces/ILendingPoolToken.sol'; + +contract LendingPool is ILendingPool, AccessControlUpgradeable { + IIncentiveController public incentiveController; + ILendingPoolToken public lendingPoolToken; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function deposit(uint256 amount) external override returns (uint256) { + incentiveController.onDeposit(msg.sender); + lendingPoolToken.mint(msg.sender, amount); + return 0; + } + + function requestWithdraw(uint256 amount) external override returns (uint256) { + incentiveController.claim(msg.sender); + lendingPoolToken.burn(msg.sender, amount); + return 0; + } + + function claim(uint256 index) external override returns (uint256) { + incentiveController.claim(msg.sender); + return 0; + } + + function borrow(uint256 amount) external override returns (uint256) { + return 0; + } + + function repay(uint256 amount) external override returns (uint256) { + return 0; + } + + function liquidationCall(address account) external override returns (uint256) { + return 0; + } + + function claimLiquidation(uint256 index) external override returns (uint256) { + return 0; + } + + function getUserData(address account) external override view returns (UserData memory) { + return UserData(0, 0, 0, 0); + } +} \ No newline at end of file diff --git a/contracts/lendingpool/SDX.sol b/contracts/lendingpool/SDX.sol new file mode 100644 index 00000000..5dd2cab5 --- /dev/null +++ b/contracts/lendingpool/SDX.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.16; + +import '@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol'; + +import '../interfaces/IStaderConfig.sol'; +import '../library/UtilLib.sol'; + +contract SDX is Initializable, AccessControlUpgradeable, ERC20Upgradeable, PausableUpgradeable{ + event UpdatedStaderConfig(address indexed _staderConfig); + + IStaderConfig public staderConfig; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _admin, address _staderConfig) external initializer { + UtilLib.checkNonZeroAddress(_admin); + UtilLib.checkNonZeroAddress(_staderConfig); + + __ERC20_init('Interest bearing SD token', 'SDx'); + __Pausable_init(); + __AccessControl_init(); + + staderConfig = IStaderConfig(_staderConfig); + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + emit UpdatedStaderConfig(_staderConfig); + } + + /** + * @notice Mints SDx when called by an authorized caller + * @param to the account to mint to + * @param amount the amount of SDx to mint + */ + function mint(address to, uint256 amount) external whenNotPaused { + UtilLib.onlyStaderContract(msg.sender, staderConfig, staderConfig.LENDING_POOL_CONTRACT()); + + _mint(to, amount); + } + + /** + * @notice Burns SDx when called by an authorized caller + * @param account the account to burn from + * @param amount the amount of SDx to burn + */ + function burnFrom(address account, uint256 amount) external whenNotPaused { + UtilLib.onlyStaderContract(msg.sender, staderConfig, staderConfig.LENDING_POOL_CONTRACT()); + + _burn(account, amount); + } + + /** + * @dev Triggers stopped state. + * Contract must not be paused. + */ + function pause() external { + UtilLib.onlyManagerRole(msg.sender, staderConfig); + + _pause(); + } + + /** + * @dev Returns to normal state. + * Contract must be paused + */ + function unpause() external { + UtilLib.onlyManagerRole(msg.sender, staderConfig); + + _unpause(); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal override whenNotPaused { + super._beforeTokenTransfer(from, to, amount); + } + + function updateStaderConfig(address _staderConfig) external onlyRole(DEFAULT_ADMIN_ROLE) { + UtilLib.checkNonZeroAddress(_staderConfig); + staderConfig = IStaderConfig(_staderConfig); + emit UpdatedStaderConfig(_staderConfig); + } +}