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): Pulse Gas Benchmark #2467

Merged
merged 7 commits into from
Mar 14, 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
2 changes: 1 addition & 1 deletion apps/argus/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

158 changes: 34 additions & 124 deletions target_chains/ethereum/contracts/forge-test/Pulse.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "./utils/PulseTestUtils.t.sol";
import "../contracts/pulse/PulseUpgradeable.sol";
import "../contracts/pulse/IPulse.sol";
import "../contracts/pulse/PulseState.sol";
Expand Down Expand Up @@ -84,7 +85,7 @@ contract CustomErrorPulseConsumer is IPulseConsumer {
}

// FIXME: this shouldn't be IPulseConsumer.
contract PulseTest is Test, PulseEvents, IPulseConsumer {
contract PulseTest is Test, PulseEvents, IPulseConsumer, PulseTestUtils {
ERC1967Proxy public proxy;
PulseUpgradeable public pulse;
MockPulseConsumer public consumer;
Expand All @@ -97,20 +98,6 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
uint128 constant DEFAULT_PROVIDER_FEE_PER_GAS = 1 wei;
uint128 constant DEFAULT_PROVIDER_BASE_FEE = 1 wei;
uint128 constant DEFAULT_PROVIDER_FEE_PER_FEED = 10 wei;
uint constant MOCK_PYTH_FEE_PER_FEED = 10 wei;

uint128 constant CALLBACK_GAS_LIMIT = 1_000_000;
bytes32 constant BTC_PRICE_FEED_ID =
0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43;
bytes32 constant ETH_PRICE_FEED_ID =
0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace;

// Price feed constants
int8 constant MOCK_PRICE_FEED_EXPO = -8;
int64 constant MOCK_BTC_PRICE = 5_000_000_000_000; // $50,000
int64 constant MOCK_ETH_PRICE = 300_000_000_000; // $3,000
uint64 constant MOCK_BTC_CONF = 10_000_000_000; // $100
uint64 constant MOCK_ETH_CONF = 5_000_000_000; // $50

function setUp() public {
owner = address(1);
Expand Down Expand Up @@ -139,102 +126,13 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
consumer = new MockPulseConsumer(address(proxy));
}

// Helper function to create price IDs array
function createPriceIds() internal pure returns (bytes32[] memory) {
bytes32[] memory priceIds = new bytes32[](2);
priceIds[0] = BTC_PRICE_FEED_ID;
priceIds[1] = ETH_PRICE_FEED_ID;
return priceIds;
}

// Helper function to create mock price feeds
function createMockPriceFeeds(
uint256 publishTime
) internal pure returns (PythStructs.PriceFeed[] memory) {
PythStructs.PriceFeed[] memory priceFeeds = new PythStructs.PriceFeed[](
2
);

priceFeeds[0].id = BTC_PRICE_FEED_ID;
priceFeeds[0].price.price = MOCK_BTC_PRICE;
priceFeeds[0].price.conf = MOCK_BTC_CONF;
priceFeeds[0].price.expo = MOCK_PRICE_FEED_EXPO;
priceFeeds[0].price.publishTime = publishTime;

priceFeeds[1].id = ETH_PRICE_FEED_ID;
priceFeeds[1].price.price = MOCK_ETH_PRICE;
priceFeeds[1].price.conf = MOCK_ETH_CONF;
priceFeeds[1].price.expo = MOCK_PRICE_FEED_EXPO;
priceFeeds[1].price.publishTime = publishTime;

return priceFeeds;
}

// Helper function to mock Pyth response
function mockParsePriceFeedUpdates(
PythStructs.PriceFeed[] memory priceFeeds
) internal {
uint expectedFee = MOCK_PYTH_FEE_PER_FEED * priceFeeds.length;

vm.mockCall(
address(pyth),
abi.encodeWithSelector(IPyth.getUpdateFee.selector),
abi.encode(expectedFee)
);

vm.mockCall(
address(pyth),
expectedFee,
abi.encodeWithSelector(IPyth.parsePriceFeedUpdates.selector),
abi.encode(priceFeeds)
);
}

// Helper function to create mock update data
function createMockUpdateData(
PythStructs.PriceFeed[] memory priceFeeds
) internal pure returns (bytes[] memory) {
bytes[] memory updateData = new bytes[](2);
updateData[0] = abi.encode(priceFeeds[0]);
updateData[1] = abi.encode(priceFeeds[1]);
return updateData;
}

// Helper function to calculate total fee
// FIXME: I think this helper probably needs to take some arguments.
function calculateTotalFee() internal view returns (uint128) {
return
pulse.getFee(defaultProvider, CALLBACK_GAS_LIMIT, createPriceIds());
}

// Helper function to setup consumer request
function setupConsumerRequest(
address consumerAddress
)
internal
returns (
uint64 sequenceNumber,
bytes32[] memory priceIds,
uint64 publishTime
)
{
priceIds = createPriceIds();
publishTime = SafeCast.toUint64(block.timestamp);
vm.deal(consumerAddress, 1 gwei);

uint128 totalFee = calculateTotalFee();

vm.prank(consumerAddress);
sequenceNumber = pulse.requestPriceUpdatesWithCallback{value: totalFee}(
defaultProvider,
publishTime,
priceIds,
CALLBACK_GAS_LIMIT
);

return (sequenceNumber, priceIds, publishTime);
}

function testRequestPriceUpdate() public {
// Set a realistic gas price
vm.txGasPrice(30 gwei);
Expand Down Expand Up @@ -334,7 +232,7 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
publishTime
);
// FIXME: this test doesn't ensure the Pyth fee is paid.
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);

// Create arrays for expected event data
int64[] memory expectedPrices = new int64[](2);
Expand Down Expand Up @@ -405,12 +303,16 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
uint64 sequenceNumber,
bytes32[] memory priceIds,
uint256 publishTime
) = setupConsumerRequest(address(failingConsumer));
) = setupConsumerRequest(
pulse,
defaultProvider,
address(failingConsumer)
);

PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
publishTime
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.expectEmit();
Expand Down Expand Up @@ -440,12 +342,16 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
uint64 sequenceNumber,
bytes32[] memory priceIds,
uint256 publishTime
) = setupConsumerRequest(address(failingConsumer));
) = setupConsumerRequest(
pulse,
defaultProvider,
address(failingConsumer)
);

PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
publishTime
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.expectEmit();
Expand All @@ -472,13 +378,13 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
uint64 sequenceNumber,
bytes32[] memory priceIds,
uint256 publishTime
) = setupConsumerRequest(address(consumer));
) = setupConsumerRequest(pulse, defaultProvider, address(consumer));

// Setup mock data
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
publishTime
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Try executing with only 100K gas when 1M is required
Expand Down Expand Up @@ -508,7 +414,7 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
futureTime // Mock price feeds with future timestamp
);
mockParsePriceFeedUpdates(priceFeeds); // This will make parsePriceFeedUpdates return future-dated prices
mockParsePriceFeedUpdates(pyth, priceFeeds); // This will make parsePriceFeedUpdates return future-dated prices
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.prank(defaultProvider);
Expand Down Expand Up @@ -555,12 +461,12 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
uint64 sequenceNumber,
bytes32[] memory priceIds,
uint256 publishTime
) = setupConsumerRequest(address(consumer));
) = setupConsumerRequest(pulse, defaultProvider, address(consumer));

PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
publishTime
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// First execution
Expand Down Expand Up @@ -747,7 +653,11 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
uint256 publishTime = block.timestamp;

// Setup request
(uint64 sequenceNumber, , ) = setupConsumerRequest(address(consumer));
(uint64 sequenceNumber, , ) = setupConsumerRequest(
pulse,
defaultProvider,
address(consumer)
);

// Create different priceIds
bytes32[] memory wrongPriceIds = new bytes32[](2);
Expand All @@ -757,7 +667,7 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
publishTime
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Should revert when trying to execute with wrong priceIds
Expand Down Expand Up @@ -923,13 +833,13 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
uint64 sequenceNumber,
bytes32[] memory priceIds,
uint256 publishTime
) = setupConsumerRequest(address(consumer));
) = setupConsumerRequest(pulse, defaultProvider, address(consumer));

// Setup mock data
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
publishTime
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Try to execute with second provider during exclusivity period
Expand Down Expand Up @@ -965,13 +875,13 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
uint64 sequenceNumber,
bytes32[] memory priceIds,
uint256 publishTime
) = setupConsumerRequest(address(consumer));
) = setupConsumerRequest(pulse, defaultProvider, address(consumer));

// Setup mock data
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
publishTime
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Wait for exclusivity period to end
Expand Down Expand Up @@ -1006,13 +916,13 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
uint64 sequenceNumber,
bytes32[] memory priceIds,
uint256 publishTime
) = setupConsumerRequest(address(consumer));
) = setupConsumerRequest(pulse, defaultProvider, address(consumer));

// Setup mock data
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
publishTime
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Try at 29 seconds (should fail for second provider)
Expand Down Expand Up @@ -1080,7 +990,7 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
SafeCast.toUint64(block.timestamp)
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
updateData = createMockUpdateData(priceFeeds);

vm.deal(defaultProvider, 2 ether); // Increase ETH allocation to prevent OutOfFunds
Expand Down Expand Up @@ -1208,7 +1118,7 @@ contract PulseTest is Test, PulseEvents, IPulseConsumer {
PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
publishTime
);
mockParsePriceFeedUpdates(priceFeeds);
mockParsePriceFeedUpdates(pyth, priceFeeds);
bytes[] memory updateData = createMockUpdateData(priceFeeds);

// Create 20 requests with some gaps
Expand Down
Loading
Loading