Skip to content
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

feat(pulse): add provider #2279

Merged
merged 9 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 19 additions & 5 deletions target_chains/ethereum/contracts/contracts/pulse/IPulse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import "./PulseState.sol";
interface IPulseConsumer {
function pulseCallback(
uint64 sequenceNumber,
address updater,
address provider,
cctdaniel marked this conversation as resolved.
Show resolved Hide resolved
PythStructs.PriceFeed[] memory priceFeeds
) external;
}
Expand All @@ -33,7 +33,8 @@ interface IPulse is PulseEvents {
function requestPriceUpdatesWithCallback(
uint256 publishTime,
bytes32[] calldata priceIds,
uint256 callbackGasLimit
uint256 callbackGasLimit,
address provider
) external payable returns (uint64 sequenceNumber);

/**
Expand Down Expand Up @@ -62,10 +63,12 @@ interface IPulse is PulseEvents {
* @notice Calculates the total fee required for a price update request
* @dev Total fee = base Pyth protocol fee + gas costs for callback
* @param callbackGasLimit The amount of gas allocated for callback execution
* @param provider The provider to use for the fee calculation
* @return feeAmount The total fee in wei that must be provided as msg.value
*/
function getFee(
uint256 callbackGasLimit
uint256 callbackGasLimit,
address provider
) external view returns (uint128 feeAmount);

function getAccruedFees() external view returns (uint128 accruedFeesInWei);
Expand All @@ -74,8 +77,19 @@ 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;
}
104 changes: 91 additions & 13 deletions target_chains/ethereum/contracts/contracts/pulse/Pulse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@ abstract contract Pulse is IPulse, PulseState {
address admin,
cctdaniel marked this conversation as resolved.
Show resolved Hide resolved
uint128 pythFeeInWei,
address pythAddress,
address defaultProvider,
bool prefillRequestStorage
) 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;
_state.defaultProvider = defaultProvider;

if (prefillRequestStorage) {
for (uint8 i = 0; i < NUM_REQUESTS; i++) {
Expand All @@ -43,8 +49,17 @@ abstract contract Pulse is IPulse, PulseState {
function requestPriceUpdatesWithCallback(
uint256 publishTime,
bytes32[] calldata priceIds,
uint256 callbackGasLimit
uint256 callbackGasLimit,
address provider
) external payable override returns (uint64 requestSequenceNumber) {
if (provider == address(0)) {
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 @@ -56,7 +71,7 @@ abstract contract Pulse is IPulse, PulseState {
}
requestSequenceNumber = _state.currentSequenceNumber++;

uint128 requiredFee = getFee(callbackGasLimit);
uint128 requiredFee = getFee(callbackGasLimit, provider);
if (msg.value < requiredFee) revert InsufficientFee();

Request storage req = allocRequest(requestSequenceNumber);
Expand All @@ -65,13 +80,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 Down Expand Up @@ -171,10 +190,15 @@ abstract contract Pulse is IPulse, PulseState {
}

function getFee(
uint256 callbackGasLimit
uint256 callbackGasLimit,
address provider
) public view override returns (uint128 feeAmount) {
if (provider == address(0)) {
provider = _state.defaultProvider;
}
uint128 baseFee = _state.pythFeeInWei;
uint256 gasFee = callbackGasLimit * tx.gasprice;
uint128 providerFeeInWei = _state.providers[provider].feeInWei;
uint256 gasFee = callbackGasLimit * providerFeeInWei;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

as discussed, changed this to use provider fee to protect from future price jumps/drops

cctdaniel marked this conversation as resolved.
Show resolved Hide resolved
feeAmount = baseFee + SafeCast.toUint128(gasFee);
}

Expand Down Expand Up @@ -271,21 +295,75 @@ 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);
}
}
12 changes: 10 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,12 @@ 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);
}
11 changes: 10 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,10 @@ contract PulseState {
uint128 accruedFeesInWei;
address pyth;
uint64 currentSequenceNumber;
address feeManager;
address defaultProvider;
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,6 +23,7 @@ contract PulseUpgradeable is
address admin,
uint128 pythFeeInWei,
address pythAddress,
address defaultProvider,
bool prefillRequestStorage
) public initializer {
require(owner != address(0), "owner is zero address");
Expand All @@ -35,6 +36,7 @@ contract PulseUpgradeable is
admin,
pythFeeInWei,
pythAddress,
defaultProvider,
prefillRequestStorage
);

Expand Down
Loading
Loading