Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial v0.5.0 contracts #15273

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
574 changes: 574 additions & 0 deletions contracts/src/v0.8/llo-feeds/v0.5.0/FeeManager.sol

Large diffs are not rendered by default.

329 changes: 329 additions & 0 deletions contracts/src/v0.8/llo-feeds/v0.5.0/RewardManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol";
import {IRewardManager} from "./interfaces/IRewardManager.sol";
import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC20.sol";
import {TypeAndVersionInterface} from "../../interfaces/TypeAndVersionInterface.sol";
import {Common} from "../libraries/Common.sol";
import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";

/**
* @title RewardManager
* @author Michael Fletcher
* @author Austin Born
* @author ad0ll
* @notice This contract will be used to reward any configured recipients within a pool. Recipients will receive a share of their pool relative to their configured weight.
*/
contract RewardManager is IRewardManager, ConfirmedOwner, TypeAndVersionInterface {
using SafeERC20 for IERC20;

// @dev The mapping of total fees collected for a particular pot: s_totalRewardRecipientFees[poolId]
mapping(bytes32 => uint256) public s_totalRewardRecipientFees;

// @dev The mapping of fee balances for each pot last time the recipient claimed: s_totalRewardRecipientFeesLastClaimedAmounts[poolId][recipient]
mapping(bytes32 => mapping(address => uint256)) public s_totalRewardRecipientFeesLastClaimedAmounts;

// @dev The mapping of RewardRecipient weights for a particular poolId: s_rewardRecipientWeights[poolId][rewardRecipient].
mapping(bytes32 => mapping(address => uint256)) public s_rewardRecipientWeights;

// @dev Keep track of the reward recipient weights that have been set to prevent duplicates
mapping(bytes32 => bool) public s_rewardRecipientWeightsSet;

// @dev Store a list of pool ids that have been registered, to make off chain lookups easier
bytes32[] public s_registeredPoolIds;

// @dev The address for the LINK contract
address public immutable i_linkAddress;

// The total weight of all RewardRecipients. 1e18 = 100% of the pool fees
uint64 private constant PERCENTAGE_SCALAR = 1e18;

// The fee manager address
mapping(address => address) public s_feeManagerAddressList;

// @notice Thrown whenever the RewardRecipient weights are invalid
error InvalidWeights();

// @notice Thrown when any given address is invalid
error InvalidAddress();

// @notice Thrown when the pool id is invalid
error InvalidPoolId();

// @notice Thrown when the calling contract is not within the authorized contracts
error Unauthorized();

// @notice Thrown when getAvailableRewardPoolIds parameters are incorrectly set
error InvalidPoolLength();

// Events emitted upon state change
event RewardRecipientsUpdated(bytes32 indexed poolId, Common.AddressAndWeight[] newRewardRecipients);
event RewardsClaimed(bytes32 indexed poolId, address indexed recipient, uint192 quantity);
event FeeManagerUpdated(address newFeeManagerAddress);
event FeePaid(FeePayment[] payments, address payer);

/**
* @notice Constructor
* @param linkAddress address of the wrapped LINK token
*/
constructor(address linkAddress) ConfirmedOwner(msg.sender) {
//ensure that the address ia not zero
if (linkAddress == address(0)) revert InvalidAddress();

i_linkAddress = linkAddress;
}

// @inheritdoc TypeAndVersionInterface
function typeAndVersion() external pure override returns (string memory) {
return "RewardManager 0.5.0";
}

// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(IRewardManager).interfaceId;
}

modifier onlyOwnerOrFeeManager() {
if (msg.sender != s_feeManagerAddressList[msg.sender] && msg.sender != owner()) revert Unauthorized();
_;
}

modifier onlyOwnerOrRecipientInPool(bytes32 poolId) {
if (s_rewardRecipientWeights[poolId][msg.sender] == 0 && msg.sender != owner()) revert Unauthorized();
_;
}

modifier onlyFeeManager() {
if (msg.sender != s_feeManagerAddressList[msg.sender]) revert Unauthorized();
_;
}

/// @inheritdoc IRewardManager
function onFeePaid(FeePayment[] calldata payments, address payer) external override onlyFeeManager {
uint256 totalFeeAmount;
for (uint256 i; i < payments.length; ++i) {
unchecked {
//the total amount for any ERC-20 asset cannot exceed 2^256 - 1
//see https://github.com/OpenZeppelin/openzeppelin-contracts/blob/36bf1e46fa811f0f07d38eb9cfbc69a955f300ce/contracts/token/ERC20/ERC20.sol#L266
//for example implementation.
s_totalRewardRecipientFees[payments[i].poolId] += payments[i].amount;

//tally the total payable fees
totalFeeAmount += payments[i].amount;
}
}

//transfer the fees to this contract
IERC20(i_linkAddress).safeTransferFrom(payer, address(this), totalFeeAmount);

emit FeePaid(payments, payer);
}

/// @inheritdoc IRewardManager
function claimRewards(bytes32[] memory poolIds) external override {
_claimRewards(msg.sender, poolIds);
}

// wrapper impl for claimRewards
function _claimRewards(address recipient, bytes32[] memory poolIds) internal returns (uint256) {
//get the total amount claimable for this recipient
uint256 claimAmount;

//loop and claim all the rewards in the poolId pot
for (uint256 i; i < poolIds.length; ++i) {
//get the poolId to be claimed
bytes32 poolId = poolIds[i];

//get the total fees for the pot
uint256 totalFeesInPot = s_totalRewardRecipientFees[poolId];

unchecked {
//avoid unnecessary storage reads if there's no fees in the pot
if (totalFeesInPot == 0) continue;

//get the claimable amount for this recipient, this calculation will never exceed the amount in the pot
uint256 claimableAmount = totalFeesInPot - s_totalRewardRecipientFeesLastClaimedAmounts[poolId][recipient];

//calculate the recipients share of the fees, which is their weighted share of the difference between the last amount they claimed and the current amount in the pot. This can never be more than the total amount in existence
uint256 recipientShare = (claimableAmount * s_rewardRecipientWeights[poolId][recipient]) / PERCENTAGE_SCALAR;

//if there's no fees to claim, continue as there's nothing to update
if (recipientShare == 0) continue;

//keep track of the total amount claimable, this can never be more than the total amount in existence
claimAmount += recipientShare;

//set the current total amount of fees in the pot as it's used to calculate future claims
s_totalRewardRecipientFeesLastClaimedAmounts[poolId][recipient] = totalFeesInPot;

//emit event if the recipient has rewards to claim
emit RewardsClaimed(poolIds[i], recipient, uint192(recipientShare));
}
}

//check if there's any rewards to claim in the given poolId
if (claimAmount != 0) {
//transfer the reward to the recipient
IERC20(i_linkAddress).safeTransfer(recipient, claimAmount);
}

return claimAmount;
}

/// @inheritdoc IRewardManager
function setRewardRecipients(
bytes32 poolId,
Common.AddressAndWeight[] calldata rewardRecipientAndWeights
) external override onlyOwnerOrFeeManager {
//revert if there are no recipients to set
if (rewardRecipientAndWeights.length == 0) revert InvalidAddress();

//check that the weights have not been previously set
if (s_rewardRecipientWeightsSet[poolId]) revert InvalidPoolId();

//keep track of the registered poolIds to make off chain lookups easier
s_registeredPoolIds.push(poolId);

//keep track of which pools have had their reward recipients set
s_rewardRecipientWeightsSet[poolId] = true;

//set the reward recipients, this will only be called once and contain the full set of RewardRecipients with a total weight of 100%
_setRewardRecipientWeights(poolId, rewardRecipientAndWeights, PERCENTAGE_SCALAR);

emit RewardRecipientsUpdated(poolId, rewardRecipientAndWeights);
}

function _setRewardRecipientWeights(
bytes32 poolId,
Common.AddressAndWeight[] calldata rewardRecipientAndWeights,
uint256 expectedWeight
) internal {
//we can't update the weights if it contains duplicates
if (Common._hasDuplicateAddresses(rewardRecipientAndWeights)) revert InvalidAddress();

//loop all the reward recipients and validate the weight and address
uint256 totalWeight;
for (uint256 i; i < rewardRecipientAndWeights.length; ++i) {
//get the weight
uint256 recipientWeight = rewardRecipientAndWeights[i].weight;
//get the address
address recipientAddress = rewardRecipientAndWeights[i].addr;

//ensure the reward recipient address is not zero
if (recipientAddress == address(0)) revert InvalidAddress();

//save/overwrite the weight for the reward recipient
s_rewardRecipientWeights[poolId][recipientAddress] = recipientWeight;

unchecked {
//keep track of the cumulative weight, this cannot overflow as the total weight is restricted at 1e18
totalWeight += recipientWeight;
}
}

//if total weight is not met, the fees will either be under or over distributed
if (totalWeight != expectedWeight) revert InvalidWeights();
}

/// @inheritdoc IRewardManager
function updateRewardRecipients(
bytes32 poolId,
Common.AddressAndWeight[] calldata newRewardRecipients
) external override onlyOwner {
//create an array of poolIds to pass to _claimRewards if required
bytes32[] memory poolIds = new bytes32[](1);
poolIds[0] = poolId;

//loop all the reward recipients and claim their rewards before updating their weights
uint256 existingTotalWeight;
for (uint256 i; i < newRewardRecipients.length; ++i) {
//get the address
address recipientAddress = newRewardRecipients[i].addr;
//get the existing weight
uint256 existingWeight = s_rewardRecipientWeights[poolId][recipientAddress];

//if a recipient is updated, the rewards must be claimed first as they can't claim previous fees at the new weight
_claimRewards(newRewardRecipients[i].addr, poolIds);

unchecked {
//keep tally of the weights so that the expected collective weight is known
existingTotalWeight += existingWeight;
}
}

//update the reward recipients, if the new collective weight isn't equal to the previous collective weight, the fees will either be under or over distributed
_setRewardRecipientWeights(poolId, newRewardRecipients, existingTotalWeight);

//emit event
emit RewardRecipientsUpdated(poolId, newRewardRecipients);
}

/// @inheritdoc IRewardManager
function payRecipients(bytes32 poolId, address[] calldata recipients) external onlyOwnerOrRecipientInPool(poolId) {
//convert poolIds to an array to match the interface of _claimRewards
bytes32[] memory poolIdsArray = new bytes32[](1);
poolIdsArray[0] = poolId;

//loop each recipient and claim the rewards for each of the pools and assets
for (uint256 i; i < recipients.length; ++i) {
_claimRewards(recipients[i], poolIdsArray);
}
}

/// @inheritdoc IRewardManager
function addFeeManager(address newFeeManagerAddress) external onlyOwner {
if (newFeeManagerAddress == address(0)) revert InvalidAddress();
if (s_feeManagerAddressList[newFeeManagerAddress] != address(0)) revert InvalidAddress();

s_feeManagerAddressList[newFeeManagerAddress] = newFeeManagerAddress;

emit FeeManagerUpdated(newFeeManagerAddress);
}

/// @inheritdoc IRewardManager
function removeFeeManager(address feeManagerAddress) external onlyOwner {
if (s_feeManagerAddressList[feeManagerAddress] == address(0)) revert InvalidAddress();
delete s_feeManagerAddressList[feeManagerAddress];
}

/// @inheritdoc IRewardManager
function getAvailableRewardPoolIds(
address recipient,
uint256 startIndex,
uint256 endIndex
) external view returns (bytes32[] memory) {
//get the length of the pool ids which we will loop through and potentially return
uint256 registeredPoolIdsLength = s_registeredPoolIds.length;

uint256 lastIndex = endIndex > registeredPoolIdsLength ? registeredPoolIdsLength : endIndex;

if (startIndex > lastIndex) revert InvalidPoolLength();

//create a new array with the maximum amount of potential pool ids
bytes32[] memory claimablePoolIds = new bytes32[](lastIndex - startIndex);
//we want the pools which a recipient has funds for to be sequential, so we need to keep track of the index
uint256 poolIdArrayIndex;

//loop all the pool ids, and check if the recipient has a registered weight and a claimable amount
for (uint256 i = startIndex; i < lastIndex; ++i) {
//get the poolId
bytes32 poolId = s_registeredPoolIds[i];

//if the recipient has a weight, they are a recipient of this poolId
if (s_rewardRecipientWeights[poolId][recipient] != 0) {
//get the total in this pool
uint256 totalPoolAmount = s_totalRewardRecipientFees[poolId];
//if the recipient has any LINK, then add the poolId to the array
unchecked {
//s_totalRewardRecipientFeesLastClaimedAmounts can never exceed total pool amount, and the number of pools can't exceed the max array length
if (totalPoolAmount - s_totalRewardRecipientFeesLastClaimedAmounts[poolId][recipient] != 0) {
claimablePoolIds[poolIdArrayIndex++] = poolId;
}
}
}
}

return claimablePoolIds;
}
}
Loading
Loading