Skip to content

feat(pulse): add provider #2279

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 9 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions target_chains/ethereum/contracts/contracts/pulse/IPulse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import "./PulseState.sol";
interface IPulseConsumer {
function pulseCallback(
uint64 sequenceNumber,
address updater,
PythStructs.PriceFeed[] memory priceFeeds
) external;
}
Expand Down Expand Up @@ -74,8 +73,23 @@ interface IPulse is PulseEvents {
uint64 sequenceNumber
) external view returns (PulseState.Request memory req);

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

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

function registerProvider(uint128 feeInWei) external;

function setProviderFee(uint128 newFeeInWei) external;

function getProviderInfo(
address provider
) external view returns (PulseState.ProviderInfo memory);

function getDefaultProvider() external view returns (address);

function setDefaultProvider(address provider) external;

function setExclusivityPeriod(uint256 periodSeconds) external;

function getExclusivityPeriod() external view returns (uint256);
}
141 changes: 118 additions & 23 deletions target_chains/ethereum/contracts/contracts/pulse/Pulse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,30 @@ abstract contract Pulse is IPulse, PulseState {
address admin,
uint128 pythFeeInWei,
address pythAddress,
bool prefillRequestStorage
address defaultProvider,
bool prefillRequestStorage,
uint256 exclusivityPeriodSeconds
) internal {
require(admin != address(0), "admin is zero address");
require(pythAddress != address(0), "pyth is zero address");
require(
defaultProvider != address(0),
"defaultProvider is zero address"
);

_state.admin = admin;
_state.accruedFeesInWei = 0;
_state.pythFeeInWei = pythFeeInWei;
_state.pyth = pythAddress;
_state.currentSequenceNumber = 1;

// Two-step initialization process:
// 1. Set the default provider address here
// 2. Provider must call registerProvider() in a separate transaction to set their fee
// This ensures the provider maintains control over their own fee settings
_state.defaultProvider = defaultProvider;
_state.exclusivityPeriodSeconds = exclusivityPeriodSeconds;

if (prefillRequestStorage) {
for (uint8 i = 0; i < NUM_REQUESTS; i++) {
Request storage req = _state.requests[i];
Expand All @@ -45,6 +58,12 @@ abstract contract Pulse is IPulse, PulseState {
bytes32[] calldata priceIds,
uint256 callbackGasLimit
) external payable override returns (uint64 requestSequenceNumber) {
address provider = _state.defaultProvider;
require(
_state.providers[provider].isRegistered,
"Provider not registered"
);

// NOTE: The 60-second future limit on publishTime prevents a DoS vector where
// attackers could submit many low-fee requests for far-future updates when gas prices
// are low, forcing executors to fulfill them later when gas prices might be much higher.
Expand All @@ -65,13 +84,17 @@ abstract contract Pulse is IPulse, PulseState {
req.callbackGasLimit = callbackGasLimit;
req.requester = msg.sender;
req.numPriceIds = uint8(priceIds.length);
req.provider = provider;

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

_state.accruedFeesInWei += SafeCast.toUint128(msg.value);
_state.providers[provider].accruedFeesInWei += SafeCast.toUint128(
msg.value - _state.pythFeeInWei
);
_state.accruedFeesInWei += _state.pythFeeInWei;

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

// Check provider exclusivity using configurable period
if (
block.timestamp < req.publishTime + _state.exclusivityPeriodSeconds
) {
require(
msg.sender == req.provider,
"Only assigned provider during exclusivity period"
);
}

// Verify priceIds match
require(
priceIds.length == req.numPriceIds,
Expand All @@ -105,19 +138,10 @@ abstract contract Pulse is IPulse, PulseState {

clearRequest(sequenceNumber);

// Check if enough gas remains for callback + events/cleanup
// We need extra gas beyond callbackGasLimit for:
// 1. Emitting success/failure events
// 2. Error handling in catch blocks
// 3. State cleanup operations
if (gasleft() < (req.callbackGasLimit * 3) / 2) {
revert InsufficientGas();
}

Comment on lines -108 to -116
Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed this because if insufficient gas error occurs within callback then PriceUpdateCallbackFailed will be emitted with "low-level error (possibly out of gas)" and otherwise if insufficient gas for the executeCallback function itself, it will revert with out-of-gas

try
IPulseConsumer(req.requester).pulseCallback{
gas: req.callbackGasLimit
}(sequenceNumber, msg.sender, priceFeeds)
}(sequenceNumber, priceFeeds)
{
// Callback succeeded
emitPriceUpdate(sequenceNumber, priceIds, priceFeeds);
Expand Down Expand Up @@ -173,9 +197,12 @@ abstract contract Pulse is IPulse, PulseState {
function getFee(
uint256 callbackGasLimit
) public view override returns (uint128 feeAmount) {
uint128 baseFee = _state.pythFeeInWei;
uint256 gasFee = callbackGasLimit * tx.gasprice;
feeAmount = baseFee + SafeCast.toUint128(gasFee);
uint128 baseFee = _state.pythFeeInWei; // Fixed fee to Pyth
uint128 providerFeeInWei = _state
.providers[_state.defaultProvider]
.feeInWei; // Provider's per-gas rate
uint256 gasFee = callbackGasLimit * providerFeeInWei; // Total provider fee based on gas
feeAmount = baseFee + SafeCast.toUint128(gasFee); // Total fee user needs to pay
}

function getPythFeeInWei()
Expand Down Expand Up @@ -271,21 +298,89 @@ abstract contract Pulse is IPulse, PulseState {
}

function setFeeManager(address manager) external override {
require(msg.sender == _state.admin, "Only admin can set fee manager");
address oldFeeManager = _state.feeManager;
_state.feeManager = manager;
emit FeeManagerUpdated(_state.admin, oldFeeManager, manager);
require(
_state.providers[msg.sender].isRegistered,
"Provider not registered"
);
address oldFeeManager = _state.providers[msg.sender].feeManager;
_state.providers[msg.sender].feeManager = manager;
emit FeeManagerUpdated(msg.sender, oldFeeManager, manager);
}

function withdrawAsFeeManager(uint128 amount) external override {
require(msg.sender == _state.feeManager, "Only fee manager");
require(_state.accruedFeesInWei >= amount, "Insufficient balance");
function withdrawAsFeeManager(
address provider,
uint128 amount
) external override {
require(
msg.sender == _state.providers[provider].feeManager,
"Only fee manager"
);
require(
_state.providers[provider].accruedFeesInWei >= amount,
"Insufficient balance"
);

_state.accruedFeesInWei -= amount;
_state.providers[provider].accruedFeesInWei -= amount;

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

emit FeesWithdrawn(msg.sender, amount);
}

function registerProvider(uint128 feeInWei) external override {
ProviderInfo storage provider = _state.providers[msg.sender];
require(!provider.isRegistered, "Provider already registered");
provider.feeInWei = feeInWei;
provider.isRegistered = true;
emit ProviderRegistered(msg.sender, feeInWei);
}

function setProviderFee(uint128 newFeeInWei) external override {
require(
_state.providers[msg.sender].isRegistered,
"Provider not registered"
);
uint128 oldFee = _state.providers[msg.sender].feeInWei;
_state.providers[msg.sender].feeInWei = newFeeInWei;
emit ProviderFeeUpdated(msg.sender, oldFee, newFeeInWei);
}

function getProviderInfo(
address provider
) external view override returns (ProviderInfo memory) {
return _state.providers[provider];
}

function getDefaultProvider() external view override returns (address) {
return _state.defaultProvider;
}

function setDefaultProvider(address provider) external override {
require(
msg.sender == _state.admin,
"Only admin can set default provider"
);
require(
_state.providers[provider].isRegistered,
"Provider not registered"
);
address oldProvider = _state.defaultProvider;
_state.defaultProvider = provider;
emit DefaultProviderUpdated(oldProvider, provider);
}

function setExclusivityPeriod(uint256 periodSeconds) external override {
require(
msg.sender == _state.admin,
"Only admin can set exclusivity period"
);
uint256 oldPeriod = _state.exclusivityPeriodSeconds;
_state.exclusivityPeriodSeconds = periodSeconds;
emit ExclusivityPeriodUpdated(oldPeriod, periodSeconds);
}

function getExclusivityPeriod() external view override returns (uint256) {
return _state.exclusivityPeriodSeconds;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ error CallbackFailed();
error InvalidPriceIds(bytes32 providedPriceIdsHash, bytes32 storedPriceIdsHash);
error InvalidCallbackGasLimit(uint256 requested, uint256 stored);
error ExceedsMaxPrices(uint32 requested, uint32 maxAllowed);
error InsufficientGas();
error TooManyPriceIds(uint256 provided, uint256 maximum);
17 changes: 15 additions & 2 deletions target_chains/ethereum/contracts/contracts/pulse/PulseEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface PulseEvents {

event PriceUpdateExecuted(
uint64 indexed sequenceNumber,
address indexed updater,
address indexed provider,
bytes32[] priceIds,
int64[] prices,
uint64[] conf,
Expand All @@ -20,7 +20,7 @@ interface PulseEvents {

event PriceUpdateCallbackFailed(
uint64 indexed sequenceNumber,
address indexed updater,
address indexed provider,
bytes32[] priceIds,
address requester,
string reason
Expand All @@ -31,4 +31,17 @@ interface PulseEvents {
address oldFeeManager,
address newFeeManager
);

event ProviderRegistered(address indexed provider, uint128 feeInWei);
event ProviderFeeUpdated(
address indexed provider,
uint128 oldFee,
uint128 newFee
);
event DefaultProviderUpdated(address oldProvider, address newProvider);

event ExclusivityPeriodUpdated(
uint256 oldPeriodSeconds,
uint256 newPeriodSeconds
);
}
12 changes: 11 additions & 1 deletion target_chains/ethereum/contracts/contracts/pulse/PulseState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ contract PulseState {
uint8 numPriceIds; // Actual number of price IDs used
uint256 callbackGasLimit;
address requester;
address provider;
}

struct ProviderInfo {
uint128 feeInWei;
uint128 accruedFeesInWei;
address feeManager;
bool isRegistered;
}

struct State {
Expand All @@ -24,9 +32,11 @@ contract PulseState {
uint128 accruedFeesInWei;
address pyth;
uint64 currentSequenceNumber;
address feeManager;
address defaultProvider;
uint256 exclusivityPeriodSeconds;
Request[NUM_REQUESTS] requests;
mapping(bytes32 => Request) requestsOverflow;
mapping(address => ProviderInfo) providers;
}

State internal _state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ contract PulseUpgradeable is
address admin,
uint128 pythFeeInWei,
address pythAddress,
bool prefillRequestStorage
) public initializer {
address defaultProvider,
bool prefillRequestStorage,
uint256 exclusivityPeriodSeconds
) external initializer {
require(owner != address(0), "owner is zero address");
require(admin != address(0), "admin is zero address");

Expand All @@ -35,7 +37,9 @@ contract PulseUpgradeable is
admin,
pythFeeInWei,
pythAddress,
prefillRequestStorage
defaultProvider,
prefillRequestStorage,
exclusivityPeriodSeconds
);

_transferOwnership(owner);
Expand Down
Loading
Loading