Skip to content

Commit 8d7bba7

Browse files
feat(pulse): keeper payment (#2617)
* feat: keeper payment * fix: let gasbenchmark test contract accept payment * fix: use checks-effects-interactions for the pyth and keeper fees
1 parent 449b4c2 commit 8d7bba7

File tree

5 files changed

+235
-45
lines changed

5 files changed

+235
-45
lines changed

target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ abstract contract Scheduler is IScheduler, SchedulerState {
250250
bytes[] calldata updateData,
251251
bytes32[] calldata priceIds
252252
) external override {
253+
uint256 startGas = gasleft();
254+
253255
SubscriptionStatus storage status = _state.subscriptionStatuses[
254256
subscriptionId
255257
];
@@ -261,9 +263,12 @@ abstract contract Scheduler is IScheduler, SchedulerState {
261263
revert InactiveSubscription();
262264
}
263265

264-
// Verify price IDs match subscription
266+
// Verify price IDs match subscription length
265267
if (priceIds.length != params.priceIds.length) {
266-
revert InvalidPriceIdsLength(priceIds[0], params.priceIds[0]);
268+
revert InvalidPriceIdsLength(
269+
priceIds.length,
270+
params.priceIds.length
271+
);
267272
}
268273

269274
// Keepers must provide priceIds in the exact same order as defined in the subscription
@@ -277,27 +282,27 @@ abstract contract Scheduler is IScheduler, SchedulerState {
277282
IPyth pyth = IPyth(_state.pyth);
278283
uint256 pythFee = pyth.getUpdateFee(updateData);
279284

280-
// Check if subscription has enough balance
285+
// If we don't have enough balance, revert
281286
if (status.balanceInWei < pythFee) {
282287
revert InsufficientBalance();
283288
}
284289

285290
// Parse the price feed updates with an acceptable timestamp range of [-1h, +10s] from now.
286291
// We will validate the trigger conditions ourselves.
287292
uint64 curTime = SafeCast.toUint64(block.timestamp);
288-
uint64 maxPublishTime = curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD;
289-
uint64 minPublishTime = curTime > PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
290-
? curTime - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
291-
: 0;
292293
(
293294
PythStructs.PriceFeed[] memory priceFeeds,
294295
uint64[] memory slots
295296
) = pyth.parsePriceFeedUpdatesWithSlots{value: pythFee}(
296297
updateData,
297298
priceIds,
298-
minPublishTime,
299-
maxPublishTime
299+
curTime > PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
300+
? curTime - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
301+
: 0,
302+
curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD
300303
);
304+
status.balanceInWei -= pythFee;
305+
status.totalSpent += pythFee;
301306

302307
// Verify all price feeds have the same Pythnet slot.
303308
// All feeds in a subscription must be updated at the same time.
@@ -312,36 +317,21 @@ abstract contract Scheduler is IScheduler, SchedulerState {
312317
// is more recent than latest stored update's. Reverts if not.
313318
_validateShouldUpdatePrices(subscriptionId, params, status, priceFeeds);
314319

315-
// Store the price updates, update status, and emit event
316-
_storePriceUpdatesAndStatus(
317-
subscriptionId,
318-
status,
319-
priceFeeds,
320-
pythFee
321-
);
322-
}
323-
324-
/**
325-
* @notice Stores the price updates, updates subscription status, and emits event.
326-
*/
327-
function _storePriceUpdatesAndStatus(
328-
uint256 subscriptionId,
329-
SubscriptionStatus storage status,
330-
PythStructs.PriceFeed[] memory priceFeeds,
331-
uint256 pythFee
332-
) internal {
333-
// Store the price updates
320+
// Update status and store the updates
321+
uint256 latestPublishTime = 0; // Use the most recent publish time from the validated feeds
334322
for (uint8 i = 0; i < priceFeeds.length; i++) {
335-
_state.priceUpdates[subscriptionId][priceFeeds[i].id] = priceFeeds[
336-
i
337-
];
323+
if (priceFeeds[i].price.publishTime > latestPublishTime) {
324+
latestPublishTime = priceFeeds[i].price.publishTime;
325+
}
338326
}
339-
status.priceLastUpdatedAt = priceFeeds[0].price.publishTime;
340-
status.balanceInWei -= pythFee;
341-
status.totalUpdates += 1;
342-
status.totalSpent += pythFee;
327+
status.priceLastUpdatedAt = latestPublishTime;
328+
status.totalUpdates += priceFeeds.length;
329+
330+
_storePriceUpdates(subscriptionId, priceFeeds);
343331

344-
emit PricesUpdated(subscriptionId, priceFeeds[0].price.publishTime);
332+
_processFeesAndPayKeeper(status, startGas, priceIds.length);
333+
334+
emit PricesUpdated(subscriptionId, latestPublishTime);
345335
}
346336

347337
/**
@@ -737,4 +727,53 @@ abstract contract Scheduler is IScheduler, SchedulerState {
737727
_state.activeSubscriptionIndex[subscriptionId] = 0;
738728
}
739729
}
730+
731+
/**
732+
* @notice Internal function to store the parsed price feeds.
733+
* @param subscriptionId The ID of the subscription.
734+
* @param priceFeeds The array of price feeds to store.
735+
*/
736+
function _storePriceUpdates(
737+
uint256 subscriptionId,
738+
PythStructs.PriceFeed[] memory priceFeeds
739+
) internal {
740+
for (uint8 i = 0; i < priceFeeds.length; i++) {
741+
_state.priceUpdates[subscriptionId][priceFeeds[i].id] = priceFeeds[
742+
i
743+
];
744+
}
745+
}
746+
747+
/**
748+
* @notice Internal function to calculate total fees, deduct from balance, and pay the keeper.
749+
* @dev This function sends funds to `msg.sender`, so be sure that this is being called by a keeper.
750+
* @dev Note that the Pyth fee is already paid in the parsePriceFeedUpdatesWithSlots call.
751+
* @param status Storage reference to the subscription's status.
752+
* @param startGas Gas remaining at the start of the parent function call.
753+
* @param numPriceIds Number of price IDs being updated.
754+
*/
755+
function _processFeesAndPayKeeper(
756+
SubscriptionStatus storage status,
757+
uint256 startGas,
758+
uint256 numPriceIds
759+
) internal {
760+
// Calculate fee components
761+
uint256 gasCost = (startGas - gasleft() + GAS_OVERHEAD) * tx.gasprice;
762+
uint256 keeperSpecificFee = uint256(_state.singleUpdateKeeperFeeInWei) *
763+
numPriceIds;
764+
uint256 totalKeeperFee = gasCost + keeperSpecificFee;
765+
766+
// Check balance
767+
if (status.balanceInWei < totalKeeperFee) {
768+
revert InsufficientBalance();
769+
}
770+
771+
// Pay keeper and update status if successful
772+
(bool sent, ) = msg.sender.call{value: totalKeeperFee}("");
773+
if (!sent) {
774+
revert KeeperPaymentFailed();
775+
}
776+
status.balanceInWei -= totalKeeperFee;
777+
status.totalSpent += totalKeeperFee;
778+
}
740779
}

target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ error CannotUpdatePermanentSubscription();
1212

1313
// Price feed errors
1414
error InvalidPriceId(bytes32 providedPriceId, bytes32 expectedPriceId);
15-
error InvalidPriceIdsLength(bytes32 providedLength, bytes32 expectedLength);
15+
error InvalidPriceIdsLength(uint256 providedLength, uint256 expectedLength);
1616
error EmptyPriceIds();
1717
error TooManyPriceIds(uint256 provided, uint256 maximum);
1818
error DuplicatePriceId(bytes32 priceId);
@@ -29,3 +29,6 @@ error TimestampOlderThanLastUpdate(
2929
// Whitelist errors
3030
error TooManyWhitelistedReaders(uint256 provided, uint256 maximum);
3131
error DuplicateWhitelistAddress(address addr);
32+
33+
// Payment errors
34+
error KeeperPaymentFailed();

target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ contract SchedulerState {
1717
/// Maximum time in the future (relative to current block timestamp)
1818
/// for which a price update timestamp is considered valid
1919
uint64 public constant FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD = 10 seconds;
20+
/// Fixed gas overhead component used in keeper fee calculation.
21+
/// This is a rough estimate of the tx overhead for a keeper to call updatePriceFeeds.
22+
uint256 public constant GAS_OVERHEAD = 30000;
2023

2124
struct State {
2225
/// Monotonically increasing counter for subscription IDs

0 commit comments

Comments
 (0)