Skip to content

Commit f67734f

Browse files
authored
add exclusivity period to provider (#2282)
1 parent d68155e commit f67734f

File tree

5 files changed

+169
-22
lines changed

5 files changed

+169
-22
lines changed

target_chains/ethereum/contracts/contracts/pulse/IPulse.sol

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ interface IPulse is PulseEvents {
3333
function requestPriceUpdatesWithCallback(
3434
uint256 publishTime,
3535
bytes32[] calldata priceIds,
36-
uint256 callbackGasLimit,
37-
address provider
36+
uint256 callbackGasLimit
3837
) external payable returns (uint64 sequenceNumber);
3938

4039
/**
@@ -92,4 +91,8 @@ interface IPulse is PulseEvents {
9291
function getDefaultProvider() external view returns (address);
9392

9493
function setDefaultProvider(address provider) external;
94+
95+
function setExclusivityPeriod(uint256 periodSeconds) external;
96+
97+
function getExclusivityPeriod() external view returns (uint256);
9598
}

target_chains/ethereum/contracts/contracts/pulse/Pulse.sol

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ abstract contract Pulse is IPulse, PulseState {
2929
_state.pyth = pythAddress;
3030
_state.currentSequenceNumber = 1;
3131
_state.defaultProvider = defaultProvider;
32+
_state.exclusivityPeriodSeconds = 15; // Default to 15 seconds
3233

3334
if (prefillRequestStorage) {
3435
for (uint8 i = 0; i < NUM_REQUESTS; i++) {
@@ -49,12 +50,9 @@ abstract contract Pulse is IPulse, PulseState {
4950
function requestPriceUpdatesWithCallback(
5051
uint256 publishTime,
5152
bytes32[] calldata priceIds,
52-
uint256 callbackGasLimit,
53-
address provider
53+
uint256 callbackGasLimit
5454
) external payable override returns (uint64 requestSequenceNumber) {
55-
if (provider == address(0)) {
56-
provider = _state.defaultProvider;
57-
}
55+
address provider = _state.defaultProvider;
5856
require(
5957
_state.providers[provider].isRegistered,
6058
"Provider not registered"
@@ -102,6 +100,16 @@ abstract contract Pulse is IPulse, PulseState {
102100
) external payable override {
103101
Request storage req = findActiveRequest(sequenceNumber);
104102

103+
// Check provider exclusivity using configurable period
104+
if (
105+
block.timestamp < req.publishTime + _state.exclusivityPeriodSeconds
106+
) {
107+
require(
108+
msg.sender == req.provider,
109+
"Only assigned provider during exclusivity period"
110+
);
111+
}
112+
105113
// Verify priceIds match
106114
require(
107115
priceIds.length == req.numPriceIds,
@@ -357,4 +365,18 @@ abstract contract Pulse is IPulse, PulseState {
357365
_state.defaultProvider = provider;
358366
emit DefaultProviderUpdated(oldProvider, provider);
359367
}
368+
369+
function setExclusivityPeriod(uint256 periodSeconds) external override {
370+
require(
371+
msg.sender == _state.admin,
372+
"Only admin can set exclusivity period"
373+
);
374+
uint256 oldPeriod = _state.exclusivityPeriodSeconds;
375+
_state.exclusivityPeriodSeconds = periodSeconds;
376+
emit ExclusivityPeriodUpdated(oldPeriod, periodSeconds);
377+
}
378+
379+
function getExclusivityPeriod() external view override returns (uint256) {
380+
return _state.exclusivityPeriodSeconds;
381+
}
360382
}

target_chains/ethereum/contracts/contracts/pulse/PulseEvents.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,9 @@ interface PulseEvents {
3939
uint128 newFee
4040
);
4141
event DefaultProviderUpdated(address oldProvider, address newProvider);
42+
43+
event ExclusivityPeriodUpdated(
44+
uint256 oldPeriodSeconds,
45+
uint256 newPeriodSeconds
46+
);
4247
}

target_chains/ethereum/contracts/contracts/pulse/PulseState.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ contract PulseState {
3333
address pyth;
3434
uint64 currentSequenceNumber;
3535
address defaultProvider;
36+
uint256 exclusivityPeriodSeconds;
3637
Request[NUM_REQUESTS] requests;
3738
mapping(bytes32 => Request) requestsOverflow;
3839
mapping(address => ProviderInfo) providers;

target_chains/ethereum/contracts/forge-test/Pulse.t.sol

Lines changed: 131 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,7 @@ contract PulseTest is Test, PulseEvents {
179179
sequenceNumber = pulse.requestPriceUpdatesWithCallback{value: totalFee}(
180180
publishTime,
181181
priceIds,
182-
CALLBACK_GAS_LIMIT,
183-
provider
182+
CALLBACK_GAS_LIMIT
184183
);
185184

186185
return (sequenceNumber, priceIds, publishTime);
@@ -226,8 +225,7 @@ contract PulseTest is Test, PulseEvents {
226225
pulse.requestPriceUpdatesWithCallback{value: totalFee}(
227226
publishTime,
228227
priceIds,
229-
CALLBACK_GAS_LIMIT,
230-
defaultProvider
228+
CALLBACK_GAS_LIMIT
231229
);
232230

233231
// Additional assertions to verify event data was stored correctly
@@ -260,8 +258,7 @@ contract PulseTest is Test, PulseEvents {
260258
pulse.requestPriceUpdatesWithCallback{value: PYTH_FEE}( // Intentionally low fee
261259
block.timestamp,
262260
priceIds,
263-
CALLBACK_GAS_LIMIT,
264-
defaultProvider
261+
CALLBACK_GAS_LIMIT
265262
);
266263
}
267264

@@ -277,7 +274,7 @@ contract PulseTest is Test, PulseEvents {
277274
vm.prank(address(consumer));
278275
uint64 sequenceNumber = pulse.requestPriceUpdatesWithCallback{
279276
value: totalFee
280-
}(publishTime, priceIds, CALLBACK_GAS_LIMIT, defaultProvider);
277+
}(publishTime, priceIds, CALLBACK_GAS_LIMIT);
281278

282279
// Step 2: Create mock price feeds and setup Pyth response
283280
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
@@ -432,7 +429,7 @@ contract PulseTest is Test, PulseEvents {
432429
vm.prank(address(consumer));
433430
uint64 sequenceNumber = pulse.requestPriceUpdatesWithCallback{
434431
value: totalFee
435-
}(futureTime, priceIds, CALLBACK_GAS_LIMIT, defaultProvider);
432+
}(futureTime, priceIds, CALLBACK_GAS_LIMIT);
436433

437434
// Try to execute callback before the requested timestamp
438435
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
@@ -470,8 +467,7 @@ contract PulseTest is Test, PulseEvents {
470467
pulse.requestPriceUpdatesWithCallback{value: totalFee}(
471468
farFutureTime,
472469
priceIds,
473-
CALLBACK_GAS_LIMIT,
474-
defaultProvider
470+
CALLBACK_GAS_LIMIT
475471
);
476472
}
477473

@@ -536,7 +532,7 @@ contract PulseTest is Test, PulseEvents {
536532
vm.prank(address(consumer));
537533
pulse.requestPriceUpdatesWithCallback{
538534
value: calculateTotalFee(defaultProvider)
539-
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT, defaultProvider);
535+
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT);
540536

541537
// Get admin's balance before withdrawal
542538
uint256 adminBalanceBefore = admin.balance;
@@ -584,7 +580,7 @@ contract PulseTest is Test, PulseEvents {
584580
vm.prank(address(consumer));
585581
pulse.requestPriceUpdatesWithCallback{
586582
value: calculateTotalFee(defaultProvider)
587-
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT, defaultProvider);
583+
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT);
588584

589585
// Get provider's accrued fees instead of total fees
590586
PulseState.ProviderInfo memory providerInfo = pulse.getProviderInfo(
@@ -691,8 +687,7 @@ contract PulseTest is Test, PulseEvents {
691687
pulse.requestPriceUpdatesWithCallback{value: totalFee}(
692688
block.timestamp,
693689
priceIds,
694-
CALLBACK_GAS_LIMIT,
695-
defaultProvider
690+
CALLBACK_GAS_LIMIT
696691
);
697692
}
698693

@@ -755,9 +750,130 @@ contract PulseTest is Test, PulseEvents {
755750
vm.prank(address(consumer));
756751
uint64 sequenceNumber = pulse.requestPriceUpdatesWithCallback{
757752
value: totalFee
758-
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT, provider);
753+
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT);
759754

760755
PulseState.Request memory req = pulse.getRequest(sequenceNumber);
761756
assertEq(req.provider, provider);
762757
}
758+
759+
function testExclusivityPeriod() public {
760+
// Test initial value
761+
assertEq(
762+
pulse.getExclusivityPeriod(),
763+
15,
764+
"Initial exclusivity period should be 15 seconds"
765+
);
766+
767+
// Test setting new value
768+
vm.prank(admin);
769+
vm.expectEmit();
770+
emit ExclusivityPeriodUpdated(15, 30);
771+
pulse.setExclusivityPeriod(30);
772+
773+
assertEq(
774+
pulse.getExclusivityPeriod(),
775+
30,
776+
"Exclusivity period should be updated"
777+
);
778+
}
779+
780+
function testSetExclusivityPeriodUnauthorized() public {
781+
vm.prank(address(0xdead));
782+
vm.expectRevert("Only admin can set exclusivity period");
783+
pulse.setExclusivityPeriod(30);
784+
}
785+
786+
function testExecuteCallbackDuringExclusivity() public {
787+
// Register a second provider
788+
address secondProvider = address(0x456);
789+
vm.prank(secondProvider);
790+
pulse.registerProvider(DEFAULT_PROVIDER_FEE);
791+
792+
// Setup request
793+
(
794+
uint64 sequenceNumber,
795+
bytes32[] memory priceIds,
796+
uint256 publishTime
797+
) = setupConsumerRequest(address(consumer), defaultProvider);
798+
799+
// Setup mock data
800+
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
801+
publishTime
802+
);
803+
mockParsePriceFeedUpdates(priceFeeds);
804+
bytes[] memory updateData = createMockUpdateData(priceFeeds);
805+
806+
// Try to execute with second provider during exclusivity period
807+
vm.prank(secondProvider);
808+
vm.expectRevert("Only assigned provider during exclusivity period");
809+
pulse.executeCallback(sequenceNumber, updateData, priceIds);
810+
811+
// Original provider should succeed
812+
vm.prank(defaultProvider);
813+
pulse.executeCallback(sequenceNumber, updateData, priceIds);
814+
}
815+
816+
function testExecuteCallbackAfterExclusivity() public {
817+
// Register a second provider
818+
address secondProvider = address(0x456);
819+
vm.prank(secondProvider);
820+
pulse.registerProvider(DEFAULT_PROVIDER_FEE);
821+
822+
// Setup request
823+
(
824+
uint64 sequenceNumber,
825+
bytes32[] memory priceIds,
826+
uint256 publishTime
827+
) = setupConsumerRequest(address(consumer), defaultProvider);
828+
829+
// Setup mock data
830+
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
831+
publishTime
832+
);
833+
mockParsePriceFeedUpdates(priceFeeds);
834+
bytes[] memory updateData = createMockUpdateData(priceFeeds);
835+
836+
// Wait for exclusivity period to end
837+
vm.warp(block.timestamp + pulse.getExclusivityPeriod() + 1);
838+
839+
// Second provider should now succeed
840+
vm.prank(secondProvider);
841+
pulse.executeCallback(sequenceNumber, updateData, priceIds);
842+
}
843+
844+
function testExecuteCallbackWithCustomExclusivityPeriod() public {
845+
// Register a second provider
846+
address secondProvider = address(0x456);
847+
vm.prank(secondProvider);
848+
pulse.registerProvider(DEFAULT_PROVIDER_FEE);
849+
850+
// Set custom exclusivity period
851+
vm.prank(admin);
852+
pulse.setExclusivityPeriod(30);
853+
854+
// Setup request
855+
(
856+
uint64 sequenceNumber,
857+
bytes32[] memory priceIds,
858+
uint256 publishTime
859+
) = setupConsumerRequest(address(consumer), defaultProvider);
860+
861+
// Setup mock data
862+
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
863+
publishTime
864+
);
865+
mockParsePriceFeedUpdates(priceFeeds);
866+
bytes[] memory updateData = createMockUpdateData(priceFeeds);
867+
868+
// Try at 29 seconds (should fail for second provider)
869+
vm.warp(block.timestamp + 29);
870+
vm.prank(secondProvider);
871+
vm.expectRevert("Only assigned provider during exclusivity period");
872+
pulse.executeCallback(sequenceNumber, updateData, priceIds);
873+
874+
// Try at 31 seconds (should succeed for second provider)
875+
vm.warp(block.timestamp + 2);
876+
vm.prank(secondProvider);
877+
pulse.executeCallback(sequenceNumber, updateData, priceIds);
878+
}
763879
}

0 commit comments

Comments
 (0)