From fa256267d0554c60101ee711bc7a10511c0c1834 Mon Sep 17 00:00:00 2001 From: Ludo Galabru Date: Wed, 27 Dec 2023 10:43:39 -0500 Subject: [PATCH 1/3] feat: add decode-price-feeds to oracle --- Clarinet.toml | 4 +- contracts/pyth-governance-v1.clar | 2 +- ...yth-oracle-v1.clar => pyth-oracle-v2.clar} | 20 ++++++++ unit-tests/pyth/oracle.test.ts | 4 +- unit-tests/pyth/pnau.test.ts | 50 ++++++++++++++++++- unit-tests/pyth/ptgm.test.ts | 2 +- 6 files changed, 75 insertions(+), 7 deletions(-) rename contracts/{pyth-oracle-v1.clar => pyth-oracle-v2.clar} (66%) diff --git a/Clarinet.toml b/Clarinet.toml index af380ed..10787aa 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -24,8 +24,8 @@ path = 'contracts/pyth-governance-v1.clar' clarity_version = 2 epoch = 2.4 -[contracts.pyth-oracle-v1] -path = 'contracts/pyth-oracle-v1.clar' +[contracts.pyth-oracle-v2] +path = 'contracts/pyth-oracle-v2.clar' clarity_version = 2 epoch = 2.4 diff --git a/contracts/pyth-governance-v1.clar b/contracts/pyth-governance-v1.clar index 4ac891c..4f46dcf 100644 --- a/contracts/pyth-governance-v1.clar +++ b/contracts/pyth-governance-v1.clar @@ -72,7 +72,7 @@ pyth-storage-contract: principal, wormhole-core-contract: principal } { - pyth-oracle-contract: .pyth-oracle-v1, + pyth-oracle-contract: .pyth-oracle-v2, pyth-decoder-contract: .pyth-pnau-decoder-v1, pyth-storage-contract: .pyth-store-v1, wormhole-core-contract: .wormhole-core-v1 diff --git a/contracts/pyth-oracle-v1.clar b/contracts/pyth-oracle-v2.clar similarity index 66% rename from contracts/pyth-oracle-v1.clar rename to contracts/pyth-oracle-v2.clar index aeb5cff..05560d1 100644 --- a/contracts/pyth-oracle-v1.clar +++ b/contracts/pyth-oracle-v2.clar @@ -42,3 +42,23 @@ ;; Charge fee (unwrap! (stx-transfer? fee-amount tx-sender (get address fee-info)) ERR_BALANCE_INSUFFICIENT) (ok updated-prices)))) + +(define-public (decode-price-feeds + (price-feed-bytes (buff 8192)) + (execution-plan { + pyth-storage-contract: , + pyth-decoder-contract: , + wormhole-core-contract: + })) + (begin + ;; Check execution flow + (try! (contract-call? .pyth-governance-v1 check-execution-flow contract-caller (some execution-plan))) + ;; Perform contract-call + (let ((pyth-decoder-contract (get pyth-decoder-contract execution-plan)) + (wormhole-core-contract (get wormhole-core-contract execution-plan)) + (decoded-prices (try! (contract-call? pyth-decoder-contract decode-and-verify-price-feeds price-feed-bytes wormhole-core-contract))) + (fee-info (contract-call? .pyth-governance-v1 get-fee-info)) + (fee-amount (* (len decoded-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) + (ok decoded-prices)))) diff --git a/unit-tests/pyth/oracle.test.ts b/unit-tests/pyth/oracle.test.ts index fa9d04d..88e2115 100644 --- a/unit-tests/pyth/oracle.test.ts +++ b/unit-tests/pyth/oracle.test.ts @@ -4,12 +4,12 @@ import { ParsedTransactionResult } from "@hirosystems/clarinet-sdk"; import { pnauMainnetVaas } from "./fixtures"; import { wormhole } from "../wormhole/helpers"; -const pythOracleContractName = "pyth-oracle-v1"; +const pythOracleContractName = "pyth-oracle-v2"; const pythDecoderPnauContractName = "pyth-pnau-decoder-v1"; const pythStorageContractName = "pyth-store-v1"; const wormholeCoreContractName = "wormhole-core-v1"; -describe("pyth-oracle-v1::decode-and-verify-price-feeds mainnet VAAs", () => { +describe("pyth-oracle-v2::decode-and-verify-price-feeds mainnet VAAs", () => { const accounts = simnet.getAccounts(); const sender = accounts.get("wallet_1")!; diff --git a/unit-tests/pyth/pnau.test.ts b/unit-tests/pyth/pnau.test.ts index 647b62e..aa56ded 100644 --- a/unit-tests/pyth/pnau.test.ts +++ b/unit-tests/pyth/pnau.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from "vitest"; import { wormhole } from "../wormhole/helpers"; import { pyth } from "./helpers"; -const pythOracleContractName = "pyth-oracle-v1"; +const pythOracleContractName = "pyth-oracle-v2"; const pythDecoderPnauContractName = "pyth-pnau-decoder-v1"; const pythGovernanceContractName = "pyth-governance-v1"; const pythStorageContractName = "pyth-store-v1"; @@ -788,5 +788,53 @@ describe("pyth-pnau-decoder-v1::decode-and-verify-price-feeds failures", () => { }), ]), ); + + // decode-price-feeds should not be filtering the outdated results + res = simnet.callPublicFn( + pythOracleContractName, + "decode-price-feeds", + [Cl.buffer(pnau), executionPlan], + sender, + ); + expect(res.result).toBeOk( + Cl.list([ + Cl.tuple({ + "price-identifier": Cl.buffer(pyth.BtcPriceIdentifier), + price: Cl.int(actualPricesUpdates.decoded[0].price), + conf: Cl.uint(actualPricesUpdates.decoded[0].conf), + "ema-conf": Cl.uint(actualPricesUpdates.decoded[0].emaConf), + "ema-price": Cl.int(actualPricesUpdates.decoded[0].emaPrice), + expo: Cl.int(actualPricesUpdates.decoded[0].expo), + "prev-publish-time": Cl.uint( + actualPricesUpdates.decoded[0].prevPublishTime, + ), + "publish-time": Cl.uint(actualPricesUpdates.decoded[0].publishTime), + }), + 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), + }), + 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), + }), + ]), + ); }); }); diff --git a/unit-tests/pyth/ptgm.test.ts b/unit-tests/pyth/ptgm.test.ts index 99d853f..f18bb9e 100644 --- a/unit-tests/pyth/ptgm.test.ts +++ b/unit-tests/pyth/ptgm.test.ts @@ -616,7 +616,7 @@ describe("pyth-governance-v1::update-pyth-oracle-contract", () => { const guardianSet = wormhole.generateGuardianSetKeychain(19); let updateOracleContract = { address: "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG", - contractName: "pyth-oracle-v2", + contractName: "pyth-oracle-new-version", }; let ptgmVaaPayload = pyth.buildPtgmVaaPayload({ updateOracleContract }); From a91e95ae88a816e1a206283dd863dd73016ea08f Mon Sep 17 00:00:00 2001 From: Ludo Galabru Date: Wed, 27 Dec 2023 10:54:17 -0500 Subject: [PATCH 2/3] chore: remove helper --- Clarinet.toml | 5 ----- contracts/pyth-helper-v1.clar | 12 ------------ unit-tests/pyth/ptgm.test.ts | 2 +- 3 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 contracts/pyth-helper-v1.clar diff --git a/Clarinet.toml b/Clarinet.toml index 10787aa..de8d8c1 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -14,11 +14,6 @@ contract_id = 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-ecc-v1' [[project.requirements]] contract_id = 'SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F.hk-merkle-tree-keccak160-v1' -[contracts.pyth-helper-v1] -path = 'contracts/pyth-helper-v1.clar' -clarity_version = 2 -epoch = 2.4 - [contracts.pyth-governance-v1] path = 'contracts/pyth-governance-v1.clar' clarity_version = 2 diff --git a/contracts/pyth-helper-v1.clar b/contracts/pyth-helper-v1.clar deleted file mode 100644 index f67d8ac..0000000 --- a/contracts/pyth-helper-v1.clar +++ /dev/null @@ -1,12 +0,0 @@ - -(define-public (verify-and-update-price (pnau-bytes (buff 8192))) - (contract-call? .pyth-oracle-v1 verify-and-update-price-feeds - pnau-bytes - { - pyth-storage-contract: .pyth-store-v1, - pyth-decoder-contract: .pyth-pnau-decoder-v1, - wormhole-core-contract: .wormhole-core-v1 - })) - -(define-public (read-price (price-feed-id (buff 32))) - (contract-call? .pyth-oracle-v1 read-price-feed price-feed-id .pyth-store-v1)) diff --git a/unit-tests/pyth/ptgm.test.ts b/unit-tests/pyth/ptgm.test.ts index f18bb9e..c06fc68 100644 --- a/unit-tests/pyth/ptgm.test.ts +++ b/unit-tests/pyth/ptgm.test.ts @@ -6,7 +6,7 @@ import { hexToBytes } from "@noble/hashes/utils"; import { ParsedTransactionResult } from "@hirosystems/clarinet-sdk"; import { ptgmTestnetVaas } from "./fixtures"; -const pythOracleContractName = "pyth-oracle-v1"; +const pythOracleContractName = "pyth-oracle-v2"; const pythStorageContractName = "pyth-store-v1"; const pythDecoderPnauContractName = "pyth-pnau-decoder-v1"; const pythGovernanceContractName = "pyth-governance-v1"; From 4a6ae2eedc2753b4d1f3d17858f94e4ac24550d8 Mon Sep 17 00:00:00 2001 From: Ludo Galabru Date: Wed, 27 Dec 2023 11:00:58 -0500 Subject: [PATCH 3/3] chore: update readme --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 809665a..accffef 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,14 @@ That can be consumed with the following invocation: ```clarity (contract-call? - 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.pyth-helper-v1 ;; Address of the helper contract + 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.pyth-oracle-v2 ;; Address of the helper contract read-price 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43) ;; BTC-USD price identifier + { + pyth-storage-contract: 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.pyth-storage-v1, + pyth-decoder-contract: 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.pyth-pnau-decoder-v1, + wormhole-core-contract: 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.wormhole-core-v1 + } ``` The authenticity of the price feeds is verified during their ingestion, making the cost of queries as light as possible. @@ -121,9 +126,14 @@ This VAA can be encoded as a Clarity buffer, and submitted to the Pyth contract ```clarity (contract-call? - 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.pyth-helper-v1 ;; Address of the helper contract + 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.pyth-oracle-v2 ;; Address of the helper contract verify-and-update-price - 0x504e41550100000003b8...a7b10321ad7c2404a910) ;; BTC-USD price update + 0x504e41550100000003b8...a7b10321ad7c2404a910 ;; BTC-USD price update + { + pyth-storage-contract: 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.pyth-storage-v1, + pyth-decoder-contract: 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.pyth-pnau-decoder-v1, + wormhole-core-contract: 'SP2T5JKWWP3FYYX4YRK8GK5BG2YCNGEAEY2P2PKN0.wormhole-core-v1 + }) ``` If the VAA is valid, the contract call will return a payload with the following signature: