Эх сурвалжийг харах

feat(pulse): add provider (#2279)

* add provider

* remove unnecessary code

* fix test

* add exclusivity period to provider (#2282)

* make exclusivityPeriodSeconds configurable

* remove provider arg from getFee

* remove provider from callback args

* add comments

* add comments
Daniel Chew 10 сар өмнө
parent
commit
93fbbaa6eb

+ 17 - 3
target_chains/ethereum/contracts/contracts/pulse/IPulse.sol

@@ -9,7 +9,6 @@ import "./PulseState.sol";
 interface IPulseConsumer {
     function pulseCallback(
         uint64 sequenceNumber,
-        address updater,
         PythStructs.PriceFeed[] memory priceFeeds
     ) external;
 }
@@ -74,8 +73,23 @@ interface IPulse is PulseEvents {
         uint64 sequenceNumber
     ) external view returns (PulseState.Request memory req);
 
-    // Add these functions to the IPulse interface
     function setFeeManager(address manager) external;
 
-    function withdrawAsFeeManager(uint128 amount) external;
+    function withdrawAsFeeManager(address provider, uint128 amount) external;
+
+    function registerProvider(uint128 feeInWei) external;
+
+    function setProviderFee(uint128 newFeeInWei) external;
+
+    function getProviderInfo(
+        address provider
+    ) external view returns (PulseState.ProviderInfo memory);
+
+    function getDefaultProvider() external view returns (address);
+
+    function setDefaultProvider(address provider) external;
+
+    function setExclusivityPeriod(uint256 periodSeconds) external;
+
+    function getExclusivityPeriod() external view returns (uint256);
 }

+ 118 - 23
target_chains/ethereum/contracts/contracts/pulse/Pulse.sol

@@ -13,10 +13,16 @@ abstract contract Pulse is IPulse, PulseState {
         address admin,
         uint128 pythFeeInWei,
         address pythAddress,
-        bool prefillRequestStorage
+        address defaultProvider,
+        bool prefillRequestStorage,
+        uint256 exclusivityPeriodSeconds
     ) internal {
         require(admin != address(0), "admin is zero address");
         require(pythAddress != address(0), "pyth is zero address");
+        require(
+            defaultProvider != address(0),
+            "defaultProvider is zero address"
+        );
 
         _state.admin = admin;
         _state.accruedFeesInWei = 0;
@@ -24,6 +30,13 @@ abstract contract Pulse is IPulse, PulseState {
         _state.pyth = pythAddress;
         _state.currentSequenceNumber = 1;
 
+        // Two-step initialization process:
+        // 1. Set the default provider address here
+        // 2. Provider must call registerProvider() in a separate transaction to set their fee
+        // This ensures the provider maintains control over their own fee settings
+        _state.defaultProvider = defaultProvider;
+        _state.exclusivityPeriodSeconds = exclusivityPeriodSeconds;
+
         if (prefillRequestStorage) {
             for (uint8 i = 0; i < NUM_REQUESTS; i++) {
                 Request storage req = _state.requests[i];
@@ -45,6 +58,12 @@ abstract contract Pulse is IPulse, PulseState {
         bytes32[] calldata priceIds,
         uint256 callbackGasLimit
     ) external payable override returns (uint64 requestSequenceNumber) {
+        address provider = _state.defaultProvider;
+        require(
+            _state.providers[provider].isRegistered,
+            "Provider not registered"
+        );
+
         // NOTE: The 60-second future limit on publishTime prevents a DoS vector where
         //      attackers could submit many low-fee requests for far-future updates when gas prices
         //      are low, forcing executors to fulfill them later when gas prices might be much higher.
@@ -65,13 +84,17 @@ abstract contract Pulse is IPulse, PulseState {
         req.callbackGasLimit = callbackGasLimit;
         req.requester = msg.sender;
         req.numPriceIds = uint8(priceIds.length);
+        req.provider = provider;
 
         // Copy price IDs to storage
         for (uint8 i = 0; i < priceIds.length; i++) {
             req.priceIds[i] = priceIds[i];
         }
 
-        _state.accruedFeesInWei += SafeCast.toUint128(msg.value);
+        _state.providers[provider].accruedFeesInWei += SafeCast.toUint128(
+            msg.value - _state.pythFeeInWei
+        );
+        _state.accruedFeesInWei += _state.pythFeeInWei;
 
         emit PriceUpdateRequested(req, priceIds);
     }
@@ -83,6 +106,16 @@ abstract contract Pulse is IPulse, PulseState {
     ) external payable override {
         Request storage req = findActiveRequest(sequenceNumber);
 
+        // Check provider exclusivity using configurable period
+        if (
+            block.timestamp < req.publishTime + _state.exclusivityPeriodSeconds
+        ) {
+            require(
+                msg.sender == req.provider,
+                "Only assigned provider during exclusivity period"
+            );
+        }
+
         // Verify priceIds match
         require(
             priceIds.length == req.numPriceIds,
@@ -105,19 +138,10 @@ abstract contract Pulse is IPulse, PulseState {
 
         clearRequest(sequenceNumber);
 
-        // Check if enough gas remains for callback + events/cleanup
-        // We need extra gas beyond callbackGasLimit for:
-        // 1. Emitting success/failure events
-        // 2. Error handling in catch blocks
-        // 3. State cleanup operations
-        if (gasleft() < (req.callbackGasLimit * 3) / 2) {
-            revert InsufficientGas();
-        }
-
         try
             IPulseConsumer(req.requester).pulseCallback{
                 gas: req.callbackGasLimit
-            }(sequenceNumber, msg.sender, priceFeeds)
+            }(sequenceNumber, priceFeeds)
         {
             // Callback succeeded
             emitPriceUpdate(sequenceNumber, priceIds, priceFeeds);
@@ -173,9 +197,12 @@ abstract contract Pulse is IPulse, PulseState {
     function getFee(
         uint256 callbackGasLimit
     ) public view override returns (uint128 feeAmount) {
-        uint128 baseFee = _state.pythFeeInWei;
-        uint256 gasFee = callbackGasLimit * tx.gasprice;
-        feeAmount = baseFee + SafeCast.toUint128(gasFee);
+        uint128 baseFee = _state.pythFeeInWei; // Fixed fee to Pyth
+        uint128 providerFeeInWei = _state
+            .providers[_state.defaultProvider]
+            .feeInWei; // Provider's per-gas rate
+        uint256 gasFee = callbackGasLimit * providerFeeInWei; // Total provider fee based on gas
+        feeAmount = baseFee + SafeCast.toUint128(gasFee); // Total fee user needs to pay
     }
 
     function getPythFeeInWei()
@@ -271,21 +298,89 @@ abstract contract Pulse is IPulse, PulseState {
     }
 
     function setFeeManager(address manager) external override {
-        require(msg.sender == _state.admin, "Only admin can set fee manager");
-        address oldFeeManager = _state.feeManager;
-        _state.feeManager = manager;
-        emit FeeManagerUpdated(_state.admin, oldFeeManager, manager);
+        require(
+            _state.providers[msg.sender].isRegistered,
+            "Provider not registered"
+        );
+        address oldFeeManager = _state.providers[msg.sender].feeManager;
+        _state.providers[msg.sender].feeManager = manager;
+        emit FeeManagerUpdated(msg.sender, oldFeeManager, manager);
     }
 
-    function withdrawAsFeeManager(uint128 amount) external override {
-        require(msg.sender == _state.feeManager, "Only fee manager");
-        require(_state.accruedFeesInWei >= amount, "Insufficient balance");
+    function withdrawAsFeeManager(
+        address provider,
+        uint128 amount
+    ) external override {
+        require(
+            msg.sender == _state.providers[provider].feeManager,
+            "Only fee manager"
+        );
+        require(
+            _state.providers[provider].accruedFeesInWei >= amount,
+            "Insufficient balance"
+        );
 
-        _state.accruedFeesInWei -= amount;
+        _state.providers[provider].accruedFeesInWei -= amount;
 
         (bool sent, ) = msg.sender.call{value: amount}("");
         require(sent, "Failed to send fees");
 
         emit FeesWithdrawn(msg.sender, amount);
     }
+
+    function registerProvider(uint128 feeInWei) external override {
+        ProviderInfo storage provider = _state.providers[msg.sender];
+        require(!provider.isRegistered, "Provider already registered");
+        provider.feeInWei = feeInWei;
+        provider.isRegistered = true;
+        emit ProviderRegistered(msg.sender, feeInWei);
+    }
+
+    function setProviderFee(uint128 newFeeInWei) external override {
+        require(
+            _state.providers[msg.sender].isRegistered,
+            "Provider not registered"
+        );
+        uint128 oldFee = _state.providers[msg.sender].feeInWei;
+        _state.providers[msg.sender].feeInWei = newFeeInWei;
+        emit ProviderFeeUpdated(msg.sender, oldFee, newFeeInWei);
+    }
+
+    function getProviderInfo(
+        address provider
+    ) external view override returns (ProviderInfo memory) {
+        return _state.providers[provider];
+    }
+
+    function getDefaultProvider() external view override returns (address) {
+        return _state.defaultProvider;
+    }
+
+    function setDefaultProvider(address provider) external override {
+        require(
+            msg.sender == _state.admin,
+            "Only admin can set default provider"
+        );
+        require(
+            _state.providers[provider].isRegistered,
+            "Provider not registered"
+        );
+        address oldProvider = _state.defaultProvider;
+        _state.defaultProvider = provider;
+        emit DefaultProviderUpdated(oldProvider, provider);
+    }
+
+    function setExclusivityPeriod(uint256 periodSeconds) external override {
+        require(
+            msg.sender == _state.admin,
+            "Only admin can set exclusivity period"
+        );
+        uint256 oldPeriod = _state.exclusivityPeriodSeconds;
+        _state.exclusivityPeriodSeconds = periodSeconds;
+        emit ExclusivityPeriodUpdated(oldPeriod, periodSeconds);
+    }
+
+    function getExclusivityPeriod() external view override returns (uint256) {
+        return _state.exclusivityPeriodSeconds;
+    }
 }

+ 0 - 1
target_chains/ethereum/contracts/contracts/pulse/PulseErrors.sol

@@ -11,5 +11,4 @@ error CallbackFailed();
 error InvalidPriceIds(bytes32 providedPriceIdsHash, bytes32 storedPriceIdsHash);
 error InvalidCallbackGasLimit(uint256 requested, uint256 stored);
 error ExceedsMaxPrices(uint32 requested, uint32 maxAllowed);
-error InsufficientGas();
 error TooManyPriceIds(uint256 provided, uint256 maximum);

+ 15 - 2
target_chains/ethereum/contracts/contracts/pulse/PulseEvents.sol

@@ -8,7 +8,7 @@ interface PulseEvents {
 
     event PriceUpdateExecuted(
         uint64 indexed sequenceNumber,
-        address indexed updater,
+        address indexed provider,
         bytes32[] priceIds,
         int64[] prices,
         uint64[] conf,
@@ -20,7 +20,7 @@ interface PulseEvents {
 
     event PriceUpdateCallbackFailed(
         uint64 indexed sequenceNumber,
-        address indexed updater,
+        address indexed provider,
         bytes32[] priceIds,
         address requester,
         string reason
@@ -31,4 +31,17 @@ interface PulseEvents {
         address oldFeeManager,
         address newFeeManager
     );
+
+    event ProviderRegistered(address indexed provider, uint128 feeInWei);
+    event ProviderFeeUpdated(
+        address indexed provider,
+        uint128 oldFee,
+        uint128 newFee
+    );
+    event DefaultProviderUpdated(address oldProvider, address newProvider);
+
+    event ExclusivityPeriodUpdated(
+        uint256 oldPeriodSeconds,
+        uint256 newPeriodSeconds
+    );
 }

+ 11 - 1
target_chains/ethereum/contracts/contracts/pulse/PulseState.sol

@@ -16,6 +16,14 @@ contract PulseState {
         uint8 numPriceIds; // Actual number of price IDs used
         uint256 callbackGasLimit;
         address requester;
+        address provider;
+    }
+
+    struct ProviderInfo {
+        uint128 feeInWei;
+        uint128 accruedFeesInWei;
+        address feeManager;
+        bool isRegistered;
     }
 
     struct State {
@@ -24,9 +32,11 @@ contract PulseState {
         uint128 accruedFeesInWei;
         address pyth;
         uint64 currentSequenceNumber;
-        address feeManager;
+        address defaultProvider;
+        uint256 exclusivityPeriodSeconds;
         Request[NUM_REQUESTS] requests;
         mapping(bytes32 => Request) requestsOverflow;
+        mapping(address => ProviderInfo) providers;
     }
 
     State internal _state;

+ 7 - 3
target_chains/ethereum/contracts/contracts/pulse/PulseUpgradeable.sol

@@ -23,8 +23,10 @@ contract PulseUpgradeable is
         address admin,
         uint128 pythFeeInWei,
         address pythAddress,
-        bool prefillRequestStorage
-    ) public initializer {
+        address defaultProvider,
+        bool prefillRequestStorage,
+        uint256 exclusivityPeriodSeconds
+    ) external initializer {
         require(owner != address(0), "owner is zero address");
         require(admin != address(0), "admin is zero address");
 
@@ -35,7 +37,9 @@ contract PulseUpgradeable is
             admin,
             pythFeeInWei,
             pythAddress,
-            prefillRequestStorage
+            defaultProvider,
+            prefillRequestStorage,
+            exclusivityPeriodSeconds
         );
 
         _transferOwnership(owner);

+ 235 - 75
target_chains/ethereum/contracts/forge-test/Pulse.t.sol

@@ -13,16 +13,13 @@ import "../contracts/pulse/PulseErrors.sol";
 
 contract MockPulseConsumer is IPulseConsumer {
     uint64 public lastSequenceNumber;
-    address public lastUpdater;
     PythStructs.PriceFeed[] private _lastPriceFeeds;
 
     function pulseCallback(
         uint64 sequenceNumber,
-        address updater,
         PythStructs.PriceFeed[] memory priceFeeds
     ) external override {
         lastSequenceNumber = sequenceNumber;
-        lastUpdater = updater;
         for (uint i = 0; i < priceFeeds.length; i++) {
             _lastPriceFeeds.push(priceFeeds[i]);
         }
@@ -40,7 +37,6 @@ contract MockPulseConsumer is IPulseConsumer {
 contract FailingPulseConsumer is IPulseConsumer {
     function pulseCallback(
         uint64,
-        address,
         PythStructs.PriceFeed[] memory
     ) external pure override {
         revert("callback failed");
@@ -52,7 +48,6 @@ contract CustomErrorPulseConsumer is IPulseConsumer {
 
     function pulseCallback(
         uint64,
-        address,
         PythStructs.PriceFeed[] memory
     ) external pure override {
         revert CustomError("callback failed");
@@ -65,11 +60,11 @@ contract PulseTest is Test, PulseEvents {
     MockPulseConsumer public consumer;
     address public owner;
     address public admin;
-    address public updater;
     address public pyth;
-
+    address public defaultProvider;
     // Constants
     uint128 constant PYTH_FEE = 1 wei;
+    uint128 constant DEFAULT_PROVIDER_FEE = 1 wei;
     uint128 constant CALLBACK_GAS_LIMIT = 1_000_000;
     bytes32 constant BTC_PRICE_FEED_ID =
         0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43;
@@ -86,14 +81,23 @@ contract PulseTest is Test, PulseEvents {
     function setUp() public {
         owner = address(1);
         admin = address(2);
-        updater = address(3);
-        pyth = address(4);
-
+        pyth = address(3);
+        defaultProvider = address(4);
         PulseUpgradeable _pulse = new PulseUpgradeable();
         proxy = new ERC1967Proxy(address(_pulse), "");
         pulse = PulseUpgradeable(address(proxy));
 
-        pulse.initialize(owner, admin, PYTH_FEE, pyth, false);
+        pulse.initialize(
+            owner,
+            admin,
+            PYTH_FEE,
+            pyth,
+            defaultProvider,
+            false,
+            15
+        );
+        vm.prank(defaultProvider);
+        pulse.registerProvider(DEFAULT_PROVIDER_FEE);
         consumer = new MockPulseConsumer();
     }
 
@@ -210,7 +214,8 @@ contract PulseTest is Test, PulseEvents {
             ],
             numPriceIds: 2,
             callbackGasLimit: CALLBACK_GAS_LIMIT,
-            requester: address(consumer)
+            requester: address(consumer),
+            provider: defaultProvider
         });
 
         vm.expectEmit();
@@ -298,7 +303,7 @@ contract PulseTest is Test, PulseEvents {
         vm.expectEmit();
         emit PriceUpdateExecuted(
             sequenceNumber,
-            updater,
+            defaultProvider,
             priceIds,
             expectedPrices,
             expectedConf,
@@ -309,7 +314,7 @@ contract PulseTest is Test, PulseEvents {
         // Create mock update data and execute callback
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
-        vm.prank(updater);
+        vm.prank(defaultProvider);
         pulse.executeCallback(sequenceNumber, updateData, priceIds);
 
         // Verify callback was executed
@@ -350,13 +355,13 @@ contract PulseTest is Test, PulseEvents {
         vm.expectEmit();
         emit PriceUpdateCallbackFailed(
             sequenceNumber,
-            updater,
+            defaultProvider,
             priceIds,
             address(failingConsumer),
             "callback failed"
         );
 
-        vm.prank(updater);
+        vm.prank(defaultProvider);
         pulse.executeCallback(sequenceNumber, updateData, priceIds);
     }
 
@@ -378,13 +383,13 @@ contract PulseTest is Test, PulseEvents {
         vm.expectEmit();
         emit PriceUpdateCallbackFailed(
             sequenceNumber,
-            updater,
+            defaultProvider,
             priceIds,
             address(failingConsumer),
             "low-level error (possibly out of gas)"
         );
 
-        vm.prank(updater);
+        vm.prank(defaultProvider);
         pulse.executeCallback(sequenceNumber, updateData, priceIds);
     }
 
@@ -404,8 +409,8 @@ contract PulseTest is Test, PulseEvents {
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
         // Try executing with only 100K gas when 1M is required
-        vm.prank(updater);
-        vm.expectRevert(InsufficientGas.selector);
+        vm.prank(defaultProvider);
+        vm.expectRevert(); // Just expect any revert since it will be an out-of-gas error
         pulse.executeCallback{gas: 100000}(
             sequenceNumber,
             updateData,
@@ -432,7 +437,7 @@ contract PulseTest is Test, PulseEvents {
         mockParsePriceFeedUpdates(priceFeeds); // This will make parsePriceFeedUpdates return future-dated prices
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
-        vm.prank(updater);
+        vm.prank(defaultProvider);
         // Should succeed because we're simulating receiving future-dated price updates
         pulse.executeCallback(sequenceNumber, updateData, priceIds);
 
@@ -479,11 +484,11 @@ contract PulseTest is Test, PulseEvents {
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
         // First execution
-        vm.prank(updater);
+        vm.prank(defaultProvider);
         pulse.executeCallback(sequenceNumber, updateData, priceIds);
 
         // Second execution should fail
-        vm.prank(updater);
+        vm.prank(defaultProvider);
         vm.expectRevert(NoSuchRequest.selector);
         pulse.executeCallback(sequenceNumber, updateData, priceIds);
     }
@@ -497,8 +502,9 @@ contract PulseTest is Test, PulseEvents {
 
         for (uint256 i = 0; i < gasLimits.length; i++) {
             uint256 gasLimit = gasLimits[i];
-            uint128 expectedFee = SafeCast.toUint128(tx.gasprice * gasLimit) +
-                PYTH_FEE;
+            uint128 expectedFee = SafeCast.toUint128(
+                DEFAULT_PROVIDER_FEE * gasLimit
+            ) + PYTH_FEE;
             uint128 actualFee = pulse.getFee(gasLimit);
             assertEq(
                 actualFee,
@@ -565,8 +571,7 @@ contract PulseTest is Test, PulseEvents {
     function testSetAndWithdrawAsFeeManager() public {
         address feeManager = address(0x789);
 
-        // Set fee manager as admin
-        vm.prank(admin);
+        vm.prank(defaultProvider);
         pulse.setFeeManager(feeManager);
 
         // Setup: Request price update to accrue some fees
@@ -580,47 +585,53 @@ contract PulseTest is Test, PulseEvents {
             CALLBACK_GAS_LIMIT
         );
 
-        // Test withdrawal as fee manager
+        // Get provider's accrued fees instead of total fees
+        PulseState.ProviderInfo memory providerInfo = pulse.getProviderInfo(
+            defaultProvider
+        );
+        uint128 providerAccruedFees = providerInfo.accruedFeesInWei;
+
         uint256 managerBalanceBefore = feeManager.balance;
-        uint128 accruedFees = pulse.getAccruedFees();
 
         vm.prank(feeManager);
-        pulse.withdrawAsFeeManager(accruedFees);
+        pulse.withdrawAsFeeManager(defaultProvider, providerAccruedFees);
 
         assertEq(
             feeManager.balance,
-            managerBalanceBefore + accruedFees,
+            managerBalanceBefore + providerAccruedFees,
             "Fee manager balance should increase by withdrawn amount"
         );
+
+        providerInfo = pulse.getProviderInfo(defaultProvider);
         assertEq(
-            pulse.getAccruedFees(),
+            providerInfo.accruedFeesInWei,
             0,
-            "Contract should have no fees after withdrawal"
+            "Provider should have no fees after withdrawal"
         );
     }
 
     function testSetFeeManagerUnauthorized() public {
         address feeManager = address(0x789);
         vm.prank(address(0xdead));
-        vm.expectRevert("Only admin can set fee manager");
+        vm.expectRevert("Provider not registered");
         pulse.setFeeManager(feeManager);
     }
 
     function testWithdrawAsFeeManagerUnauthorized() public {
         vm.prank(address(0xdead));
         vm.expectRevert("Only fee manager");
-        pulse.withdrawAsFeeManager(1 ether);
+        pulse.withdrawAsFeeManager(defaultProvider, 1 ether);
     }
 
     function testWithdrawAsFeeManagerInsufficientBalance() public {
         // Set up fee manager first
         address feeManager = address(0x789);
-        vm.prank(admin);
+        vm.prank(defaultProvider);
         pulse.setFeeManager(feeManager);
 
         vm.prank(feeManager);
         vm.expectRevert("Insufficient balance");
-        pulse.withdrawAsFeeManager(1 ether);
+        pulse.withdrawAsFeeManager(defaultProvider, 1 ether);
     }
 
     // Add new test for invalid priceIds
@@ -643,7 +654,7 @@ contract PulseTest is Test, PulseEvents {
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
         // Should revert when trying to execute with wrong priceIds
-        vm.prank(updater);
+        vm.prank(defaultProvider);
         vm.expectRevert(
             abi.encodeWithSelector(
                 InvalidPriceIds.selector,
@@ -654,43 +665,6 @@ contract PulseTest is Test, PulseEvents {
         pulse.executeCallback(sequenceNumber, updateData, wrongPriceIds);
     }
 
-    function testExecuteCallbackGasOverhead() public {
-        // Setup request with 1M gas limit
-        (
-            uint64 sequenceNumber,
-            bytes32[] memory priceIds,
-            uint256 publishTime
-        ) = setupConsumerRequest(address(consumer));
-
-        // Setup mock data
-        PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
-            publishTime
-        );
-        mockParsePriceFeedUpdates(priceFeeds);
-        bytes[] memory updateData = createMockUpdateData(priceFeeds);
-
-        // Should fail with exactly 1.4x gas (less than required 1.5x)
-        vm.prank(updater);
-        vm.expectRevert(InsufficientGas.selector);
-        pulse.executeCallback{gas: (CALLBACK_GAS_LIMIT * 14) / 10}(
-            sequenceNumber,
-            updateData,
-            priceIds
-        );
-
-        // Should succeed with 1.6x gas
-        vm.prank(updater);
-        pulse.executeCallback{gas: (CALLBACK_GAS_LIMIT * 16) / 10}(
-            sequenceNumber,
-            updateData,
-            priceIds
-        );
-
-        // Verify callback was executed successfully
-        assertEq(consumer.lastSequenceNumber(), sequenceNumber);
-        assertEq(consumer.lastUpdater(), updater);
-    }
-
     function testRevertOnTooManyPriceIds() public {
         uint256 maxPriceIds = uint256(pulse.MAX_PRICE_IDS());
         // Create array with MAX_PRICE_IDS + 1 price IDs
@@ -716,4 +690,190 @@ contract PulseTest is Test, PulseEvents {
             CALLBACK_GAS_LIMIT
         );
     }
+
+    function testProviderRegistration() public {
+        address provider = address(0x123);
+        uint128 providerFee = 1000;
+
+        vm.prank(provider);
+        pulse.registerProvider(providerFee);
+
+        PulseState.ProviderInfo memory info = pulse.getProviderInfo(provider);
+        assertEq(info.feeInWei, providerFee);
+        assertTrue(info.isRegistered);
+    }
+
+    function testSetProviderFee() public {
+        address provider = address(0x123);
+        uint128 initialFee = 1000;
+        uint128 newFee = 2000;
+
+        vm.prank(provider);
+        pulse.registerProvider(initialFee);
+
+        vm.prank(provider);
+        pulse.setProviderFee(newFee);
+
+        PulseState.ProviderInfo memory info = pulse.getProviderInfo(provider);
+        assertEq(info.feeInWei, newFee);
+    }
+
+    function testDefaultProvider() public {
+        address provider = address(0x123);
+        uint128 providerFee = 1000;
+
+        vm.prank(provider);
+        pulse.registerProvider(providerFee);
+
+        vm.prank(admin);
+        pulse.setDefaultProvider(provider);
+
+        assertEq(pulse.getDefaultProvider(), provider);
+    }
+
+    function testRequestWithProvider() public {
+        address provider = address(0x123);
+        uint128 providerFee = 1000;
+
+        vm.prank(provider);
+        pulse.registerProvider(providerFee);
+
+        vm.prank(admin);
+        pulse.setDefaultProvider(provider);
+
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = bytes32(uint256(1));
+
+        uint128 totalFee = pulse.getFee(CALLBACK_GAS_LIMIT);
+
+        vm.deal(address(consumer), totalFee);
+        vm.prank(address(consumer));
+        uint64 sequenceNumber = pulse.requestPriceUpdatesWithCallback{
+            value: totalFee
+        }(block.timestamp, priceIds, CALLBACK_GAS_LIMIT);
+
+        PulseState.Request memory req = pulse.getRequest(sequenceNumber);
+        assertEq(req.provider, provider);
+    }
+
+    function testExclusivityPeriod() public {
+        // Test initial value
+        assertEq(
+            pulse.getExclusivityPeriod(),
+            15,
+            "Initial exclusivity period should be 15 seconds"
+        );
+
+        // Test setting new value
+        vm.prank(admin);
+        vm.expectEmit();
+        emit ExclusivityPeriodUpdated(15, 30);
+        pulse.setExclusivityPeriod(30);
+
+        assertEq(
+            pulse.getExclusivityPeriod(),
+            30,
+            "Exclusivity period should be updated"
+        );
+    }
+
+    function testSetExclusivityPeriodUnauthorized() public {
+        vm.prank(address(0xdead));
+        vm.expectRevert("Only admin can set exclusivity period");
+        pulse.setExclusivityPeriod(30);
+    }
+
+    function testExecuteCallbackDuringExclusivity() public {
+        // Register a second provider
+        address secondProvider = address(0x456);
+        vm.prank(secondProvider);
+        pulse.registerProvider(DEFAULT_PROVIDER_FEE);
+
+        // Setup request
+        (
+            uint64 sequenceNumber,
+            bytes32[] memory priceIds,
+            uint256 publishTime
+        ) = setupConsumerRequest(address(consumer));
+
+        // Setup mock data
+        PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
+            publishTime
+        );
+        mockParsePriceFeedUpdates(priceFeeds);
+        bytes[] memory updateData = createMockUpdateData(priceFeeds);
+
+        // Try to execute with second provider during exclusivity period
+        vm.prank(secondProvider);
+        vm.expectRevert("Only assigned provider during exclusivity period");
+        pulse.executeCallback(sequenceNumber, updateData, priceIds);
+
+        // Original provider should succeed
+        vm.prank(defaultProvider);
+        pulse.executeCallback(sequenceNumber, updateData, priceIds);
+    }
+
+    function testExecuteCallbackAfterExclusivity() public {
+        // Register a second provider
+        address secondProvider = address(0x456);
+        vm.prank(secondProvider);
+        pulse.registerProvider(DEFAULT_PROVIDER_FEE);
+
+        // Setup request
+        (
+            uint64 sequenceNumber,
+            bytes32[] memory priceIds,
+            uint256 publishTime
+        ) = setupConsumerRequest(address(consumer));
+
+        // Setup mock data
+        PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
+            publishTime
+        );
+        mockParsePriceFeedUpdates(priceFeeds);
+        bytes[] memory updateData = createMockUpdateData(priceFeeds);
+
+        // Wait for exclusivity period to end
+        vm.warp(block.timestamp + pulse.getExclusivityPeriod() + 1);
+
+        // Second provider should now succeed
+        vm.prank(secondProvider);
+        pulse.executeCallback(sequenceNumber, updateData, priceIds);
+    }
+
+    function testExecuteCallbackWithCustomExclusivityPeriod() public {
+        // Register a second provider
+        address secondProvider = address(0x456);
+        vm.prank(secondProvider);
+        pulse.registerProvider(DEFAULT_PROVIDER_FEE);
+
+        // Set custom exclusivity period
+        vm.prank(admin);
+        pulse.setExclusivityPeriod(30);
+
+        // Setup request
+        (
+            uint64 sequenceNumber,
+            bytes32[] memory priceIds,
+            uint256 publishTime
+        ) = setupConsumerRequest(address(consumer));
+
+        // Setup mock data
+        PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
+            publishTime
+        );
+        mockParsePriceFeedUpdates(priceFeeds);
+        bytes[] memory updateData = createMockUpdateData(priceFeeds);
+
+        // Try at 29 seconds (should fail for second provider)
+        vm.warp(block.timestamp + 29);
+        vm.prank(secondProvider);
+        vm.expectRevert("Only assigned provider during exclusivity period");
+        pulse.executeCallback(sequenceNumber, updateData, priceIds);
+
+        // Try at 31 seconds (should succeed for second provider)
+        vm.warp(block.timestamp + 2);
+        vm.prank(secondProvider);
+        pulse.executeCallback(sequenceNumber, updateData, priceIds);
+    }
 }