diff --git a/contracts/pyth-store-v1.clar b/contracts/pyth-store-v1.clar index 386585d..df14884 100644 --- a/contracts/pyth-store-v1.clar +++ b/contracts/pyth-store-v1.clar @@ -53,7 +53,7 @@ prev-publish-time: uint, })) (let ((stale-price-threshold (contract-call? .pyth-governance-v1 get-stale-price-threshold)) - (latest-bitcoin-timestamp (unwrap! (get-block-info? time burn-block-height) ERR_STALE_PRICE))) + (latest-bitcoin-timestamp (unwrap! (get-block-info? time (- block-height u1)) ERR_STALE_PRICE))) ;; Ensure that we have not processed a newer price (asserts! (is-price-update-more-recent (get price-identifier entry) (get publish-time entry)) ERR_NEWER_PRICE_AVAILABLE) ;; Ensure that price is not stale diff --git a/unit-tests/pyth/helpers.ts b/unit-tests/pyth/helpers.ts index 8fbbdc6..48f64d0 100644 --- a/unit-tests/pyth/helpers.ts +++ b/unit-tests/pyth/helpers.ts @@ -30,27 +30,27 @@ export namespace pyth { "ec7a775f46379b5e943c3526b1c8d54cd49749176b0b98e02dde68d1bd335c17", "hex", ); - export const BatPriceIdentifer = Buffer.from( + export const BatPriceIdentifier = Buffer.from( "8e860fb74e60e5736b455d82f60b3728049c348e94961add5f961b02fdee2535", "hex", ); - export const DaiPriceIdentifer = Buffer.from( + export const DaiPriceIdentifier = Buffer.from( "b0948a5e5313200c632b51bb5ca32f6de0d36e9950a942d19751e833f70dabfd", "hex", ); - export const UsdcPriceIdentifer = Buffer.from( + export const UsdcPriceIdentifier = Buffer.from( "eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a", "hex", ); - export const UsdtPriceIdentifer = Buffer.from( + export const UsdtPriceIdentifier = Buffer.from( "2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b", "hex", ); - export const WbtcPriceIdentifer = Buffer.from( + export const WbtcPriceIdentifier = Buffer.from( "c9d8b075a5c69303365ae23633d4e085199bf5c520a3b90fed1322a0342ffc33", "hex", ); - export const TbtcPriceIdentifer = Buffer.from( + export const TbtcPriceIdentifier = Buffer.from( "56a3121958b01f99fdc4e1fd01e81050602c7ace3a571918bb55c6a96657cca9", "hex", ); @@ -665,6 +665,39 @@ export namespace pyth { return res; } + export function applyStalePriceThresholdUpdate( + updateStalePriceThreshold: { threshold: bigint }, + emitter: wormhole.Emitter, + guardianSet: wormhole.Guardian[], + txSenderAddress: string, + pythGovernanceContractName: string, + wormholeCoreContractName: string, + sequence: bigint, + ) { + let ptgmVaaPayload = pyth.buildPtgmVaaPayload({ + updateStalePriceThreshold, + }); + let payload = pyth.serializePtgmVaaPayloadToBuffer(ptgmVaaPayload); + let body = wormhole.buildValidVaaBodySpecs({ payload, sequence, emitter }); + let header = wormhole.buildValidVaaHeader(guardianSet, body, { + version: 1, + guardianSetId: 1, + }); + let vaa = wormhole.serializeVaaToBuffer(header, body); + let wormholeContract = Cl.contractPrincipal( + simnet.deployer, + wormholeCoreContractName, + ); + let res = simnet.callPublicFn( + pythGovernanceContractName, + `update-stale-price-threshold`, + [Cl.buffer(vaa), wormholeContract], + txSenderAddress, + ); + + return res; + } + export namespace fc_ext { export const priceUpdate = (opts?: PriceUpdateBuildOptions) => { // price diff --git a/unit-tests/pyth/pnau.test.ts b/unit-tests/pyth/pnau.test.ts index f35d04b..647b62e 100644 --- a/unit-tests/pyth/pnau.test.ts +++ b/unit-tests/pyth/pnau.test.ts @@ -16,17 +16,17 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds success", () => { let pricesUpdates = pyth.buildPriceUpdateBatch([ [pyth.BtcPriceIdentifier], [pyth.StxPriceIdentifier], - [pyth.BatPriceIdentifer], - [pyth.DaiPriceIdentifer], - [pyth.TbtcPriceIdentifer], - [pyth.UsdcPriceIdentifer], - [pyth.UsdtPriceIdentifer], - [pyth.WbtcPriceIdentifer], + [pyth.BatPriceIdentifier], + [pyth.DaiPriceIdentifier], + [pyth.TbtcPriceIdentifier], + [pyth.UsdcPriceIdentifier], + [pyth.UsdtPriceIdentifier], + [pyth.WbtcPriceIdentifier], ]); let pricesUpdatesToSubmit = [ pyth.BtcPriceIdentifier, pyth.StxPriceIdentifier, - pyth.UsdcPriceIdentifer, + pyth.UsdcPriceIdentifier, ]; let pricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload(pricesUpdates); @@ -137,17 +137,17 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { let pricesUpdates = pyth.buildPriceUpdateBatch([ [pyth.BtcPriceIdentifier], [pyth.StxPriceIdentifier], - [pyth.BatPriceIdentifer], - [pyth.DaiPriceIdentifer], - [pyth.TbtcPriceIdentifer], - [pyth.UsdcPriceIdentifer], - [pyth.UsdtPriceIdentifer], - [pyth.WbtcPriceIdentifer], + [pyth.BatPriceIdentifier], + [pyth.DaiPriceIdentifier], + [pyth.TbtcPriceIdentifier], + [pyth.UsdcPriceIdentifier], + [pyth.UsdtPriceIdentifier], + [pyth.WbtcPriceIdentifier], ]); let pricesUpdatesToSubmit = [ pyth.BtcPriceIdentifier, pyth.StxPriceIdentifier, - pyth.UsdcPriceIdentifer, + pyth.UsdcPriceIdentifier, ]; let pricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload(pricesUpdates); let executionPlan = Cl.tuple({ @@ -194,6 +194,16 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { 3n, ); + pyth.applyStalePriceThresholdUpdate( + { threshold: 10800n }, + pyth.DefaultGovernanceDataSource, + guardianSet, + sender, + pythGovernanceContractName, + wormholeCoreContractName, + 4n, + ); + let payload = pyth.serializeAuwvVaaPayloadToBuffer(pricesUpdatesVaaPayload); let vaaBody = wormhole.buildValidVaaBodySpecs({ payload, @@ -211,14 +221,53 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { pricesUpdatesToSubmit, }); - pricesUpdates.decoded[0]; - - simnet.callPublicFn( + let res = simnet.callPublicFn( pythOracleContractName, "verify-and-update-price-feeds", [Cl.buffer(pnau), executionPlan], sender, ); + + expect(res.result).toBeOk( + Cl.list([ + Cl.tuple({ + "price-identifier": Cl.buffer(pyth.BtcPriceIdentifier), + price: Cl.int(pricesUpdates.decoded[0].price), + conf: Cl.uint(pricesUpdates.decoded[0].conf), + "ema-conf": Cl.uint(pricesUpdates.decoded[0].emaConf), + "ema-price": Cl.int(pricesUpdates.decoded[0].emaPrice), + expo: Cl.int(pricesUpdates.decoded[0].expo), + "prev-publish-time": Cl.uint( + pricesUpdates.decoded[0].prevPublishTime, + ), + "publish-time": Cl.uint(pricesUpdates.decoded[0].publishTime), + }), + Cl.tuple({ + "price-identifier": Cl.buffer(pyth.StxPriceIdentifier), + price: Cl.int(pricesUpdates.decoded[1].price), + conf: Cl.uint(pricesUpdates.decoded[1].conf), + "ema-conf": Cl.uint(pricesUpdates.decoded[1].emaConf), + "ema-price": Cl.int(pricesUpdates.decoded[1].emaPrice), + expo: Cl.int(pricesUpdates.decoded[1].expo), + "prev-publish-time": Cl.uint( + pricesUpdates.decoded[1].prevPublishTime, + ), + "publish-time": Cl.uint(pricesUpdates.decoded[1].publishTime), + }), + Cl.tuple({ + "price-identifier": Cl.buffer(pyth.UsdcPriceIdentifier), + price: Cl.int(pricesUpdates.decoded[2].price), + conf: Cl.uint(pricesUpdates.decoded[2].conf), + "ema-conf": Cl.uint(pricesUpdates.decoded[2].emaConf), + "ema-price": Cl.int(pricesUpdates.decoded[2].emaPrice), + expo: Cl.int(pricesUpdates.decoded[2].expo), + "prev-publish-time": Cl.uint( + pricesUpdates.decoded[2].prevPublishTime, + ), + "publish-time": Cl.uint(pricesUpdates.decoded[2].publishTime), + }), + ]), + ); }); it("should succeed updating prices once", () => { @@ -248,12 +297,12 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { let outdatedPricesUpdates = pyth.buildPriceUpdateBatch([ [pyth.BtcPriceIdentifier, { price: 0n, publishTime: 9999999n }], [pyth.StxPriceIdentifier], - [pyth.BatPriceIdentifer], - [pyth.DaiPriceIdentifer], - [pyth.TbtcPriceIdentifer], - [pyth.UsdcPriceIdentifer], - [pyth.UsdtPriceIdentifer], - [pyth.WbtcPriceIdentifer], + [pyth.BatPriceIdentifier], + [pyth.DaiPriceIdentifier], + [pyth.TbtcPriceIdentifier], + [pyth.UsdcPriceIdentifier], + [pyth.UsdtPriceIdentifier], + [pyth.WbtcPriceIdentifier], ]); let outdatedPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload( outdatedPricesUpdates, @@ -310,12 +359,12 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { let actualPricesUpdates = pyth.buildPriceUpdateBatch([ [pyth.BtcPriceIdentifier], [pyth.StxPriceIdentifier], - [pyth.BatPriceIdentifer], - [pyth.DaiPriceIdentifer], - [pyth.TbtcPriceIdentifer], - [pyth.UsdcPriceIdentifer], - [pyth.UsdtPriceIdentifer], - [pyth.WbtcPriceIdentifer], + [pyth.BatPriceIdentifier], + [pyth.DaiPriceIdentifier], + [pyth.TbtcPriceIdentifier], + [pyth.UsdcPriceIdentifier], + [pyth.UsdtPriceIdentifier], + [pyth.WbtcPriceIdentifier], ]); let actualPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload(actualPricesUpdates); @@ -622,13 +671,16 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { expect(res.result).toBeErr(Cl.uint(2008)); }); - it("should fail if the price is above stale threshold", () => { + it("should fail if the price is below stale threshold", () => { + // Everytime a simnet block is being mined, we add 1500s to the initial genesis time - usually bringing us to the future. + let onChainTime = pyth.timestampNow() + 1500n * BigInt(simnet.blockHeight); + simnet.mineEmptyBlocks(7); // stale threshold set to 10800 (3 hours), so by mining 7 blocks (1800s), we are advancing enough let actualPricesUpdates = pyth.buildPriceUpdateBatch([ [ pyth.BtcPriceIdentifier, { price: 100n, - publishTime: pyth.timestampNow() - (5n * 365n * 60n * 60n + 1n), + publishTime: onChainTime, }, ], ]); @@ -663,16 +715,24 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { }); it("should only return validated prices and filter invalid prices", () => { + // Everytime a simnet block is being mined, we add 1500s to the initial genesis time - usually bringing us to the future. + let previousOnChainTime = + pyth.timestampNow() + 1500n * BigInt(simnet.blockHeight); + simnet.mineEmptyBlocks(7); // stale threshold set to 10800 (3 hours), so by mining 7 blocks (1800s), we are advancing enough + let newOnChainTime = + pyth.timestampNow() + 1500n * BigInt(simnet.blockHeight); let actualPricesUpdates = pyth.buildPriceUpdateBatch([ [ pyth.BtcPriceIdentifier, { price: 100n, - publishTime: pyth.timestampNow() - (5n * 365n * 60n * 60n + 1n), + publishTime: previousOnChainTime, }, ], - [pyth.StxPriceIdentifier, { price: 100n }], + [pyth.StxPriceIdentifier, { price: 100n, publishTime: newOnChainTime }], + [pyth.UsdcPriceIdentifier, { price: 100n, publishTime: newOnChainTime }], ]); + let actualPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload(actualPricesUpdates); let payload = pyth.serializeAuwvVaaPayloadToBuffer( @@ -714,6 +774,18 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { ), "publish-time": Cl.uint(actualPricesUpdates.decoded[1].publishTime), }), + Cl.tuple({ + "price-identifier": Cl.buffer(pyth.UsdcPriceIdentifier), + price: Cl.int(actualPricesUpdates.decoded[2].price), + conf: Cl.uint(actualPricesUpdates.decoded[2].conf), + "ema-conf": Cl.uint(actualPricesUpdates.decoded[2].emaConf), + "ema-price": Cl.int(actualPricesUpdates.decoded[2].emaPrice), + expo: Cl.int(actualPricesUpdates.decoded[2].expo), + "prev-publish-time": Cl.uint( + actualPricesUpdates.decoded[2].prevPublishTime, + ), + "publish-time": Cl.uint(actualPricesUpdates.decoded[2].publishTime), + }), ]), ); });