diff --git a/contracts/SDRewardManager.sol b/contracts/SDRewardManager.sol new file mode 100644 index 00000000..3b4fe23e --- /dev/null +++ b/contracts/SDRewardManager.sol @@ -0,0 +1,93 @@ +pragma solidity 0.8.16; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { IStaderConfig } from "./interfaces/IStaderConfig.sol"; +import { INodeRegistry } from "./interfaces/INodeRegistry.sol"; +import { UtilLib } from "./library/UtilLib.sol"; + +contract SDRewardManager is Initializable { + using SafeERC20Upgradeable for IERC20Upgradeable; + + struct SDRewardEntry { + uint256 cycleNumber; + uint256 amount; // in exact SD value, not in gwei or wei + bool approved; + } + + IStaderConfig public staderConfig; + + uint256 public latestCycleNumber; + + // Mapping of cycle numbers to reward entries + mapping(uint256 => SDRewardEntry) public rewardEntries; + + // Event emitted when a new reward entry is created + event NewRewardEntry(uint256 indexed cycleNumber, uint256 amount); + + // Event emitted when a reward entry is approved + event RewardEntryApproved(uint256 indexed cycleNumber, uint256 amount); + + error AccessDenied(address account); + error EntryNotFound(uint256 cycleNumber); + error EntryAlreadyRegistered(uint256 cycleNumber); + error EntryAlreadApproved(uint256 cycleNumber); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _staderConfig) external initializer { + UtilLib.checkNonZeroAddress(_staderConfig); + staderConfig = IStaderConfig(_staderConfig); + } + + function addRewardEntry(uint256 _cycleNumber, uint256 _amount) external { + if (!staderConfig.isAllowedToCall(msg.sender, "addRewardEntry(uint256,uint256)")) { + revert AccessDenied(msg.sender); + } + + if (_cycleNumber <= latestCycleNumber) { + revert EntryAlreadyRegistered(_cycleNumber); + } + + SDRewardEntry storage rewardEntry = rewardEntries[_cycleNumber]; + rewardEntry.cycleNumber = _cycleNumber; + rewardEntry.amount = _amount; + latestCycleNumber = _cycleNumber; + + emit NewRewardEntry(_cycleNumber, _amount); + } + + function approveEntry(uint256 _cycleNumber, uint256 _amount) external { + if (!staderConfig.isAllowedToCall(msg.sender, "approveEntry(uint256,uint256)")) { + revert AccessDenied(msg.sender); + } + + SDRewardEntry storage rewardEntry = rewardEntries[_cycleNumber]; + + if (rewardEntry.cycleNumber == 0) { + revert EntryNotFound(_cycleNumber); + } + + if (rewardEntry.approved) { + revert EntryAlreadApproved(_cycleNumber); + } + + rewardEntry.approved = true; + if (rewardEntry.amount > 0) { + IERC20Upgradeable(staderConfig.getStaderToken()).safeTransferFrom( + msg.sender, + staderConfig.getPermissionlessSocializingPool(), + _amount + ); + emit RewardEntryApproved(_cycleNumber, _amount); + } + } + + function viewLatestEntry() external view returns (SDRewardEntry memory) { + return rewardEntries[latestCycleNumber]; + } +} diff --git a/contracts/StaderConfig.sol b/contracts/StaderConfig.sol index 24af1f35..d9f9b920 100644 --- a/contracts/StaderConfig.sol +++ b/contracts/StaderConfig.sol @@ -297,6 +297,27 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable { setContract(SD_INCENTIVE_CONTROLLER, _sdIncentiveController); } + // Access Control + function giveCallPermission( + address contractAddress, + string calldata functionSig, + address accountToPermit + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + bytes32 role = keccak256(abi.encodePacked(contractAddress, functionSig)); + grantRole(role, accountToPermit); + emit PermissionGranted(accountToPermit, contractAddress, functionSig); + } + + function revokeCallPermission( + address contractAddress, + string calldata functionSig, + address accountToRevoke + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + bytes32 role = keccak256(abi.encodePacked(contractAddress, functionSig)); + revokeRole(role, accountToRevoke); + emit PermissionRevoked(accountToRevoke, contractAddress, functionSig); + } + //Constants Getters function getStakedEthPerNode() external view override returns (uint256) { @@ -537,6 +558,11 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable { return hasRole(OPERATOR, account); } + function isAllowedToCall(address account, string calldata functionSig) external view returns (bool) { + bytes32 role = keccak256(abi.encodePacked(msg.sender, functionSig)); + return hasRole(role, account); + } + function verifyDepositAndWithdrawLimits() internal view { if ( !(variablesMap[MIN_DEPOSIT_AMOUNT] != 0 && diff --git a/contracts/interfaces/IStaderConfig.sol b/contracts/interfaces/IStaderConfig.sol index f8f9854a..cf49f219 100644 --- a/contracts/interfaces/IStaderConfig.sol +++ b/contracts/interfaces/IStaderConfig.sol @@ -16,6 +16,8 @@ interface IStaderConfig { event SetAccount(bytes32 key, address newAddress); event SetContract(bytes32 key, address newAddress); event SetToken(bytes32 key, address newAddress); + event PermissionGranted(address indexed accountToPermit, address indexed contractAddress, string functionSig); + event PermissionRevoked(address indexed accountToRevoke, address indexed contractAddress, string functionSig); //Contracts function POOL_UTILS() external view returns (bytes32); @@ -171,4 +173,14 @@ interface IStaderConfig { function onlyManagerRole(address account) external view returns (bool); function onlyOperatorRole(address account) external view returns (bool); + + function isAllowedToCall(address account, string calldata functionSig) external view returns (bool); + + function giveCallPermission(address contractAddress, string calldata functionSig, address accountToPermit) external; + + function revokeCallPermission( + address contractAddress, + string calldata functionSig, + address accountToRevoke + ) external; }