[evm] parsePriceFeed with uniqueness validation (#1089)

* Implement uniqueness
* Add tests and update abi
* Fix MockPyth for the unique version
* Bump version
* Add gas benchmark functions
This commit is contained in:
Amin Moghaddam 2023-10-13 14:15:42 +02:00 committed by GitHub
parent d11216f309
commit eb9526675c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 538 additions and 39 deletions

4
package-lock.json generated
View File

@ -58545,7 +58545,7 @@
},
"target_chains/ethereum/contracts": {
"name": "@pythnetwork/pyth-evm-contract",
"version": "1.3.1",
"version": "1.4.0",
"license": "ISC",
"dependencies": {
"@certusone/wormhole-sdk": "^0.9.22",
@ -59572,7 +59572,7 @@
},
"target_chains/ethereum/sdk/solidity": {
"name": "@pythnetwork/pyth-sdk-solidity",
"version": "2.2.1",
"version": "2.3.0",
"license": "Apache-2.0",
"devDependencies": {
"prettier": "^2.7.1",

View File

@ -440,17 +440,11 @@ abstract contract Pyth is
if (!verifyPythVM(vm)) revert PythErrors.InvalidUpdateDataSource();
}
function parsePriceFeedUpdates(
function parsePriceFeedUpdatesInternal(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
uint64 maxPublishTime
)
external
payable
override
returns (PythStructs.PriceFeed[] memory priceFeeds)
{
PythInternalStructs.ParseConfig memory config
) internal returns (PythStructs.PriceFeed[] memory priceFeeds) {
{
uint requiredFee = getUpdateFee(updateData);
if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
@ -494,10 +488,12 @@ abstract contract Pyth is
for (uint j = 0; j < numUpdates; j++) {
PythInternalStructs.PriceInfo memory info;
bytes32 priceId;
uint64 prevPublishTime;
(
offset,
info,
priceId
priceId,
prevPublishTime
) = extractPriceInfoFromMerkleProof(
digest,
encoded,
@ -519,8 +515,10 @@ abstract contract Pyth is
// If is not, default id value of 0 will still be set and
// this will allow other updates for this price id to be processed.
if (
publishTime >= minPublishTime &&
publishTime <= maxPublishTime
publishTime >= config.minPublishTime &&
publishTime <= config.maxPublishTime &&
(!config.checkUniqueness ||
config.minPublishTime > prevPublishTime)
) {
fillPriceFeedFromPriceInfo(
priceFeeds,
@ -592,8 +590,9 @@ abstract contract Pyth is
// If is not, default id value of 0 will still be set and
// this will allow other updates for this price id to be processed.
if (
publishTime >= minPublishTime &&
publishTime <= maxPublishTime
publishTime >= config.minPublishTime &&
publishTime <= config.maxPublishTime &&
!config.checkUniqueness // do not allow batch updates to be used by parsePriceFeedUpdatesUnique
) {
fillPriceFeedFromPriceInfo(
priceFeeds,
@ -617,6 +616,52 @@ abstract contract Pyth is
}
}
function parsePriceFeedUpdates(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
uint64 maxPublishTime
)
external
payable
override
returns (PythStructs.PriceFeed[] memory priceFeeds)
{
return
parsePriceFeedUpdatesInternal(
updateData,
priceIds,
PythInternalStructs.ParseConfig(
minPublishTime,
maxPublishTime,
false
)
);
}
function parsePriceFeedUpdatesUnique(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
uint64 maxPublishTime
)
external
payable
override
returns (PythStructs.PriceFeed[] memory priceFeeds)
{
return
parsePriceFeedUpdatesInternal(
updateData,
priceIds,
PythInternalStructs.ParseConfig(
minPublishTime,
maxPublishTime,
true
)
);
}
function getTotalFee(
uint totalNumUpdates
) private view returns (uint requiredFee) {
@ -682,6 +727,6 @@ abstract contract Pyth is
}
function version() public pure returns (string memory) {
return "1.3.1";
return "1.3.2";
}
}

View File

@ -224,7 +224,8 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
returns (
uint endOffset,
PythInternalStructs.PriceInfo memory priceInfo,
bytes32 priceId
bytes32 priceId,
uint64 prevPublishTime
)
{
unchecked {
@ -257,12 +258,15 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
UnsafeCalldataBytesLib.toUint8(encodedMessage, 0)
);
if (messageType == MessageType.PriceFeed) {
(priceInfo, priceId) = parsePriceFeedMessage(encodedMessage, 1);
(priceInfo, priceId, prevPublishTime) = parsePriceFeedMessage(
encodedMessage,
1
);
} else {
revert PythErrors.InvalidUpdateData();
}
return (endOffset, priceInfo, priceId);
return (endOffset, priceInfo, priceId, prevPublishTime);
}
}
@ -274,7 +278,8 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
pure
returns (
PythInternalStructs.PriceInfo memory priceInfo,
bytes32 priceId
bytes32 priceId,
uint64 prevPublishTime
)
{
unchecked {
@ -311,7 +316,7 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
offset += 8;
// We do not store this field because it is not used on the latest feed queries.
// uint64 prevPublishTime = UnsafeBytesLib.toUint64(encodedPriceFeed, offset);
prevPublishTime = UnsafeBytesLib.toUint64(encodedPriceFeed, offset);
offset += 8;
priceInfo.emaPrice = int64(
@ -359,11 +364,13 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
for (uint i = 0; i < numUpdates; i++) {
PythInternalStructs.PriceInfo memory priceInfo;
bytes32 priceId;
(offset, priceInfo, priceId) = extractPriceInfoFromMerkleProof(
digest,
encoded,
offset
);
uint64 prevPublishTime;
(
offset,
priceInfo,
priceId,
prevPublishTime
) = extractPriceInfoFromMerkleProof(digest, encoded, offset);
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
if (priceInfo.publishTime > latestPublishTime) {
setLatestPriceInfo(priceId, priceInfo);

View File

@ -9,6 +9,12 @@ import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
contract PythInternalStructs {
using BytesLib for bytes;
struct ParseConfig {
uint64 minPublishTime;
uint64 maxPublishTime;
bool checkUniqueness;
}
struct PriceInfo {
// slot 1
uint64 publishTime;

View File

@ -65,7 +65,7 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
}
for (uint i = 0; i < NUM_PRICES; ++i) {
uint64 publishTime = uint64(getRand() % 10);
uint64 publishTime = uint64(getRand() % 10) + 1; // to make sure prevPublishTime is >= 0
cachedPrices.push(
PythStructs.Price(
@ -274,6 +274,37 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
);
}
function testBenchmarkParsePriceFeedUpdatesUniqueForWhMerkle() public {
bytes32[] memory ids = new bytes32[](1);
ids[0] = priceIds[0];
pyth.parsePriceFeedUpdatesUnique{
value: freshPricesWhMerkleUpdateFee[0]
}(
freshPricesWhMerkleUpdateData[0],
ids,
uint64(freshPrices[0].publishTime),
100
);
}
function testBenchmarkParsePriceFeedUpdatesUniqueWhMerkleForOnePriceFeedNotWithinRange()
public
{
bytes32[] memory ids = new bytes32[](1);
ids[0] = priceIds[0];
vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
pyth.parsePriceFeedUpdatesUnique{
value: freshPricesWhMerkleUpdateFee[0]
}(
freshPricesWhMerkleUpdateData[0],
ids,
uint64(freshPrices[0].publishTime) - 1,
100
);
}
function testBenchmarkParsePriceFeedUpdatesForWhMerkle1() public {
uint numIds = 1;

View File

@ -418,7 +418,7 @@ contract PythWormholeMerkleAccumulatorTest is
assertPriceFeedMessageStored(priceFeedMessages1[0]);
}
function testParsePriceFeedUpdatesWithWormholeMerklWorksWithOurOfOrderUpdateMultiCall()
function testParsePriceFeedUpdatesWithWormholeMerkleWorksWithOutOfOrderUpdateMultiCall()
public
{
PriceFeedMessage[]
@ -851,6 +851,53 @@ contract PythWormholeMerkleAccumulatorTest is
}
}
function testParsePriceFeedUniqueWithWormholeMerkleWorks(uint seed) public {
setRandSeed(seed);
uint numPriceFeeds = (getRandUint() % 10) + 1;
PriceFeedMessage[]
memory priceFeedMessages = generateRandomPriceFeedMessage(
numPriceFeeds
);
uint64 publishTime = getRandUint64();
bytes32[] memory priceIds = new bytes32[](1);
priceIds[0] = priceFeedMessages[0].priceId;
for (uint i = 0; i < numPriceFeeds; i++) {
priceFeedMessages[i].priceId = priceFeedMessages[0].priceId;
priceFeedMessages[i].publishTime = publishTime;
priceFeedMessages[i].prevPublishTime = publishTime;
}
uint firstUpdate = (getRandUint() % numPriceFeeds);
priceFeedMessages[firstUpdate].prevPublishTime = publishTime - 1;
(
bytes[] memory updateData,
uint updateFee
) = createWormholeMerkleUpdateData(priceFeedMessages);
vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
PythStructs.PriceFeed[] memory priceFeeds = pyth
.parsePriceFeedUpdatesUnique{value: updateFee}(
updateData,
priceIds,
publishTime - 1,
MAX_UINT64
);
priceFeeds = pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
updateData,
priceIds,
publishTime,
MAX_UINT64
);
assertEq(priceFeeds.length, 1);
assertParsedPriceFeedEqualsMessage(
priceFeeds[0],
priceFeedMessages[firstUpdate],
priceIds[0]
);
}
function testParsePriceFeedWithWormholeMerkleWorksRandomDistinctUpdatesInput(
uint seed
) public {

View File

@ -354,6 +354,9 @@ abstract contract PythTestUtils is Test, WormholeTestUtils {
priceFeedMessages[i].conf = prices[i].conf;
priceFeedMessages[i].expo = prices[i].expo;
priceFeedMessages[i].publishTime = uint64(prices[i].publishTime);
priceFeedMessages[i].prevPublishTime =
uint64(prices[i].publishTime) -
1;
priceFeedMessages[i].emaPrice = prices[i].price;
priceFeedMessages[i].emaConf = prices[i].conf;
}

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/pyth-evm-contract",
"version": "1.3.1",
"version": "1.4.0",
"description": "",
"private": "true",
"devDependencies": {
@ -25,7 +25,7 @@
"coverage": "./coverage.sh"
},
"author": "",
"license": "ISC",
"license": "Apache-2.0",
"dependencies": {
"@certusone/wormhole-sdk": "^0.9.22",
"@matterlabs/hardhat-zksync-deploy": "^0.6.2",

View File

@ -121,4 +121,16 @@ abstract contract AbstractPyth is IPyth {
virtual
override
returns (PythStructs.PriceFeed[] memory priceFeeds);
function parsePriceFeedUpdatesUnique(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
uint64 maxPublishTime
)
external
payable
virtual
override
returns (PythStructs.PriceFeed[] memory priceFeeds);
}

View File

@ -136,4 +136,23 @@ interface IPyth is IPythEvents {
uint64 minPublishTime,
uint64 maxPublishTime
) external payable returns (PythStructs.PriceFeed[] memory priceFeeds);
/// @notice Similar to `parsePriceFeedUpdates` but ensures the updates returned are
/// the first updates published in minPublishTime. That is, if there are multiple updates for a given timestamp,
/// this method will return the first update.
///
///
/// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid or there is
/// no update for any of the given `priceIds` within the given time range and uniqueness condition.
/// @param updateData Array of price update data.
/// @param priceIds Array of price ids.
/// @param minPublishTime minimum acceptable publishTime for the given `priceIds`.
/// @param maxPublishTime maximum acceptable publishTime for the given `priceIds`.
/// @return priceFeeds Array of the price feeds corresponding to the given `priceIds` (with the same order).
function parsePriceFeedUpdatesUnique(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
uint64 maxPublishTime
) external payable returns (PythStructs.PriceFeed[] memory priceFeeds);
}

View File

@ -78,12 +78,13 @@ contract MockPyth is AbstractPyth {
return singleUpdateFeeInWei * updateData.length;
}
function parsePriceFeedUpdates(
function parsePriceFeedUpdatesInternal(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
uint64 maxPublishTime
) external payable override returns (PythStructs.PriceFeed[] memory feeds) {
uint64 maxPublishTime,
bool unique
) internal returns (PythStructs.PriceFeed[] memory feeds) {
uint requiredFee = getUpdateFee(updateData);
if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
@ -91,13 +92,18 @@ contract MockPyth is AbstractPyth {
for (uint i = 0; i < priceIds.length; i++) {
for (uint j = 0; j < updateData.length; j++) {
feeds[i] = abi.decode(updateData[j], (PythStructs.PriceFeed));
uint64 prevPublishTime;
(feeds[i], prevPublishTime) = abi.decode(
updateData[j],
(PythStructs.PriceFeed, uint64)
);
if (feeds[i].id == priceIds[i]) {
uint publishTime = feeds[i].price.publishTime;
if (
minPublishTime <= publishTime &&
publishTime <= maxPublishTime
publishTime <= maxPublishTime &&
(!unique || prevPublishTime < minPublishTime)
) {
break;
} else {
@ -111,6 +117,38 @@ contract MockPyth is AbstractPyth {
}
}
function parsePriceFeedUpdates(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
uint64 maxPublishTime
) external payable override returns (PythStructs.PriceFeed[] memory feeds) {
return
parsePriceFeedUpdatesInternal(
updateData,
priceIds,
minPublishTime,
maxPublishTime,
false
);
}
function parsePriceFeedUpdatesUnique(
bytes[] calldata updateData,
bytes32[] calldata priceIds,
uint64 minPublishTime,
uint64 maxPublishTime
) external payable override returns (PythStructs.PriceFeed[] memory feeds) {
return
parsePriceFeedUpdatesInternal(
updateData,
priceIds,
minPublishTime,
maxPublishTime,
true
);
}
function createPriceFeedUpdateData(
bytes32 id,
int64 price,
@ -118,7 +156,8 @@ contract MockPyth is AbstractPyth {
int32 expo,
int64 emaPrice,
uint64 emaConf,
uint64 publishTime
uint64 publishTime,
uint64 prevPublishTime
) public pure returns (bytes memory priceFeedData) {
PythStructs.PriceFeed memory priceFeed;
@ -134,6 +173,6 @@ contract MockPyth is AbstractPyth {
priceFeed.emaPrice.expo = expo;
priceFeed.emaPrice.publishTime = publishTime;
priceFeedData = abi.encode(priceFeed);
priceFeedData = abi.encode(priceFeed, prevPublishTime);
}
}

View File

@ -447,6 +447,101 @@
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes[]",
"name": "updateData",
"type": "bytes[]"
},
{
"internalType": "bytes32[]",
"name": "priceIds",
"type": "bytes32[]"
},
{
"internalType": "uint64",
"name": "minPublishTime",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "maxPublishTime",
"type": "uint64"
}
],
"name": "parsePriceFeedUpdatesUnique",
"outputs": [
{
"components": [
{
"internalType": "bytes32",
"name": "id",
"type": "bytes32"
},
{
"components": [
{
"internalType": "int64",
"name": "price",
"type": "int64"
},
{
"internalType": "uint64",
"name": "conf",
"type": "uint64"
},
{
"internalType": "int32",
"name": "expo",
"type": "int32"
},
{
"internalType": "uint256",
"name": "publishTime",
"type": "uint256"
}
],
"internalType": "struct PythStructs.Price",
"name": "price",
"type": "tuple"
},
{
"components": [
{
"internalType": "int64",
"name": "price",
"type": "int64"
},
{
"internalType": "uint64",
"name": "conf",
"type": "uint64"
},
{
"internalType": "int32",
"name": "expo",
"type": "int32"
},
{
"internalType": "uint256",
"name": "publishTime",
"type": "uint256"
}
],
"internalType": "struct PythStructs.Price",
"name": "emaPrice",
"type": "tuple"
}
],
"internalType": "struct PythStructs.PriceFeed[]",
"name": "priceFeeds",
"type": "tuple[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{

View File

@ -432,6 +432,101 @@
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes[]",
"name": "updateData",
"type": "bytes[]"
},
{
"internalType": "bytes32[]",
"name": "priceIds",
"type": "bytes32[]"
},
{
"internalType": "uint64",
"name": "minPublishTime",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "maxPublishTime",
"type": "uint64"
}
],
"name": "parsePriceFeedUpdatesUnique",
"outputs": [
{
"components": [
{
"internalType": "bytes32",
"name": "id",
"type": "bytes32"
},
{
"components": [
{
"internalType": "int64",
"name": "price",
"type": "int64"
},
{
"internalType": "uint64",
"name": "conf",
"type": "uint64"
},
{
"internalType": "int32",
"name": "expo",
"type": "int32"
},
{
"internalType": "uint256",
"name": "publishTime",
"type": "uint256"
}
],
"internalType": "struct PythStructs.Price",
"name": "price",
"type": "tuple"
},
{
"components": [
{
"internalType": "int64",
"name": "price",
"type": "int64"
},
{
"internalType": "uint64",
"name": "conf",
"type": "uint64"
},
{
"internalType": "int32",
"name": "expo",
"type": "int32"
},
{
"internalType": "uint256",
"name": "publishTime",
"type": "uint256"
}
],
"internalType": "struct PythStructs.Price",
"name": "emaPrice",
"type": "tuple"
}
],
"internalType": "struct PythStructs.PriceFeed[]",
"name": "priceFeeds",
"type": "tuple[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{

View File

@ -131,6 +131,11 @@
"internalType": "uint64",
"name": "publishTime",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "prevPublishTime",
"type": "uint64"
}
],
"name": "createPriceFeedUpdateData",
@ -527,6 +532,101 @@
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes[]",
"name": "updateData",
"type": "bytes[]"
},
{
"internalType": "bytes32[]",
"name": "priceIds",
"type": "bytes32[]"
},
{
"internalType": "uint64",
"name": "minPublishTime",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "maxPublishTime",
"type": "uint64"
}
],
"name": "parsePriceFeedUpdatesUnique",
"outputs": [
{
"components": [
{
"internalType": "bytes32",
"name": "id",
"type": "bytes32"
},
{
"components": [
{
"internalType": "int64",
"name": "price",
"type": "int64"
},
{
"internalType": "uint64",
"name": "conf",
"type": "uint64"
},
{
"internalType": "int32",
"name": "expo",
"type": "int32"
},
{
"internalType": "uint256",
"name": "publishTime",
"type": "uint256"
}
],
"internalType": "struct PythStructs.Price",
"name": "price",
"type": "tuple"
},
{
"components": [
{
"internalType": "int64",
"name": "price",
"type": "int64"
},
{
"internalType": "uint64",
"name": "conf",
"type": "uint64"
},
{
"internalType": "int32",
"name": "expo",
"type": "int32"
},
{
"internalType": "uint256",
"name": "publishTime",
"type": "uint256"
}
],
"internalType": "struct PythStructs.Price",
"name": "emaPrice",
"type": "tuple"
}
],
"internalType": "struct PythStructs.PriceFeed[]",
"name": "feeds",
"type": "tuple[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/pyth-sdk-solidity",
"version": "2.2.1",
"version": "2.3.0",
"description": "Read prices from the Pyth oracle",
"repository": {
"type": "git",