Skip to content

Commit 235d457

Browse files
committed
feat: keeper payment
1 parent e2caed9 commit 235d457

File tree

4 files changed

+232
-45
lines changed

4 files changed

+232
-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,26 +282,24 @@ 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
);
301304

302305
// Verify all price feeds have the same Pythnet slot.
@@ -312,36 +315,21 @@ abstract contract Scheduler is IScheduler, SchedulerState {
312315
// is more recent than latest stored update's. Reverts if not.
313316
_validateShouldUpdatePrices(subscriptionId, params, status, priceFeeds);
314317

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
318+
// Update status and store the updates
319+
uint256 latestPublishTime = 0; // Use the most recent publish time from the validated feeds
334320
for (uint8 i = 0; i < priceFeeds.length; i++) {
335-
_state.priceUpdates[subscriptionId][priceFeeds[i].id] = priceFeeds[
336-
i
337-
];
321+
if (priceFeeds[i].price.publishTime > latestPublishTime) {
322+
latestPublishTime = priceFeeds[i].price.publishTime;
323+
}
338324
}
339-
status.priceLastUpdatedAt = priceFeeds[0].price.publishTime;
340-
status.balanceInWei -= pythFee;
341-
status.totalUpdates += 1;
342-
status.totalSpent += pythFee;
325+
status.priceLastUpdatedAt = latestPublishTime;
326+
status.totalUpdates += priceFeeds.length;
343327

344-
emit PricesUpdated(subscriptionId, priceFeeds[0].price.publishTime);
328+
_storePriceUpdates(subscriptionId, priceFeeds);
329+
330+
_processFeesAndPayKeeper(status, startGas, priceIds.length, pythFee);
331+
332+
emit PricesUpdated(subscriptionId, latestPublishTime);
345333
}
346334

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