Skip to content

Commit

Permalink
feat(pulse): add provider (#2279)
Browse files Browse the repository at this point in the history
* add provider

* remove unnecessary code

* fix test

* add exclusivity period to provider (#2282)

* make exclusivityPeriodSeconds configurable

* remove provider arg from getFee

* remove provider from callback args

* add comments

* add comments
  • Loading branch information
cctdaniel authored Jan 24, 2025
1 parent c2716a2 commit 93fbbaa
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 108 deletions.
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();
}

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

0 comments on commit 93fbbaa

Please sign in to comment.