diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/FeeManager.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/FeeManager.sol new file mode 100644 index 00000000000..42ad8044759 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/FeeManager.sol @@ -0,0 +1,525 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; +import {IFeeManager} from "./interfaces/IFeeManager.sol"; +import {TypeAndVersionInterface} from "../../interfaces/TypeAndVersionInterface.sol"; +import {IERC165} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {Common} from "../libraries/Common.sol"; +import {IRewardManager} from "./interfaces/IRewardManager.sol"; +import {IWERC20} from "../../shared/interfaces/IWERC20.sol"; +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC20.sol"; +import {Math} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/Math.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IVerifierFeeManager} from "./interfaces/IVerifierFeeManager.sol"; + +/** + * @title FeeManager + * @author Michael Fletcher + * @author Austin Born + * @notice This contract is used for the handling of fees required for users verifying reports. + */ +contract FeeManager is IFeeManager, ConfirmedOwner, TypeAndVersionInterface { + using SafeERC20 for IERC20; + + /// @notice list of subscribers and their discounts subscriberDiscounts[subscriber][feedId][token] + mapping(address => mapping(bytes32 => mapping(address => uint256))) public s_subscriberDiscounts; + + /// @notice map of global discounts + mapping(address => mapping(address => uint256)) public s_globalDiscounts; + + /// @notice keep track of any subsidised link that is owed to the reward manager. + mapping(bytes32 => uint256) public s_linkDeficit; + + /// @notice the total discount that can be applied to a fee, 1e18 = 100% discount + uint64 private constant PERCENTAGE_SCALAR = 1e18; + + /// @notice the LINK token address + address public immutable i_linkAddress; + + /// @notice the native token address + address public immutable i_nativeAddress; + + /// @notice the proxy address + address public immutable i_proxyAddress; + + /// @notice the reward manager address + IRewardManager public immutable i_rewardManager; + + // @notice the mask to apply to get the report version + bytes32 private constant REPORT_VERSION_MASK = 0xffff000000000000000000000000000000000000000000000000000000000000; + + // @notice the different report versions + bytes32 private constant REPORT_V1 = 0x0001000000000000000000000000000000000000000000000000000000000000; + + /// @notice the surcharge fee to be paid if paying in native + uint256 public s_nativeSurcharge; + + /// @notice the error thrown if the discount or surcharge is invalid + error InvalidSurcharge(); + + /// @notice the error thrown if the discount is invalid + error InvalidDiscount(); + + /// @notice the error thrown if the address is invalid + error InvalidAddress(); + + /// @notice thrown if msg.value is supplied with a bad quote + error InvalidDeposit(); + + /// @notice thrown if a report has expired + error ExpiredReport(); + + /// @notice thrown if a report has no quote + error InvalidQuote(); + + // @notice thrown when the caller is not authorized + error Unauthorized(); + + // @notice thrown when trying to clear a zero deficit + error ZeroDeficit(); + + /// @notice thrown when trying to pay an address that cannot except funds + error InvalidReceivingAddress(); + + /// @notice Emitted whenever a subscriber's discount is updated + /// @param subscriber address of the subscriber to update discounts for + /// @param feedId Feed ID for the discount + /// @param token Token address for the discount + /// @param discount Discount to apply, in relation to the PERCENTAGE_SCALAR + event SubscriberDiscountUpdated(address indexed subscriber, bytes32 indexed feedId, address token, uint64 discount); + + /// @notice Emitted when updating the native surcharge + /// @param newSurcharge Surcharge amount to apply relative to PERCENTAGE_SCALAR + event NativeSurchargeUpdated(uint64 newSurcharge); + + /// @notice Emits when this contract does not have enough LINK to send to the reward manager when paying in native + /// @param rewards Config digest and link fees which could not be subsidised + event InsufficientLink(IRewardManager.FeePayment[] rewards); + + /// @notice Emitted when funds are withdrawn + /// @param adminAddress Address of the admin + /// @param recipient Address of the recipient + /// @param assetAddress Address of the asset withdrawn + /// @param quantity Amount of the asset withdrawn + event Withdraw(address adminAddress, address recipient, address assetAddress, uint192 quantity); + + /// @notice Emits when a deficit has been cleared for a particular config digest + /// @param configDigest Config digest of the deficit cleared + /// @param linkQuantity Amount of LINK required to pay the deficit + event LinkDeficitCleared(bytes32 indexed configDigest, uint256 linkQuantity); + + /// @notice Emits when a fee has been processed + /// @param configDigest Config digest of the fee processed + /// @param subscriber Address of the subscriber who paid the fee + /// @param fee Fee paid + /// @param reward Reward paid + /// @param appliedDiscount Discount applied to the fee + event DiscountApplied( + bytes32 indexed configDigest, + address indexed subscriber, + Common.Asset fee, + Common.Asset reward, + uint256 appliedDiscount + ); + + /** + * @notice Construct the FeeManager contract + * @param _linkAddress The address of the LINK token + * @param _nativeAddress The address of the wrapped ERC-20 version of the native token (represents fee in native or wrapped) + * @param _proxyAddress The address of the proxy contract + * @param _rewardManagerAddress The address of the reward manager contract + */ + constructor( + address _linkAddress, + address _nativeAddress, + address _proxyAddress, + address _rewardManagerAddress + ) ConfirmedOwner(msg.sender) { + if ( + _linkAddress == address(0) || + _nativeAddress == address(0) || + _proxyAddress == address(0) || + _rewardManagerAddress == address(0) + ) revert InvalidAddress(); + + i_linkAddress = _linkAddress; + i_nativeAddress = _nativeAddress; + i_proxyAddress = _proxyAddress; + i_rewardManager = IRewardManager(_rewardManagerAddress); + + IERC20(i_linkAddress).approve(address(i_rewardManager), type(uint256).max); + } + + modifier onlyOwnerOrProxy() { + if (msg.sender != i_proxyAddress && msg.sender != owner()) revert Unauthorized(); + _; + } + + modifier onlyProxy() { + if (msg.sender != i_proxyAddress) revert Unauthorized(); + _; + } + + /// @inheritdoc TypeAndVersionInterface + function typeAndVersion() external pure override returns (string memory) { + return "FeeManager 2.1.0"; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == this.processFee.selector || interfaceId == this.processFeeBulk.selector; + } + + /// @inheritdoc IVerifierFeeManager + function processFee( + bytes calldata payload, + bytes calldata parameterPayload, + address subscriber + ) external payable override onlyProxy { + (Common.Asset memory fee, Common.Asset memory reward, uint256 appliedDiscount) = _processFee( + payload, + parameterPayload, + subscriber + ); + + if (fee.amount == 0) { + _transfer(subscriber, msg.value); + return; + } + + IFeeManager.FeeAndReward[] memory feeAndReward = new IFeeManager.FeeAndReward[](1); + feeAndReward[0] = IFeeManager.FeeAndReward(bytes32(payload), fee, reward, appliedDiscount); + + if (fee.assetAddress == i_linkAddress) { + _handleFeesAndRewards(subscriber, feeAndReward, 1, 0); + } else { + _handleFeesAndRewards(subscriber, feeAndReward, 0, 1); + } + } + + /// @inheritdoc IVerifierFeeManager + function processFeeBulk( + bytes[] calldata payloads, + bytes calldata parameterPayload, + address subscriber + ) external payable override onlyProxy { + FeeAndReward[] memory feesAndRewards = new IFeeManager.FeeAndReward[](payloads.length); + + //keep track of the number of fees to prevent over initialising the FeePayment array within _convertToLinkAndNativeFees + uint256 numberOfLinkFees; + uint256 numberOfNativeFees; + + uint256 feesAndRewardsIndex; + for (uint256 i; i < payloads.length; ++i) { + (Common.Asset memory fee, Common.Asset memory reward, uint256 appliedDiscount) = _processFee( + payloads[i], + parameterPayload, + subscriber + ); + + if (fee.amount != 0) { + feesAndRewards[feesAndRewardsIndex++] = IFeeManager.FeeAndReward( + bytes32(payloads[i]), + fee, + reward, + appliedDiscount + ); + + unchecked { + //keep track of some tallys to make downstream calculations more efficient + if (fee.assetAddress == i_linkAddress) { + ++numberOfLinkFees; + } else { + ++numberOfNativeFees; + } + } + } + } + + if (numberOfLinkFees != 0 || numberOfNativeFees != 0) { + _handleFeesAndRewards(subscriber, feesAndRewards, numberOfLinkFees, numberOfNativeFees); + } else { + _transfer(subscriber, msg.value); + } + } + + /// @inheritdoc IFeeManager + function getFeeAndReward( + address subscriber, + bytes memory report, + address quoteAddress + ) public view returns (Common.Asset memory, Common.Asset memory, uint256) { + Common.Asset memory fee; + Common.Asset memory reward; + + //get the feedId from the report + bytes32 feedId = bytes32(report); + + //the report needs to be a support version + bytes32 reportVersion = _getReportVersion(feedId); + + //version 1 of the reports don't require quotes, so the fee will be 0 + if (reportVersion == REPORT_V1) { + fee.assetAddress = i_nativeAddress; + reward.assetAddress = i_linkAddress; + return (fee, reward, 0); + } + + //verify the quote payload is a supported token + if (quoteAddress != i_nativeAddress && quoteAddress != i_linkAddress) { + revert InvalidQuote(); + } + + //decode the report depending on the version + uint256 linkQuantity; + uint256 nativeQuantity; + uint256 expiresAt; + (, , , nativeQuantity, linkQuantity, expiresAt) = abi.decode( + report, + (bytes32, uint32, uint32, uint192, uint192, uint32) + ); + + //read the timestamp bytes from the report data and verify it has not expired + if (expiresAt < block.timestamp) { + revert ExpiredReport(); + } + + //get the discount being applied + uint256 discount = s_subscriberDiscounts[subscriber][feedId][quoteAddress]; + + if (discount == 0) { + //check if a global discount has been applied + discount = s_globalDiscounts[subscriber][quoteAddress]; + } + + //the reward is always set in LINK + reward.assetAddress = i_linkAddress; + reward.amount = Math.ceilDiv(linkQuantity * (PERCENTAGE_SCALAR - discount), PERCENTAGE_SCALAR); + + //calculate either the LINK fee or native fee if it's within the report + if (quoteAddress == i_linkAddress) { + fee.assetAddress = i_linkAddress; + fee.amount = reward.amount; + } else { + uint256 surchargedFee = Math.ceilDiv(nativeQuantity * (PERCENTAGE_SCALAR + s_nativeSurcharge), PERCENTAGE_SCALAR); + + fee.assetAddress = i_nativeAddress; + fee.amount = Math.ceilDiv(surchargedFee * (PERCENTAGE_SCALAR - discount), PERCENTAGE_SCALAR); + } + + //return the fee + return (fee, reward, discount); + } + + /// @inheritdoc IVerifierFeeManager + function setFeeRecipients( + bytes32 configDigest, + Common.AddressAndWeight[] calldata rewardRecipientAndWeights + ) external onlyOwnerOrProxy { + i_rewardManager.setRewardRecipients(configDigest, rewardRecipientAndWeights); + } + + /// @inheritdoc IFeeManager + function setNativeSurcharge(uint64 surcharge) external onlyOwner { + if (surcharge > PERCENTAGE_SCALAR) revert InvalidSurcharge(); + + s_nativeSurcharge = surcharge; + + emit NativeSurchargeUpdated(surcharge); + } + + /// @inheritdoc IFeeManager + function updateSubscriberDiscount( + address subscriber, + bytes32 feedId, + address token, + uint64 discount + ) external onlyOwner { + //make sure the discount is not greater than the total discount that can be applied + if (discount > PERCENTAGE_SCALAR) revert InvalidDiscount(); + //make sure the token is either LINK or native + if (token != i_linkAddress && token != i_nativeAddress) revert InvalidAddress(); + + s_subscriberDiscounts[subscriber][feedId][token] = discount; + + emit SubscriberDiscountUpdated(subscriber, feedId, token, discount); + } + + function updateSubscriberGlobalDiscount(address subscriber, address token, uint64 discount) external onlyOwner { + //make sure the discount is not greater than the total discount that can be applied + if (discount > PERCENTAGE_SCALAR) revert InvalidDiscount(); + //make sure the token is either LINK or native + if (token != i_linkAddress && token != i_nativeAddress) revert InvalidAddress(); + + s_globalDiscounts[subscriber][token] = discount; + + emit SubscriberDiscountUpdated(subscriber, bytes32(0), token, discount); + } + + /// @inheritdoc IFeeManager + function withdraw(address assetAddress, address recipient, uint192 quantity) external onlyOwner { + //address 0 is used to withdraw native in the context of withdrawing + if (assetAddress == address(0)) { + _transfer(recipient, quantity); + return; + } + + //withdraw the requested asset + IERC20(assetAddress).safeTransfer(recipient, quantity); + + //emit event when funds are withdrawn + emit Withdraw(msg.sender, recipient, assetAddress, uint192(quantity)); + } + + /// @inheritdoc IFeeManager + function linkAvailableForPayment() external view returns (uint256) { + //return the amount of LINK this contact has available to pay rewards + return IERC20(i_linkAddress).balanceOf(address(this)); + } + + /** + * @notice Gets the current version of the report that is encoded as the last two bytes of the feed + * @param feedId feed id to get the report version for + */ + function _getReportVersion(bytes32 feedId) internal pure returns (bytes32) { + return REPORT_VERSION_MASK & feedId; + } + + function _processFee( + bytes calldata payload, + bytes calldata parameterPayload, + address subscriber + ) internal view returns (Common.Asset memory, Common.Asset memory, uint256) { + if (subscriber == address(this)) revert InvalidAddress(); + + //decode the report from the payload + (, bytes memory report) = abi.decode(payload, (bytes32[3], bytes)); + + //get the feedId from the report + bytes32 feedId = bytes32(report); + + //v1 doesn't need a quote payload, so skip the decoding + address quote; + if (_getReportVersion(feedId) != REPORT_V1) { + //decode the quote from the bytes + (quote) = abi.decode(parameterPayload, (address)); + } + + //decode the fee, it will always be native or LINK + return getFeeAndReward(subscriber, report, quote); + } + + function _handleFeesAndRewards( + address subscriber, + FeeAndReward[] memory feesAndRewards, + uint256 numberOfLinkFees, + uint256 numberOfNativeFees + ) internal { + IRewardManager.FeePayment[] memory linkRewards = new IRewardManager.FeePayment[](numberOfLinkFees); + IRewardManager.FeePayment[] memory nativeFeeLinkRewards = new IRewardManager.FeePayment[](numberOfNativeFees); + + uint256 totalNativeFee; + uint256 totalNativeFeeLinkValue; + + uint256 linkRewardsIndex; + uint256 nativeFeeLinkRewardsIndex; + + uint256 totalNumberOfFees = numberOfLinkFees + numberOfNativeFees; + for (uint256 i; i < totalNumberOfFees; ++i) { + if (feesAndRewards[i].fee.assetAddress == i_linkAddress) { + linkRewards[linkRewardsIndex++] = IRewardManager.FeePayment( + feesAndRewards[i].configDigest, + uint192(feesAndRewards[i].reward.amount) + ); + } else { + nativeFeeLinkRewards[nativeFeeLinkRewardsIndex++] = IRewardManager.FeePayment( + feesAndRewards[i].configDigest, + uint192(feesAndRewards[i].reward.amount) + ); + totalNativeFee += feesAndRewards[i].fee.amount; + totalNativeFeeLinkValue += feesAndRewards[i].reward.amount; + } + + if (feesAndRewards[i].appliedDiscount != 0) { + emit DiscountApplied( + feesAndRewards[i].configDigest, + subscriber, + feesAndRewards[i].fee, + feesAndRewards[i].reward, + feesAndRewards[i].appliedDiscount + ); + } + } + + //keep track of change in case of any over payment + uint256 change; + + if (msg.value != 0) { + //there must be enough to cover the fee + if (totalNativeFee > msg.value) revert InvalidDeposit(); + + //wrap the amount required to pay the fee & approve as the subscriber paid in wrapped native + IWERC20(i_nativeAddress).deposit{value: totalNativeFee}(); + + unchecked { + //msg.value is always >= to fee.amount + change = msg.value - totalNativeFee; + } + } else { + if (totalNativeFee != 0) { + //subscriber has paid in wrapped native, so transfer the native to this contract + IERC20(i_nativeAddress).safeTransferFrom(subscriber, address(this), totalNativeFee); + } + } + + if (linkRewards.length != 0) { + i_rewardManager.onFeePaid(linkRewards, subscriber); + } + + if (nativeFeeLinkRewards.length != 0) { + //distribute subsidised fees paid in Native + if (totalNativeFeeLinkValue > IERC20(i_linkAddress).balanceOf(address(this))) { + // If not enough LINK on this contract to forward for rewards, tally the deficit to be paid by out-of-band LINK + for (uint256 i; i < nativeFeeLinkRewards.length; ++i) { + unchecked { + //we have previously tallied the fees, any overflows would have already reverted + s_linkDeficit[nativeFeeLinkRewards[i].poolId] += nativeFeeLinkRewards[i].amount; + } + } + + emit InsufficientLink(nativeFeeLinkRewards); + } else { + //distribute the fees + i_rewardManager.onFeePaid(nativeFeeLinkRewards, address(this)); + } + } + + // a refund may be needed if the payee has paid in excess of the fee + _transfer(subscriber, change); + } + + function _transfer(address to, uint256 quantity) internal { + if (quantity != 0) { + (bool success, ) = payable(to).call{value: quantity}(""); + if (!success) revert InvalidReceivingAddress(); + } + } + + /// @inheritdoc IFeeManager + function payLinkDeficit(bytes32 configDigest) external onlyOwner { + uint256 deficit = s_linkDeficit[configDigest]; + + if (deficit == 0) revert ZeroDeficit(); + + delete s_linkDeficit[configDigest]; + + IRewardManager.FeePayment[] memory deficitFeePayment = new IRewardManager.FeePayment[](1); + + deficitFeePayment[0] = IRewardManager.FeePayment(configDigest, uint192(deficit)); + + i_rewardManager.onFeePaid(deficitFeePayment, address(this)); + + emit LinkDeficitCleared(configDigest, deficit); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/RewardManager.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/RewardManager.sol new file mode 100644 index 00000000000..49fef51c569 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/RewardManager.sol @@ -0,0 +1,321 @@ +// 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 + * @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 + address public s_feeManagerAddress; + + // @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 1.1.0"; + } + + // @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == this.onFeePaid.selector; + } + + modifier onlyOwnerOrFeeManager() { + if (msg.sender != owner() && msg.sender != s_feeManagerAddress) revert Unauthorized(); + _; + } + + modifier onlyOwnerOrRecipientInPool(bytes32 poolId) { + if (msg.sender != owner() && s_rewardRecipientWeights[poolId][msg.sender] == 0) revert Unauthorized(); + _; + } + + modifier onlyFeeManager() { + if (msg.sender != s_feeManagerAddress) 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 setFeeManager(address newFeeManagerAddress) external onlyOwner { + if (newFeeManagerAddress == address(0)) revert InvalidAddress(); + + s_feeManagerAddress = newFeeManagerAddress; + + emit FeeManagerUpdated(newFeeManagerAddress); + } + + /// @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; + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/Verifier.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/Verifier.sol new file mode 100644 index 00000000000..371aa3e9d30 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/Verifier.sol @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; +import {IVerifier} from "./interfaces/IVerifier.sol"; +import {IVerifierProxy} from "./interfaces/IVerifierProxy.sol"; +import {TypeAndVersionInterface} from "../../interfaces/TypeAndVersionInterface.sol"; +import {IERC165} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {Common} from "../libraries/Common.sol"; + +// OCR2 standard +uint256 constant MAX_NUM_ORACLES = 31; + +/* + * The verifier contract is used to verify offchain reports signed + * by DONs. A report consists of a price, block number and feed Id. It + * represents the observed price of an asset at a specified block number for + * a feed. The verifier contract is used to verify that such reports have + * been signed by the correct signers. + **/ +contract Verifier is IVerifier, ConfirmedOwner, TypeAndVersionInterface { + // The first byte of the mask can be 0, because we only ever have 31 oracles + uint256 internal constant ORACLE_MASK = 0x0001010101010101010101010101010101010101010101010101010101010101; + + enum Role { + // Default role for an oracle address. This means that the oracle address + // is not a signer + Unset, + // Role given to an oracle address that is allowed to sign a report + Signer + } + + struct Signer { + // Index of oracle in a configuration + uint8 index; + // The oracle's role + Role role; + } + + struct VerifierState { + // The block number of the block the last time the configuration was updated. + uint32 latestConfigBlockNumber; + // Whether the config is deactivated + bool isActive; + // Fault tolerance + uint8 f; + // Number of signers + uint8 oracleCount; + // Map of signer addresses to oracles + mapping(address => Signer) oracles; + } + + /// @notice This event is emitted when a new report is verified. + /// It is used to keep a historical record of verified reports. + event ReportVerified(bytes32 indexed feedId, address requester); + + /// @notice This event is emitted whenever a new DON configuration is set. + event ConfigSet( + bytes32 indexed configDigest, + address[] signers, + uint8 f + ); + + /// @notice This event is + event ConfigUpdated( + bytes32 indexed configDigest, + address[] prevSigners, + address[] newSigners + ); + + /// @notice This event is emitted whenever a configuration is deactivated + event ConfigDeactivated(bytes32 indexed configDigest); + + /// @notice This event is emitted whenever a configuration is activated + event ConfigActivated(bytes32 indexed configDigest); + + /// @notice This error is thrown whenever an address tries + /// to exeecute a transaction that it is not authorized to do so + error AccessForbidden(); + + /// @notice This error is thrown whenever a zero address is passed + error ZeroAddress(); + + /// @notice This error is thrown whenever the config digest + /// is empty + error DigestEmpty(); + + /// @notice This error is thrown whenever the config digest + /// passed in has not been set in this verifier + /// @param configDigest The config digest that has not been set + error DigestNotSet(bytes32 configDigest); + + /// @notice This error is thrown whenever the config digest + /// has been deactivated + /// @param configDigest The config digest that is inactive + error DigestInactive(bytes32 configDigest); + + /// @notice This error is thrown whenever trying to set a config + /// with a fault tolerance of 0 + error FaultToleranceMustBePositive(); + + /// @notice This error is thrown whenever a report is signed + /// with more than the max number of signers + /// @param numSigners The number of signers who have signed the report + /// @param maxSigners The maximum number of signers that can sign a report + error ExcessSigners(uint256 numSigners, uint256 maxSigners); + + /// @notice This error is thrown whenever a report is signed + /// with less than the minimum number of signers + /// @param numSigners The number of signers who have signed the report + /// @param minSigners The minimum number of signers that need to sign a report + error InsufficientSigners(uint256 numSigners, uint256 minSigners); + + /// @notice This error is thrown whenever a report is signed + /// with an incorrect number of signers + /// @param numSigners The number of signers who have signed the report + /// @param expectedNumSigners The expected number of signers that need to sign + /// a report + error IncorrectSignatureCount(uint256 numSigners, uint256 expectedNumSigners); + + /// @notice This error is thrown whenever the R and S signer components + /// have different lengths + /// @param rsLength The number of r signature components + /// @param ssLength The number of s signature components + error MismatchedSignatures(uint256 rsLength, uint256 ssLength); + + /// @notice This error is thrown whenever setting a config with duplicate signatures + error NonUniqueSignatures(); + + /// @notice This error is thrown whenever a report fails to verify due to bad or duplicate signatures + error BadVerification(); + + /// @notice This error is thrown whenever a config digest is already set when setting the configuration + error ConfigDigestAlreadySet(); + + /// @notice The address of the verifier proxy + address private immutable i_verifierProxyAddr; + + /// @notice Verifier states keyed on config digest + mapping(bytes32 => VerifierState) internal s_verifierStates; + + /// @param verifierProxyAddr The address of the VerifierProxy contract + constructor(address verifierProxyAddr) ConfirmedOwner(msg.sender) { + if (verifierProxyAddr == address(0)) revert ZeroAddress(); + i_verifierProxyAddr = verifierProxyAddr; + } + + modifier checkConfigValid(uint256 numSigners, uint256 f) { + if (f == 0) revert FaultToleranceMustBePositive(); + if (numSigners > MAX_NUM_ORACLES) revert ExcessSigners(numSigners, MAX_NUM_ORACLES); + if (numSigners <= 3 * f) revert InsufficientSigners(numSigners, 3 * f + 1); + _; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) external pure override returns (bool isVerifier) { + return interfaceId == this.verify.selector; + } + + /// @inheritdoc TypeAndVersionInterface + function typeAndVersion() external pure override returns (string memory) { + return "Verifier 2.0.0"; + } + + /// @inheritdoc IVerifier + function verify( + bytes calldata signedReport, + address sender + ) external override returns (bytes memory verifierResponse) { + if (msg.sender != i_verifierProxyAddr) revert AccessForbidden(); + ( + bytes32[3] memory reportContext, + bytes memory reportData, + bytes32[] memory rs, + bytes32[] memory ss, + bytes32 rawVs + ) = abi.decode(signedReport, (bytes32[3], bytes, bytes32[], bytes32[], bytes32)); + + // reportContext consists of: + // reportContext[0]: ConfigDigest + // reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round + // reportContext[2]: ExtraHash + bytes32 configDigest = reportContext[0]; + + VerifierState storage verifierState = s_verifierStates[configDigest]; + + _validateReport(configDigest, rs, ss, verifierState); + + bytes32 hashedReport = keccak256(reportData); + + _verifySignatures(hashedReport, reportContext, rs, ss, rawVs, verifierState); + emit ReportVerified(bytes32(reportData), sender); + + return reportData; + } + + /// @notice Validates parameters of the report + /// @param configDigest Config digest from the report + /// @param rs R components from the report + /// @param ss S components from the report + /// @param config Config for the given digest + function _validateReport( + bytes32 configDigest, + bytes32[] memory rs, + bytes32[] memory ss, + VerifierState storage config + ) private view { + uint8 expectedNumSignatures = config.f + 1; + + if (!config.isActive) revert DigestInactive(configDigest); + if (rs.length != expectedNumSignatures) revert IncorrectSignatureCount(rs.length, expectedNumSignatures); + if (rs.length != ss.length) revert MismatchedSignatures(rs.length, ss.length); + } + + + /// @notice Verifies that a report has been signed by the correct + /// signers and that enough signers have signed the reports. + /// @param hashedReport The keccak256 hash of the raw report's bytes + /// @param reportContext The context the report was signed in + /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param rawVs ith element is the the V component of the ith signature + /// @param config The config digest the report was signed for + function _verifySignatures( + bytes32 hashedReport, + bytes32[3] memory reportContext, + bytes32[] memory rs, + bytes32[] memory ss, + bytes32 rawVs, + VerifierState storage config + ) private view { + bytes32 h = keccak256(abi.encodePacked(hashedReport, reportContext)); + // i-th byte counts number of sigs made by i-th signer + uint256 signedCount; + + Signer memory o; + address signerAddress; + uint256 numSigners = rs.length; + for (uint256 i; i < numSigners; ++i) { + signerAddress = ecrecover(h, uint8(rawVs[i]) + 27, rs[i], ss[i]); + o = config.oracles[signerAddress]; + if (o.role != Role.Signer) revert BadVerification(); + unchecked { + signedCount += 1 << (8 * o.index); + } + } + + if (signedCount & ORACLE_MASK != signedCount) revert BadVerification(); + } + + /// @inheritdoc IVerifier + function updateConfig( + bytes32 configDigest, + address[] calldata prevSigners, + address[] calldata newSigners, + uint8 f + ) external override checkConfigValid(newSigners.length, f) onlyOwner { + VerifierState storage config = s_verifierStates[configDigest]; + + if (config.f == 0) revert DigestNotSet(configDigest); + + // We must be removing the number of signers that were originally set + if(config.oracleCount != prevSigners.length){ + revert NonUniqueSignatures(); + } + + for (uint256 i; i < prevSigners.length; ++i) { + // Check the signers being removed are not zero address or duplicates + if(config.oracles[prevSigners[i]].role == Role.Unset){ + revert NonUniqueSignatures(); + } + + delete config.oracles[prevSigners[i]]; + } + + // Once signers have been cleared we can set the new signers + _setConfig(configDigest, newSigners, f, new Common.AddressAndWeight[](0), true); + + + emit ConfigUpdated(configDigest, prevSigners, newSigners); + } + + /// @inheritdoc IVerifier + function setConfig( + bytes32 configDigest, + address[] calldata signers, + uint8 f, + Common.AddressAndWeight[] memory recipientAddressesAndWeights + ) external override checkConfigValid(signers.length, f) onlyOwner { + _setConfig(configDigest, signers, f, recipientAddressesAndWeights, false); + } + + function _setConfig( + bytes32 configDigest, + address[] calldata signers, + uint8 f, + Common.AddressAndWeight[] memory recipientAddressesAndWeights, + bool updateConfig + ) internal { + VerifierState storage verifierState = s_verifierStates[configDigest]; + + if(verifierState.f > 0 && !updateConfig) { + revert ConfigDigestAlreadySet(); + } + + verifierState.latestConfigBlockNumber = uint32(block.number); + verifierState.f = f; + verifierState.isActive = true; + verifierState.oracleCount = uint8(signers.length); + + for (uint8 i; i < signers.length; ++i) { + address signerAddr = signers[i]; + if (signerAddr == address(0)) revert ZeroAddress(); + + // All signer roles are unset by default for a new config digest. + // Here the contract checks to see if a signer's address has already + // been set to ensure that the group of signer addresses that will + // sign reports with the config digest are unique. + bool isSignerAlreadySet = verifierState.oracles[signerAddr].role != + Role.Unset; + if (isSignerAlreadySet) revert NonUniqueSignatures(); + verifierState.oracles[signerAddr] = Signer({ + role: Role.Signer, + index: i + }); + } + + if(!updateConfig) { + IVerifierProxy(i_verifierProxyAddr).setVerifier( + bytes32(0), + configDigest, + recipientAddressesAndWeights + ); + + emit ConfigSet( + configDigest, + signers, + f + ); + } + } + + /// @inheritdoc IVerifier + function activateConfig(bytes32 configDigest) external onlyOwner { + VerifierState storage verifierState = s_verifierStates[configDigest]; + + if (configDigest == bytes32("")) revert DigestEmpty(); + if (verifierState.f == 0) revert DigestNotSet(configDigest); + verifierState.isActive = true; + emit ConfigActivated(configDigest); + } + + /// @inheritdoc IVerifier + function deactivateConfig(bytes32 configDigest) external onlyOwner { + VerifierState storage verifierState = s_verifierStates[configDigest]; + + if (configDigest == bytes32("")) revert DigestEmpty(); + if (verifierState.f == 0) revert DigestNotSet(configDigest); + verifierState.isActive = false; + emit ConfigDeactivated(configDigest); + } + + /// @inheritdoc IVerifier + function latestConfigDetails( + bytes32 configDigest + ) external view override returns (uint32 blockNumber) { + VerifierState storage verifierState = s_verifierStates[configDigest]; + return ( + verifierState.latestConfigBlockNumber + ); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/VerifierProxy.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/VerifierProxy.sol new file mode 100644 index 00000000000..c06312dd7be --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/VerifierProxy.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; +import {IVerifierProxy} from "./interfaces/IVerifierProxy.sol"; +import {IVerifier} from "./interfaces/IVerifier.sol"; +import {TypeAndVersionInterface} from "../../interfaces/TypeAndVersionInterface.sol"; +import {AccessControllerInterface} from "../../shared/interfaces/AccessControllerInterface.sol"; +import {IERC165} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {IVerifierFeeManager} from "./interfaces/IVerifierFeeManager.sol"; +import {Common} from "../libraries/Common.sol"; + +/** + * The verifier proxy contract is the gateway for all report verification requests + * on a chain. It is responsible for taking in a verification request and routing + * it to the correct verifier contract. + */ +contract VerifierProxy is IVerifierProxy, ConfirmedOwner, TypeAndVersionInterface { + /// @notice This event is emitted whenever a new verifier contract is set + /// @param oldConfigDigest The config digest that was previously the latest config + /// digest of the verifier contract at the verifier address. + /// @param oldConfigDigest The latest config digest of the verifier contract + /// at the verifier address. + /// @param verifierAddress The address of the verifier contract that verifies reports for + /// a given digest + event VerifierSet(bytes32 oldConfigDigest, bytes32 newConfigDigest, address verifierAddress); + + /// @notice This event is emitted whenever a new verifier contract is initialized + /// @param verifierAddress The address of the verifier contract that verifies reports + event VerifierInitialized(address verifierAddress); + + /// @notice This event is emitted whenever a verifier is unset + /// @param configDigest The config digest that was unset + /// @param verifierAddress The Verifier contract address unset + event VerifierUnset(bytes32 configDigest, address verifierAddress); + + /// @notice This event is emitted when a new access controller is set + /// @param oldAccessController The old access controller address + /// @param newAccessController The new access controller address + event AccessControllerSet(address oldAccessController, address newAccessController); + + /// @notice This event is emitted when a new fee manager is set + /// @param oldFeeManager The old fee manager address + /// @param newFeeManager The new fee manager address + event FeeManagerSet(address oldFeeManager, address newFeeManager); + + /// @notice This error is thrown whenever an address tries + /// to exeecute a transaction that it is not authorized to do so + error AccessForbidden(); + + /// @notice This error is thrown whenever a zero address is passed + error ZeroAddress(); + + /// @notice This error is thrown when trying to set a verifier address + /// for a digest that has already been initialized + /// @param configDigest The digest for the verifier that has + /// already been set + /// @param verifier The address of the verifier the digest was set for + error ConfigDigestAlreadySet(bytes32 configDigest, address verifier); + + /// @notice This error is thrown when trying to set a verifier address that has already been initialized + error VerifierAlreadyInitialized(address verifier); + + /// @notice This error is thrown when the verifier at an address does + /// not conform to the verifier interface + error VerifierInvalid(); + + /// @notice This error is thrown when the fee manager at an address does + /// not conform to the fee manager interface + error FeeManagerInvalid(); + + /// @notice This error is thrown whenever a verifier is not found + /// @param configDigest The digest for which a verifier is not found + error VerifierNotFound(bytes32 configDigest); + + /// @notice This error is thrown whenever billing fails. + error BadVerification(); + + /// @notice Mapping of authorized verifiers + mapping(address => bool) private s_initializedVerifiers; + + /// @notice Mapping between config digests and verifiers + mapping(bytes32 => address) private s_verifiersByConfig; + + /// @notice The contract to control addresses that are allowed to verify reports + AccessControllerInterface public s_accessController; + + /// @notice The contract to control fees for report verification + IVerifierFeeManager public s_feeManager; + + constructor(AccessControllerInterface accessController) ConfirmedOwner(msg.sender) { + s_accessController = accessController; + } + + modifier checkAccess() { + AccessControllerInterface ac = s_accessController; + if (address(ac) != address(0) && !ac.hasAccess(msg.sender, msg.data)) revert AccessForbidden(); + _; + } + + modifier onlyInitializedVerifier() { + if (!s_initializedVerifiers[msg.sender]) revert AccessForbidden(); + _; + } + + modifier onlyValidVerifier(address verifierAddress) { + if (verifierAddress == address(0)) revert ZeroAddress(); + if (!IERC165(verifierAddress).supportsInterface(IVerifier.verify.selector)) revert VerifierInvalid(); + _; + } + + modifier onlyUnsetConfigDigest(bytes32 configDigest) { + address configDigestVerifier = s_verifiersByConfig[configDigest]; + if (configDigestVerifier != address(0)) revert ConfigDigestAlreadySet(configDigest, configDigestVerifier); + _; + } + + /// @inheritdoc TypeAndVersionInterface + function typeAndVersion() external pure override returns (string memory) { + return "VerifierProxy 2.0.0"; + } + + /// @inheritdoc IVerifierProxy + function verify( + bytes calldata payload, + bytes calldata parameterPayload + ) external payable checkAccess returns (bytes memory) { + IVerifierFeeManager feeManager = s_feeManager; + + // Bill the verifier + if (address(feeManager) != address(0)) { + feeManager.processFee{value: msg.value}(payload, parameterPayload, msg.sender); + } + + return _verify(payload); + } + + /// @inheritdoc IVerifierProxy + function verifyBulk( + bytes[] calldata payloads, + bytes calldata parameterPayload + ) external payable checkAccess returns (bytes[] memory verifiedReports) { + IVerifierFeeManager feeManager = s_feeManager; + + // Bill the verifier + if (address(feeManager) != address(0)) { + feeManager.processFeeBulk{value: msg.value}(payloads, parameterPayload, msg.sender); + } + + //verify the reports + verifiedReports = new bytes[](payloads.length); + for (uint256 i; i < payloads.length; ++i) { + verifiedReports[i] = _verify(payloads[i]); + } + + return verifiedReports; + } + + function _verify(bytes calldata payload) internal returns (bytes memory verifiedReport) { + // First 32 bytes of the signed report is the config digest + bytes32 configDigest = bytes32(payload); + address verifierAddress = s_verifiersByConfig[configDigest]; + if (verifierAddress == address(0)) revert VerifierNotFound(configDigest); + + return IVerifier(verifierAddress).verify(payload, msg.sender); + } + + /// @inheritdoc IVerifierProxy + function initializeVerifier(address verifierAddress) external override onlyOwner onlyValidVerifier(verifierAddress) { + if (s_initializedVerifiers[verifierAddress]) revert VerifierAlreadyInitialized(verifierAddress); + + s_initializedVerifiers[verifierAddress] = true; + emit VerifierInitialized(verifierAddress); + } + + /// @inheritdoc IVerifierProxy + function setVerifier( + bytes32 currentConfigDigest, + bytes32 newConfigDigest, + Common.AddressAndWeight[] calldata addressesAndWeights + ) external override onlyUnsetConfigDigest(newConfigDigest) onlyInitializedVerifier { + s_verifiersByConfig[newConfigDigest] = msg.sender; + + // Empty recipients array will be ignored and must be set off chain + if (addressesAndWeights.length > 0) { + if (address(s_feeManager) == address(0)) { + revert ZeroAddress(); + } + + s_feeManager.setFeeRecipients(newConfigDigest, addressesAndWeights); + } + + emit VerifierSet(currentConfigDigest, newConfigDigest, msg.sender); + } + + /// @inheritdoc IVerifierProxy + function unsetVerifier(bytes32 configDigest) external override onlyOwner { + address verifierAddress = s_verifiersByConfig[configDigest]; + if (verifierAddress == address(0)) revert VerifierNotFound(configDigest); + delete s_verifiersByConfig[configDigest]; + emit VerifierUnset(configDigest, verifierAddress); + } + + /// @inheritdoc IVerifierProxy + function getVerifier(bytes32 configDigest) external view override returns (address) { + return s_verifiersByConfig[configDigest]; + } + + /// @inheritdoc IVerifierProxy + function setAccessController(AccessControllerInterface accessController) external onlyOwner { + address oldAccessController = address(s_accessController); + s_accessController = accessController; + emit AccessControllerSet(oldAccessController, address(accessController)); + } + + /// @inheritdoc IVerifierProxy + function setFeeManager(IVerifierFeeManager feeManager) external onlyOwner { + if (address(feeManager) == address(0)) revert ZeroAddress(); + + if ( + !IERC165(feeManager).supportsInterface(IVerifierFeeManager.processFee.selector) || + !IERC165(feeManager).supportsInterface(IVerifierFeeManager.processFeeBulk.selector) + ) revert FeeManagerInvalid(); + + address oldFeeManager = address(s_feeManager); + s_feeManager = IVerifierFeeManager(feeManager); + emit FeeManagerSet(oldFeeManager, address(feeManager)); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IFeeManager.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IFeeManager.sol new file mode 100644 index 00000000000..5ea331ea8fd --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IFeeManager.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {Common} from "../../libraries/Common.sol"; +import {IVerifierFeeManager} from "./IVerifierFeeManager.sol"; + +interface IFeeManager is IERC165, IVerifierFeeManager { + /** + * @notice Calculate the applied fee and the reward from a report. If the sender is a subscriber, they will receive a discount. + * @param subscriber address trying to verify + * @param report report to calculate the fee for + * @param quoteAddress address of the quote payment token + * @return (fee, reward, totalDiscount) fee and the reward data with the discount applied + */ + function getFeeAndReward( + address subscriber, + bytes memory report, + address quoteAddress + ) external returns (Common.Asset memory, Common.Asset memory, uint256); + + /** + * @notice Sets the native surcharge + * @param surcharge surcharge to be paid if paying in native + */ + function setNativeSurcharge(uint64 surcharge) external; + + /** + * @notice Adds a subscriber to the fee manager + * @param subscriber address of the subscriber + * @param feedId feed id to apply the discount to + * @param token token to apply the discount to + * @param discount discount to be applied to the fee + */ + function updateSubscriberDiscount(address subscriber, bytes32 feedId, address token, uint64 discount) external; + + /** + * @notice Withdraws any native or LINK rewards to the owner address + * @param assetAddress address of the asset to withdraw + * @param recipientAddress address to withdraw to + * @param quantity quantity to withdraw + */ + function withdraw(address assetAddress, address recipientAddress, uint192 quantity) external; + + /** + * @notice Returns the link balance of the fee manager + * @return link balance of the fee manager + */ + function linkAvailableForPayment() external returns (uint256); + + /** + * @notice Admin function to pay the LINK deficit for a given config digest + * @param configDigest the config digest to pay the deficit for + */ + function payLinkDeficit(bytes32 configDigest) external; + + /** + * @notice Adds a subscriber to the fee manager + * @param subscriber address of the subscriber + * @param token token to apply the discount to + * @param discount discount to be applied to the fee + */ + function updateSubscriberGlobalDiscount(address subscriber, address token, uint64 discount) external; + + /** + * @notice The structure to hold a fee and reward to verify a report + * @param digest the digest linked to the fee and reward + * @param fee the fee paid to verify the report + * @param reward the reward paid upon verification + & @param appliedDiscount the discount applied to the reward + */ + struct FeeAndReward { + bytes32 configDigest; + Common.Asset fee; + Common.Asset reward; + uint256 appliedDiscount; + } + + /** + * @notice The structure to hold quote metadata + * @param quoteAddress the address of the quote + */ + struct Quote { + address quoteAddress; + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IRewardManager.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IRewardManager.sol new file mode 100644 index 00000000000..f08ce34db29 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IRewardManager.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {Common} from "../../libraries/Common.sol"; + +interface IRewardManager is IERC165 { + /** + * @notice Record the fee received for a particular pool + * @param payments array of structs containing pool id and amount + * @param payee the user the funds should be retrieved from + */ + function onFeePaid(FeePayment[] calldata payments, address payee) external; + + /** + * @notice Claims the rewards in a specific pool + * @param poolIds array of poolIds to claim rewards for + */ + function claimRewards(bytes32[] calldata poolIds) external; + + /** + * @notice Set the RewardRecipients and weights for a specific pool. This should only be called once per pool Id. Else updateRewardRecipients should be used. + * @param poolId poolId to set RewardRecipients and weights for + * @param rewardRecipientAndWeights array of each RewardRecipient and associated weight + */ + function setRewardRecipients(bytes32 poolId, Common.AddressAndWeight[] calldata rewardRecipientAndWeights) external; + + /** + * @notice Updates a subset the reward recipients for a specific poolId. The collective weight of the recipients should add up to the recipients existing weights. Any recipients with a weight of 0 will be removed. + * @param poolId the poolId to update + * @param newRewardRecipients array of new reward recipients + */ + function updateRewardRecipients(bytes32 poolId, Common.AddressAndWeight[] calldata newRewardRecipients) external; + + /** + * @notice Pays all the recipients for each of the pool ids + * @param poolId the pool id to pay recipients for + * @param recipients array of recipients to pay within the pool + */ + function payRecipients(bytes32 poolId, address[] calldata recipients) external; + + /** + * @notice Sets the fee manager. This needs to be done post construction to prevent a circular dependency. + * @param newFeeManager address of the new verifier proxy + */ + function setFeeManager(address newFeeManager) external; + + /** + * @notice Gets a list of pool ids which have reward for a specific recipient. + * @param recipient address of the recipient to get pool ids for + * @param startIndex the index to start from + * @param endIndex the index to stop at + */ + function getAvailableRewardPoolIds( + address recipient, + uint256 startIndex, + uint256 endIndex + ) external view returns (bytes32[] memory); + + /** + * @notice The structure to hold a fee payment notice + * @param poolId the poolId receiving the payment + * @param amount the amount being paid + */ + struct FeePayment { + bytes32 poolId; + uint192 amount; + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifier.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifier.sol new file mode 100644 index 00000000000..304ef31b94f --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifier.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {Common} from "../../libraries/Common.sol"; + +interface IVerifier is IERC165 { + /** + * @notice Verifies that the data encoded has been signed + * correctly by routing to the correct verifier. + * @param signedReport The encoded data to be verified. + * @param sender The address that requested to verify the contract. + * This is only used for logging purposes. + * @dev Verification is typically only done through the proxy contract so + * we can't just use msg.sender to log the requester as the msg.sender + * contract will always be the proxy. + * @return verifierResponse The encoded verified response. + */ + function verify(bytes calldata signedReport, address sender) external returns (bytes memory verifierResponse); + + /** + * @notice sets a configuration and its associated keys and f + * @param configDigest The digest of the configuration we're setting + * @param signers addresses with which oracles sign the reports + * @param f number of faulty oracles the system can tolerate + * @param recipientAddressesAndWeights the addresses and weights of all the recipients to receive rewards + */ + function setConfig( + bytes32 configDigest, + address[] calldata signers, + uint8 f, + Common.AddressAndWeight[] memory recipientAddressesAndWeights + ) external; + + /** + * @notice updates a configuration that has been set + * @param configDigest The digest of the configuration we're updating + * @param prevSigners the existing signers that need to be removed + * @param newSigners the signers to be added + * @param f the newnumber of faulty oracles the system can tolerate + */ + function updateConfig( + bytes32 configDigest, + address[] calldata prevSigners, + address[] calldata newSigners, + uint8 f + ) external; + + /** + * @notice Activates the configuration for a config digest + * @param configDigest The config digest to activate + * @dev This function can be called by the contract admin to activate a configuration. + */ + function activateConfig(bytes32 configDigest) external; + + /** + * @notice Deactivates the configuration for a config digest + * @param configDigest The config digest to deactivate + * @dev This function can be called by the contract admin to deactivate an incorrect configuration. + */ + function deactivateConfig(bytes32 configDigest) external; + + /** + * @notice information about current offchain reporting protocol configuration + * @param configDigest Config Digest to fetch data for + * @return blockNumber block at which this config was set + */ + function latestConfigDetails( + bytes32 configDigest + ) external view returns (uint32 blockNumber); +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierFeeManager.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierFeeManager.sol new file mode 100644 index 00000000000..da3fdfac153 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierFeeManager.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {Common} from "../../libraries/Common.sol"; + +interface IVerifierFeeManager is IERC165 { + /** + * @notice Handles fees for a report from the subscriber and manages rewards + * @param payload report to process the fee for + * @param parameterPayload fee payload + * @param subscriber address of the fee will be applied + */ + function processFee(bytes calldata payload, bytes calldata parameterPayload, address subscriber) external payable; + + /** + * @notice Processes the fees for each report in the payload, billing the subscriber and paying the reward manager + * @param payloads reports to process + * @param parameterPayload fee payload + * @param subscriber address of the user to process fee for + */ + function processFeeBulk( + bytes[] calldata payloads, + bytes calldata parameterPayload, + address subscriber + ) external payable; + + /** + * @notice Sets the fee recipients according to the fee manager + * @param configDigest digest of the configuration + * @param rewardRecipientAndWeights the address and weights of all the recipients to receive rewards + */ + function setFeeRecipients( + bytes32 configDigest, + Common.AddressAndWeight[] calldata rewardRecipientAndWeights + ) external; +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierProxy.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierProxy.sol new file mode 100644 index 00000000000..6609e4869ae --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierProxy.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Common} from "../../libraries/Common.sol"; +import {AccessControllerInterface} from "../../../shared/interfaces/AccessControllerInterface.sol"; +import {IVerifierFeeManager} from "./IVerifierFeeManager.sol"; + +interface IVerifierProxy { + /** + * @notice Verifies that the data encoded has been signed + * correctly by routing to the correct verifier, and bills the user if applicable. + * @param payload The encoded data to be verified, including the signed + * report. + * @param parameterPayload fee metadata for billing + * @return verifierResponse The encoded report from the verifier. + */ + function verify( + bytes calldata payload, + bytes calldata parameterPayload + ) external payable returns (bytes memory verifierResponse); + + /** + * @notice Bulk verifies that the data encoded has been signed + * correctly by routing to the correct verifier, and bills the user if applicable. + * @param payloads The encoded payloads to be verified, including the signed + * report. + * @param parameterPayload fee metadata for billing + * @return verifiedReports The encoded reports from the verifier. + */ + function verifyBulk( + bytes[] calldata payloads, + bytes calldata parameterPayload + ) external payable returns (bytes[] memory verifiedReports); + + /** + * @notice Sets the verifier address initially, allowing `setVerifier` to be set by this Verifier in the future + * @param verifierAddress The address of the verifier contract to initialize + */ + function initializeVerifier(address verifierAddress) external; + + /** + * @notice Sets a new verifier for a config digest + * @param currentConfigDigest The current config digest + * @param newConfigDigest The config digest to set + * @param addressesAndWeights The addresses and weights of reward recipients + * reports for a given config digest. + */ + function setVerifier( + bytes32 currentConfigDigest, + bytes32 newConfigDigest, + Common.AddressAndWeight[] memory addressesAndWeights + ) external; + + /** + * @notice Removes a verifier for a given config digest + * @param configDigest The config digest of the verifier to remove + */ + function unsetVerifier(bytes32 configDigest) external; + + /** + * @notice Retrieves the verifier address that verifies reports + * for a config digest. + * @param configDigest The config digest to query for + * @return verifierAddress The address of the verifier contract that verifies + * reports for a given config digest. + */ + function getVerifier(bytes32 configDigest) external view returns (address verifierAddress); + + /** + * @notice Called by the admin to set an access controller contract + * @param accessController The new access controller to set + */ + function setAccessController(AccessControllerInterface accessController) external; + + /** + * @notice Updates the fee manager + * @param feeManager The new fee manager + */ + function setFeeManager(IVerifierFeeManager feeManager) external; +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/BaseFeeManager.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/BaseFeeManager.t.sol new file mode 100644 index 00000000000..764bf4f088f --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/BaseFeeManager.t.sol @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {FeeManager} from "../../FeeManager.sol"; +import {RewardManager} from "../../RewardManager.sol"; +import {Common} from "../../../libraries/Common.sol"; +import {ERC20Mock} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/mocks/ERC20Mock.sol"; +import {WERC20Mock} from "../../../../shared/mocks/WERC20Mock.sol"; +import {IRewardManager} from "../../interfaces/IRewardManager.sol"; +import {FeeManagerProxy} from "../mocks/FeeManagerProxy.sol"; + +/** + * @title BaseFeeManagerTest + * @author Michael Fletcher + * @notice Base class for all feeManager tests + * @dev This contract is intended to be inherited from and not used directly. It contains functionality to setup the feeManager + */ +contract BaseFeeManagerTest is Test { + //contracts + FeeManager internal feeManager; + RewardManager internal rewardManager; + FeeManagerProxy internal feeManagerProxy; + + ERC20Mock internal link; + WERC20Mock internal native; + + //erc20 config + uint256 internal constant DEFAULT_LINK_MINT_QUANTITY = 100 ether; + uint256 internal constant DEFAULT_NATIVE_MINT_QUANTITY = 100 ether; + + //contract owner + address internal constant INVALID_ADDRESS = address(0); + address internal constant ADMIN = address(uint160(uint256(keccak256("ADMIN")))); + address internal constant USER = address(uint160(uint256(keccak256("USER")))); + address internal constant PROXY = address(uint160(uint256(keccak256("PROXY")))); + + //version masks + bytes32 internal constant V_MASK = 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + bytes32 internal constant V1_BITMASK = 0x0001000000000000000000000000000000000000000000000000000000000000; + bytes32 internal constant V2_BITMASK = 0x0002000000000000000000000000000000000000000000000000000000000000; + bytes32 internal constant V3_BITMASK = 0x0003000000000000000000000000000000000000000000000000000000000000; + + //feed ids & config digests + bytes32 internal constant DEFAULT_FEED_1_V1 = (keccak256("ETH-USD") & V_MASK) | V1_BITMASK; + bytes32 internal constant DEFAULT_FEED_1_V2 = (keccak256("ETH-USD") & V_MASK) | V2_BITMASK; + bytes32 internal constant DEFAULT_FEED_1_V3 = (keccak256("ETH-USD") & V_MASK) | V3_BITMASK; + + bytes32 internal constant DEFAULT_FEED_2_V3 = (keccak256("LINK-USD") & V_MASK) | V3_BITMASK; + bytes32 internal constant DEFAULT_CONFIG_DIGEST = keccak256("DEFAULT_CONFIG_DIGEST"); + + //report + uint256 internal constant DEFAULT_REPORT_LINK_FEE = 1e10; + uint256 internal constant DEFAULT_REPORT_NATIVE_FEE = 1e12; + + //rewards + uint64 internal constant FEE_SCALAR = 1e18; + + address internal constant NATIVE_WITHDRAW_ADDRESS = address(0); + + //the selector for each error + bytes4 internal immutable INVALID_DISCOUNT_ERROR = FeeManager.InvalidDiscount.selector; + bytes4 internal immutable INVALID_ADDRESS_ERROR = FeeManager.InvalidAddress.selector; + bytes4 internal immutable INVALID_SURCHARGE_ERROR = FeeManager.InvalidSurcharge.selector; + bytes4 internal immutable EXPIRED_REPORT_ERROR = FeeManager.ExpiredReport.selector; + bytes4 internal immutable INVALID_DEPOSIT_ERROR = FeeManager.InvalidDeposit.selector; + bytes4 internal immutable INVALID_QUOTE_ERROR = FeeManager.InvalidQuote.selector; + bytes4 internal immutable UNAUTHORIZED_ERROR = FeeManager.Unauthorized.selector; + bytes internal constant ONLY_CALLABLE_BY_OWNER_ERROR = "Only callable by owner"; + bytes internal constant INSUFFICIENT_ALLOWANCE_ERROR = "ERC20: insufficient allowance"; + bytes4 internal immutable ZERO_DEFICIT = FeeManager.ZeroDeficit.selector; + + //events emitted + event SubscriberDiscountUpdated(address indexed subscriber, bytes32 indexed feedId, address token, uint64 discount); + event NativeSurchargeUpdated(uint64 newSurcharge); + event InsufficientLink(IRewardManager.FeePayment[] feesAndRewards); + event Withdraw(address adminAddress, address recipient, address assetAddress, uint192 quantity); + event LinkDeficitCleared(bytes32 indexed configDigest, uint256 linkQuantity); + event DiscountApplied( + bytes32 indexed configDigest, + address indexed subscriber, + Common.Asset fee, + Common.Asset reward, + uint256 appliedDiscountQuantity + ); + + function setUp() public virtual { + //change to admin user + vm.startPrank(ADMIN); + + //init required contracts + _initializeContracts(); + } + + function _initializeContracts() internal { + link = new ERC20Mock("LINK", "LINK", ADMIN, 0); + native = new WERC20Mock(); + + feeManagerProxy = new FeeManagerProxy(); + rewardManager = new RewardManager(address(link)); + feeManager = new FeeManager(address(link), address(native), address(feeManagerProxy), address(rewardManager)); + + //link the feeManager to the proxy + feeManagerProxy.setFeeManager(feeManager); + + //link the feeManager to the reward manager + rewardManager.setFeeManager(address(feeManager)); + + //mint some tokens to the admin + link.mint(ADMIN, DEFAULT_LINK_MINT_QUANTITY); + native.mint(ADMIN, DEFAULT_NATIVE_MINT_QUANTITY); + vm.deal(ADMIN, DEFAULT_NATIVE_MINT_QUANTITY); + + //mint some tokens to the user + link.mint(USER, DEFAULT_LINK_MINT_QUANTITY); + native.mint(USER, DEFAULT_NATIVE_MINT_QUANTITY); + vm.deal(USER, DEFAULT_NATIVE_MINT_QUANTITY); + + //mint some tokens to the proxy + link.mint(PROXY, DEFAULT_LINK_MINT_QUANTITY); + native.mint(PROXY, DEFAULT_NATIVE_MINT_QUANTITY); + vm.deal(PROXY, DEFAULT_NATIVE_MINT_QUANTITY); + } + + function setSubscriberDiscount( + address subscriber, + bytes32 feedId, + address token, + uint256 discount, + address sender + ) internal { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //set the discount + feeManager.updateSubscriberDiscount(subscriber, feedId, token, uint64(discount)); + + //change back to the original address + changePrank(originalAddr); + } + + function setSubscriberGlobalDiscount(address subscriber, address token, uint256 discount, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //set the discount + feeManager.updateSubscriberGlobalDiscount(subscriber, token, uint64(discount)); + + //change back to the original address + changePrank(originalAddr); + } + + function setNativeSurcharge(uint256 surcharge, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //set the surcharge + feeManager.setNativeSurcharge(uint64(surcharge)); + + //change back to the original address + changePrank(originalAddr); + } + + // solium-disable-next-line no-unused-vars + function getFee(bytes memory report, address quote, address subscriber) public view returns (Common.Asset memory) { + //get the fee + (Common.Asset memory fee, , ) = feeManager.getFeeAndReward(subscriber, report, quote); + + return fee; + } + + function getReward(bytes memory report, address quote, address subscriber) public view returns (Common.Asset memory) { + //get the reward + (, Common.Asset memory reward, ) = feeManager.getFeeAndReward(subscriber, report, quote); + + return reward; + } + + function getAppliedDiscount(bytes memory report, address quote, address subscriber) public view returns (uint256) { + //get the reward + (, , uint256 appliedDiscount) = feeManager.getFeeAndReward(subscriber, report, quote); + + return appliedDiscount; + } + + function getV1Report(bytes32 feedId) public pure returns (bytes memory) { + return abi.encode(feedId, uint32(0), int192(0), int192(0), int192(0), uint64(0), bytes32(0), uint64(0), uint64(0)); + } + + function getV2Report(bytes32 feedId) public view returns (bytes memory) { + return + abi.encode( + feedId, + uint32(0), + uint32(0), + uint192(DEFAULT_REPORT_NATIVE_FEE), + uint192(DEFAULT_REPORT_LINK_FEE), + uint32(block.timestamp), + int192(0) + ); + } + + function getV3Report(bytes32 feedId) public view returns (bytes memory) { + return + abi.encode( + feedId, + uint32(0), + uint32(0), + uint192(DEFAULT_REPORT_NATIVE_FEE), + uint192(DEFAULT_REPORT_LINK_FEE), + uint32(block.timestamp), + int192(0), + int192(0), + int192(0) + ); + } + + function getV3ReportWithCustomExpiryAndFee( + bytes32 feedId, + uint256 expiry, + uint256 linkFee, + uint256 nativeFee + ) public pure returns (bytes memory) { + return + abi.encode( + feedId, + uint32(0), + uint32(0), + uint192(nativeFee), + uint192(linkFee), + uint32(expiry), + int192(0), + int192(0), + int192(0) + ); + } + + function getLinkQuote() public view returns (address) { + return address(link); + } + + function getNativeQuote() public view returns (address) { + return address(native); + } + + function withdraw(address assetAddress, address recipient, uint256 amount, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //set the surcharge + feeManager.withdraw(assetAddress, recipient, uint192(amount)); + + //change back to the original address + changePrank(originalAddr); + } + + function getLinkBalance(address balanceAddress) public view returns (uint256) { + return link.balanceOf(balanceAddress); + } + + function getNativeBalance(address balanceAddress) public view returns (uint256) { + return native.balanceOf(balanceAddress); + } + + function getNativeUnwrappedBalance(address balanceAddress) public view returns (uint256) { + return balanceAddress.balance; + } + + function mintLink(address recipient, uint256 amount) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(ADMIN); + + //mint the link to the recipient + link.mint(recipient, amount); + + //change back to the original address + changePrank(originalAddr); + } + + function mintNative(address recipient, uint256 amount, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //mint the native to the recipient + native.mint(recipient, amount); + + //change back to the original address + changePrank(originalAddr); + } + + function issueUnwrappedNative(address recipient, uint256 quantity) public { + vm.deal(recipient, quantity); + } + + function ProcessFeeAsUser( + bytes memory payload, + address subscriber, + address tokenAddress, + uint256 wrappedNativeValue, + address sender + ) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //process the fee + feeManager.processFee{value: wrappedNativeValue}(payload, abi.encode(tokenAddress), subscriber); + + //change ProcessFeeAsUserback to the original address + changePrank(originalAddr); + } + + function processFee(bytes memory payload, address subscriber, address feeAddress, uint256 wrappedNativeValue) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(subscriber); + + //process the fee + feeManagerProxy.processFee{value: wrappedNativeValue}(payload, abi.encode(feeAddress)); + + //change back to the original address + changePrank(originalAddr); + } + + function processFee( + bytes[] memory payloads, + address subscriber, + address feeAddress, + uint256 wrappedNativeValue + ) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(subscriber); + + //process the fee + feeManagerProxy.processFeeBulk{value: wrappedNativeValue}(payloads, abi.encode(feeAddress)); + + //change back to the original address + changePrank(originalAddr); + } + + function getPayload(bytes memory reportPayload) public pure returns (bytes memory) { + return abi.encode([DEFAULT_CONFIG_DIGEST, 0, 0], reportPayload, new bytes32[](1), new bytes32[](1), bytes32("")); + } + + function approveLink(address spender, uint256 quantity, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //approve the link to be transferred + link.approve(spender, quantity); + + //change back to the original address + changePrank(originalAddr); + } + + function approveNative(address spender, uint256 quantity, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //approve the link to be transferred + native.approve(spender, quantity); + + //change back to the original address + changePrank(originalAddr); + } + + function payLinkDeficit(bytes32 configDigest, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //approve the link to be transferred + feeManager.payLinkDeficit(configDigest); + + //change back to the original address + changePrank(originalAddr); + } + + function getLinkDeficit(bytes32 configDigest) public view returns (uint256) { + return feeManager.s_linkDeficit(configDigest); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.general.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.general.t.sol new file mode 100644 index 00000000000..571d34823f4 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.general.t.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import "./BaseFeeManager.t.sol"; + +/** + * @title BaseFeeManagerTest + * @author Michael Fletcher + * @notice This contract will test the setup functionality of the feemanager + */ +contract FeeManagerProcessFeeTest is BaseFeeManagerTest { + function setUp() public override { + super.setUp(); + } + + function test_WithdrawERC20() public { + //simulate a fee + mintLink(address(feeManager), DEFAULT_LINK_MINT_QUANTITY); + + //get the balances to ne used for comparison + uint256 contractBalance = getLinkBalance(address(feeManager)); + uint256 adminBalance = getLinkBalance(ADMIN); + + //the amount to withdraw + uint256 withdrawAmount = contractBalance / 2; + + //withdraw some balance + withdraw(address(link), ADMIN, withdrawAmount, ADMIN); + + //check the balance has been reduced + uint256 newContractBalance = getLinkBalance(address(feeManager)); + uint256 newAdminBalance = getLinkBalance(ADMIN); + + //check the balance is greater than zero + assertGt(newContractBalance, 0); + //check the balance has been reduced by the correct amount + assertEq(newContractBalance, contractBalance - withdrawAmount); + //check the admin balance has increased by the correct amount + assertEq(newAdminBalance, adminBalance + withdrawAmount); + } + + function test_WithdrawUnwrappedNative() public { + //issue funds straight to the contract to bypass the lack of fallback function + issueUnwrappedNative(address(feeManager), DEFAULT_NATIVE_MINT_QUANTITY); + + //get the balances to be used for comparison + uint256 contractBalance = getNativeUnwrappedBalance(address(feeManager)); + uint256 adminBalance = getNativeUnwrappedBalance(ADMIN); + + //the amount to withdraw + uint256 withdrawAmount = contractBalance / 2; + + //withdraw some balance + withdraw(NATIVE_WITHDRAW_ADDRESS, ADMIN, withdrawAmount, ADMIN); + + //check the balance has been reduced + uint256 newContractBalance = getNativeUnwrappedBalance(address(feeManager)); + uint256 newAdminBalance = getNativeUnwrappedBalance(ADMIN); + + //check the balance is greater than zero + assertGt(newContractBalance, 0); + //check the balance has been reduced by the correct amount + assertEq(newContractBalance, contractBalance - withdrawAmount); + //check the admin balance has increased by the correct amount + assertEq(newAdminBalance, adminBalance + withdrawAmount); + } + + function test_WithdrawNonAdminAddr() public { + //simulate a fee + mintLink(address(feeManager), DEFAULT_LINK_MINT_QUANTITY); + + //should revert if not admin + vm.expectRevert(ONLY_CALLABLE_BY_OWNER_ERROR); + + //withdraw some balance + withdraw(address(link), ADMIN, DEFAULT_LINK_MINT_QUANTITY, USER); + } + + function test_eventIsEmittedAfterSurchargeIsSet() public { + //native surcharge + uint64 nativeSurcharge = FEE_SCALAR / 5; + + //expect an emit + vm.expectEmit(); + + //emit the event that is expected to be emitted + emit NativeSurchargeUpdated(nativeSurcharge); + + //set the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + } + + function test_subscriberDiscountEventIsEmittedOnUpdate() public { + //native surcharge + uint64 discount = FEE_SCALAR / 3; + + //an event should be emitted + vm.expectEmit(); + + //emit the event that is expected to be emitted + emit SubscriberDiscountUpdated(USER, DEFAULT_FEED_1_V3, address(native), discount); + + //set the surcharge + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), discount, ADMIN); + } + + function test_eventIsEmittedUponWithdraw() public { + //simulate a fee + mintLink(address(feeManager), DEFAULT_LINK_MINT_QUANTITY); + + //the amount to withdraw + uint192 withdrawAmount = 1; + + //expect an emit + vm.expectEmit(); + + //the event to be emitted + emit Withdraw(ADMIN, ADMIN, address(link), withdrawAmount); + + //withdraw some balance + withdraw(address(link), ADMIN, withdrawAmount, ADMIN); + } + + function test_linkAvailableForPaymentReturnsLinkBalance() public { + //simulate a deposit of link for the conversion pool + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //check there's a balance + assertGt(getLinkBalance(address(feeManager)), 0); + + //check the link available for payment is the link balance + assertEq(feeManager.linkAvailableForPayment(), getLinkBalance(address(feeManager))); + } + + function test_payLinkDeficit() public { + //get the default payload + bytes memory payload = getPayload(getV2Report(DEFAULT_FEED_1_V3)); + + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE, USER); + + //not enough funds in the reward pool should trigger an insufficient link event + vm.expectEmit(); + + IRewardManager.FeePayment[] memory contractFees = new IRewardManager.FeePayment[](1); + contractFees[0] = IRewardManager.FeePayment(DEFAULT_CONFIG_DIGEST, uint192(DEFAULT_REPORT_LINK_FEE)); + + emit InsufficientLink(contractFees); + + //process the fee + processFee(payload, USER, address(native), 0); + + //double check the rewardManager balance is 0 + assertEq(getLinkBalance(address(rewardManager)), 0); + + //simulate a deposit of link to cover the deficit + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + vm.expectEmit(); + emit LinkDeficitCleared(DEFAULT_CONFIG_DIGEST, DEFAULT_REPORT_LINK_FEE); + + //pay the deficit which will transfer link from the rewardManager to the rewardManager + payLinkDeficit(DEFAULT_CONFIG_DIGEST, ADMIN); + + //check the rewardManager received the link + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + } + + function test_payLinkDeficitTwice() public { + //get the default payload + bytes memory payload = getPayload(getV2Report(DEFAULT_FEED_1_V3)); + + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE, USER); + + //not enough funds in the reward pool should trigger an insufficient link event + vm.expectEmit(); + + IRewardManager.FeePayment[] memory contractFees = new IRewardManager.FeePayment[](1); + contractFees[0] = IRewardManager.FeePayment(DEFAULT_CONFIG_DIGEST, uint192(DEFAULT_REPORT_LINK_FEE)); + + //emit the event that is expected to be emitted + emit InsufficientLink(contractFees); + + //process the fee + processFee(payload, USER, address(native), 0); + + //double check the rewardManager balance is 0 + assertEq(getLinkBalance(address(rewardManager)), 0); + + //simulate a deposit of link to cover the deficit + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + vm.expectEmit(); + emit LinkDeficitCleared(DEFAULT_CONFIG_DIGEST, DEFAULT_REPORT_LINK_FEE); + + //pay the deficit which will transfer link from the rewardManager to the rewardManager + payLinkDeficit(DEFAULT_CONFIG_DIGEST, ADMIN); + + //check the rewardManager received the link + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + //paying again should revert with 0 + vm.expectRevert(ZERO_DEFICIT); + + payLinkDeficit(DEFAULT_CONFIG_DIGEST, ADMIN); + } + + function test_payLinkDeficitPaysAllFeesProcessed() public { + //get the default payload + bytes memory payload = getPayload(getV2Report(DEFAULT_FEED_1_V3)); + + //approve the native to be transferred from the user + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE * 2, USER); + + //processing the fee will transfer the native from the user to the feeManager + processFee(payload, USER, address(native), 0); + processFee(payload, USER, address(native), 0); + + //check the deficit has been increased twice + assertEq(getLinkDeficit(DEFAULT_CONFIG_DIGEST), DEFAULT_REPORT_LINK_FEE * 2); + + //double check the rewardManager balance is 0 + assertEq(getLinkBalance(address(rewardManager)), 0); + + //simulate a deposit of link to cover the deficit + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE * 2); + + vm.expectEmit(); + emit LinkDeficitCleared(DEFAULT_CONFIG_DIGEST, DEFAULT_REPORT_LINK_FEE * 2); + + //pay the deficit which will transfer link from the rewardManager to the rewardManager + payLinkDeficit(DEFAULT_CONFIG_DIGEST, ADMIN); + + //check the rewardManager received the link + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE * 2); + } + + function test_payLinkDeficitOnlyCallableByAdmin() public { + vm.expectRevert(ONLY_CALLABLE_BY_OWNER_ERROR); + + payLinkDeficit(DEFAULT_CONFIG_DIGEST, USER); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.getFeeAndReward.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.getFeeAndReward.t.sol new file mode 100644 index 00000000000..68b5f9c95a9 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.getFeeAndReward.t.sol @@ -0,0 +1,720 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Common} from "../../../libraries/Common.sol"; +import "./BaseFeeManager.t.sol"; + +/** + * @title BaseFeeManagerTest + * @author Michael Fletcher + * @notice This contract will test the functionality of the feeManager's getFeeAndReward + */ +contract FeeManagerProcessFeeTest is BaseFeeManagerTest { + function test_baseFeeIsAppliedForNative() public view { + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be the default + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE); + } + + function test_baseFeeIsAppliedForLink() public view { + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be the default + assertEq(fee.amount, DEFAULT_REPORT_LINK_FEE); + } + + function test_discountAIsNotAppliedWhenSetForOtherUsers() public { + //set the subscriber discount for another user + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), INVALID_ADDRESS); + + //fee should be the default + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE); + } + + function test_discountIsNotAppliedForInvalidTokenAddress() public { + //should revert with invalid address as it's not a configured token + vm.expectRevert(INVALID_ADDRESS_ERROR); + + //set the subscriber discount for another user + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, INVALID_ADDRESS, FEE_SCALAR / 2, ADMIN); + } + + function test_discountIsAppliedForLink() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be half the default + assertEq(fee.amount, DEFAULT_REPORT_LINK_FEE / 2); + } + + function test_DiscountIsAppliedForNative() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be half the default + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE / 2); + } + + function test_discountIsNoLongerAppliedAfterRemoving() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be half the default + assertEq(fee.amount, DEFAULT_REPORT_LINK_FEE / 2); + + //remove the discount + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), 0, ADMIN); + + //get the fee required by the feeManager + fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be the default + assertEq(fee.amount, DEFAULT_REPORT_LINK_FEE); + } + + function test_surchargeIsApplied() public { + //native surcharge + uint256 nativeSurcharge = FEE_SCALAR / 5; + + //set the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected surcharge + uint256 expectedSurcharge = ((DEFAULT_REPORT_NATIVE_FEE * nativeSurcharge) / FEE_SCALAR); + + //expected fee should the base fee offset by the surcharge and discount + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE + expectedSurcharge); + } + + function test_surchargeIsNotAppliedForLinkFee() public { + //native surcharge + uint256 nativeSurcharge = FEE_SCALAR / 5; + + //set the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be the default + assertEq(fee.amount, DEFAULT_REPORT_LINK_FEE); + } + + function test_surchargeIsNoLongerAppliedAfterRemoving() public { + //native surcharge + uint256 nativeSurcharge = FEE_SCALAR / 5; + + //set the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected surcharge + uint256 expectedSurcharge = ((DEFAULT_REPORT_NATIVE_FEE * nativeSurcharge) / FEE_SCALAR); + + //expected fee should be the base fee offset by the surcharge and discount + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE + expectedSurcharge); + + //remove the surcharge + setNativeSurcharge(0, ADMIN); + + //get the fee required by the feeManager + fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be the default + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE); + } + + function test_feeIsUpdatedAfterNewSurchargeIsApplied() public { + //native surcharge + uint256 nativeSurcharge = FEE_SCALAR / 5; + + //set the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected surcharge + uint256 expectedSurcharge = ((DEFAULT_REPORT_NATIVE_FEE * nativeSurcharge) / FEE_SCALAR); + + //expected fee should the base fee offset by the surcharge and discount + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE + expectedSurcharge); + + //change the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + + //get the fee required by the feeManager + fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected surcharge + expectedSurcharge = ((DEFAULT_REPORT_NATIVE_FEE * nativeSurcharge) / FEE_SCALAR); + + //expected fee should the base fee offset by the surcharge and discount + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE + expectedSurcharge); + } + + function test_surchargeIsAppliedForNativeFeeWithDiscount() public { + //native surcharge + uint256 nativeSurcharge = FEE_SCALAR / 5; + + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //set the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected surcharge quantity + uint256 expectedSurcharge = ((DEFAULT_REPORT_NATIVE_FEE * nativeSurcharge) / FEE_SCALAR); + + //calculate the expected discount quantity + uint256 expectedDiscount = ((DEFAULT_REPORT_NATIVE_FEE + expectedSurcharge) / 2); + + //expected fee should the base fee offset by the surcharge and discount + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE + expectedSurcharge - expectedDiscount); + } + + function test_emptyQuoteRevertsWithError() public { + //expect a revert + vm.expectRevert(INVALID_QUOTE_ERROR); + + //get the fee required by the feeManager + getFee(getV3Report(DEFAULT_FEED_1_V3), address(0), USER); + } + + function test_nativeSurcharge100Percent() public { + //set the surcharge + setNativeSurcharge(FEE_SCALAR, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be twice the base fee + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE * 2); + } + + function test_nativeSurcharge0Percent() public { + //set the surcharge + setNativeSurcharge(0, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should base fee + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE); + } + + function test_nativeSurchargeCannotExceed100Percent() public { + //should revert if surcharge is greater than 100% + vm.expectRevert(INVALID_SURCHARGE_ERROR); + + //set the surcharge above the max + setNativeSurcharge(FEE_SCALAR + 1, ADMIN); + } + + function test_discountIsAppliedWith100PercentSurcharge() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //set the surcharge + setNativeSurcharge(FEE_SCALAR, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected discount quantity + uint256 expectedDiscount = DEFAULT_REPORT_NATIVE_FEE; + + //fee should be twice the surcharge minus the discount + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE * 2 - expectedDiscount); + } + + function test_feeIsZeroWith100PercentDiscount() public { + //set the subscriber discount to 100% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be zero + assertEq(fee.amount, 0); + } + + function test_feeIsUpdatedAfterDiscountIsRemoved() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected discount quantity + uint256 expectedDiscount = DEFAULT_REPORT_NATIVE_FEE / 2; + + //fee should be 50% of the base fee + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE - expectedDiscount); + + //remove the discount + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), 0, ADMIN); + + //get the fee required by the feeManager + fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be the base fee + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE); + } + + function test_feeIsUpdatedAfterNewDiscountIsApplied() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected discount quantity + uint256 expectedDiscount = DEFAULT_REPORT_NATIVE_FEE / 2; + + //fee should be 50% of the base fee + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE - expectedDiscount); + + //change the discount to 25% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 4, ADMIN); + + //get the fee required by the feeManager + fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //expected discount is now 25% + expectedDiscount = DEFAULT_REPORT_NATIVE_FEE / 4; + + //fee should be the base fee minus the expected discount + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE - expectedDiscount); + } + + function test_setDiscountOver100Percent() public { + //should revert with invalid discount + vm.expectRevert(INVALID_DISCOUNT_ERROR); + + //set the subscriber discount to over 100% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR + 1, ADMIN); + } + + function test_surchargeIsNotAppliedWith100PercentDiscount() public { + //native surcharge + uint256 nativeSurcharge = FEE_SCALAR / 5; + + //set the subscriber discount to 100% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR, ADMIN); + + //set the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be zero + assertEq(fee.amount, 0); + } + + function test_nonAdminUserCanNotSetDiscount() public { + //should revert with unauthorized + vm.expectRevert(ONLY_CALLABLE_BY_OWNER_ERROR); + + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR, USER); + } + + function test_surchargeFeeRoundsUpWhenUneven() public { + //native surcharge + uint256 nativeSurcharge = FEE_SCALAR / 3; + + //set the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected surcharge quantity + uint256 expectedSurcharge = (DEFAULT_REPORT_NATIVE_FEE * nativeSurcharge) / FEE_SCALAR; + + //expected fee should the base fee offset by the expected surcharge + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE + expectedSurcharge + 1); + } + + function test_discountFeeRoundsDownWhenUneven() public { + //native surcharge + uint256 discount = FEE_SCALAR / 3; + + //set the subscriber discount to 33.333% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), discount, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected quantity + uint256 expectedDiscount = ((DEFAULT_REPORT_NATIVE_FEE * discount) / FEE_SCALAR); + + //expected fee should the base fee offset by the expected surcharge + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE - expectedDiscount); + } + + function test_reportWithNoExpiryOrFeeReturnsZero() public view { + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV1Report(DEFAULT_FEED_1_V1), getNativeQuote(), USER); + + //fee should be zero + assertEq(fee.amount, 0); + } + + function test_correctDiscountIsAppliedWhenBothTokensAreDiscounted() public { + //set the subscriber and native discounts + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 4, ADMIN); + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager for both tokens + Common.Asset memory linkFee = getFee(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + Common.Asset memory nativeFee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //calculate the expected discount quantity for each token + uint256 expectedDiscountLink = (DEFAULT_REPORT_LINK_FEE * FEE_SCALAR) / 4 / FEE_SCALAR; + uint256 expectedDiscountNative = (DEFAULT_REPORT_NATIVE_FEE * FEE_SCALAR) / 2 / FEE_SCALAR; + + //check the fee calculation for each token + assertEq(linkFee.amount, DEFAULT_REPORT_LINK_FEE - expectedDiscountLink); + assertEq(nativeFee.amount, DEFAULT_REPORT_NATIVE_FEE - expectedDiscountNative); + } + + function test_discountIsNotAppliedToOtherFeeds() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_2_V3), getNativeQuote(), USER); + + //fee should be the base fee + assertEq(fee.amount, DEFAULT_REPORT_NATIVE_FEE); + } + + function test_noFeeIsAppliedWhenReportHasZeroFee() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee( + getV3ReportWithCustomExpiryAndFee(DEFAULT_FEED_1_V3, uint32(block.timestamp), 0, 0), + getNativeQuote(), + USER + ); + + //fee should be zero + assertEq(fee.amount, 0); + } + + function test_noFeeIsAppliedWhenReportHasZeroFeeAndDiscountAndSurchargeIsSet() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //set the surcharge + setNativeSurcharge(FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee( + getV3ReportWithCustomExpiryAndFee(DEFAULT_FEED_1_V3, uint32(block.timestamp), 0, 0), + getNativeQuote(), + USER + ); + + //fee should be zero + assertEq(fee.amount, 0); + } + + function test_nativeSurchargeEventIsEmittedOnUpdate() public { + //native surcharge + uint64 nativeSurcharge = FEE_SCALAR / 3; + + //an event should be emitted + vm.expectEmit(); + + //emit the event that is expected to be emitted + emit NativeSurchargeUpdated(nativeSurcharge); + + //set the surcharge + setNativeSurcharge(nativeSurcharge, ADMIN); + } + + function test_getBaseRewardWithLinkQuote() public view { + //get the fee required by the feeManager + Common.Asset memory reward = getReward(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //the reward should equal the base fee + assertEq(reward.amount, DEFAULT_REPORT_LINK_FEE); + } + + function test_getRewardWithLinkQuoteAndLinkDiscount() public { + //set the link discount + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory reward = getReward(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //the reward should equal the discounted base fee + assertEq(reward.amount, DEFAULT_REPORT_LINK_FEE / 2); + } + + function test_getRewardWithNativeQuote() public view { + //get the fee required by the feeManager + Common.Asset memory reward = getReward(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //the reward should equal the base fee in link + assertEq(reward.amount, DEFAULT_REPORT_LINK_FEE); + } + + function test_getRewardWithNativeQuoteAndSurcharge() public { + //set the native surcharge + setNativeSurcharge(FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory reward = getReward(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //the reward should equal the base fee in link regardless of the surcharge + assertEq(reward.amount, DEFAULT_REPORT_LINK_FEE); + } + + function test_getRewardWithLinkDiscount() public { + //set the link discount + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 2, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory reward = getReward(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //the reward should equal the discounted base fee + assertEq(reward.amount, DEFAULT_REPORT_LINK_FEE / 2); + } + + function test_getLinkFeeIsRoundedUp() public { + //set the link discount + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 3, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //the reward should equal .66% + 1 of the base fee due to a 33% discount rounded up + assertEq(fee.amount, (DEFAULT_REPORT_LINK_FEE * 2) / 3 + 1); + } + + function test_getLinkRewardIsSameAsFee() public { + //set the link discount + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 3, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + Common.Asset memory reward = getReward(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //check the reward is in link + assertEq(fee.assetAddress, address(link)); + + //the reward should equal .66% of the base fee due to a 33% discount rounded down + assertEq(reward.amount, fee.amount); + } + + function test_getLinkRewardWithNativeQuoteAndSurchargeWithLinkDiscount() public { + //set the native surcharge + setNativeSurcharge(FEE_SCALAR / 2, ADMIN); + + //set the link discount + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 3, ADMIN); + + //get the fee required by the feeManager + Common.Asset memory reward = getReward(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //the reward should equal the base fee in link regardless of the surcharge + assertEq(reward.amount, DEFAULT_REPORT_LINK_FEE); + } + + function test_testRevertIfReportHasExpired() public { + //expect a revert + vm.expectRevert(EXPIRED_REPORT_ERROR); + + //get the fee required by the feeManager + getFee( + getV3ReportWithCustomExpiryAndFee( + DEFAULT_FEED_1_V3, + block.timestamp - 1, + DEFAULT_REPORT_LINK_FEE, + DEFAULT_REPORT_NATIVE_FEE + ), + getNativeQuote(), + USER + ); + } + + function test_discountIsReturnedForLink() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 2, ADMIN); + + //get the fee applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 2); + } + + function test_DiscountIsReturnedForNative() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //get the discount applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 2); + } + + function test_DiscountIsReturnedForNativeWithSurcharge() public { + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //set the surcharge + setNativeSurcharge(FEE_SCALAR / 5, ADMIN); + + //get the discount applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 2); + } + + function test_GlobalDiscountWithNative() public { + //set the global discount to 50% + setSubscriberGlobalDiscount(USER, address(native), FEE_SCALAR / 2, ADMIN); + + //get the discount applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 2); + } + + function test_GlobalDiscountWithLink() public { + //set the global discount to 50% + setSubscriberGlobalDiscount(USER, address(link), FEE_SCALAR / 2, ADMIN); + + //get the discount applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 2); + } + + function test_GlobalDiscountWithNativeAndLink() public { + //set the global discount to 50% + setSubscriberGlobalDiscount(USER, address(native), FEE_SCALAR / 2, ADMIN); + setSubscriberGlobalDiscount(USER, address(link), FEE_SCALAR / 2, ADMIN); + + //get the discount applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 2); + } + + function test_GlobalDiscountIsOverridenByIndividualDiscountNative() public { + //set the global discount to 50% + setSubscriberGlobalDiscount(USER, address(native), FEE_SCALAR / 2, ADMIN); + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 4, ADMIN); + + //get the discount applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 4); + } + + function test_GlobalDiscountIsOverridenByIndividualDiscountLink() public { + //set the global discount to 50% + setSubscriberGlobalDiscount(USER, address(link), FEE_SCALAR / 2, ADMIN); + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(link), FEE_SCALAR / 4, ADMIN); + + //get the discount applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 4); + } + + function test_GlobalDiscountIsUpdatedAfterBeingSetToZeroLink() public { + //set the global discount to 50% + setSubscriberGlobalDiscount(USER, address(link), FEE_SCALAR / 2, ADMIN); + + //get the discount applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 2); + + //set the global discount to zero + setSubscriberGlobalDiscount(USER, address(link), 0, ADMIN); + + //get the discount applied + discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getLinkQuote(), USER); + + //fee should be zero + assertEq(discount, 0); + } + + function test_GlobalDiscountIsUpdatedAfterBeingSetToZeroNative() public { + //set the global discount to 50% + setSubscriberGlobalDiscount(USER, address(native), FEE_SCALAR / 2, ADMIN); + + //get the discount applied + uint256 discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be half the default + assertEq(discount, FEE_SCALAR / 2); + + //set the global discount to zero + setSubscriberGlobalDiscount(USER, address(native), 0, ADMIN); + + //get the discount applied + discount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + //fee should be zero + assertEq(discount, 0); + } + + function test_GlobalDiscountCantBeSetToMoreThanMaximum() public { + //should revert with invalid discount + vm.expectRevert(INVALID_DISCOUNT_ERROR); + + //set the global discount to 101% + setSubscriberGlobalDiscount(USER, address(native), FEE_SCALAR + 1, ADMIN); + } + + function test_onlyOwnerCanSetGlobalDiscount() public { + //should revert with unauthorized + vm.expectRevert(ONLY_CALLABLE_BY_OWNER_ERROR); + + //set the global discount to 50% + setSubscriberGlobalDiscount(USER, address(native), FEE_SCALAR / 2, USER); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.processFee.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.processFee.t.sol new file mode 100644 index 00000000000..0e0ed8977bc --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.processFee.t.sol @@ -0,0 +1,492 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Common} from "../../../libraries/Common.sol"; +import "./BaseFeeManager.t.sol"; +import {IRewardManager} from "../../interfaces/IRewardManager.sol"; + +/** + * @title BaseFeeManagerTest + * @author Michael Fletcher + * @notice This contract will test the functionality of the feeManager processFee + */ +contract FeeManagerProcessFeeTest is BaseFeeManagerTest { + function setUp() public override { + super.setUp(); + } + + function test_nonAdminProxyUserCannotProcessFee() public { + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //should revert as the user is not the owner + vm.expectRevert(UNAUTHORIZED_ERROR); + + //process the fee + ProcessFeeAsUser(payload, USER, address(link), 0, USER); + } + + function test_processFeeAsProxy() public { + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //approve the link to be transferred from the from the subscriber to the rewardManager + approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + + //processing the fee will transfer the link from the user to the rewardManager + processFee(payload, USER, address(link), 0); + + //check the link has been transferred + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + //check the user has had the link fee deducted + assertEq(getLinkBalance(USER), DEFAULT_LINK_MINT_QUANTITY - DEFAULT_REPORT_LINK_FEE); + } + + function test_processFeeIfSubscriberIsSelf() public { + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //expect a revert due to the feeManager being the subscriber + vm.expectRevert(INVALID_ADDRESS_ERROR); + + //process the fee will fail due to assertion + processFee(payload, address(feeManager), address(native), 0); + } + + function test_processFeeWithWithEmptyQuotePayload() public { + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //expect a revert as the quote is invalid + vm.expectRevert(); + + //processing the fee will transfer the link by default + processFee(payload, USER, address(0), 0); + } + + function test_processFeeWithWithZeroQuotePayload() public { + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //expect a revert as the quote is invalid + vm.expectRevert(INVALID_QUOTE_ERROR); + + //processing the fee will transfer the link by default + processFee(payload, USER, INVALID_ADDRESS, 0); + } + + function test_processFeeWithWithCorruptQuotePayload() public { + //get the default payload + bytes memory payload = abi.encode( + [DEFAULT_CONFIG_DIGEST, 0, 0], + getV3Report(DEFAULT_FEED_1_V3), + new bytes32[](1), + new bytes32[](1), + bytes32("") + ); + + //expect an evm revert as the quote is corrupt + vm.expectRevert(); + + //processing the fee will not withdraw anything as there is no fee to collect + processFee(payload, USER, address(link), 0); + } + + function test_processFeeDefaultReportsStillVerifiesWithEmptyQuote() public { + //get the default payload + bytes memory payload = getPayload(getV1Report(DEFAULT_FEED_1_V1)); + + //processing the fee will transfer the link from the user to the rewardManager + processFee(payload, USER, address(0), 0); + } + + function test_processFeeWithDefaultReportPayloadAndQuoteStillVerifies() public { + //get the default payload + bytes memory payload = getPayload(getV1Report(DEFAULT_FEED_1_V1)); + + //processing the fee will not withdraw anything as there is no fee to collect + processFee(payload, USER, address(link), 0); + } + + function test_processFeeNative() public { + //simulate a deposit of link for the conversion pool + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //approve the native to be transferred from the user + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE, USER); + + //processing the fee will transfer the native from the user to the feeManager + processFee(payload, USER, address(native), 0); + + //check the native has been transferred + assertEq(getNativeBalance(address(feeManager)), DEFAULT_REPORT_NATIVE_FEE); + + //check the link has been transferred to the rewardManager + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + //check the feeManager has had the link deducted, the remaining balance should be 0 + assertEq(getLinkBalance(address(feeManager)), 0); + + //check the subscriber has had the native deducted + assertEq(getNativeBalance(USER), DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE); + } + + function test_processFeeEmitsEventIfNotEnoughLink() public { + //simulate a deposit of half the link required for the fee + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE / 2); + + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //approve the native to be transferred from the user + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE, USER); + + //expect an emit as there's not enough link + vm.expectEmit(); + + IRewardManager.FeePayment[] memory contractFees = new IRewardManager.FeePayment[](1); + contractFees[0] = IRewardManager.FeePayment(DEFAULT_CONFIG_DIGEST, uint192(DEFAULT_REPORT_LINK_FEE)); + + //emit the event that is expected to be emitted + emit InsufficientLink(contractFees); + + //processing the fee will transfer the native from the user to the feeManager + processFee(payload, USER, address(native), 0); + + //check the native has been transferred + assertEq(getNativeBalance(address(feeManager)), DEFAULT_REPORT_NATIVE_FEE); + + //check no link has been transferred to the rewardManager + assertEq(getLinkBalance(address(rewardManager)), 0); + assertEq(getLinkBalance(address(feeManager)), DEFAULT_REPORT_LINK_FEE / 2); + + //check the subscriber has had the native deducted + assertEq(getNativeBalance(USER), DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE); + } + + function test_processFeeWithUnwrappedNative() public { + //simulate a deposit of link for the conversion pool + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //only the proxy or admin can call processFee, they will pass in the native value on the users behalf + processFee(payload, USER, address(native), DEFAULT_REPORT_NATIVE_FEE); + + //check the native has been transferred and converted to wrapped native + assertEq(getNativeBalance(address(feeManager)), DEFAULT_REPORT_NATIVE_FEE); + assertEq(getNativeUnwrappedBalance(address(feeManager)), 0); + + //check the link has been transferred to the rewardManager + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + //check the feeManager has had the link deducted, the remaining balance should be 0 + assertEq(getLinkBalance(address(feeManager)), 0); + + //check the subscriber has had the native deducted + assertEq(getNativeUnwrappedBalance(USER), DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE); + } + + function test_processFeeWithUnwrappedNativeShortFunds() public { + //simulate a deposit of link for the conversion pool + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //expect a revert as not enough funds + vm.expectRevert(INVALID_DEPOSIT_ERROR); + + //only the proxy or admin can call processFee, they will pass in the native value on the users behalf + processFee(payload, USER, address(native), DEFAULT_REPORT_NATIVE_FEE - 1); + } + + function test_processFeeWithUnwrappedNativeLinkAddress() public { + //simulate a deposit of link for the conversion pool + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //expect a revert as not enough funds + vm.expectRevert(INSUFFICIENT_ALLOWANCE_ERROR); + + //the change will be returned and the user will attempted to be billed in LINK + processFee(payload, USER, address(link), DEFAULT_REPORT_NATIVE_FEE - 1); + } + + function test_processFeeWithUnwrappedNativeLinkAddressExcessiveFee() public { + //approve the link to be transferred from the from the subscriber to the rewardManager + approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, PROXY); + + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //call processFee from the proxy to test whether the funds are returned to the subscriber. In reality, the funds would be returned to the caller of the proxy. + processFee(payload, PROXY, address(link), DEFAULT_REPORT_NATIVE_FEE); + + //check the native unwrapped is no longer in the account + assertEq(getNativeBalance(address(feeManager)), 0); + assertEq(getNativeUnwrappedBalance(address(feeManager)), 0); + + //check the link has been transferred to the rewardManager + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + //check the feeManager has had the link deducted, the remaining balance should be 0 + assertEq(getLinkBalance(address(feeManager)), 0); + + //native should not be deducted + assertEq(getNativeUnwrappedBalance(PROXY), DEFAULT_NATIVE_MINT_QUANTITY); + } + + function test_processFeeWithUnwrappedNativeWithExcessiveFee() public { + //simulate a deposit of link for the conversion pool + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //call processFee from the proxy to test whether the funds are returned to the subscriber. In reality, the funds would be returned to the caller of the proxy. + processFee(payload, PROXY, address(native), DEFAULT_REPORT_NATIVE_FEE * 2); + + //check the native has been transferred and converted to wrapped native + assertEq(getNativeBalance(address(feeManager)), DEFAULT_REPORT_NATIVE_FEE); + assertEq(getNativeUnwrappedBalance(address(feeManager)), 0); + + //check the link has been transferred to the rewardManager + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + //check the feeManager has had the link deducted, the remaining balance should be 0 + assertEq(getLinkBalance(address(feeManager)), 0); + + //check the subscriber has had the native deducted + assertEq(getNativeUnwrappedBalance(PROXY), DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE); + } + + function test_processFeeUsesCorrectDigest() public { + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //approve the link to be transferred from the from the subscriber to the rewardManager + approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + + //processing the fee will transfer the link from the user to the rewardManager + processFee(payload, USER, address(link), 0); + + //check the link has been transferred + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + //check the user has had the link fee deducted + assertEq(getLinkBalance(USER), DEFAULT_LINK_MINT_QUANTITY - DEFAULT_REPORT_LINK_FEE); + + //check funds have been paid to the reward manager + assertEq(rewardManager.s_totalRewardRecipientFees(DEFAULT_CONFIG_DIGEST), DEFAULT_REPORT_LINK_FEE); + } + + function test_V1PayloadVerifies() public { + //replicate a default payload + bytes memory payload = abi.encode( + [DEFAULT_CONFIG_DIGEST, 0, 0], + getV2Report(DEFAULT_FEED_1_V1), + new bytes32[](1), + new bytes32[](1), + bytes32("") + ); + + //processing the fee will transfer the link from the user to the rewardManager + processFee(payload, USER, address(0), 0); + } + + function test_V2PayloadVerifies() public { + //get the default payload + bytes memory payload = getPayload(getV2Report(DEFAULT_FEED_1_V2)); + + //approve the link to be transferred from the from the subscriber to the rewardManager + approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + + //processing the fee will transfer the link from the user to the rewardManager + processFee(payload, USER, address(link), 0); + + //check the link has been transferred + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + //check the user has had the link fee deducted + assertEq(getLinkBalance(USER), DEFAULT_LINK_MINT_QUANTITY - DEFAULT_REPORT_LINK_FEE); + } + + function test_V2PayloadWithoutQuoteFails() public { + //get the default payload + bytes memory payload = getPayload(getV2Report(DEFAULT_FEED_1_V2)); + + //expect a revert as the quote is invalid + vm.expectRevert(); + + //processing the fee will transfer the link from the user to the rewardManager + processFee(payload, USER, address(0), 0); + } + + function test_V2PayloadWithoutZeroFee() public { + //get the default payload + bytes memory payload = getPayload(getV2Report(DEFAULT_FEED_1_V2)); + + //expect a revert as the quote is invalid + vm.expectRevert(); + + //processing the fee will transfer the link from the user to the rewardManager + processFee(payload, USER, address(link), 0); + } + + function test_processFeeWithInvalidReportVersionFailsToDecode() public { + bytes memory data = abi.encode(0x0000100000000000000000000000000000000000000000000000000000000000); + + //get the default payload + bytes memory payload = getPayload(data); + + //serialization will fail as there is no report to decode + vm.expectRevert(); + + //processing the fee will not withdraw anything as there is no fee to collect + processFee(payload, USER, address(link), 0); + } + + function test_processFeeWithZeroNativeNonZeroLinkWithNativeQuote() public { + //get the default payload + bytes memory payload = getPayload( + getV3ReportWithCustomExpiryAndFee(DEFAULT_FEED_1_V3, block.timestamp, DEFAULT_REPORT_LINK_FEE, 0) + ); + + //call processFee should not revert as the fee is 0 + processFee(payload, PROXY, address(native), 0); + } + + function test_processFeeWithZeroNativeNonZeroLinkWithLinkQuote() public { + //get the default payload + bytes memory payload = getPayload( + getV3ReportWithCustomExpiryAndFee(DEFAULT_FEED_1_V3, block.timestamp, DEFAULT_REPORT_LINK_FEE, 0) + ); + + //approve the link to be transferred from the from the subscriber to the rewardManager + approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + + //processing the fee will transfer the link to the rewardManager from the user + processFee(payload, USER, address(link), 0); + + //check the link has been transferred + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + //check the user has had the link fee deducted + assertEq(getLinkBalance(USER), DEFAULT_LINK_MINT_QUANTITY - DEFAULT_REPORT_LINK_FEE); + } + + function test_processFeeWithZeroLinkNonZeroNativeWithNativeQuote() public { + //simulate a deposit of link for the conversion pool + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //get the default payload + bytes memory payload = getPayload( + getV3ReportWithCustomExpiryAndFee(DEFAULT_FEED_1_V3, block.timestamp, 0, DEFAULT_REPORT_NATIVE_FEE) + ); + + //approve the native to be transferred from the user + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE, USER); + + //processing the fee will transfer the native from the user to the feeManager + processFee(payload, USER, address(native), 0); + + //check the native has been transferred + assertEq(getNativeBalance(address(feeManager)), DEFAULT_REPORT_NATIVE_FEE); + + //check no link has been transferred to the rewardManager + assertEq(getLinkBalance(address(rewardManager)), 0); + + //check the feeManager has had no link deducted + assertEq(getLinkBalance(address(feeManager)), DEFAULT_REPORT_LINK_FEE); + + //check the subscriber has had the native deducted + assertEq(getNativeBalance(USER), DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE); + } + + function test_processFeeWithZeroLinkNonZeroNativeWithLinkQuote() public { + //get the default payload + bytes memory payload = getPayload( + getV3ReportWithCustomExpiryAndFee(DEFAULT_FEED_1_V3, block.timestamp, 0, DEFAULT_REPORT_NATIVE_FEE) + ); + + //call processFee should not revert as the fee is 0 + processFee(payload, USER, address(link), 0); + } + + function test_processFeeWithZeroNativeNonZeroLinkReturnsChange() public { + //get the default payload + bytes memory payload = getPayload( + getV3ReportWithCustomExpiryAndFee(DEFAULT_FEED_1_V3, block.timestamp, 0, DEFAULT_REPORT_NATIVE_FEE) + ); + + //call processFee should not revert as the fee is 0 + processFee(payload, USER, address(link), DEFAULT_REPORT_NATIVE_FEE); + + //check the change has been returned + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY); + } + + function test_V1PayloadVerifiesAndReturnsChange() public { + //emulate a V1 payload with no quote + bytes memory payload = getPayload(getV1Report(DEFAULT_FEED_1_V1)); + + processFee(payload, USER, address(0), DEFAULT_REPORT_NATIVE_FEE); + + //Fee manager should not contain any native + assertEq(address(feeManager).balance, 0); + assertEq(getNativeBalance(address(feeManager)), 0); + + //check the unused native passed in is returned + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY); + } + + function test_processFeeWithDiscountEmitsEvent() public { + //simulate a deposit of link for the conversion pool + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //set the subscriber discount to 50% + setSubscriberDiscount(USER, DEFAULT_FEED_1_V3, address(native), FEE_SCALAR / 2, ADMIN); + + //approve the native to be transferred from the user + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE / 2, USER); + + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + Common.Asset memory fee = getFee(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + Common.Asset memory reward = getReward(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + uint256 appliedDiscount = getAppliedDiscount(getV3Report(DEFAULT_FEED_1_V3), getNativeQuote(), USER); + + vm.expectEmit(); + + emit DiscountApplied(DEFAULT_CONFIG_DIGEST, USER, fee, reward, appliedDiscount); + + //call processFee should not revert as the fee is 0 + processFee(payload, USER, address(native), 0); + } + + function test_processFeeWithNoDiscountDoesNotEmitEvent() public { + //simulate a deposit of link for the conversion pool + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //approve the native to be transferred from the user + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE, USER); + + //get the default payload + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + //call processFee should not revert as the fee is 0 + processFee(payload, USER, address(native), 0); + + //no logs should have been emitted + assertEq(vm.getRecordedLogs().length, 0); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.processFeeBulk.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.processFeeBulk.t.sol new file mode 100644 index 00000000000..fa77b98d3cf --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.processFeeBulk.t.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import "./BaseFeeManager.t.sol"; +import {IRewardManager} from "../../interfaces/IRewardManager.sol"; + +/** + * @title BaseFeeManagerTest + * @author Michael Fletcher + * @notice This contract will test the functionality of the feeManager processFee + */ +contract FeeManagerProcessFeeTest is BaseFeeManagerTest { + uint256 internal constant NUMBER_OF_REPORTS = 5; + + function setUp() public override { + super.setUp(); + } + + function test_processMultipleLinkReports() public { + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + bytes[] memory payloads = new bytes[](NUMBER_OF_REPORTS); + for (uint256 i = 0; i < NUMBER_OF_REPORTS; ++i) { + payloads[i] = payload; + } + + approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS, USER); + + processFee(payloads, USER, address(link), DEFAULT_NATIVE_MINT_QUANTITY); + + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS); + assertEq(getLinkBalance(address(feeManager)), 0); + assertEq(getLinkBalance(USER), DEFAULT_LINK_MINT_QUANTITY - DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS); + + //the subscriber (user) should receive funds back and not the proxy, although when live the proxy will forward the funds sent and not cover it seen here + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY); + assertEq(PROXY.balance, DEFAULT_NATIVE_MINT_QUANTITY); + } + + function test_processMultipleWrappedNativeReports() public { + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS + 1); + + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + bytes[] memory payloads = new bytes[](NUMBER_OF_REPORTS); + for (uint256 i; i < NUMBER_OF_REPORTS; ++i) { + payloads[i] = payload; + } + + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS, USER); + + processFee(payloads, USER, address(native), 0); + + assertEq(getNativeBalance(address(feeManager)), DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS); + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS); + assertEq(getLinkBalance(address(feeManager)), 1); + assertEq(getNativeBalance(USER), DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS); + } + + function test_processMultipleUnwrappedNativeReports() public { + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS + 1); + + bytes memory payload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + bytes[] memory payloads = new bytes[](NUMBER_OF_REPORTS); + for (uint256 i; i < NUMBER_OF_REPORTS; ++i) { + payloads[i] = payload; + } + + processFee(payloads, USER, address(native), DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS * 2); + + assertEq(getNativeBalance(address(feeManager)), DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS); + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS); + assertEq(getLinkBalance(address(feeManager)), 1); + + assertEq(PROXY.balance, DEFAULT_NATIVE_MINT_QUANTITY); + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS); + } + + function test_processV1V2V3Reports() public { + mintLink(address(feeManager), 1); + + bytes memory payloadV1 = abi.encode( + [DEFAULT_CONFIG_DIGEST, 0, 0], + getV1Report(DEFAULT_FEED_1_V1), + new bytes32[](1), + new bytes32[](1), + bytes32("") + ); + + bytes memory linkPayloadV2 = getPayload(getV2Report(DEFAULT_FEED_1_V2)); + bytes memory linkPayloadV3 = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + bytes[] memory payloads = new bytes[](5); + payloads[0] = payloadV1; + payloads[1] = linkPayloadV2; + payloads[2] = linkPayloadV2; + payloads[3] = linkPayloadV3; + payloads[4] = linkPayloadV3; + + approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE * 4, USER); + + processFee(payloads, USER, address(link), 0); + + assertEq(getNativeBalance(address(feeManager)), 0); + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE * 4); + assertEq(getLinkBalance(address(feeManager)), 1); + + assertEq(getLinkBalance(USER), DEFAULT_LINK_MINT_QUANTITY - DEFAULT_REPORT_LINK_FEE * 4); + assertEq(getNativeBalance(USER), DEFAULT_NATIVE_MINT_QUANTITY - 0); + } + + function test_processV1V2V3ReportsWithUnwrapped() public { + mintLink(address(feeManager), DEFAULT_REPORT_LINK_FEE * 4 + 1); + + bytes memory payloadV1 = abi.encode( + [DEFAULT_CONFIG_DIGEST, 0, 0], + getV1Report(DEFAULT_FEED_1_V1), + new bytes32[](1), + new bytes32[](1), + bytes32("") + ); + + bytes memory nativePayloadV2 = getPayload(getV2Report(DEFAULT_FEED_1_V2)); + bytes memory nativePayloadV3 = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + bytes[] memory payloads = new bytes[](5); + payloads[0] = payloadV1; + payloads[1] = nativePayloadV2; + payloads[2] = nativePayloadV2; + payloads[3] = nativePayloadV3; + payloads[4] = nativePayloadV3; + + processFee(payloads, USER, address(native), DEFAULT_REPORT_NATIVE_FEE * 4); + + assertEq(getNativeBalance(address(feeManager)), DEFAULT_REPORT_NATIVE_FEE * 4); + assertEq(getLinkBalance(address(rewardManager)), DEFAULT_REPORT_LINK_FEE * 4); + assertEq(getLinkBalance(address(feeManager)), 1); + + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE * 4); + assertEq(PROXY.balance, DEFAULT_NATIVE_MINT_QUANTITY); + } + + function test_processMultipleV1Reports() public { + bytes memory payload = abi.encode( + [DEFAULT_CONFIG_DIGEST, 0, 0], + getV1Report(DEFAULT_FEED_1_V1), + new bytes32[](1), + new bytes32[](1), + bytes32("") + ); + + bytes[] memory payloads = new bytes[](NUMBER_OF_REPORTS); + for (uint256 i = 0; i < NUMBER_OF_REPORTS; ++i) { + payloads[i] = payload; + } + + processFee(payloads, USER, address(native), DEFAULT_REPORT_NATIVE_FEE * 5); + + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY); + assertEq(PROXY.balance, DEFAULT_NATIVE_MINT_QUANTITY); + } + + function test_eventIsEmittedIfNotEnoughLink() public { + bytes memory nativePayload = getPayload(getV3Report(DEFAULT_FEED_1_V3)); + + bytes[] memory payloads = new bytes[](5); + payloads[0] = nativePayload; + payloads[1] = nativePayload; + payloads[2] = nativePayload; + payloads[3] = nativePayload; + payloads[4] = nativePayload; + + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE * 5, USER); + + IRewardManager.FeePayment[] memory payments = new IRewardManager.FeePayment[](5); + payments[0] = IRewardManager.FeePayment(DEFAULT_CONFIG_DIGEST, uint192(DEFAULT_REPORT_LINK_FEE)); + payments[1] = IRewardManager.FeePayment(DEFAULT_CONFIG_DIGEST, uint192(DEFAULT_REPORT_LINK_FEE)); + payments[2] = IRewardManager.FeePayment(DEFAULT_CONFIG_DIGEST, uint192(DEFAULT_REPORT_LINK_FEE)); + payments[3] = IRewardManager.FeePayment(DEFAULT_CONFIG_DIGEST, uint192(DEFAULT_REPORT_LINK_FEE)); + payments[4] = IRewardManager.FeePayment(DEFAULT_CONFIG_DIGEST, uint192(DEFAULT_REPORT_LINK_FEE)); + + vm.expectEmit(); + + emit InsufficientLink(payments); + + processFee(payloads, USER, address(native), 0); + + assertEq(getNativeBalance(address(feeManager)), DEFAULT_REPORT_NATIVE_FEE * 5); + assertEq(getNativeBalance(USER), DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE * 5); + assertEq(getLinkBalance(USER), DEFAULT_LINK_MINT_QUANTITY); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/gas/Gas_VerifierTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/gas/Gas_VerifierTest.t.sol new file mode 100644 index 00000000000..bbbda4f619f --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/gas/Gas_VerifierTest.t.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest, BaseTestWithConfiguredVerifierAndFeeManager} from "../verifier/BaseVerifierTest.t.sol"; +import {SimpleWriteAccessController} from "../../../../shared/access/SimpleWriteAccessController.sol"; +import {Common} from "../../../libraries/Common.sol"; +import {IRewardManager} from "../../interfaces/IRewardManager.sol"; + +contract Verifier_setConfig is BaseTest { + address[] internal s_signerAddrs; + + function setUp() public override { + BaseTest.setUp(); + Signer[] memory signers = _getSigners(MAX_ORACLES); + s_signerAddrs = _getSignerAddresses(signers); + s_verifierProxy.initializeVerifier(address(s_verifier)); + } + + function testSetConfigSuccess_gas() public { + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + s_signerAddrs, + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + 1, + bytes("") + ); + + s_verifier.setConfig( + configDigest, + s_signerAddrs, + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + } +} + +contract Verifier_verifyWithFee is BaseTestWithConfiguredVerifierAndFeeManager { + uint256 internal constant DEFAULT_LINK_MINT_QUANTITY = 100 ether; + uint256 internal constant DEFAULT_NATIVE_MINT_QUANTITY = 100 ether; + + function setUp() public virtual override { + super.setUp(); + + //mint some link and eth to warm the storage + link.mint(address(rewardManager), DEFAULT_LINK_MINT_QUANTITY); + native.mint(address(feeManager), DEFAULT_NATIVE_MINT_QUANTITY); + + //warm the rewardManager + link.mint(address(this), DEFAULT_NATIVE_MINT_QUANTITY); + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, address(this)); + bytes32 latestConfigDigest = v1ConfigDigest; + + //mint some tokens to the user + link.mint(USER, DEFAULT_LINK_MINT_QUANTITY); + native.mint(USER, DEFAULT_NATIVE_MINT_QUANTITY); + vm.deal(USER, DEFAULT_NATIVE_MINT_QUANTITY); + + //mint some link tokens to the feeManager pool + link.mint(address(feeManager), DEFAULT_REPORT_LINK_FEE); + + //approve funds prior to test + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + _approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE, USER); + + IRewardManager.FeePayment[] memory payments = new IRewardManager.FeePayment[](1); + payments[0] = IRewardManager.FeePayment(latestConfigDigest, uint192(DEFAULT_REPORT_LINK_FEE)); + + changePrank(address(feeManager)); + rewardManager.onFeePaid(payments, address(this)); + + changePrank(USER); + } + + function testVerifyProxyWithLinkFeeSuccess_gas() public { + bytes memory signedLinkPayload = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + s_verifierProxy.verify(signedLinkPayload, abi.encode(link)); + } + + function testVerifyProxyWithNativeFeeSuccess_gas() public { + bytes memory signedNativePayload = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + s_verifierProxy.verify(signedNativePayload, abi.encode(native)); + } +} + +contract Verifier_bulkVerifyWithFee is BaseTestWithConfiguredVerifierAndFeeManager { + uint256 internal constant DEFAULT_LINK_MINT_QUANTITY = 100 ether; + uint256 internal constant DEFAULT_NATIVE_MINT_QUANTITY = 100 ether; + uint256 internal constant NUMBER_OF_REPORTS_TO_VERIFY = 5; + + function setUp() public virtual override { + super.setUp(); + + //mint some link and eth to warm the storage + link.mint(address(rewardManager), DEFAULT_LINK_MINT_QUANTITY); + native.mint(address(feeManager), DEFAULT_NATIVE_MINT_QUANTITY); + + //warm the rewardManager + link.mint(address(this), DEFAULT_NATIVE_MINT_QUANTITY); + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, address(this)); + bytes32 latestConfigDigest = v1ConfigDigest; + + //mint some tokens to the user + link.mint(USER, DEFAULT_LINK_MINT_QUANTITY); + native.mint(USER, DEFAULT_NATIVE_MINT_QUANTITY); + vm.deal(USER, DEFAULT_NATIVE_MINT_QUANTITY); + + //mint some link tokens to the feeManager pool + link.mint(address(feeManager), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS_TO_VERIFY); + + //approve funds prior to test + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS_TO_VERIFY, USER); + _approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS_TO_VERIFY, USER); + + IRewardManager.FeePayment[] memory payments = new IRewardManager.FeePayment[](1); + payments[0] = IRewardManager.FeePayment(latestConfigDigest, uint192(DEFAULT_REPORT_LINK_FEE)); + + changePrank(address(feeManager)); + rewardManager.onFeePaid(payments, address(this)); + + changePrank(USER); + } + + function testBulkVerifyProxyWithLinkFeeSuccess_gas() public { + bytes memory signedLinkPayload = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes[] memory signedLinkPayloads = new bytes[](NUMBER_OF_REPORTS_TO_VERIFY); + for (uint256 i = 0; i < NUMBER_OF_REPORTS_TO_VERIFY; i++) { + signedLinkPayloads[i] = signedLinkPayload; + } + + s_verifierProxy.verifyBulk(signedLinkPayloads, abi.encode(link)); + } + + function testBulkVerifyProxyWithNativeFeeSuccess_gas() public { + bytes memory signedNativePayload = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes[] memory signedNativePayloads = new bytes[](NUMBER_OF_REPORTS_TO_VERIFY); + for (uint256 i = 0; i < NUMBER_OF_REPORTS_TO_VERIFY; i++) { + signedNativePayloads[i] = signedNativePayload; + } + + s_verifierProxy.verifyBulk(signedNativePayloads, abi.encode(native)); + } +} + +contract Verifier_verify is BaseTestWithConfiguredVerifierAndFeeManager { + bytes internal s_signedReport; + bytes32 internal s_configDigest; + + function setUp() public override { + BaseTestWithConfiguredVerifierAndFeeManager.setUp(); + BaseTest.V1Report memory s_testReportOne = _createV1Report( + FEED_ID, + OBSERVATIONS_TIMESTAMP, + MEDIAN, + BID, + ASK, + BLOCKNUMBER_UPPER_BOUND, + blockhash(BLOCKNUMBER_UPPER_BOUND), + BLOCKNUMBER_LOWER_BOUND, + uint32(block.timestamp) + ); + s_configDigest = v1ConfigDigest; + bytes32[3] memory reportContext; + reportContext[0] = s_configDigest; + reportContext[1] = bytes32(abi.encode(uint32(5), uint8(1))); + s_signedReport = _generateV1EncodedBlob(s_testReportOne, reportContext, _getSigners(FAULT_TOLERANCE + 1)); + } + + function testVerifySuccess_gas() public { + changePrank(address(s_verifierProxy)); + + s_verifier.verify(s_signedReport, msg.sender); + } + + function testVerifyProxySuccess_gas() public { + s_verifierProxy.verify(s_signedReport, abi.encode(native)); + } +} + +contract Verifier_accessControlledVerify is BaseTestWithConfiguredVerifierAndFeeManager { + bytes internal s_signedReport; + bytes32 internal s_configDigest; + SimpleWriteAccessController s_accessController; + + address internal constant CLIENT = address(9000); + address internal constant ACCESS_CONTROLLER_ADDR = address(10000); + + function setUp() public override { + BaseTestWithConfiguredVerifierAndFeeManager.setUp(); + BaseTest.V1Report memory s_testReportOne = _createV1Report( + FEED_ID, + OBSERVATIONS_TIMESTAMP, + MEDIAN, + BID, + ASK, + BLOCKNUMBER_UPPER_BOUND, + blockhash(BLOCKNUMBER_UPPER_BOUND), + BLOCKNUMBER_LOWER_BOUND, + uint32(block.timestamp) + ); + s_configDigest = v1ConfigDigest; + bytes32[3] memory reportContext; + reportContext[0] = s_configDigest; + reportContext[1] = bytes32(abi.encode(uint32(5), uint8(1))); + s_signedReport = _generateV1EncodedBlob(s_testReportOne, reportContext, _getSigners(FAULT_TOLERANCE + 1)); + s_accessController = new SimpleWriteAccessController(); + s_verifierProxy.setAccessController(s_accessController); + s_accessController.addAccess(CLIENT); + } + + function testVerifyWithAccessControl_gas() public { + changePrank(CLIENT); + s_verifierProxy.verify(s_signedReport, abi.encode(native)); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/ErroredVerifier.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/ErroredVerifier.sol new file mode 100644 index 00000000000..c92bcb1ac27 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/ErroredVerifier.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {IVerifier} from "../../interfaces/IVerifier.sol"; +import {Common} from "../../../libraries/Common.sol"; + +contract ErroredVerifier is IVerifier { + function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { + return interfaceId == this.verify.selector; + } + + //define each of the errors thrown in the revert below + + error FailedToVerify(); + error FailedToSetConfig(); + error FailedToUnsetConfig(); + error FailedToActivateConfig(); + error FailedToDeactivateConfig(); + error FailedToActivateFeed(); + error FailedToDeactivateFeed(); + error FailedToGetLatestConfigDigestAndEpoch(); + error FailedToGetLatestConfigDetails(); + + function verify( + bytes memory, + /** + * signedReport* + */ + address + ) + external + pure + override + returns ( + /** + * sender* + */ + bytes memory + ) + { + revert FailedToVerify(); + } + + function updateConfig(bytes32, address[] calldata, address[] calldata, uint8) external pure { + revert FailedToUnsetConfig(); + } + + function setConfig( + bytes32, + address[] calldata, + uint8, + Common.AddressAndWeight[] calldata + ) external pure override { + revert FailedToSetConfig(); + } + + function activateConfig(bytes32) external pure { + revert FailedToActivateConfig(); + } + + function deactivateConfig(bytes32) external pure { + revert FailedToDeactivateConfig(); + } + + function latestConfigDetails(bytes32) external pure override returns (uint32) { + revert FailedToGetLatestConfigDetails(); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/ExposedVerifier.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/ExposedVerifier.sol new file mode 100644 index 00000000000..1c004bf3846 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/ExposedVerifier.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +// ExposedVerifier exposes certain internal Verifier +// methods/structures so that golang code can access them, and we get +// reliable type checking on their usage +contract ExposedVerifier { + constructor() {} + + function _configDigestFromConfigData( + bytes32 feedId, + uint256 chainId, + address contractAddress, + uint64 configCount, + address[] memory signers, + bytes32[] memory offchainTransmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) internal pure returns (bytes32) { + uint256 h = uint256( + keccak256( + abi.encode( + feedId, + chainId, + contractAddress, + configCount, + signers, + offchainTransmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ) + ) + ); + uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 prefix = 0x0006 << (256 - 16); // 0x000600..00 + return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + } + + function exposedConfigDigestFromConfigData( + bytes32 _feedId, + uint256 _chainId, + address _contractAddress, + uint64 _configCount, + address[] memory _signers, + bytes32[] memory _offchainTransmitters, + uint8 _f, + bytes calldata _onchainConfig, + uint64 _encodedConfigVersion, + bytes memory _encodedConfig + ) public pure returns (bytes32) { + return + _configDigestFromConfigData( + _feedId, + _chainId, + _contractAddress, + _configCount, + _signers, + _offchainTransmitters, + _f, + _onchainConfig, + _encodedConfigVersion, + _encodedConfig + ); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/FeeManagerProxy.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/FeeManagerProxy.sol new file mode 100644 index 00000000000..9abb4c50c29 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/FeeManagerProxy.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IFeeManager} from "../../interfaces/IFeeManager.sol"; + +contract FeeManagerProxy { + IFeeManager internal s_feeManager; + + function processFee(bytes calldata payload, bytes calldata parameterPayload) public payable { + s_feeManager.processFee{value: msg.value}(payload, parameterPayload, msg.sender); + } + + function processFeeBulk(bytes[] calldata payloads, bytes calldata parameterPayload) public payable { + s_feeManager.processFeeBulk{value: msg.value}(payloads, parameterPayload, msg.sender); + } + + function setFeeManager(IFeeManager feeManager) public { + s_feeManager = feeManager; + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/MockConfigurator.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/MockConfigurator.sol new file mode 100644 index 00000000000..f9dab43140b --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/MockConfigurator.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +// Derived from v0.5/Configurator.sol +contract MockConfigurator { + struct ConfigurationState { + uint64 configCount; + uint32 latestConfigBlockNumber; + bytes32 configDigest; + } + + mapping(bytes32 => ConfigurationState) public s_configurationStates; + + function setStagingConfig( + bytes32 configId, + bytes[] memory signers, + bytes32[] memory offchainTransmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external { + ConfigurationState storage configurationState = s_configurationStates[configId]; + + uint64 newConfigCount = ++configurationState.configCount; + + bytes32 configDigest = _configDigestFromConfigData( + configId, + block.chainid, + address(this), + newConfigCount, + signers, + offchainTransmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + + s_configurationStates[configId].configDigest = configDigest; + configurationState.latestConfigBlockNumber = uint32(block.number); + } + + function _configDigestFromConfigData( + bytes32 configId, + uint256 sourceChainId, + address sourceAddress, + uint64 configCount, + bytes[] memory signers, + bytes32[] memory offchainTransmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) internal pure returns (bytes32) { + uint256 h = uint256( + keccak256( + abi.encode( + configId, + sourceChainId, + sourceAddress, + configCount, + signers, + offchainTransmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ) + ) + ); + uint256 prefixMask = type(uint256).max << (256 - 16); + uint256 prefix = 0x0009 << (256 - 16); + return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/BaseRewardManager.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/BaseRewardManager.t.sol new file mode 100644 index 00000000000..b002342b411 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/BaseRewardManager.t.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {ERC20Mock} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/mocks/ERC20Mock.sol"; +import {RewardManager} from "../../RewardManager.sol"; +import {Common} from "../../../libraries/Common.sol"; +import {IRewardManager} from "../../interfaces/IRewardManager.sol"; + +/** + * @title BaseRewardManagerTest + * @author Michael Fletcher + * @notice Base class for all reward manager tests + * @dev This contract is intended to be inherited from and not used directly. It contains functionality to setup a primary and secondary pool + */ +contract BaseRewardManagerTest is Test { + //contracts + ERC20Mock internal asset; + ERC20Mock internal unsupported; + RewardManager internal rewardManager; + + //default address for unregistered recipient + address internal constant INVALID_ADDRESS = address(0); + //contract owner + address internal constant ADMIN = address(uint160(uint256(keccak256("ADMIN")))); + //address to represent verifier contract + address internal constant FEE_MANAGER = address(uint160(uint256(keccak256("FEE_MANAGER")))); + //a general user + address internal constant USER = address(uint160(uint256(keccak256("USER")))); + + //default recipients configured in reward manager + address internal constant DEFAULT_RECIPIENT_1 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_1")))); + address internal constant DEFAULT_RECIPIENT_2 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_2")))); + address internal constant DEFAULT_RECIPIENT_3 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_3")))); + address internal constant DEFAULT_RECIPIENT_4 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_4")))); + address internal constant DEFAULT_RECIPIENT_5 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_5")))); + address internal constant DEFAULT_RECIPIENT_6 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_6")))); + address internal constant DEFAULT_RECIPIENT_7 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_7")))); + + //additional recipients not in the reward manager + address internal constant DEFAULT_RECIPIENT_8 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_8")))); + address internal constant DEFAULT_RECIPIENT_9 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_9")))); + + //two pools should be enough to test all edge cases + bytes32 internal constant PRIMARY_POOL_ID = keccak256("primary_pool"); + bytes32 internal constant SECONDARY_POOL_ID = keccak256("secondary_pool"); + bytes32 internal constant INVALID_POOL_ID = keccak256("invalid_pool"); + bytes32 internal constant ZERO_POOL_ID = bytes32(0); + + //convenience arrays of all pool combinations used for testing + bytes32[] internal PRIMARY_POOL_ARRAY = [PRIMARY_POOL_ID]; + bytes32[] internal SECONDARY_POOL_ARRAY = [SECONDARY_POOL_ID]; + bytes32[] internal ALL_POOLS = [PRIMARY_POOL_ID, SECONDARY_POOL_ID]; + + //erc20 config + uint256 internal constant DEFAULT_MINT_QUANTITY = 100 ether; + + //reward scalar (this should match the const in the contract) + uint64 internal constant POOL_SCALAR = 1e18; + uint64 internal constant ONE_PERCENT = POOL_SCALAR / 100; + uint64 internal constant FIFTY_PERCENT = POOL_SCALAR / 2; + uint64 internal constant TEN_PERCENT = POOL_SCALAR / 10; + + //the selector for each error + bytes4 internal immutable UNAUTHORIZED_ERROR_SELECTOR = RewardManager.Unauthorized.selector; + bytes4 internal immutable INVALID_ADDRESS_ERROR_SELECTOR = RewardManager.InvalidAddress.selector; + bytes4 internal immutable INVALID_WEIGHT_ERROR_SELECTOR = RewardManager.InvalidWeights.selector; + bytes4 internal immutable INVALID_POOL_ID_ERROR_SELECTOR = RewardManager.InvalidPoolId.selector; + bytes internal constant ONLY_CALLABLE_BY_OWNER_ERROR = "Only callable by owner"; + bytes4 internal immutable INVALID_POOL_LENGTH_SELECTOR = RewardManager.InvalidPoolLength.selector; + + // Events emitted within the reward manager + event RewardRecipientsUpdated(bytes32 indexed poolId, Common.AddressAndWeight[] newRewardRecipients); + event RewardsClaimed(bytes32 indexed poolId, address indexed recipient, uint192 quantity); + event FeeManagerUpdated(address newProxyAddress); + event FeePaid(IRewardManager.FeePayment[] payments, address payee); + + function setUp() public virtual { + //change to admin user + vm.startPrank(ADMIN); + + //init required contracts + _initializeERC20Contracts(); + _initializeRewardManager(); + } + + function _initializeERC20Contracts() internal { + //create the contracts + asset = new ERC20Mock("ASSET", "AST", ADMIN, 0); + unsupported = new ERC20Mock("UNSUPPORTED", "UNS", ADMIN, 0); + + //mint some tokens to the admin + asset.mint(ADMIN, DEFAULT_MINT_QUANTITY); + unsupported.mint(ADMIN, DEFAULT_MINT_QUANTITY); + + //mint some tokens to the user + asset.mint(FEE_MANAGER, DEFAULT_MINT_QUANTITY); + unsupported.mint(FEE_MANAGER, DEFAULT_MINT_QUANTITY); + } + + function _initializeRewardManager() internal { + //create the contract + rewardManager = new RewardManager(address(asset)); + + rewardManager.setFeeManager(FEE_MANAGER); + } + + function createPrimaryPool() public { + rewardManager.setRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients()); + } + + function createSecondaryPool() public { + rewardManager.setRewardRecipients(SECONDARY_POOL_ID, getSecondaryRecipients()); + } + + //override this to test variations of different recipients. changing this function will require existing tests to be updated as constants are hardcoded to be explicit + function getPrimaryRecipients() public virtual returns (Common.AddressAndWeight[] memory) { + //array of recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + + //init each recipient with even weights. 2500 = 25% of pool + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, POOL_SCALAR / 4); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, POOL_SCALAR / 4); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, POOL_SCALAR / 4); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, POOL_SCALAR / 4); + + return recipients; + } + + function getPrimaryRecipientAddresses() public pure returns (address[] memory) { + //array of recipients + address[] memory recipients = new address[](4); + + recipients[0] = DEFAULT_RECIPIENT_1; + recipients[1] = DEFAULT_RECIPIENT_2; + recipients[2] = DEFAULT_RECIPIENT_3; + recipients[3] = DEFAULT_RECIPIENT_4; + + return recipients; + } + + //override this to test variations of different recipients. + function getSecondaryRecipients() public virtual returns (Common.AddressAndWeight[] memory) { + //array of recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + + //init each recipient with even weights. 2500 = 25% of pool + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, POOL_SCALAR / 4); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_5, POOL_SCALAR / 4); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_6, POOL_SCALAR / 4); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_7, POOL_SCALAR / 4); + + return recipients; + } + + function getSecondaryRecipientAddresses() public pure returns (address[] memory) { + //array of recipients + address[] memory recipients = new address[](4); + + recipients[0] = DEFAULT_RECIPIENT_1; + recipients[1] = DEFAULT_RECIPIENT_5; + recipients[2] = DEFAULT_RECIPIENT_6; + recipients[3] = DEFAULT_RECIPIENT_7; + + return recipients; + } + + function addFundsToPool(bytes32 poolId, Common.Asset memory amount, address sender) public { + IRewardManager.FeePayment[] memory payments = new IRewardManager.FeePayment[](1); + payments[0] = IRewardManager.FeePayment(poolId, uint192(amount.amount)); + + addFundsToPool(payments, sender); + } + + function addFundsToPool(IRewardManager.FeePayment[] memory payments, address sender) public { + //record the current address and switch to the sender + address originalAddr = msg.sender; + changePrank(sender); + + uint256 totalPayment; + for (uint256 i; i < payments.length; ++i) { + totalPayment += payments[i].amount; + } + + //approve the amount being paid into the pool + ERC20Mock(address(asset)).approve(address(rewardManager), totalPayment); + + //this represents the verifier adding some funds to the pool + rewardManager.onFeePaid(payments, sender); + + //change back to the original address + changePrank(originalAddr); + } + + function getAsset(uint256 quantity) public view returns (Common.Asset memory) { + return Common.Asset(address(asset), quantity); + } + + function getAssetBalance(address addr) public view returns (uint256) { + return asset.balanceOf(addr); + } + + function claimRewards(bytes32[] memory poolIds, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //claim the rewards + rewardManager.claimRewards(poolIds); + + //change back to the original address + changePrank(originalAddr); + } + + function payRecipients(bytes32 poolId, address[] memory recipients, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //pay the recipients + rewardManager.payRecipients(poolId, recipients); + + //change back to the original address + changePrank(originalAddr); + } + + function setRewardRecipients(bytes32 poolId, Common.AddressAndWeight[] memory recipients, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //pay the recipients + rewardManager.setRewardRecipients(poolId, recipients); + + //change back to the original address + changePrank(originalAddr); + } + + function setFeeManager(address feeManager, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //update the proxy + rewardManager.setFeeManager(feeManager); + + //change back to the original address + changePrank(originalAddr); + } + + function updateRewardRecipients(bytes32 poolId, Common.AddressAndWeight[] memory recipients, address sender) public { + //record the current address and switch to the recipient + address originalAddr = msg.sender; + changePrank(sender); + + //pay the recipients + rewardManager.updateRewardRecipients(poolId, recipients); + + //change back to the original address + changePrank(originalAddr); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.claim.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.claim.t.sol new file mode 100644 index 00000000000..efbe9fd6b36 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.claim.t.sol @@ -0,0 +1,790 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseRewardManagerTest} from "./BaseRewardManager.t.sol"; +import {Common} from "../../../libraries/Common.sol"; + +/** + * @title BaseRewardManagerTest + * @author Michael Fletcher + * @notice This contract will test the claim functionality of the RewardManager contract. + */ +contract RewardManagerClaimTest is BaseRewardManagerTest { + uint256 internal constant POOL_DEPOSIT_AMOUNT = 10e18; + + function setUp() public override { + //setup contracts + super.setUp(); + + //create a single pool for these tests + createPrimaryPool(); + + //add funds to the pool to be split among the recipients + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + } + + function test_claimAllRecipients() public { + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //claim funds for each recipient within the pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + } + + function test_claimRewardsWithDuplicatePoolIdsDoesNotPayoutTwice() public { + //add funds to a different pool to ensure they're not claimed + addFundsToPool(SECONDARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //create an array containing duplicate poolIds + bytes32[] memory poolIds = new bytes32[](2); + poolIds[0] = PRIMARY_POOL_ID; + poolIds[1] = PRIMARY_POOL_ID; + + //claim funds for each recipient within the pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(poolIds, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //the pool should still have the remaining + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + } + + function test_claimSingleRecipient() public { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[0]; + + //claim the individual rewards for this recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check the recipients balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + + //check the rewardManager has the remaining quantity + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT - expectedRecipientAmount); + } + + function test_claimMultipleRecipients() public { + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, getPrimaryRecipients()[0].addr); + claimRewards(PRIMARY_POOL_ARRAY, getPrimaryRecipients()[1].addr); + + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check the recipients balance matches the ratio the recipient should have received + assertEq(getAssetBalance(getPrimaryRecipients()[0].addr), expectedRecipientAmount); + assertEq(getAssetBalance(getPrimaryRecipients()[1].addr), expectedRecipientAmount); + + //check the rewardManager has the remaining quantity + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT - (expectedRecipientAmount * 2)); + } + + function test_claimUnregisteredRecipient() public { + //claim the rewards for a recipient who isn't in this pool + claimRewards(PRIMARY_POOL_ARRAY, getSecondaryRecipients()[1].addr); + + //check the recipients didn't receive any fees from this pool + assertEq(getAssetBalance(getSecondaryRecipients()[1].addr), 0); + + //check the rewardManager has the remaining quantity + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + } + + function test_claimUnevenAmountRoundsDown() public { + //adding 1 to the pool should leave 1 wei worth of dust, which the contract doesn't handle due to it being economically infeasible + addFundsToPool(PRIMARY_POOL_ID, getAsset(1), FEE_MANAGER); + + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //claim funds for each recipient within the pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //check the rewardManager has the remaining quantity equals 1 wei + assertEq(getAssetBalance(address(rewardManager)), 1); + } + + function test_claimUnregisteredPoolId() public { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[0]; + + //claim the individual rewards for this recipient + claimRewards(SECONDARY_POOL_ARRAY, recipient.addr); + + //check the recipients balance is still 0 as there's no pool to receive fees from + assertEq(getAssetBalance(recipient.addr), 0); + + //check the rewardManager has the full amount + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + } + + function test_singleRecipientClaimMultipleDeposits() public { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[0]; + + //claim the individual rewards for this recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check the recipients balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + + //check the rewardManager has the remaining quantity, which is 3/4 of the initial deposit + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT - expectedRecipientAmount); + + //add funds to the pool to be split among the recipients + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + + //claim the individual rewards for this recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the recipients balance matches the ratio the recipient should have received, which is 1/4 of each deposit + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount * 2); + + //check the rewardManager has the remaining quantity, which is now 3/4 of both deposits + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT * 2 - (expectedRecipientAmount * 2)); + } + + function test_recipientsClaimMultipleDeposits() public { + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //claim funds for each recipient within the pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //the reward manager balance should be 0 as all of the funds have been claimed + assertEq(getAssetBalance(address(rewardManager)), 0); + + //add funds to the pool to be split among the recipients + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + + //claim funds for each recipient within the pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //expected recipient amount is 1/4 of the pool deposit + expectedRecipientAmount = (POOL_DEPOSIT_AMOUNT / 4) * 2; + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //the reward manager balance should again be 0 as all of the funds have been claimed + assertEq(getAssetBalance(address(rewardManager)), 0); + } + + function test_eventIsEmittedUponClaim() public { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[0]; + + //expect an emit + vm.expectEmit(); + + //emit the event that is expected to be emitted + emit RewardsClaimed(PRIMARY_POOL_ID, recipient.addr, uint192(POOL_DEPOSIT_AMOUNT / 4)); + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + } + + function test_eventIsNotEmittedUponUnsuccessfulClaim() public { + //record logs to check no events were emitted + vm.recordLogs(); + + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[0]; + + //claim the individual rewards for each recipient + claimRewards(SECONDARY_POOL_ARRAY, recipient.addr); + + //no logs should have been emitted + assertEq(vm.getRecordedLogs().length, 0); + } +} + +contract RewardManagerRecipientClaimMultiplePoolsTest is BaseRewardManagerTest { + uint256 internal constant POOL_DEPOSIT_AMOUNT = 10e18; + + function setUp() public override { + //setup contracts + super.setUp(); + + //create a two pools + createPrimaryPool(); + createSecondaryPool(); + + //add funds to each of the pools to be split among the recipients + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + addFundsToPool(SECONDARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + } + + function test_claimAllRecipientsSinglePool() public { + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //claim funds for each recipient within the pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //check the pool balance is still equal to DEPOSIT_AMOUNT as the test only claims for one of the pools + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + } + + function test_claimMultipleRecipientsSinglePool() public { + //claim the individual rewards for each recipient + claimRewards(SECONDARY_POOL_ARRAY, getSecondaryRecipients()[0].addr); + claimRewards(SECONDARY_POOL_ARRAY, getSecondaryRecipients()[1].addr); + + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check the recipients balance matches the ratio the recipient should have received + assertEq(getAssetBalance(getSecondaryRecipients()[0].addr), expectedRecipientAmount); + assertEq(getAssetBalance(getSecondaryRecipients()[1].addr), expectedRecipientAmount); + + //check the rewardManager has the remaining quantity + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT * 2 - (expectedRecipientAmount * 2)); + } + + function test_claimMultipleRecipientsMultiplePools() public { + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, getPrimaryRecipients()[0].addr); + claimRewards(PRIMARY_POOL_ARRAY, getPrimaryRecipients()[1].addr); + claimRewards(SECONDARY_POOL_ARRAY, getSecondaryRecipients()[0].addr); + claimRewards(SECONDARY_POOL_ARRAY, getSecondaryRecipients()[1].addr); + + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check the recipients balance matches the ratio the recipient should have received. The first recipient is shared across both pools so should receive 1/4 of each pool + assertEq(getAssetBalance(getPrimaryRecipients()[0].addr), expectedRecipientAmount * 2); + assertEq(getAssetBalance(getPrimaryRecipients()[1].addr), expectedRecipientAmount); + assertEq(getAssetBalance(getSecondaryRecipients()[1].addr), expectedRecipientAmount); + + //check the rewardManager has the remaining quantity + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + } + + function test_claimAllRecipientsMultiplePools() public { + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //claim funds for each recipient within the pool + for (uint256 i = 1; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //claim funds for each recipient within the pool + for (uint256 i = 1; i < getSecondaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory secondaryRecipient = getSecondaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(SECONDARY_POOL_ARRAY, secondaryRecipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(secondaryRecipient.addr), expectedRecipientAmount); + } + + //special case to handle the first recipient of each pool as they're the same address + Common.AddressAndWeight memory commonRecipient = getPrimaryRecipients()[0]; + + //claim the individual rewards for each pool + claimRewards(PRIMARY_POOL_ARRAY, commonRecipient.addr); + claimRewards(SECONDARY_POOL_ARRAY, commonRecipient.addr); + + //check the balance matches the ratio the recipient should have received, which is 1/4 of each deposit for each pool + assertEq(getAssetBalance(commonRecipient.addr), expectedRecipientAmount * 2); + } + + function test_claimSingleUniqueRecipient() public { + //the first recipient of the secondary pool is in both pools, so take the second recipient which is unique + Common.AddressAndWeight memory recipient = getSecondaryRecipients()[1]; + + //claim the individual rewards for this recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + claimRewards(SECONDARY_POOL_ARRAY, recipient.addr); + + //the recipient should have received 1/4 of the deposit amount + uint256 recipientExpectedAmount = POOL_DEPOSIT_AMOUNT / 4; + + //the recipient should have received 1/4 of the deposit amount + assertEq(getAssetBalance(recipient.addr), recipientExpectedAmount); + + //check the rewardManager has the remaining quantity + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT * 2 - recipientExpectedAmount); + } + + function test_claimSingleRecipientMultiplePools() public { + //the first recipient of the secondary pool is in both pools + Common.AddressAndWeight memory recipient = getSecondaryRecipients()[0]; + + //claim the individual rewards for this recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + claimRewards(SECONDARY_POOL_ARRAY, recipient.addr); + + //the recipient should have received 1/4 of the deposit amount for each pool + uint256 recipientExpectedAmount = (POOL_DEPOSIT_AMOUNT / 4) * 2; + + //this recipient belongs in both pools so should have received 1/4 of each + assertEq(getAssetBalance(recipient.addr), recipientExpectedAmount); + + //check the rewardManager has the remaining quantity + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT * 2 - recipientExpectedAmount); + } + + function test_claimUnregisteredRecipient() public { + //claim the individual rewards for this recipient + claimRewards(PRIMARY_POOL_ARRAY, getSecondaryRecipients()[1].addr); + claimRewards(SECONDARY_POOL_ARRAY, getPrimaryRecipients()[1].addr); + + //check the recipients didn't receive any fees from this pool + assertEq(getAssetBalance(getSecondaryRecipients()[1].addr), 0); + assertEq(getAssetBalance(getPrimaryRecipients()[1].addr), 0); + + //check the rewardManager has the remaining quantity + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT * 2); + } + + function test_claimUnevenAmountRoundsDown() public { + //adding an uneven amount of dust to each pool, this should round down to the nearest whole number with 4 remaining in the contract + addFundsToPool(PRIMARY_POOL_ID, getAsset(3), FEE_MANAGER); + addFundsToPool(SECONDARY_POOL_ID, getAsset(1), FEE_MANAGER); + + //the recipient should have received 1/4 of the deposit amount for each pool + uint256 recipientExpectedAmount = POOL_DEPOSIT_AMOUNT / 4; + + //claim funds for each recipient within the pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), recipientExpectedAmount); + } + + //special case to handle the first recipient of each pool as they're the same address + claimRewards(SECONDARY_POOL_ARRAY, getSecondaryRecipients()[0].addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(getSecondaryRecipients()[0].addr), recipientExpectedAmount * 2); + + //claim funds for each recipient of the secondary pool except the first + for (uint256 i = 1; i < getSecondaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getSecondaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(SECONDARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), recipientExpectedAmount); + } + + //contract should have 4 remaining + assertEq(getAssetBalance(address(rewardManager)), 4); + } + + function test_singleRecipientClaimMultipleDeposits() public { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getSecondaryRecipients()[0]; + + //claim the individual rewards for this recipient + claimRewards(SECONDARY_POOL_ARRAY, recipient.addr); + + //the recipient should have received 1/4 of the deposit amount + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check the recipients balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + + //check the rewardManager has the remaining quantity, which is 3/4 of the initial deposit plus the deposit from the second pool + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT * 2 - expectedRecipientAmount); + + //add funds to the pool to be split among the recipients + addFundsToPool(SECONDARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + + //claim the individual rewards for this recipient + claimRewards(SECONDARY_POOL_ARRAY, recipient.addr); + + //the recipient should have received 1/4 of the next deposit amount + expectedRecipientAmount += POOL_DEPOSIT_AMOUNT / 4; + + //check the recipients balance matches the ratio the recipient should have received, which is 1/4 of each deposit + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + + //check the rewardManager has the remaining quantity, which is now 3/4 of both deposits + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT * 3 - expectedRecipientAmount); + } + + function test_recipientsClaimMultipleDeposits() public { + //the recipient should have received 1/4 of the deposit amount + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //claim funds for each recipient within the pool + for (uint256 i; i < getSecondaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getSecondaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(SECONDARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //the reward manager balance should contain only the funds of the secondary pool + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + + //add funds to the pool to be split among the recipients + addFundsToPool(SECONDARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + + //special case to handle the first recipient of each pool as they're the same address + claimRewards(SECONDARY_POOL_ARRAY, getSecondaryRecipients()[0].addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(getSecondaryRecipients()[0].addr), expectedRecipientAmount * 2); + + //claim funds for each recipient within the pool except the first + for (uint256 i = 1; i < getSecondaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getSecondaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(SECONDARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount * 2); + } + + //the reward manager balance should again be the balance of the secondary pool as the primary pool has been emptied twice + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + } + + function test_claimEmptyPoolWhenSecondPoolContainsFunds() public { + //the recipient should have received 1/4 of the deposit amount + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //claim all rewards for each recipient in the primary pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //claim all the rewards again for the first recipient as that address is a member of both pools + claimRewards(PRIMARY_POOL_ARRAY, getSecondaryRecipients()[0].addr); + + //check the balance + assertEq(getAssetBalance(getSecondaryRecipients()[0].addr), expectedRecipientAmount); + } + + function test_getRewardsAvailableToRecipientInBothPools() public { + //get index 0 as this recipient is in both default pools + bytes32[] memory poolIds = rewardManager.getAvailableRewardPoolIds( + getPrimaryRecipients()[0].addr, + 0, + type(uint256).max + ); + + //check the recipient is in both pools + assertEq(poolIds[0], PRIMARY_POOL_ID); + assertEq(poolIds[1], SECONDARY_POOL_ID); + } + + function test_getRewardsAvailableToRecipientInSinglePool() public { + //get index 0 as this recipient is in both default pools + bytes32[] memory poolIds = rewardManager.getAvailableRewardPoolIds( + getPrimaryRecipients()[1].addr, + 0, + type(uint256).max + ); + + //check the recipient is in both pools + assertEq(poolIds[0], PRIMARY_POOL_ID); + assertEq(poolIds[1], ZERO_POOL_ID); + } + + function test_getRewardsAvailableToRecipientInNoPools() public view { + //get index 0 as this recipient is in both default pools + bytes32[] memory poolIds = rewardManager.getAvailableRewardPoolIds(FEE_MANAGER, 0, type(uint256).max); + + //check the recipient is in neither pool + assertEq(poolIds[0], ZERO_POOL_ID); + assertEq(poolIds[1], ZERO_POOL_ID); + } + + function test_getRewardsAvailableToRecipientInBothPoolsWhereAlreadyClaimed() public { + //get index 0 as this recipient is in both default pools + bytes32[] memory poolIds = rewardManager.getAvailableRewardPoolIds( + getPrimaryRecipients()[0].addr, + 0, + type(uint256).max + ); + + //check the recipient is in both pools + assertEq(poolIds[0], PRIMARY_POOL_ID); + assertEq(poolIds[1], SECONDARY_POOL_ID); + + //claim the rewards for each pool + claimRewards(PRIMARY_POOL_ARRAY, getPrimaryRecipients()[0].addr); + claimRewards(SECONDARY_POOL_ARRAY, getSecondaryRecipients()[0].addr); + + //get the available pools again + poolIds = rewardManager.getAvailableRewardPoolIds(getPrimaryRecipients()[0].addr, 0, type(uint256).max); + + //user should not be in any pool + assertEq(poolIds[0], ZERO_POOL_ID); + assertEq(poolIds[1], ZERO_POOL_ID); + } + + function test_getAvailableRewardsCursorCannotBeGreaterThanTotalPools() public { + vm.expectRevert(INVALID_POOL_LENGTH_SELECTOR); + + rewardManager.getAvailableRewardPoolIds(FEE_MANAGER, type(uint256).max, 0); + } + + function test_getAvailableRewardsCursorAndTotalPoolsEqual() public { + bytes32[] memory poolIds = rewardManager.getAvailableRewardPoolIds(getPrimaryRecipients()[0].addr, 2, 2); + + assertEq(poolIds.length, 0); + } + + function test_getAvailableRewardsCursorSingleResult() public { + bytes32[] memory poolIds = rewardManager.getAvailableRewardPoolIds(getPrimaryRecipients()[0].addr, 0, 1); + + assertEq(poolIds[0], PRIMARY_POOL_ID); + } +} + +contract RewardManagerRecipientClaimDifferentWeightsTest is BaseRewardManagerTest { + uint256 internal constant POOL_DEPOSIT_AMOUNT = 10e18; + + function setUp() public override { + //setup contracts + super.setUp(); + + //create a single pool for these tests + createPrimaryPool(); + + //add funds to the pool to be split among the recipients + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + } + + function getPrimaryRecipients() public virtual override returns (Common.AddressAndWeight[] memory) { + //array of recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + + //init each recipient with uneven weights + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, TEN_PERCENT); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, TEN_PERCENT * 8); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, ONE_PERCENT * 6); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, ONE_PERCENT * 4); + + return recipients; + } + + function test_allRecipientsClaimingReceiveExpectedAmount() public { + //loop all the recipients and claim their expected amount + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //the recipient should have received a share proportional to their weight + uint256 expectedRecipientAmount = (POOL_DEPOSIT_AMOUNT * recipient.weight) / POOL_SCALAR; + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + } +} + +contract RewardManagerRecipientClaimUnevenWeightTest is BaseRewardManagerTest { + uint256 internal constant POOL_DEPOSIT_AMOUNT = 10e18; + + function setUp() public override { + //setup contracts + super.setUp(); + + //create a single pool for these tests + createPrimaryPool(); + } + + function getPrimaryRecipients() public virtual override returns (Common.AddressAndWeight[] memory) { + //array of recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](2); + + uint64 oneThird = POOL_SCALAR / 3; + + //init each recipient with even weights. + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, oneThird); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, 2 * oneThird + 1); + + return recipients; + } + + function test_allRecipientsClaimingReceiveExpectedAmountWithSmallDeposit() public { + //add a smaller amount of funds to the pool + uint256 smallDeposit = 1e8; + + //add a smaller amount of funds to the pool + addFundsToPool(PRIMARY_POOL_ID, getAsset(smallDeposit), FEE_MANAGER); + + //loop all the recipients and claim their expected amount + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //the recipient should have received a share proportional to their weight + uint256 expectedRecipientAmount = (smallDeposit * recipient.weight) / POOL_SCALAR; + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //smaller deposits will consequently have less precision and will not be able to be split as evenly, the remaining 1 will be lost due to 333...|... being paid out instead of 333...4| + assertEq(getAssetBalance(address(rewardManager)), 1); + } + + function test_allRecipientsClaimingReceiveExpectedAmount() public { + //add funds to the pool to be split among the recipients + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + + //loop all the recipients and claim their expected amount + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //claim the individual rewards for each recipient + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //the recipient should have received a share proportional to their weight + uint256 expectedRecipientAmount = (POOL_DEPOSIT_AMOUNT * recipient.weight) / POOL_SCALAR; + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + + //their should be 0 wei left over indicating a successful split + assertEq(getAssetBalance(address(rewardManager)), 0); + } +} + +contract RewardManagerNoRecipientSet is BaseRewardManagerTest { + uint256 internal constant POOL_DEPOSIT_AMOUNT = 10e18; + + function setUp() public override { + //setup contracts + super.setUp(); + + //add funds to the pool to be split among the recipients once registered + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + } + + function test_claimAllRecipientsAfterRecipientsSet() public { + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //try and claim funds for each recipient within the pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //there should be no rewards claimed as the recipient is not registered + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the recipient received nothing + assertEq(getAssetBalance(recipient.addr), 0); + } + + //Set the recipients after the rewards have been paid into the pool + setRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), ADMIN); + + //claim funds for each recipient within the pool + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //get the recipient that is claiming + Common.AddressAndWeight memory recipient = getPrimaryRecipients()[i]; + + //there should be no rewards claimed as the recipient is registered + claimRewards(PRIMARY_POOL_ARRAY, recipient.addr); + + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipient.addr), expectedRecipientAmount); + } + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.general.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.general.t.sol new file mode 100644 index 00000000000..b2e0cf99bb9 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.general.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseRewardManagerTest} from "./BaseRewardManager.t.sol"; +import {RewardManager} from "../../RewardManager.sol"; +import {ERC20Mock} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/mocks/ERC20Mock.sol"; +import {IRewardManager} from "../../interfaces/IRewardManager.sol"; + +/** + * @title BaseRewardManagerTest + * @author Michael Fletcher + * @notice This contract will test the core functionality of the RewardManager contract + */ +contract RewardManagerSetupTest is BaseRewardManagerTest { + uint256 internal constant POOL_DEPOSIT_AMOUNT = 10e18; + + function setUp() public override { + //setup contracts + super.setUp(); + } + + function test_rejectsZeroLinkAddressOnConstruction() public { + //should revert if the contract is a zero address + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + + //create a rewardManager with a zero link address + new RewardManager(address(0)); + } + + function test_eventEmittedUponFeeManagerUpdate() public { + //expect the event to be emitted + vm.expectEmit(); + + //emit the event that is expected to be emitted + emit FeeManagerUpdated(FEE_MANAGER); + + //set the verifier proxy + setFeeManager(FEE_MANAGER, ADMIN); + } + + function test_eventEmittedUponFeePaid() public { + //create pool and add funds + createPrimaryPool(); + + //change to the feeManager who is the one who will be paying the fees + changePrank(FEE_MANAGER); + + //approve the amount being paid into the pool + ERC20Mock(getAsset(POOL_DEPOSIT_AMOUNT).assetAddress).approve(address(rewardManager), POOL_DEPOSIT_AMOUNT); + + IRewardManager.FeePayment[] memory payments = new IRewardManager.FeePayment[](1); + payments[0] = IRewardManager.FeePayment(PRIMARY_POOL_ID, uint192(POOL_DEPOSIT_AMOUNT)); + + //event is emitted when funds are added + vm.expectEmit(); + emit FeePaid(payments, FEE_MANAGER); + + //this represents the verifier adding some funds to the pool + rewardManager.onFeePaid(payments, FEE_MANAGER); + } + + function test_setFeeManagerZeroAddress() public { + //should revert if the contract is a zero address + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + + //set the verifier proxy + setFeeManager(address(0), ADMIN); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.payRecipients.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.payRecipients.t.sol new file mode 100644 index 00000000000..89fac663ba3 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.payRecipients.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseRewardManagerTest} from "./BaseRewardManager.t.sol"; +import {IRewardManager} from "../../interfaces/IRewardManager.sol"; + +/** + * @title BaseRewardManagerTest + * @author Michael Fletcher + * @notice This contract will test the payRecipients functionality of the RewardManager contract + */ +contract RewardManagerPayRecipientsTest is BaseRewardManagerTest { + uint256 internal constant POOL_DEPOSIT_AMOUNT = 10e18; + + function setUp() public override { + //setup contracts + super.setUp(); + + //create a single pool for these tests + createPrimaryPool(); + + //add funds to the pool to be split among the recipients + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + } + + function test_payAllRecipients() public { + //pay all the recipients in the pool + payRecipients(PRIMARY_POOL_ID, getPrimaryRecipientAddresses(), ADMIN); + + //each recipient should receive 1/4 of the pool + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check each recipient received the correct amount + for (uint256 i = 0; i < getPrimaryRecipientAddresses().length; i++) { + assertEq(getAssetBalance(getPrimaryRecipientAddresses()[i]), expectedRecipientAmount); + } + } + + function test_paySingleRecipient() public { + //get the first individual recipient + address recipient = getPrimaryRecipientAddresses()[0]; + + //get a single recipient as an array + address[] memory recipients = new address[](1); + recipients[0] = recipient; + + //pay a single recipient + payRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + + //the recipient should have received 1/4 of the deposit amount + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + assertEq(getAssetBalance(recipient), expectedRecipientAmount); + } + + function test_payRecipientWithInvalidPool() public { + //get the first individual recipient + address recipient = getPrimaryRecipientAddresses()[0]; + + //get a single recipient as an array + address[] memory recipients = new address[](1); + recipients[0] = recipient; + + //pay a single recipient + payRecipients(SECONDARY_POOL_ID, recipients, ADMIN); + + //the recipient should have received nothing + assertEq(getAssetBalance(recipient), 0); + } + + function test_payRecipientsEmptyRecipientList() public { + //get a single recipient + address[] memory recipients = new address[](0); + + //pay a single recipient + payRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + + //rewardManager should have the full balance + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + } + + function test_payAllRecipientsWithAdditionalUnregisteredRecipient() public { + //load all the recipients and add an additional one who is not in the pool + address[] memory recipients = new address[](getPrimaryRecipientAddresses().length + 1); + for (uint256 i = 0; i < getPrimaryRecipientAddresses().length; i++) { + recipients[i] = getPrimaryRecipientAddresses()[i]; + } + recipients[recipients.length - 1] = DEFAULT_RECIPIENT_5; + + //pay the recipients + payRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + + //each recipient should receive 1/4 of the pool except the last + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check each recipient received the correct amount + for (uint256 i = 0; i < getPrimaryRecipientAddresses().length; i++) { + assertEq(getAssetBalance(getPrimaryRecipientAddresses()[i]), expectedRecipientAmount); + } + + //the unregistered recipient should receive nothing + assertEq(getAssetBalance(DEFAULT_RECIPIENT_5), 0); + } + + function test_payAllRecipientsWithAdditionalInvalidRecipient() public { + //load all the recipients and add an additional one which is invalid, that should receive nothing + address[] memory recipients = new address[](getPrimaryRecipientAddresses().length + 1); + for (uint256 i = 0; i < getPrimaryRecipientAddresses().length; i++) { + recipients[i] = getPrimaryRecipientAddresses()[i]; + } + recipients[recipients.length - 1] = INVALID_ADDRESS; + + //pay the recipients + payRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + + //each recipient should receive 1/4 of the pool except the last + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check each recipient received the correct amount + for (uint256 i = 0; i < getPrimaryRecipientAddresses().length; i++) { + assertEq(getAssetBalance(getPrimaryRecipientAddresses()[i]), expectedRecipientAmount); + } + } + + function test_paySubsetOfRecipientsInPool() public { + //load a subset of the recipients into an array + address[] memory recipients = new address[](getPrimaryRecipientAddresses().length - 1); + for (uint256 i = 0; i < recipients.length; i++) { + recipients[i] = getPrimaryRecipientAddresses()[i]; + } + + //pay the subset of recipients + payRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + + //each recipient should receive 1/4 of the pool except the last + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check each subset of recipients received the correct amount + for (uint256 i = 0; i < recipients.length - 1; i++) { + assertEq(getAssetBalance(recipients[i]), expectedRecipientAmount); + } + + //check the pool has the remaining balance + assertEq( + getAssetBalance(address(rewardManager)), + POOL_DEPOSIT_AMOUNT - expectedRecipientAmount * recipients.length + ); + } + + function test_payAllRecipientsFromNonAdminUser() public { + //should revert if the caller isn't an admin or recipient within the pool + vm.expectRevert(UNAUTHORIZED_ERROR_SELECTOR); + + //pay all the recipients in the pool + payRecipients(PRIMARY_POOL_ID, getPrimaryRecipientAddresses(), FEE_MANAGER); + } + + function test_payAllRecipientsFromRecipientInPool() public { + //pay all the recipients in the pool + payRecipients(PRIMARY_POOL_ID, getPrimaryRecipientAddresses(), DEFAULT_RECIPIENT_1); + + //each recipient should receive 1/4 of the pool + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //check each recipient received the correct amount + for (uint256 i = 0; i < getPrimaryRecipientAddresses().length; i++) { + assertEq(getAssetBalance(getPrimaryRecipientAddresses()[i]), expectedRecipientAmount); + } + } + + function test_payRecipientsWithInvalidPoolId() public { + //pay all the recipients in the pool + payRecipients(INVALID_POOL_ID, getPrimaryRecipientAddresses(), ADMIN); + + //pool should still contain the full balance + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + } + + function test_addFundsToPoolAsOwner() public { + //add funds to the pool + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + } + + function test_addFundsToPoolAsNonOwnerOrFeeManager() public { + //should revert if the caller isn't an admin or recipient within the pool + vm.expectRevert(UNAUTHORIZED_ERROR_SELECTOR); + + IRewardManager.FeePayment[] memory payments = new IRewardManager.FeePayment[](1); + payments[0] = IRewardManager.FeePayment(PRIMARY_POOL_ID, uint192(POOL_DEPOSIT_AMOUNT)); + + //add funds to the pool + rewardManager.onFeePaid(payments, USER); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.setRecipients.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.setRecipients.t.sol new file mode 100644 index 00000000000..d3e6990bd9f --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.setRecipients.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseRewardManagerTest} from "./BaseRewardManager.t.sol"; +import {Common} from "../../../libraries/Common.sol"; + +/** + * @title BaseRewardManagerTest + * @author Michael Fletcher + * @notice This contract will test the setRecipient functionality of the RewardManager contract + */ +contract RewardManagerSetRecipientsTest is BaseRewardManagerTest { + function setUp() public override { + //setup contracts + super.setUp(); + } + + function test_setRewardRecipients() public { + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), ADMIN); + } + + function test_setRewardRecipientsIsEmpty() public { + //array of recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + + //should revert if the recipients array is empty + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_setRewardRecipientWithZeroWeight() public { + //array of recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](5); + + //init each recipient with even weights + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, ONE_PERCENT * 25); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, ONE_PERCENT * 25); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, ONE_PERCENT * 25); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, ONE_PERCENT * 25); + recipients[4] = Common.AddressAndWeight(DEFAULT_RECIPIENT_5, 0); + + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_setRewardRecipientWithZeroAddress() public { + //array of recipients + Common.AddressAndWeight[] memory recipients = getPrimaryRecipients(); + + //override the first recipient with a zero address + recipients[0].addr = address(0); + + //should revert if the recipients array is empty + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_setRewardRecipientWeights() public { + //array of recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + + //init each recipient with even weights + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, 25); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, 25); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, 25); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, 25); + + //should revert if the recipients array is empty + vm.expectRevert(INVALID_WEIGHT_ERROR_SELECTOR); + + //set the recipients with a recipient with a weight of 100% + setRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_setSingleRewardRecipient() public { + //array of recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](1); + + //init each recipient with even weights + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, POOL_SCALAR); + + //set the recipients with a recipient with a weight of 100% + setRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_setRewardRecipientTwice() public { + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), ADMIN); + + //should revert if recipients for this pool have already been set + vm.expectRevert(INVALID_POOL_ID_ERROR_SELECTOR); + + //set the recipients again + setRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), ADMIN); + } + + function test_setRewardRecipientFromNonOwnerOrFeeManagerAddress() public { + //should revert if the sender is not the owner or proxy + vm.expectRevert(UNAUTHORIZED_ERROR_SELECTOR); + + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), USER); + } + + function test_setRewardRecipientFromManagerAddress() public { + //update the proxy address + setFeeManager(FEE_MANAGER, ADMIN); + + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), FEE_MANAGER); + } + + function test_eventIsEmittedUponSetRecipients() public { + //expect an emit + vm.expectEmit(); + + //emit the event that is expected to be emitted + emit RewardRecipientsUpdated(PRIMARY_POOL_ID, getPrimaryRecipients()); + + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), ADMIN); + } + + function test_setRecipientContainsDuplicateRecipients() public { + //create a new array to hold the existing recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](getPrimaryRecipients().length * 2); + + //add all the existing recipients + for (uint256 i; i < getPrimaryRecipients().length; i++) { + recipients[i] = getPrimaryRecipients()[i]; + } + //add all the existing recipients again + for (uint256 i; i < getPrimaryRecipients().length; i++) { + recipients[i + getPrimaryRecipients().length] = getPrimaryRecipients()[i]; + } + + //should revert as the list contains a duplicate + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.updateRewardRecipients.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.updateRewardRecipients.t.sol new file mode 100644 index 00000000000..0d3a2b69b39 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.updateRewardRecipients.t.sol @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseRewardManagerTest} from "./BaseRewardManager.t.sol"; +import {Common} from "../../../libraries/Common.sol"; + +/** + * @title BaseRewardManagerTest + * @author Michael Fletcher + * @notice This contract will test the updateRecipient functionality of the RewardManager contract + */ +contract RewardManagerUpdateRewardRecipientsTest is BaseRewardManagerTest { + uint256 internal constant POOL_DEPOSIT_AMOUNT = 10e18; + + function setUp() public override { + //setup contracts + super.setUp(); + + //create a single pool for these tests + createPrimaryPool(); + + //add funds to the pool to be split among the recipients + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + } + + function test_onlyAdminCanUpdateRecipients() public { + //should revert if the caller is not the admin + vm.expectRevert(ONLY_CALLABLE_BY_OWNER_ERROR); + + //updating a recipient should force the funds to be paid out + updateRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), FEE_MANAGER); + } + + function test_updateAllRecipientsWithSameAddressAndWeight() public { + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //updating a recipient should force the funds to be paid out + updateRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), ADMIN); + + //check each recipient received the correct amount + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(getPrimaryRecipients()[i].addr), expectedRecipientAmount); + } + } + + function test_updatePartialRecipientsWithSameAddressAndWeight() public { + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //get a subset of the recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](2); + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, ONE_PERCENT * 25); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, ONE_PERCENT * 25); + + //updating a recipient should force the funds to be paid out + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + + //check each recipient received the correct amount + for (uint256 i; i < recipients.length; i++) { + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipients[i].addr), expectedRecipientAmount); + } + + //the reward manager should still have half remaining funds + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT / 2); + } + + function test_updateRecipientWithNewZeroAddress() public { + //create a new array to hold the existing recipients plus a new zero address + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](getPrimaryRecipients().length + 1); + + //add all the existing recipients + for (uint256 i; i < getPrimaryRecipients().length; i++) { + recipients[i] = getPrimaryRecipients()[i]; + } + //add a new address to the primary recipients + recipients[recipients.length - 1] = Common.AddressAndWeight(address(0), 0); + + //should revert if the recipient is a zero address + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + + //update the recipients with invalid address + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updateRecipientsContainsDuplicateRecipients() public { + //create a new array to hold the existing recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](getPrimaryRecipients().length * 2); + + //add all the existing recipients + for (uint256 i; i < getPrimaryRecipients().length; i++) { + recipients[i] = getPrimaryRecipients()[i]; + } + //add all the existing recipients again + for (uint256 i; i < getPrimaryRecipients().length; i++) { + recipients[i + getPrimaryRecipients().length] = getPrimaryRecipients()[i]; + } + + //should revert as the list contains a duplicate + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + + //update the recipients with the duplicate addresses + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updateRecipientsToDifferentSet() public { + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](getPrimaryRecipients().length + 4); + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //copy the recipient and set the weight to 0 which implies the recipient is being replaced + recipients[i] = Common.AddressAndWeight(getPrimaryRecipients()[i].addr, 0); + } + + //add the new recipients individually + recipients[4] = Common.AddressAndWeight(DEFAULT_RECIPIENT_5, ONE_PERCENT * 25); + recipients[5] = Common.AddressAndWeight(DEFAULT_RECIPIENT_6, ONE_PERCENT * 25); + recipients[6] = Common.AddressAndWeight(DEFAULT_RECIPIENT_7, ONE_PERCENT * 25); + recipients[7] = Common.AddressAndWeight(DEFAULT_RECIPIENT_8, ONE_PERCENT * 25); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updateRecipientsToDifferentPartialSet() public { + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](getPrimaryRecipients().length + 2); + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //copy the recipient and set the weight to 0 which implies the recipient is being replaced + recipients[i] = Common.AddressAndWeight(getPrimaryRecipients()[i].addr, 0); + } + + //add the new recipients individually + recipients[4] = Common.AddressAndWeight(DEFAULT_RECIPIENT_5, FIFTY_PERCENT); + recipients[5] = Common.AddressAndWeight(DEFAULT_RECIPIENT_6, FIFTY_PERCENT); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updateRecipientsToDifferentLargerSet() public { + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](getPrimaryRecipients().length + 5); + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //copy the recipient and set the weight to 0 which implies the recipient is being replaced + recipients[i] = Common.AddressAndWeight(getPrimaryRecipients()[i].addr, 0); + } + + //add the new recipients individually + recipients[4] = Common.AddressAndWeight(DEFAULT_RECIPIENT_5, TEN_PERCENT * 2); + recipients[5] = Common.AddressAndWeight(DEFAULT_RECIPIENT_6, TEN_PERCENT * 2); + recipients[6] = Common.AddressAndWeight(DEFAULT_RECIPIENT_7, TEN_PERCENT * 2); + recipients[7] = Common.AddressAndWeight(DEFAULT_RECIPIENT_8, TEN_PERCENT * 2); + recipients[8] = Common.AddressAndWeight(DEFAULT_RECIPIENT_9, TEN_PERCENT * 2); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updateRecipientsUpdateAndRemoveExistingForLargerSet() public { + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](9); + + //update the existing recipients + recipients[0] = Common.AddressAndWeight(getPrimaryRecipients()[0].addr, 0); + recipients[1] = Common.AddressAndWeight(getPrimaryRecipients()[1].addr, 0); + recipients[2] = Common.AddressAndWeight(getPrimaryRecipients()[2].addr, TEN_PERCENT * 3); + recipients[3] = Common.AddressAndWeight(getPrimaryRecipients()[3].addr, TEN_PERCENT * 3); + + //add the new recipients individually + recipients[4] = Common.AddressAndWeight(DEFAULT_RECIPIENT_5, TEN_PERCENT); + recipients[5] = Common.AddressAndWeight(DEFAULT_RECIPIENT_6, TEN_PERCENT); + recipients[6] = Common.AddressAndWeight(DEFAULT_RECIPIENT_7, TEN_PERCENT); + recipients[7] = Common.AddressAndWeight(DEFAULT_RECIPIENT_8, TEN_PERCENT); + recipients[8] = Common.AddressAndWeight(DEFAULT_RECIPIENT_9, TEN_PERCENT); + + //should revert as the weight does not equal 100% + vm.expectRevert(INVALID_WEIGHT_ERROR_SELECTOR); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updateRecipientsUpdateAndRemoveExistingForSmallerSet() public { + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](5); + + //update the existing recipients + recipients[0] = Common.AddressAndWeight(getPrimaryRecipients()[0].addr, 0); + recipients[1] = Common.AddressAndWeight(getPrimaryRecipients()[1].addr, 0); + recipients[2] = Common.AddressAndWeight(getPrimaryRecipients()[2].addr, TEN_PERCENT * 3); + recipients[3] = Common.AddressAndWeight(getPrimaryRecipients()[3].addr, TEN_PERCENT * 2); + + //add the new recipients individually + recipients[4] = Common.AddressAndWeight(DEFAULT_RECIPIENT_5, TEN_PERCENT * 5); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updateRecipientsToDifferentSetWithInvalidWeights() public { + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](getPrimaryRecipients().length + 2); + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //copy the recipient and set the weight to 0 which implies the recipient is being replaced + recipients[i] = Common.AddressAndWeight(getPrimaryRecipients()[i].addr, 0); + } + + //add the new recipients individually + recipients[4] = Common.AddressAndWeight(DEFAULT_RECIPIENT_5, TEN_PERCENT * 5); + recipients[5] = Common.AddressAndWeight(DEFAULT_RECIPIENT_6, TEN_PERCENT); + + //should revert as the weight will not equal 100% + vm.expectRevert(INVALID_WEIGHT_ERROR_SELECTOR); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updatePartialRecipientsToSubset() public { + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, 0); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, 0); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, TEN_PERCENT * 5); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, TEN_PERCENT * 5); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updatePartialRecipientsWithUnderWeightSet() public { + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, TEN_PERCENT); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, TEN_PERCENT); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, TEN_PERCENT); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, TEN_PERCENT); + + //should revert as the new weights exceed the previous weights being replaced + vm.expectRevert(INVALID_WEIGHT_ERROR_SELECTOR); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updatePartialRecipientsWithExcessiveWeight() public { + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, TEN_PERCENT); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, TEN_PERCENT); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, TEN_PERCENT); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, POOL_SCALAR); + + //should revert as the new weights exceed the previous weights being replaced + vm.expectRevert(INVALID_WEIGHT_ERROR_SELECTOR); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + } + + function test_updateRecipientWeights() public { + //expected recipient amount is 1/4 of the pool deposit for original recipients + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //create a list of containing recipients from the primary configured set with their new weights + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, TEN_PERCENT); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, TEN_PERCENT); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, TEN_PERCENT * 3); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, TEN_PERCENT * 5); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + + //check each recipient received the correct amount + for (uint256 i; i < recipients.length; i++) { + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipients[i].addr), expectedRecipientAmount); + } + + //the reward manager should have no funds remaining + assertEq(getAssetBalance(address(rewardManager)), 0); + + //add more funds to the pool to check new distribution + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + + //loop each user and claim the rewards + for (uint256 i; i < recipients.length; i++) { + //claim the rewards for this recipient + claimRewards(PRIMARY_POOL_ARRAY, recipients[i].addr); + } + + //manually check the balance of each recipient + assertEq( + getAssetBalance(DEFAULT_RECIPIENT_1), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT) / POOL_SCALAR + expectedRecipientAmount + ); + assertEq( + getAssetBalance(DEFAULT_RECIPIENT_2), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT) / POOL_SCALAR + expectedRecipientAmount + ); + assertEq( + getAssetBalance(DEFAULT_RECIPIENT_3), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT * 3) / POOL_SCALAR + expectedRecipientAmount + ); + assertEq( + getAssetBalance(DEFAULT_RECIPIENT_4), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT * 5) / POOL_SCALAR + expectedRecipientAmount + ); + } + + function test_partialUpdateRecipientWeights() public { + //expected recipient amount is 1/4 of the pool deposit for original recipients + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //create a list of containing recipients from the primary configured set with their new weights + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](2); + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, TEN_PERCENT); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, TEN_PERCENT * 4); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + + //check each recipient received the correct amount + for (uint256 i; i < recipients.length; i++) { + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipients[i].addr), expectedRecipientAmount); + } + + //the reward manager should have half the funds remaining + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT / 2); + + //add more funds to the pool to check new distribution + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + + //loop each user and claim the rewards + for (uint256 i; i < recipients.length; i++) { + //claim the rewards for this recipient + claimRewards(PRIMARY_POOL_ARRAY, recipients[i].addr); + } + + //manually check the balance of each recipient + assertEq( + getAssetBalance(DEFAULT_RECIPIENT_1), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT) / POOL_SCALAR + expectedRecipientAmount + ); + assertEq( + getAssetBalance(DEFAULT_RECIPIENT_2), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT * 4) / POOL_SCALAR + expectedRecipientAmount + ); + + //the reward manager should have half the funds remaining + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + } + + function test_eventIsEmittedUponUpdateRecipients() public { + //expect an emit + vm.expectEmit(); + + //emit the event that is expected to be emitted + emit RewardRecipientsUpdated(PRIMARY_POOL_ID, getPrimaryRecipients()); + + //expected recipient amount is 1/4 of the pool deposit + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //updating a recipient should force the funds to be paid out + updateRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), ADMIN); + + //check each recipient received the correct amount + for (uint256 i; i < getPrimaryRecipients().length; i++) { + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(getPrimaryRecipients()[i].addr), expectedRecipientAmount); + } + } +} + +contract RewardManagerUpdateRewardRecipientsMultiplePoolsTest is BaseRewardManagerTest { + uint256 internal constant POOL_DEPOSIT_AMOUNT = 10e18; + + function setUp() public override { + //setup contracts + super.setUp(); + + //create a single pool for these tests + createPrimaryPool(); + createSecondaryPool(); + + //add funds to the pool to be split among the recipients + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + addFundsToPool(SECONDARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + } + + function getSecondaryRecipients() public override returns (Common.AddressAndWeight[] memory) { + //for testing purposes, the primary and secondary pool to contain the same recipients + return getPrimaryRecipients(); + } + + function test_updatePrimaryRecipientWeights() public { + //expected recipient amount is 1/4 of the pool deposit for original recipients + uint256 expectedRecipientAmount = POOL_DEPOSIT_AMOUNT / 4; + + //create a list of containing recipients from the primary configured set, and new recipients + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](4); + recipients[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, TEN_PERCENT * 4); + recipients[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, TEN_PERCENT * 4); + recipients[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, TEN_PERCENT); + recipients[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, TEN_PERCENT); + + //updating a recipient should force the funds to be paid out for the primary recipients + updateRewardRecipients(PRIMARY_POOL_ID, recipients, ADMIN); + + //check each recipient received the correct amount + for (uint256 i; i < recipients.length; i++) { + //check the balance matches the ratio the recipient should have received + assertEq(getAssetBalance(recipients[i].addr), expectedRecipientAmount); + } + + //the reward manager should still have the funds for the secondary pool + assertEq(getAssetBalance(address(rewardManager)), POOL_DEPOSIT_AMOUNT); + + //add more funds to the pool to check new distribution + addFundsToPool(PRIMARY_POOL_ID, getAsset(POOL_DEPOSIT_AMOUNT), FEE_MANAGER); + + //claim the rewards for the updated recipients manually + claimRewards(PRIMARY_POOL_ARRAY, recipients[0].addr); + claimRewards(PRIMARY_POOL_ARRAY, recipients[1].addr); + claimRewards(PRIMARY_POOL_ARRAY, recipients[2].addr); + claimRewards(PRIMARY_POOL_ARRAY, recipients[3].addr); + + //check the balance matches the ratio the recipient who were updated should have received + assertEq( + getAssetBalance(recipients[0].addr), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT * 4) / POOL_SCALAR + expectedRecipientAmount + ); + assertEq( + getAssetBalance(recipients[1].addr), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT * 4) / POOL_SCALAR + expectedRecipientAmount + ); + assertEq( + getAssetBalance(recipients[2].addr), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT) / POOL_SCALAR + expectedRecipientAmount + ); + assertEq( + getAssetBalance(recipients[3].addr), + (POOL_DEPOSIT_AMOUNT * TEN_PERCENT) / POOL_SCALAR + expectedRecipientAmount + ); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/BaseVerifierTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/BaseVerifierTest.t.sol new file mode 100644 index 00000000000..db220f6547f --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/BaseVerifierTest.t.sol @@ -0,0 +1,557 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; +import {IERC165} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {IVerifier} from "../../interfaces/IVerifier.sol"; +import {ErroredVerifier} from "../mocks/ErroredVerifier.sol"; +import {Verifier} from "../../Verifier.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {AccessControllerInterface} from "../../../../shared/interfaces/AccessControllerInterface.sol"; +import {FeeManager} from "../../FeeManager.sol"; +import {Common} from "../../../libraries/Common.sol"; +import {ERC20Mock} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/mocks/ERC20Mock.sol"; +import {WERC20Mock} from "../../../../shared/mocks/WERC20Mock.sol"; +import {RewardManager} from "../../RewardManager.sol"; + +contract BaseTest is Test { + uint256 internal constant MAX_ORACLES = 31; + address internal constant ADMIN = address(1); + address internal constant USER = address(2); + address internal constant MOCK_VERIFIER_ADDRESS = address(100); + address internal constant MOCK_VERIFIER_ADDRESS_TWO = address(200); + address internal constant ACCESS_CONTROLLER_ADDRESS = address(300); + + bytes32 internal constant V_MASK = 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + bytes32 internal constant V1_BITMASK = 0x0001000000000000000000000000000000000000000000000000000000000000; + bytes32 internal constant V2_BITMASK = 0x0002000000000000000000000000000000000000000000000000000000000000; + bytes32 internal constant V3_BITMASK = 0x0003000000000000000000000000000000000000000000000000000000000000; + + uint256 internal constant SOURCE_CHAIN_ID = 0x1234; + address internal constant SOURCE_ADDRESS = address(0x1234); + + //version 0 feeds + bytes32 internal constant FEED_ID = (keccak256("ETH-USD") & V_MASK) | V1_BITMASK; + bytes32 internal constant FEED_ID_2 = (keccak256("LINK-USD") & V_MASK) | V1_BITMASK; + bytes32 internal constant FEED_ID_3 = (keccak256("BTC-USD") & V_MASK) | V1_BITMASK; + + //version 3 feeds + bytes32 internal constant FEED_ID_V3 = (keccak256("ETH-USD") & V_MASK) | V3_BITMASK; + + bytes32 internal constant INVALID_FEED = keccak256("INVALID"); + uint32 internal constant OBSERVATIONS_TIMESTAMP = 1000; + uint64 internal constant BLOCKNUMBER_LOWER_BOUND = 1000; + uint64 internal constant BLOCKNUMBER_UPPER_BOUND = BLOCKNUMBER_LOWER_BOUND + 5; + int192 internal constant MEDIAN = 1 ether; + int192 internal constant BID = 500000000 gwei; + int192 internal constant ASK = 2 ether; + + bytes32 internal constant EMPTY_BYTES = bytes32(""); + + uint8 internal constant FAULT_TOLERANCE = 10; + uint64 internal constant VERIFIER_VERSION = 1; + + string internal constant SERVER_URL = "https://mercury.server/client/"; + uint8 internal constant MAX_COMMITMENT_DELAY = 5; + + VerifierProxy internal s_verifierProxy; + Verifier internal s_verifier; + Verifier internal s_verifier_2; + ErroredVerifier internal s_erroredVerifier; + + struct Signer { + uint256 mockPrivateKey; + address signerAddress; + } + + struct V1Report { + // The feed ID the report has data for + bytes32 feedId; + // The time the median value was observed on + uint32 observationsTimestamp; + // The median value agreed in an OCR round + int192 median; + // The best bid value agreed in an OCR round + int192 bid; + // The best ask value agreed in an OCR round + int192 ask; + // The upper bound of the block range the median value was observed within + uint64 blocknumberUpperBound; + // The blockhash for the upper bound of block range (ensures correct blockchain) + bytes32 upperBlockhash; + // The lower bound of the block range the median value was observed within + uint64 blocknumberLowerBound; + // The current block timestamp + uint64 currentBlockTimestamp; + } + + Signer[MAX_ORACLES] internal s_signers; + bytes32[] internal s_offchaintransmitters; + bool private s_baseTestInitialized; + + function setUp() public virtual { + // BaseTest.setUp is often called multiple times from tests' setUp due to inheritance. + if (s_baseTestInitialized) return; + s_baseTestInitialized = true; + + vm.startPrank(ADMIN); + vm.mockCall( + MOCK_VERIFIER_ADDRESS, + abi.encodeWithSelector(IERC165.supportsInterface.selector, IVerifier.verify.selector), + abi.encode(true) + ); + s_verifierProxy = new VerifierProxy(AccessControllerInterface(address(0))); + + s_verifier = new Verifier(address(s_verifierProxy)); + s_verifier_2 = new Verifier(address(s_verifierProxy)); + s_erroredVerifier = new ErroredVerifier(); + + for (uint256 i; i < MAX_ORACLES; i++) { + uint256 mockPK = i + 1; + s_signers[i].mockPrivateKey = mockPK; + s_signers[i].signerAddress = vm.addr(mockPK); + } + } + + function _getSigners(uint256 numSigners) internal view returns (Signer[] memory) { + Signer[] memory signers = new Signer[](numSigners); + for (uint256 i; i < numSigners; i++) { + signers[i] = s_signers[i]; + } + return signers; + } + + function _getSignerAddresses(Signer[] memory signers) internal view returns (address[] memory) { + address[] memory signerAddrs = new address[](signers.length); + for (uint256 i = 0; i < signerAddrs.length; i++) { + signerAddrs[i] = s_signers[i].signerAddress; + } + return signerAddrs; + } + + function _generateSignerSignatures( + bytes memory report, + bytes32[3] memory reportContext, + Signer[] memory signers + ) internal pure returns (bytes32[] memory rawRs, bytes32[] memory rawSs, bytes32 rawVs) { + bytes32[] memory rs = new bytes32[](signers.length); + bytes32[] memory ss = new bytes32[](signers.length); + bytes memory vs = new bytes(signers.length); + + bytes32 hash = keccak256(abi.encodePacked(keccak256(report), reportContext)); + + for (uint256 i = 0; i < signers.length; i++) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signers[i].mockPrivateKey, hash); + rs[i] = r; + ss[i] = s; + vs[i] = bytes1(v - 27); + } + return (rs, ss, bytes32(vs)); + } + + function _encodeReport(V1Report memory report) internal pure returns (bytes memory) { + return + abi.encode( + report.feedId, + report.observationsTimestamp, + report.median, + report.bid, + report.ask, + report.blocknumberUpperBound, + report.upperBlockhash, + report.blocknumberLowerBound, + report.currentBlockTimestamp + ); + } + + function _generateV1EncodedBlob( + V1Report memory report, + bytes32[3] memory reportContext, + Signer[] memory signers + ) internal pure returns (bytes memory) { + bytes memory reportBytes = _encodeReport(report); + (bytes32[] memory rs, bytes32[] memory ss, bytes32 rawVs) = _generateSignerSignatures( + reportBytes, + reportContext, + signers + ); + return abi.encode(reportContext, reportBytes, rs, ss, rawVs); + } + + function _configDigestFromConfigData( + bytes32 feedId, + uint256 chainId, + address verifierAddr, + uint64 configCount, + address[] memory signers, + bytes32[] memory offchainTransmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) internal pure returns (bytes32) { + + // Convert addresses to bytes array to match configurator + bytes[] memory signersAsBytes = new bytes[](signers.length); + for (uint i; i < signers.length; ++i){ + signersAsBytes[i] = abi.encodePacked(signers[i]); + } + + uint256 h = uint256( + keccak256( + abi.encode( + feedId, + chainId, + verifierAddr, + configCount, + signersAsBytes, + offchainTransmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ) + ) + ); + uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 prefix = 0x0009 << (256 - 16); // 0x000900..00 + return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + } + + function _createV1Report( + bytes32 feedId, + uint32 observationsTimestamp, + int192 median, + int192 bid, + int192 ask, + uint64 blocknumberUpperBound, + bytes32 upperBlockhash, + uint64 blocknumberLowerBound, + uint32 currentBlockTimestamp + ) internal pure returns (V1Report memory) { + return + V1Report({ + feedId: feedId, + observationsTimestamp: observationsTimestamp, + median: median, + bid: bid, + ask: ask, + blocknumberUpperBound: blocknumberUpperBound, + upperBlockhash: upperBlockhash, + blocknumberLowerBound: blocknumberLowerBound, + currentBlockTimestamp: currentBlockTimestamp + }); + } + + function _ccipReadURL(bytes32 feedId, uint256 commitmentBlock) internal pure returns (string memory url) { + return + string( + abi.encodePacked( + SERVER_URL, + "?feedIDHex=", + Strings.toHexString(uint256(feedId)), + "&L2Blocknumber=", + Strings.toString(commitmentBlock) + ) + ); + } +} + +contract BaseTestWithConfiguredVerifierAndFeeManager is BaseTest { + FeeManager internal feeManager; + RewardManager internal rewardManager; + ERC20Mock internal link; + WERC20Mock internal native; + + uint256 internal constant DEFAULT_REPORT_LINK_FEE = 1e10; + uint256 internal constant DEFAULT_REPORT_NATIVE_FEE = 1e12; + + bytes32 internal v1ConfigDigest; + bytes32 internal v3ConfigDigest; + + struct V3Report { + // The feed ID the report has data for + bytes32 feedId; + // The time the median value was observed on + uint32 observationsTimestamp; + // The timestamp the report is valid from + uint32 validFromTimestamp; + // The link fee + uint192 linkFee; + // The native fee + uint192 nativeFee; + // The expiry of the report + uint32 expiresAt; + // The median value agreed in an OCR round + int192 benchmarkPrice; + // The best bid value agreed in an OCR round + int192 bid; + // The best ask value agreed in an OCR round + int192 ask; + } + + function setUp() public virtual override { + BaseTest.setUp(); + Signer[] memory signers = _getSigners(MAX_ORACLES); + + s_verifierProxy.initializeVerifier(address(s_verifier)); + + v1ConfigDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(signers), + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + v1ConfigDigest, + _getSignerAddresses(signers), + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + + v3ConfigDigest = _configDigestFromConfigData( + FEED_ID_V3, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(signers), + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + v3ConfigDigest, + _getSignerAddresses(signers), + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + + link = new ERC20Mock("LINK", "LINK", ADMIN, 0); + native = new WERC20Mock(); + + rewardManager = new RewardManager(address(link)); + feeManager = new FeeManager(address(link), address(native), address(s_verifierProxy), address(rewardManager)); + + s_verifierProxy.setFeeManager(feeManager); + rewardManager.setFeeManager(address(feeManager)); + } + + function _encodeReport(V3Report memory report) internal pure returns (bytes memory) { + return + abi.encode( + report.feedId, + report.observationsTimestamp, + report.validFromTimestamp, + report.nativeFee, + report.linkFee, + report.expiresAt, + report.benchmarkPrice, + report.bid, + report.ask + ); + } + + function _generateV3EncodedBlob( + V3Report memory report, + bytes32[3] memory reportContext, + Signer[] memory signers + ) internal pure returns (bytes memory) { + bytes memory reportBytes = _encodeReport(report); + (bytes32[] memory rs, bytes32[] memory ss, bytes32 rawVs) = _generateSignerSignatures( + reportBytes, + reportContext, + signers + ); + return abi.encode(reportContext, reportBytes, rs, ss, rawVs); + } + + function _generateV1Report() internal view returns (V1Report memory) { + return + _createV1Report( + FEED_ID, + OBSERVATIONS_TIMESTAMP, + MEDIAN, + BID, + ASK, + BLOCKNUMBER_UPPER_BOUND, + bytes32(blockhash(BLOCKNUMBER_UPPER_BOUND)), + BLOCKNUMBER_LOWER_BOUND, + uint32(block.timestamp) + ); + } + + function _generateV3Report() internal view returns (V3Report memory) { + return + V3Report({ + feedId: FEED_ID_V3, + observationsTimestamp: OBSERVATIONS_TIMESTAMP, + validFromTimestamp: uint32(block.timestamp), + nativeFee: uint192(DEFAULT_REPORT_NATIVE_FEE), + linkFee: uint192(DEFAULT_REPORT_LINK_FEE), + expiresAt: uint32(block.timestamp), + benchmarkPrice: MEDIAN, + bid: BID, + ask: ASK + }); + } + + function _generateReportContext(bytes32 configDigest) internal pure returns (bytes32[3] memory) { + bytes32[3] memory reportContext; + reportContext[0] = configDigest; + reportContext[1] = bytes32(abi.encode(uint32(5), uint8(1))); + return reportContext; + } + + function _approveLink(address spender, uint256 quantity, address sender) internal { + address originalAddr = msg.sender; + changePrank(sender); + + link.approve(spender, quantity); + changePrank(originalAddr); + } + + function _approveNative(address spender, uint256 quantity, address sender) internal { + address originalAddr = msg.sender; + changePrank(sender); + + native.approve(spender, quantity); + changePrank(originalAddr); + } + + function _verify(bytes memory payload, address feeAddress, uint256 wrappedNativeValue, address sender) internal { + address originalAddr = msg.sender; + changePrank(sender); + + s_verifierProxy.verify{value: wrappedNativeValue}(payload, abi.encode(feeAddress)); + + changePrank(originalAddr); + } + + function _verifyBulk( + bytes[] memory payload, + address feeAddress, + uint256 wrappedNativeValue, + address sender + ) internal { + address originalAddr = msg.sender; + changePrank(sender); + + s_verifierProxy.verifyBulk{value: wrappedNativeValue}(payload, abi.encode(feeAddress)); + + changePrank(originalAddr); + } +} + +contract BaseTestWithMultipleConfiguredDigests is BaseTestWithConfiguredVerifierAndFeeManager { + bytes32 internal s_configDigestOne; + bytes32 internal s_configDigestTwo; + bytes32 internal s_configDigestThree; + bytes32 internal s_configDigestFour; + bytes32 internal s_configDigestFive; + + uint32 internal s_numConfigsSet; + + uint8 internal constant FAULT_TOLERANCE_TWO = 2; + uint8 internal constant FAULT_TOLERANCE_THREE = 1; + + function setUp() public virtual override { + BaseTestWithConfiguredVerifierAndFeeManager.setUp(); + Signer[] memory signers = _getSigners(MAX_ORACLES); + + s_configDigestOne = v1ConfigDigest; + + // Verifier 1, Feed 1, Config 2 + Signer[] memory secondSetOfSigners = _getSigners(8); + s_configDigestTwo = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 2, + _getSignerAddresses(secondSetOfSigners), + s_offchaintransmitters, + FAULT_TOLERANCE_TWO, + bytes(""), + 2, + bytes("") + ); + s_verifier.setConfig( + s_configDigestTwo, + _getSignerAddresses(secondSetOfSigners), + FAULT_TOLERANCE_TWO, + new Common.AddressAndWeight[](0) + ); + + // Verifier 1, Feed 1, Config 3 + Signer[] memory thirdSetOfSigners = _getSigners(5); + s_configDigestThree = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 3, + _getSignerAddresses(thirdSetOfSigners), + s_offchaintransmitters, + FAULT_TOLERANCE_THREE, + bytes(""), + 3, + bytes("") + ); + s_verifier.setConfig( + s_configDigestThree, + _getSignerAddresses(thirdSetOfSigners), + FAULT_TOLERANCE_THREE, + new Common.AddressAndWeight[](0) + ); + + // Verifier 1, Feed 2, Config 1 + s_configDigestFour = _configDigestFromConfigData( + FEED_ID_2, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(signers), + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + 4, + bytes("") + ); + s_verifier.setConfig( + s_configDigestFour, + _getSignerAddresses(signers), + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + + // Verifier 2, Feed 3, Config 1 + s_verifierProxy.initializeVerifier(address(s_verifier_2)); + s_configDigestFive = _configDigestFromConfigData( + FEED_ID_3, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(signers), + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + 5, + bytes("") + ); + s_verifier_2.setConfig( + s_configDigestFive, + _getSignerAddresses(signers), + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierActivateConfigTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierActivateConfigTest.t.sol new file mode 100644 index 00000000000..b7d0975c143 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierActivateConfigTest.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTestWithConfiguredVerifierAndFeeManager, BaseTestWithMultipleConfiguredDigests} from "./BaseVerifierTest.t.sol"; +import {Verifier} from "../../Verifier.sol"; + +contract VerifierActivateConfigTest is BaseTestWithConfiguredVerifierAndFeeManager { + function test_revertsIfNotOwner() public { + vm.expectRevert("Only callable by owner"); + + changePrank(address(s_verifierProxy)); + s_verifier.activateConfig(bytes32("mock")); + } + + function test_revertsIfDigestIsEmpty() public { + vm.expectRevert(abi.encodeWithSelector(Verifier.DigestEmpty.selector)); + s_verifier.activateConfig(bytes32("")); + } + + function test_revertsIfDigestNotSet() public { + vm.expectRevert(abi.encodeWithSelector(Verifier.DigestNotSet.selector, bytes32("non-existent-digest"))); + s_verifier.activateConfig(bytes32("non-existent-digest")); + } +} + +contract VerifierActivateConfigWithDeactivatedConfigTest is BaseTestWithMultipleConfiguredDigests { + bytes32[3] internal s_reportContext; + + event ConfigActivated(bytes32 configDigest); + + V1Report internal s_testReportOne; + + function setUp() public override { + BaseTestWithMultipleConfiguredDigests.setUp(); + s_reportContext[0] = s_configDigestTwo; + s_reportContext[1] = bytes32(abi.encode(uint32(5), uint8(1))); + s_testReportOne = _createV1Report( + FEED_ID, + uint32(block.timestamp), + MEDIAN, + BID, + ASK, + uint64(block.number), + blockhash(block.number + 3), + uint64(block.number + 3), + uint32(block.timestamp) + ); + + s_verifier.deactivateConfig(s_configDigestTwo); + } + + function test_allowsVerification() public { + s_verifier.activateConfig(s_configDigestTwo); + changePrank(address(s_verifierProxy)); + + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE_TWO + 1) + ); + s_verifier.verify(signedReport, msg.sender); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyConstructorTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyConstructorTest.t.sol new file mode 100644 index 00000000000..811a9b440ee --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyConstructorTest.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseVerifierTest.t.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; +import {AccessControllerInterface} from "../../../../shared/interfaces/AccessControllerInterface.sol"; + +contract VerifierProxyConstructorTest is BaseTest { + function test_correctlySetsTheOwner() public { + VerifierProxy proxy = new VerifierProxy(AccessControllerInterface(address(0))); + assertEq(proxy.owner(), ADMIN); + } + + function test_correctlySetsTheCorrectAccessControllerInterface() public { + address accessControllerAddr = address(1234); + VerifierProxy proxy = new VerifierProxy(AccessControllerInterface(accessControllerAddr)); + assertEq(address(proxy.s_accessController()), accessControllerAddr); + } + + function test_correctlySetsVersion() public view { + string memory version = s_verifierProxy.typeAndVersion(); + assertEq(version, "VerifierProxy 2.0.0"); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyInitializeVerifierTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyInitializeVerifierTest.t.sol new file mode 100644 index 00000000000..e02b14fe56e --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyInitializeVerifierTest.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseVerifierTest.t.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; + +contract VerifierProxyInitializeVerifierTest is BaseTest { + bytes32 latestDigest; + + function setUp() public override { + BaseTest.setUp(); + } + + function test_revertsIfNotOwner() public { + changePrank(USER); + vm.expectRevert("Only callable by owner"); + s_verifierProxy.initializeVerifier(address(s_verifier)); + } + + function test_revertsIfZeroAddress() public { + vm.expectRevert(abi.encodeWithSelector(VerifierProxy.ZeroAddress.selector)); + s_verifierProxy.initializeVerifier(address(0)); + } + + function test_revertsIfVerifierAlreadyInitialized() public { + s_verifierProxy.initializeVerifier(address(s_verifier)); + vm.expectRevert(abi.encodeWithSelector(VerifierProxy.VerifierAlreadyInitialized.selector, address(s_verifier))); + s_verifierProxy.initializeVerifier(address(s_verifier)); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxySetAccessControllerTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxySetAccessControllerTest.t.sol new file mode 100644 index 00000000000..03bd6d97ee5 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxySetAccessControllerTest.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseVerifierTest.t.sol"; +import {AccessControllerInterface} from "../../../../shared/interfaces/AccessControllerInterface.sol"; + +contract VerifierProxySetAccessControllerTest is BaseTest { + event AccessControllerSet(address oldAccessController, address newAccessController); + + function test_revertsIfCalledByNonOwner() public { + vm.expectRevert("Only callable by owner"); + + changePrank(USER); + s_verifierProxy.setAccessController(AccessControllerInterface(ACCESS_CONTROLLER_ADDRESS)); + } + + function test_successfullySetsNewAccessController() public { + s_verifierProxy.setAccessController(AccessControllerInterface(ACCESS_CONTROLLER_ADDRESS)); + AccessControllerInterface ac = s_verifierProxy.s_accessController(); + assertEq(address(ac), ACCESS_CONTROLLER_ADDRESS); + } + + function test_successfullySetsNewAccessControllerIsEmpty() public { + s_verifierProxy.setAccessController(AccessControllerInterface(address(0))); + AccessControllerInterface ac = s_verifierProxy.s_accessController(); + assertEq(address(ac), address(0)); + } + + function test_emitsTheCorrectEvent() public { + vm.expectEmit(true, false, false, false); + emit AccessControllerSet(address(0), ACCESS_CONTROLLER_ADDRESS); + s_verifierProxy.setAccessController(AccessControllerInterface(ACCESS_CONTROLLER_ADDRESS)); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxySetVerifierTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxySetVerifierTest.t.sol new file mode 100644 index 00000000000..45f64bf5533 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxySetVerifierTest.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTestWithConfiguredVerifierAndFeeManager} from "./BaseVerifierTest.t.sol"; +import {IVerifier} from "../../interfaces/IVerifier.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; +import {IERC165} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {Common} from "../../../libraries/Common.sol"; + +contract VerifierProxyInitializeVerifierTest is BaseTestWithConfiguredVerifierAndFeeManager { + function test_revertsIfNotCorrectVerifier() public { + vm.expectRevert(abi.encodeWithSelector(VerifierProxy.AccessForbidden.selector)); + s_verifierProxy.setVerifier(bytes32("prev-config"), bytes32("new-config"), new Common.AddressAndWeight[](0)); + } + + function test_revertsIfDigestAlreadySet() public { + bytes32 takenDigest = v1ConfigDigest; + + address maliciousVerifier = address(666); + bytes32 maliciousDigest = bytes32("malicious-digest"); + vm.mockCall( + maliciousVerifier, + abi.encodeWithSelector(IERC165.supportsInterface.selector, IVerifier.verify.selector), + abi.encode(true) + ); + s_verifierProxy.initializeVerifier(maliciousVerifier); + vm.expectRevert( + abi.encodeWithSelector(VerifierProxy.ConfigDigestAlreadySet.selector, takenDigest, address(s_verifier)) + ); + changePrank(address(maliciousVerifier)); + s_verifierProxy.setVerifier(maliciousDigest, takenDigest, new Common.AddressAndWeight[](0)); + } + + function test_updatesVerifierIfVerifier() public { + bytes32 prevDigest = v1ConfigDigest; + changePrank(address(s_verifier)); + s_verifierProxy.setVerifier(prevDigest, bytes32("new-config"), new Common.AddressAndWeight[](0)); + assertEq(s_verifierProxy.getVerifier(bytes32("new-config")), address(s_verifier)); + assertEq(s_verifierProxy.getVerifier(prevDigest), address(s_verifier)); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyTest.t.sol new file mode 100644 index 00000000000..ea7e02d7409 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyTest.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTestWithConfiguredVerifierAndFeeManager} from "./BaseVerifierTest.t.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; +import {FeeManager} from "../../FeeManager.sol"; + +contract VerifierProxyInitializeVerifierTest is BaseTestWithConfiguredVerifierAndFeeManager { + function test_setFeeManagerZeroAddress() public { + vm.expectRevert(abi.encodeWithSelector(VerifierProxy.ZeroAddress.selector)); + s_verifierProxy.setFeeManager(FeeManager(address(0))); + } + + function test_setFeeManagerWhichDoesntHonourInterface() public { + vm.expectRevert(abi.encodeWithSelector(VerifierProxy.FeeManagerInvalid.selector)); + s_verifierProxy.setFeeManager(FeeManager(address(s_verifier))); + } + + function test_setFeeManagerWhichDoesntHonourIERC165Interface() public { + vm.expectRevert(); + s_verifierProxy.setFeeManager(FeeManager(address(1))); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyUnsetVerifierTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyUnsetVerifierTest.t.sol new file mode 100644 index 00000000000..74da281a3cd --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyUnsetVerifierTest.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest, BaseTestWithConfiguredVerifierAndFeeManager} from "./BaseVerifierTest.t.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; + +contract VerifierProxyUnsetVerifierTest is BaseTest { + function test_revertsIfNotAdmin() public { + vm.expectRevert("Only callable by owner"); + + changePrank(USER); + s_verifierProxy.unsetVerifier(bytes32("")); + } + + function test_revertsIfDigestDoesNotExist() public { + vm.expectRevert(abi.encodeWithSelector(VerifierProxy.VerifierNotFound.selector, bytes32(""))); + s_verifierProxy.unsetVerifier(bytes32("")); + } +} + +contract VerifierProxyUnsetVerifierWithPreviouslySetVerifierTest is BaseTestWithConfiguredVerifierAndFeeManager { + bytes32 internal s_configDigest; + + event VerifierUnset(bytes32 configDigest, address verifierAddr); + + function setUp() public override { + BaseTestWithConfiguredVerifierAndFeeManager.setUp(); + s_configDigest = v3ConfigDigest; + } + + function test_correctlyUnsetsVerifier() public { + s_verifierProxy.unsetVerifier(s_configDigest); + address verifierAddr = s_verifierProxy.getVerifier(s_configDigest); + assertEq(verifierAddr, address(0)); + } + + function test_emitsAnEventAfterUnsettingVerifier() public { + vm.expectEmit(true, false, false, false); + emit VerifierUnset(s_configDigest, address(s_verifier)); + s_verifierProxy.unsetVerifier(s_configDigest); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetConfigTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetConfigTest.t.sol new file mode 100644 index 00000000000..9276450e133 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetConfigTest.t.sol @@ -0,0 +1,578 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest, BaseTestWithMultipleConfiguredDigests} from "./BaseVerifierTest.t.sol"; +import {Verifier} from "../../Verifier.sol"; +import {Common} from "../../../libraries/Common.sol"; +import {MockConfigurator} from "../mocks/MockConfigurator.sol"; + +contract VerifierSetConfigTest is BaseTest { + function setUp() public virtual override { + BaseTest.setUp(); + } + + function test_revertsIfCalledByNonOwner() public { + vm.expectRevert("Only callable by owner"); + Signer[] memory signers = _getSigners(MAX_ORACLES); + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(signers), + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + changePrank(USER); + s_verifier.setConfig( + configDigest, + _getSignerAddresses(signers), + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + } + + function test_revertsIfSetWithTooManySigners() public { + address[] memory signers = new address[](MAX_ORACLES + 1); + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + signers, + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + vm.expectRevert(abi.encodeWithSelector(Verifier.ExcessSigners.selector, signers.length, MAX_ORACLES)); + s_verifier.setConfig( + configDigest, + signers, + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + } + + function test_revertsIfFaultToleranceIsZero() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(signers), + s_offchaintransmitters, + 0, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + vm.expectRevert(abi.encodeWithSelector(Verifier.FaultToleranceMustBePositive.selector)); + s_verifier.setConfig( + configDigest, + _getSignerAddresses(signers), + 0, + new Common.AddressAndWeight[](0) + ); + } + + function test_revertsIfNotEnoughSigners() public { + address[] memory signers = new address[](2); + signers[0] = address(1000); + signers[1] = address(1001); + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + signers, + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + vm.expectRevert( + abi.encodeWithSelector(Verifier.InsufficientSigners.selector, signers.length, FAULT_TOLERANCE * 3 + 1) + ); + s_verifier.setConfig( + configDigest, + signers, + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + } + + function test_revertsIfDuplicateSigners() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + signerAddrs[0] = signerAddrs[1]; + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + signerAddrs, + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + vm.expectRevert(abi.encodeWithSelector(Verifier.NonUniqueSignatures.selector)); + s_verifier.setConfig( + configDigest, + signerAddrs, + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + } + + function test_revertsIfSignerContainsZeroAddress() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + signerAddrs[0] = address(0); + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + signerAddrs, + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + vm.expectRevert(abi.encodeWithSelector(Verifier.ZeroAddress.selector)); + s_verifier.setConfig( + configDigest, + signerAddrs, + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + } + + function test_correctlyUpdatesTheConfig() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + + s_verifierProxy.initializeVerifier(address(s_verifier)); + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(signers), + s_offchaintransmitters, + FAULT_TOLERANCE, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + configDigest, + _getSignerAddresses(signers), + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + + uint32 blockNumber = s_verifier.latestConfigDetails(configDigest); + assertEq(blockNumber, block.number); + } +} + +contract VerifierUpdateConfigTest is BaseTest { + + function setUp() public virtual override { + BaseTest.setUp(); + + s_verifierProxy.initializeVerifier(address(s_verifier)); + } + + function test_updateConfig() public { + // Get initial signers and config digest + address[] memory signerAddresses = _getSignerAddresses(_getSigners(15)); + bytes32 configDigest = keccak256("test222"); + + // Set initial config + s_verifier.setConfig( + configDigest, + signerAddresses, + 4, + new Common.AddressAndWeight[](0) + ); + + // Unset the config + s_verifier.updateConfig(configDigest, signerAddresses, signerAddresses, 4); + } + + function test_updateConfigRevertsIfFIsZero() public { + // Get initial signers and config digest + address[] memory signerAddresses = _getSignerAddresses(_getSigners(15)); + bytes32 configDigest = keccak256("test"); + + // Set initial config + s_verifier.setConfig( + configDigest, + signerAddresses, + 4, + new Common.AddressAndWeight[](0) + ); + + // Try to update with f=0 + vm.expectRevert(Verifier.FaultToleranceMustBePositive.selector); + s_verifier.updateConfig(configDigest, signerAddresses, signerAddresses, 0); + } + + function test_updateConfigRevertsIfFTooHigh() public { + // Get initial signers and config digest + address[] memory signerAddresses = _getSignerAddresses(_getSigners(15)); + bytes32 configDigest = keccak256("test"); + + // Set initial config + s_verifier.setConfig( + configDigest, + signerAddresses, + 4, + new Common.AddressAndWeight[](0) + ); + + // Try to update with f too high + vm.expectRevert(abi.encodeWithSelector(Verifier.InsufficientSigners.selector, signerAddresses.length, 46)); + s_verifier.updateConfig(configDigest, signerAddresses, signerAddresses, 15); + } + + function test_updateConfigWithDifferentSigners() public { + // Get initial signers and config digest + address[] memory initialSigners = _getSignerAddresses(_getSigners(15)); + bytes32 configDigest = keccak256("test"); + + // Set initial config + s_verifier.setConfig( + configDigest, + initialSigners, + 4, + new Common.AddressAndWeight[](0) + ); + + // Get new signers + address[] memory newSigners = _getSignerAddresses(_getSigners(20)); + + // Update config with new signers + s_verifier.updateConfig(configDigest, initialSigners, newSigners, 6); + + // Verify config was updated + uint32 blockNumber = s_verifier.latestConfigDetails(configDigest); + assertEq(blockNumber, block.number); + } + + function test_updateConfigRevertsIfDigestNotSet() public { + address[] memory signerAddresses = _getSignerAddresses(_getSigners(15)); + bytes32 nonExistentDigest = keccak256("nonexistent"); + + vm.expectRevert(abi.encodeWithSelector(Verifier.DigestNotSet.selector, nonExistentDigest)); + s_verifier.updateConfig(nonExistentDigest, signerAddresses, signerAddresses, 4); + } + + function test_updateConfigRevertsIfPrevSignersLengthMismatch() public { + // Get initial signers and config digest + address[] memory initialSigners = _getSignerAddresses(_getSigners(15)); + bytes32 configDigest = keccak256("test"); + + // Set initial config + s_verifier.setConfig( + configDigest, + initialSigners, + 4, + new Common.AddressAndWeight[](0) + ); + + // Try to update with wrong number of previous signers + address[] memory wrongPrevSigners = _getSignerAddresses(_getSigners(10)); + address[] memory newSigners = _getSignerAddresses(_getSigners(15)); + + vm.expectRevert(Verifier.NonUniqueSignatures.selector); + s_verifier.updateConfig(configDigest, wrongPrevSigners, newSigners, 4); + } + + function test_updateConfigRevertsIfCalledByNonOwner() public { + address[] memory signerAddresses = _getSignerAddresses(_getSigners(15)); + bytes32 configDigest = keccak256("test"); + + // Set initial config + s_verifier.setConfig( + configDigest, + signerAddresses, + 4, + new Common.AddressAndWeight[](0) + ); + + // Try to update as non-owner + changePrank(USER); + vm.expectRevert("Only callable by owner"); + s_verifier.updateConfig(configDigest, signerAddresses, signerAddresses, 4); + } +} + +contract VerifierSetConfigWhenThereAreMultipleDigestsTest is BaseTestWithMultipleConfiguredDigests { + function test_correctlyUpdatesTheDigestInTheProxy() public { + Signer[] memory newSigners = _getSigners(15); + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(newSigners), + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + configDigest, + _getSignerAddresses(newSigners), + 4, + new Common.AddressAndWeight[](0) + ); + + address verifierAddr = s_verifierProxy.getVerifier(configDigest); + assertEq(verifierAddr, address(s_verifier)); + } + + function test_correctlyUpdatesDigestsOnMultipleVerifiersInTheProxy() public { + Signer[] memory newSigners = _getSigners(15); + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID_2, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(newSigners), + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + configDigest, + _getSignerAddresses(newSigners), + 4, + new Common.AddressAndWeight[](0) + ); + + address verifierAddr = s_verifierProxy.getVerifier(configDigest); + assertEq(verifierAddr, address(s_verifier)); + + bytes32 configDigest2 = _configDigestFromConfigData( + FEED_ID_3, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(newSigners), + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier_2.setConfig( + configDigest2, + _getSignerAddresses(newSigners), + 4, + new Common.AddressAndWeight[](0) + ); + + address verifierAddr2 = s_verifierProxy.getVerifier(configDigest2); + assertEq(verifierAddr2, address(s_verifier_2)); + } + + function test_correctlySetsConfigWhenDigestsAreRemoved() public { + s_verifier.deactivateConfig(s_configDigestTwo); + + Signer[] memory newSigners = _getSigners(15); + + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(newSigners), + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + configDigest, + _getSignerAddresses(newSigners), + 4, + new Common.AddressAndWeight[](0) + ); + + uint32 blockNumber = s_verifier.latestConfigDetails(configDigest); + + assertEq(blockNumber, block.number); + } + + function test_revertsIfDuplicateConfigIsSet() public { + // Set initial config + bytes32 configDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(_getSigners(15)), + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + configDigest, + _getSignerAddresses(_getSigners(15)), + 4, + new Common.AddressAndWeight[](0) + ); + + // Try to set same config again + vm.expectRevert(abi.encodeWithSelector(Verifier.ConfigDigestAlreadySet.selector)); + s_verifier.setConfig( + configDigest, + _getSignerAddresses(_getSigners(15)), + 4, + new Common.AddressAndWeight[](0) + ); + } + + function test_incrementalConfigUpdates() public { + // Set initial config + bytes32 configDigest1 = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 1, + _getSignerAddresses(_getSigners(15)), + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + configDigest1, + _getSignerAddresses(_getSigners(15)), + 4, + new Common.AddressAndWeight[](0) + ); + + // Set second config + bytes32 configDigest2 = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 2, + _getSignerAddresses(_getSigners(15)), + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + configDigest2, + _getSignerAddresses(_getSigners(15)), + 4, + new Common.AddressAndWeight[](0) + ); + + // Set third config + bytes32 configDigest3 = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 3, + _getSignerAddresses(_getSigners(15)), + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + configDigest3, + _getSignerAddresses(_getSigners(15)), + 4, + new Common.AddressAndWeight[](0) + ); + } + + function test_configDigestMatchesConfiguratorDigest() public { + MockConfigurator configurator = new MockConfigurator(); + + // Convert addresses to bytes array + Signer[] memory signers = _getSigners(15); + bytes[] memory signersAsBytes = new bytes[](signers.length); + for (uint i; i < signers.length; ++i){ + signersAsBytes[i] = abi.encodePacked(signers[i].signerAddress); + } + + configurator.setStagingConfig( + FEED_ID, + signersAsBytes, + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + bytes32 expectedConfigDigest = _configDigestFromConfigData( + FEED_ID, + block.chainid, + address(configurator), + 1, + _getSignerAddresses(signers), + s_offchaintransmitters, + 4, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + (,,bytes32 configDigest) = configurator.s_configurationStates(FEED_ID); + + assertEq(configDigest, expectedConfigDigest); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTest.t.sol new file mode 100644 index 00000000000..ec0268bb74a --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTest.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseVerifierTest.t.sol"; +import {Verifier} from "../../Verifier.sol"; + +contract VerifierConstructorTest is BaseTest { + function test_revertsIfInitializedWithEmptyVerifierProxy() public { + vm.expectRevert(abi.encodeWithSelector(Verifier.ZeroAddress.selector)); + new Verifier(address(0)); + } + + function test_setsTheCorrectProperties() public { + Verifier v = new Verifier(address(s_verifierProxy)); + assertEq(v.owner(), ADMIN); + + (uint32 blockNumber) = v.latestConfigDetails(FEED_ID); + assertEq(blockNumber, 0); + + string memory typeAndVersion = s_verifier.typeAndVersion(); + assertEq(typeAndVersion, "Verifier 2.0.0"); + } +} + +contract VerifierSupportsInterfaceTest is BaseTest { + function test_falseIfIsNotCorrectInterface() public view { + bool isInterface = s_verifier.supportsInterface(bytes4("abcd")); + assertEq(isInterface, false); + } + + function test_trueIfIsCorrectInterface() public view { + bool isInterface = s_verifier.supportsInterface(Verifier.verify.selector); + assertEq(isInterface, true); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestBillingReport.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestBillingReport.t.sol new file mode 100644 index 00000000000..ad67a239ba8 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestBillingReport.t.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTestWithConfiguredVerifierAndFeeManager} from "./BaseVerifierTest.t.sol"; + +contract VerifierTestWithConfiguredVerifierAndFeeManager is BaseTestWithConfiguredVerifierAndFeeManager { + uint256 internal constant DEFAULT_LINK_MINT_QUANTITY = 100 ether; + uint256 internal constant DEFAULT_NATIVE_MINT_QUANTITY = 100 ether; + + function setUp() public virtual override { + super.setUp(); + + //mint some tokens to the user + link.mint(USER, DEFAULT_LINK_MINT_QUANTITY); + native.mint(USER, DEFAULT_NATIVE_MINT_QUANTITY); + vm.deal(USER, DEFAULT_NATIVE_MINT_QUANTITY); + + //mint some link tokens to the feeManager pool + link.mint(address(feeManager), DEFAULT_REPORT_LINK_FEE); + } +} + +contract VerifierTestBillingReport is VerifierTestWithConfiguredVerifierAndFeeManager { + function test_verifyWithLink() public { + bytes memory signedReport = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + + _verify(signedReport, address(link), 0, USER); + + assertEq(link.balanceOf(USER), DEFAULT_LINK_MINT_QUANTITY - DEFAULT_REPORT_LINK_FEE); + } + + function test_verifyWithNative() public { + bytes memory signedReport = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + _approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE, USER); + + _verify(signedReport, address(native), 0, USER); + + assertEq(native.balanceOf(USER), DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE); + assertEq(link.balanceOf(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + } + + function test_verifyWithNativeUnwrapped() public { + bytes memory signedReport = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + _verify(signedReport, address(native), DEFAULT_REPORT_NATIVE_FEE, USER); + + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE); + assertEq(address(feeManager).balance, 0); + } + + function test_verifyWithNativeUnwrappedReturnsChange() public { + bytes memory signedReport = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + _verify(signedReport, address(native), DEFAULT_REPORT_NATIVE_FEE * 2, USER); + + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE); + assertEq(address(feeManager).balance, 0); + } +} + +contract VerifierBulkVerifyBillingReport is VerifierTestWithConfiguredVerifierAndFeeManager { + uint256 internal constant NUMBERS_OF_REPORTS = 5; + + function test_verifyWithBulkLink() public { + bytes memory signedReport = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes[] memory signedReports = new bytes[](NUMBERS_OF_REPORTS); + for (uint256 i = 0; i < NUMBERS_OF_REPORTS; i++) { + signedReports[i] = signedReport; + } + + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE * NUMBERS_OF_REPORTS, USER); + + _verifyBulk(signedReports, address(link), 0, USER); + + assertEq(link.balanceOf(USER), DEFAULT_LINK_MINT_QUANTITY - DEFAULT_REPORT_LINK_FEE * NUMBERS_OF_REPORTS); + assertEq(link.balanceOf(address(rewardManager)), DEFAULT_REPORT_LINK_FEE * NUMBERS_OF_REPORTS); + } + + function test_verifyWithBulkNative() public { + bytes memory signedReport = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes[] memory signedReports = new bytes[](NUMBERS_OF_REPORTS); + for (uint256 i = 0; i < NUMBERS_OF_REPORTS; i++) { + signedReports[i] = signedReport; + } + + _approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE * NUMBERS_OF_REPORTS, USER); + + _verifyBulk(signedReports, address(native), 0, USER); + + assertEq(native.balanceOf(USER), DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE * NUMBERS_OF_REPORTS); + } + + function test_verifyWithBulkNativeUnwrapped() public { + bytes memory signedReport = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes[] memory signedReports = new bytes[](NUMBERS_OF_REPORTS); + for (uint256 i; i < NUMBERS_OF_REPORTS; i++) { + signedReports[i] = signedReport; + } + + _verifyBulk(signedReports, address(native), DEFAULT_REPORT_NATIVE_FEE * NUMBERS_OF_REPORTS, USER); + + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE * 5); + assertEq(address(feeManager).balance, 0); + } + + function test_verifyWithBulkNativeUnwrappedReturnsChange() public { + bytes memory signedReport = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes[] memory signedReports = new bytes[](NUMBERS_OF_REPORTS); + for (uint256 i = 0; i < NUMBERS_OF_REPORTS; i++) { + signedReports[i] = signedReport; + } + + _verifyBulk(signedReports, address(native), DEFAULT_REPORT_NATIVE_FEE * (NUMBERS_OF_REPORTS * 2), USER); + + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY - DEFAULT_REPORT_NATIVE_FEE * NUMBERS_OF_REPORTS); + assertEq(address(feeManager).balance, 0); + } + + function test_verifyMultiVersions() public { + bytes memory signedReportV1 = _generateV1EncodedBlob( + _generateV1Report(), + _generateReportContext(v1ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes memory signedReportV3 = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes[] memory signedReports = new bytes[](3); + + signedReports[0] = signedReportV1; + signedReports[1] = signedReportV3; + signedReports[2] = signedReportV3; + + _approveLink(address(rewardManager), 2 * DEFAULT_REPORT_LINK_FEE, USER); + + _verifyBulk(signedReports, address(link), 0, USER); + + assertEq(link.balanceOf(USER), DEFAULT_LINK_MINT_QUANTITY - 2 * DEFAULT_REPORT_LINK_FEE); + assertEq(native.balanceOf(USER), DEFAULT_NATIVE_MINT_QUANTITY); + assertEq(link.balanceOf(address(rewardManager)), DEFAULT_REPORT_LINK_FEE * 2); + } + + function test_verifyMultiVersionsReturnsVerifiedReports() public { + bytes memory signedReportV1 = _generateV1EncodedBlob( + _generateV1Report(), + _generateReportContext(v1ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes memory signedReportV3 = _generateV3EncodedBlob( + _generateV3Report(), + _generateReportContext(v3ConfigDigest), + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes[] memory signedReports = new bytes[](3); + + signedReports[0] = signedReportV1; + signedReports[1] = signedReportV3; + signedReports[2] = signedReportV3; + + _approveLink(address(rewardManager), 2 * DEFAULT_REPORT_LINK_FEE, USER); + + address originalAddr = msg.sender; + changePrank(USER); + + bytes[] memory verifierReports = s_verifierProxy.verifyBulk{value: 0}(signedReports, abi.encode(link)); + + changePrank(originalAddr); + + assertEq(verifierReports[0], _encodeReport(_generateV1Report())); + assertEq(verifierReports[1], _encodeReport(_generateV3Report())); + assertEq(verifierReports[2], _encodeReport(_generateV3Report())); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierUnsetConfigTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierUnsetConfigTest.t.sol new file mode 100644 index 00000000000..64c9f77a7e7 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierUnsetConfigTest.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTestWithMultipleConfiguredDigests} from "./BaseVerifierTest.t.sol"; +import {Verifier} from "../../Verifier.sol"; + +contract VerificationdeactivateConfigWhenThereAreMultipleDigestsTest is BaseTestWithMultipleConfiguredDigests { + function test_revertsIfCalledByNonOwner() public { + vm.expectRevert("Only callable by owner"); + + changePrank(USER); + s_verifier.deactivateConfig(bytes32("")); + } + + function test_revertsIfRemovingAnEmptyDigest() public { + vm.expectRevert(abi.encodeWithSelector(Verifier.DigestEmpty.selector)); + s_verifier.deactivateConfig(bytes32("")); + } + + function test_revertsIfRemovingAnNonExistentDigest() public { + vm.expectRevert(abi.encodeWithSelector(Verifier.DigestNotSet.selector, bytes32("mock-digest"))); + s_verifier.deactivateConfig(bytes32("mock-digest")); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierVerifyTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierVerifyTest.t.sol new file mode 100644 index 00000000000..8441bf6d06b --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierVerifyTest.t.sol @@ -0,0 +1,377 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTestWithConfiguredVerifierAndFeeManager} from "./BaseVerifierTest.t.sol"; +import {Verifier} from "../../Verifier.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; +import {AccessControllerInterface} from "../../../../shared/interfaces/AccessControllerInterface.sol"; +import {Common} from "../../../libraries/Common.sol"; + +contract VerifierVerifyTest is BaseTestWithConfiguredVerifierAndFeeManager { + bytes32[3] internal s_reportContext; + + event ReportVerified(bytes32 indexed feedId, address requester); + + V1Report internal s_testReportOne; + + function setUp() public virtual override { + BaseTestWithConfiguredVerifierAndFeeManager.setUp(); + s_reportContext[0] = v1ConfigDigest; + s_reportContext[1] = bytes32(abi.encode(uint32(5), uint8(1))); + s_testReportOne = _createV1Report( + FEED_ID, + OBSERVATIONS_TIMESTAMP, + MEDIAN, + BID, + ASK, + BLOCKNUMBER_UPPER_BOUND, + blockhash(BLOCKNUMBER_UPPER_BOUND), + BLOCKNUMBER_LOWER_BOUND, + uint32(block.timestamp) + ); + } + + function assertReportsEqual(bytes memory response, V1Report memory testReport) public pure { + ( + bytes32 feedId, + uint32 timestamp, + int192 median, + int192 bid, + int192 ask, + uint64 blockNumUB, + bytes32 upperBlockhash, + uint64 blockNumLB + ) = abi.decode(response, (bytes32, uint32, int192, int192, int192, uint64, bytes32, uint64)); + assertEq(feedId, testReport.feedId); + assertEq(timestamp, testReport.observationsTimestamp); + assertEq(median, testReport.median); + assertEq(bid, testReport.bid); + assertEq(ask, testReport.ask); + assertEq(blockNumLB, testReport.blocknumberLowerBound); + assertEq(blockNumUB, testReport.blocknumberUpperBound); + assertEq(upperBlockhash, testReport.upperBlockhash); + } +} + +contract VerifierProxyVerifyTest is VerifierVerifyTest { + function test_revertsIfNoVerifierConfigured() public { + s_reportContext[0] = bytes32("corrupt-digest"); + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + vm.expectRevert(abi.encodeWithSelector(VerifierProxy.VerifierNotFound.selector, bytes32("corrupt-digest"))); + s_verifierProxy.verify(signedReport, bytes("")); + } + + function test_proxiesToTheCorrectVerifier() public { + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes memory response = s_verifierProxy.verify(signedReport, abi.encode(native)); + assertReportsEqual(response, s_testReportOne); + } +} + +contract VerifierProxyAccessControlledVerificationTest is VerifierVerifyTest { + function setUp() public override { + VerifierVerifyTest.setUp(); + AccessControllerInterface accessController = AccessControllerInterface(ACCESS_CONTROLLER_ADDRESS); + + s_verifierProxy.setAccessController(accessController); + } + + function test_revertsIfNoAccess() public { + vm.mockCall( + ACCESS_CONTROLLER_ADDRESS, + abi.encodeWithSelector(AccessControllerInterface.hasAccess.selector, USER), + abi.encode(false) + ); + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + vm.expectRevert(abi.encodeWithSelector(VerifierProxy.AccessForbidden.selector)); + + changePrank(USER); + s_verifierProxy.verify(signedReport, abi.encode(native)); + } + + function test_proxiesToTheVerifierIfHasAccess() public { + vm.mockCall( + ACCESS_CONTROLLER_ADDRESS, + abi.encodeWithSelector(AccessControllerInterface.hasAccess.selector, USER), + abi.encode(true) + ); + + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + + changePrank(USER); + bytes memory response = s_verifierProxy.verify(signedReport, bytes("")); + assertReportsEqual(response, s_testReportOne); + } +} + +contract VerifierVerifySingleConfigDigestTest is VerifierVerifyTest { + function test_revertsIfVerifiedByNonProxy() public { + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + vm.expectRevert(abi.encodeWithSelector(Verifier.AccessForbidden.selector)); + s_verifier.verify(signedReport, msg.sender); + } + + function test_revertsIfVerifiedWithIncorrectAddresses() public { + Signer[] memory signers = _getSigners(FAULT_TOLERANCE + 1); + signers[10].mockPrivateKey = 1234; + bytes memory signedReport = _generateV1EncodedBlob(s_testReportOne, s_reportContext, signers); + changePrank(address(s_verifierProxy)); + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_verifier.verify(signedReport, msg.sender); + } + + function test_revertsIfMismatchedSignatureLength() public { + bytes32[] memory rs = new bytes32[](FAULT_TOLERANCE + 1); + bytes32[] memory ss = new bytes32[](FAULT_TOLERANCE + 3); + bytes32 rawVs = bytes32(""); + bytes memory signedReport = abi.encode(s_reportContext, abi.encode(s_testReportOne), rs, ss, rawVs); + changePrank(address(s_verifierProxy)); + vm.expectRevert(abi.encodeWithSelector(Verifier.MismatchedSignatures.selector, rs.length, ss.length)); + s_verifier.verify(signedReport, msg.sender); + } + + function test_revertsIfConfigDigestNotSet() public { + bytes32[3] memory reportContext = s_reportContext; + reportContext[0] = bytes32("wrong-context-digest"); + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + vm.expectRevert(abi.encodeWithSelector(Verifier.DigestInactive.selector, reportContext[0])); + changePrank(address(s_verifierProxy)); + s_verifier.verify(signedReport, msg.sender); + } + + function test_revertsIfReportHasUnconfiguredConfigDigest() public { + V1Report memory report = _createV1Report( + FEED_ID_2, + OBSERVATIONS_TIMESTAMP, + MEDIAN, + BID, + ASK, + BLOCKNUMBER_UPPER_BOUND, + blockhash(BLOCKNUMBER_UPPER_BOUND), + BLOCKNUMBER_LOWER_BOUND, + uint32(block.timestamp) + ); + s_reportContext[0] = keccak256("unconfigured-digesty"); + bytes memory signedReport = _generateV1EncodedBlob(report, s_reportContext, _getSigners(FAULT_TOLERANCE + 1)); + vm.expectRevert(abi.encodeWithSelector(Verifier.DigestInactive.selector, s_reportContext[0])); + changePrank(address(s_verifierProxy)); + s_verifier.verify(signedReport, msg.sender); + } + + function test_revertsIfWrongNumberOfSigners() public { + bytes memory signedReport = _generateV1EncodedBlob(s_testReportOne, s_reportContext, _getSigners(10)); + vm.expectRevert(abi.encodeWithSelector(Verifier.IncorrectSignatureCount.selector, 10, FAULT_TOLERANCE + 1)); + changePrank(address(s_verifierProxy)); + s_verifier.verify(signedReport, msg.sender); + } + + function test_revertsIfDuplicateSignersHaveSigned() public { + Signer[] memory signers = _getSigners(FAULT_TOLERANCE + 1); + // Duplicate signer at index 1 + signers[0] = signers[1]; + bytes memory signedReport = _generateV1EncodedBlob(s_testReportOne, s_reportContext, signers); + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + changePrank(address(s_verifierProxy)); + s_verifier.verify(signedReport, msg.sender); + } + + function test_returnsThePriceAndBlockNumIfReportVerified() public { + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + changePrank(address(s_verifierProxy)); + bytes memory response = s_verifier.verify(signedReport, msg.sender); + + assertReportsEqual(response, s_testReportOne); + } + + function test_emitsAnEventIfReportVerified() public { + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + vm.expectEmit(true, true, true, true, address(s_verifier)); + emit ReportVerified(s_testReportOne.feedId, msg.sender); + changePrank(address(s_verifierProxy)); + s_verifier.verify(signedReport, msg.sender); + } +} + +contract VerifierVerifyMultipleConfigDigestTest is VerifierVerifyTest { + bytes32 internal s_oldConfigDigest; + bytes32 internal s_newConfigDigest; + + uint8 internal constant FAULT_TOLERANCE_TWO = 5; + + function setUp() public override { + VerifierVerifyTest.setUp(); + s_oldConfigDigest = v1ConfigDigest; + + s_newConfigDigest = _configDigestFromConfigData( + FEED_ID, + SOURCE_CHAIN_ID, + SOURCE_ADDRESS, + 2, + _getSignerAddresses(_getSigners(20)), + s_offchaintransmitters, + FAULT_TOLERANCE_TWO, + bytes(""), + VERIFIER_VERSION, + bytes("") + ); + + s_verifier.setConfig( + s_newConfigDigest, + _getSignerAddresses(_getSigners(20)), + FAULT_TOLERANCE_TWO, + new Common.AddressAndWeight[](0) + ); + } + + function test_revertsIfVerifyingWithAnUnsetDigest() public { + s_verifier.deactivateConfig(s_oldConfigDigest); + + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + changePrank(address(s_verifierProxy)); + vm.expectRevert(abi.encodeWithSelector(Verifier.DigestInactive.selector, s_reportContext[0])); + s_verifier.verify(signedReport, msg.sender); + } + + function test_canVerifyOlderReportsWithOlderConfigs() public { + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + changePrank(address(s_verifierProxy)); + bytes memory response = s_verifier.verify(signedReport, msg.sender); + assertReportsEqual(response, s_testReportOne); + } + + function test_canVerifyNewerReportsWithNewerConfigs() public { + s_reportContext[0] = s_newConfigDigest; + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE_TWO + 1) + ); + changePrank(address(s_verifierProxy)); + bytes memory response = s_verifier.verify(signedReport, msg.sender); + assertReportsEqual(response, s_testReportOne); + } + + function test_revertsIfAReportIsVerifiedWithAnExistingButIncorrectDigest() public { + // Try sending the older digest signed with the new set of signers + s_reportContext[0] = s_oldConfigDigest; + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(FAULT_TOLERANCE_TWO + 1) + ); + vm.expectRevert( + abi.encodeWithSelector(Verifier.IncorrectSignatureCount.selector, FAULT_TOLERANCE_TWO + 1, FAULT_TOLERANCE + 1) + ); + changePrank(address(s_verifierProxy)); + s_verifier.verify(signedReport, msg.sender); + } + + function test_verifyAfterConfigUpdate() public { + // Get initial signers and set initial config + address[] memory initialSigners = _getSignerAddresses(_getSigners(15)); + bytes32 configDigest = keccak256("test"); + + s_verifier.setConfig( + configDigest, + initialSigners, + 4, + new Common.AddressAndWeight[](0) + ); + + // Get new signers and update config + address[] memory newSigners = _getSignerAddresses(_getSigners(20)); + s_verifier.updateConfig(configDigest, initialSigners, newSigners, 6); + + // Verify report with new signers should pass + s_reportContext[0] = configDigest; + bytes memory signedReportNewSigners = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(7) // More than f=6 signers + ); + + bytes memory response = s_verifierProxy.verify(signedReportNewSigners, bytes("")); + assertReportsEqual(response, s_testReportOne); + + // Verify report with old signers should fail + bytes memory signedReportOldSigners = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(5) // Old number of signers + ); + vm.expectRevert( + abi.encodeWithSelector(Verifier.IncorrectSignatureCount.selector, 5, 7) + ); + + s_verifierProxy.verify(signedReportOldSigners, bytes("")); + } + + function test_verifyAfterConfigUpdateWithExistingSigners() public { + // Get initial signers and set initial config + address[] memory signers = _getSignerAddresses(_getSigners(15)); + bytes32 configDigest = keccak256("test"); + + s_verifier.setConfig( + configDigest, + signers, + 4, + new Common.AddressAndWeight[](0) + ); + + // Update config with same signers and f + s_verifier.updateConfig(configDigest, signers, signers, 4); + + // Verify report should pass + s_reportContext[0] = configDigest; + bytes memory signedReport = _generateV1EncodedBlob( + s_testReportOne, + s_reportContext, + _getSigners(5) // More than f=4 signers + ); + + bytes memory response = s_verifierProxy.verify(signedReport, bytes("")); + assertReportsEqual(response, s_testReportOne); + + } +}