diff --git a/contracts/pyth-governance-v1.clar b/contracts/pyth-governance-v1.clar index 68fe43b..f62a939 100644 --- a/contracts/pyth-governance-v1.clar +++ b/contracts/pyth-governance-v1.clar @@ -19,6 +19,8 @@ (define-constant PTGM_UPDATE_PRICES_DATA_SOURCES 0x02) ;; Fee is charged when you submit a new price (define-constant PTGM_UPDATE_FEE 0x03) +;; Stale price threshold +(define-constant PTGM_STALE_PRICE_THRESHOLD 0x04) ;; Upgrade wormhole contract (define-constant PTGM_UPDATE_WORMHOLE_CORE_ADDRESS 0x06) ;; Special Stacks operation: update recipient address @@ -27,10 +29,10 @@ (define-constant PTGM_UPDATE_PYTH_STORE_ADDRESS 0xa1) ;; Special Stacks operation: update decoder contract address (define-constant PTGM_UPDATE_PYTH_DECODER_ADDRESS 0xa2) -;; TODO: Pyth team to assign a chain id to Stacks. -(define-constant EXPECTED_CHAIN_ID 0x00) -;; TODO: Pyth team to assign a module to Stacks. -(define-constant EXPECTED_MODULE 0x00) +;; Stacks chain id attributed by Pyth +(define-constant EXPECTED_CHAIN_ID (if is-in-mainnet 0xea86 0xc377)) +;; Stacks module id attributed by Pyth +(define-constant EXPECTED_MODULE 0x03) ;; Error unauthorized control flow (define-constant ERR_UNAUTHORIZED_ACCESS (err u4004)) @@ -59,27 +61,22 @@ (define-data-var fee-value { mantissa: uint, exponent: uint } { mantissa: u1, exponent: u1 }) -(define-data-var fee-recipient-address principal tx-sender) -(define-data-var last-sequence-processed uint u0) ;; TODO: set initial value +(define-data-var stale-price-threshold uint (if is-in-mainnet (* u2 u60 u60) (* u5 u365 u24 u60 u60))) ;; defaults: 2 hours on Mainnet, 5 years on Testnet +(define-data-var fee-recipient-address principal (if is-in-mainnet 'SP3CRXBDXQ2N5P7E25Q39MEX1HSMRDSEAP3CFK2Z3 'ST3CRXBDXQ2N5P7E25Q39MEX1HSMRDSEAP1JST19D)) +(define-data-var last-sequence-processed uint u0) - -(define-map execution-plans uint { +;; Execution plan management +(define-data-var current-execution-plan { pyth-oracle-contract: principal, pyth-decoder-contract: principal, pyth-storage-contract: principal, wormhole-core-contract: principal -}) -(define-data-var current-execution-plan-id uint u0) - -;; Execution plan management -;; Initialize governance v1 with v1 contracts -(begin - (map-insert execution-plans u0 { +} { pyth-oracle-contract: .pyth-oracle-v1, pyth-decoder-contract: .pyth-pnau-decoder-v1, pyth-storage-contract: .pyth-store-v1, wormhole-core-contract: .wormhole-core-v1 - })) +}) (define-read-only (check-execution-flow (former-contract-caller principal) @@ -88,7 +85,7 @@ pyth-decoder-contract: , wormhole-core-contract: }))) - (let ((expected-execution-plan (get-current-execution-plan)) + (let ((expected-execution-plan (var-get current-execution-plan)) (success (if (is-eq contract-caller (get pyth-storage-contract expected-execution-plan)) ;; The storage contract is checking its execution flow ;; Must always be invoked by the proxy @@ -111,21 +108,24 @@ (define-read-only (check-storage-contract (storage-contract )) - (let ((expected-execution-plan (get-current-execution-plan))) + (let ((expected-execution-plan (var-get current-execution-plan))) ;; Ensure that storage contract is the one expected (expect-active-storage-contract storage-contract expected-execution-plan))) (define-read-only (get-current-execution-plan) - (unwrap-panic (map-get? execution-plans (var-get current-execution-plan-id)))) + (var-get current-execution-plan)) (define-read-only (get-fee-info) (merge (var-get fee-value) { address: (var-get fee-recipient-address) })) +(define-read-only (get-stale-price-threshold) + (var-get stale-price-threshold)) + (define-read-only (get-authorized-prices-data-sources) (var-get prices-data-sources)) (define-public (update-fee-value (vaa-bytes (buff 8192)) (wormhole-core-contract )) - (let ((expected-execution-plan (get-current-execution-plan)) + (let ((expected-execution-plan (var-get current-execution-plan)) (vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes))) (ptgm (try! (parse-and-verify-ptgm (get payload vaa) (get sequence vaa))))) ;; Ensure action's expectation @@ -139,8 +139,23 @@ (var-set fee-value updated-data) (ok updated-data)))) +(define-public (update-stale-price-threshold (vaa-bytes (buff 8192)) (wormhole-core-contract )) + (let ((expected-execution-plan (var-get current-execution-plan)) + (vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes))) + (ptgm (try! (parse-and-verify-ptgm (get payload vaa) (get sequence vaa))))) + ;; Ensure action's expectation + (asserts! (is-eq (get action ptgm) PTGM_STALE_PRICE_THRESHOLD) ERR_UNEXPECTED_ACTION) + ;; Ensure that the action is authorized + (try! (check-update-source (get emitter-chain vaa) (get emitter-address vaa))) + ;; Ensure that the lastest wormhole contract is used + (try! (expect-active-wormhole-contract wormhole-core-contract expected-execution-plan)) + ;; Update fee-value + (let ((updated-data (try! (parse-and-verify-stale-price-threshold (get body ptgm))))) + (var-set stale-price-threshold updated-data) + (ok updated-data)))) + (define-public (update-fee-recipient-address (vaa-bytes (buff 8192)) (wormhole-core-contract )) - (let ((expected-execution-plan (get-current-execution-plan)) + (let ((expected-execution-plan (var-get current-execution-plan)) (vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes))) (ptgm (try! (parse-and-verify-ptgm (get payload vaa) (get sequence vaa))))) ;; Ensure action's expectation @@ -155,8 +170,7 @@ (ok updated-data)))) (define-public (update-wormhole-core-contract (vaa-bytes (buff 8192)) (wormhole-core-contract )) - (let ((expected-execution-plan (get-current-execution-plan)) - (next-execution-plan-id (+ (var-get current-execution-plan-id) u1)) + (let ((expected-execution-plan (var-get current-execution-plan)) (vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes))) (ptgm (try! (parse-and-verify-ptgm (get payload vaa) (get sequence vaa))))) ;; Ensure action's expectation @@ -167,13 +181,11 @@ (try! (expect-active-wormhole-contract wormhole-core-contract expected-execution-plan)) ;; Update execution plan (let ((updated-data (unwrap! (from-consensus-buff? principal (get body ptgm)) ERR_UNEXPECTED_ACTION_PAYLOAD))) - (map-set execution-plans next-execution-plan-id (merge expected-execution-plan { wormhole-core-contract: updated-data })) - (var-set current-execution-plan-id next-execution-plan-id) - (ok (get-current-execution-plan))))) + (var-set current-execution-plan (merge expected-execution-plan { wormhole-core-contract: updated-data })) + (ok (var-get current-execution-plan))))) (define-public (update-pyth-oracle-contract (vaa-bytes (buff 8192)) (wormhole-core-contract )) - (let ((expected-execution-plan (get-current-execution-plan)) - (next-execution-plan-id (+ (var-get current-execution-plan-id) u1)) + (let ((expected-execution-plan (var-get current-execution-plan)) (vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes))) (ptgm (try! (parse-and-verify-ptgm (get payload vaa) (get sequence vaa))))) ;; Ensure action's expectation @@ -184,13 +196,11 @@ (try! (expect-active-wormhole-contract wormhole-core-contract expected-execution-plan)) ;; Update execution plan (let ((updated-data (unwrap! (from-consensus-buff? principal (get body ptgm)) ERR_UNEXPECTED_ACTION_PAYLOAD))) - (map-set execution-plans next-execution-plan-id (merge expected-execution-plan { pyth-oracle-contract: updated-data })) - (var-set current-execution-plan-id next-execution-plan-id) - (ok (get-current-execution-plan))))) + (var-set current-execution-plan (merge expected-execution-plan { pyth-oracle-contract: updated-data })) + (ok (var-get current-execution-plan))))) (define-public (update-pyth-decoder-contract (vaa-bytes (buff 8192)) (wormhole-core-contract )) - (let ((expected-execution-plan (get-current-execution-plan)) - (next-execution-plan-id (+ (var-get current-execution-plan-id) u1)) + (let ((expected-execution-plan (var-get current-execution-plan)) (vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes))) (ptgm (try! (parse-and-verify-ptgm (get payload vaa) (get sequence vaa))))) ;; Ensure action's expectation @@ -201,13 +211,11 @@ (try! (expect-active-wormhole-contract wormhole-core-contract expected-execution-plan)) ;; Update execution plan (let ((updated-data (unwrap! (from-consensus-buff? principal (get body ptgm)) ERR_UNEXPECTED_ACTION_PAYLOAD))) - (map-set execution-plans next-execution-plan-id (merge expected-execution-plan { pyth-decoder-contract: updated-data })) - (var-set current-execution-plan-id next-execution-plan-id) - (ok (get-current-execution-plan))))) + (var-set current-execution-plan (merge expected-execution-plan { pyth-decoder-contract: updated-data })) + (ok (var-get current-execution-plan))))) (define-public (update-pyth-store-contract (vaa-bytes (buff 8192)) (wormhole-core-contract )) - (let ((expected-execution-plan (get-current-execution-plan)) - (next-execution-plan-id (+ (var-get current-execution-plan-id) u1)) + (let ((expected-execution-plan (var-get current-execution-plan)) (vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes))) (ptgm (try! (parse-and-verify-ptgm (get payload vaa) (get sequence vaa))))) ;; Ensure action's expectation @@ -218,12 +226,11 @@ (try! (expect-active-wormhole-contract wormhole-core-contract expected-execution-plan)) ;; Update execution plan (let ((updated-data (unwrap! (from-consensus-buff? principal (get body ptgm)) ERR_UNEXPECTED_ACTION_PAYLOAD))) - (map-set execution-plans next-execution-plan-id (merge expected-execution-plan { pyth-storage-contract: updated-data })) - (var-set current-execution-plan-id next-execution-plan-id) - (ok (get-current-execution-plan))))) + (var-set current-execution-plan (merge expected-execution-plan { pyth-storage-contract: updated-data })) + (ok (var-get current-execution-plan))))) (define-public (update-prices-data-sources (vaa-bytes (buff 8192)) (wormhole-core-contract )) - (let ((expected-execution-plan (get-current-execution-plan)) + (let ((expected-execution-plan (var-get current-execution-plan)) (vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes))) (ptgm (try! (parse-and-verify-ptgm (get payload vaa) (get sequence vaa))))) ;; Ensure action's expectation @@ -238,7 +245,7 @@ (ok updated-data)))) (define-public (update-governance-data-source (vaa-bytes (buff 8192)) (wormhole-core-contract )) - (let ((expected-execution-plan (get-current-execution-plan)) + (let ((expected-execution-plan (var-get current-execution-plan)) (vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes))) (ptgm (try! (parse-and-verify-ptgm (get payload vaa) (get sequence vaa))))) ;; Ensure action's expectation @@ -330,7 +337,7 @@ ERR_INVALID_PTGM)) (cursor-action (unwrap! (contract-call? 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-cursor-v2 read-buff-1 (get next cursor-module)) ERR_INVALID_PTGM)) - (cursor-target-chain-id (unwrap! (contract-call? 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-cursor-v2 read-buff-1 (get next cursor-action)) + (cursor-target-chain-id (unwrap! (contract-call? 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-cursor-v2 read-buff-2 (get next cursor-action)) ERR_INVALID_PTGM)) (cursor-body (unwrap! (contract-call? 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-cursor-v2 read-buff-8192-max (get next cursor-target-chain-id) none) ERR_INVALID_PTGM))) @@ -363,6 +370,12 @@ exponent: (get value cursor-exponent) }))) +(define-private (parse-and-verify-stale-price-threshold (ptgm-body (buff 8192))) + (let ((cursor-ptgm-body (contract-call? 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-cursor-v2 new ptgm-body none)) + (cursor-stale-price-threshold (unwrap! (contract-call? 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-cursor-v2 read-uint-64 (get next cursor-ptgm-body)) + ERR_INVALID_ACTION_PAYLOAD))) + (ok (get value cursor-stale-price-threshold)))) + (define-private (parse-and-verify-governance-data-source (ptgm-body (buff 8192))) (let ((cursor-ptgm-body (contract-call? 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-cursor-v2 new ptgm-body none)) (cursor-emitter-chain (unwrap! (contract-call? 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-cursor-v2 read-uint-16 (get next cursor-ptgm-body)) diff --git a/contracts/pyth-oracle-v1.clar b/contracts/pyth-oracle-v1.clar index 4698483..aeb5cff 100644 --- a/contracts/pyth-oracle-v1.clar +++ b/contracts/pyth-oracle-v1.clar @@ -35,12 +35,10 @@ (let ((pyth-decoder-contract (get pyth-decoder-contract execution-plan)) (wormhole-core-contract (get wormhole-core-contract execution-plan)) (pyth-storage-contract (get pyth-storage-contract execution-plan)) - (prices-updates (try! (contract-call? pyth-decoder-contract decode-and-verify-price-feeds price-feed-bytes wormhole-core-contract))) + (decoded-prices (try! (contract-call? pyth-decoder-contract decode-and-verify-price-feeds price-feed-bytes wormhole-core-contract))) + (updated-prices (try! (contract-call? pyth-storage-contract write decoded-prices))) (fee-info (contract-call? .pyth-governance-v1 get-fee-info)) - (fee-amount (+ u1 ;; Dust fee - (* (len prices-updates) (* (get mantissa fee-info) (pow u10 (get exponent fee-info))))))) + (fee-amount (* (len updated-prices) (* (get mantissa fee-info) (pow u10 (get exponent fee-info)))))) ;; Charge fee (unwrap! (stx-transfer? fee-amount tx-sender (get address fee-info)) ERR_BALANCE_INSUFFICIENT) - ;; Update storage - (try! (contract-call? pyth-storage-contract write prices-updates)) - (ok prices-updates)))) + (ok updated-prices)))) diff --git a/contracts/pyth-store-v1.clar b/contracts/pyth-store-v1.clar index d3b4675..386585d 100644 --- a/contracts/pyth-store-v1.clar +++ b/contracts/pyth-store-v1.clar @@ -5,6 +5,10 @@ (impl-trait .pyth-traits-v1.storage-trait) +(define-constant ERR_NEWER_PRICE_AVAILABLE (err u5000)) +(define-constant ERR_STALE_PRICE (err u5001)) +(define-constant ERR_INVALID_UPDATES (err u5003)) + (define-map prices (buff 32) { price: int, conf: uint, @@ -31,11 +35,12 @@ publish-time: uint, prev-publish-time: uint, }))) - (begin + (let ((successful-updates (map unwrapped-entry (filter only-ok-entry (map write-batch-entry batch-updates))))) ;; Ensure that updates are always coming from the right contract (try! (contract-call? .pyth-governance-v1 check-execution-flow contract-caller none)) - ;; Update storage, count the number of updates - (ok (fold + (map write-batch-entry batch-updates) u0)))) + ;; Ensure we have at least one entry + (asserts! (> (len successful-updates) u0) ERR_INVALID_UPDATES) + (ok successful-updates))) (define-private (write-batch-entry (entry { price-identifier: (buff 32), @@ -46,28 +51,56 @@ ema-conf: uint, publish-time: uint, prev-publish-time: uint, - })) - (if (is-price-update-more-recent (get price-identifier entry) (get publish-time entry)) - (begin - (map-set prices - (get price-identifier entry) - { - price: (get price entry), - conf: (get conf entry), - expo: (get expo entry), - ema-price: (get ema-price entry), - ema-conf: (get ema-conf entry), - publish-time: (get publish-time entry), - prev-publish-time: (get prev-publish-time entry) - }) - (print { - type: "price-feed", - action: "updated", - data: entry + })) + (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))) + ;; 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 + (asserts! (>= (get publish-time entry) (- latest-bitcoin-timestamp stale-price-threshold)) ERR_STALE_PRICE) + ;; Update storage + (map-set prices + (get price-identifier entry) + { + price: (get price entry), + conf: (get conf entry), + expo: (get expo entry), + ema-price: (get ema-price entry), + ema-conf: (get ema-conf entry), + publish-time: (get publish-time entry), + prev-publish-time: (get prev-publish-time entry) }) - (map-set timestamps (get price-identifier entry) (get publish-time entry)) - u1) - u0)) + ;; Emit event + (print { + type: "price-feed", + action: "updated", + data: entry + }) + ;; Update timestamps tracking + (map-set timestamps (get price-identifier entry) (get publish-time entry)) + (ok entry))) + +(define-private (only-ok-entry (entry (response { + price-identifier: (buff 32), + price: int, + conf: uint, + expo: int, + ema-price: int, + ema-conf: uint, + publish-time: uint, + prev-publish-time: uint, + } uint))) (is-ok entry)) + +(define-private (unwrapped-entry (entry (response { + price-identifier: (buff 32), + price: int, + conf: uint, + expo: int, + ema-price: int, + ema-conf: uint, + publish-time: uint, + prev-publish-time: uint, + } uint))) (unwrap-panic entry)) (define-private (is-price-update-more-recent (price-identifier (buff 32)) (publish-time uint)) (> publish-time (default-to u0 (map-get? timestamps price-identifier)))) diff --git a/contracts/pyth-traits-v1.clar b/contracts/pyth-traits-v1.clar index 10f353b..f70bcda 100644 --- a/contracts/pyth-traits-v1.clar +++ b/contracts/pyth-traits-v1.clar @@ -41,7 +41,16 @@ ema-conf: uint, publish-time: uint, prev-publish-time: uint, - })) (response uint uint)) + })) (response (list 64 { + price-identifier: (buff 32), + price: int, + conf: uint, + expo: int, + ema-price: int, + ema-conf: uint, + publish-time: uint, + prev-publish-time: uint, + }) uint)) ) ) diff --git a/contracts/wormhole/wormhole-core-v1.clar b/contracts/wormhole/wormhole-core-v1.clar index a7f6254..e7a235f 100644 --- a/contracts/wormhole/wormhole-core-v1.clar +++ b/contracts/wormhole/wormhole-core-v1.clar @@ -1,5 +1,5 @@ ;; Title: wormhole-core -;; Version: Developer Preview 1 +;; Version: v1 ;; Check for latest version: https://github.com/hirosystems/stacks-pyth-bridge#latest-version ;; Report an issue: https://github.com/hirosystems/stacks-pyth-bridge/issues diff --git a/unit-tests/pyth/helpers.ts b/unit-tests/pyth/helpers.ts index b078e18..8fbbdc6 100644 --- a/unit-tests/pyth/helpers.ts +++ b/unit-tests/pyth/helpers.ts @@ -129,6 +129,7 @@ export namespace pyth { targetChainId: number; updateFeeValue?: PtgmUpdateFeeValue; updateFeeRecipient?: PtgmUpdateFeeRecipient; + updateStalePriceThreshold?: PtgmUpdateStalePriceThreshold; updateWormholeContract?: PtgmUpdateContract; updateOracleContract?: PtgmUpdateContract; updateStoreContract?: PtgmUpdateContract; @@ -142,6 +143,10 @@ export namespace pyth { exponent: bigint; } + export interface PtgmUpdateStalePriceThreshold { + threshold: bigint; + } + export interface PtgmUpdateFeeRecipient { address: string; contractName?: string; @@ -161,6 +166,7 @@ export namespace pyth { updateFeeRecipient?: PtgmUpdateFeeRecipient; updatePricesDataSources?: wormhole.Emitter[]; updateWormholeContract?: PtgmUpdateContract; + updateStalePriceThreshold?: PtgmUpdateStalePriceThreshold; updateOracleContract?: PtgmUpdateContract; updateStoreContract?: PtgmUpdateContract; updateDecoderContract?: PtgmUpdateContract; @@ -265,6 +271,8 @@ export namespace pyth { action = 0x00; } else if (opts?.updateWormholeContract) { action = 0x06; + } else if (opts?.updateStalePriceThreshold) { + action = 0x04; } else if (opts?.updateDecoderContract) { action = 0xa2; } else if (opts?.updateStoreContract) { @@ -279,9 +287,10 @@ export namespace pyth { return { magicBytes: opts?.magicBytes || PgtmMagicBytes, action, - targetChainId: opts?.targetChainId || 0, - module: opts?.module || 0, + targetChainId: opts?.targetChainId || 50039, + module: opts?.module || 3, updateFeeRecipient: opts?.updateFeeRecipient, + updateStalePriceThreshold: opts?.updateStalePriceThreshold, updateFeeValue: opts?.updateFeeValue, updateOracleContract: opts?.updateOracleContract, updateWormholeContract: opts?.updateWormholeContract, @@ -307,8 +316,8 @@ export namespace pyth { v.writeUint8(payload.action, 0); components.push(v); // Chain id - v = Buffer.alloc(1); - v.writeUint8(payload.targetChainId, 0); + v = Buffer.alloc(2); + v.writeUint16BE(payload.targetChainId, 0); components.push(v); if (payload.updateFeeValue) { @@ -349,6 +358,10 @@ export namespace pyth { payload.updateDecoderContract.contractName, ); components.push(clarityValueToBuffer(principal)); + } else if (payload.updateStalePriceThreshold) { + components.push( + bigintToBuffer(payload.updateStalePriceThreshold.threshold, 8), + ); } else if (payload.updateGovernanceDataSource) { // Chain id v = Buffer.alloc(2); @@ -577,11 +590,15 @@ export namespace pyth { emaPrice: opts?.emaPrice || 95n, emaConf: opts?.emaConf || 9n, expo: -4, - publishTime: opts?.publishTime || 10000001n, - prevPublishTime: opts?.prevPublishTime || 10000000n, + publishTime: opts?.publishTime || timestampNow(), + prevPublishTime: opts?.prevPublishTime || timestampNow() - 10n, }; } + export function timestampNow(): bigint { + return BigInt(Math.floor(Date.now() / 1000)); + } + export function applyGovernanceDataSourceUpdate( updateGovernanceDataSource: wormhole.Emitter, emitter: wormhole.Emitter, diff --git a/unit-tests/pyth/pnau.test.ts b/unit-tests/pyth/pnau.test.ts index c2afd7f..f35d04b 100644 --- a/unit-tests/pyth/pnau.test.ts +++ b/unit-tests/pyth/pnau.test.ts @@ -308,7 +308,7 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { it("should succeed storing new subsequent updates", () => { let actualPricesUpdates = pyth.buildPriceUpdateBatch([ - [pyth.BtcPriceIdentifier, { price: 100n, publishTime: 10000003n }], + [pyth.BtcPriceIdentifier], [pyth.StxPriceIdentifier], [pyth.BatPriceIdentifer], [pyth.DaiPriceIdentifer], @@ -371,7 +371,7 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { it("should fail if AUWV payloadType is incorrect", () => { let actualPricesUpdates = pyth.buildPriceUpdateBatch([ - [pyth.BtcPriceIdentifier, { price: 100n, publishTime: 10000003n }], + [pyth.BtcPriceIdentifier, { price: 100n }], ]); let actualPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload( actualPricesUpdates, @@ -407,7 +407,7 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { it("should fail if AUWV updateType is incorrect", () => { let actualPricesUpdates = pyth.buildPriceUpdateBatch([ - [pyth.BtcPriceIdentifier, { price: 100n, publishTime: 10000003n }], + [pyth.BtcPriceIdentifier, { price: 100n }], ]); let actualPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload( actualPricesUpdates, @@ -443,7 +443,7 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { it("should fail if PNAU magic bytes is incorrect", () => { let actualPricesUpdates = pyth.buildPriceUpdateBatch([ - [pyth.BtcPriceIdentifier, { price: 100n, publishTime: 10000003n }], + [pyth.BtcPriceIdentifier, { price: 100n }], ]); let actualPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload(actualPricesUpdates); @@ -479,7 +479,7 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { it("should fail if PNAU major version is incorrect", () => { let actualPricesUpdates = pyth.buildPriceUpdateBatch([ - [pyth.BtcPriceIdentifier, { price: 100n, publishTime: 10000003n }], + [pyth.BtcPriceIdentifier, { price: 100n }], ]); let actualPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload(actualPricesUpdates); @@ -515,7 +515,7 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { it("should fail if PNAU minor version is incorrect", () => { let actualPricesUpdates = pyth.buildPriceUpdateBatch([ - [pyth.BtcPriceIdentifier, { price: 100n, publishTime: 10000003n }], + [pyth.BtcPriceIdentifier, { price: 100n }], ]); let actualPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload(actualPricesUpdates); @@ -551,7 +551,7 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { it("should fail if PNAU proof type version is incorrect", () => { let actualPricesUpdates = pyth.buildPriceUpdateBatch([ - [pyth.BtcPriceIdentifier, { price: 100n, publishTime: 10000003n }], + [pyth.BtcPriceIdentifier, { price: 100n }], ]); let actualPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload(actualPricesUpdates); @@ -587,7 +587,7 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { it("should fail if PNAU include Merkle root mismatches", () => { let actualPricesUpdates = pyth.buildPriceUpdateBatch([ - [pyth.BtcPriceIdentifier, { price: 100n, publishTime: 10000003n }], + [pyth.BtcPriceIdentifier, { price: 100n }], ]); let actualPricesUpdatesVaaPayload = pyth.buildAuwvVaaPayload(actualPricesUpdates); @@ -621,4 +621,100 @@ 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", () => { + let actualPricesUpdates = pyth.buildPriceUpdateBatch([ + [ + pyth.BtcPriceIdentifier, + { + price: 100n, + publishTime: pyth.timestampNow() - (5n * 365n * 60n * 60n + 1n), + }, + ], + ]); + let actualPricesUpdatesVaaPayload = + pyth.buildAuwvVaaPayload(actualPricesUpdates); + let payload = pyth.serializeAuwvVaaPayloadToBuffer( + actualPricesUpdatesVaaPayload, + ); + let vaaBody = wormhole.buildValidVaaBodySpecs({ + payload, + emitter: pyth.DefaultPricesDataSources[0], + }); + let vaaHeader = wormhole.buildValidVaaHeader(guardianSet, vaaBody, { + version: 1, + guardianSetId: 1, + }); + let vaa = wormhole.serializeVaaToBuffer(vaaHeader, vaaBody); + let pnauHeader = pyth.buildPnauHeader(); + let pnau = pyth.serializePnauToBuffer(pnauHeader, { + vaa, + pricesUpdates: actualPricesUpdates, + pricesUpdatesToSubmit, + }); + + let res = simnet.callPublicFn( + pythOracleContractName, + "verify-and-update-price-feeds", + [Cl.buffer(pnau), executionPlan], + sender, + ); + expect(res.result).toBeErr(Cl.uint(5003)); + }); + + it("should only return validated prices and filter invalid prices", () => { + let actualPricesUpdates = pyth.buildPriceUpdateBatch([ + [ + pyth.BtcPriceIdentifier, + { + price: 100n, + publishTime: pyth.timestampNow() - (5n * 365n * 60n * 60n + 1n), + }, + ], + [pyth.StxPriceIdentifier, { price: 100n }], + ]); + let actualPricesUpdatesVaaPayload = + pyth.buildAuwvVaaPayload(actualPricesUpdates); + let payload = pyth.serializeAuwvVaaPayloadToBuffer( + actualPricesUpdatesVaaPayload, + ); + let vaaBody = wormhole.buildValidVaaBodySpecs({ + payload, + emitter: pyth.DefaultPricesDataSources[0], + }); + let vaaHeader = wormhole.buildValidVaaHeader(guardianSet, vaaBody, { + version: 1, + guardianSetId: 1, + }); + let vaa = wormhole.serializeVaaToBuffer(vaaHeader, vaaBody); + let pnauHeader = pyth.buildPnauHeader(); + let pnau = pyth.serializePnauToBuffer(pnauHeader, { + vaa, + pricesUpdates: actualPricesUpdates, + pricesUpdatesToSubmit, + }); + + 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.StxPriceIdentifier), + price: Cl.int(actualPricesUpdates.decoded[1].price), + conf: Cl.uint(actualPricesUpdates.decoded[1].conf), + "ema-conf": Cl.uint(actualPricesUpdates.decoded[1].emaConf), + "ema-price": Cl.int(actualPricesUpdates.decoded[1].emaPrice), + expo: Cl.int(actualPricesUpdates.decoded[1].expo), + "prev-publish-time": Cl.uint( + actualPricesUpdates.decoded[1].prevPublishTime, + ), + "publish-time": Cl.uint(actualPricesUpdates.decoded[1].publishTime), + }), + ]), + ); + }); }); diff --git a/unit-tests/pyth/ptgm.test.ts b/unit-tests/pyth/ptgm.test.ts index f312634..1d20180 100644 --- a/unit-tests/pyth/ptgm.test.ts +++ b/unit-tests/pyth/ptgm.test.ts @@ -13,7 +13,7 @@ const wormholeCoreContractName = "wormhole-core-v1"; describe("pyth-governance-v1::update-fee-value", () => { const accounts = simnet.getAccounts(); const sender = accounts.get("wallet_1")!; - const initialDeployer = accounts.get("deployer")!; + const initialFeeRecipient = "ST3CRXBDXQ2N5P7E25Q39MEX1HSMRDSEAP1JST19D"; const guardianSet = wormhole.generateGuardianSetKeychain(19); let updateFeeValue = { mantissa: 2n, @@ -66,7 +66,7 @@ describe("pyth-governance-v1::update-fee-value", () => { expect(Cl.ok(res.result)).toBeOk( Cl.tuple({ - address: Cl.standardPrincipal(initialDeployer), + address: Cl.standardPrincipal(initialFeeRecipient), exponent: Cl.uint(updateFeeValue.exponent), mantissa: Cl.uint(updateFeeValue.mantissa), }), @@ -985,3 +985,78 @@ describe("pyth-governance-v1::update-governance-data-source", () => { expect(res.result).toBeErr(Cl.uint(4006)); }); }); + +describe("pyth-governance-v1::update-stale-price-threshold", () => { + const accounts = simnet.getAccounts(); + const sender = accounts.get("wallet_1")!; + const guardianSet = wormhole.generateGuardianSetKeychain(19); + let updateStalePriceThreshold = { + threshold: 60n, + }; + let ptgmVaaPayload = pyth.buildPtgmVaaPayload({ updateStalePriceThreshold }); + + // Before starting the test suite, we have to setup the guardian set. + beforeEach(async () => { + wormhole.applyGuardianSetUpdate( + guardianSet, + 1, + sender, + wormholeCoreContractName, + ); + }); + + it("should update stale price threshold on successful updates", () => { + let payload = pyth.serializePtgmVaaPayloadToBuffer(ptgmVaaPayload); + let body = wormhole.buildValidVaaBodySpecs({ payload }); + 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], + sender, + ); + expect(res.result).toBeOk(Cl.uint(updateStalePriceThreshold.threshold)); + + res = simnet.callReadOnlyFn( + pythGovernanceContractName, + `get-stale-price-threshold`, + [], + sender, + ); + expect(Cl.ok(res.result)).toBeOk( + Cl.uint(updateStalePriceThreshold.threshold), + ); + }); + + it("should fail if action mismatches", () => { + ptgmVaaPayload.action = 0xff; + let payload = pyth.serializePtgmVaaPayloadToBuffer(ptgmVaaPayload); + let body = wormhole.buildValidVaaBodySpecs({ payload }); + 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], + sender, + ); + expect(res.result).toBeErr(Cl.uint(4001)); + }); +});