[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:
parent
a94194184b
commit
d0ceb076d8
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue