[evm] Persist price updates if more recent (#1208)

* Persist price info if it is more recent in parse functions

* Refactor setLatestPrice to include checks and event in a single place

* Add test cases
This commit is contained in:
Amin Moghaddam 2024-01-09 10:30:20 +01:00 committed by GitHub
parent a94194184b
commit d0ceb076d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 188 additions and 78 deletions

View File

@ -168,17 +168,7 @@ abstract contract Pyth is
index += attestationSize;
// Store the attestation
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
if (info.publishTime > latestPublishTime) {
setLatestPriceInfo(priceId, info);
emit PriceFeedUpdate(
priceId,
info.publishTime,
info.price,
info.conf
);
}
updateLatestPriceIfNecessary(priceId, info);
}
emit BatchPriceFeedUpdate(vm.emitterChainId, vm.sequence);
@ -486,12 +476,12 @@ abstract contract Pyth is
);
for (uint j = 0; j < numUpdates; j++) {
PythInternalStructs.PriceInfo memory info;
PythInternalStructs.PriceInfo memory priceInfo;
bytes32 priceId;
uint64 prevPublishTime;
(
offset,
info,
priceInfo,
priceId,
prevPublishTime
) = extractPriceInfoFromMerkleProof(
@ -499,6 +489,7 @@ abstract contract Pyth is
encoded,
offset
);
updateLatestPriceIfNecessary(priceId, priceInfo);
{
// check whether caller requested for this data
uint k = findIndexOfPriceId(priceIds, priceId);
@ -509,7 +500,7 @@ abstract contract Pyth is
continue;
}
uint publishTime = uint(info.publishTime);
uint publishTime = uint(priceInfo.publishTime);
// Check the publish time of the price is within the given range
// and only fill the priceFeedsInfo if it is.
// If is not, default id value of 0 will still be set and
@ -524,7 +515,7 @@ abstract contract Pyth is
priceFeeds,
k,
priceId,
info,
priceInfo,
publishTime
);
}
@ -576,7 +567,7 @@ abstract contract Pyth is
}
(
PythInternalStructs.PriceInfo memory info,
PythInternalStructs.PriceInfo memory priceInfo,
) = parseSingleAttestationFromBatch(
encoded,
@ -584,7 +575,9 @@ abstract contract Pyth is
attestationSize
);
uint publishTime = uint(info.publishTime);
updateLatestPriceIfNecessary(priceId, priceInfo);
uint publishTime = uint(priceInfo.publishTime);
// Check the publish time of the price is within the given range
// and only fill the priceFeedsInfo if it is.
// If is not, default id value of 0 will still be set and
@ -598,7 +591,7 @@ abstract contract Pyth is
priceFeeds,
k,
priceId,
info,
priceInfo,
publishTime
);
}
@ -727,6 +720,6 @@ abstract contract Pyth is
}
function version() public pure returns (string memory) {
return "1.3.3";
return "1.4.3";
}
}

View File

@ -371,16 +371,7 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
priceId,
prevPublishTime
) = extractPriceInfoFromMerkleProof(digest, encoded, offset);
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
if (priceInfo.publishTime > latestPublishTime) {
setLatestPriceInfo(priceId, priceInfo);
emit PriceFeedUpdate(
priceId,
priceInfo.publishTime,
priceInfo.price,
priceInfo.conf
);
}
updateLatestPriceIfNecessary(priceId, priceInfo);
}
}
if (offset != encoded.length) revert PythErrors.InvalidUpdateData();

View File

@ -4,17 +4,27 @@
pragma solidity ^0.8.0;
import "./PythState.sol";
import "@pythnetwork/pyth-sdk-solidity/IPythEvents.sol";
contract PythSetters is PythState {
contract PythSetters is PythState, IPythEvents {
function setWormhole(address wh) internal {
_state.wormhole = payable(wh);
}
function setLatestPriceInfo(
function updateLatestPriceIfNecessary(
bytes32 priceId,
PythInternalStructs.PriceInfo memory info
) internal {
_state.latestPriceInfo[priceId] = info;
uint64 latestPublishTime = _state.latestPriceInfo[priceId].publishTime;
if (info.publishTime > latestPublishTime) {
_state.latestPriceInfo[priceId] = info;
emit PriceFeedUpdate(
priceId,
info.publishTime,
info.price,
info.conf
);
}
}
function setSingleUpdateFeeInWei(uint fee) internal {

View File

@ -898,6 +898,85 @@ contract PythWormholeMerkleAccumulatorTest is
);
}
function testParsePriceFeedUniqueWithWormholeMerkleUpdatesLatestPriceIfNecessary(
uint seed
) public {
setRandSeed(seed);
uint64 numPriceFeeds = (getRandUint64() % 10) + 2;
PriceFeedMessage[]
memory priceFeedMessages = generateRandomPriceFeedMessage(
numPriceFeeds
);
uint64 publishTime = getRandUint64();
bytes32[] memory priceIds = new bytes32[](1);
priceIds[0] = priceFeedMessages[0].priceId;
for (uint64 i = 0; i < numPriceFeeds; i++) {
priceFeedMessages[i].priceId = priceFeedMessages[0].priceId;
priceFeedMessages[i].publishTime = publishTime;
priceFeedMessages[i].prevPublishTime = publishTime;
}
// firstUpdate is the one we expect to be returned and latestUpdate is the one we expect to be stored
uint latestUpdate = (getRandUint() % numPriceFeeds);
priceFeedMessages[latestUpdate].prevPublishTime = publishTime + 1000;
priceFeedMessages[latestUpdate].publishTime = publishTime + 1000;
uint firstUpdate = (getRandUint() % numPriceFeeds);
while (firstUpdate == latestUpdate) {
firstUpdate = (getRandUint() % numPriceFeeds);
}
priceFeedMessages[firstUpdate].prevPublishTime = publishTime - 1;
(
bytes[] memory updateData,
uint updateFee
) = createWormholeMerkleUpdateData(priceFeedMessages);
// firstUpdate is returned but latestUpdate is stored
PythStructs.PriceFeed[] memory priceFeeds = pyth
.parsePriceFeedUpdatesUnique{value: updateFee}(
updateData,
priceIds,
publishTime,
MAX_UINT64
);
assertEq(priceFeeds.length, 1);
assertParsedPriceFeedEqualsMessage(
priceFeeds[0],
priceFeedMessages[firstUpdate],
priceIds[0]
);
assertPriceFeedMessageStored(priceFeedMessages[latestUpdate]);
// increase the latestUpdate publish time and make a new updateData
priceFeedMessages[latestUpdate].publishTime = publishTime + 2000;
(updateData, updateFee) = createWormholeMerkleUpdateData(
priceFeedMessages
);
// since there is a revert, the latestUpdate is not stored
vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
updateData,
priceIds,
publishTime - 1,
MAX_UINT64
);
assertEq(
pyth.getPriceUnsafe(priceIds[0]).publishTime,
publishTime + 1000
);
// there is no revert, the latestPrice is updated with the latestUpdate
pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
updateData,
priceIds,
publishTime,
MAX_UINT64
);
assertPriceFeedMessageStored(priceFeedMessages[latestUpdate]);
}
function testParsePriceFeedWithWormholeMerkleWorksRandomDistinctUpdatesInput(
uint seed
) public {

View File

@ -525,4 +525,79 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
MAX_UINT64
);
}
function testParsePriceFeedUpdatesLatestPriceIfNecessary() public {
uint numAttestations = 10;
(
bytes32[] memory priceIds,
PriceAttestation[] memory attestations
) = generateRandomPriceAttestations(numAttestations);
for (uint i = 0; i < numAttestations; i++) {
// Set status to Trading so publishTime is used
attestations[i].status = PriceAttestationStatus.Trading;
attestations[i].publishTime = uint64((getRandUint() % 101)); // All between [0, 100]
}
(
bytes[] memory updateData,
uint updateFee
) = createBatchedUpdateDataFromAttestations(attestations);
// Request for parse within the given time range should work and update the latest price
pyth.parsePriceFeedUpdates{value: updateFee}(
updateData,
priceIds,
0,
100
);
// Check if the latest price is updated
for (uint i = 0; i < numAttestations; i++) {
assertEq(
pyth.getPriceUnsafe(priceIds[i]).publishTime,
attestations[i].publishTime
);
}
for (uint i = 0; i < numAttestations; i++) {
// Set status to Trading so publishTime is used
attestations[i].status = PriceAttestationStatus.Trading;
attestations[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200]
}
(updateData, updateFee) = createBatchedUpdateDataFromAttestations(
attestations
);
// Request for parse after the time range should revert.
vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
pyth.parsePriceFeedUpdates{value: updateFee}(
updateData,
priceIds,
300,
400
);
// parse function reverted so publishTimes should remain less than or equal to 100
for (uint i = 0; i < numAttestations; i++) {
assertGe(100, pyth.getPriceUnsafe(priceIds[i]).publishTime);
}
// Time range is now fixed, so parse should work and update the latest price
pyth.parsePriceFeedUpdates{value: updateFee}(
updateData,
priceIds,
100,
200
);
// Check if the latest price is updated
for (uint i = 0; i < numAttestations; i++) {
assertEq(
pyth.getPriceUnsafe(priceIds[i]).publishTime,
attestations[i].publishTime
);
}
}
}

