Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions contracts/gas-snapshots/llo-feeds.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ FeeManagerProcessFeeTestV05:test_surchargeIsNotAppliedForLinkFee() (gas: 52244)
FeeManagerProcessFeeTestV05:test_surchargeIsNotAppliedWith100PercentDiscount() (gas: 78309)
FeeManagerProcessFeeTestV05:test_testRevertIfReportHasExpired() (gas: 14941)
MultiVerifierBillingTests:test_multipleFeeManagersAndVerifiers() (gas: 4598140)
NoOpFeeManagerIntegrationTestV051:test_verificationWorkflowCompatibility() (gas: 377733)
NoOpFeeManagerTest:test_adminFunctionsDoNotRevert() (gas: 12766)
NoOpFeeManagerTest:test_discountGettersMatchFeeManagerSignature() (gas: 24169)
NoOpFeeManagerTest:test_discountGettersReturn100Percent() (gas: 12229)
Expand All @@ -594,15 +595,21 @@ NoOpFeeManagerTest:test_processFeeBulkRefundsETH() (gas: 51685)
NoOpFeeManagerTest:test_processFeeRefundsETH() (gas: 50731)
NoOpFeeManagerTest:test_supportsInterface() (gas: 11003)
NoOpFeeManagerTest:test_typeAndVersion() (gas: 9688)
NoOpFeeManagerTestV051:test_adminFunctionsDoNotRevert() (gas: 12766)
NoOpFeeManagerTestV051:test_discountGettersMatchFeeManagerSignature() (gas: 24169)
NoOpFeeManagerTestV051:test_discountGettersReturn100Percent() (gas: 12229)
NoOpFeeManagerTestV051:test_getFeeAndReward() (gas: 13233)
NoOpFeeManagerTestV051:test_linkAvailableForPayment() (gas: 8366)
NoOpFeeManagerTestV051:test_processFeeBulkRefundsETH() (gas: 51685)
NoOpFeeManagerTestV051:test_processFeeRefundsETH() (gas: 50731)
NoOpFeeManagerTestV051:test_supportsInterface() (gas: 11003)
NoOpFeeManagerTestV051:test_typeAndVersion() (gas: 9688)
NoOpFeeManagerTestV051:test_adminFunctionsDoNotRevert() (gas: 12789)
NoOpFeeManagerTestV051:test_constructorParameters() (gas: 17359)
NoOpFeeManagerTestV051:test_discountGettersMatchFeeManagerSignature() (gas: 24213)
NoOpFeeManagerTestV051:test_discountGettersReturn100Percent() (gas: 12206)
NoOpFeeManagerTestV051:test_getFeeAndReward() (gas: 13293)
NoOpFeeManagerTestV051:test_linkAvailableForPayment() (gas: 8410)
NoOpFeeManagerTestV051:test_nativeSurchargeReturnsZero() (gas: 8370)
NoOpFeeManagerTestV051:test_processFeeBulkRefundsETH() (gas: 51751)
NoOpFeeManagerTestV051:test_processFeeRefundsETH() (gas: 50776)
NoOpFeeManagerTestV051:test_supportsInterface_BackwardCompatibility() (gas: 9912)
NoOpFeeManagerTestV051:test_supportsInterface_ERC165() (gas: 8551)
NoOpFeeManagerTestV051:test_supportsInterface_IFeeManager() (gas: 8513)
NoOpFeeManagerTestV051:test_supportsInterface_IVerifierFeeManager() (gas: 8529)
NoOpFeeManagerTestV051:test_supportsInterface_UnsupportedInterface() (gas: 8607)
NoOpFeeManagerTestV051:test_typeAndVersion() (gas: 9710)
RewardManagerClaimTest:test_claimAllRecipients() (gas: 277087)
RewardManagerClaimTest:test_claimMultipleRecipients() (gas: 154297)
RewardManagerClaimTest:test_claimRewardsWithDuplicatePoolIdsDoesNotPayoutTwice() (gas: 330011)
Expand Down
41 changes: 39 additions & 2 deletions contracts/src/v0.8/llo-feeds/v0.5.1/NoOpFeeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.19;
import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol";
import {Common} from "../libraries/Common.sol";
import {IFeeManager} from "./interfaces/IFeeManager.sol";
import {IRewardManager} from "./interfaces/IRewardManager.sol";
import {IVerifierFeeManager} from "./interfaces/IVerifierFeeManager.sol";
import {IERC165} from "@openzeppelin/contracts@4.8.3/interfaces/IERC165.sol";

Expand All @@ -12,6 +13,8 @@ import {IERC165} from "@openzeppelin/contracts@4.8.3/interfaces/IERC165.sol";
* @notice A no-op implementation of IFeeManager that does not collect fees.
* @dev All functions return successfully without performing any fee collection or state changes.
* Any ETH sent to payable functions is refunded to the subscriber.
* Constructor parameters are stored for interface compatibility with integrators
* who call i_linkAddress, i_nativeAddress, or i_rewardManager before getFeeAndReward().
*/
contract NoOpFeeManager is IFeeManager, ITypeAndVersion {
/// @notice Error thrown when ETH refund fails
Expand All @@ -20,16 +23,41 @@ contract NoOpFeeManager is IFeeManager, ITypeAndVersion {
/// @notice The scalar representing 100% discount (1e18 = 100%)
uint64 private constant PERCENTAGE_SCALAR = 1e18;

/// @notice The LINK token address (required for interface compatibility)
address public immutable i_linkAddress;

/// @notice The native token address (required for interface compatibility)
address public immutable i_nativeAddress;

/// @notice The reward manager address (required for interface compatibility)
IRewardManager public immutable i_rewardManager;

/**
* @notice Construct the NoOpFeeManager contract
* @param _linkAddress The address of the LINK token (for interface compatibility)
* @param _nativeAddress The address of the wrapped native token (for interface compatibility)
* @param _rewardManagerAddress The address of the reward manager (for interface compatibility)
* @dev These addresses are not used internally but are required for compatibility
* with integrators who call these getters before getFeeAndReward().
*/
constructor(address _linkAddress, address _nativeAddress, address _rewardManagerAddress) {
i_linkAddress = _linkAddress;
i_nativeAddress = _nativeAddress;
i_rewardManager = IRewardManager(_rewardManagerAddress);
}

/// @inheritdoc ITypeAndVersion
function typeAndVersion() external pure override returns (string memory) {
return "NoOpFeeManager 0.5.0";
return "NoOpFeeManager 0.5.1";
}

/// @inheritdoc IERC165
function supportsInterface(
bytes4 interfaceId
) external pure override returns (bool) {
return interfaceId == this.processFee.selector || interfaceId == this.processFeeBulk.selector;
return interfaceId == type(IERC165).interfaceId || interfaceId == type(IFeeManager).interfaceId
|| interfaceId == type(IVerifierFeeManager).interfaceId || interfaceId == IVerifierFeeManager.processFee.selector
|| interfaceId == IVerifierFeeManager.processFeeBulk.selector;
}

/// @inheritdoc IVerifierFeeManager
Expand Down Expand Up @@ -111,6 +139,15 @@ contract NoOpFeeManager is IFeeManager, ITypeAndVersion {
return PERCENTAGE_SCALAR;
}

/**
* @notice Returns 0 surcharge since no fees are charged
* @dev Replicates public state variable getter from FeeManager for backwards compatibility
*/
// solhint-disable-next-line func-name-mixedcase
function s_nativeSurcharge() external pure returns (uint256) {
return 0;
}

/**
* @notice Refunds any ETH sent to the contract
* @param recipient The address to refund ETH to
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;

import {Common} from "../../../libraries/Common.sol";
import {NoOpFeeManager} from "../../NoOpFeeManager.sol";
import {BaseTestWithConfiguredVerifierAndFeeManager} from "../verifier/BaseVerifierTest.t.sol";

/// @notice Extended interface matching what real clients call on FeeManager/NoOpFeeManager
interface IFeeManagerWithGetters {
function getFeeAndReward(
address subscriber,
bytes memory report,
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);
}

/**
* @title NoOpFeeManagerIntegrationTest
* @notice Tests that NoOpFeeManager is compatible with the Data Streams verification workflow when billing is enabled.
* @dev Uses a single _runVerificationWorkflow() function to ensure identical code paths are tested
* with both FeeManager and NoOpFeeManager.
*/
contract NoOpFeeManagerIntegrationTestV051 is BaseTestWithConfiguredVerifierAndFeeManager {
NoOpFeeManager internal noOpFeeManager;

uint64 internal constant PERCENTAGE_SCALAR = 1e18;
uint256 internal constant DEFAULT_LINK_MINT = 100 ether;

/// @notice Result of running the verification workflow
struct VerificationResult {
bytes verifiedReport;
uint256 feeAmount;
uint256 rewardAmount;
uint256 discount;
uint256 linkBalanceBefore;
uint256 linkBalanceAfter;
int192 decodedPrice;
bytes32 decodedFeedId;
}

function setUp() public override {
BaseTestWithConfiguredVerifierAndFeeManager.setUp();

// Deploy NoOpFeeManager with same addresses as FeeManager
noOpFeeManager = new NoOpFeeManager(address(link), address(native), address(rewardManager));

// Mint tokens for users
link.mint(USER, DEFAULT_LINK_MINT);
vm.deal(USER, DEFAULT_LINK_MINT);
}

// ═══════════════════════════════════════════════════════════════════════════
// Main Test: Verification Workflow Compatibility
// ═══════════════════════════════════════════════════════════════════════════

function test_verificationWorkflowCompatibility() public {
// Generate report once - used for both workflows
V3Report memory report = _generateV3Report();
Signer[] memory signers = _getSigners(FAULT_TOLERANCE + 1);
bytes32[3] memory reportContext = _generateReportContext(v3ConfigDigest);
bytes memory signedReport = _generateV3EncodedBlob(report, reportContext, signers);
bytes memory reportData = _encodeReport(report);

// ─── Run 1: FeeManager (billing enabled) ─────────────────────────────────
VerificationResult memory result1 = _runVerificationWorkflow(signedReport, reportData, USER);

// Validate FeeManager was invoked: fee charged, LINK transferred
assertEq(result1.feeAmount, DEFAULT_REPORT_LINK_FEE, "Fee should match report linkFee");
assertEq(result1.rewardAmount, DEFAULT_REPORT_LINK_FEE, "Reward should match report linkFee");
assertEq(result1.discount, 0, "No discount configured");
assertLt(result1.linkBalanceAfter, result1.linkBalanceBefore, "LINK should be transferred");
assertEq(
result1.linkBalanceBefore - result1.linkBalanceAfter,
DEFAULT_REPORT_LINK_FEE,
"Transferred amount should match fee"
);

// Validate report decoded correctly
assertEq(result1.decodedPrice, MEDIAN, "Decoded price should match");
assertEq(result1.decodedFeedId, FEED_ID_V3, "Decoded feedId should match");

// ─── Swap to NoOpFeeManager ──────────────────────────────────────────────
changePrank(ADMIN);
s_verifierProxy.setFeeManager(noOpFeeManager);

// ─── Run 2: NoOpFeeManager (billing deactivated) ─────────────────────────
VerificationResult memory result2 = _runVerificationWorkflow(signedReport, reportData, USER);

// Validate NoOpFeeManager: no fees, no transfer
assertEq(result2.feeAmount, 0, "NoOp fee should be 0");
assertEq(result2.rewardAmount, 0, "NoOp reward should be 0");
assertEq(result2.discount, PERCENTAGE_SCALAR, "NoOp discount should be 100%");
assertEq(result2.linkBalanceAfter, result2.linkBalanceBefore, "No LINK should be transferred");

// Validate same report decoded - proves verification still works
assertEq(result2.decodedPrice, result1.decodedPrice, "Same price decoded after swap");
assertEq(result2.decodedFeedId, result1.decodedFeedId, "Same feedId decoded after swap");
}

// ═══════════════════════════════════════════════════════════════════════════
// Verification Workflow
// ═══════════════════════════════════════════════════════════════════════════

/**
* @notice Runs the Data Streams verification workflow when billing is enabled
* @dev This function mirrors the integration pattern from the docs:
* 1. Query s_feeManager() from proxy to get active fee manager
* 2. Call i_linkAddress() to get fee token
* 3. Call i_rewardManager() to get active reward manager
* 4. Call getFeeAndReward() to calculate fees
* 5. Approve RewardManager to collect the fee amount
* 6. Call verify() on VerifierProxy
* @param signedReport The signed report blob to verify
* @param reportData The encoded report data for fee calculation
* @param sender The address to run the workflow as
* @return result The verification result containing all workflow outputs
*/
function _runVerificationWorkflow(
bytes memory signedReport,
bytes memory reportData,
address sender
) internal returns (VerificationResult memory result) {
changePrank(sender);

// 1. Get active fee manager from proxy (exactly like real integration)
IFeeManagerWithGetters activeFeeManager = IFeeManagerWithGetters(address(s_verifierProxy.s_feeManager()));

// 2. Query fee token address
address feeToken = activeFeeManager.i_linkAddress();

// 3. Query reward manager for approval
address rewardMgr = address(activeFeeManager.i_rewardManager());

// 4. Calculate fee
(Common.Asset memory fee, Common.Asset memory reward, uint256 discount) =
activeFeeManager.getFeeAndReward(sender, reportData, feeToken);

result.feeAmount = fee.amount;
result.rewardAmount = reward.amount;
result.discount = discount;

// 5. Record balance before and approve
result.linkBalanceBefore = link.balanceOf(sender);
if (fee.amount > 0) {
link.approve(rewardMgr, fee.amount);
}

// 6. Verify through proxy
result.verifiedReport = s_verifierProxy.verify(signedReport, abi.encode(feeToken));

// Record balance after
result.linkBalanceAfter = link.balanceOf(sender);

// Decode verified report
V3Report memory decoded = abi.decode(result.verifiedReport, (V3Report));
result.decodedPrice = decoded.benchmarkPrice;
result.decodedFeedId = decoded.feedId;

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {Common} from "../../../libraries/Common.sol";
import {FeeManager} from "../../FeeManager.sol";
import {NoOpFeeManager} from "../../NoOpFeeManager.sol";
import {RewardManager} from "../../RewardManager.sol";
import {IFeeManager} from "../../interfaces/IFeeManager.sol";
import {IVerifierFeeManager} from "../../interfaces/IVerifierFeeManager.sol";
import {FeeManagerProxy} from "../mocks/FeeManagerProxy.sol";
import {IERC165} from "@openzeppelin/contracts@4.8.3/interfaces/IERC165.sol";
import {Test} from "forge-std/Test.sol";

contract NoOpFeeManagerTestV051 is Test {
Expand All @@ -30,7 +32,6 @@ contract NoOpFeeManagerTestV051 is Test {

link = new ERC20Mock(18);
native = new WERC20Mock();
noOpFeeManager = new NoOpFeeManager();

// Deploy real FeeManager for comparison tests
feeManagerProxy = new FeeManagerProxy();
Expand All @@ -39,20 +40,48 @@ contract NoOpFeeManagerTestV051 is Test {
feeManagerProxy.setFeeManager(feeManager);
rewardManager.setFeeManager(address(feeManager));

noOpFeeManager = new NoOpFeeManager(address(link), address(native), address(rewardManager));

vm.stopPrank();
}

function test_typeAndVersion() public view {
assertEq(noOpFeeManager.typeAndVersion(), "NoOpFeeManager 0.5.0");
assertEq(noOpFeeManager.typeAndVersion(), "NoOpFeeManager 0.5.1");
}

// ERC-165 compliance: must return true for IERC165 interface ID
function test_supportsInterface_ERC165() public view {
assertTrue(noOpFeeManager.supportsInterface(type(IERC165).interfaceId));
}

// ERC-165 compliance: must return true for IFeeManager interface ID
function test_supportsInterface_IFeeManager() public view {
assertTrue(noOpFeeManager.supportsInterface(type(IFeeManager).interfaceId));
}

// ERC-165 compliance: must return true for IVerifierFeeManager interface ID
function test_supportsInterface_IVerifierFeeManager() public view {
assertTrue(noOpFeeManager.supportsInterface(type(IVerifierFeeManager).interfaceId));
}

// VerifierProxy checks these interface support before accepting a fee manager
function test_supportsInterface() public view {
// Backward compatibility: VerifierProxy checks these function selectors before accepting a fee manager
function test_supportsInterface_BackwardCompatibility() public view {
assertTrue(noOpFeeManager.supportsInterface(IVerifierFeeManager.processFee.selector));
assertTrue(noOpFeeManager.supportsInterface(IVerifierFeeManager.processFeeBulk.selector));
}

// Must return false for unsupported interfaces
function test_supportsInterface_UnsupportedInterface() public view {
assertFalse(noOpFeeManager.supportsInterface(bytes4(0xdeadbeef)));
}

// Verify constructor parameters are stored correctly for interface compatibility
function test_constructorParameters() public view {
assertEq(noOpFeeManager.i_linkAddress(), address(link));
assertEq(noOpFeeManager.i_nativeAddress(), address(native));
assertEq(address(noOpFeeManager.i_rewardManager()), address(rewardManager));
}

// Some code queries these to check discount status - must always return 100%
function test_discountGettersReturn100Percent() public view {
bytes32 feedId = keccak256("ETH-USD");
Expand All @@ -61,6 +90,11 @@ contract NoOpFeeManagerTestV051 is Test {
assertEq(noOpFeeManager.s_subscriberDiscounts(SUBSCRIBER, feedId, address(link)), PERCENTAGE_SCALAR);
}

// Native surcharge getter should return 0 since no fees are charged
function test_nativeSurchargeReturnsZero() public view {
assertEq(noOpFeeManager.s_nativeSurcharge(), 0);
}

// Ensures our view functions match FeeManager's public mapping getter signatures
// so code can swap between implementations without breaking changes
function test_discountGettersMatchFeeManagerSignature() public view {
Expand Down
Loading
Loading