diff --git a/src/contracts/core/RewardsCoordinator.sol b/src/contracts/core/RewardsCoordinator.sol index 2ebddfce36..57ff9db1ad 100644 --- a/src/contracts/core/RewardsCoordinator.sol +++ b/src/contracts/core/RewardsCoordinator.sol @@ -154,6 +154,25 @@ contract RewardsCoordinator is } } + function distributeIncentives( + uint256 incentivesVersion, + RewardsSubmission calldata incentivesSubmission, + bytes calldata additionalData + ) external onlyWhenNotPaused(PAUSED_INCENTIVES_DISTRIBUTION) onlyRewardsForAllSubmitter nonReentrant { + uint256 nonce = submissionNonce[msg.sender]; + bytes32 incentivesSubmissionHash = keccak256(abi.encode(msg.sender, nonce, incentivesSubmission)); + + _validateRewardsSubmission(incentivesSubmission); + + isRewardsSubmissionForAllEarnersHash[msg.sender][incentivesSubmissionHash] = true; + submissionNonce[msg.sender] = nonce + 1; + + emit IncentivesDistributed( + msg.sender, nonce, incentivesSubmissionHash, incentivesSubmission, incentivesVersion, additionalData + ); + incentivesSubmission.token.safeTransferFrom(msg.sender, address(this), incentivesSubmission.amount); + } + /// @inheritdoc IRewardsCoordinator function createOperatorDirectedAVSRewardsSubmission( address avs, diff --git a/src/contracts/core/RewardsCoordinatorStorage.sol b/src/contracts/core/RewardsCoordinatorStorage.sol index 25a2fa6848..a231928da7 100644 --- a/src/contracts/core/RewardsCoordinatorStorage.sol +++ b/src/contracts/core/RewardsCoordinatorStorage.sol @@ -35,6 +35,8 @@ abstract contract RewardsCoordinatorStorage is IRewardsCoordinator { uint8 internal constant PAUSED_OPERATOR_SET_SPLIT = 8; /// @dev Index for flag that pauses calling setOperatorSetPerformanceRewardsSubmission uint8 internal constant PAUSED_OPERATOR_DIRECTED_OPERATOR_SET_REWARDS_SUBMISSION = 9; + /// @dev Index for flag that pauses calling distributeIncentives + uint8 internal constant PAUSED_INCENTIVES_DISTRIBUTION = 10; /// @dev Salt for the earner leaf, meant to distinguish from tokenLeaf since they have the same sized data uint8 internal constant EARNER_LEAF_SALT = 0; @@ -133,6 +135,9 @@ abstract contract RewardsCoordinatorStorage is IRewardsCoordinator { mapping(address avs => mapping(bytes32 hash => bool valid)) public isOperatorDirectedOperatorSetRewardsSubmissionHash; + /// @notice Returns whether a `hash` is a `valid` rewards submission for all earners hash for a given `avs`. + mapping(address avs => mapping(bytes32 hash => bool valid)) public isIncentivesSubmissionHash; + // Construction constructor( @@ -164,5 +169,5 @@ abstract contract RewardsCoordinatorStorage is IRewardsCoordinator { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[35] private __gap; + uint256[34] private __gap; } diff --git a/src/contracts/incentives/ProgrammaticIncentivesConfig.sol b/src/contracts/incentives/ProgrammaticIncentivesConfig.sol new file mode 100644 index 0000000000..cc93fec391 --- /dev/null +++ b/src/contracts/incentives/ProgrammaticIncentivesConfig.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "./Vectors.sol"; +import "./Streams.sol"; + +interface IProgrammaticIncentivesConfig { + // events for rewards-boost whitelist + event AVSAddedToWhitelist(address indexed avs); + event AVSRemovedFromWhitelist(address indexed avs); + event TokenAddedToWhitelist(address indexed token); + event TokenRemovedFromWhitelist(address indexed token); + + error InputLengthMismatch(); + + // @notice Returns the weighting vector at `vectorIndex` (currently used to compare Strategy shares) + function vector(uint256 vectorIndex) external view returns (VectorEntry[] memory); + + /** + * @notice Getter function for querying whether a `key` is in the `vectorIndex`-th vector + * @dev Will not revert even in the event that the `vectorIndex`-th vector does not exist + */ + function isInVector(uint256 vectorIndex, address key) external view returns (bool); + + // @notice Returns the total number of existing vectors + function numberOfWeightingVectors() external view returns (uint256); + + /** + * @notice Getter function for fetching the index of a `key` within the `vectorIndex`-th vector + * @dev Reverts if the key is not in the vector + */ + function keyIndex(uint256 vectorIndex, address key) external view returns (uint256); + + // TODO: determine if this function should be called by the recipient itself or can be called on behalf of them + // @notice Claims all pending inflationary tokens for the `substreamRecipient` + function claimForSubstream(address substreamRecipient) external; + + // contract owner-only functions + // @notice mapping avs => whether or not the avs is considered for incentives types that are filtered by the AVS whitelist + function avsIsWhitelisted(address avs) external view returns (bool); + + // @notice Called by the contract owner modify the whitelist status of an AVS + function editAVSWhitelistStatus(address avs, bool newWhitelistStatus) external; + + // @notice mapping token => whether or not the token is considered for incentives types that are filtered by the token whitelist + function tokenIsWhitelisted(address avs) external view returns (bool); + + // TODO: there may be additional inputs required, especially if e.g. this function is supposed to deploy a token oracle contract + // @notice Called by the contract owner modify the whitelist status of a token + function editTokenWhitelistStatus(address avs, bool newWhitelistStatus) external; + + // @notice Called by the contract owner to change the relative amount of inflation going to the `substreamRecipient` + function updateSubstreamWeight(address substreamRecipient, uint256 newWeight) external; + + // @notice Called by the contract owner to create a new vector indicating how different tokens are weighted relative to one another + function createNewVector(VectorEntry[] memory initialVectorEntries) external; + + // @notice Called by the contract owner to add additional entries to an existing vector + function addEntries(uint256 vectorIndex, VectorEntry[] memory newEntries) external; + + // @notice Called by the contract owner to remove entries from an existing vector + function removeKeys(uint256 vectorIndex, address[] memory keysToRemove, uint256[] memory keyIndices) external; + + +} + +/** + * @notice Central contract for managing the configuration of many Programmatic Incentives parameters. + * Uses LinearVectors and the associated LinearVectorOps library to manage lists of strategies & weights used by Distributor contracts. + */ +contract ProgrammaticIncentivesConfig is Initializable, OwnableUpgradeable, IProgrammaticIncentivesConfig { + using LinearVectorOps for *; + using StreamMath for *; + + // 4% of initial supply of EIGEN + uint256 constant internal CURRENT_YEARLY_INFLATION = 66945866731386400000000160; + + // @inheritdoc IProgrammaticIncentivesConfig + mapping(address => bool) public avsIsWhitelisted; + + // @inheritdoc IProgrammaticIncentivesConfig + mapping(address => bool) public tokenIsWhitelisted; + + LinearVector[] internal _weightingVectors; + + // @notice single inflationary stream of EIGEN tokens + // TODO: figure out what automatic getter(s) for this look like! + NonNormalizedStream public stream; + + address public bEIGEN; + + constructor() { + _disableInitializers(); + } + + function initialize(address initialOwner, address _bEIGEN) external initializer { + _transferOwnership(initialOwner); + bEIGEN = _bEIGEN; + stream.rate = CURRENT_YEARLY_INFLATION * TIMESCALE / 365 days; + stream.lastUpdatedTimestamp = block.timestamp; + // TODO: initiate substreams? + } + + // @inheritdoc IProgrammaticIncentivesConfig + function editAVSWhitelistStatus(address avs, bool newWhitelistStatus) external onlyOwner { + if (avsIsWhitelisted[avs] && !newWhitelistStatus) { + avsIsWhitelisted[avs] = false; + emit AVSRemovedFromWhitelist(avs); + } else if (!avsIsWhitelisted[avs] && newWhitelistStatus) { + avsIsWhitelisted[avs] = true; + emit AVSAddedToWhitelist(avs); + } + } + + // @inheritdoc IProgrammaticIncentivesConfig + function editTokenWhitelistStatus(address token, bool newWhitelistStatus) external onlyOwner { + if (tokenIsWhitelisted[token] && !newWhitelistStatus) { + tokenIsWhitelisted[token] = false; + emit TokenRemovedFromWhitelist(token); + } else if (!tokenIsWhitelisted[token] && newWhitelistStatus) { + tokenIsWhitelisted[token] = true; + emit TokenAddedToWhitelist(token); + } + } + + // @inheritdoc IProgrammaticIncentivesConfig + function claimForSubstream(address substreamRecipient) external { + // TODO: access control? could just use msg.sender instead of having `substreamRecipient` input + stream.claimForSubstream(substreamRecipient, bEIGEN); + } + + // @inheritdoc IProgrammaticIncentivesConfig + function updateSubstreamWeight(address substreamRecipient, uint256 newWeight) external onlyOwner { + stream.updateSubstreamWeight(substreamRecipient, newWeight, bEIGEN); + } + + // @inheritdoc IProgrammaticIncentivesConfig + function createNewVector(VectorEntry[] memory initialVectorEntries) external onlyOwner { + _weightingVectors.push(); + _addEntries({ + vectorIndex: _weightingVectors.length - 1, + newEntries: initialVectorEntries + }); + } + + // @inheritdoc IProgrammaticIncentivesConfig + function numberOfWeightingVectors() external view returns (uint256) { + return _weightingVectors.length; + } + + // @inheritdoc IProgrammaticIncentivesConfig + function vector(uint256 vectorIndex) external view returns (VectorEntry[] memory) { + return _weightingVectors[vectorIndex].vector; + } + + // @inheritdoc IProgrammaticIncentivesConfig + function isInVector(uint256 vectorIndex, address key) external view returns (bool) { + return _weightingVectors[vectorIndex].isInVector[key]; + } + + // @inheritdoc IProgrammaticIncentivesConfig + function keyIndex(uint256 vectorIndex, address key) external view returns (uint256) { + return _weightingVectors[vectorIndex].findKeyIndex(key); + } + + function _addEntries(uint256 vectorIndex, VectorEntry[] memory newEntries) internal { + for (uint256 i = 0; i < newEntries.length; ++i) { + _weightingVectors[vectorIndex].addEntry(newEntries[i]); + } + } + + function _removeKeys(uint256 vectorIndex, address[] memory keysToRemove, uint256[] memory keyIndices) internal { + require(keysToRemove.length == keyIndices.length, InputLengthMismatch()); + for (uint256 i = 0; i < keysToRemove.length; ++i) { + _weightingVectors[vectorIndex].removeKey(keysToRemove[i], keyIndices[i]); + } + } + + // @inheritdoc IProgrammaticIncentivesConfig + function addEntries(uint256 vectorIndex, VectorEntry[] memory newEntries) external onlyOwner { + _addEntries(vectorIndex, newEntries); + } + + // @inheritdoc IProgrammaticIncentivesConfig + function removeKeys(uint256 vectorIndex, address[] memory keysToRemove, uint256[] memory keyIndices) external onlyOwner { + _removeKeys(vectorIndex, keysToRemove, keyIndices); + } +} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/contracts/incentives/RewardAllStakersDistributor.sol b/src/contracts/incentives/RewardAllStakersDistributor.sol new file mode 100644 index 0000000000..b98e874b96 --- /dev/null +++ b/src/contracts/incentives/RewardAllStakersDistributor.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../interfaces/IEigen.sol"; +import "./ProgrammaticIncentivesConfig.sol"; + +/** + * @notice Version of the IRewardsCoordinator interface with struct names/definitions + * modified to work smoothly with the LinearVector library. + */ +interface IRewardsCoordinator_VectorModification { + struct RewardsSubmission { + VectorEntry[] strategiesAndMultipliers; + IERC20 token; + uint256 amount; + uint32 startTimestamp; + uint32 duration; + } + + function createRewardsForAllEarners( + RewardsSubmission[] calldata rewardsSubmissions + ) external; +} + +interface IIncentivesDistributor { + function distributeIncentives() external; + event IncentivesDistributed(uint256 amountEIGEN); +} + +/** + * @notice Programmatically distributes EIGEN incentives via minting new EIGEN tokens and calling + * the RewardsCoordinator.createRewardsForAllEarners(...) function + * @dev Reads from a single, fixes streamID and vectorIndex of the ProgrammaticIncentivesConfig and + * mints tokens via the TokenInflationNexus contract + */ +contract RewardAllStakersDistributor is IIncentivesDistributor { + using SafeERC20 for IERC20; + + IProgrammaticIncentivesConfig public immutable programmaticIncentivesConfig; + address public immutable rewardsCoordinator; + IERC20 public immutable bEIGEN; + IERC20 public immutable EIGEN; + uint256 public immutable vectorIndex; + + /** + * @notice The last time that `distributeIncentives` was called successfully. + * @dev The initial value of this variable is set via constructor input. + */ + uint256 public lastDistributionTimestamp; + + /** + * @dev Note that lastDistributionTimestamp can be initialized to either a past or future timestamp, depending on the desired behavior + * (an initial distribution reaching into the past or an initial distribution starting in the future, respectively). + * It is initialized to the current time if the `init_lastDistributionTimestamp` input is set to zero. Due to timestamp treatment + * in the `distributeIncentives` function, what ultimately matters for initialization is `init_lastDistributionTimestamp / TIMESCALE`. + */ + constructor( + IProgrammaticIncentivesConfig _programmaticIncentivesConfig, + address _rewardsCoordinator, + IERC20 _bEIGEN, + IERC20 _EIGEN, + uint256 _vectorIndex, + uint256 init_lastDistributionTimestamp + ) { + programmaticIncentivesConfig = _programmaticIncentivesConfig; + rewardsCoordinator = _rewardsCoordinator; + bEIGEN = _bEIGEN; + EIGEN = _EIGEN; + vectorIndex = _vectorIndex; + if (init_lastDistributionTimestamp == 0) { + lastDistributionTimestamp = block.timestamp; + } else { + lastDistributionTimestamp = init_lastDistributionTimestamp; + } + } + + function distributeIncentives() external { + // 0) mint new tokens + programmaticIncentivesConfig.claimForSubstream({substreamRecipient: address(this)}); + + // 1) check how many tokens were minted (also accounts for transfers in) + uint256 tokenAmount = bEIGEN.balanceOf(address(this)); + + if (tokenAmount > 0) { + uint256 timescalesElapsed = (block.timestamp / TIMESCALE) - (lastDistributionTimestamp / TIMESCALE); + uint32 duration = uint32(timescalesElapsed * TIMESCALE); + lastDistributionTimestamp = block.timestamp; + // round up to timescale (i.e. start of *next* 'TIMESCALE' interval, in UTC time) + // start earlier in time if multiple TIMESCALE borders have been crossed since lastDistributionTimestamp + uint32 startTimestamp = uint32(((block.timestamp / TIMESCALE) + 2 - timescalesElapsed) * TIMESCALE); + + // 2) approve the bEIGEN token for transfer so it can be wrapped + bEIGEN.safeApprove(address(EIGEN), tokenAmount); + + // 3) wrap the bEIGEN token to receive EIGEN + IEigen(address(EIGEN)).wrap(tokenAmount); + + // 4) Set the proper allowance on the coordinator + EIGEN.safeApprove(address(rewardsCoordinator), tokenAmount); + + // 5) Call the reward coordinator's ForAll API + IRewardsCoordinator_VectorModification.RewardsSubmission[] memory rewardsSubmission = + new IRewardsCoordinator_VectorModification.RewardsSubmission[](1); + rewardsSubmission[0] = IRewardsCoordinator_VectorModification.RewardsSubmission({ + strategiesAndMultipliers: programmaticIncentivesConfig.vector(vectorIndex), + token: EIGEN, + amount: tokenAmount, + startTimestamp: startTimestamp, + duration: uint32(duration) + }); + IRewardsCoordinator_VectorModification(rewardsCoordinator).createRewardsForAllEarners(rewardsSubmission); + + emit IncentivesDistributed(tokenAmount); + } + } +} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/contracts/incentives/Streams.sol b/src/contracts/incentives/Streams.sol new file mode 100644 index 0000000000..5af8d4ae3d --- /dev/null +++ b/src/contracts/incentives/Streams.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +interface IMintableToken { + function mint(address recipient, uint256 amount) external; +} + +interface IRecipient { + function funder() external view returns (address); + function onMint(address token, uint256 amount) external; +} + +uint256 constant TIMESCALE = 4 weeks; + +// @notice Determines the relative size of a substream within a token stream, and accounts for the tokens already minted by the substream +struct Substream { + uint256 weight; + uint256 rewardDebt; +} + +/** + * @notice Defines a token stream, which may contain any number of substreams, each accumulating a fraction of the `rate` + * @dev Non-normalization refers to the fact that totalWeight (the divisor for substream size) is floating, not fixed + * i.e. substreams are a fraction of the total stream size, with the fraction defined by (substreams[address] / totalWeight) + */ +struct NonNormalizedStream { + // @dev note that this is in terms of TIMESCALE + uint256 rate; + uint256 lastUpdatedTimestamp; + // TODO: enforce some max total weight? + uint256 totalWeight; + uint256 scaledCumulativeRewardDebtPerWeight; + mapping(address => Substream) substreams; +} + +uint256 constant NORMALIZED_STREAM_TOTAL_WEIGHT = 1e18; + +/** + * @notice Defines a token stream, which may contain any number of substreams, each accumulating a fraction of the `rate` + * @dev This is identified as normalized because the totalWeight is designed to be fixed at NORMALIZED_STREAM_TOTAL_WEIGHT. + * Each substream is a fraction of the total stream size, with the fraction defined by (substreams[address] / NORMALIZED_STREAM_TOTAL_WEIGHT) + */ +struct NormalizedStream { + // @dev note that this is in terms of TIMESCALE + uint256 rate; + // @dev note that this is in terms of TIMESCALE + uint256 lastUpdatedTimestamp; + uint256 unassignedWeight; + uint256 scaledCumulativeRewardDebtPerWeight; + mapping(address => Substream) substreams; +} + + +// TODO: deal with totalWeight of zero! +// TODO: events +library StreamMath { + +// functions for NonNormalizedStream + function substreamRate( + NonNormalizedStream storage stream, + address substreamRecipient + ) internal view returns (uint256) { + Substream storage substream = stream.substreams[substreamRecipient]; + + return stream.rate * substream.weight / stream.totalWeight; + } + + function pendingAmountToClaim( + NonNormalizedStream storage stream, + address substreamRecipient + ) internal view returns (uint256) { + Substream storage substream = stream.substreams[substreamRecipient]; + + // calculate pending increase to scaled cumulative reward debt per weight + uint256 _scaledCumulativeRewardDebtPerWeight = stream.scaledCumulativeRewardDebtPerWeight; + uint256 _totalWeight = stream.totalWeight; + if (_totalWeight !=0 ) { + _scaledCumulativeRewardDebtPerWeight = _scaledCumulativeRewardDebtPerWeight + + ((1e18 * ((block.timestamp - stream.lastUpdatedTimestamp) / TIMESCALE) * stream.rate) / _totalWeight); + } + uint256 cumulativeAmount = (substream.weight * _scaledCumulativeRewardDebtPerWeight) / 1e18; + uint256 _rewardDebt = substream.rewardDebt; + // TODO: consider rounding and the possibility of underflows + // TODO: will making cumulativeAmount and rewardDebt also scaled up help solve this? + if (cumulativeAmount > _rewardDebt) { + return (cumulativeAmount - _rewardDebt); + } else { + return 0; + } + } + + // @dev returns updated value of scaledCumulativeRewardDebtPerWeight + function updateStream(NonNormalizedStream storage stream) internal returns (uint256) { + // increase scaled cumulative reward debt per weight + uint256 _scaledCumulativeRewardDebtPerWeight = stream.scaledCumulativeRewardDebtPerWeight; + uint256 _totalWeight = stream.totalWeight; + if (_totalWeight !=0 ) { + // TODO: event + _scaledCumulativeRewardDebtPerWeight = _scaledCumulativeRewardDebtPerWeight + + ((1e18 * ((block.timestamp - stream.lastUpdatedTimestamp) / TIMESCALE) * stream.rate) / _totalWeight); + stream.scaledCumulativeRewardDebtPerWeight = _scaledCumulativeRewardDebtPerWeight; + stream.lastUpdatedTimestamp = block.timestamp; + } + return _scaledCumulativeRewardDebtPerWeight; + } + + function claimForSubstream( + NonNormalizedStream storage stream, + address substreamRecipient, + address streamToken + ) internal { + Substream storage substream = stream.substreams[substreamRecipient]; + + // increase scaled cumulative reward debt per weight + uint256 _scaledCumulativeRewardDebtPerWeight = updateStream(stream); + + uint256 cumulativeAmount = (substream.weight * _scaledCumulativeRewardDebtPerWeight) / 1e18; + uint256 _rewardDebt = substream.rewardDebt; + // TODO: consider rounding and the possibility of underflows + if (cumulativeAmount > _rewardDebt) { + uint256 amountToMint = cumulativeAmount - _rewardDebt; + IMintableToken(streamToken).mint(substreamRecipient, amountToMint); + substream.rewardDebt = cumulativeAmount; + // TODO: more on this interface? + IRecipient(substreamRecipient).onMint(streamToken, amountToMint); + } + } + + function updateSubstreamWeight( + NonNormalizedStream storage stream, + address substreamRecipient, + uint256 newWeight, + address streamToken + ) internal { + Substream storage substream = stream.substreams[substreamRecipient]; + + claimForSubstream(stream, substreamRecipient, streamToken); + // TODO: event + uint256 _totalWeight = stream.totalWeight - substream.weight + newWeight; + stream.totalWeight = _totalWeight; + + // TODO: event + substream.weight = newWeight; + // adjust rewardDebt to make pending rewards correct + uint256 cumulativeAmount = (newWeight * stream.scaledCumulativeRewardDebtPerWeight) / 1e18; + substream.rewardDebt = cumulativeAmount; + } + + function updateStreamRate( + NonNormalizedStream storage stream, + uint256 newRate + ) internal { + // perform an update using the old rate + updateStream(stream); + // TODO: emit event + stream.rate = newRate; + } + + +// functions for NormalizedStream + + function substreamRate( + NormalizedStream storage stream, + address substreamRecipient + ) internal view returns (uint256) { + Substream storage substream = stream.substreams[substreamRecipient]; + + return stream.rate * substream.weight / NORMALIZED_STREAM_TOTAL_WEIGHT; + } + + function pendingAmountToClaim( + NormalizedStream storage stream, + address substreamRecipient + ) internal view returns (uint256) { + Substream storage substream = stream.substreams[substreamRecipient]; + + // calculate pending increase to scaled cumulative reward debt per weight + uint256 _scaledCumulativeRewardDebtPerWeight = stream.scaledCumulativeRewardDebtPerWeight; + _scaledCumulativeRewardDebtPerWeight = _scaledCumulativeRewardDebtPerWeight + + ((1e18 * ((block.timestamp - stream.lastUpdatedTimestamp) / TIMESCALE) * stream.rate) / NORMALIZED_STREAM_TOTAL_WEIGHT); + uint256 cumulativeAmount = (substream.weight * _scaledCumulativeRewardDebtPerWeight) / 1e18; + uint256 _rewardDebt = substream.rewardDebt; + // TODO: consider rounding and the possibility of underflows + // TODO: will making cumulativeAmount and rewardDebt also scaled up help solve this? + if (cumulativeAmount > _rewardDebt) { + return (cumulativeAmount - _rewardDebt); + } else { + return 0; + } + } + + // @dev returns updated value of scaledCumulativeRewardDebtPerWeight + function updateStream(NormalizedStream storage stream) internal returns (uint256) { + // increase scaled cumulative reward debt per weight + uint256 _scaledCumulativeRewardDebtPerWeight = stream.scaledCumulativeRewardDebtPerWeight; + // TODO: event + _scaledCumulativeRewardDebtPerWeight = _scaledCumulativeRewardDebtPerWeight + + ((1e18 * ((block.timestamp - stream.lastUpdatedTimestamp) / TIMESCALE) * stream.rate) / NORMALIZED_STREAM_TOTAL_WEIGHT); + stream.scaledCumulativeRewardDebtPerWeight = _scaledCumulativeRewardDebtPerWeight; + stream.lastUpdatedTimestamp = block.timestamp; + return _scaledCumulativeRewardDebtPerWeight; + } + + + function claimForSubstream( + NormalizedStream storage stream, + address substreamRecipient, + address streamToken + ) internal { + Substream storage substream = stream.substreams[substreamRecipient]; + + // increase scaled cumulative reward debt per weight + uint256 _scaledCumulativeRewardDebtPerWeight = updateStream(stream); + + uint256 cumulativeAmount = (substream.weight * _scaledCumulativeRewardDebtPerWeight) / 1e18; + uint256 _rewardDebt = substream.rewardDebt; + // TODO: consider rounding and the possibility of underflows + if (cumulativeAmount > _rewardDebt) { + uint256 amountToMint = cumulativeAmount - _rewardDebt; + IMintableToken(streamToken).mint(substreamRecipient, amountToMint); + substream.rewardDebt = cumulativeAmount; + // TODO: more on this interface? + IRecipient(substreamRecipient).onMint(streamToken, amountToMint); + } + } + + function updateSubstreamWeight( + NormalizedStream storage stream, + address substreamRecipient, + uint256 newWeight, + address streamToken + ) internal { + Substream storage substream = stream.substreams[substreamRecipient]; + + claimForSubstream(stream, substreamRecipient, streamToken); + // TODO: event + uint256 _unassignedWeight = stream.unassignedWeight + substream.weight - newWeight; + stream.unassignedWeight = _unassignedWeight; + + // TODO: event + substream.weight = newWeight; + // adjust rewardDebt to make pending rewards correct + uint256 cumulativeAmount = (newWeight * stream.scaledCumulativeRewardDebtPerWeight) / 1e18; + substream.rewardDebt = cumulativeAmount; + } + + function updateStreamRate( + NormalizedStream storage stream, + uint256 newRate + ) internal { + // perform an update using the old rate + updateStream(stream); + // TODO: emit event + stream.rate = newRate; + } +} + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/contracts/incentives/Vectors.sol b/src/contracts/incentives/Vectors.sol new file mode 100644 index 0000000000..36448871c8 --- /dev/null +++ b/src/contracts/incentives/Vectors.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +// @notice Packed struct, takes only one slot in storage +struct VectorEntry { + address key; + uint96 value; +} + +// @notice A list of vector entries paired with a mapping to track which keys the list contains +struct LinearVector { + VectorEntry[] vector; + mapping(address => bool) isInVector; +} + +// @notice Library for managing entries in a LinearVector. Enforces the behavior of no duplicate keys in the vector. +library LinearVectorOps { + + error CannotAddDuplicateKey(); + error KeyNotInVector(); + error IncorrectIndexInput(); + + // TODO: figure out if there is a way to identify the relevant vectorStorage in this event, in the case of multiple vectorStorage structs in one contract + event EntryAddedToVector(address indexed newKey, uint96 value); + event KeyRemovedFromVector(address indexed keyRemoved); + + function addEntry(LinearVector storage vectorStorage, VectorEntry memory newEntry) internal { + require(!vectorStorage.isInVector[newEntry.key], CannotAddDuplicateKey()); + emit EntryAddedToVector(newEntry.key, newEntry.value); + vectorStorage.isInVector[newEntry.key] = true; + vectorStorage.vector.push(newEntry); + } + + function removeKey(LinearVector storage vectorStorage, address keyToRemove, uint256 indexOfKey) internal { + require(vectorStorage.isInVector[keyToRemove], KeyNotInVector()); + require(vectorStorage.vector[indexOfKey].key == keyToRemove, IncorrectIndexInput()); + // swap and pop + emit KeyRemovedFromVector(keyToRemove); + vectorStorage.isInVector[keyToRemove] = false; + vectorStorage.vector[indexOfKey] = vectorStorage.vector[vectorStorage.vector.length - 1]; + vectorStorage.vector.pop(); + } + + function findKeyIndex(LinearVector storage vectorStorage, address key) internal view returns (uint256) { + require(vectorStorage.isInVector[key], KeyNotInVector()); + uint256 length = vectorStorage.vector.length; + uint256 index = 0; + for (; index < length; ++index) { + if (key == vectorStorage.vector[index].key) { + break; + } + } + return index; + } +} + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/contracts/interfaces/IRewardsCoordinator.sol b/src/contracts/interfaces/IRewardsCoordinator.sol index ed3ef30a04..6bcc1b132d 100644 --- a/src/contracts/interfaces/IRewardsCoordinator.sol +++ b/src/contracts/interfaces/IRewardsCoordinator.sol @@ -264,6 +264,16 @@ interface IRewardsCoordinatorEvents is IRewardsCoordinatorTypes { RewardsSubmission rewardsSubmission ); + // TODO: documentation -- perhaps want to reduce number of event fields as well? + event IncentivesDistributed( + address indexed submitter, + uint256 indexed submissionNonce, + bytes32 indexed incentivesSubmissionHash, + RewardsSubmission incentivesSubmission, + uint256 incentivesVersion, + bytes additionalData + ); + /** * @notice Emitted when an AVS creates a valid `OperatorDirectedRewardsSubmission` * @param caller The address calling `createOperatorDirectedAVSRewardsSubmission`. @@ -439,6 +449,22 @@ interface IRewardsCoordinator is IRewardsCoordinatorErrors, IRewardsCoordinatorE RewardsSubmission[] calldata rewardsSubmissions ) external; +// TODO: decide if managing valid `incentivesVersion`s makes sense! + /** + * @notice Creates a new incentives submission. The method of distribution amongst stakers and operators depends + * on the `incentivesVersion`, whilethe details of the distribution are dependent on the `incentivesSubmission` + * as well as potentially on the `additionalData` input + * @param incentivesVersion The version of incentives logic to + * @param incentivesSubmission Details about the incentives being distributed + * @param additionalData Optional field (perhaps required for some `incentivesVersion`s!) providing other + * information on how the incentives will be distributed + */ + function distributeIncentives( + uint256 incentivesVersion, + RewardsSubmission calldata incentivesSubmission, + bytes calldata additionalData + ) external; + /** * @notice Creates a new operator-directed rewards submission on behalf of an AVS, to be split amongst the operators and * set of stakers delegated to operators who are registered to the `avs`.