Skip to content

feat(pulse): add provider exclusivity period #2282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 23, 2025
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
7 changes: 5 additions & 2 deletions target_chains/ethereum/contracts/contracts/pulse/IPulse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ interface IPulse is PulseEvents {
function requestPriceUpdatesWithCallback(
uint256 publishTime,
bytes32[] calldata priceIds,
uint256 callbackGasLimit,
address provider
uint256 callbackGasLimit
) external payable returns (uint64 sequenceNumber);

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

function setDefaultProvider(address provider) external;

function setExclusivityPeriod(uint256 periodSeconds) external;

function getExclusivityPeriod() external view returns (uint256);
}
32 changes: 27 additions & 5 deletions target_chains/ethereum/contracts/contracts/pulse/Pulse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ abstract contract Pulse is IPulse, PulseState {
_state.pyth = pythAddress;
_state.currentSequenceNumber = 1;
_state.defaultProvider = defaultProvider;
_state.exclusivityPeriodSeconds = 15; // Default to 15 seconds

if (prefillRequestStorage) {
for (uint8 i = 0; i < NUM_REQUESTS; i++) {
Expand All @@ -49,12 +50,9 @@ abstract contract Pulse is IPulse, PulseState {
function requestPriceUpdatesWithCallback(
uint256 publishTime,
bytes32[] calldata priceIds,
uint256 callbackGasLimit,
address provider
uint256 callbackGasLimit
) external payable override returns (uint64 requestSequenceNumber) {
if (provider == address(0)) {
provider = _state.defaultProvider;
}
address provider = _state.defaultProvider;
require(
_state.providers[provider].isRegistered,
"Provider not registered"
Expand Down Expand Up @@ -102,6 +100,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,
Expand Down Expand Up @@ -357,4 +365,18 @@ abstract contract Pulse is IPulse, PulseState {
_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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ interface PulseEvents {
uint128 newFee
);
event DefaultProviderUpdated(address oldProvider, address newProvider);

event ExclusivityPeriodUpdated(
uint256 oldPeriodSeconds,
uint256 newPeriodSeconds
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ contract PulseState {
address pyth;
uint64 currentSequenceNumber;
address defaultProvider;
uint256 exclusivityPeriodSeconds;
Request[NUM_REQUESTS] requests;
mapping(bytes32 => Request) requestsOverflow;
mapping(address => ProviderInfo) providers;
Expand Down
146 changes: 131 additions & 15 deletions target_chains/ethereum/contracts/forge-test/Pulse.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,7 @@ contract PulseTest is Test, PulseEvents {
sequenceNumber = pulse.requestPriceUpdatesWithCallback{value: totalFee}(
publishTime,
priceIds,
CALLBACK_GAS_LIMIT,
provider
CALLBACK_GAS_LIMIT
);

return (sequenceNumber, priceIds, publishTime);
Expand Down Expand Up @@ -226,8 +225,7 @@ contract PulseTest is Test, PulseEvents {
pulse.requestPriceUpdatesWithCallback{value: totalFee}(
publishTime,
priceIds,
CALLBACK_GAS_LIMIT,
defaultProvider
CALLBACK_GAS_LIMIT
);

// Additional assertions to verify event data was stored correctly
Expand Down Expand Up @@ -260,8 +258,7 @@ contract PulseTest is Test, PulseEvents {
pulse.requestPriceUpdatesWithCallback{value: PYTH_FEE}( // Intentionally low fee
block.timestamp,
priceIds,
CALLBACK_GAS_LIMIT,
defaultProvider
CALLBACK_GAS_LIMIT
);
}

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

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

// Try to execute callback before the requested timestamp
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
Expand Down Expand Up @@ -470,8 +467,7 @@ contract PulseTest is Test, PulseEvents {
pulse.requestPriceUpdatesWithCallback{value: totalFee}(
farFutureTime,
priceIds,
CALLBACK_GAS_LIMIT,
defaultProvider
CALLBACK_GAS_LIMIT
);
}

Expand Down Expand Up @@ -536,7 +532,7 @@ contract PulseTest is Test, PulseEvents {
vm.prank(address(consumer));
pulse.requestPriceUpdatesWithCallback{
value: calculateTotalFee(defaultProvider)
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT, defaultProvider);
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT);

// Get admin's balance before withdrawal
uint256 adminBalanceBefore = admin.balance;
Expand Down Expand Up @@ -584,7 +580,7 @@ contract PulseTest is Test, PulseEvents {
vm.prank(address(consumer));
pulse.requestPriceUpdatesWithCallback{
value: calculateTotalFee(defaultProvider)
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT, defaultProvider);
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT);

// Get provider's accrued fees instead of total fees
PulseState.ProviderInfo memory providerInfo = pulse.getProviderInfo(
Expand Down Expand Up @@ -691,8 +687,7 @@ contract PulseTest is Test, PulseEvents {
pulse.requestPriceUpdatesWithCallback{value: totalFee}(
block.timestamp,
priceIds,
CALLBACK_GAS_LIMIT,
defaultProvider
CALLBACK_GAS_LIMIT
);
}

Expand Down Expand Up @@ -755,9 +750,130 @@ contract PulseTest is Test, PulseEvents {
vm.prank(address(consumer));
uint64 sequenceNumber = pulse.requestPriceUpdatesWithCallback{
value: totalFee
}(block.timestamp, priceIds, CALLBACK_GAS_LIMIT, provider);
}(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), defaultProvider);

// 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), defaultProvider);

// 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), defaultProvider);

// 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);
}
}
Loading