View File

@ -428,17 +428,7 @@ contract PythExperimental is Pyth {
PythInternalStructs.PriceInfo memory info,
bytes32 priceId
) = parseSingleAttestationFromBatch(data, 0, data.length);
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
if (info.publishTime > latestPublishTime) {
setLatestPriceInfo(priceId, info);
emit PriceFeedUpdate(
priceId,
info.publishTime,
info.price,
info.conf
);
}
updateLatestPriceIfNecessary(priceId, info);
}
// Update a single price feed via a threshold-signed merkle proof.
@ -459,17 +449,7 @@ contract PythExperimental is Pyth {
PythInternalStructs.PriceInfo memory info,
bytes32 priceId
) = parseSingleAttestationFromBatch(data, 0, data.length);
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
if (info.publishTime > latestPublishTime) {
setLatestPriceInfo(priceId, info);
emit PriceFeedUpdate(
priceId,
info.publishTime,
info.price,
info.conf
);
}
updateLatestPriceIfNecessary(priceId, info);
}
// Update a single price feed via a threshold-signed price update.
@ -486,17 +466,7 @@ contract PythExperimental is Pyth {
PythInternalStructs.PriceInfo memory info,
bytes32 priceId
) = parseSingleAttestationFromBatch(data, 0, data.length);
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
if (info.publishTime > latestPublishTime) {
setLatestPriceInfo(priceId, info);
emit PriceFeedUpdate(
priceId,
info.publishTime,
info.price,
info.conf
);
}
updateLatestPriceIfNecessary(priceId, info);
}
// Update a single price feed via a "native" price update (i.e., using the default ethereum tx signature for authentication).
@ -510,17 +480,7 @@ contract PythExperimental is Pyth {
PythInternalStructs.PriceInfo memory info,
bytes32 priceId
) = parseSingleAttestationFromBatch(data, 0, data.length);
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
if (info.publishTime > latestPublishTime) {
setLatestPriceInfo(priceId, info);
emit PriceFeedUpdate(
priceId,
info.publishTime,
info.price,
info.conf
);
}
updateLatestPriceIfNecessary(priceId, info);
}
// Verify that signature is a valid ECDSA signature of messageHash by signer.

View File

@ -117,7 +117,8 @@ interface IPyth is IPythEvents {
/// within `minPublishTime` and `maxPublishTime`.
///
/// You can use this method if you want to use a Pyth price at a fixed time and not the most recent price;
/// otherwise, please consider using `updatePriceFeeds`. This method does not store the price updates on-chain.
/// otherwise, please consider using `updatePriceFeeds`. This method may store the price updates on-chain, if they
/// are more recent than the current stored prices.
///
/// This method requires the caller to pay a fee in wei; the required fee can be computed by calling
/// `getUpdateFee` with the length of the `updateData` array.
@ -139,7 +140,8 @@ interface IPyth is IPythEvents {
/// @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.
/// this method will return the first update. This method may store the price updates on-chain, if they
/// are more recent than the current stored prices.
///
///
/// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid or there is