Skip to content

Commit 92a1737

Browse files
authored
fix(pulse): Miscellaneous contract fixes (#2454)
* fix stuff * more stuff * pulsse fixes * fix * fix tests * gr
1 parent a9e0dcd commit 92a1737

File tree

6 files changed

+429
-128
lines changed

6 files changed

+429
-128
lines changed

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

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,44 @@ import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
66
import "./PulseEvents.sol";
77
import "./PulseState.sol";
88

9-
interface IPulseConsumer {
9+
abstract contract IPulseConsumer {
10+
// This method is called by Pulse to provide the price updates to the consumer.
11+
// It asserts that the msg.sender is the Pulse contract. It is not meant to be
12+
// overridden by the consumer.
13+
function _pulseCallback(
14+
uint64 sequenceNumber,
15+
PythStructs.PriceFeed[] memory priceFeeds
16+
) external {
17+
address pulse = getPulse();
18+
require(pulse != address(0), "Pulse address not set");
19+
require(msg.sender == pulse, "Only Pulse can call this function");
20+
21+
pulseCallback(sequenceNumber, priceFeeds);
22+
}
23+
24+
// getPulse returns the Pulse contract address. The method is being used to check that the
25+
// callback is indeed from the Pulse contract. The consumer is expected to implement this method.
26+
function getPulse() internal view virtual returns (address);
27+
28+
// This method is expected to be implemented by the consumer to handle the price updates.
29+
// It will be called by _pulseCallback after _pulseCallback ensures that the call is
30+
// indeed from Pulse contract.
1031
function pulseCallback(
1132
uint64 sequenceNumber,
1233
PythStructs.PriceFeed[] memory priceFeeds
13-
) external;
34+
) internal virtual;
1435
}
1536

1637
interface IPulse is PulseEvents {
1738
// Core functions
1839
/**
1940
* @notice Requests price updates with a callback
2041
* @dev The msg.value must be equal to getFee(callbackGasLimit)
21-
* @param callbackGasLimit The amount of gas allocated for the callback execution
42+
* @param provider The provider to fulfill the request
2243
* @param publishTime The minimum publish time for price updates, it should be less than or equal to block.timestamp + 60
2344
* @param priceIds The price feed IDs to update. Maximum 10 price feeds per request.
2445
* Requests requiring more feeds should be split into multiple calls.
46+
* @param callbackGasLimit The amount of gas allocated for the callback execution
2547
* @return sequenceNumber The sequence number assigned to this request
2648
* @dev Security note: The 60-second future limit on publishTime prevents a DoS vector where
2749
* attackers could submit many low-fee requests for far-future updates when gas prices
@@ -30,7 +52,8 @@ interface IPulse is PulseEvents {
3052
* the fee estimation unreliable.
3153
*/
3254
function requestPriceUpdatesWithCallback(
33-
uint256 publishTime,
55+
address provider,
56+
uint64 publishTime,
3457
bytes32[] calldata priceIds,
3558
uint256 callbackGasLimit
3659
) external payable returns (uint64 sequenceNumber);
@@ -39,11 +62,13 @@ interface IPulse is PulseEvents {
3962
* @notice Executes the callback for a price update request
4063
* @dev Requires 1.5x the callback gas limit to account for cross-contract call overhead
4164
* For example, if callbackGasLimit is 1M, the transaction needs at least 1.5M gas + some gas for some other operations in the function before the callback
65+
* @param providerToCredit The provider to credit for fulfilling the request. This may not be the provider that submitted the request (if the exclusivity period has elapsed).
4266
* @param sequenceNumber The sequence number of the request
4367
* @param updateData The raw price update data from Pyth
4468
* @param priceIds The price feed IDs to update, must match the request
4569
*/
4670
function executeCallback(
71+
address providerToCredit,
4772
uint64 sequenceNumber,
4873
bytes[] calldata updateData,
4974
bytes32[] calldata priceIds
@@ -59,15 +84,22 @@ interface IPulse is PulseEvents {
5984

6085
/**
6186
* @notice Calculates the total fee required for a price update request
62-
* @dev Total fee = base Pyth protocol fee + gas costs for callback
87+
* @dev Total fee = base Pyth protocol fee + base provider fee + provider fee per feed + gas costs for callback
88+
* @param provider The provider to fulfill the request
6389
* @param callbackGasLimit The amount of gas allocated for callback execution
90+
* @param priceIds The price feed IDs to update.
6491
* @return feeAmount The total fee in wei that must be provided as msg.value
6592
*/
6693
function getFee(
67-
uint256 callbackGasLimit
94+
address provider,
95+
uint256 callbackGasLimit,
96+
bytes32[] calldata priceIds
6897
) external view returns (uint128 feeAmount);
6998

70-
function getAccruedFees() external view returns (uint128 accruedFeesInWei);
99+
function getAccruedPythFees()
100+
external
101+
view
102+
returns (uint128 accruedFeesInWei);
71103

72104
function getRequest(
73105
uint64 sequenceNumber
@@ -83,9 +115,18 @@ interface IPulse is PulseEvents {
83115

84116
function withdrawAsFeeManager(address provider, uint128 amount) external;
85117

86-
function registerProvider(uint128 feeInWei) external;
118+
function registerProvider(
119+
uint128 baseFeeInWei,
120+
uint128 feePerFeedInWei,
121+
uint128 feePerGasInWei
122+
) external;
87123

88-
function setProviderFee(uint128 newFeeInWei) external;
124+
function setProviderFee(
125+
address provider,
126+
uint128 newBaseFeeInWei,
127+
uint128 newFeePerFeedInWei,
128+
uint128 newFeePerGasInWei
129+
) external;
89130

90131
function getProviderInfo(
91132
address provider

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

Lines changed: 102 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,19 @@ abstract contract Pulse is IPulse, PulseState {
5353
}
5454
}
5555

56+
// TODO: there can be a separate wrapper function that defaults the provider (or uses the cheapest or something).
5657
function requestPriceUpdatesWithCallback(
57-
uint256 publishTime,
58+
address provider,
59+
uint64 publishTime,
5860
bytes32[] calldata priceIds,
5961
uint256 callbackGasLimit
6062
) external payable override returns (uint64 requestSequenceNumber) {
61-
address provider = _state.defaultProvider;
6263
require(
6364
_state.providers[provider].isRegistered,
6465
"Provider not registered"
6566
);
6667

68+
// FIXME: this comment is wrong. (we're not using tx.gasprice)
6769
// NOTE: The 60-second future limit on publishTime prevents a DoS vector where
6870
// attackers could submit many low-fee requests for far-future updates when gas prices
6971
// are low, forcing executors to fulfill them later when gas prices might be much higher.
@@ -75,7 +77,7 @@ abstract contract Pulse is IPulse, PulseState {
7577
}
7678
requestSequenceNumber = _state.currentSequenceNumber++;
7779

78-
uint128 requiredFee = getFee(callbackGasLimit);
80+
uint128 requiredFee = getFee(provider, callbackGasLimit, priceIds);
7981
if (msg.value < requiredFee) revert InsufficientFee();
8082

8183
Request storage req = allocRequest(requestSequenceNumber);
@@ -85,21 +87,20 @@ abstract contract Pulse is IPulse, PulseState {
8587
req.requester = msg.sender;
8688
req.numPriceIds = uint8(priceIds.length);
8789
req.provider = provider;
90+
req.fee = SafeCast.toUint128(msg.value - _state.pythFeeInWei);
8891

8992
// Copy price IDs to storage
9093
for (uint8 i = 0; i < priceIds.length; i++) {
9194
req.priceIds[i] = priceIds[i];
9295
}
93-
94-
_state.providers[provider].accruedFeesInWei += SafeCast.toUint128(
95-
msg.value - _state.pythFeeInWei
96-
);
9796
_state.accruedFeesInWei += _state.pythFeeInWei;
9897

9998
emit PriceUpdateRequested(req, priceIds);
10099
}
101100

101+
// TODO: does this need to be payable? Any cost paid to Pyth could be taken out of the provider's accrued fees.
102102
function executeCallback(
103+
address providerToCredit,
103104
uint64 sequenceNumber,
104105
bytes[] calldata updateData,
105106
bytes32[] calldata priceIds
@@ -111,7 +112,7 @@ abstract contract Pulse is IPulse, PulseState {
111112
block.timestamp < req.publishTime + _state.exclusivityPeriodSeconds
112113
) {
113114
require(
114-
msg.sender == req.provider,
115+
providerToCredit == req.provider,
115116
"Only assigned provider during exclusivity period"
116117
);
117118
}
@@ -127,19 +128,41 @@ abstract contract Pulse is IPulse, PulseState {
127128
}
128129
}
129130

130-
// Parse price feeds first to measure gas usage
131-
PythStructs.PriceFeed[] memory priceFeeds = IPyth(_state.pyth)
132-
.parsePriceFeedUpdates(
133-
updateData,
134-
priceIds,
135-
SafeCast.toUint64(req.publishTime),
136-
SafeCast.toUint64(req.publishTime)
137-
);
131+
// TODO: should this use parsePriceFeedUpdatesUnique? also, do we need to add 1 to maxPublishTime?
132+
IPyth pyth = IPyth(_state.pyth);
133+
uint256 pythFee = pyth.getUpdateFee(updateData);
134+
PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
135+
value: pythFee
136+
}(
137+
updateData,
138+
priceIds,
139+
SafeCast.toUint64(req.publishTime),
140+
SafeCast.toUint64(req.publishTime)
141+
);
142+
143+
// TODO: if this effect occurs here, we need to guarantee that executeCallback can never revert.
144+
// If executeCallback can revert, then funds can be permanently locked in the contract.
145+
// TODO: there also needs to be some penalty mechanism in case the expected provider doesn't execute the callback.
146+
// This should take funds from the expected provider and give to providerToCredit. The penalty should probably scale
147+
// with time in order to ensure that the callback eventually gets executed.
148+
// (There may be exploits with ^ though if the consumer contract is malicious ?)
149+
_state.providers[providerToCredit].accruedFeesInWei += SafeCast
150+
.toUint128((req.fee + msg.value) - pythFee);
138151

139152
clearRequest(sequenceNumber);
140153

154+
// TODO: I'm pretty sure this is going to use a lot of gas because it's doing a storage lookup for each sequence number.
155+
// a better solution would be a doubly-linked list of active requests.
156+
// After successful callback, update firstUnfulfilledSeq if needed
157+
while (
158+
_state.firstUnfulfilledSeq < _state.currentSequenceNumber &&
159+
!isActive(findRequest(_state.firstUnfulfilledSeq))
160+
) {
161+
_state.firstUnfulfilledSeq++;
162+
}
163+
141164
try
142-
IPulseConsumer(req.requester).pulseCallback{
165+
IPulseConsumer(req.requester)._pulseCallback{
143166
gas: req.callbackGasLimit
144167
}(sequenceNumber, priceFeeds)
145168
{
@@ -149,7 +172,7 @@ abstract contract Pulse is IPulse, PulseState {
149172
// Explicit revert/require
150173
emit PriceUpdateCallbackFailed(
151174
sequenceNumber,
152-
msg.sender,
175+
providerToCredit,
153176
priceIds,
154177
req.requester,
155178
reason
@@ -158,20 +181,12 @@ abstract contract Pulse is IPulse, PulseState {
158181
// Out of gas or other low-level errors
159182
emit PriceUpdateCallbackFailed(
160183
sequenceNumber,
161-
msg.sender,
184+
providerToCredit,
162185
priceIds,
163186
req.requester,
164187
"low-level error (possibly out of gas)"
165188
);
166189
}
167-
168-
// After successful callback, update firstUnfulfilledSeq if needed
169-
while (
170-
_state.firstUnfulfilledSeq < _state.currentSequenceNumber &&
171-
!isActive(findRequest(_state.firstUnfulfilledSeq))
172-
) {
173-
_state.firstUnfulfilledSeq++;
174-
}
175190
}
176191

177192
function emitPriceUpdate(
@@ -182,13 +197,16 @@ abstract contract Pulse is IPulse, PulseState {
182197
int64[] memory prices = new int64[](priceFeeds.length);
183198
uint64[] memory conf = new uint64[](priceFeeds.length);
184199
int32[] memory expos = new int32[](priceFeeds.length);
185-
uint256[] memory publishTimes = new uint256[](priceFeeds.length);
200+
uint64[] memory publishTimes = new uint64[](priceFeeds.length);
186201

187202
for (uint i = 0; i < priceFeeds.length; i++) {
188203
prices[i] = priceFeeds[i].price.price;
189204
conf[i] = priceFeeds[i].price.conf;
190205
expos[i] = priceFeeds[i].price.expo;
191-
publishTimes[i] = priceFeeds[i].price.publishTime;
206+
// Safe cast because this is a unix timestamp in seconds.
207+
publishTimes[i] = SafeCast.toUint64(
208+
priceFeeds[i].price.publishTime
209+
);
192210
}
193211

194212
emit PriceUpdateExecuted(
@@ -203,14 +221,25 @@ abstract contract Pulse is IPulse, PulseState {
203221
}
204222

205223
function getFee(
206-
uint256 callbackGasLimit
224+
address provider,
225+
uint256 callbackGasLimit,
226+
bytes32[] calldata priceIds
207227
) public view override returns (uint128 feeAmount) {
208228
uint128 baseFee = _state.pythFeeInWei; // Fixed fee to Pyth
209-
uint128 providerFeeInWei = _state
210-
.providers[_state.defaultProvider]
211-
.feeInWei; // Provider's per-gas rate
229+
// Note: The provider needs to set its fees to include the fee charged by the Pyth contract.
230+
// Ideally, we would be able to automatically compute the pyth fees from the priceIds, but the
231+
// fee computation on IPyth assumes it has the full updated data.
232+
uint128 providerBaseFee = _state.providers[provider].baseFeeInWei;
233+
uint128 providerFeedFee = SafeCast.toUint128(
234+
priceIds.length * _state.providers[provider].feePerFeedInWei
235+
);
236+
uint128 providerFeeInWei = _state.providers[provider].feePerGasInWei; // Provider's per-gas rate
212237
uint256 gasFee = callbackGasLimit * providerFeeInWei; // Total provider fee based on gas
213-
feeAmount = baseFee + SafeCast.toUint128(gasFee); // Total fee user needs to pay
238+
feeAmount =
239+
baseFee +
240+
providerBaseFee +
241+
providerFeedFee +
242+
SafeCast.toUint128(gasFee); // Total fee user needs to pay
214243
}
215244

216245
function getPythFeeInWei()
@@ -222,7 +251,7 @@ abstract contract Pulse is IPulse, PulseState {
222251
pythFeeInWei = _state.pythFeeInWei;
223252
}
224253

225-
function getAccruedFees()
254+
function getAccruedPythFees()
226255
public
227256
view
228257
override
@@ -244,6 +273,7 @@ abstract contract Pulse is IPulse, PulseState {
244273
shortHash = uint8(hash[0] & NUM_REQUESTS_MASK);
245274
}
246275

276+
// TODO: move out governance functions into a separate PulseGovernance contract
247277
function withdrawFees(uint128 amount) external override {
248278
require(msg.sender == _state.admin, "Only admin can withdraw fees");
249279
require(_state.accruedFeesInWei >= amount, "Insufficient balance");
@@ -336,22 +366,51 @@ abstract contract Pulse is IPulse, PulseState {
336366
emit FeesWithdrawn(msg.sender, amount);
337367
}
338368

339-
function registerProvider(uint128 feeInWei) external override {
369+
function registerProvider(
370+
uint128 baseFeeInWei,
371+
uint128 feePerFeedInWei,
372+
uint128 feePerGasInWei
373+
) external override {
340374
ProviderInfo storage provider = _state.providers[msg.sender];
341375
require(!provider.isRegistered, "Provider already registered");
342-
provider.feeInWei = feeInWei;
376+
provider.baseFeeInWei = baseFeeInWei;
377+
provider.feePerFeedInWei = feePerFeedInWei;
378+
provider.feePerGasInWei = feePerGasInWei;
343379
provider.isRegistered = true;
344-
emit ProviderRegistered(msg.sender, feeInWei);
380+
emit ProviderRegistered(msg.sender, feePerGasInWei);
345381
}
346382

347-
function setProviderFee(uint128 newFeeInWei) external override {
383+
function setProviderFee(
384+
address provider,
385+
uint128 newBaseFeeInWei,
386+
uint128 newFeePerFeedInWei,
387+
uint128 newFeePerGasInWei
388+
) external override {
348389
require(
349-
_state.providers[msg.sender].isRegistered,
390+
_state.providers[provider].isRegistered,
350391
"Provider not registered"
351392
);
352-
uint128 oldFee = _state.providers[msg.sender].feeInWei;
353-
_state.providers[msg.sender].feeInWei = newFeeInWei;
354-
emit ProviderFeeUpdated(msg.sender, oldFee, newFeeInWei);
393+
require(
394+
msg.sender == provider ||
395+
msg.sender == _state.providers[provider].feeManager,
396+
"Only provider or fee manager can invoke this method"
397+
);
398+
399+
uint128 oldBaseFee = _state.providers[provider].baseFeeInWei;
400+
uint128 oldFeePerFeed = _state.providers[provider].feePerFeedInWei;
401+
uint128 oldFeePerGas = _state.providers[provider].feePerGasInWei;
402+
_state.providers[provider].baseFeeInWei = newBaseFeeInWei;
403+
_state.providers[provider].feePerFeedInWei = newFeePerFeedInWei;
404+
_state.providers[provider].feePerGasInWei = newFeePerGasInWei;
405+
emit ProviderFeeUpdated(
406+
provider,
407+
oldBaseFee,
408+
oldFeePerFeed,
409+
oldFeePerGas,
410+
newBaseFeeInWei,
411+
newFeePerFeedInWei,
412+
newFeePerGasInWei
413+
);
355414
}
356415

357416
function getProviderInfo(

0 commit comments

Comments
 (0)