Skip to content

Commit 93fbbaa

Browse files
authored
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
1 parent c2716a2 commit 93fbbaa

File tree

7 files changed

+403
-108
lines changed

7 files changed

+403
-108
lines changed

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import "./PulseState.sol";
99
interface IPulseConsumer {
1010
function pulseCallback(
1111
uint64 sequenceNumber,
12-
address updater,
1312
PythStructs.PriceFeed[] memory priceFeeds
1413
) external;
1514
}
@@ -74,8 +73,23 @@ interface IPulse is PulseEvents {
7473
uint64 sequenceNumber
7574
) external view returns (PulseState.Request memory req);
7675

77-
// Add these functions to the IPulse interface
7876
function setFeeManager(address manager) external;
7977

80-
function withdrawAsFeeManager(uint128 amount) external;
78+
function withdrawAsFeeManager(address provider, uint128 amount) external;
79+
80+
function registerProvider(uint128 feeInWei) external;
81+
82+
function setProviderFee(uint128 newFeeInWei) external;
83+
84+
function getProviderInfo(
85+
address provider
86+
) external view returns (PulseState.ProviderInfo memory);
87+
88+
function getDefaultProvider() external view returns (address);
89+
90+
function setDefaultProvider(address provider) external;
91+
92+
function setExclusivityPeriod(uint256 periodSeconds) external;
93+
94+
function getExclusivityPeriod() external view returns (uint256);
8195
}

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

Lines changed: 118 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,30 @@ abstract contract Pulse is IPulse, PulseState {
1313
address admin,
1414
uint128 pythFeeInWei,
1515
address pythAddress,
16-
bool prefillRequestStorage
16+
address defaultProvider,
17+
bool prefillRequestStorage,
18+
uint256 exclusivityPeriodSeconds
1719
) internal {
1820
require(admin != address(0), "admin is zero address");
1921
require(pythAddress != address(0), "pyth is zero address");
22+
require(
23+
defaultProvider != address(0),
24+
"defaultProvider is zero address"
25+
);
2026

2127
_state.admin = admin;
2228
_state.accruedFeesInWei = 0;
2329
_state.pythFeeInWei = pythFeeInWei;
2430
_state.pyth = pythAddress;
2531
_state.currentSequenceNumber = 1;
2632

33+
// Two-step initialization process:
34+
// 1. Set the default provider address here
35+
// 2. Provider must call registerProvider() in a separate transaction to set their fee
36+
// This ensures the provider maintains control over their own fee settings
37+
_state.defaultProvider = defaultProvider;
38+
_state.exclusivityPeriodSeconds = exclusivityPeriodSeconds;
39+
2740
if (prefillRequestStorage) {
2841
for (uint8 i = 0; i < NUM_REQUESTS; i++) {
2942
Request storage req = _state.requests[i];
@@ -45,6 +58,12 @@ abstract contract Pulse is IPulse, PulseState {
4558
bytes32[] calldata priceIds,
4659
uint256 callbackGasLimit
4760
) external payable override returns (uint64 requestSequenceNumber) {
61+
address provider = _state.defaultProvider;
62+
require(
63+
_state.providers[provider].isRegistered,
64+
"Provider not registered"
65+
);
66+
4867
// NOTE: The 60-second future limit on publishTime prevents a DoS vector where
4968
// attackers could submit many low-fee requests for far-future updates when gas prices
5069
// 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 {
6584
req.callbackGasLimit = callbackGasLimit;
6685
req.requester = msg.sender;
6786
req.numPriceIds = uint8(priceIds.length);
87+
req.provider = provider;
6888

6989
// Copy price IDs to storage
7090
for (uint8 i = 0; i < priceIds.length; i++) {
7191
req.priceIds[i] = priceIds[i];
7292
}
7393

74-
_state.accruedFeesInWei += SafeCast.toUint128(msg.value);
94+
_state.providers[provider].accruedFeesInWei += SafeCast.toUint128(
95+
msg.value - _state.pythFeeInWei
96+
);
97+
_state.accruedFeesInWei += _state.pythFeeInWei;
7598

7699
emit PriceUpdateRequested(req, priceIds);
77100
}
@@ -83,6 +106,16 @@ abstract contract Pulse is IPulse, PulseState {
83106
) external payable override {
84107
Request storage req = findActiveRequest(sequenceNumber);
85108

109+
// Check provider exclusivity using configurable period
110+
if (
111+
block.timestamp < req.publishTime + _state.exclusivityPeriodSeconds
112+
) {
113+
require(
114+
msg.sender == req.provider,
115+
"Only assigned provider during exclusivity period"
116+
);
117+
}
118+
86119
// Verify priceIds match
87120
require(
88121
priceIds.length == req.numPriceIds,
@@ -105,19 +138,10 @@ abstract contract Pulse is IPulse, PulseState {
105138

106139
clearRequest(sequenceNumber);
107140

108-
// Check if enough gas remains for callback + events/cleanup
109-
// We need extra gas beyond callbackGasLimit for:
110-
// 1. Emitting success/failure events
111-
// 2. Error handling in catch blocks
112-
// 3. State cleanup operations
113-
if (gasleft() < (req.callbackGasLimit * 3) / 2) {
114-
revert InsufficientGas();
115-
}
116-
117141
try
118142
IPulseConsumer(req.requester).pulseCallback{
119143
gas: req.callbackGasLimit
120-
}(sequenceNumber, msg.sender, priceFeeds)
144+
}(sequenceNumber, priceFeeds)
121145
{
122146
// Callback succeeded
123147
emitPriceUpdate(sequenceNumber, priceIds, priceFeeds);
@@ -173,9 +197,12 @@ abstract contract Pulse is IPulse, PulseState {
173197
function getFee(
174198
uint256 callbackGasLimit
175199
) public view override returns (uint128 feeAmount) {
176-
uint128 baseFee = _state.pythFeeInWei;
177-
uint256 gasFee = callbackGasLimit * tx.gasprice;
178-
feeAmount = baseFee + SafeCast.toUint128(gasFee);
200+
uint128 baseFee = _state.pythFeeInWei; // Fixed fee to Pyth
201+
uint128 providerFeeInWei = _state
202+
.providers[_state.defaultProvider]
203+
.feeInWei; // Provider's per-gas rate
204+
uint256 gasFee = callbackGasLimit * providerFeeInWei; // Total provider fee based on gas
205+
feeAmount = baseFee + SafeCast.toUint128(gasFee); // Total fee user needs to pay
179206
}
180207

181208
function getPythFeeInWei()
@@ -271,21 +298,89 @@ abstract contract Pulse is IPulse, PulseState {
271298
}
272299

273300
function setFeeManager(address manager) external override {
274-
require(msg.sender == _state.admin, "Only admin can set fee manager");
275-
address oldFeeManager = _state.feeManager;
276-
_state.feeManager = manager;
277-
emit FeeManagerUpdated(_state.admin, oldFeeManager, manager);
301+
require(
302+
_state.providers[msg.sender].isRegistered,
303+
"Provider not registered"
304+
);
305+
address oldFeeManager = _state.providers[msg.sender].feeManager;
306+
_state.providers[msg.sender].feeManager = manager;
307+
emit FeeManagerUpdated(msg.sender, oldFeeManager, manager);
278308
}
279309

280-
function withdrawAsFeeManager(uint128 amount) external override {
281-
require(msg.sender == _state.feeManager, "Only fee manager");
282-
require(_state.accruedFeesInWei >= amount, "Insufficient balance");
310+
function withdrawAsFeeManager(
311+
address provider,
312+
uint128 amount
313+
) external override {
314+
require(
315+
msg.sender == _state.providers[provider].feeManager,
316+
"Only fee manager"
317+
);
318+
require(
319+
_state.providers[provider].accruedFeesInWei >= amount,
320+
"Insufficient balance"
321+
);
283322

284-
_state.accruedFeesInWei -= amount;
323+
_state.providers[provider].accruedFeesInWei -= amount;
285324

286325
(bool sent, ) = msg.sender.call{value: amount}("");
287326
require(sent, "Failed to send fees");
288327

289328
emit FeesWithdrawn(msg.sender, amount);
290329
}
330+
331+
function registerProvider(uint128 feeInWei) external override {
332+
ProviderInfo storage provider = _state.providers[msg.sender];
333+
require(!provider.isRegistered, "Provider already registered");
334+
provider.feeInWei = feeInWei;
335+
provider.isRegistered = true;
336+
emit ProviderRegistered(msg.sender, feeInWei);
337+
}
338+
339+
function setProviderFee(uint128 newFeeInWei) external override {
340+
require(
341+
_state.providers[msg.sender].isRegistered,
342+
"Provider not registered"
343+
);
344+
uint128 oldFee = _state.providers[msg.sender].feeInWei;
345+
_state.providers[msg.sender].feeInWei = newFeeInWei;
346+
emit ProviderFeeUpdated(msg.sender, oldFee, newFeeInWei);
347+
}
348+
349+
function getProviderInfo(
350+
address provider
351+
) external view override returns (ProviderInfo memory) {
352+
return _state.providers[provider];
353+
}
354+
355+
function getDefaultProvider() external view override returns (address) {
356+
return _state.defaultProvider;
357+
}
358+
359+
function setDefaultProvider(address provider) external override {
360+
require(
361+
msg.sender == _state.admin,
362+
"Only admin can set default provider"
363+
);
364+
require(
365+
_state.providers[provider].isRegistered,
366+
"Provider not registered"
367+
);
368+
address oldProvider = _state.defaultProvider;
369+
_state.defaultProvider = provider;
370+
emit DefaultProviderUpdated(oldProvider, provider);
371+
}
372+
373+
function setExclusivityPeriod(uint256 periodSeconds) external override {
374+
require(
375+
msg.sender == _state.admin,
376+
"Only admin can set exclusivity period"
377+
);
378+
uint256 oldPeriod = _state.exclusivityPeriodSeconds;
379+
_state.exclusivityPeriodSeconds = periodSeconds;
380+
emit ExclusivityPeriodUpdated(oldPeriod, periodSeconds);
381+
}
382+
383+
function getExclusivityPeriod() external view override returns (uint256) {
384+
return _state.exclusivityPeriodSeconds;
385+
}
291386
}

target_chains/ethereum/contracts/contracts/pulse/PulseErrors.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,4 @@ error CallbackFailed();
1111
error InvalidPriceIds(bytes32 providedPriceIdsHash, bytes32 storedPriceIdsHash);
1212
error InvalidCallbackGasLimit(uint256 requested, uint256 stored);
1313
error ExceedsMaxPrices(uint32 requested, uint32 maxAllowed);
14-
error InsufficientGas();
1514
error TooManyPriceIds(uint256 provided, uint256 maximum);

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface PulseEvents {
88

99
event PriceUpdateExecuted(
1010
uint64 indexed sequenceNumber,
11-
address indexed updater,
11+
address indexed provider,
1212
bytes32[] priceIds,
1313
int64[] prices,
1414
uint64[] conf,
@@ -20,7 +20,7 @@ interface PulseEvents {
2020

2121
event PriceUpdateCallbackFailed(
2222
uint64 indexed sequenceNumber,
23-
address indexed updater,
23+
address indexed provider,
2424
bytes32[] priceIds,
2525
address requester,
2626
string reason
@@ -31,4 +31,17 @@ interface PulseEvents {
3131
address oldFeeManager,
3232
address newFeeManager
3333
);
34+
35+
event ProviderRegistered(address indexed provider, uint128 feeInWei);
36+
event ProviderFeeUpdated(
37+
address indexed provider,
38+
uint128 oldFee,
39+
uint128 newFee
40+
);
41+
event DefaultProviderUpdated(address oldProvider, address newProvider);
42+
43+
event ExclusivityPeriodUpdated(
44+
uint256 oldPeriodSeconds,
45+
uint256 newPeriodSeconds
46+
);
3447
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ contract PulseState {
1616
uint8 numPriceIds; // Actual number of price IDs used
1717
uint256 callbackGasLimit;
1818
address requester;
19+
address provider;
20+
}
21+
22+
struct ProviderInfo {
23+
uint128 feeInWei;
24+
uint128 accruedFeesInWei;
25+
address feeManager;
26+
bool isRegistered;
1927
}
2028

2129
struct State {
@@ -24,9 +32,11 @@ contract PulseState {
2432
uint128 accruedFeesInWei;
2533
address pyth;
2634
uint64 currentSequenceNumber;
27-
address feeManager;
35+
address defaultProvider;
36+
uint256 exclusivityPeriodSeconds;
2837
Request[NUM_REQUESTS] requests;
2938
mapping(bytes32 => Request) requestsOverflow;
39+
mapping(address => ProviderInfo) providers;
3040
}
3141

3242
State internal _state;

target_chains/ethereum/contracts/contracts/pulse/PulseUpgradeable.sol

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ contract PulseUpgradeable is
2323
address admin,
2424
uint128 pythFeeInWei,
2525
address pythAddress,
26-
bool prefillRequestStorage
27-
) public initializer {
26+
address defaultProvider,
27+
bool prefillRequestStorage,
28+
uint256 exclusivityPeriodSeconds
29+
) external initializer {
2830
require(owner != address(0), "owner is zero address");
2931
require(admin != address(0), "admin is zero address");
3032

@@ -35,7 +37,9 @@ contract PulseUpgradeable is
3537
admin,
3638
pythFeeInWei,
3739
pythAddress,
38-
prefillRequestStorage
40+
defaultProvider,
41+
prefillRequestStorage,
42+
exclusivityPeriodSeconds
3943
);
4044

4145
_transferOwnership(owner);

0 commit comments

Comments
 (0)