diff --git a/contracts/deployment-info.json b/contracts/deployment-info.json new file mode 100644 index 00000000000..1da619e685e --- /dev/null +++ b/contracts/deployment-info.json @@ -0,0 +1,7 @@ +{ + "contracts": { + "DestinationVerifierProxy": { + "address": "0x9E642fFD7e30444AacF75Cd31c65A100EA84b029" + } + } +} diff --git a/contracts/deployment-info.json.bu b/contracts/deployment-info.json.bu new file mode 100644 index 00000000000..76fc996855e --- /dev/null +++ b/contracts/deployment-info.json.bu @@ -0,0 +1,28 @@ +{ + "contracts": { + "DestinationVerifierProxy": { + "address": "0x2C035CcA662ab487a332c86Fe3099Df010F493C1" + }, + "DestinationVerifier": { + "address": "0x50F740fb1C4eC0D4c0A994e06fC7737Be47b43ea", + "params": { + "destinationVerifierProxy": "0x2C035CcA662ab487a332c86Fe3099Df010F493C1" + } + }, + "DestinationFeeManager": { + "address": "0xd7bc414e453027D126a618874Ae81a232C3a71C1", + "params": { + "linkToken": "0x779877A7B0D9E8603169DdbD7836e478b4624789", + "nativeToken": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", + "verifier": "0x50F740fb1C4eC0D4c0A994e06fC7737Be47b43ea", + "rewardManager": "0x9ba07C3Ae6E23EEc301f5A5f4c3D14c0510f5f74" + } + }, + "DestinationRewardManager": { + "address": "0x9ba07C3Ae6E23EEc301f5A5f4c3D14c0510f5f74", + "params": { + "linkToken": "0x779877A7B0D9E8603169DdbD7836e478b4624789" + } + } + } +} diff --git a/contracts/src/v0.8/feeds/DualAggregatorOriginal.sol b/contracts/src/v0.8/feeds/DualAggregatorOriginal.sol new file mode 100644 index 00000000000..9c9827e0392 --- /dev/null +++ b/contracts/src/v0.8/feeds/DualAggregatorOriginal.sol @@ -0,0 +1,1623 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.19; + +import "./interfaces/AccessControllerInterface.sol"; +import "./interfaces/AggregatorV2V3Interface.sol"; +import "./interfaces/AggregatorValidatorInterface.sol"; +import "./interfaces/LinkTokenInterface.sol"; +import "./interfaces/TypeAndVersionInterface.sol"; +import "./OCR2Abstract.sol"; +import "./OwnerIsCreator.sol"; + + +/** + * @notice OCR2Aggregator for numerical data with billing support. + + * @dev + * If you read or change this, be sure to read or adjust the comments. They + * track the units of the values under consideration, and are crucial to + * the readability of the operations it specifies. + + * @notice + * Billing Trust Model: + + * Nothing in this contract prevents a billing admin from setting insane + * values for the billing parameters in setBilling. Oracles + * participating in this contract should regularly check that the + * parameters make sense. Similarly, the outstanding obligations of this + * contract to the oracles can exceed the funds held by the contract. + * Oracles participating in this contract should regularly check that it + * holds sufficient funds and stop interacting with it if funding runs + * out. + + * This still leaves oracles with some risk due to TOCTOU issues. + * However, since the sums involved are pretty small (Ethereum + * transactions aren't that expensive in the end) and an oracle would + * likely stop participating in a contract it repeatedly lost money on, + * this risk is deemed acceptable. Oracles should also regularly + * withdraw any funds in the contract to prevent issues where the + * contract becomes underfunded at a later time, and different oracles + * are competing for the left-over funds. + + * Finally, note that any change to the set of oracles or to the billing + * parameters will trigger payout of all oracles first (using the old + * parameters), a billing admin cannot take away funds that are already + * marked for payment. + */ +contract OCR2Aggregator is OCR2Abstract, OwnerIsCreator, AggregatorV2V3Interface { + // This contract is divided into sections. Each section defines a set of + // variables, events, and functions that belong together. + + /*************************************************************************** + * Section: Variables used in multiple other sections + **************************************************************************/ + + struct Transmitter { + bool active; + + // Index of oracle in s_signersList/s_transmittersList + uint8 index; + + // juels-denominated payment for transmitters, covering gas costs incurred + // by the transmitter plus additional rewards. The entire LINK supply (1e9 + // LINK = 1e27 Juels) will always fit into a uint96. + uint96 paymentJuels; + } + mapping (address /* transmitter address */ => Transmitter) internal s_transmitters; + + struct Signer { + bool active; + + // Index of oracle in s_signersList/s_transmittersList + uint8 index; + } + mapping (address /* signer address */ => Signer) internal s_signers; + + // s_signersList contains the signing address of each oracle + address[] internal s_signersList; + + // s_transmittersList contains the transmission address of each oracle, + // i.e. the address the oracle actually sends transactions to the contract from + address[] internal s_transmittersList; + + // We assume that all oracles contribute observations to all rounds. this + // variable tracks (per-oracle) from what round an oracle should be rewarded, + // i.e. the oracle gets (latestAggregatorRoundId - + // rewardFromAggregatorRoundId) * reward + uint32[maxNumOracles] internal s_rewardFromAggregatorRoundId; + + bytes32 s_latestConfigDigest; + + // Storing these fields used on the hot path in a HotVars variable reduces the + // retrieval of all of them to a single SLOAD. + struct HotVars { + // maximum number of faulty oracles + uint8 f; + + // epoch and round from OCR protocol. + // 32 most sig bits for epoch, 8 least sig bits for round + uint40 latestEpochAndRound; + + // Chainlink Aggregators expose a roundId to consumers. The offchain reporting + // protocol does not use this id anywhere. We increment it whenever a new + // transmission is made to provide callers with contiguous ids for successive + // reports. + uint32 latestAggregatorRoundId; + + // Highest compensated gas price, in gwei uints + uint32 maximumGasPriceGwei; + + // If gas price is less (in gwei units), transmitter gets half the savings + uint32 reasonableGasPriceGwei; + + // Fixed LINK reward for each observer + uint32 observationPaymentGjuels; + + // Fixed reward for transmitter + uint32 transmissionPaymentGjuels; + + // Overhead incurred by accounting logic + uint24 accountingGas; + } + HotVars internal s_hotVars; + + // Transmission records the median answer from the transmit transaction at + // time timestamp + struct Transmission { + int192 answer; // 192 bits ought to be enough for anyone + uint32 observationsTimestamp; // when were observations made offchain + uint32 transmissionTimestamp; // when was report received onchain + } + mapping(uint32 /* aggregator round ID */ => Transmission) internal s_transmissions; + + // Lowest answer the system is allowed to report in response to transmissions + int192 immutable public minAnswer; + // Highest answer the system is allowed to report in response to transmissions + int192 immutable public maxAnswer; + + /*************************************************************************** + * Section: Constructor + **************************************************************************/ + + /** + * @param link address of the LINK contract + * @param minAnswer_ lowest answer the median of a report is allowed to be + * @param maxAnswer_ highest answer the median of a report is allowed to be + * @param requesterAccessController access controller for requesting new rounds + * @param decimals_ answers are stored in fixed-point format, with this many digits of precision + * @param description_ short human-readable description of observable this contract's answers pertain to + */ + constructor( + LinkTokenInterface link, + int192 minAnswer_, + int192 maxAnswer_, + AccessControllerInterface billingAccessController, + AccessControllerInterface requesterAccessController, + uint8 decimals_, + string memory description_ + ) { + s_linkToken = link; + emit LinkTokenSet(LinkTokenInterface(address(0)), link); + _setBillingAccessController(billingAccessController); + + decimals = decimals_; + s_description = description_; + setRequesterAccessController(requesterAccessController); + setValidatorConfig(AggregatorValidatorInterface(address(0x0)), 0); + minAnswer = minAnswer_; + maxAnswer = maxAnswer_; + } + + + /*************************************************************************** + * Section: OCR2Abstract Configuration + **************************************************************************/ + + // incremented each time a new config is posted. This count is incorporated + // into the config digest to prevent replay attacks. + uint32 internal s_configCount; + + // makes it easier for offchain systems to extract config from logs + uint32 internal s_latestConfigBlockNumber; + + // left as a function so this check can be disabled in derived contracts + function _requirePositiveF ( + uint256 f + ) + internal + pure + virtual + { + require(0 < f, "f must be positive"); + } + + struct SetConfigArgs { + address[] signers; + address[] transmitters; + uint8 f; + bytes onchainConfig; + uint64 offchainConfigVersion; + bytes offchainConfig; + } + + /// @inheritdoc OCR2Abstract + function setConfig( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) + external + override + onlyOwner() + { + require(signers.length <= maxNumOracles, "too many oracles"); + require(signers.length == transmitters.length, "oracle length mismatch"); + require(3*f < signers.length, "faulty-oracle f too high"); + _requirePositiveF(f); + require(keccak256(onchainConfig) == keccak256(abi.encodePacked(uint8(1) /*version*/, minAnswer, maxAnswer)), "invalid onchainConfig"); + + SetConfigArgs memory args = SetConfigArgs({ + signers: signers, + transmitters: transmitters, + f: f, + onchainConfig: onchainConfig, + offchainConfigVersion: offchainConfigVersion, + offchainConfig: offchainConfig + }); + + s_hotVars.latestEpochAndRound = 0; + _payOracles(); + + // remove any old signer/transmitter addresses + uint256 oldLength = s_signersList.length; + for (uint256 i = 0; i < oldLength; i++) { + address signer = s_signersList[i]; + address transmitter = s_transmittersList[i]; + delete s_signers[signer]; + delete s_transmitters[transmitter]; + } + delete s_signersList; + delete s_transmittersList; + + // add new signer/transmitter addresses + for (uint i = 0; i < args.signers.length; i++) { + require( + !s_signers[args.signers[i]].active, + "repeated signer address" + ); + s_signers[args.signers[i]] = Signer({ + active: true, + index: uint8(i) + }); + require( + !s_transmitters[args.transmitters[i]].active, + "repeated transmitter address" + ); + s_transmitters[args.transmitters[i]] = Transmitter({ + active: true, + index: uint8(i), + paymentJuels: 0 + }); + } + s_signersList = args.signers; + s_transmittersList = args.transmitters; + + s_hotVars.f = args.f; + uint32 previousConfigBlockNumber = s_latestConfigBlockNumber; + s_latestConfigBlockNumber = uint32(block.number); + s_configCount += 1; + s_latestConfigDigest = _configDigestFromConfigData( + block.chainid, + address(this), + s_configCount, + args.signers, + args.transmitters, + args.f, + args.onchainConfig, + args.offchainConfigVersion, + args.offchainConfig + ); + + emit ConfigSet( + previousConfigBlockNumber, + s_latestConfigDigest, + s_configCount, + args.signers, + args.transmitters, + args.f, + args.onchainConfig, + args.offchainConfigVersion, + args.offchainConfig + ); + + uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId; + for (uint256 i = 0; i < args.signers.length; i++) { + s_rewardFromAggregatorRoundId[i] = latestAggregatorRoundId; + } + } + + /// @inheritdoc OCR2Abstract + function latestConfigDetails() + external + override + view + returns ( + uint32 configCount, + uint32 blockNumber, + bytes32 configDigest + ) + { + return (s_configCount, s_latestConfigBlockNumber, s_latestConfigDigest); + } + + /** + * @return list of addresses permitted to transmit reports to this contract + + * @dev The list will match the order used to specify the transmitter during setConfig + */ + function getTransmitters() + external + view + returns(address[] memory) + { + return s_transmittersList; + } + + /*************************************************************************** + * Section: Onchain Validation + **************************************************************************/ + + // Configuration for validator + struct ValidatorConfig { + AggregatorValidatorInterface validator; + uint32 gasLimit; + } + ValidatorConfig private s_validatorConfig; + + /** + * @notice indicates that the validator configuration has been set + * @param previousValidator previous validator contract + * @param previousGasLimit previous gas limit for validate calls + * @param currentValidator current validator contract + * @param currentGasLimit current gas limit for validate calls + */ + event ValidatorConfigSet( + AggregatorValidatorInterface indexed previousValidator, + uint32 previousGasLimit, + AggregatorValidatorInterface indexed currentValidator, + uint32 currentGasLimit + ); + + /** + * @notice validator configuration + * @return validator validator contract + * @return gasLimit gas limit for validate calls + */ + function getValidatorConfig() + external + view + returns (AggregatorValidatorInterface validator, uint32 gasLimit) + { + ValidatorConfig memory vc = s_validatorConfig; + return (vc.validator, vc.gasLimit); + } + + /** + * @notice sets validator configuration + * @dev set newValidator to 0x0 to disable validate calls + * @param newValidator address of the new validator contract + * @param newGasLimit new gas limit for validate calls + */ + function setValidatorConfig( + AggregatorValidatorInterface newValidator, + uint32 newGasLimit + ) + public + onlyOwner() + { + ValidatorConfig memory previous = s_validatorConfig; + + if (previous.validator != newValidator || previous.gasLimit != newGasLimit) { + s_validatorConfig = ValidatorConfig({ + validator: newValidator, + gasLimit: newGasLimit + }); + + emit ValidatorConfigSet(previous.validator, previous.gasLimit, newValidator, newGasLimit); + } + } + + function _validateAnswer( + uint32 aggregatorRoundId, + int256 answer + ) + private + { + ValidatorConfig memory vc = s_validatorConfig; + + if (address(vc.validator) == address(0)) { + return; + } + + uint32 prevAggregatorRoundId = aggregatorRoundId - 1; + int256 prevAggregatorRoundAnswer = s_transmissions[prevAggregatorRoundId].answer; + require( + _callWithExactGasEvenIfTargetIsNoContract( + vc.gasLimit, + address(vc.validator), + abi.encodeWithSignature( + "validate(uint256,int256,uint256,int256)", + uint256(prevAggregatorRoundId), + prevAggregatorRoundAnswer, + uint256(aggregatorRoundId), + answer + ) + ), + "insufficient gas" + ); + } + + uint256 private constant CALL_WITH_EXACT_GAS_CUSHION = 5_000; + + /** + * @dev calls target address with exactly gasAmount gas and data as calldata + * or reverts if at least gasAmount gas is not available. + */ + function _callWithExactGasEvenIfTargetIsNoContract( + uint256 gasAmount, + address target, + bytes memory data + ) + private + returns (bool sufficientGas) + { + // solhint-disable-next-line no-inline-assembly + assembly { + let g := gas() + // Compute g -= CALL_WITH_EXACT_GAS_CUSHION and check for underflow. We + // need the cushion since the logic following the above call to gas also + // costs gas which we cannot account for exactly. So cushion is a + // conservative upper bound for the cost of this logic. + if iszero(lt(g, CALL_WITH_EXACT_GAS_CUSHION)) { + g := sub(g, CALL_WITH_EXACT_GAS_CUSHION) + // If g - g//64 <= gasAmount, we don't have enough gas. (We subtract g//64 + // because of EIP-150.) + if gt(sub(g, div(g, 64)), gasAmount) { + // Call and ignore success/return data. Note that we did not check + // whether a contract actually exists at the target address. + pop(call(gasAmount, target, 0, add(data, 0x20), mload(data), 0, 0)) + sufficientGas := true + } + } + } + } + + /*************************************************************************** + * Section: RequestNewRound + **************************************************************************/ + + AccessControllerInterface internal s_requesterAccessController; + + /** + * @notice emitted when a new requester access controller contract is set + * @param old the address prior to the current setting + * @param current the address of the new access controller contract + */ + event RequesterAccessControllerSet(AccessControllerInterface old, AccessControllerInterface current); + + /** + * @notice emitted to immediately request a new round + * @param requester the address of the requester + * @param configDigest the latest transmission's configDigest + * @param epoch the latest transmission's epoch + * @param round the latest transmission's round + */ + event RoundRequested(address indexed requester, bytes32 configDigest, uint32 epoch, uint8 round); + + /** + * @notice address of the requester access controller contract + * @return requester access controller address + */ + function getRequesterAccessController() + external + view + returns (AccessControllerInterface) + { + return s_requesterAccessController; + } + + /** + * @notice sets the requester access controller + * @param requesterAccessController designates the address of the new requester access controller + */ + function setRequesterAccessController(AccessControllerInterface requesterAccessController) + public + onlyOwner() + { + AccessControllerInterface oldController = s_requesterAccessController; + if (requesterAccessController != oldController) { + s_requesterAccessController = AccessControllerInterface(requesterAccessController); + emit RequesterAccessControllerSet(oldController, requesterAccessController); + } + } + + /** + * @notice immediately requests a new round + * @return the aggregatorRoundId of the next round. Note: The report for this round may have been + * transmitted (but not yet mined) *before* requestNewRound() was even called. There is *no* + * guarantee of causality between the request and the report at aggregatorRoundId. + */ + function requestNewRound() external returns (uint80) { + require(msg.sender == owner() || s_requesterAccessController.hasAccess(msg.sender, msg.data), + "Only owner&requester can call"); + + uint40 latestEpochAndRound = s_hotVars.latestEpochAndRound; + uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId; + + emit RoundRequested( + msg.sender, + s_latestConfigDigest, + uint32(latestEpochAndRound >> 8), + uint8(latestEpochAndRound) + ); + return latestAggregatorRoundId + 1; + } + + /*************************************************************************** + * Section: Transmission + **************************************************************************/ + + /** + * @notice indicates that a new report was transmitted + * @param aggregatorRoundId the round to which this report was assigned + * @param answer median of the observations attached to this report + * @param transmitter address from which the report was transmitted + * @param observationsTimestamp when were observations made offchain + * @param observations observations transmitted with this report + * @param observers i-th element is the oracle id of the oracle that made the i-th observation + * @param juelsPerFeeCoin exchange rate between feeCoin (e.g. ETH on Ethereum) and LINK, denominated in juels + * @param configDigest configDigest of transmission + * @param epochAndRound least-significant byte is the OCR protocol round number, the other bytes give the big-endian OCR protocol epoch number + */ + event NewTransmission( + uint32 indexed aggregatorRoundId, + int192 answer, + address transmitter, + uint32 observationsTimestamp, + int192[] observations, + bytes observers, + int192 juelsPerFeeCoin, + bytes32 configDigest, + uint40 epochAndRound + ); + + // Used to relieve stack pressure in transmit + struct Report { + uint32 observationsTimestamp; + bytes observers; // ith element is the index of the ith observer + int192[] observations; // ith element is the ith observation + int192 juelsPerFeeCoin; + } + + // _decodeReport decodes a serialized report into a Report struct + function _decodeReport(bytes memory rawReport) + internal + pure + returns ( + Report memory + ) + { + uint32 observationsTimestamp; + bytes32 rawObservers; + int192[] memory observations; + int192 juelsPerFeeCoin; + (observationsTimestamp, rawObservers, observations, juelsPerFeeCoin) = abi.decode(rawReport, (uint32, bytes32, int192[], int192)); + + _requireExpectedReportLength(rawReport, observations); + + uint256 numObservations = observations.length; + bytes memory observers = abi.encodePacked(rawObservers); + assembly { + // we truncate observers from length 32 to the number of observations + mstore(observers, numObservations) + } + + return Report({ + observationsTimestamp: observationsTimestamp, + observers: observers, + observations: observations, + juelsPerFeeCoin: juelsPerFeeCoin + }); + } + + // The constant-length components of the msg.data sent to transmit. + // See the "If we wanted to call sam" example on for example reasoning + // https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html + uint256 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT = + 4 + // function selector + 32 * 3 + // 3 words containing reportContext + 32 + // word containing start location of abiencoded report value + 32 + // word containing start location of abiencoded rs value + 32 + // word containing start location of abiencoded ss value + 32 + // rawVs value + 32 + // word containing length of report + 32 + // word containing length rs + 32 + // word containing length of ss + 0; // placeholder + + // Make sure the calldata length matches the inputs. Otherwise, the + // transmitter could append an arbitrarily long (up to gas-block limit) + // string of 0 bytes, which we would reimburse at a rate of 16 gas/byte, but + // which would only cost the transmitter 4 gas/byte. + function _requireExpectedMsgDataLength( + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss + ) + private + pure + { + // calldata will never be big enough to make this overflow + uint256 expected = TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT + + report.length + // one byte per entry in report + rs.length * 32 + // 32 bytes per entry in rs + ss.length * 32 + // 32 bytes per entry in ss + 0; // placeholder + require(msg.data.length == expected, "calldata length mismatch"); + } + + /// @inheritdoc OCR2Abstract + function transmit( + // reportContext consists of: + // reportContext[0]: ConfigDigest + // reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round + // reportContext[2]: ExtraHash + bytes32[3] calldata reportContext, + bytes calldata report, + // ECDSA signatures + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs + ) + external + override + { + // NOTE: If the arguments to this function are changed, _requireExpectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + + uint256 initialGas = gasleft(); // This line must come first + + HotVars memory hotVars = s_hotVars; + + uint40 epochAndRound = uint40(uint256(reportContext[1])); + + require(hotVars.latestEpochAndRound < epochAndRound, "stale report"); + + require(s_transmitters[msg.sender].active, "unauthorized transmitter"); + + require(s_latestConfigDigest == reportContext[0], "configDigest mismatch"); + + _requireExpectedMsgDataLength(report, rs, ss); + + require(rs.length == hotVars.f + 1, "wrong number of signatures"); + require(rs.length == ss.length, "signatures out of registration"); + + // Verify signatures attached to report + { + bytes32 h = keccak256(abi.encode(keccak256(report), reportContext)); + + // i-th byte counts number of sigs made by i-th signer + uint256 signedCount = 0; + + Signer memory signer; + for (uint i = 0; i < rs.length; i++) { + address signerAddress = ecrecover(h, uint8(rawVs[i])+27, rs[i], ss[i]); + signer = s_signers[signerAddress]; + require(signer.active, "signature error"); + unchecked{ + signedCount += 1 << (8 * signer.index); + } + } + + // The first byte of the mask can be 0, because we only ever have 31 oracles + require(signedCount & 0x0001010101010101010101010101010101010101010101010101010101010101 == signedCount, "duplicate signer"); + } + + int192 juelsPerFeeCoin = _report(hotVars, reportContext[0], epochAndRound, report); + + _payTransmitter(hotVars, juelsPerFeeCoin, uint32(initialGas), msg.sender); + } + + /** + * @notice details about the most recent report + * @return configDigest domain separation tag for the latest report + * @return epoch epoch in which the latest report was generated + * @return round OCR round in which the latest report was generated + * @return latestAnswer_ median value from latest report + * @return latestTimestamp_ when the latest report was transmitted + */ + function latestTransmissionDetails() + external + view + returns ( + bytes32 configDigest, + uint32 epoch, + uint8 round, + int192 latestAnswer_, + uint64 latestTimestamp_ + ) + { + require(msg.sender == tx.origin, "Only callable by EOA"); + return ( + s_latestConfigDigest, + uint32(s_hotVars.latestEpochAndRound >> 8), + uint8(s_hotVars.latestEpochAndRound), + s_transmissions[s_hotVars.latestAggregatorRoundId].answer, + s_transmissions[s_hotVars.latestAggregatorRoundId].transmissionTimestamp + ); + } + + /// @inheritdoc OCR2Abstract + function latestConfigDigestAndEpoch() + external + override + view + virtual + returns( + bool scanLogs, + bytes32 configDigest, + uint32 epoch + ) + { + return (false, s_latestConfigDigest, uint32(s_hotVars.latestEpochAndRound >> 8)); + } + + function _requireExpectedReportLength( + bytes memory report, + int192[] memory observations + ) + private + pure + { + uint256 expected = + 32 + // observationsTimestamp + 32 + // rawObservers + 32 + // observations offset + 32 + // juelsPerFeeCoin + 32 + // observations length + 32 * observations.length + // observations payload + 0; + require(report.length == expected, "report length mismatch"); + } + + function _report( + HotVars memory hotVars, + bytes32 configDigest, + uint40 epochAndRound, + bytes memory rawReport + ) + internal + returns (int192 juelsPerFeeCoin) + { + Report memory report = _decodeReport(rawReport); + + + require(report.observations.length <= maxNumOracles, "num observations out of bounds"); + // Offchain logic ensures that a quorum of oracles is operating on a matching set of at least + // 2f+1 observations. By assumption, up to f of those can be faulty, which includes being + // malformed. Conversely, more than f observations have to be well-formed and sent on chain. + require(hotVars.f < report.observations.length, "too few values to trust median"); + + hotVars.latestEpochAndRound = epochAndRound; + + // get median, validate its range, store it in new aggregator round + int192 median = report.observations[report.observations.length/2]; + require(minAnswer <= median && median <= maxAnswer, "median is out of min-max range"); + hotVars.latestAggregatorRoundId++; + s_transmissions[hotVars.latestAggregatorRoundId] = + Transmission({ + answer: median, + observationsTimestamp: report.observationsTimestamp, + transmissionTimestamp: uint32(block.timestamp) + }); + + // persist updates to hotVars + s_hotVars = hotVars; + + emit NewTransmission( + hotVars.latestAggregatorRoundId, + median, + msg.sender, + report.observationsTimestamp, + report.observations, + report.observers, + report.juelsPerFeeCoin, + configDigest, + epochAndRound + ); + // Emit these for backwards compatibility with offchain consumers + // that only support legacy events + emit NewRound( + hotVars.latestAggregatorRoundId, + address(0x0), // use zero address since we don't have anybody "starting" the round here + report.observationsTimestamp + ); + emit AnswerUpdated( + median, + hotVars.latestAggregatorRoundId, + block.timestamp + ); + + _validateAnswer(hotVars.latestAggregatorRoundId, median); + + return report.juelsPerFeeCoin; + } + + /*************************************************************************** + * Section: v2 AggregatorInterface + **************************************************************************/ + + /** + * @notice median from the most recent report + */ + function latestAnswer() + public + override + view + virtual + returns (int256) + { + return s_transmissions[s_hotVars.latestAggregatorRoundId].answer; + } + + /** + * @notice timestamp of block in which last report was transmitted + */ + function latestTimestamp() + public + override + view + virtual + returns (uint256) + { + return s_transmissions[s_hotVars.latestAggregatorRoundId].transmissionTimestamp; + } + + /** + * @notice Aggregator round (NOT OCR round) in which last report was transmitted + */ + function latestRound() + public + override + view + virtual + returns (uint256) + { + return s_hotVars.latestAggregatorRoundId; + } + + /** + * @notice median of report from given aggregator round (NOT OCR round) + * @param roundId the aggregator round of the target report + */ + function getAnswer(uint256 roundId) + public + override + view + virtual + returns (int256) + { + if (roundId > 0xFFFFFFFF) { return 0; } + return s_transmissions[uint32(roundId)].answer; + } + + /** + * @notice timestamp of block in which report from given aggregator round was transmitted + * @param roundId aggregator round (NOT OCR round) of target report + */ + function getTimestamp(uint256 roundId) + public + override + view + virtual + returns (uint256) + { + if (roundId > 0xFFFFFFFF) { return 0; } + return s_transmissions[uint32(roundId)].transmissionTimestamp; + } + + /*************************************************************************** + * Section: v3 AggregatorInterface + **************************************************************************/ + + /** + * @return answers are stored in fixed-point format, with this many digits of precision + */ + uint8 immutable public override decimals; + + /** + * @notice aggregator contract version + */ + uint256 constant public override version = 6; + + string internal s_description; + + /** + * @notice human-readable description of observable this contract is reporting on + */ + function description() + public + override + view + virtual + returns (string memory) + { + return s_description; + } + + /** + * @notice details for the given aggregator round + * @param roundId target aggregator round (NOT OCR round). Must fit in uint32 + * @return roundId_ roundId + * @return answer median of report from given roundId + * @return startedAt timestamp of when observations were made offchain + * @return updatedAt timestamp of block in which report from given roundId was transmitted + * @return answeredInRound roundId + */ + function getRoundData(uint80 roundId) + public + override + view + virtual + returns ( + uint80 roundId_, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + if(roundId > type(uint32).max) { return (0, 0, 0, 0, 0); } + Transmission memory transmission = s_transmissions[uint32(roundId)]; + return ( + roundId, + transmission.answer, + transmission.observationsTimestamp, + transmission.transmissionTimestamp, + roundId + ); + } + + /** + * @notice aggregator details for the most recently transmitted report + * @return roundId aggregator round of latest report (NOT OCR round) + * @return answer median of latest report + * @return startedAt timestamp of when observations were made offchain + * @return updatedAt timestamp of block containing latest report + * @return answeredInRound aggregator round of latest report + */ + function latestRoundData() + public + override + view + virtual + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId; + + Transmission memory transmission = s_transmissions[latestAggregatorRoundId]; + return ( + latestAggregatorRoundId, + transmission.answer, + transmission.observationsTimestamp, + transmission.transmissionTimestamp, + latestAggregatorRoundId + ); + } + + /*************************************************************************** + * Section: Configurable LINK Token + **************************************************************************/ + + // We assume that the token contract is correct. This contract is not written + // to handle misbehaving ERC20 tokens! + LinkTokenInterface internal s_linkToken; + + /* + * @notice emitted when the LINK token contract is set + * @param oldLinkToken the address of the old LINK token contract + * @param newLinkToken the address of the new LINK token contract + */ + event LinkTokenSet( + LinkTokenInterface indexed oldLinkToken, + LinkTokenInterface indexed newLinkToken + ); + + /** + * @notice sets the LINK token contract used for paying oracles + * @param linkToken the address of the LINK token contract + * @param recipient remaining funds from the previous token contract are transferred + * here + * @dev this function will return early (without an error) without changing any state + * if linkToken equals getLinkToken(). + * @dev this will trigger a payout so that a malicious owner cannot take from oracles + * what is already owed to them. + * @dev we assume that the token contract is correct. This contract is not written + * to handle misbehaving ERC20 tokens! + */ + function setLinkToken( + LinkTokenInterface linkToken, + address recipient + ) external + onlyOwner() + { + LinkTokenInterface oldLinkToken = s_linkToken; + if (linkToken == oldLinkToken) { + // No change, nothing to be done + return; + } + // call balanceOf as a sanity check on whether we're talking to a token + // contract + linkToken.balanceOf(address(this)); + // we break CEI here, but that's okay because we're dealing with a correct + // token contract (by assumption). + _payOracles(); + uint256 remainingBalance = oldLinkToken.balanceOf(address(this)); + require(oldLinkToken.transfer(recipient, remainingBalance), "transfer remaining funds failed"); + s_linkToken = linkToken; + emit LinkTokenSet(oldLinkToken, linkToken); + } + + /* + * @notice gets the LINK token contract used for paying oracles + * @return linkToken the address of the LINK token contract + */ + function getLinkToken() + external + view + returns(LinkTokenInterface linkToken) + { + return s_linkToken; + } + + /*************************************************************************** + * Section: BillingAccessController Management + **************************************************************************/ + + // Controls who can change billing parameters. A billingAdmin is not able to + // affect any OCR protocol settings and therefore cannot tamper with the + // liveness or integrity of a data feed. However, a billingAdmin can set + // faulty billing parameters causing oracles to be underpaid, or causing them + // to be paid so much that further calls to setConfig, setBilling, + // setLinkToken will always fail due to the contract being underfunded. + AccessControllerInterface internal s_billingAccessController; + + /** + * @notice emitted when a new access-control contract is set + * @param old the address prior to the current setting + * @param current the address of the new access-control contract + */ + event BillingAccessControllerSet(AccessControllerInterface old, AccessControllerInterface current); + + function _setBillingAccessController(AccessControllerInterface billingAccessController) + internal + { + AccessControllerInterface oldController = s_billingAccessController; + if (billingAccessController != oldController) { + s_billingAccessController = billingAccessController; + emit BillingAccessControllerSet( + oldController, + billingAccessController + ); + } + } + + /** + * @notice sets billingAccessController + * @param _billingAccessController new billingAccessController contract address + * @dev only owner can call this + */ + function setBillingAccessController(AccessControllerInterface _billingAccessController) + external + onlyOwner + { + _setBillingAccessController(_billingAccessController); + } + + /** + * @notice gets billingAccessController + * @return address of billingAccessController contract + */ + function getBillingAccessController() + external + view + returns (AccessControllerInterface) + { + return s_billingAccessController; + } + + /*************************************************************************** + * Section: Billing Configuration + **************************************************************************/ + + /** + * @notice emitted when billing parameters are set + * @param maximumGasPriceGwei highest gas price for which transmitter will be compensated + * @param reasonableGasPriceGwei transmitter will receive reward for gas prices under this value + * @param observationPaymentGjuels reward to oracle for contributing an observation to a successfully transmitted report + * @param transmissionPaymentGjuels reward to transmitter of a successful report + * @param accountingGas gas overhead incurred by accounting logic + */ + event BillingSet( + uint32 maximumGasPriceGwei, + uint32 reasonableGasPriceGwei, + uint32 observationPaymentGjuels, + uint32 transmissionPaymentGjuels, + uint24 accountingGas + ); + + /** + * @notice sets billing parameters + * @param maximumGasPriceGwei highest gas price for which transmitter will be compensated + * @param reasonableGasPriceGwei transmitter will receive reward for gas prices under this value + * @param observationPaymentGjuels reward to oracle for contributing an observation to a successfully transmitted report + * @param transmissionPaymentGjuels reward to transmitter of a successful report + * @param accountingGas gas overhead incurred by accounting logic + * @dev access control provided by billingAccessController + */ + function setBilling( + uint32 maximumGasPriceGwei, + uint32 reasonableGasPriceGwei, + uint32 observationPaymentGjuels, + uint32 transmissionPaymentGjuels, + uint24 accountingGas + ) + external + { + AccessControllerInterface access = s_billingAccessController; + require(msg.sender == owner() || access.hasAccess(msg.sender, msg.data), + "Only owner&billingAdmin can call"); + _payOracles(); + + s_hotVars.maximumGasPriceGwei = maximumGasPriceGwei; + s_hotVars.reasonableGasPriceGwei = reasonableGasPriceGwei; + s_hotVars.observationPaymentGjuels = observationPaymentGjuels; + s_hotVars.transmissionPaymentGjuels = transmissionPaymentGjuels; + s_hotVars.accountingGas = accountingGas; + + emit BillingSet(maximumGasPriceGwei, reasonableGasPriceGwei, + observationPaymentGjuels, transmissionPaymentGjuels, accountingGas); + } + + /** + * @notice gets billing parameters + * @param maximumGasPriceGwei highest gas price for which transmitter will be compensated + * @param reasonableGasPriceGwei transmitter will receive reward for gas prices under this value + * @param observationPaymentGjuels reward to oracle for contributing an observation to a successfully transmitted report + * @param transmissionPaymentGjuels reward to transmitter of a successful report + * @param accountingGas gas overhead of the accounting logic + */ + function getBilling() + external + view + returns ( + uint32 maximumGasPriceGwei, + uint32 reasonableGasPriceGwei, + uint32 observationPaymentGjuels, + uint32 transmissionPaymentGjuels, + uint24 accountingGas + ) + { + return ( + s_hotVars.maximumGasPriceGwei, + s_hotVars.reasonableGasPriceGwei, + s_hotVars.observationPaymentGjuels, + s_hotVars.transmissionPaymentGjuels, + s_hotVars.accountingGas + ); + } + + /*************************************************************************** + * Section: Payments and Withdrawals + **************************************************************************/ + + /** + * @notice withdraws an oracle's payment from the contract + * @param transmitter the transmitter address of the oracle + * @dev must be called by oracle's payee address + */ + function withdrawPayment(address transmitter) + external + { + require(msg.sender == s_payees[transmitter], "Only payee can withdraw"); + _payOracle(transmitter); + } + + /** + * @notice query an oracle's payment amount, denominated in juels + * @param transmitterAddress the transmitter address of the oracle + */ + function owedPayment(address transmitterAddress) + public + view + returns (uint256) + { + Transmitter memory transmitter = s_transmitters[transmitterAddress]; + if (!transmitter.active) { return 0; } + // safe from overflow: + // s_hotVars.latestAggregatorRoundId - s_rewardFromAggregatorRoundId[transmitter.index] <= 2**32 + // s_hotVars.observationPaymentGjuels <= 2**32 + // 1 gwei <= 2**32 + // hence juelsAmount <= 2**96 + uint256 juelsAmount = + uint256(s_hotVars.latestAggregatorRoundId - s_rewardFromAggregatorRoundId[transmitter.index]) * + uint256(s_hotVars.observationPaymentGjuels) * + (1 gwei); + juelsAmount += transmitter.paymentJuels; + return juelsAmount; + } + + /** + * @notice emitted when an oracle has been paid LINK + * @param transmitter address from which the oracle sends reports to the transmit method + * @param payee address to which the payment is sent + * @param amount amount of LINK sent + * @param linkToken address of the LINK token contract + */ + event OraclePaid( + address indexed transmitter, + address indexed payee, + uint256 amount, + LinkTokenInterface indexed linkToken + ); + + // _payOracle pays out transmitter's balance to the corresponding payee, and zeros it out + function _payOracle(address transmitterAddress) + internal + { + Transmitter memory transmitter = s_transmitters[transmitterAddress]; + if (!transmitter.active) { return; } + uint256 juelsAmount = owedPayment(transmitterAddress); + if (juelsAmount > 0) { + address payee = s_payees[transmitterAddress]; + // Poses no re-entrancy issues, because LINK.transfer does not yield + // control flow. + require(s_linkToken.transfer(payee, juelsAmount), "insufficient funds"); + s_rewardFromAggregatorRoundId[transmitter.index] = s_hotVars.latestAggregatorRoundId; + s_transmitters[transmitterAddress].paymentJuels = 0; + emit OraclePaid(transmitterAddress, payee, juelsAmount, s_linkToken); + } + } + + // _payOracles pays out all transmitters, and zeros out their balances. + // + // It's much more gas-efficient to do this as a single operation, to avoid + // hitting storage too much. + function _payOracles() + internal + { + unchecked { + LinkTokenInterface linkToken = s_linkToken; + uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId; + uint32[maxNumOracles] memory rewardFromAggregatorRoundId = s_rewardFromAggregatorRoundId; + address[] memory transmitters = s_transmittersList; + for (uint transmitteridx = 0; transmitteridx < transmitters.length; transmitteridx++) { + uint256 reimbursementAmountJuels = s_transmitters[transmitters[transmitteridx]].paymentJuels; + s_transmitters[transmitters[transmitteridx]].paymentJuels = 0; + uint256 obsCount = latestAggregatorRoundId - rewardFromAggregatorRoundId[transmitteridx]; + uint256 juelsAmount = + obsCount * uint256(s_hotVars.observationPaymentGjuels) * (1 gwei) + reimbursementAmountJuels; + if (juelsAmount > 0) { + address payee = s_payees[transmitters[transmitteridx]]; + // Poses no re-entrancy issues, because LINK.transfer does not yield + // control flow. + require(linkToken.transfer(payee, juelsAmount), "insufficient funds"); + rewardFromAggregatorRoundId[transmitteridx] = latestAggregatorRoundId; + emit OraclePaid(transmitters[transmitteridx], payee, juelsAmount, linkToken); + } + } + // "Zero" the accounting storage variables + s_rewardFromAggregatorRoundId = rewardFromAggregatorRoundId; + } + } + + /** + * @notice withdraw any available funds left in the contract, up to amount, after accounting for the funds due to participants in past reports + * @param recipient address to send funds to + * @param amount maximum amount to withdraw, denominated in LINK-wei. + * @dev access control provided by billingAccessController + */ + function withdrawFunds( + address recipient, + uint256 amount + ) + external + { + require(msg.sender == owner() || s_billingAccessController.hasAccess(msg.sender, msg.data), + "Only owner&billingAdmin can call"); + uint256 linkDue = _totalLinkDue(); + uint256 linkBalance = s_linkToken.balanceOf(address(this)); + require(linkBalance >= linkDue, "insufficient balance"); + require(s_linkToken.transfer(recipient, _min(linkBalance - linkDue, amount)), "insufficient funds"); + } + + // Total LINK due to participants in past reports (denominated in Juels). + function _totalLinkDue() + internal + view + returns (uint256 linkDue) + { + // Argument for overflow safety: We do all computations in + // uint256s. The inputs to linkDue are: + // - the <= 31 observation rewards each of which has less than + // 64 bits (32 bits for observationPaymentGjuels, 32 bits + // for wei/gwei conversion). Hence 69 bits are sufficient for this part. + // - the <= 31 gas reimbursements, each of which consists of at most 96 + // bits. Hence 101 bits are sufficient for this part. + // So we never need more than 102 bits. + + address[] memory transmitters = s_transmittersList; + uint256 n = transmitters.length; + + uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId; + uint32[maxNumOracles] memory rewardFromAggregatorRoundId = s_rewardFromAggregatorRoundId; + for (uint i = 0; i < n; i++) { + linkDue += latestAggregatorRoundId - rewardFromAggregatorRoundId[i]; + } + // Convert observationPaymentGjuels to uint256, or this overflows! + linkDue *= uint256(s_hotVars.observationPaymentGjuels) * (1 gwei); + for (uint i = 0; i < n; i++) { + linkDue += uint256(s_transmitters[transmitters[i]].paymentJuels); + } + } + + /** + * @notice allows oracles to check that sufficient LINK balance is available + * @return availableBalance LINK available on this contract, after accounting for outstanding obligations. can become negative + */ + function linkAvailableForPayment() + external + view + returns (int256 availableBalance) + { + // there are at most one billion LINK, so this cast is safe + int256 balance = int256(s_linkToken.balanceOf(address(this))); + // according to the argument in the definition of _totalLinkDue, + // _totalLinkDue is never greater than 2**102, so this cast is safe + int256 due = int256(_totalLinkDue()); + // safe from overflow according to above sizes + return int256(balance) - int256(due); + } + + /** + * @notice number of observations oracle is due to be reimbursed for + * @param transmitterAddress address used by oracle for signing or transmitting reports + */ + function oracleObservationCount(address transmitterAddress) + external + view + returns (uint32) + { + Transmitter memory transmitter = s_transmitters[transmitterAddress]; + if (!transmitter.active) { return 0; } + return s_hotVars.latestAggregatorRoundId - s_rewardFromAggregatorRoundId[transmitter.index]; + } + + /*************************************************************************** + * Section: Transmitter Payment + **************************************************************************/ + + // Gas price at which the transmitter should be reimbursed, in gwei/gas + function _reimbursementGasPriceGwei( + uint256 txGasPriceGwei, + uint256 reasonableGasPriceGwei, + uint256 maximumGasPriceGwei + ) + internal + pure + returns (uint256) + { + // this happens on the path for transmissions. we'd rather pay out + // a wrong reward than risk a liveness failure due to a revert. + unchecked { + // Reward the transmitter for choosing an efficient gas price: if they manage + // to come in lower than considered reasonable, give them half the savings. + uint256 gasPriceGwei = txGasPriceGwei; + if (txGasPriceGwei < reasonableGasPriceGwei) { + // Give transmitter half the savings for coming in under the reasonable gas price + gasPriceGwei += (reasonableGasPriceGwei - txGasPriceGwei) / 2; + } + // Don't reimburse a gas price higher than maximumGasPriceGwei + return _min(gasPriceGwei, maximumGasPriceGwei); + } + } + + // gas reimbursement due the transmitter, in wei + function _transmitterGasCostWei( + uint256 initialGas, + uint256 gasPriceGwei, + uint256 callDataGas, + uint256 accountingGas, + uint256 leftGas + ) + internal + pure + returns (uint256) + { + // this happens on the path for transmissions. we'd rather pay out + // a wrong reward than risk a liveness failure due to a revert. + unchecked { + require(initialGas >= leftGas, "leftGas cannot exceed initialGas"); + uint256 usedGas = + initialGas - leftGas + // observed gas usage + callDataGas + accountingGas; // estimated gas usage + uint256 fullGasCostWei = usedGas * gasPriceGwei * (1 gwei); + return fullGasCostWei; + } + } + + function _payTransmitter( + HotVars memory hotVars, + int192 juelsPerFeeCoin, + uint32 initialGas, + address transmitter + ) + internal + virtual + { + // this happens on the path for transmissions. we'd rather pay out + // a wrong reward than risk a liveness failure due to a revert. + unchecked { + // we can't deal with negative juelsPerFeeCoin, better to just not pay + if (juelsPerFeeCoin < 0) { + return; + } + + // Reimburse transmitter of the report for gas usage + uint256 gasPriceGwei = _reimbursementGasPriceGwei( + tx.gasprice / (1 gwei), // convert to ETH-gwei units + hotVars.reasonableGasPriceGwei, + hotVars.maximumGasPriceGwei + ); + // The following is only an upper bound, as it ignores the cheaper cost for + // 0 bytes. Safe from overflow, because calldata just isn't that long. + uint256 callDataGasCost = 16 * msg.data.length; + uint256 gasLeft = gasleft(); + uint256 gasCostEthWei = _transmitterGasCostWei( + uint256(initialGas), + gasPriceGwei, + callDataGasCost, + hotVars.accountingGas, + gasLeft + ); + + // Even if we assume absurdly large values, this still does not overflow. With + // - usedGas <= 1'000'000 gas <= 2**20 gas + // - weiPerGas <= 1'000'000 gwei <= 2**50 wei + // - hence gasCostEthWei <= 2**70 + // - juelsPerFeeCoin <= 2**96 (more than the entire supply) + // we still fit into 166 bits + uint256 gasCostJuels = (gasCostEthWei * uint192(juelsPerFeeCoin))/1e18; + + uint96 oldTransmitterPaymentJuels = s_transmitters[transmitter].paymentJuels; + uint96 newTransmitterPaymentJuels = uint96(uint256(oldTransmitterPaymentJuels) + + gasCostJuels + uint256(hotVars.transmissionPaymentGjuels) * (1 gwei)); + + // overflow *should* never happen, but if it does, let's not persist it. + if (newTransmitterPaymentJuels < oldTransmitterPaymentJuels) { + return; + } + s_transmitters[transmitter].paymentJuels = newTransmitterPaymentJuels; + } + } + + /*************************************************************************** + * Section: Payee Management + **************************************************************************/ + + // Addresses at which oracles want to receive payments, by transmitter address + mapping (address /* transmitter */ => address /* payment address */) + internal + s_payees; + + // Payee addresses which must be approved by the owner + mapping (address /* transmitter */ => address /* payment address */) + internal + s_proposedPayees; + + /** + * @notice emitted when a transfer of an oracle's payee address has been initiated + * @param transmitter address from which the oracle sends reports to the transmit method + * @param current the payee address for the oracle, prior to this setting + * @param proposed the proposed new payee address for the oracle + */ + event PayeeshipTransferRequested( + address indexed transmitter, + address indexed current, + address indexed proposed + ); + + /** + * @notice emitted when a transfer of an oracle's payee address has been completed + * @param transmitter address from which the oracle sends reports to the transmit method + * @param current the payee address for the oracle, prior to this setting + */ + event PayeeshipTransferred( + address indexed transmitter, + address indexed previous, + address indexed current + ); + + /** + * @notice sets the payees for transmitting addresses + * @param transmitters addresses oracles use to transmit the reports + * @param payees addresses of payees corresponding to list of transmitters + * @dev must be called by owner + * @dev cannot be used to change payee addresses, only to initially populate them + */ + function setPayees( + address[] calldata transmitters, + address[] calldata payees + ) + external + onlyOwner() + { + require(transmitters.length == payees.length, "transmitters.size != payees.size"); + + for (uint i = 0; i < transmitters.length; i++) { + address transmitter = transmitters[i]; + address payee = payees[i]; + address currentPayee = s_payees[transmitter]; + bool zeroedOut = currentPayee == address(0); + require(zeroedOut || currentPayee == payee, "payee already set"); + s_payees[transmitter] = payee; + + if (currentPayee != payee) { + emit PayeeshipTransferred(transmitter, currentPayee, payee); + } + } + } + + /** + * @notice first step of payeeship transfer (safe transfer pattern) + * @param transmitter transmitter address of oracle whose payee is changing + * @param proposed new payee address + * @dev can only be called by payee address + */ + function transferPayeeship( + address transmitter, + address proposed + ) + external + { + require(msg.sender == s_payees[transmitter], "only current payee can update"); + require(msg.sender != proposed, "cannot transfer to self"); + + address previousProposed = s_proposedPayees[transmitter]; + s_proposedPayees[transmitter] = proposed; + + if (previousProposed != proposed) { + emit PayeeshipTransferRequested(transmitter, msg.sender, proposed); + } + } + + /** + * @notice second step of payeeship transfer (safe transfer pattern) + * @param transmitter transmitter address of oracle whose payee is changing + * @dev can only be called by proposed new payee address + */ + function acceptPayeeship( + address transmitter + ) + external + { + require(msg.sender == s_proposedPayees[transmitter], "only proposed payees can accept"); + + address currentPayee = s_payees[transmitter]; + s_payees[transmitter] = msg.sender; + s_proposedPayees[transmitter] = address(0); + + emit PayeeshipTransferred(transmitter, currentPayee, msg.sender); + } + + /*************************************************************************** + * Section: TypeAndVersionInterface + **************************************************************************/ + + function typeAndVersion() + external + override + pure + virtual + returns (string memory) + { + return "OCR2Aggregator 1.0.0"; + } + + /*************************************************************************** + * Section: Helper Functions + **************************************************************************/ + + function _min( + uint256 a, + uint256 b + ) + internal + pure + returns (uint256) + { + unchecked { + if (a < b) { return a; } + return b; + } + } +} \ No newline at end of file diff --git a/contracts/src/v0.8/llo-feeds/v0.4.0/output.json b/contracts/src/v0.8/llo-feeds/v0.4.0/output.json new file mode 100644 index 00000000000..88170408f50 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.4.0/output.json @@ -0,0 +1,820 @@ +{ + "compiler": { + "version": "0.8.19+commit.7dd6d404" + }, + "language": "Solidity", + "output": { + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "verifierProxy", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "type": "error", + "name": "AccessForbidden" + }, + { + "inputs": [], + "type": "error", + "name": "BadActivationTime" + }, + { + "inputs": [], + "type": "error", + "name": "BadVerification" + }, + { + "inputs": [ + { + "internalType": "bytes24", + "name": "donConfigId", + "type": "bytes24" + } + ], + "type": "error", + "name": "DonConfigAlreadyExists" + }, + { + "inputs": [], + "type": "error", + "name": "DonConfigDoesNotExist" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "numSigners", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxSigners", + "type": "uint256" + } + ], + "type": "error", + "name": "ExcessSigners" + }, + { + "inputs": [], + "type": "error", + "name": "FaultToleranceMustBePositive" + }, + { + "inputs": [], + "type": "error", + "name": "FeeManagerInvalid" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "numSigners", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minSigners", + "type": "uint256" + } + ], + "type": "error", + "name": "InsufficientSigners" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "rsLength", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ssLength", + "type": "uint256" + } + ], + "type": "error", + "name": "MismatchedSignatures" + }, + { + "inputs": [], + "type": "error", + "name": "NoSigners" + }, + { + "inputs": [], + "type": "error", + "name": "NonUniqueSignatures" + }, + { + "inputs": [], + "type": "error", + "name": "ZeroAddress" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "oldAccessController", + "type": "address", + "indexed": false + }, + { + "internalType": "address", + "name": "newAccessController", + "type": "address", + "indexed": false + } + ], + "type": "event", + "name": "AccessControllerSet", + "anonymous": false + }, + { + "inputs": [ + { + "internalType": "bytes24", + "name": "donConfigId", + "type": "bytes24", + "indexed": false + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool", + "indexed": false + } + ], + "type": "event", + "name": "ConfigActivated", + "anonymous": false + }, + { + "inputs": [ + { + "internalType": "bytes24", + "name": "donConfigId", + "type": "bytes24", + "indexed": false + } + ], + "type": "event", + "name": "ConfigRemoved", + "anonymous": false + }, + { + "inputs": [ + { + "internalType": "bytes24", + "name": "donConfigId", + "type": "bytes24", + "indexed": true + }, + { + "internalType": "address[]", + "name": "signers", + "type": "address[]", + "indexed": false + }, + { + "internalType": "uint8", + "name": "f", + "type": "uint8", + "indexed": false + }, + { + "internalType": "struct Common.AddressAndWeight[]", + "name": "recipientAddressesAndWeights", + "type": "tuple[]", + "components": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint64", + "name": "weight", + "type": "uint64" + } + ], + "indexed": false + }, + { + "internalType": "uint16", + "name": "donConfigIndex", + "type": "uint16", + "indexed": false + } + ], + "type": "event", + "name": "ConfigSet", + "anonymous": false + }, + { + "inputs": [ + { + "internalType": "address", + "name": "oldFeeManager", + "type": "address", + "indexed": false + }, + { + "internalType": "address", + "name": "newFeeManager", + "type": "address", + "indexed": false + } + ], + "type": "event", + "name": "FeeManagerSet", + "anonymous": false + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address", + "indexed": true + }, + { + "internalType": "address", + "name": "to", + "type": "address", + "indexed": true + } + ], + "type": "event", + "name": "OwnershipTransferRequested", + "anonymous": false + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address", + "indexed": true + }, + { + "internalType": "address", + "name": "to", + "type": "address", + "indexed": true + } + ], + "type": "event", + "name": "OwnershipTransferred", + "anonymous": false + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "feedId", + "type": "bytes32", + "indexed": true + }, + { + "internalType": "address", + "name": "requester", + "type": "address", + "indexed": false + } + ], + "type": "event", + "name": "ReportVerified", + "anonymous": false + }, + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "name": "acceptOwnership" + }, + { + "inputs": [], + "stateMutability": "view", + "type": "function", + "name": "i_verifierProxy", + "outputs": [ + { + "internalType": "contract IDestinationVerifierProxy", + "name": "", + "type": "address" + } + ] + }, + { + "inputs": [], + "stateMutability": "view", + "type": "function", + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ] + }, + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "name": "removeLatestConfig" + }, + { + "inputs": [], + "stateMutability": "view", + "type": "function", + "name": "s_accessController", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ] + }, + { + "inputs": [], + "stateMutability": "view", + "type": "function", + "name": "s_feeManager", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ] + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessController", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "name": "setAccessController" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "signers", + "type": "address[]" + }, + { + "internalType": "uint8", + "name": "f", + "type": "uint8" + }, + { + "internalType": "struct Common.AddressAndWeight[]", + "name": "recipientAddressesAndWeights", + "type": "tuple[]", + "components": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint64", + "name": "weight", + "type": "uint64" + } + ] + } + ], + "stateMutability": "nonpayable", + "type": "function", + "name": "setConfig" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "donConfigIndex", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "name": "setConfigActive" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "signers", + "type": "address[]" + }, + { + "internalType": "uint8", + "name": "f", + "type": "uint8" + }, + { + "internalType": "struct Common.AddressAndWeight[]", + "name": "recipientAddressesAndWeights", + "type": "tuple[]", + "components": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint64", + "name": "weight", + "type": "uint64" + } + ] + }, + { + "internalType": "uint32", + "name": "activationTime", + "type": "uint32" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "name": "setConfigWithActivationTime" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "feeManager", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "name": "setFeeManager" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "stateMutability": "pure", + "type": "function", + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ] + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "name": "transferOwnership" + }, + { + "inputs": [], + "stateMutability": "pure", + "type": "function", + "name": "typeAndVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ] + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "signedReport", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "parameterPayload", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "function", + "name": "verify", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ] + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "signedReports", + "type": "bytes[]" + }, + { + "internalType": "bytes", + "name": "parameterPayload", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "function", + "name": "verifyBulk", + "outputs": [ + { + "internalType": "bytes[]", + "name": "", + "type": "bytes[]" + } + ] + } + ], + "devdoc": { + "kind": "dev", + "methods": { + "setAccessController(address)": { + "params": { + "accessController": "The address of the access controller" + } + }, + "setConfig(address[],uint8,(address,uint64)[])": { + "params": { + "f": "number of faulty oracles the system can tolerate", + "recipientAddressesAndWeights": "the addresses and weights of all the recipients to receive rewards", + "signers": "addresses with which oracles sign the reports" + } + }, + "setConfigActive(uint256,bool)": { + "params": { + "donConfigId": "The ID of the config to update", + "isActive": "The new config active status" + } + }, + "setConfigWithActivationTime(address[],uint8,(address,uint64)[],uint32)": { + "params": { + "activationTime": "the time at which the config was activated", + "f": "number of faulty oracles the system can tolerate", + "recipientAddressesAndWeights": "the addresses and weights of all the recipients to receive rewards", + "signers": "addresses with which oracles sign the reports" + } + }, + "setFeeManager(address)": { + "params": { + "feeManager": "The address of the fee manager" + } + }, + "supportsInterface(bytes4)": { + "details": "Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas." + }, + "verify(bytes,bytes,address)": { + "details": "Verification is typically only done through the proxy contract so we can't just use msg.sender.", + "params": { + "parameterPayload": "The encoded parameters to be used in the verification and billing process.", + "sender": "The address that requested to verify the contract.Used for logging and applying the fee.", + "signedReport": "The encoded data to be verified." + }, + "returns": { + "_0": "The encoded verified response." + } + }, + "verifyBulk(bytes[],bytes,address)": { + "details": "Verification is typically only done through the proxy contract so we can't just use msg.sender.", + "params": { + "parameterPayload": "The encoded parameters to be used in the verification and billing process.", + "sender": "The address that requested to verify the contract. Used for logging and applying the fee.", + "signedReports": "The encoded data to be verified." + }, + "returns": { + "_0": "The encoded verified responses." + } + } + }, + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": { + "acceptOwnership()": { + "notice": "Allows an ownership transfer to be completed by the recipient." + }, + "i_verifierProxy()": { + "notice": "The address of the verifierProxy" + }, + "owner()": { + "notice": "Get the current owner" + }, + "removeLatestConfig()": { + "notice": "Removes the latest config" + }, + "s_accessController()": { + "notice": "The address of the access controller" + }, + "s_feeManager()": { + "notice": "The address of the verifierProxy" + }, + "setAccessController(address)": { + "notice": "Sets the access controller address" + }, + "setConfig(address[],uint8,(address,uint64)[])": { + "notice": "sets off-chain reporting protocol configuration incl. participating oracles" + }, + "setConfigActive(uint256,bool)": { + "notice": "Updates the config active status" + }, + "setConfigWithActivationTime(address[],uint8,(address,uint64)[],uint32)": { + "notice": "sets off-chain reporting protocol configuration incl. participating oracles" + }, + "setFeeManager(address)": { + "notice": "Sets the fee manager address" + }, + "transferOwnership(address)": { + "notice": "Allows an owner to begin transferring ownership to a new address." + }, + "verify(bytes,bytes,address)": { + "notice": "Verifies that the data encoded has been signed correctly using the signatures included within the payload." + }, + "verifyBulk(bytes[],bytes,address)": { + "notice": "Bulk verifies that the data encoded has been signed correctly using the signatures included within the payload." + } + }, + "version": 1 + } + }, + "settings": { + "remappings": [ + "@arbitrum/=node_modules/@arbitrum/", + "@eth-optimism/=node_modules/@eth-optimism/", + "@openzeppelin/=node_modules/@openzeppelin/", + "@scroll-tech/=node_modules/@scroll-tech/", + "forge-std/=src/v0.8/vendor/forge-std/src/", + "hardhat/=node_modules/hardhat/" + ], + "optimizer": { + "enabled": true, + "runs": 1000000 + }, + "metadata": { + "bytecodeHash": "none" + }, + "compilationTarget": { + "src/v0.8/llo-feeds/v0.4.0/DestinationVerifier.sol": "DestinationVerifier" + }, + "evmVersion": "paris", + "libraries": {} + }, + "sources": { + "src/v0.8/interfaces/TypeAndVersionInterface.sol": { + "keccak256": "0x805cc9a91d54db1bea60cb19f38364f1eac2735bddb3476294fb803c2f6b7097", + "urls": [ + "bzz-raw://05762f3335bb50fde2ece5ffbb735f22db35dc9489ea4716a4e731aa0aeee1e1", + "dweb:/ipfs/QmNu4sZk9T8PZYMn2BvxECF911hAviCjE2T846Zir8H7RB" + ], + "license": "MIT" + }, + "src/v0.8/llo-feeds/libraries/Common.sol": { + "keccak256": "0xc62ed807abb37c89f59c93eaaf8bd08e9e424c8451614d936247a31468e3e1d3", + "urls": [ + "bzz-raw://7b27a40291586a6b6ae313308d31ab0a994c4c9d1ce542eaf80df1cf8d3c364e", + "dweb:/ipfs/QmZEkT2Cy2mQE3WPKtoB9pUJtSdreWXCZ6c4xy8YvbkF5E" + ], + "license": "MIT" + }, + "src/v0.8/llo-feeds/v0.4.0/DestinationVerifier.sol": { + "keccak256": "0x67d621c4b1a813e98df66aa49dbc782dc065609ecbf756a8f9d35e310bac607a", + "urls": [ + "bzz-raw://eaac54ab4bdb5eeca140a97de3d6093970d96b107b9f2c22555e2a43f32c5363", + "dweb:/ipfs/QmWWu7cz7f7HENSTWfpK4kJabU4zgx7EwKWZUzK5XjMd67" + ], + "license": "MIT" + }, + "src/v0.8/llo-feeds/v0.4.0/interfaces/IDestinationVerifier.sol": { + "keccak256": "0x3ce59799aa111866c4406aa404a90c2c8db41008ef080829491338da8409bd5a", + "urls": [ + "bzz-raw://feebec4557c6c0251301b7c1ade2ce1e94434a6f4ae246785560da298266fbf7", + "dweb:/ipfs/QmduKruDFCM1XmtdMDZi5NevFcnVhG66MiT115SiqBBKgC" + ], + "license": "MIT" + }, + "src/v0.8/llo-feeds/v0.4.0/interfaces/IDestinationVerifierFeeManager.sol": { + "keccak256": "0x55d78909877c0c4d2519aab956cfccabaaa6c0763c58a410acf118f90b1990a6", + "urls": [ + "bzz-raw://99e6fd7693b264db0a173dcb229b8eb8d07c782b685fe21046a21b05e891bcaf", + "dweb:/ipfs/QmUajistuCUATFbmXVGtFB9BBM1FqSKAgbxHbsKrF1DYdA" + ], + "license": "MIT" + }, + "src/v0.8/llo-feeds/v0.4.0/interfaces/IDestinationVerifierProxy.sol": { + "keccak256": "0x7774f6c4c32fa6f577c4be6679ca2eecd37617d42a9c235e219e55f281229fb4", + "urls": [ + "bzz-raw://2795aecf936121b7c19d6d367a997f333d875bbb272d6007f446a080752ebf3b", + "dweb:/ipfs/QmREk7kKfYDw3ngqVtsnPfAMpcNL53cCAVwJb7zrmj5No5" + ], + "license": "MIT" + }, + "src/v0.8/llo-feeds/v0.4.0/interfaces/IDestinationVerifierProxyVerifier.sol": { + "keccak256": "0x733816deb843e1a96d301727489198fcb6fa8133372e247c1dc14940015f9171", + "urls": [ + "bzz-raw://fbd226966cb1147bce156274a4ed2d8f7c3e892d9de3b9b6f75370e3c4f5e438", + "dweb:/ipfs/QmQSgFTGyXUBtJwpvQL7FxqtAFFZtVfyEzLHBBjhtaKKDp" + ], + "license": "MIT" + }, + "src/v0.8/shared/access/ConfirmedOwner.sol": { + "keccak256": "0xdcb0e9135ddbe71ee27ba99fa06656960c66c964cf2ecb29696da1c1427d9861", + "urls": [ + "bzz-raw://f914a1b638300e82d8f5a020a4195235599afebab4ef1e10c6992f3c90e7df3e", + "dweb:/ipfs/Qmf2MbuVB16qbCGii3U5cjcBvVjAHHYzKp9voJa2eDch9B" + ], + "license": "MIT" + }, + "src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol": { + "keccak256": "0x2422a055657a87e98be61f8f31abb1824ec50fd0f73949f4e3c6ac877efb6da8", + "urls": [ + "bzz-raw://fde3b9ac3a4c42ea43e2f92b037d32ab20e30818471c6e20d2590147a6c2958a", + "dweb:/ipfs/QmQ2ohQP4GnhPUsiWCvCfb1dsoGYDdxSap3dxtnYTV4rmT" + ], + "license": "MIT" + }, + "src/v0.8/shared/interfaces/IAccessController.sol": { + "keccak256": "0x2bdd0e819a586c8a0f326f227157197e3ded4f0e2c75117cc04fded3cb07ed81", + "urls": [ + "bzz-raw://0e27d99e49f62a445fc415eaa7f07b9eb475f1e3fe61e2f1187391e187d7fb8a", + "dweb:/ipfs/QmRQdCivLYqH5dv5oox7FV6vK8zYN4hPHEYAjeAort48M2" + ], + "license": "MIT" + }, + "src/v0.8/shared/interfaces/IOwnable.sol": { + "keccak256": "0x885de72b7b4e4f1bf8ba817a3f2bcc37fd9022d342c4ce76782151c30122d767", + "urls": [ + "bzz-raw://17c636625a5d29a140612db496d2cca9fb4b48c673adb0fd7b3957d287e75921", + "dweb:/ipfs/QmNoBX8TY424bdQWyQC7y3kpKfgxyWxhLw7KEhhEEoBN9q" + ], + "license": "MIT" + }, + "src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol": { + "keccak256": "0x34568ee847aaa6b80e18743f8e216a60420c201364cff3b14965835b92681831", + "urls": [ + "bzz-raw://4e4000df7513871b3e8f456f0da0dc3550a72dc010867fd49ab49e8b4d08a7bc", + "dweb:/ipfs/QmQCLsQehJCcuc5QRCqEdCogXTdnyhvLzkvs7c3qdNULQV" + ], + "license": "MIT" + }, + "src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol": { + "keccak256": "0xa36a31b4bb17fad88d023474893b3b895fa421650543b1ce5aefc78efbd43244", + "urls": [ + "bzz-raw://0f235b9175d95111f301d729566e214c32559e55a2b0579c947484748e20679d", + "dweb:/ipfs/QmSsNBuPejy1wNe2u3JSt2p9wFhrjvBjFrnAaNe1nDNkEA" + ], + "license": "MIT" + } + }, + "version": 1 +} 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..fa436575bc4 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/FeeManager.sol @@ -0,0 +1,582 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.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 {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 {IRewardManager} from "./interfaces/IRewardManager.sol"; +import {IFeeManager} from "./interfaces/IFeeManager.sol"; +import {IVerifierFeeManager} from "./interfaces/IVerifierFeeManager.sol"; + +/** + * @title FeeManager + * @author Michael Fletcher + * @author Austin Born + * @author ad0ll + * @notice This contract is used for the handling of fees required for users verifying reports. + */ +contract FeeManager is IFeeManager, IVerifierFeeManager, 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 verifier address + mapping(address => address) public s_verifierAddressList; + + /// @notice the reward manager address + IRewardManager public 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 thrown when trying to bulk verify reports where theres not a matching number of poolIds + error PoolIdMismatch(); + + /// @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 _verifierAddress The address of the verifier contract + * @param _rewardManagerAddress The address of the reward manager contract + */ + constructor( + address _linkAddress, + address _nativeAddress, + address _verifierAddress, + address _rewardManagerAddress + ) ConfirmedOwner(msg.sender) { + if ( + _linkAddress == address(0) || + _nativeAddress == address(0) || + _verifierAddress == address(0) || + _rewardManagerAddress == address(0) + ) revert InvalidAddress(); + + i_linkAddress = _linkAddress; + i_nativeAddress = _nativeAddress; + s_verifierAddressList[_verifierAddress] = _verifierAddress; + i_rewardManager = IRewardManager(_rewardManagerAddress); + + IERC20(i_linkAddress).approve(address(i_rewardManager), type(uint256).max); + } + + modifier onlyVerifier() { + if (msg.sender != s_verifierAddressList[msg.sender]) revert Unauthorized(); + _; + } + + /// @inheritdoc TypeAndVersionInterface + function typeAndVersion() external pure override returns (string memory) { + return "FeeManager 0.5.0"; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == type(IFeeManager).interfaceId || interfaceId == type(IVerifierFeeManager).interfaceId; + } + + /// @inheritdoc IVerifierFeeManager + function processFee( + bytes calldata payload, + bytes calldata parameterPayload, + address subscriber + ) external payable { + bytes32 poolId = bytes32(payload); + processFee(poolId, payload, parameterPayload, subscriber); + } + + /// @inheritdoc IVerifierFeeManager + function processFee( + bytes32 recipient, + bytes calldata payload, + bytes calldata parameterPayload, + address subscriber + ) public payable override onlyVerifier { + (Common.Asset memory fee, Common.Asset memory reward, uint256 appliedDiscount) = _calculateFee( + payload, + parameterPayload, + subscriber + ); + + if (fee.amount == 0) { + _tryReturnChange(subscriber, msg.value); + return; + } + + IFeeManager.FeeAndReward[] memory feeAndReward = new IFeeManager.FeeAndReward[](1); + feeAndReward[0] = IFeeManager.FeeAndReward(recipient, 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 { + bytes32[] memory poolIds = new bytes32[](payloads.length); + + for(uint256 i; i < poolIds.length; ++i) { + poolIds[i] = bytes32(payloads[i]); + } + + processFeeBulk(poolIds, payloads, parameterPayload, subscriber); + } + + + /// @inheritdoc IVerifierFeeManager + function processFeeBulk( + bytes32[] memory poolIds, + bytes[] calldata payloads, + bytes calldata parameterPayload, + address subscriber + ) public payable override onlyVerifier { + //poolIDs are mapped to payloads, so they should be the same length + if (poolIds.length != payloads.length) revert PoolIdMismatch(); + + IFeeManager.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) { + if (poolIds[i] == bytes32(0)) revert InvalidAddress(); + + (Common.Asset memory fee, Common.Asset memory reward, uint256 appliedDiscount) = _calculateFee( + payloads[i], + parameterPayload, + subscriber + ); + + if (fee.amount != 0) { + feesAndRewards[feesAndRewardsIndex++] = IFeeManager.FeeAndReward(poolIds[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 { + _tryReturnChange(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(); + } + + //check if feed discount has been 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 onlyVerifier { + 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)) { + (bool success, ) = payable(recipient).call{value: quantity}(""); + + if (!success) revert InvalidReceivingAddress(); + 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 _calculateFee( + 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, + IFeeManager.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 + _tryReturnChange(subscriber, change); + } + + function _tryReturnChange(address subscriber, uint256 quantity) internal { + if (quantity != 0) { + payable(subscriber).transfer(quantity); + } + } + + /// @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); + } + + /// @inheritdoc IFeeManager + function addVerifier(address verifierAddress) external onlyOwner { + if (verifierAddress == address(0)) revert InvalidAddress(); + //check doesn't already exist + if (s_verifierAddressList[verifierAddress] != address(0)) revert InvalidAddress(); + s_verifierAddressList[verifierAddress] = verifierAddress; + } + + /// @inheritdoc IFeeManager + function removeVerifier(address verifierAddress) external onlyOwner { + if (verifierAddress == address(0)) revert InvalidAddress(); + //check doesn't already exist + if (s_verifierAddressList[verifierAddress] == address(0)) revert InvalidAddress(); + delete s_verifierAddressList[verifierAddress]; + } + + /// @inheritdoc IFeeManager + function setRewardManager(address rewardManagerAddress) external onlyOwner { + if (rewardManagerAddress == address(0)) revert InvalidAddress(); + + if (!IERC165(rewardManagerAddress).supportsInterface(type(IRewardManager).interfaceId)) { + revert InvalidAddress(); + } + + IERC20(i_linkAddress).approve(address(i_rewardManager), 0); + i_rewardManager = IRewardManager(rewardManagerAddress); + IERC20(i_linkAddress).approve(address(rewardManagerAddress), type(uint256).max); + } +} 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..4aaf430b0e2 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/RewardManager.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; +import {IRewardManager} from "./interfaces/IRewardManager.sol"; +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC20.sol"; +import {TypeAndVersionInterface} from "../../interfaces/TypeAndVersionInterface.sol"; +import {Common} from "../libraries/Common.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title RewardManager + * @author Michael Fletcher + * @author Austin Born + * @author ad0ll + * @notice This contract will be used to reward any configured recipients within a pool. Recipients will receive a share of their pool relative to their configured weight. + */ +contract RewardManager is IRewardManager, ConfirmedOwner, TypeAndVersionInterface { + using SafeERC20 for IERC20; + + // @dev The mapping of total fees collected for a particular pot: s_totalRewardRecipientFees[poolId] + mapping(bytes32 => uint256) public s_totalRewardRecipientFees; + + // @dev The mapping of fee balances for each pot last time the recipient claimed: s_totalRewardRecipientFeesLastClaimedAmounts[poolId][recipient] + mapping(bytes32 => mapping(address => uint256)) public s_totalRewardRecipientFeesLastClaimedAmounts; + + // @dev The mapping of RewardRecipient weights for a particular poolId: s_rewardRecipientWeights[poolId][rewardRecipient]. + mapping(bytes32 => mapping(address => uint256)) public s_rewardRecipientWeights; + + // @dev Keep track of the reward recipient weights that have been set to prevent duplicates + mapping(bytes32 => bool) public s_rewardRecipientWeightsSet; + + // @dev Store a list of pool ids that have been registered, to make off chain lookups easier + bytes32[] public s_registeredPoolIds; + + // @dev The address for the LINK contract + address public immutable i_linkAddress; + + // The total weight of all RewardRecipients. 1e18 = 100% of the pool fees + uint64 private constant PERCENTAGE_SCALAR = 1e18; + + // The fee manager address + mapping(address => address) public s_feeManagerAddressList; + + // @notice Thrown whenever the RewardRecipient weights are invalid + error InvalidWeights(); + + // @notice Thrown when any given address is invalid + error InvalidAddress(); + + // @notice Thrown when the pool id is invalid + error InvalidPoolId(); + + // @notice Thrown when the calling contract is not within the authorized contracts + error Unauthorized(); + + // @notice Thrown when getAvailableRewardPoolIds parameters are incorrectly set + error InvalidPoolLength(); + + // Events emitted upon state change + event RewardRecipientsUpdated(bytes32 indexed poolId, Common.AddressAndWeight[] newRewardRecipients); + event RewardsClaimed(bytes32 indexed poolId, address indexed recipient, uint192 quantity); + event FeeManagerUpdated(address newFeeManagerAddress); + event FeePaid(FeePayment[] payments, address payer); + + /** + * @notice Constructor + * @param linkAddress address of the wrapped LINK token + */ + constructor(address linkAddress) ConfirmedOwner(msg.sender) { + //ensure that the address ia not zero + if (linkAddress == address(0)) revert InvalidAddress(); + + i_linkAddress = linkAddress; + } + + // @inheritdoc TypeAndVersionInterface + function typeAndVersion() external pure override returns (string memory) { + return "RewardManager 0.5.0"; + } + + // @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == type(IRewardManager).interfaceId; + } + + modifier onlyOwnerOrFeeManager() { + if (msg.sender != s_feeManagerAddressList[msg.sender] && msg.sender != owner()) revert Unauthorized(); + _; + } + + modifier onlyOwnerOrRecipientInPool(bytes32 poolId) { + if (s_rewardRecipientWeights[poolId][msg.sender] == 0 && msg.sender != owner()) revert Unauthorized(); + _; + } + + modifier onlyFeeManager() { + if (msg.sender != s_feeManagerAddressList[msg.sender]) revert Unauthorized(); + _; + } + + /// @inheritdoc IRewardManager + function onFeePaid(FeePayment[] calldata payments, address payer) external override onlyFeeManager { + uint256 totalFeeAmount; + for (uint256 i; i < payments.length; ++i) { + unchecked { + //the total amount for any ERC-20 asset cannot exceed 2^256 - 1 + //see https://github.com/OpenZeppelin/openzeppelin-contracts/blob/36bf1e46fa811f0f07d38eb9cfbc69a955f300ce/contracts/token/ERC20/ERC20.sol#L266 + //for example implementation. + s_totalRewardRecipientFees[payments[i].poolId] += payments[i].amount; + + //tally the total payable fees + totalFeeAmount += payments[i].amount; + } + } + + //transfer the fees to this contract + IERC20(i_linkAddress).safeTransferFrom(payer, address(this), totalFeeAmount); + + emit FeePaid(payments, payer); + } + + /// @inheritdoc IRewardManager + function claimRewards(bytes32[] memory poolIds) external override { + _claimRewards(msg.sender, poolIds); + } + + // wrapper impl for claimRewards + function _claimRewards(address recipient, bytes32[] memory poolIds) internal returns (uint256) { + //get the total amount claimable for this recipient + uint256 claimAmount; + + //loop and claim all the rewards in the poolId pot + for (uint256 i; i < poolIds.length; ++i) { + //get the poolId to be claimed + bytes32 poolId = poolIds[i]; + + //get the total fees for the pot + uint256 totalFeesInPot = s_totalRewardRecipientFees[poolId]; + + unchecked { + //avoid unnecessary storage reads if there's no fees in the pot + if (totalFeesInPot == 0) continue; + + //get the claimable amount for this recipient, this calculation will never exceed the amount in the pot + uint256 claimableAmount = totalFeesInPot - s_totalRewardRecipientFeesLastClaimedAmounts[poolId][recipient]; + + //calculate the recipients share of the fees, which is their weighted share of the difference between the last amount they claimed and the current amount in the pot. This can never be more than the total amount in existence + uint256 recipientShare = (claimableAmount * s_rewardRecipientWeights[poolId][recipient]) / PERCENTAGE_SCALAR; + + //if there's no fees to claim, continue as there's nothing to update + if (recipientShare == 0) continue; + + //keep track of the total amount claimable, this can never be more than the total amount in existence + claimAmount += recipientShare; + + //set the current total amount of fees in the pot as it's used to calculate future claims + s_totalRewardRecipientFeesLastClaimedAmounts[poolId][recipient] = totalFeesInPot; + + //emit event if the recipient has rewards to claim + emit RewardsClaimed(poolIds[i], recipient, uint192(recipientShare)); + } + } + + //check if there's any rewards to claim in the given poolId + if (claimAmount != 0) { + //transfer the reward to the recipient + IERC20(i_linkAddress).safeTransfer(recipient, claimAmount); + } + + return claimAmount; + } + + /// @inheritdoc IRewardManager + function setRewardRecipients( + bytes32 poolId, + Common.AddressAndWeight[] calldata rewardRecipientAndWeights + ) external override onlyOwnerOrFeeManager { + //revert if there are no recipients to set + if (rewardRecipientAndWeights.length == 0) revert InvalidAddress(); + + //check that the weights have not been previously set + if (s_rewardRecipientWeightsSet[poolId]) revert InvalidPoolId(); + + //keep track of the registered poolIds to make off chain lookups easier + s_registeredPoolIds.push(poolId); + + //keep track of which pools have had their reward recipients set + s_rewardRecipientWeightsSet[poolId] = true; + + //set the reward recipients, this will only be called once and contain the full set of RewardRecipients with a total weight of 100% + _setRewardRecipientWeights(poolId, rewardRecipientAndWeights, PERCENTAGE_SCALAR); + + emit RewardRecipientsUpdated(poolId, rewardRecipientAndWeights); + } + + function _setRewardRecipientWeights( + bytes32 poolId, + Common.AddressAndWeight[] calldata rewardRecipientAndWeights, + uint256 expectedWeight + ) internal { + //we can't update the weights if it contains duplicates + if (Common._hasDuplicateAddresses(rewardRecipientAndWeights)) revert InvalidAddress(); + + //loop all the reward recipients and validate the weight and address + uint256 totalWeight; + for (uint256 i; i < rewardRecipientAndWeights.length; ++i) { + //get the weight + uint256 recipientWeight = rewardRecipientAndWeights[i].weight; + //get the address + address recipientAddress = rewardRecipientAndWeights[i].addr; + + //ensure the reward recipient address is not zero + if (recipientAddress == address(0)) revert InvalidAddress(); + + //save/overwrite the weight for the reward recipient + s_rewardRecipientWeights[poolId][recipientAddress] = recipientWeight; + + unchecked { + //keep track of the cumulative weight, this cannot overflow as the total weight is restricted at 1e18 + totalWeight += recipientWeight; + } + } + + //if total weight is not met, the fees will either be under or over distributed + if (totalWeight != expectedWeight) revert InvalidWeights(); + } + + /// @inheritdoc IRewardManager + function updateRewardRecipients( + bytes32 poolId, + Common.AddressAndWeight[] calldata newRewardRecipients + ) external override onlyOwner { + //create an array of poolIds to pass to _claimRewards if required + bytes32[] memory poolIds = new bytes32[](1); + poolIds[0] = poolId; + + //loop all the reward recipients and claim their rewards before updating their weights + uint256 existingTotalWeight; + for (uint256 i; i < newRewardRecipients.length; ++i) { + //get the address + address recipientAddress = newRewardRecipients[i].addr; + //get the existing weight + uint256 existingWeight = s_rewardRecipientWeights[poolId][recipientAddress]; + + //if a recipient is updated, the rewards must be claimed first as they can't claim previous fees at the new weight + _claimRewards(newRewardRecipients[i].addr, poolIds); + + unchecked { + //keep tally of the weights so that the expected collective weight is known + existingTotalWeight += existingWeight; + } + } + + //update the reward recipients, if the new collective weight isn't equal to the previous collective weight, the fees will either be under or over distributed + _setRewardRecipientWeights(poolId, newRewardRecipients, existingTotalWeight); + + //emit event + emit RewardRecipientsUpdated(poolId, newRewardRecipients); + } + + /// @inheritdoc IRewardManager + function payRecipients(bytes32 poolId, address[] calldata recipients) external onlyOwnerOrRecipientInPool(poolId) { + //convert poolIds to an array to match the interface of _claimRewards + bytes32[] memory poolIdsArray = new bytes32[](1); + poolIdsArray[0] = poolId; + + //loop each recipient and claim the rewards for each of the pools and assets + for (uint256 i; i < recipients.length; ++i) { + _claimRewards(recipients[i], poolIdsArray); + } + } + + /// @inheritdoc IRewardManager + function addFeeManager(address newFeeManagerAddress) external onlyOwner { + if (newFeeManagerAddress == address(0)) revert InvalidAddress(); + if (s_feeManagerAddressList[newFeeManagerAddress] != address(0)) revert InvalidAddress(); + + s_feeManagerAddressList[newFeeManagerAddress] = newFeeManagerAddress; + + emit FeeManagerUpdated(newFeeManagerAddress); + } + + /// @inheritdoc IRewardManager + function removeFeeManager(address feeManagerAddress) external onlyOwner { + if (s_feeManagerAddressList[feeManagerAddress] == address(0)) revert InvalidAddress(); + delete s_feeManagerAddressList[feeManagerAddress]; + } + + /// @inheritdoc IRewardManager + function getAvailableRewardPoolIds( + address recipient, + uint256 startIndex, + uint256 endIndex + ) external view returns (bytes32[] memory) { + //get the length of the pool ids which we will loop through and potentially return + uint256 registeredPoolIdsLength = s_registeredPoolIds.length; + + uint256 lastIndex = endIndex > registeredPoolIdsLength ? registeredPoolIdsLength : endIndex; + + if (startIndex > lastIndex) revert InvalidPoolLength(); + + //create a new array with the maximum amount of potential pool ids + bytes32[] memory claimablePoolIds = new bytes32[](lastIndex - startIndex); + //we want the pools which a recipient has funds for to be sequential, so we need to keep track of the index + uint256 poolIdArrayIndex; + + //loop all the pool ids, and check if the recipient has a registered weight and a claimable amount + for (uint256 i = startIndex; i < lastIndex; ++i) { + //get the poolId + bytes32 poolId = s_registeredPoolIds[i]; + + //if the recipient has a weight, they are a recipient of this poolId + if (s_rewardRecipientWeights[poolId][recipient] != 0) { + //get the total in this pool + uint256 totalPoolAmount = s_totalRewardRecipientFees[poolId]; + //if the recipient has any LINK, then add the poolId to the array + unchecked { + //s_totalRewardRecipientFeesLastClaimedAmounts can never exceed total pool amount, and the number of pools can't exceed the max array length + if (totalPoolAmount - s_totalRewardRecipientFeesLastClaimedAmounts[poolId][recipient] != 0) { + claimablePoolIds[poolIdArrayIndex++] = poolId; + } + } + } + } + + return claimablePoolIds; + } +} 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..ba06ac83a38 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/Verifier.sol @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {TypeAndVersionInterface} from "../../interfaces/TypeAndVersionInterface.sol"; +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; + +import {IAccessController} from "../../shared/interfaces/IAccessController.sol"; +import {IERC165} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {Common} from "../libraries/Common.sol"; +import {CommonV5} from "./libraries/CommonV5.sol"; + +import {IVerifier} from "./interfaces/IVerifier.sol"; + +import {IVerifierFeeManager} from "./interfaces/IVerifierFeeManager.sol"; +import {IVerifierProxyV03} from "./interfaces/IVerifierProxyV03.sol"; +import {IVerifierProxy} from "./interfaces/IVerifierProxy.sol"; +import {IVerifierProxyVerifier} from "./interfaces/IVerifierProxyVerifier.sol"; + +// OCR2 standard +uint256 constant MAX_NUM_ORACLES = 31; + +/** + * @title Verifier + * @author Michael Fletcher + * @author ad0ll + * @notice This contract will be used to verify reports based on the oracle signatures. This is not the source verifier which required individual fee configurations, instead, this checks that a report has been signed by one of the configured configDigest. + */ +contract Verifier is IVerifier, IVerifierProxyVerifier, ConfirmedOwner, TypeAndVersionInterface { + /// @notice Mapping of configDigest to Config, used to look up the verification configuration for the configDigest of the incoming report + mapping(bytes32 => CommonV5.Config) private s_configs; + + /// @notice The address of the verifierProxy + address public s_feeManager; + + /// @notice The address of the access controller + address public s_accessController; + + /// @notice The address of the V0.3 verifierProxy + address public immutable i_verifierProxy; + + /// @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 or expected to be 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 submitted with no signatures + error NoSigners(); + + /// @notice This error is thrown whenever a Config already exists + /// @param configDigest The ID of the Config that already exists + error ConfigAlreadyExists(bytes32 configDigest); + + /// @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. This error be thrown for multiple reasons and it's purposely like + * this to prevent information being leaked about the verification process which could be used to enable free verifications maliciously + */ + error BadVerification(); + + /// @notice This error is thrown whenever a zero address is passed + error ZeroAddress(); + + /// @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 when the proxy is neither a valid v0.3 or v0.4 proxy + error VerifierProxyInvalid(); + + /// @notice This error is thrown whenever an address tries + /// to execute a verification that it is not authorized to do so + error AccessForbidden(); + + /// @notice This error is thrown whenever a config does not exist + error ConfigDoesNotExist(); + + /// @notice This error is thrown when removing an invalid oracle from a configuration + error MismatchedSigners(); + + /// @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 configuration is activated or deactivated + event ConfigActivated(bytes32 configDigest, bool isActive); + + /// @notice event is emitted whenever a new Config is set + event ConfigSet( + bytes32 indexed configDigest, + address[] signers, + uint8 f, + Common.AddressAndWeight[] recipientAddressesAndWeights + ); + + /// @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 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 event is emitted whenever a new Config is removed + event ConfigUnset(bytes32 configDigest, address[] signers); + + bytes32 constant V03_PROXY_TYPE_AND_VERSION = keccak256(bytes("VerifierProxy 2.0.0")); + + constructor(address verifierProxy) ConfirmedOwner(msg.sender) { + if (verifierProxy == address(0)) { + revert ZeroAddress(); + } + + i_verifierProxy = verifierProxy; + } + + /// @inheritdoc IVerifierProxyVerifier + function verify( + bytes calldata signedReport, + bytes calldata parameterPayload, + address sender + ) external payable override onlyProxy checkAccess(sender) returns (bytes memory) { + (bytes memory verifierResponse, bytes32 configDigest) = _verify(signedReport, sender); + + address fm = s_feeManager; + if (fm != address(0)) { + //process the fee and catch the error + try IVerifierFeeManager(fm).processFee{value: msg.value}(configDigest, signedReport, parameterPayload, sender) { + //do nothing + } catch { + // we purposefully obfuscate the error here to prevent information leaking leading to free verifications + revert BadVerification(); + } + } + + return verifierResponse; + } + + /// @inheritdoc IVerifierProxyVerifier + function verifyBulk( + bytes[] calldata signedReports, + bytes calldata parameterPayload, + address sender + ) external payable override onlyProxy checkAccess(sender) returns (bytes[] memory) { + bytes[] memory verifierResponses = new bytes[](signedReports.length); + bytes32[] memory donConfigs = new bytes32[](signedReports.length); + + for (uint256 i; i < signedReports.length; ++i) { + (bytes memory report, bytes32 config) = _verify(signedReports[i], sender); + verifierResponses[i] = report; + donConfigs[i] = config; + } + + address fm = s_feeManager; + if (fm != address(0)) { + //process the fee and catch the error + try + IVerifierFeeManager(fm).processFeeBulk{value: msg.value}(donConfigs, signedReports, parameterPayload, sender) + { + //do nothing + } catch { + // we purposefully obfuscate the error here to prevent information leaking leading to free verifications + revert BadVerification(); + } + } + + return verifierResponses; + } + + function _verify(bytes calldata signedReport, address sender) internal returns (bytes memory, bytes32) { + ( + bytes32[3] memory reportContext, + bytes memory reportData, + bytes32[] memory rs, + bytes32[] memory ss, + bytes32 rawVs + ) = abi.decode(signedReport, (bytes32[3], bytes, bytes32[], bytes32[], bytes32)); + + // Signature lengths must match + if (rs.length != ss.length) revert MismatchedSignatures(rs.length, ss.length); + + //Must always be at least 1 signer + if (rs.length == 0) revert NoSigners(); + + // The payload is hashed and signed by the oracles - we need to recover the addresses + bytes32 signedPayload = keccak256(abi.encodePacked(keccak256(reportData), reportContext)); + address[] memory signers = new address[](rs.length); + for (uint256 i; i < rs.length; ++i) { + signers[i] = ecrecover(signedPayload, uint8(rawVs[i]) + 27, rs[i], ss[i]); + } + + // Duplicate signatures are not allowed + if (Common._hasDuplicateAddresses(signers)) { + revert BadVerification(); + } + + // Find the config for this report + CommonV5.Config storage config = s_configs[reportContext[0]]; //reportContext[0] is the config digest + + // Check a config has been set + if (config.configDigest == bytes32(0)) { + revert BadVerification(); + } + + //check the config is active + if (!config.isActive) { + revert BadVerification(); + } + + //check we have enough signatures + if (signers.length <= config.f) { + revert BadVerification(); + } + + //check each signer is registered against the config + for (uint256 i; i < signers.length; ++i) { + if (!config.oracles[signers[i]]) { + revert BadVerification(); + } + } + + emit ReportVerified(bytes32(reportData), sender); + + return (reportData, config.configDigest); + } + + /// @inheritdoc IVerifier + function setConfig( + bytes32 configDigest, + address[] calldata signers, + uint8 f, + Common.AddressAndWeight[] calldata recipientAddressesAndWeights + ) external override checkConfigValid(signers.length, f) onlyOwner { + _setConfig(configDigest, signers, f, recipientAddressesAndWeights, new address[](0)); + } + + /// @inheritdoc IVerifier + function unsetConfig( + bytes32 configDigest, + address[] calldata signers + ) external override onlyOwner { + CommonV5.Config storage config = s_configs[configDigest]; + // Config must exist + if (config.configDigest == bytes32(0)) { + revert ConfigDoesNotExist(); + } + + // We must be removing the number of signers that were originally set + if(s_configs[configDigest].oracleCount != signers.length){ + revert MismatchedSigners(); + } + + for (uint256 i; i < signers.length; ++i) { + // Check the signers being removed are not zero address or duplicates + if(!config.oracles[signers[i]]){ + revert MismatchedSigners(); + } + + delete config.oracles[signers[i]]; + } + + delete s_configs[configDigest]; + + emit ConfigUnset(configDigest, signers); + } + + function _setConfig( + bytes32 configDigest, + address[] calldata signers, + uint8 f, + Common.AddressAndWeight[] calldata recipientAddressesAndWeights, + address[] memory prevSigners + ) internal { + if (configDigest == bytes32(0)) { + revert ZeroAddress(); + } + + // If it's a v0.3 interface, register the verifier and the recipients else recipients are registered through this classes FM + if(keccak256(bytes(TypeAndVersionInterface(i_verifierProxy).typeAndVersion())) == V03_PROXY_TYPE_AND_VERSION){ + IVerifierProxyV03(i_verifierProxy).setVerifier(bytes32(0), configDigest, recipientAddressesAndWeights); //First param is currentConfigDigest, since all configs are new, this is always 0 + } else { + // We may want to register these later or skip this step in the unlikely scenario they've previously been registered in the RewardsManager + if (recipientAddressesAndWeights.length != 0) { + if (s_feeManager == address(0)) { + revert FeeManagerInvalid(); + } + IVerifierFeeManager(s_feeManager).setFeeRecipients(configDigest, recipientAddressesAndWeights); + } + } + + // Duplicate addresses would break protocol rules + if (Common._hasDuplicateAddresses(signers)) { + revert NonUniqueSignatures(); + } + + // Check the config does not already exist + if (s_configs[configDigest].configDigest != bytes32(0)) { + revert ConfigAlreadyExists(configDigest); + } + + // Store the config fields individually instead of struct assignment + s_configs[configDigest].configDigest = configDigest; + s_configs[configDigest].f = f; + s_configs[configDigest].isActive = true; + s_configs[configDigest].oracleCount = uint8(signers.length); + + // Generate signers mapping for the config, used to efficiently lookup whether a signer is allowed to appear when verifying a report + for (uint256 i; i < signers.length; ++i) { + if (signers[i] == address(0)) revert ZeroAddress(); + s_configs[configDigest].oracles[signers[i]] = true; + } + + emit ConfigSet(configDigest, signers, f, recipientAddressesAndWeights); + } + + /// @inheritdoc IVerifier + function setFeeManager(address feeManager) external override onlyOwner { + if (address(feeManager) != address(0) && !IERC165(feeManager).supportsInterface(type(IVerifierFeeManager).interfaceId)) revert FeeManagerInvalid(); + + address oldFeeManager = s_feeManager; + s_feeManager = feeManager; + + emit FeeManagerSet(oldFeeManager, feeManager); + } + + /// @inheritdoc IVerifier + function setAccessController(address accessController) external override onlyOwner { + address oldAccessController = s_accessController; + s_accessController = accessController; + emit AccessControllerSet(oldAccessController, accessController); + } + + /// @inheritdoc IVerifier + function setConfigActive(bytes32 configDigest, bool isActive) external onlyOwner { + // Fetch config + CommonV5.Config storage config = s_configs[configDigest]; + + // Check config is set before update + if (config.configDigest == bytes32(0)) { + revert ConfigDoesNotExist(); + } + config.isActive = isActive; + + emit ConfigActivated(config.configDigest, isActive); + } + + 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); + _; + } + + modifier onlyProxy() { + if (i_verifierProxy != msg.sender) { + revert AccessForbidden(); + } + _; + } + + modifier checkAccess(address sender) { + address ac = s_accessController; + if (address(ac) != address(0) && !IAccessController(ac).hasAccess(sender, msg.data)) revert AccessForbidden(); + _; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { + return interfaceId == type(IVerifier).interfaceId || interfaceId == type(IVerifierProxyVerifier).interfaceId; + } + + /// @inheritdoc TypeAndVersionInterface + function typeAndVersion() external pure override returns (string memory) { + return "Verifier 0.5.0"; + } +} 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..388a6cdc018 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/VerifierProxy.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; +import {TypeAndVersionInterface} from "../../interfaces/TypeAndVersionInterface.sol"; +import {IERC165} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {IVerifierProxy} from "./interfaces/IVerifierProxy.sol"; +import {IVerifierProxyVerifier} from "./interfaces/IVerifierProxyVerifier.sol"; + +/** + * @title VerifierProxy + * @author Michael Fletcher + * @author ad0ll + * @notice This contract will be used to route all requests through to the assigned verifier contract. This contract does not support individual feed configurations and is aimed at being a simple proxy for the verifier contract on any chain. + */ +contract VerifierProxy is IVerifierProxy, ConfirmedOwner, TypeAndVersionInterface { + /// @notice The active verifier for this proxy + IVerifierProxyVerifier private s_verifier; + + /// @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 that does not implement the expected interface + error VerifierInvalid(address verifierAddress); + + constructor() ConfirmedOwner(msg.sender) {} + + /// @inheritdoc TypeAndVersionInterface + function typeAndVersion() external pure override returns (string memory) { + return "VerifierProxy 0.5.0"; + } + + /// @inheritdoc IVerifierProxy + function verify(bytes calldata payload, bytes calldata parameterPayload) external payable returns (bytes memory) { + return s_verifier.verify{value: msg.value}(payload, parameterPayload, msg.sender); + } + + /// @inheritdoc IVerifierProxy + function verifyBulk( + bytes[] calldata payloads, + bytes calldata parameterPayload + ) external payable returns (bytes[] memory verifiedReports) { + return s_verifier.verifyBulk{value: msg.value}(payloads, parameterPayload, msg.sender); + } + + /// @inheritdoc IVerifierProxy + function setVerifier(address verifierAddress) external onlyOwner { + //check it supports the functions we need + if (!IERC165(verifierAddress).supportsInterface(type(IVerifierProxyVerifier).interfaceId)) + revert VerifierInvalid(verifierAddress); + + s_verifier = IVerifierProxyVerifier(verifierAddress); + } + + /// @inheritdoc IVerifierProxy + // solhint-disable-next-line func-name-mixedcase + function s_feeManager() external view override returns (address) { + return s_verifier.s_feeManager(); + } + + /// @inheritdoc IVerifierProxy + // solhint-disable-next-line func-name-mixedcase + function s_accessController() external view override returns (address) { + return s_verifier.s_accessController(); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == type(IVerifierProxy).interfaceId; + } +} 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..f00da99f036 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IFeeManager.sol @@ -0,0 +1,103 @@ +// 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 IFeeManager is IERC165 { + /** + * @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 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 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 the verifier to the list of verifiers able to use the feeManager + * @param verifier address of the verifier + */ + function addVerifier(address verifier) external; + + /** + * @notice Removes the verifier from the list of verifiers able to use the feeManager + * @param verifier address of the verifier + */ + function removeVerifier(address verifier) external; + + /** + * @notice Sets the reward manager to the address + * @param rewardManager address of the reward manager + */ + function setRewardManager(address rewardManager) 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..bc6e057ec61 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IRewardManager.sol @@ -0,0 +1,75 @@ +// 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 Add the fee manager to the list of feeManagers able to call the reward manager + * @param newFeeManager address of the new verifier proxy + */ + function addFeeManager(address newFeeManager) external; + + /** + * @notice Removes the fee manager. This needs to be done post construction to prevent a circular dependency. + * @param feeManager address of the verifier proxy to remove + */ + function removeFeeManager(address feeManager) 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..ec1b383f747 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifier.sol @@ -0,0 +1,48 @@ +// 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 {CommonV5} from "../libraries/CommonV5.sol"; + +interface IVerifier is IERC165 { + /** + * @notice sets off-chain reporting protocol configuration incl. participating oracles + * @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[] memory signers, + uint8 f, + Common.AddressAndWeight[] memory recipientAddressesAndWeights + ) external; + + /** + * @notice unsets off-chain reporting protocol configuration + * @param signers addresses with which oracles sign the reports + * @param signers within the existing config + */ + function unsetConfig(bytes32 configDigest, address[] calldata signers) external; + + + /** + * @notice Sets the fee manager address + * @param feeManager The address of the fee manager + */ + function setFeeManager(address feeManager) external; + + /** + * @notice Sets the access controller address + * @param accessController The address of the access controller + */ + function setAccessController(address accessController) external; + + /** + * @notice Updates the config active status + * @param configDigest The ID of the config to update + * @param isActive The new config active status + */ + function setConfigActive(bytes32 configDigest, bool isActive) external; +} 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..485ab4ecd87 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierFeeManager.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 IVerifierFeeManager is IERC165 { + /** + * @notice Handles fees for a report from the subscriber and manages rewards + * @param poolId pool id of the pool to pay into + * @param payload report to process the fee for + * @param parameterPayload fee payload + * @param subscriber address of the fee will be applied + */ + function processFee( + bytes32 poolId, + bytes calldata payload, + bytes calldata parameterPayload, + address subscriber + ) external payable; + + /** + * @notice Handles fees for a report from the subscriber and manages rewards. PoolId is retrieved from the first 32 bytes of the payload (configDigest). + * @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 poolIds pool ids of the pool to pay into + * @param payloads reports to process + * @param parameterPayload fee payload + * @param subscriber address of the user to process fee for + */ + function processFeeBulk( + bytes32[] memory poolIds, + bytes[] calldata payloads, + 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. PoolIds are retrieved from the first 32 bytes of each payload (configDigest). + * @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..7ba305859dd --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierProxy.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; + +interface IVerifierProxy is IERC165 { + /** + * @notice Verifies that the data encoded has been signed + * correctly by routing to the 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 active verifier for this proxy + * @param verifierAddress The address of the verifier contract + */ + function setVerifier(address verifierAddress) external; + + /** + * @notice Used to honor the source verifierProxy feeManager interface + * @return IVerifierFeeManager + */ + // solhint-disable-next-line func-name-mixedcase + function s_feeManager() external view returns (address); + + /** + * @notice Used to honor the source verifierProxy feeManager interface + * @return AccessControllerInterface + */ + // solhint-disable-next-line func-name-mixedcase + function s_accessController() external view returns (address); +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierProxyV03.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierProxyV03.sol new file mode 100644 index 00000000000..c091a9fc757 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierProxyV03.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 IVerifierProxyV03 { + /** + * @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/interfaces/IVerifierProxyVerifier.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierProxyVerifier.sol new file mode 100644 index 00000000000..0b5e08cfced --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/interfaces/IVerifierProxyVerifier.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; + +interface IVerifierProxyVerifier is IERC165 { + /** + * @notice Verifies that the data encoded has been signed correctly using the signatures included within the payload. + * @param signedReport The encoded data to be verified. + * @param parameterPayload The encoded parameters to be used in the verification and billing process. + * @param sender The address that requested to verify the contract.Used for logging and applying the fee. + * @dev Verification is typically only done through the proxy contract so we can't just use msg.sender. + * @return verifierResponse The encoded verified response. + */ + function verify( + bytes calldata signedReport, + bytes calldata parameterPayload, + address sender + ) external payable returns (bytes memory verifierResponse); + + /** + * @notice Bulk verifies that the data encoded has been signed correctly using the signatures included within the payload. + * @param signedReports The encoded data to be verified. + * @param parameterPayload The encoded parameters to be used in the verification and billing process. + * @param sender The address that requested to verify the contract. Used for logging and applying the fee. + * @dev Verification is typically only done through the proxy contract so we can't just use msg.sender. + * @return verifiedReports The encoded verified responses. + */ + function verifyBulk( + bytes[] calldata signedReports, + bytes calldata parameterPayload, + address sender + ) external payable returns (bytes[] memory verifiedReports); + + /* + * @notice Returns the reward manager + * @return IRewardManager + */ + // solhint-disable-next-line func-name-mixedcase + function s_feeManager() external view returns (address); + + /** + * @notice Returns the access controller + * @return IFeeManager + */ + // solhint-disable-next-line func-name-mixedcase + function s_accessController() external view returns (address); +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/libraries/CommonV5.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/libraries/CommonV5.sol new file mode 100644 index 00000000000..2b49474c485 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/libraries/CommonV5.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/* + * @title Common + * @author Michael Fletcher + * @notice Common functions and structs + */ +library CommonV5 { + struct Config { + // The ID of the Config + bytes32 configDigest; + // Fault tolerance of the DON + uint8 f; + // Whether the config is active + bool isActive; + // Number of oracles + uint8 oracleCount; + // Map of signer addresses to configDigest + mapping(address => bool) oracles; + } +} 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..8b091416b17 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/BaseFeeManager.t.sol @@ -0,0 +1,401 @@ +// 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"); + bytes32 internal constant DEFAULT_CONFIG_DIGEST2 = keccak256("DEFAULT_CONFIG_DIGEST2"); + + //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; + bytes4 internal immutable POOLID_MISMATCH_ERROR = FeeManager.PoolIdMismatch.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(address(feeManager)); + + //link the feeManager to the reward manager + rewardManager.addFeeManager(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( + bytes32 poolId, + 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}(poolId, payload, abi.encode(tokenAddress), subscriber); + + //change ProcessFeeAsUserback to the original address + changePrank(originalAddr); + } + + function processFee( + bytes32 poolId, + 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}(poolId, payload, abi.encode(feeAddress)); + + //change back to the original address + changePrank(originalAddr); + } + + function processFee( + bytes32[] memory poolIds, + 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}(poolIds, 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..bf92f13ac37 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.general.t.sol @@ -0,0 +1,299 @@ +// 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(contractFees[0].poolId, 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(contractFees[0].poolId, 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(DEFAULT_CONFIG_DIGEST, payload, USER, address(native), 0); + processFee(DEFAULT_CONFIG_DIGEST, 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); + } + + function test_revertOnSettingAnAddressZeroVerifier() public { + vm.expectRevert(INVALID_ADDRESS_ERROR); + feeManager.addVerifier(address(0)); + } + + function test_onlyCallableByOwnerReverts() public { + address STRANGER = address(999); + changePrank(STRANGER); + vm.expectRevert(bytes("Only callable by owner")); + feeManager.addVerifier(address(0)); + } + + function test_addVerifierExistingAddress() public { + address dummyAddress = address(998); + feeManager.addVerifier(dummyAddress); + vm.expectRevert(INVALID_ADDRESS_ERROR); + feeManager.addVerifier(dummyAddress); + } + + function test_addVerifier() public { + address dummyAddress = address(998); + feeManager.addVerifier(dummyAddress); + vm.expectRevert(INVALID_ADDRESS_ERROR); + feeManager.addVerifier(dummyAddress); + + // check calls to setFeeRecipients it should not error unauthorized + changePrank(dummyAddress); + bytes32 dummyConfigDigest = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; + Common.AddressAndWeight[] memory recipients = new Common.AddressAndWeight[](1); + recipients[0] = Common.AddressAndWeight(address(991), 1e18); + feeManager.setFeeRecipients(dummyConfigDigest, recipients); + + // removing this verifier should result in unauthorized when calling setFeeRecipients + changePrank(ADMIN); + feeManager.removeVerifier(dummyAddress); + changePrank(dummyAddress); + vm.expectRevert(UNAUTHORIZED_ERROR); + feeManager.setFeeRecipients(dummyConfigDigest, recipients); + } + + function test_removeVerifierZeroAaddress() public { + address dummyAddress = address(0); + vm.expectRevert(INVALID_ADDRESS_ERROR); + feeManager.removeVerifier(dummyAddress); + } + + function test_removeVerifierNonExistentAddress() public { + address dummyAddress = address(991); + vm.expectRevert(INVALID_ADDRESS_ERROR); + feeManager.removeVerifier(dummyAddress); + } + + function test_setRewardManagerZeroAddress() public { + vm.expectRevert(INVALID_ADDRESS_ERROR); + feeManager.setRewardManager(address(0)); + } +} 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..c2ce83bd38c --- /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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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(DEFAULT_CONFIG_DIGEST, 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..d99b1cb3a50 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/fee-manager/FeeManager.processFeeBulk.t.sol @@ -0,0 +1,310 @@ +// 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; + } + + bytes32[] memory poolIds = new bytes32[](NUMBER_OF_REPORTS); + for (uint256 i = 0; i < NUMBER_OF_REPORTS; ++i) { + poolIds[i] = DEFAULT_CONFIG_DIGEST; + } + + approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS, USER); + + processFee(poolIds, 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; + } + + bytes32[] memory poolIds = new bytes32[](NUMBER_OF_REPORTS); + for (uint256 i = 0; i < NUMBER_OF_REPORTS; ++i) { + poolIds[i] = DEFAULT_CONFIG_DIGEST; + } + + approveNative(address(feeManager), DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS, USER); + + processFee(poolIds, 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; + } + + bytes32[] memory poolIds = new bytes32[](NUMBER_OF_REPORTS); + for (uint256 i = 0; i < NUMBER_OF_REPORTS; ++i) { + poolIds[i] = DEFAULT_CONFIG_DIGEST; + } + + processFee(poolIds, 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); + + bytes32[] memory poolIds = new bytes32[](5); + for (uint256 i = 0; i < 5; ++i) { + poolIds[i] = DEFAULT_CONFIG_DIGEST; + } + + processFee(poolIds, 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; + + bytes32[] memory poolIds = new bytes32[](5); + for (uint256 i = 0; i < 5; ++i) { + poolIds[i] = DEFAULT_CONFIG_DIGEST; + } + + processFee(poolIds, 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; + } + + bytes32[] memory poolIds = new bytes32[](NUMBER_OF_REPORTS); + for (uint256 i = 0; i < NUMBER_OF_REPORTS; ++i) { + poolIds[i] = DEFAULT_CONFIG_DIGEST; + } + + processFee(poolIds, 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(); + + bytes32[] memory poolIds = new bytes32[](5); + for (uint256 i = 0; i < 5; ++i) { + poolIds[i] = payments[i].poolId; + } + + emit InsufficientLink(payments); + + processFee(poolIds, 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); + } + + function test_processPoolIdsPassedMismatched() 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; + } + + // poolIds passed are different that number of reports in payload + bytes32[] memory poolIds = new bytes32[](NUMBER_OF_REPORTS - 1); + for (uint256 i = 0; i < NUMBER_OF_REPORTS - 1; ++i) { + poolIds[i] = DEFAULT_CONFIG_DIGEST; + } + + vm.expectRevert(POOLID_MISMATCH_ERROR); + processFee(poolIds, payloads, USER, address(native), DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS * 2); + } + + function test_poolIdsCannotBeZeroAddress() 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; + } + + bytes32[] memory poolIds = new bytes32[](NUMBER_OF_REPORTS); + for (uint256 i = 0; i < NUMBER_OF_REPORTS; ++i) { + poolIds[i] = DEFAULT_CONFIG_DIGEST; + } + + poolIds[2] = 0x000; + vm.expectRevert(INVALID_ADDRESS_ERROR); + processFee(poolIds, payloads, USER, address(native), DEFAULT_REPORT_NATIVE_FEE * NUMBER_OF_REPORTS * 2); + } + + function test_rewardsAreCorrectlySentToEachAssociatedPoolWhenVerifyingInBulk() 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; + } + + bytes32[] memory poolIds = new bytes32[](NUMBER_OF_REPORTS); + for (uint256 i = 0; i < NUMBER_OF_REPORTS - 1; ++i) { + poolIds[i] = DEFAULT_CONFIG_DIGEST; + } + poolIds[NUMBER_OF_REPORTS - 1] = DEFAULT_CONFIG_DIGEST2; + + approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE * NUMBER_OF_REPORTS, USER); + + // Checking no rewards yet for each pool + for (uint256 i = 0; i < NUMBER_OF_REPORTS; ++i) { + bytes32 p_id = poolIds[i]; + uint256 poolDeficit = rewardManager.s_totalRewardRecipientFees(p_id); + assertEq(poolDeficit, 0); + } + + processFee(poolIds, 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); + + assertEq(USER.balance, DEFAULT_NATIVE_MINT_QUANTITY); + assertEq(PROXY.balance, DEFAULT_NATIVE_MINT_QUANTITY); + + // Checking each pool got the correct rewards + uint256 expectedRewards = DEFAULT_REPORT_LINK_FEE * (NUMBER_OF_REPORTS - 1); + uint256 poolRewards = rewardManager.s_totalRewardRecipientFees(DEFAULT_CONFIG_DIGEST); + assertEq(poolRewards, expectedRewards); + + expectedRewards = DEFAULT_REPORT_LINK_FEE; + poolRewards = rewardManager.s_totalRewardRecipientFees(DEFAULT_CONFIG_DIGEST2); + assertEq(poolRewards, expectedRewards); + } +} 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..2545220adbb --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/mocks/FeeManagerProxy.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IVerifierFeeManager} from "../../interfaces/IVerifierFeeManager.sol"; + +contract FeeManagerProxy { + IVerifierFeeManager internal s_feeManager; + + function processFee(bytes32 poolId, bytes calldata payload, bytes calldata parameterPayload) public payable { + s_feeManager.processFee{value: msg.value}(poolId, payload, parameterPayload, msg.sender); + } + + function processFeeBulk( + bytes32[] memory poolIds, + bytes[] calldata payloads, + bytes calldata parameterPayload + ) public payable { + s_feeManager.processFeeBulk{value: msg.value}(poolIds, payloads, parameterPayload, msg.sender); + } + + function setFeeManager(address feeManager) public { + s_feeManager = IVerifierFeeManager(feeManager); + } +} 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..6f6822a0132 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/BaseRewardManager.t.sol @@ -0,0 +1,264 @@ +// 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 RewardManagerTest + * @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 feeManager contract + address internal constant FEE_MANAGER = address(uint160(uint256(keccak256("FEE_MANAGER")))); + //address to represent another feeManager + address internal constant FEE_MANAGER_2 = address(uint160(uint256(keccak256("FEE_MANAGER_2")))); + //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.addFeeManager(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.addFeeManager(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..bde7a6234aa --- /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 RewardManagerClaimTest + * @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..e39086e7c33 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/reward-manager/RewardManager.general.t.sol @@ -0,0 +1,99 @@ +// 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 RewardManagerSetupTest + * @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_2); + + //set the verifier proxy + setFeeManager(FEE_MANAGER_2, 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); + } + + function test_addFeeManagerZeroAddress() public { + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + rewardManager.addFeeManager(address(0)); + } + + function test_addFeeManagerExistingAddress() public { + address dummyAddress = address(998); + rewardManager.addFeeManager(dummyAddress); + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + rewardManager.addFeeManager(dummyAddress); + } + + function test_removeFeeManagerNonExistentAddress() public { + address dummyAddress = address(991); + vm.expectRevert(INVALID_ADDRESS_ERROR_SELECTOR); + rewardManager.removeFeeManager(dummyAddress); + } + + function test_addRemoveFeeManager() public { + address dummyAddress1 = address(1); + address dummyAddress2 = address(2); + rewardManager.addFeeManager(dummyAddress1); + rewardManager.addFeeManager(dummyAddress2); + assertEq(rewardManager.s_feeManagerAddressList(dummyAddress1), dummyAddress1); + assertEq(rewardManager.s_feeManagerAddressList(dummyAddress2), dummyAddress2); + rewardManager.removeFeeManager(dummyAddress1); + assertEq(rewardManager.s_feeManagerAddressList(dummyAddress1), address(0)); + assertEq(rewardManager.s_feeManagerAddressList(dummyAddress2), dummyAddress2); + } +} 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..b27fe4409e4 --- /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 RewardManagerPayRecipientsTest + * @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..fc416563151 --- /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 RewardManagerSetRecipientsTest + * @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_2, ADMIN); + + //set the recipients + setRewardRecipients(PRIMARY_POOL_ID, getPrimaryRecipients(), FEE_MANAGER_2); + } + + 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..942157d17ac --- /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 RewardManagerUpdateRewardRecipientsTest + * @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..6300c915353 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/BaseVerifierTest.t.sol @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {WERC20Mock} from "../../../../shared/mocks/WERC20Mock.sol"; +import {ERC20Mock} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/mocks/ERC20Mock.sol"; +import {Common} from "../../../libraries/Common.sol"; +import {FeeManager} from "../../FeeManager.sol"; + +import {RewardManager} from "../../RewardManager.sol"; +import {Verifier} from "../../Verifier.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; +import {Test} from "forge-std/Test.sol"; + +contract BaseTest is Test { + uint64 internal constant POOL_SCALAR = 1e18; + uint64 internal constant ONE_PERCENT = POOL_SCALAR / 100; + 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 ACCESS_CONTROLLER_ADDRESS = address(300); + + uint256 internal constant DEFAULT_REPORT_LINK_FEE = 1e10; + uint256 internal constant DEFAULT_REPORT_NATIVE_FEE = 1e12; + + uint64 internal constant VERIFIER_VERSION = 1; + + uint8 internal constant FAULT_TOLERANCE = 10; + + bytes32 internal DEFAULT_CONFIG_DIGEST = keccak256("DEFAULT_CONFIG_DIGEST"); + + VerifierProxy internal s_verifierProxy; + Verifier internal s_verifier; + FeeManager internal feeManager; + RewardManager internal rewardManager; + ERC20Mock internal link; + WERC20Mock internal native; + + struct Signer { + uint256 mockPrivateKey; + address signerAddress; + } + + Signer[MAX_ORACLES] internal s_signers; + bytes32[] internal s_offchaintransmitters; + bool private s_baseTestInitialized; + + 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; + } + + bytes32 internal constant V_MASK = 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + bytes32 internal constant V1_BITMASK = 0x0001000000000000000000000000000000000000000000000000000000000000; + bytes32 internal constant V2_BITMASK = 0x0002000000000000000000000000000000000000000000000000000000000000; + bytes32 internal constant V3_BITMASK = 0x0003000000000000000000000000000000000000000000000000000000000000; + + 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; + + //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; + + 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 _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 _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 _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 _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 _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); + } + + function _approveLink(address spender, uint256 quantity, address sender) internal { + address originalAddr = msg.sender; + changePrank(sender); + + link.approve(spender, quantity); + changePrank(originalAddr); + } + + 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); + + s_verifierProxy = new VerifierProxy(); + s_verifier = new Verifier(address(s_verifierProxy)); + s_verifierProxy.setVerifier(address(s_verifier)); + + // setting up FeeManager and RewardManager + native = new WERC20Mock(); + link = new ERC20Mock("LINK", "LINK", ADMIN, 0); + rewardManager = new RewardManager(address(link)); + feeManager = new FeeManager(address(link), address(native), address(s_verifier), address(rewardManager)); + + 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 _getSecondarySigners(uint256 numSigners) internal view returns (Signer[] memory) { + Signer[] memory signers = new Signer[](numSigners); + for (uint256 i; i < numSigners; i++) { + signers[i].signerAddress = vm.addr(i + 2); + signers[i].mockPrivateKey = i + 2; + } + return signers; + } + + function _getSignerAddresses(Signer[] memory signers) internal pure returns (address[] memory) { + address[] memory signerAddrs = new address[](signers.length); + for (uint256 i = 0; i < signerAddrs.length; i++) { + signerAddrs[i] = signers[i].signerAddress; + } + return signerAddrs; + } + + function _signerAddressAndDonConfigKey(address signer, bytes24 donConfigId) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(signer, donConfigId)); + } + + function _donConfigIdFromConfigData(address[] memory signers, uint8 f) internal pure returns (bytes24) { + Common._quickSort(signers, 0, int256(signers.length - 1)); + bytes24 donConfigId = bytes24(keccak256(abi.encodePacked(signers, f))); + return donConfigId; + } + + function assertReportsEqual(bytes memory response, V3Report memory testReport) public pure { + ( + bytes32 feedId, + uint32 observationsTimestamp, + uint32 validFromTimestamp, + uint192 nativeFee, + uint192 linkFee, + uint32 expiresAt, + int192 benchmarkPrice, + int192 bid, + int192 ask + ) = abi.decode(response, (bytes32, uint32, uint32, uint192, uint192, uint32, int192, int192, int192)); + assertEq(feedId, testReport.feedId); + assertEq(observationsTimestamp, testReport.observationsTimestamp); + assertEq(validFromTimestamp, testReport.validFromTimestamp); + assertEq(expiresAt, testReport.expiresAt); + assertEq(benchmarkPrice, testReport.benchmarkPrice); + assertEq(bid, testReport.bid); + assertEq(ask, testReport.ask); + assertEq(linkFee, testReport.linkFee); + assertEq(nativeFee, testReport.nativeFee); + } + + function _approveNative(address spender, uint256 quantity, address sender) internal { + address originalAddr = msg.sender; + changePrank(sender); + + native.approve(spender, quantity); + changePrank(originalAddr); + } +} + +contract VerifierWithFeeManager is BaseTest { + uint256 internal constant DEFAULT_LINK_MINT_QUANTITY = 100 ether; + uint256 internal constant DEFAULT_NATIVE_MINT_QUANTITY = 100 ether; + + function setUp() public virtual override { + BaseTest.setUp(); + + s_verifierProxy.setVerifier(address(s_verifier)); + s_verifier.setFeeManager(address(feeManager)); + rewardManager.addFeeManager(address(feeManager)); + + //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 MultipleVerifierWithMultipleFeeManagers is BaseTest { + uint256 internal constant DEFAULT_LINK_MINT_QUANTITY = 100 ether; + uint256 internal constant DEFAULT_NATIVE_MINT_QUANTITY = 100 ether; + + Verifier internal s_verifier2; + Verifier internal s_verifier3; + + VerifierProxy internal s_verifierProxy2; + VerifierProxy internal s_verifierProxy3; + + FeeManager internal feeManager2; + + function setUp() public virtual override { + /* + - Sets up 3 verifiers + - Sets up 2 Fee managers, wire the fee managers and verifiers + - Sets up a Reward Manager which can be used by both fee managers + */ + BaseTest.setUp(); + + s_verifierProxy2 = new VerifierProxy(); + s_verifierProxy3 = new VerifierProxy(); + + s_verifier2 = new Verifier(address(s_verifierProxy2)); + s_verifier3 = new Verifier(address(s_verifierProxy3)); + + s_verifierProxy2.setVerifier(address(s_verifier2)); + s_verifierProxy3.setVerifier(address(s_verifier3)); + + feeManager2 = new FeeManager(address(link), address(native), address(s_verifier), address(rewardManager)); + + s_verifier.setFeeManager(address(feeManager)); + s_verifier2.setFeeManager(address(feeManager)); + s_verifier3.setFeeManager(address(feeManager2)); + + // this is already set in the base contract + // feeManager.addVerifier(address(s_verifier)); + feeManager.addVerifier(address(s_verifier2)); + feeManager2.addVerifier(address(s_verifier3)); + + rewardManager.addFeeManager(address(feeManager)); + rewardManager.addFeeManager(address(feeManager2)); + + //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); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierInterfacesTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierInterfacesTest.t.sol new file mode 100644 index 00000000000..b1fc80a0dbe --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierInterfacesTest.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {VerifierWithFeeManager} from "./BaseVerifierTest.t.sol"; +import {IFeeManager} from "../../interfaces/IFeeManager.sol"; +import {IRewardManager} from "../../interfaces/IRewardManager.sol"; +import {Common} from "../../../libraries/Common.sol"; + +/* +This test checks the interfaces of destination verifier matches the expectations. +The code here comes from this example: + +https://docs.chain.link/chainlink-automation/guides/streams-lookup + +*/ + +// Custom interfaces for ITestVerifierProxy and ITestFeeManager +interface ITestVerifierProxy { + /** + * @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. For the current implementation this is just the abi-encoded fee token ERC-20 address. + * @return verifierResponse The encoded report from the verifier. + */ + function verify( + bytes calldata payload, + bytes calldata parameterPayload + ) external payable returns (bytes memory verifierResponse); + + function s_feeManager() external view returns (IFeeManager); +} + +interface ITestFeeManager { + /** + * @notice Calculates the fee and reward associated with verifying a report, including discounts for subscribers. + * This function assesses the fee and reward for report verification, applying a discount for recognized subscriber addresses. + * @param subscriber The address attempting to verify the report. A discount is applied if this address + * is recognized as a subscriber. + * @param unverifiedReport The report data awaiting verification. The content of this report is used to + * determine the base fee and reward, before considering subscriber discounts. + * @param quoteAddress The payment token address used for quoting fees and rewards. + * @return fee The fee assessed for verifying the report, with subscriber discounts applied where applicable. + * @return reward The reward allocated to the caller for successfully verifying the report. + * @return totalDiscount The total discount amount deducted from the fee for subscribers. + */ + function getFeeAndReward( + address subscriber, + bytes memory unverifiedReport, + address quoteAddress + ) external returns (Common.Asset memory, Common.Asset memory, uint256); + + function i_linkAddress() external view returns (address); + + function i_nativeAddress() external view returns (address); + + function i_rewardManager() external view returns (address); +} + +//Tests +// https://docs.chain.link/chainlink-automation/guides/streams-lookup +contract VerifierInterfacesTest is VerifierWithFeeManager { + address internal constant DEFAULT_RECIPIENT_1 = address(uint160(uint256(keccak256("DEFAULT_RECIPIENT_1")))); + + ITestVerifierProxy public verifier; + V3Report internal s_testReport; + + address public FEE_ADDRESS; + string public constant DATASTREAMS_FEEDLABEL = "feedIDs"; + string public constant DATASTREAMS_QUERYLABEL = "timestamp"; + int192 public last_retrieved_price; + bytes internal signedReport; + bytes32[3] internal s_reportContext; + uint8 MINIMAL_FAULT_TOLERANCE = 2; + + function setUp() public virtual override { + VerifierWithFeeManager.setUp(); + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + Signer[] memory signers = _getSigners(MAX_ORACLES); + + s_testReport = 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 + }); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](1); + weights[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, ONE_PERCENT * 100); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, MINIMAL_FAULT_TOLERANCE, weights); + signedReport = _generateV3EncodedBlob(s_testReport, s_reportContext, signers); + + verifier = ITestVerifierProxy(address(s_verifierProxy)); + } + + function test_DestinationContractInterfaces() public { + bytes memory unverifiedReport = signedReport; + + (, bytes memory reportData) = abi.decode(unverifiedReport, (bytes32[3], bytes)); + + // Report verification fees + ITestFeeManager feeManager = ITestFeeManager(address(verifier.s_feeManager())); + IRewardManager rewardManager = IRewardManager(address(feeManager.i_rewardManager())); + + address feeTokenAddress = feeManager.i_linkAddress(); + (Common.Asset memory fee, , ) = feeManager.getFeeAndReward(address(this), reportData, feeTokenAddress); + + // Approve rewardManager to spend this contract's balance in fees + _approveLink(address(rewardManager), fee.amount, USER); + _verify(unverifiedReport, address(feeTokenAddress), 0, USER); + + assertEq(link.balanceOf(USER), DEFAULT_LINK_MINT_QUANTITY - fee.amount); + assertEq(link.balanceOf(address(rewardManager)), fee.amount); + } +} 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..a8028a39d4d --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierProxyTest.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseVerifierTest.t.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; + +contract VerifierProxyInitializeVerifierTest is BaseTest { + function test_setVerifierCalledByNoOwner() public { + address STRANGER = address(999); + changePrank(STRANGER); + vm.expectRevert(bytes("Only callable by owner")); + s_verifierProxy.setVerifier(address(s_verifier)); + } + + function test_setVerifierWhichDoesntHonourInterface() public { + vm.expectRevert(abi.encodeWithSelector(VerifierProxy.VerifierInvalid.selector, address(rewardManager))); + s_verifierProxy.setVerifier(address(rewardManager)); + } + + function test_setVerifierOk() public { + s_verifierProxy.setVerifier(address(s_verifier)); + assertEq(s_verifierProxy.s_feeManager(), s_verifier.s_feeManager()); + assertEq(s_verifierProxy.s_accessController(), s_verifier.s_accessController()); + } + + function test_correctlySetsTheOwner() public { + VerifierProxy proxy = new VerifierProxy(); + assertEq(proxy.owner(), ADMIN); + } + + function test_correctlySetsVersion() public view { + string memory version = s_verifierProxy.typeAndVersion(); + assertEq(version, "VerifierProxy 0.5.0"); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetAccessControllerTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetAccessControllerTest.t.sol new file mode 100644 index 00000000000..337d652e407 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetAccessControllerTest.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseVerifierTest.t.sol"; + +contract VerifierSetAccessControllerTest is BaseTest { + event AccessControllerSet(address oldAccessController, address newAccessController); + + function test_revertsIfCalledByNonOwner() public { + vm.expectRevert("Only callable by owner"); + + changePrank(USER); + s_verifier.setAccessController(ACCESS_CONTROLLER_ADDRESS); + } + + function test_successfullySetsNewAccessController() public { + s_verifier.setAccessController(ACCESS_CONTROLLER_ADDRESS); + address ac = s_verifier.s_accessController(); + assertEq(ac, ACCESS_CONTROLLER_ADDRESS); + } + + function test_successfullySetsNewAccessControllerIsEmpty() public { + s_verifier.setAccessController(address(0)); + address ac = s_verifier.s_accessController(); + assertEq(ac, address(0)); + } + + function test_emitsTheCorrectEvent() public { + vm.expectEmit(true, false, false, false); + emit AccessControllerSet(address(0), ACCESS_CONTROLLER_ADDRESS); + s_verifier.setAccessController(ACCESS_CONTROLLER_ADDRESS); + } +} 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..2e8f1666b47 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetConfigTest.t.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Common} from "../../../libraries/Common.sol"; +import {Verifier} from "../../Verifier.sol"; +import {BaseTest} from "./BaseVerifierTest.t.sol"; + +contract VerifierSetConfigTest is BaseTest { + event ConfigUnset(bytes32 configDigest, address[] signers); + event ConfigSet(bytes32 indexed configDigest, address[] signers, uint8 f, Common.AddressAndWeight[] recipientAddressesAndWeights); + + function setUp() public virtual override { + BaseTest.setUp(); + } + + function test_revertsIfCalledByNonOwner() public { + vm.expectRevert("Only callable by owner"); + Signer[] memory signers = _getSigners(MAX_ORACLES); + changePrank(USER); + s_verifier.setConfig( + DEFAULT_CONFIG_DIGEST, + _getSignerAddresses(signers), + FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + } + + function test_revertsIfSetWithTooManySigners() public { + address[] memory signers = new address[](MAX_ORACLES + 1); + vm.expectRevert(abi.encodeWithSelector(Verifier.ExcessSigners.selector, signers.length, MAX_ORACLES)); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signers, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + } + + function test_revertsIfFaultToleranceIsZero() public { + vm.expectRevert(abi.encodeWithSelector(Verifier.FaultToleranceMustBePositive.selector)); + Signer[] memory signers = _getSigners(MAX_ORACLES); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, _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); + + vm.expectRevert( + abi.encodeWithSelector(Verifier.InsufficientSigners.selector, signers.length, FAULT_TOLERANCE * 3 + 1) + ); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, 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]; + vm.expectRevert(abi.encodeWithSelector(Verifier.NonUniqueSignatures.selector)); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, 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); + vm.expectRevert(abi.encodeWithSelector(Verifier.ZeroAddress.selector)); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + } + + function test_setConfigActiveUnknownConfigId() public { + vm.expectRevert(abi.encodeWithSelector(Verifier.ConfigDoesNotExist.selector)); + s_verifier.setConfigActive(bytes32(uint256(3)), true); + } + + function test_settingDuplicateConfigFails() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + vm.expectRevert(abi.encodeWithSelector(Verifier.ConfigAlreadyExists.selector, DEFAULT_CONFIG_DIGEST)); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + } + + function test_settingZeroConfigDigestFails() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + vm.expectRevert(abi.encodeWithSelector(Verifier.ZeroAddress.selector)); + s_verifier.setConfig(bytes32(0), signerAddrs, FAULT_TOLERANCE, weights); + } + + function test_feeManagerCanBeRemoved() public { + s_verifier.setFeeManager(address(0)); + assertEq(s_verifier.s_feeManager(), address(0)); + } + + function test_setConfigSucceedsAfterUnsetConfig() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, signerAddrs); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + } + + function test_unsetConfigFailsWithInvalidSigners() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + signerAddrs[0] = address(0); + vm.expectRevert(abi.encodeWithSelector(Verifier.MismatchedSigners.selector)); + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, signerAddrs); + } + + function test_unsetConfigFailsWithDuplicateSigners() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + signerAddrs[0] = signerAddrs[1]; + vm.expectRevert(abi.encodeWithSelector(Verifier.MismatchedSigners.selector)); + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, signerAddrs); + } + + function test_unsetConfigFailsWithSubsetOfSigners() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + address[] memory subSignerAddres = new address[](2); + subSignerAddres[0] = signerAddrs[0]; + subSignerAddres[1] = signerAddrs[1]; + vm.expectRevert(abi.encodeWithSelector(Verifier.MismatchedSigners.selector)); + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, subSignerAddres); + } + + function test_unsetSignersEmitsEvent() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + + vm.expectEmit(); + + emit ConfigUnset(DEFAULT_CONFIG_DIGEST, signerAddrs); + + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, signerAddrs); + } + + function test_setConfigEmitsEvents() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + vm.expectEmit(); + + emit ConfigSet(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + } + + function test_unsetUnsetConfig() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, signerAddrs); + vm.expectRevert(abi.encodeWithSelector(Verifier.ConfigDoesNotExist.selector)); + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, signerAddrs); + } + + function test_onlyAdminCanUnsetConfig() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + changePrank(USER); + vm.expectRevert("Only callable by owner"); + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, signerAddrs); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetFeeManagerTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetFeeManagerTest.t.sol new file mode 100644 index 00000000000..d942b61989f --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierSetFeeManagerTest.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaseTest} from "./BaseVerifierTest.t.sol"; +import {Verifier} from "../../Verifier.sol"; + +contract VerifierSetAccessControllerTest is BaseTest { + event FeeManagerSet(address oldFeeManager, address newFeeManager); + + function test_revertsIfCalledByNonOwner() public { + vm.expectRevert("Only callable by owner"); + changePrank(USER); + s_verifier.setFeeManager(address(feeManager)); + } + + function test_successfullySetsNewFeeManager() public { + vm.expectEmit(true, false, false, false); + emit FeeManagerSet(address(0), ACCESS_CONTROLLER_ADDRESS); + s_verifier.setFeeManager(address(feeManager)); + address ac = s_verifier.s_feeManager(); + assertEq(ac, address(feeManager)); + } + + function test_setFeeManagerWhichDoesntHonourInterface() public { + vm.expectRevert(abi.encodeWithSelector(Verifier.FeeManagerInvalid.selector)); + s_verifier.setFeeManager(address(rewardManager)); + } +} 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..d0c402885be --- /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"; +import {IVerifier} from "../../interfaces/IVerifier.sol"; +import {IVerifierProxyVerifier} from "../../interfaces/IVerifierProxyVerifier.sol"; +import {Common} from "../../../libraries/Common.sol"; + +contract VerifierConstructorTest is BaseTest { + bytes32[3] internal s_reportContext; + + function test_revertsIfInitializedWithEmptyVerifierProxy() public { + vm.expectRevert(abi.encodeWithSelector(Verifier.ZeroAddress.selector)); + new Verifier(address(0)); + } + + function test_typeAndVersion() public { + Verifier v = new Verifier(address(s_verifierProxy)); + assertEq(v.owner(), ADMIN); + string memory typeAndVersion = s_verifier.typeAndVersion(); + assertEq(typeAndVersion, "Verifier 0.5.0"); + } + + 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(type(IVerifier).interfaceId) && + s_verifier.supportsInterface(type(IVerifierProxyVerifier).interfaceId); + 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..9aab6bdfa63 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestBillingReport.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {VerifierWithFeeManager} from "./BaseVerifierTest.t.sol"; +import {Common} from "../../../libraries/Common.sol"; + +contract VerifierBillingTests is VerifierWithFeeManager { + bytes32[3] internal s_reportContext; + V3Report internal s_testReportThree; + + function setUp() public virtual override { + VerifierWithFeeManager.setUp(); + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + s_testReportThree = 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 test_verifyWithLinkV3Report() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers); + bytes32 expectedDonConfigId = DEFAULT_CONFIG_DIGEST; + + _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); + + // internal state checks + assertEq(feeManager.s_linkDeficit(expectedDonConfigId), 0); + assertEq(rewardManager.s_totalRewardRecipientFees(expectedDonConfigId), DEFAULT_REPORT_LINK_FEE); + assertEq(link.balanceOf(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + } + + function test_verifyWithNativeERC20() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](1); + weights[0] = Common.AddressAndWeight(signerAddrs[0], ONE_PERCENT * 100); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + bytes memory signedReport = _generateV3EncodedBlob( + s_testReportThree, + s_reportContext, + _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 { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + bytes memory signedReport = _generateV3EncodedBlob( + s_testReportThree, + s_reportContext, + _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 { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + bytes memory signedReport = _generateV3EncodedBlob( + s_testReportThree, + s_reportContext, + _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 VerifierWithFeeManager { + uint256 internal constant NUMBERS_OF_REPORTS = 5; + + bytes32[3] internal s_reportContext; + + function setUp() public virtual override { + VerifierWithFeeManager.setUp(); + // setting a DonConfig we can reuse in the rest of tests + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](0); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + } + + function test_verifyWithBulkLink() public { + bytes memory signedReport = _generateV3EncodedBlob( + _generateV3Report(), + s_reportContext, + _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(), + s_reportContext, + _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(), + s_reportContext, + _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), 200 * 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(), + s_reportContext, + _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); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestRewards.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestRewards.t.sol new file mode 100644 index 00000000000..8c83f5b4e1b --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestRewards.t.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Common} from "../../../libraries/Common.sol"; +import {VerifierWithFeeManager} from "./BaseVerifierTest.t.sol"; + +contract VerifierBillingTests is VerifierWithFeeManager { + uint8 MINIMAL_FAULT_TOLERANCE = 2; + 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")))); + + 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); + } + + bytes32[3] internal s_reportContext; + V3Report internal s_testReport; + + function setUp() public virtual override { + VerifierWithFeeManager.setUp(); + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + s_testReport = generateReportAtTimestamp(block.timestamp); + } + + function generateReportAtTimestamp(uint256 timestamp) public pure returns (V3Report memory) { + return + V3Report({ + feedId: FEED_ID_V3, + observationsTimestamp: OBSERVATIONS_TIMESTAMP, + validFromTimestamp: uint32(timestamp), + nativeFee: uint192(DEFAULT_REPORT_NATIVE_FEE), + linkFee: uint192(DEFAULT_REPORT_LINK_FEE), + // ask michael about this expires at, is it usually set at what blocks + expiresAt: uint32(timestamp) + 500, + benchmarkPrice: MEDIAN, + bid: BID, + ask: ASK + }); + } + + function getRecipientAndWeightsGroup2() public pure returns (Common.AddressAndWeight[] memory, address[] memory) { + address[] memory recipients = new address[](4); + recipients[0] = DEFAULT_RECIPIENT_4; + recipients[1] = DEFAULT_RECIPIENT_5; + recipients[2] = DEFAULT_RECIPIENT_6; + recipients[3] = DEFAULT_RECIPIENT_7; + + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](4); + //init each recipient with even weights. 2500 = 25% of pool + weights[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, POOL_SCALAR / 4); + weights[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_5, POOL_SCALAR / 4); + weights[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_6, POOL_SCALAR / 4); + weights[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_7, POOL_SCALAR / 4); + return (weights, recipients); + } + + function getRecipientAndWeightsGroup1() public pure returns (Common.AddressAndWeight[] memory, address[] memory) { + 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; + + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](4); + //init each recipient with even weights. 2500 = 25% of pool + weights[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, POOL_SCALAR / 4); + weights[1] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, POOL_SCALAR / 4); + weights[2] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, POOL_SCALAR / 4); + weights[3] = Common.AddressAndWeight(DEFAULT_RECIPIENT_4, POOL_SCALAR / 4); + return (weights, recipients); + } + + function test_rewardsAreDistributedAccordingToWeights() public { + /* + Simple test verifying that rewards are distributed according to address and weights + associated to the DonConfig used to verify the report + */ + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](1); + weights[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, ONE_PERCENT * 100); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + bytes memory signedReport = _generateV3EncodedBlob(s_testReport, s_reportContext, signers); + bytes32 expectedDonConfigId = DEFAULT_CONFIG_DIGEST; + + _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); + + // internal state checks + assertEq(feeManager.s_linkDeficit(expectedDonConfigId), 0); + assertEq(rewardManager.s_totalRewardRecipientFees(expectedDonConfigId), DEFAULT_REPORT_LINK_FEE); + assertEq(link.balanceOf(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + // check the recipients are paid according to weights + address[] memory recipients = new address[](1); + recipients[0] = DEFAULT_RECIPIENT_1; + payRecipients(expectedDonConfigId, recipients, ADMIN); + assertEq(link.balanceOf(recipients[0]), DEFAULT_REPORT_LINK_FEE); + assertEq(link.balanceOf(address(rewardManager)), 0); + } + + function test_rewardsAreDistributedAccordingToWeightsMultipleWeigths() public { + /* + Rewards are distributed according to AddressAndWeight's + associated to the DonConfig used to verify the report: + - multiple recipients + - multiple verifications + */ + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + (Common.AddressAndWeight[] memory weights, address[] memory recipients) = getRecipientAndWeightsGroup1(); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + + bytes memory signedReport = _generateV3EncodedBlob(s_testReport, s_reportContext, signers); + bytes32 expectedDonConfigId = DEFAULT_CONFIG_DIGEST; + + uint256 number_of_reports_verified = 10; + + for (uint256 i = 0; i < number_of_reports_verified; i++) { + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + _verify(signedReport, address(link), 0, USER); + } + + uint256 expected_pool_amount = DEFAULT_REPORT_LINK_FEE * number_of_reports_verified; + + //each recipient should receive 1/4 of the pool + uint256 expectedRecipientAmount = expected_pool_amount / 4; + + payRecipients(expectedDonConfigId, recipients, ADMIN); + for (uint256 i = 0; i < recipients.length; i++) { + // checking each recipient got rewards as set by the weights + assertEq(link.balanceOf(recipients[i]), expectedRecipientAmount); + } + // checking nothing left in reward manager + assertEq(link.balanceOf(address(rewardManager)), 0); + } + + function test_rewardsAreDistributedAccordingToWeightsUsingHistoricalConfigs() public { + /* + Verifies that reports verified with historical give rewards according to the verifying config AddressAndWeight. + - Sets two Configs: ConfigA and ConfigB, These two Configs have different Recipient and Weights + - Verifies a couple reports with each config + - Pays recipients + - Asserts expected rewards for each recipient + */ + + Signer[] memory signers = _getSigners(10); + address[] memory signerAddrs = _getSignerAddresses(signers); + + (Common.AddressAndWeight[] memory weights, address[] memory recipients) = getRecipientAndWeightsGroup1(); + + // Create ConfigA + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, MINIMAL_FAULT_TOLERANCE, weights); + vm.warp(block.timestamp + 100); + + V3Report memory testReportAtT1 = generateReportAtTimestamp(block.timestamp); + bytes memory signedReportT1 = _generateV3EncodedBlob(testReportAtT1, s_reportContext, signers); + bytes32 expectedDonConfigIdA = DEFAULT_CONFIG_DIGEST; + + uint256 number_of_reports_verified = 2; + + // advancing the blocktimestamp so we can test verifying with configs + vm.warp(block.timestamp + 100); + + Signer[] memory signers2 = _getSigners(12); + address[] memory signerAddrs2 = _getSignerAddresses(signers2); + (Common.AddressAndWeight[] memory weights2, address[] memory recipients2) = getRecipientAndWeightsGroup2(); + + bytes32 DUMMY_CONFIG_DIGEST = keccak256("DUMMY_CONFIG_DIGEST"); + + // Create ConfigB + s_verifier.setConfig(DUMMY_CONFIG_DIGEST, signerAddrs2, MINIMAL_FAULT_TOLERANCE, weights2); + + V3Report memory testReportAtT2 = generateReportAtTimestamp(block.timestamp); + + // verifiying using ConfigA (report with Old timestamp) + for (uint256 i = 0; i < number_of_reports_verified; i++) { + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + _verify(signedReportT1, address(link), 0, USER); + } + + s_reportContext[0] = DUMMY_CONFIG_DIGEST; + + // verifying using ConfigB (report with new timestamp) + for (uint256 i = 0; i < number_of_reports_verified; i++) { + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + _verify(_generateV3EncodedBlob(testReportAtT2, s_reportContext, signers2), address(link), 0, USER); + } + + uint256 expected_pool_amount = DEFAULT_REPORT_LINK_FEE * number_of_reports_verified; + assertEq(rewardManager.s_totalRewardRecipientFees(expectedDonConfigIdA), expected_pool_amount); + assertEq(rewardManager.s_totalRewardRecipientFees(DUMMY_CONFIG_DIGEST), expected_pool_amount); + + // check the recipients are paid according to weights + payRecipients(expectedDonConfigIdA, recipients, ADMIN); + + for (uint256 i = 0; i < recipients.length; i++) { + // //each recipient should receive 1/4 of the pool + assertEq(link.balanceOf(recipients[i]), expected_pool_amount / 4); + } + + payRecipients(DUMMY_CONFIG_DIGEST, recipients2, ADMIN); + + for (uint256 i = 1; i < recipients2.length; i++) { + // //each recipient should receive 1/4 of the pool + assertEq(link.balanceOf(recipients2[i]), expected_pool_amount / 4); + } + + // this recipient was part of the two config weights + assertEq(link.balanceOf(recipients2[0]), (expected_pool_amount / 4) * 2); + assertEq(link.balanceOf(address(rewardManager)), 0); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestRewardsMultiVefifierFeeManager.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestRewardsMultiVefifierFeeManager.t.sol new file mode 100644 index 00000000000..413503db7a5 --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierTestRewardsMultiVefifierFeeManager.t.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Common} from "../../../libraries/Common.sol"; +import {VerifierProxy} from "../../VerifierProxy.sol"; +import {MultipleVerifierWithMultipleFeeManagers} from "./BaseVerifierTest.t.sol"; +import {RewardManager} from "../../RewardManager.sol"; + +contract MultiVerifierBillingTests is MultipleVerifierWithMultipleFeeManagers { + uint8 MINIMAL_FAULT_TOLERANCE = 2; + 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")))); + + bytes32[3] internal s_reportContext; + V3Report internal s_testReport; + + function setUp() public virtual override { + MultipleVerifierWithMultipleFeeManagers.setUp(); + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + s_testReport = generateReportAtTimestamp(block.timestamp); + } + + function _verify( + VerifierProxy proxy, + bytes memory payload, + address feeAddress, + uint256 wrappedNativeValue, + address sender + ) internal { + address originalAddr = msg.sender; + changePrank(sender); + + proxy.verify{value: wrappedNativeValue}(payload, abi.encode(feeAddress)); + + changePrank(originalAddr); + } + + function generateReportAtTimestamp(uint256 timestamp) public pure returns (V3Report memory) { + return + V3Report({ + feedId: FEED_ID_V3, + observationsTimestamp: OBSERVATIONS_TIMESTAMP, + validFromTimestamp: uint32(timestamp), + nativeFee: uint192(DEFAULT_REPORT_NATIVE_FEE), + linkFee: uint192(DEFAULT_REPORT_LINK_FEE), + // ask michael about this expires at, is it usually set at what blocks + expiresAt: uint32(timestamp) + 500, + benchmarkPrice: MEDIAN, + bid: BID, + ask: ASK + }); + } + + 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 test_multipleFeeManagersAndVerifiers() public { + /* + In this test we got: + - three verifiers (verifier, verifier2, verifier3). + - two fee managers (feeManager, feeManager2) + - one reward manager + + we glue: + - feeManager is used by verifier1 and verifier2 + - feeManager is used by verifier3 + - Rewardmanager is used by feeManager and feeManager2 + + In this test we do verificatons via verifier1, verifier2 and verifier3 and check that rewards are set accordingly + + */ + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](1); + weights[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, ONE_PERCENT * 100); + + Common.AddressAndWeight[] memory weights2 = new Common.AddressAndWeight[](1); + weights2[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, ONE_PERCENT * 100); + + Common.AddressAndWeight[] memory weights3 = new Common.AddressAndWeight[](1); + weights3[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, ONE_PERCENT * 100); + + bytes32 DUMMY_CONFIG_DIGEST_1 = keccak256("DUMMY_CONFIG_DIGEST_1"); + bytes32 DUMMY_CONFIG_DIGEST_2 = keccak256("DUMMY_CONFIG_DIGEST_2"); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + s_verifier2.setConfig(DUMMY_CONFIG_DIGEST_1, signerAddrs, MINIMAL_FAULT_TOLERANCE, weights2); + s_verifier3.setConfig(DUMMY_CONFIG_DIGEST_2, signerAddrs, MINIMAL_FAULT_TOLERANCE + 1, weights3); + + bytes memory signedReport = _generateV3EncodedBlob(s_testReport, s_reportContext, signers); + s_reportContext[0] = DUMMY_CONFIG_DIGEST_1; + bytes memory signedReport2 = _generateV3EncodedBlob(s_testReport, s_reportContext, signers); + s_reportContext[0] = DUMMY_CONFIG_DIGEST_2; + bytes memory signedReport3 = _generateV3EncodedBlob(s_testReport, s_reportContext, signers); + + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + _verify(s_verifierProxy, signedReport, address(link), 0, USER); + assertEq(link.balanceOf(USER), DEFAULT_LINK_MINT_QUANTITY - DEFAULT_REPORT_LINK_FEE); + + // internal state checks + assertEq(feeManager.s_linkDeficit(DEFAULT_CONFIG_DIGEST), 0); + assertEq(rewardManager.s_totalRewardRecipientFees(DEFAULT_CONFIG_DIGEST), DEFAULT_REPORT_LINK_FEE); + assertEq(link.balanceOf(address(rewardManager)), DEFAULT_REPORT_LINK_FEE); + + // check the recipients are paid according to weights + // These rewards happened through verifier1 and feeManager1 + address[] memory recipients = new address[](1); + recipients[0] = DEFAULT_RECIPIENT_1; + payRecipients(DEFAULT_CONFIG_DIGEST, recipients, ADMIN); + assertEq(link.balanceOf(recipients[0]), DEFAULT_REPORT_LINK_FEE); + assertEq(link.balanceOf(address(rewardManager)), 0); + + // these rewards happened through verifier2 and feeManager1 + address[] memory recipients2 = new address[](1); + recipients2[0] = DEFAULT_RECIPIENT_2; + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + _verify(s_verifierProxy2, signedReport2, address(link), 0, USER); + payRecipients(DUMMY_CONFIG_DIGEST_1, recipients2, ADMIN); + assertEq(link.balanceOf(recipients2[0]), DEFAULT_REPORT_LINK_FEE); + assertEq(link.balanceOf(address(rewardManager)), 0); + + // these rewards happened through verifier3 and feeManager2 + address[] memory recipients3 = new address[](1); + recipients3[0] = DEFAULT_RECIPIENT_3; + _approveLink(address(rewardManager), DEFAULT_REPORT_LINK_FEE, USER); + _verify(s_verifierProxy3, signedReport3, address(link), 0, USER); + payRecipients(DUMMY_CONFIG_DIGEST_2, recipients3, ADMIN); + assertEq(link.balanceOf(recipients3[0]), DEFAULT_REPORT_LINK_FEE); + assertEq(link.balanceOf(address(rewardManager)), 0); + } + + function test_multipleFeeManagersAndVerifiersWithSameAddress() public { + /* + In this test we got: + - three verifiers (verifier, verifier2, verifier3). + - two fee managers (feeManager, feeManager2) + - one reward manager + + we glue: + - feeManager is used by verifier1 and verifier2 + - feeManager is used by verifier3 + - Rewardmanager is used by feeManager and feeManager2 + + In this test we do verificatons via verifier1, verifier2 and verifier3 and check that rewards are set accordingly + + */ + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + Common.AddressAndWeight[] memory weights = new Common.AddressAndWeight[](1); + weights[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_1, ONE_PERCENT * 100); + + Common.AddressAndWeight[] memory weights2 = new Common.AddressAndWeight[](1); + weights2[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_2, ONE_PERCENT * 100); + + Common.AddressAndWeight[] memory weights3 = new Common.AddressAndWeight[](1); + weights3[0] = Common.AddressAndWeight(DEFAULT_RECIPIENT_3, ONE_PERCENT * 100); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights); + + // should fail with InvalidPoolId + vm.expectRevert(abi.encodeWithSelector(RewardManager.InvalidPoolId.selector)); + + s_verifier2.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, weights2); + } +} diff --git a/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierVerifyBulkTest.t.sol b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierVerifyBulkTest.t.sol new file mode 100644 index 00000000000..0aa60a7986c --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierVerifyBulkTest.t.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {AccessControllerInterface} from "../../../../shared/interfaces/AccessControllerInterface.sol"; +import {Common} from "../../../libraries/Common.sol"; +import {Verifier} from "../../Verifier.sol"; +import {BaseTest} from "./BaseVerifierTest.t.sol"; + +contract VerifierVerifyBulkTest is BaseTest { + bytes32[3] internal s_reportContext; + V3Report internal s_testReportThree; + + function setUp() public virtual override { + BaseTest.setUp(); + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + + s_testReportThree = 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 test_revertsVerifyBulkIfNoAccess() public { + vm.mockCall( + ACCESS_CONTROLLER_ADDRESS, + abi.encodeWithSelector(AccessControllerInterface.hasAccess.selector, USER), + abi.encode(false) + ); + bytes memory signedReport = _generateV3EncodedBlob( + s_testReportThree, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + + bytes[] memory signedReports = new bytes[](2); + signedReports[0] = signedReport; + signedReports[1] = signedReport; + vm.expectRevert(abi.encodeWithSelector(Verifier.AccessForbidden.selector)); + changePrank(USER); + s_verifier.verifyBulk(signedReports, abi.encode(native), msg.sender); + } + + function test_verifyBulkSingleCaseWithSingleConfig() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + + uint8 MINIMAL_FAULT_TOLERANCE = 2; + BaseTest.Signer[] memory signersSubset1 = new BaseTest.Signer[](7); + signersSubset1[0] = signers[0]; + signersSubset1[1] = signers[1]; + signersSubset1[2] = signers[2]; + signersSubset1[3] = signers[3]; + signersSubset1[4] = signers[4]; + signersSubset1[5] = signers[5]; + signersSubset1[6] = signers[6]; + + address[] memory signersAddrSubset1 = _getSignerAddresses(signersSubset1); + // Config1 + s_verifier.setConfig( + DEFAULT_CONFIG_DIGEST, + signersAddrSubset1, + MINIMAL_FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + + V3Report memory report = 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 + }); + + BaseTest.Signer[] memory reportSigners = new BaseTest.Signer[](3); + reportSigners[0] = signers[0]; + reportSigners[1] = signers[1]; + reportSigners[2] = signers[2]; + + bytes[] memory signedReports = new bytes[](10); + + bytes memory signedReport = _generateV3EncodedBlob(report, s_reportContext, reportSigners); + + for (uint256 i = 0; i < signedReports.length; i++) { + signedReports[i] = signedReport; + } + + bytes[] memory verifierResponses = s_verifierProxy.verifyBulk(signedReports, abi.encode(native)); + + for (uint256 i = 0; i < verifierResponses.length; i++) { + bytes memory verifierResponse = verifierResponses[i]; + assertReportsEqual(verifierResponse, report); + } + } + + function test_verifyBulkWithSingleConfigOneVerifyFails() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + + uint8 MINIMAL_FAULT_TOLERANCE = 2; + BaseTest.Signer[] memory signersSubset1 = new BaseTest.Signer[](7); + signersSubset1[0] = signers[0]; + signersSubset1[1] = signers[1]; + signersSubset1[2] = signers[2]; + signersSubset1[3] = signers[3]; + signersSubset1[4] = signers[4]; + signersSubset1[5] = signers[5]; + signersSubset1[6] = signers[6]; + + address[] memory signersAddrSubset1 = _getSignerAddresses(signersSubset1); + // Config1 + s_verifier.setConfig( + bytes32(uint256(1)), + signersAddrSubset1, + MINIMAL_FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + + BaseTest.Signer[] memory reportSigners = new BaseTest.Signer[](3); + reportSigners[0] = signers[0]; + reportSigners[1] = signers[1]; + reportSigners[2] = signers[2]; + + bytes[] memory signedReports = new bytes[](11); + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, reportSigners); + + for (uint256 i = 0; i < 10; i++) { + signedReports[i] = signedReport; + } + + // Making the last report in this batch not verifiable + BaseTest.Signer[] memory reportSigners2 = new BaseTest.Signer[](3); + reportSigners2[0] = signers[30]; + reportSigners2[1] = signers[29]; + reportSigners2[2] = signers[28]; + signedReports[10] = _generateV3EncodedBlob(s_testReportThree, s_reportContext, reportSigners2); + + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_verifierProxy.verifyBulk(signedReports, abi.encode(native)); + } +} 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..ef8b3dcb80e --- /dev/null +++ b/contracts/src/v0.8/llo-feeds/v0.5.0/test/verifier/VerifierVerifyTest.t.sol @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {AccessControllerInterface} from "../../../../shared/interfaces/AccessControllerInterface.sol"; +import {Common} from "../../../libraries/Common.sol"; +import {Verifier} from "../../Verifier.sol"; +import {BaseTest} from "./BaseVerifierTest.t.sol"; + +contract VerifierVerifyTest is BaseTest { + bytes32[3] internal s_reportContext; + V3Report internal s_testReportThree; + + function setUp() public virtual override { + BaseTest.setUp(); + + s_testReportThree = 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 test_verifyReport() public { + // Simple use case just setting a config and verifying a report + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers); + + bytes memory verifierResponse = s_verifierProxy.verify(signedReport, abi.encode(native)); + assertReportsEqual(verifierResponse, s_testReportThree); + } + + + function test_verifyReportWithMultipleConfigs() public { + // Simple use case just setting a config and verifying a report + Signer[] memory signers = _getSigners(MAX_ORACLES); + Signer[] memory signers2 = _getSigners(MAX_ORACLES); + Signer[] memory signers3 = _getSigners(MAX_ORACLES); + + address[] memory signerAddrs = _getSignerAddresses(signers); + address[] memory signerAddrs2 = _getSignerAddresses(signers2); + address[] memory signerAddrs3 = _getSignerAddresses(signers3); + + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + + bytes32 DUMMY_CONFIG_1 = keccak256("DUMMY_CONFIG_1"); + bytes32 DUMMY_CONFIG_2 = keccak256("DUMMY_CONFIG_2"); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + s_verifier.setConfig(DUMMY_CONFIG_1, signerAddrs2, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + s_verifier.setConfig(DUMMY_CONFIG_2, signerAddrs3, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + + //verify + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers); + s_verifierProxy.verify(signedReport, abi.encode(native)); + + s_reportContext[0] = DUMMY_CONFIG_1; + bytes memory signedReport2 = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers2); + s_verifierProxy.verify(signedReport2, abi.encode(native)); + + s_reportContext[0] = DUMMY_CONFIG_2; + bytes memory signedReport3 = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers3); + s_verifierProxy.verify(signedReport3, abi.encode(native)); + + + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_reportContext[0] = keccak256("UNKNOWN_CONFIG"); + bytes memory signedReport4 = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers); + s_verifierProxy.verify(signedReport4, abi.encode(native)); + } + + function test_verifyTogglingActiveFlagsDonConfigs() public { + // sets config + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + // verifies report + bytes memory verifierResponse = s_verifierProxy.verify(signedReport, abi.encode(native)); + assertReportsEqual(verifierResponse, s_testReportThree); + + // test verifying via a config that is deactivated + s_verifier.setConfigActive(DEFAULT_CONFIG_DIGEST, false); + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + verifierResponse = s_verifierProxy.verify(signedReport, abi.encode(native)); + + // test verifying via a reactivated config + s_verifier.setConfigActive(DEFAULT_CONFIG_DIGEST, true); + verifierResponse = s_verifierProxy.verify(signedReport, abi.encode(native)); + assertReportsEqual(verifierResponse, s_testReportThree); + } + + function test_failToVerifyReportIfNotEnoughSigners() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + + uint8 MINIMAL_FAULT_TOLERANCE = 2; + BaseTest.Signer[] memory signersSubset1 = new BaseTest.Signer[](7); + signersSubset1[0] = signers[0]; + signersSubset1[1] = signers[1]; + signersSubset1[2] = signers[2]; + signersSubset1[3] = signers[3]; + signersSubset1[4] = signers[4]; + signersSubset1[5] = signers[5]; + signersSubset1[6] = signers[6]; + address[] memory signersAddrSubset1 = _getSignerAddresses(signersSubset1); + s_verifier.setConfig( + DEFAULT_CONFIG_DIGEST, + signersAddrSubset1, + MINIMAL_FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + + // only one signer, signers < MINIMAL_FAULT_TOLERANCE + BaseTest.Signer[] memory signersSubset2 = new BaseTest.Signer[](1); + signersSubset2[0] = signers[4]; + + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signersSubset2); + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_verifierProxy.verify(signedReport, abi.encode(native)); + } + + function test_failToVerifyReportIfNoSigners() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + + uint8 MINIMAL_FAULT_TOLERANCE = 2; + BaseTest.Signer[] memory signersSubset1 = new BaseTest.Signer[](7); + signersSubset1[0] = signers[0]; + signersSubset1[1] = signers[1]; + signersSubset1[2] = signers[2]; + signersSubset1[3] = signers[3]; + signersSubset1[4] = signers[4]; + signersSubset1[5] = signers[5]; + signersSubset1[6] = signers[6]; + address[] memory signersAddrSubset1 = _getSignerAddresses(signersSubset1); + s_verifier.setConfig( + DEFAULT_CONFIG_DIGEST, + signersAddrSubset1, + MINIMAL_FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + + // No signers for this report + BaseTest.Signer[] memory signersSubset2 = new BaseTest.Signer[](0); + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signersSubset2); + + vm.expectRevert(abi.encodeWithSelector(Verifier.NoSigners.selector)); + s_verifierProxy.verify(signedReport, abi.encode(native)); + } + + function test_failToVerifyReportIfDupSigners() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + + uint8 MINIMAL_FAULT_TOLERANCE = 2; + BaseTest.Signer[] memory signersSubset1 = new BaseTest.Signer[](7); + signersSubset1[0] = signers[0]; + signersSubset1[1] = signers[1]; + signersSubset1[2] = signers[2]; + signersSubset1[3] = signers[3]; + signersSubset1[4] = signers[4]; + signersSubset1[5] = signers[5]; + signersSubset1[6] = signers[6]; + address[] memory signersAddrSubset1 = _getSignerAddresses(signersSubset1); + s_verifier.setConfig( + DEFAULT_CONFIG_DIGEST, + signersAddrSubset1, + MINIMAL_FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + // One signer is repeated + BaseTest.Signer[] memory signersSubset2 = new BaseTest.Signer[](4); + signersSubset2[0] = signers[0]; + signersSubset2[1] = signers[1]; + // repeated signers + signersSubset2[2] = signers[2]; + signersSubset2[3] = signers[2]; + + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signersSubset2); + + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_verifierProxy.verify(signedReport, abi.encode(native)); + } + + function test_failToVerifyReportIfSignerNotInConfig() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + + uint8 MINIMAL_FAULT_TOLERANCE = 2; + BaseTest.Signer[] memory signersSubset1 = new BaseTest.Signer[](7); + signersSubset1[0] = signers[0]; + signersSubset1[1] = signers[1]; + signersSubset1[2] = signers[2]; + signersSubset1[3] = signers[3]; + signersSubset1[4] = signers[4]; + signersSubset1[5] = signers[5]; + signersSubset1[6] = signers[6]; + address[] memory signersAddrSubset1 = _getSignerAddresses(signersSubset1); + s_verifier.setConfig( + DEFAULT_CONFIG_DIGEST, + signersAddrSubset1, + MINIMAL_FAULT_TOLERANCE, + new Common.AddressAndWeight[](0) + ); + + // one report whose signer is not in the config + BaseTest.Signer[] memory reportSigners = new BaseTest.Signer[](4); + // these signers are part ofm the config + reportSigners[0] = signers[4]; + reportSigners[1] = signers[5]; + reportSigners[2] = signers[6]; + // this single signer is not in the config + reportSigners[3] = signers[7]; + + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, reportSigners); + + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_verifierProxy.verify(signedReport, abi.encode(native)); + } + + function test_revertsVerifyIfNoAccess() public { + vm.mockCall( + ACCESS_CONTROLLER_ADDRESS, + abi.encodeWithSelector(AccessControllerInterface.hasAccess.selector, USER), + abi.encode(false) + ); + bytes memory signedReport = _generateV3EncodedBlob( + s_testReportThree, + s_reportContext, + _getSigners(FAULT_TOLERANCE + 1) + ); + + vm.expectRevert(abi.encodeWithSelector(Verifier.AccessForbidden.selector)); + + changePrank(USER); + s_verifier.verify(signedReport, abi.encode(native), msg.sender); + } + + function test_verifyFailsWhenReportIsOlderThanConfig() public { + /* + - SetConfig A at time T0 + - SetConfig B at time T1 + - tries verifing report issued at blocktimestmap < T0 + */ + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + s_reportContext[0] = bytes32(abi.encode(uint32(5), uint8(1))); + + vm.warp(block.timestamp + 100); + + V3Report memory reportAtTMinus100 = V3Report({ + feedId: FEED_ID_V3, + observationsTimestamp: OBSERVATIONS_TIMESTAMP, + validFromTimestamp: uint32(block.timestamp - 100), + nativeFee: uint192(DEFAULT_REPORT_NATIVE_FEE), + linkFee: uint192(DEFAULT_REPORT_LINK_FEE), + expiresAt: uint32(block.timestamp), + benchmarkPrice: MEDIAN, + bid: BID, + ask: ASK + }); + + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + vm.warp(block.timestamp + 100); + + bytes32 DUMMY_CONFIG_1 = keccak256("DUMMY_CONFIG_1"); + s_verifier.setConfig(DUMMY_CONFIG_1, signerAddrs, FAULT_TOLERANCE - 1, new Common.AddressAndWeight[](0)); + + bytes memory signedReport = _generateV3EncodedBlob(reportAtTMinus100, s_reportContext, signers); + + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_verifierProxy.verify(signedReport, abi.encode(native)); + } + + function test_configUnsetAndSetStillVerifies() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, signerAddrs); + + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers); + + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_verifierProxy.verify(signedReport, abi.encode(native)); + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + s_verifierProxy.verify(signedReport, abi.encode(native)); + } + + function test_configUnsetAndResetWithDifferentKeysDoesNotVerifyWithPreviousKeys() public { + Signer[] memory signers = _getSigners(MAX_ORACLES); + address[] memory signerAddrs = _getSignerAddresses(signers); + s_reportContext[0] = DEFAULT_CONFIG_DIGEST; + + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + s_verifier.unsetConfig(DEFAULT_CONFIG_DIGEST, signerAddrs); + + bytes memory signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers); + + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_verifierProxy.verify(signedReport, abi.encode(native)); + + Signer[] memory signers2 = _getSecondarySigners(MAX_ORACLES); + address[] memory signerAddrs2 = _getSignerAddresses(signers2); + s_verifier.setConfig(DEFAULT_CONFIG_DIGEST, signerAddrs2, FAULT_TOLERANCE, new Common.AddressAndWeight[](0)); + + vm.expectRevert(abi.encodeWithSelector(Verifier.BadVerification.selector)); + s_verifierProxy.verify(signedReport, abi.encode(native)); + + signedReport = _generateV3EncodedBlob(s_testReportThree, s_reportContext, signers2); + s_verifierProxy.verify(signedReport, abi.encode(native)); + + } +} diff --git a/contracts/verify_v0_4_0_contracts.sh b/contracts/verify_v0_4_0_contracts.sh new file mode 100755 index 00000000000..b9fadf8b095 --- /dev/null +++ b/contracts/verify_v0_4_0_contracts.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +# Default values +ETHERSCAN_API_KEY="verifycontract" +VERIFIER_URL="https://explorer-testnet.soneium.org/api" +COMPILER_VERSION="0.8.19+commit.7dd6d404" +NUM_OF_OPTIMIZATIONS=1000000 +DEPLOYMENT_INFO_FILE="deployment-info.json" + +# Function to parse command line arguments +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --etherscan-api-key) + ETHERSCAN_API_KEY="$2" + shift 2 + ;; + --verifier-url) + VERIFIER_URL="$2" + shift 2 + ;; + --compiler-version) + COMPILER_VERSION="$2" + shift 2 + ;; + --num-of-optimizations) + NUM_OF_OPTIMIZATIONS="$2" + shift 2 + ;; + --deployment-info) + DEPLOYMENT_INFO_FILE="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac + done +} + +# Function to verify a contract +verify_contract() { + local contract_name="$1" + local address="$2" + local constructor_args="$3" + local file_path="$4" + + if [ -z "$address" ] || [ "$address" == "null" ]; then + echo "Error: Invalid address for $contract_name" + exit 1 + fi + + local cmd="forge verify-contract" + cmd+=" --etherscan-api-key $ETHERSCAN_API_KEY" + cmd+=" --verifier-url $VERIFIER_URL" + cmd+=" $address" + cmd+=" $file_path:$contract_name" + cmd+=" --compiler-version $COMPILER_VERSION" + + if [ -n "$constructor_args" ]; then + cmd+=" --constructor-args $constructor_args" + fi + + cmd+=" --num-of-optimizations $NUM_OF_OPTIMIZATIONS" + + echo "Verifying $contract_name..." + + # Execute the command + if ! eval "$cmd"; then + echo "Verification failed for $contract_name" + exit 1 + fi + + echo "Verification successful for $contract_name" +} + +# Function to safely get JSON value +get_json_value() { + local value=$(echo "$DEPLOYMENT_INFO" | jq -r "$1") + if [ "$value" == "null" ] || [ -z "$value" ]; then + echo "" + else + echo "$value" + fi +} + +# Main execution +parse_args "$@" + +# Check if the deployment info file exists +if [ ! -f "$DEPLOYMENT_INFO_FILE" ]; then + echo "Error: Deployment info file '$DEPLOYMENT_INFO_FILE' not found." + exit 1 +fi + +# Read deployment info +DEPLOYMENT_INFO=$(cat "$DEPLOYMENT_INFO_FILE") + +# Verify DestinationFeeManager if present +if [ "$(get_json_value '.contracts.DestinationFeeManager')" != "" ]; then + address=$(get_json_value '.contracts.DestinationFeeManager.address') + linkToken=$(get_json_value '.contracts.DestinationFeeManager.params.linkToken') + nativeToken=$(get_json_value '.contracts.DestinationFeeManager.params.nativeToken') + verifier=$(get_json_value '.contracts.DestinationFeeManager.params.verifier') + rewardManager=$(get_json_value '.contracts.DestinationFeeManager.params.rewardManager') + + if [ -n "$address" ] && [ -n "$linkToken" ] && [ -n "$nativeToken" ] && [ -n "$verifier" ] && [ -n "$rewardManager" ]; then + constructor_args=$(cast abi-encode "constructor(address,address,address,address)" "$linkToken" "$nativeToken" "$verifier" "$rewardManager") + verify_contract "DestinationFeeManager" "$address" "$constructor_args" "src/v0.8/llo-feeds/v0.4.0/DestinationFeeManager.sol" + else + echo "Error: Missing parameters for DestinationFeeManager" + exit 1 + fi +else + echo "DestinationFeeManager not found in deployment info. Skipping." +fi + +# Verify DestinationRewardManager if present +if [ "$(get_json_value '.contracts.DestinationRewardManager')" != "" ]; then + address=$(get_json_value '.contracts.DestinationRewardManager.address') + linkToken=$(get_json_value '.contracts.DestinationRewardManager.params.linkToken') + + if [ -n "$address" ] && [ -n "$linkToken" ]; then + constructor_args=$(cast abi-encode "constructor(address)" "$linkToken") + verify_contract "DestinationRewardManager" "$address" "$constructor_args" "src/v0.8/llo-feeds/v0.4.0/DestinationRewardManager.sol" + else + echo "Error: Missing parameters for DestinationRewardManager" + exit 1 + fi +else + echo "DestinationRewardManager not found in deployment info. Skipping." +fi + +# Verify DestinationVerifier if present +if [ "$(get_json_value '.contracts.DestinationVerifier')" != "" ]; then + address=$(get_json_value '.contracts.DestinationVerifier.address') + destinationVerifierProxy=$(get_json_value '.contracts.DestinationVerifier.params.destinationVerifierProxy') + + if [ -n "$address" ] && [ -n "$destinationVerifierProxy" ]; then + constructor_args=$(cast abi-encode "constructor(address)" "$destinationVerifierProxy") + verify_contract "DestinationVerifier" "$address" "$constructor_args" "src/v0.8/llo-feeds/v0.4.0/DestinationVerifier.sol" + else + echo "Error: Missing parameters for DestinationVerifier" + exit 1 + fi +else + echo "DestinationVerifier not found in deployment info. Skipping." +fi + +# Verify DestinationVerifierProxy if present +if [ "$(get_json_value '.contracts.DestinationVerifierProxy')" != "" ]; then + address=$(get_json_value '.contracts.DestinationVerifierProxy.address') + + if [ -n "$address" ]; then + verify_contract "DestinationVerifierProxy" "$address" "" "src/v0.8/llo-feeds/v0.4.0/DestinationVerifierProxy.sol" + else + echo "Error: Missing address for DestinationVerifierProxy" + exit 1 + fi +else + echo "DestinationVerifierProxy not found in deployment info. Skipping." +fi + +echo "All present contracts verified successfully."