Skip to content

fix(pulse): Miscellaneous contract fixes #2454

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 6 commits into from
Mar 11, 2025
Merged

fix(pulse): Miscellaneous contract fixes #2454

merged 6 commits into from
Mar 11, 2025

Conversation

jayantk
Copy link
Contributor

@jayantk jayantk commented Mar 8, 2025

Summary

I was looking through the Pulse contract and noticed some issues:

  • There's a security issue with the IPulseConsumer interface as anyone can mock data and pass it as the legitimate response to the request
  • publishTimes are unix timestamps so should be uint64
  • The provider fee / backup mechanism doesn't work properly because it doesn't account for pyth fees, and it also doesn't give any incentive to backup providers.

How has this been tested?

  • Current tests cover my changes
  • Added new tests
  • Manually tested the code

Copy link

vercel bot commented Mar 8, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
api-reference ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 8, 2025 10:55pm
proposals ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 8, 2025 10:55pm
staking ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 8, 2025 10:55pm
3 Skipped Deployments
Name Status Preview Comments Updated (UTC)
component-library ⬜️ Ignored (Inspect) Visit Preview Mar 8, 2025 10:55pm
entropy-debugger ⬜️ Ignored (Inspect) Visit Preview Mar 8, 2025 10:55pm
insights ⬜️ Ignored (Inspect) Visit Preview Mar 8, 2025 10:55pm


// This method is expected to be implemented by the consumer to handle the price updates.
// It will be called by _pulseCallback after _pulseCallback ensures that the call is
// indeed from Pulse contract.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that if this is an external method, then anyone could have called this function with a sequenceNumber and a list of PriceFeeds (which are already parsed, and thus mockable).

Fix here ensures that only the Pulse contract can invoke the callback.

) external view returns (uint128 feeAmount);

function getAccruedFees() external view returns (uint128 accruedFeesInWei);
function getAccruedPythFees()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(renamed this for clarity)

@@ -85,21 +87,20 @@ abstract contract Pulse is IPulse, PulseState {
req.requester = msg.sender;
req.numPriceIds = uint8(priceIds.length);
req.provider = provider;
req.fee = SafeCast.toUint128(msg.value - _state.pythFeeInWei);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Store the fee in the request so we can credit the provider who actually fulfilled the request in executeCallback

SafeCast.toUint64(req.publishTime),
SafeCast.toUint64(req.publishTime)
);
// TODO: should this use parsePriceFeedUpdatesUnique? also, do we need to add 1 to maxPublishTime?
Copy link
Contributor Author

@jayantk jayantk Mar 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the tests mock the parsePriceFeedUpdates call, so we don't actually know if it's being invoked correctly. (it was definitely missing the value parameter)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using parsePriceFeedUpdatesUnique here is a good idea due to its deterministic behavior

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I echo what Daniel says here. No you don't need to add 1 to it.

@@ -111,7 +112,7 @@ abstract contract Pulse is IPulse, PulseState {
block.timestamp < req.publishTime + _state.exclusivityPeriodSeconds
) {
require(
msg.sender == req.provider,
providerToCredit == req.provider,
Copy link
Contributor Author

@jayantk jayantk Mar 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the provider key is a cold key that shouldn't be used in hot actions. (This is why we have the feeManager as the hot key that has limited permissions -- if it is compromised, the provider can rotate it without too much damage)

Instead, make this method permissionless. Only the requested provider is motivated to call it during the exclusivity period as that's the only person who can earn the fees for the request.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a check to make sure the provider is registered?

require(
msg.sender == provider ||
msg.sender == _state.providers[provider].feeManager,
"Only provider or fee manager can invoke this method"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fee manager should be allowed to set fees

@@ -30,7 +52,8 @@ interface IPulse is PulseEvents {
* the fee estimation unreliable.
*/
function requestPriceUpdatesWithCallback(
uint256 publishTime,
address provider,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like people choosing their providers. It's an extra hop for integrators to think about (what are providers? how they are different? how should i choose? ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agreed. I think the thing to do is to add a wrapper method that chooses for users. I'm not exactly sure what the right way to choose is yet though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well if people are gonna use the wrapper it shouldn't be public right? (I recommend finalizing what people are gonna use)

@jayantk jayantk merged commit 92a1737 into main Mar 11, 2025
11 checks passed
@jayantk jayantk deleted the pulse branch March 11, 2025 18:42
Copy link
Collaborator

@ali-behjati ali-behjati left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR had a lot of small high context changes. I generally recommend separating bug fixes vs other changes and specially the API changes.

@@ -30,7 +52,8 @@ interface IPulse is PulseEvents {
* the fee estimation unreliable.
*/
function requestPriceUpdatesWithCallback(
uint256 publishTime,
address provider,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well if people are gonna use the wrapper it shouldn't be public right? (I recommend finalizing what people are gonna use)

Comment on lines +15 to +17
// TODO: this is going to absolutely explode gas costs. Need to do something smarter here.
// possible solution is to hash the price ids and store the hash instead.
// The ids themselves can be retrieved from the event.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided to move away from event because that adds a lot of complexity. We can come back to it if the cost is substantial (and the costs generally are getting lower on ETH which is the only chain that has high costs on these things)

Comment on lines +229 to +231
// Note: The provider needs to set its fees to include the fee charged by the Pyth contract.
// Ideally, we would be able to automatically compute the pyth fees from the priceIds, but the
// fee computation on IPyth assumes it has the full updated data.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't you at least set it a constant for the actual contract? Now the provider fees are getting complicated.

Comment on lines +143 to +144
// TODO: if this effect occurs here, we need to guarantee that executeCallback can never revert.
// If executeCallback can revert, then funds can be permanently locked in the contract.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's wrong with just considering them as Pyth fee? I think we mention the risks in the interface.

SafeCast.toUint64(req.publishTime),
SafeCast.toUint64(req.publishTime)
);
// TODO: should this use parsePriceFeedUpdatesUnique? also, do we need to add 1 to maxPublishTime?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I echo what Daniel says here. No you don't need to add 1 to it.

_state.accruedFeesInWei += _state.pythFeeInWei;

emit PriceUpdateRequested(req, priceIds);
}

// TODO: does this need to be payable? Any cost paid to Pyth could be taken out of the provider's accrued fees.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call no it's not needed.

require(
_state.providers[provider].isRegistered,
"Provider not registered"
);

// FIXME: this comment is wrong. (we're not using tx.gasprice)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the wrong comment then?

Comment on lines +206 to +209
// Safe cast because this is a unix timestamp in seconds.
publishTimes[i] = SafeCast.toUint64(
priceFeeds[i].price.publishTime
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure it helps with costs, afaik event arguments will end up as u256 regardless.

@@ -111,7 +112,7 @@ abstract contract Pulse is IPulse, PulseState {
block.timestamp < req.publishTime + _state.exclusivityPeriodSeconds
) {
require(
msg.sender == req.provider,
providerToCredit == req.provider,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a check to make sure the provider is registered?


clearRequest(sequenceNumber);

// 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.
// a better solution would be a doubly-linked list of active requests.
// After successful callback, update firstUnfulfilledSeq if needed
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah true. let's do some gas benchmarking later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants