// SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketVaultInterface} from "../../interface/RocketVaultInterface.sol"; import {RocketDAONodeTrustedInterface} from "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import {RocketDAOProtocolSettingsNetworkInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; import {RocketDAOProtocolSettingsRewardsInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsRewardsInterface.sol"; import {RocketRewardsPoolInterface} from "../../interface/rewards/RocketRewardsPoolInterface.sol"; import {RocketRewardsRelayInterface} from "../../interface/rewards/RocketRewardsRelayInterface.sol"; import {RocketSmoothingPoolInterface} from "../../interface/rewards/RocketSmoothingPoolInterface.sol"; import {RocketTokenRPLInterface} from "../../interface/token/RocketTokenRPLInterface.sol"; import {IERC20} from "../../interface/util/IERC20.sol"; import {RewardSubmission} from "../../types/RewardSubmission.sol"; import {RocketBase} from "../RocketBase.sol"; import {RocketVaultWithdrawerInterface} from "../../interface/RocketVaultWithdrawerInterface.sol"; /// @notice Holds RPL and ETH generated by the network for distribution each reward cycle contract RocketRewardsPool is RocketBase, RocketRewardsPoolInterface, RocketVaultWithdrawerInterface { // Constants uint256 constant internal treeVersion = 1; // Events event RewardSnapshotSubmitted(address indexed from, uint256 indexed rewardIndex, RewardSubmission submission, uint256 time); event RewardSnapshot(uint256 indexed rewardIndex, RewardSubmission submission, uint256 intervalStartTime, uint256 intervalEndTime, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 5; } /// @dev Needs to freely accept ETH withdrawn from the smoothing pool receive() payable external {} /// @dev Callback required to receive ETH withdrawal from the vault function receiveVaultWithdrawalETH() override external payable onlyLatestContract("rocketRewardsPool", address(this)) onlyLatestContract("rocketVault", msg.sender) {} /// @notice Accepts incoming ETH from megapool distributions for voter share into vault function depositVoterShare() override payable external { // Transfer incoming ETH directly to the vault RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketVault.depositEther{value: msg.value}(); } /// @notice Returns the amount of ETH rewards waiting to be distributed function getEthBalance() override external view returns (uint256) { RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); return rocketVault.balanceOf("rocketRewardsPool"); } /// @notice Get the reward index function getRewardIndex() override public view returns (uint256) { return getUint(keccak256("rewards.snapshot.index")); } /// @notice Increment the reward index function _incrementRewardIndex() internal { addUint(keccak256("rewards.snapshot.index"), 1); } /// @notice Get how much RPL the Rewards Pool contract currently has assigned to it as a whole /// @return uint256 Returns rpl balance of rocket rewards contract function getRPLBalance() override public view returns (uint256) { // Get the vault contract instance RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); // Check contract RPL balance return rocketVault.balanceOfToken("rocketRewardsPool", IERC20(getContractAddress("rocketTokenRPL"))); } /// @notice Returns the total amount of RPL that needs to be distributed to claimers at the current block function getPendingRPLRewards() override public view returns (uint256) { RocketTokenRPLInterface rplContract = RocketTokenRPLInterface(getContractAddress("rocketTokenRPL")); uint256 pendingInflation = rplContract.inflationCalculate(); // Any inflation that has accrued so far plus any amount that would be minted if we called it now return getRPLBalance() + pendingInflation; } /// @notice Returns the total amount of ETH in the smoothing pool ready to be distributed function getPendingETHRewards() override public view returns (uint256) { address rocketSmoothingPoolAddress = getContractAddress("rocketSmoothingPool"); return rocketSmoothingPoolAddress.balance; } /// @notice Returns the amount of pending voter share ETH ready to be distributed function getPendingVoterShare() override public view returns (uint256) { RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); return rocketVault.balanceOf("rocketRewardsPool"); } /// @notice Get the last set interval start time /// @return uint256 Last set start timestamp for a claim interval function getClaimIntervalTimeStart() override public view returns (uint256) { return getUint(keccak256("rewards.pool.claim.interval.time.start")); } /// @notice Get how many seconds in a claim interval /// @return uint256 Number of seconds in a claim interval function getClaimIntervalTime() override public view returns (uint256) { // Get from the DAO settings RocketDAOProtocolSettingsRewardsInterface daoSettingsRewards = RocketDAOProtocolSettingsRewardsInterface(getContractAddress("rocketDAOProtocolSettingsRewards")); return daoSettingsRewards.getRewardsClaimIntervalTime(); } /// @notice Compute intervals since last claim period /// @return uint256 Time intervals since last update function getClaimIntervalsPassed() override public view returns (uint256) { return (block.timestamp - getClaimIntervalTimeStart()) / getClaimIntervalTime(); } /// @notice Returns the block number that the given claim interval was executed at /// @param _interval The interval for which to grab the execution block of function getClaimIntervalExecutionBlock(uint256 _interval) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("rewards.pool.interval.execution.block", _interval))); } /// @notice Returns the address of the contract which was used to execute this reward interval /// @param _interval The interval for which to grab the address of function getClaimIntervalExecutionAddress(uint256 _interval) override external view returns (address) { return getAddress(keccak256(abi.encodePacked("rewards.pool.interval.execution.address", _interval))); } /// @notice Get the percentage this contract can claim in this interval /// @return uint256 Rewards percentage this contract can claim in this interval function getClaimingContractPerc(string memory _claimingContract) override public view returns (uint256) { // Load contract RocketDAOProtocolSettingsRewardsInterface daoSettingsRewards = RocketDAOProtocolSettingsRewardsInterface(getContractAddress("rocketDAOProtocolSettingsRewards")); // Get the % amount allocated to this claim contract return daoSettingsRewards.getRewardsClaimerPerc(_claimingContract); } /// @notice Get an array of percentages that the given contracts can claim in this interval /// @return uint256[] Array of percentages in the order of the supplied contract names function getClaimingContractsPerc(string[] memory _claimingContracts) override external view returns (uint256[] memory) { // Load contract RocketDAOProtocolSettingsRewardsInterface daoSettingsRewards = RocketDAOProtocolSettingsRewardsInterface(getContractAddress("rocketDAOProtocolSettingsRewards")); // Get the % amount allocated to this claim contract uint256[] memory percentages = new uint256[](_claimingContracts.length); for (uint256 i = 0; i < _claimingContracts.length; ++i) { percentages[i] = daoSettingsRewards.getRewardsClaimerPerc(_claimingContracts[i]); } return percentages; } /// @notice Returns whether a trusted node has submitted for a given reward index function getTrustedNodeSubmitted(address _trustedNodeAddress, uint256 _rewardIndex) override external view returns (bool) { return getBool(keccak256(abi.encode("rewards.snapshot.submitted.node", _trustedNodeAddress, _rewardIndex))); } /// @notice Returns whether a trusted node has submitted a specific RewardSubmission function getSubmissionFromNodeExists(address _trustedNodeAddress, RewardSubmission calldata _submission) override external view returns (bool) { return getBool(keccak256(abi.encode("rewards.snapshot.submitted.node.key", _trustedNodeAddress, _submission))); } /// @notice Returns the number of trusted nodes who have agreed to the given submission function getSubmissionCount(RewardSubmission calldata _submission) override external view returns (uint256) { return getUint(keccak256(abi.encode("rewards.snapshot.submitted.count", _submission))); } /// @notice Submit a reward snapshot. Only accepts calls from trusted (oracle) nodes function submitRewardSnapshot(RewardSubmission calldata _submission) override external onlyLatestContract("rocketRewardsPool", address(this)) onlyTrustedNode(msg.sender) { // Get contracts RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); // Check submission is currently enabled require(rocketDAOProtocolSettingsNetwork.getSubmitRewardsEnabled(), "Submitting rewards is currently disabled"); // Validate inputs uint256 rewardIndex = getRewardIndex(); require(_submission.rewardIndex <= rewardIndex, "Can only submit snapshot for periods up to next"); require(_submission.intervalsPassed > 0, "Invalid number of intervals passed"); require(_submission.nodeRPL.length == _submission.trustedNodeRPL.length && _submission.trustedNodeRPL.length == _submission.nodeETH.length, "Invalid array length"); // Calculate RPL reward total and validate { // Scope to prevent stack too deep uint256 totalRewardsRPL = _submission.treasuryRPL; for (uint256 i = 0; i < _submission.nodeRPL.length; ++i) { totalRewardsRPL = totalRewardsRPL + _submission.nodeRPL[i]; } for (uint256 i = 0; i < _submission.trustedNodeRPL.length; ++i) { totalRewardsRPL = totalRewardsRPL + _submission.trustedNodeRPL[i]; } require(totalRewardsRPL <= getPendingRPLRewards(), "Invalid RPL rewards"); } // Calculate ETH reward total and validate { // Scope to prevent stack too deep uint256 totalRewardsETH = _submission.treasuryETH + _submission.userETH; for (uint256 i = 0; i < _submission.nodeETH.length; ++i) { totalRewardsETH = totalRewardsETH + _submission.nodeETH[i]; } uint256 smoothingPoolBalance = getPendingETHRewards(); require(totalRewardsETH <= smoothingPoolBalance + getPendingVoterShare(), "Invalid ETH rewards"); require(_submission.smoothingPoolETH <= smoothingPoolBalance, "Invalid smoothing pool balance"); } // Store and increment vote uint256 submissionCount; { // Scope to prevent stack too deep // Check & update node submission status bytes32 nodeSubmissionKey = keccak256(abi.encode("rewards.snapshot.submitted.node.key", msg.sender, _submission)); require(!getBool(nodeSubmissionKey), "Duplicate submission from node"); setBool(nodeSubmissionKey, true); setBool(keccak256(abi.encode("rewards.snapshot.submitted.node", msg.sender, _submission.rewardIndex)), true); } { // Scope to prevent stack too deep // Increment submission count bytes32 submissionCountKey = keccak256(abi.encode("rewards.snapshot.submitted.count", _submission)); submissionCount = getUint(submissionCountKey) + 1; setUint(submissionCountKey, submissionCount); } // Emit snapshot submitted event emit RewardSnapshotSubmitted(msg.sender, _submission.rewardIndex, _submission, block.timestamp); // Return if already executed if (_submission.rewardIndex != rewardIndex) { return; } // If consensus is reached, execute the snapshot RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); if (calcBase * submissionCount / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { _executeRewardSnapshot(_submission); } } /// @notice Executes reward snapshot if consensus threshold is reached function executeRewardSnapshot(RewardSubmission calldata _submission) override external onlyLatestContract("rocketRewardsPool", address(this)) { // Validate reward index of submission require(_submission.rewardIndex == getRewardIndex(), "Can only execute snapshot for next period"); // Get submission count bytes32 submissionCountKey = keccak256(abi.encode("rewards.snapshot.submitted.count", _submission)); uint256 submissionCount = getUint(submissionCountKey); // Confirm consensus and execute RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); require(calcBase * submissionCount / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold(), "Consensus has not been reached"); _executeRewardSnapshot(_submission); } /// @notice Executes reward snapshot and sends assets to the relays for distribution to reward recipients function _executeRewardSnapshot(RewardSubmission calldata _submission) internal { // Get contract RocketTokenRPLInterface rplContract = RocketTokenRPLInterface(getContractAddress("rocketTokenRPL")); RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); // Execute inflation if required rplContract.inflationMintTokens(); // Increment the reward index and update the claim interval timestamp _incrementRewardIndex(); uint256 claimIntervalTimeStart = getClaimIntervalTimeStart(); uint256 claimIntervalTimeEnd = claimIntervalTimeStart + (getClaimIntervalTime() * _submission.intervalsPassed); // Emit reward snapshot event emit RewardSnapshot(_submission.rewardIndex, _submission, claimIntervalTimeStart, claimIntervalTimeEnd, block.timestamp); setUint(keccak256(abi.encodePacked("rewards.pool.interval.execution.block", _submission.rewardIndex)), block.number); setAddress(keccak256(abi.encodePacked("rewards.pool.interval.execution.address", _submission.rewardIndex)), address(this)); setUint(keccak256("rewards.pool.claim.interval.time.start"), claimIntervalTimeEnd); // Send out the treasury rewards if (_submission.treasuryRPL > 0) { rocketVault.transferToken("rocketClaimDAO", rplContract, _submission.treasuryRPL); } // Get the smoothing pool instance RocketSmoothingPoolInterface rocketSmoothingPool = RocketSmoothingPoolInterface(getContractAddress("rocketSmoothingPool")); // Withdraw ETH from the smoothing pool required for this interval if (_submission.smoothingPoolETH > 0) { rocketSmoothingPool.withdrawEther(address(this), _submission.smoothingPoolETH); } // Calculate total amount of ETH required for this reward interval uint256 totalETH = _submission.userETH + _submission.treasuryETH; for (uint i = 0; i < _submission.nodeETH.length; ++i) { totalETH += _submission.nodeETH[i]; } // Withdraw remaining ETH required from the vault uint256 vaultBalance = totalETH - _submission.smoothingPoolETH; if (vaultBalance > 0) { rocketVault.withdrawEther(vaultBalance); } // Send user share to rETH contract if (_submission.userETH > 0) { address rocketTokenRETHAddress = getContractAddress("rocketTokenRETH"); (bool result,) = rocketTokenRETHAddress.call{value: _submission.userETH}(""); require(result, "Failed to send user rewards"); } // Send pDAO share to treasury if (_submission.treasuryETH > 0) { address payable rocketClaimDAO = payable(getContractAddress("rocketClaimDAO")); (bool result,) = rocketClaimDAO.call{value: _submission.treasuryETH}(""); require(result, "Failed to send pDAO rewards"); } // Loop over each network and distribute rewards for (uint i = 0; i < _submission.nodeRPL.length; ++i) { // Quick out if no rewards for this network uint256 rewardsRPL = _submission.nodeRPL[i] + _submission.trustedNodeRPL[i]; uint256 rewardsETH = _submission.nodeETH[i]; if (rewardsRPL == 0 && rewardsETH == 0) { continue; } // Grab the relay address RocketRewardsRelayInterface relay; { // Scope to prevent stack too deep address networkRelayAddress; bytes32 networkRelayKey = keccak256(abi.encodePacked("rewards.relay.address", i)); networkRelayAddress = getAddress(networkRelayKey); // Validate network is valid require(networkRelayAddress != address(0), "Snapshot contains rewards for invalid network"); relay = RocketRewardsRelayInterface(networkRelayAddress); } // Transfer rewards if (rewardsRPL > 0) { // RPL rewards are withdrawn from the vault directly to the relay rocketVault.withdrawToken(address(relay), rplContract, rewardsRPL); } if (rewardsETH > 0) { // Send ETH rewards to the relay (bool result,) = address(relay).call{value: rewardsETH}(""); require(result, "Failed to send ETH rewards to relay"); } // Call into relay contract to handle distribution of rewards relay.relayRewards(_submission.rewardIndex, treeVersion, _submission.merkleRoot, rewardsRPL, rewardsETH); } } }