Skip to content

Commit

Permalink
feat: introduce stale-price-threshold
Browse files Browse the repository at this point in the history
  • Loading branch information
Ludo Galabru committed Dec 1, 2023
1 parent bacd763 commit 697dd5f
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 10 deletions.
27 changes: 27 additions & 0 deletions contracts/pyth-governance-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -59,6 +61,7 @@
(define-data-var fee-value
{ mantissa: uint, exponent: uint }
{ mantissa: u1, exponent: u1 })
(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)

Expand Down Expand Up @@ -115,6 +118,9 @@
(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))

Expand All @@ -133,6 +139,21 @@
(var-set fee-value updated-data)
(ok updated-data))))

(define-public (update-stale-price-threshold (vaa-bytes (buff 8192)) (wormhole-core-contract <wormhole-core-trait>))
(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 <wormhole-core-trait>))
(let ((expected-execution-plan (var-get current-execution-plan))
(vaa (try! (contract-call? wormhole-core-contract parse-and-verify-vaa vaa-bytes)))
Expand Down Expand Up @@ -349,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))
Expand Down
2 changes: 2 additions & 0 deletions contracts/pyth-store-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
(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)
Expand Down
21 changes: 19 additions & 2 deletions unit-tests/pyth/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export namespace pyth {
targetChainId: number;
updateFeeValue?: PtgmUpdateFeeValue;
updateFeeRecipient?: PtgmUpdateFeeRecipient;
updateStalePriceThreshold?: PtgmUpdateStalePriceThreshold;
updateWormholeContract?: PtgmUpdateContract;
updateOracleContract?: PtgmUpdateContract;
updateStoreContract?: PtgmUpdateContract;
Expand All @@ -142,6 +143,10 @@ export namespace pyth {
exponent: bigint;
}

export interface PtgmUpdateStalePriceThreshold {
threshold: bigint;
}

export interface PtgmUpdateFeeRecipient {
address: string;
contractName?: string;
Expand All @@ -161,6 +166,7 @@ export namespace pyth {
updateFeeRecipient?: PtgmUpdateFeeRecipient;
updatePricesDataSources?: wormhole.Emitter[];
updateWormholeContract?: PtgmUpdateContract;
updateStalePriceThreshold?: PtgmUpdateStalePriceThreshold;
updateOracleContract?: PtgmUpdateContract;
updateStoreContract?: PtgmUpdateContract;
updateDecoderContract?: PtgmUpdateContract;
Expand Down Expand Up @@ -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) {
Expand All @@ -282,6 +290,7 @@ export namespace pyth {
targetChainId: opts?.targetChainId || 50039,
module: opts?.module || 3,
updateFeeRecipient: opts?.updateFeeRecipient,
updateStalePriceThreshold: opts?.updateStalePriceThreshold,
updateFeeValue: opts?.updateFeeValue,
updateOracleContract: opts?.updateOracleContract,
updateWormholeContract: opts?.updateWormholeContract,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
112 changes: 104 additions & 8 deletions unit-tests/pyth/pnau.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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),
}),
]),
);
});
});
75 changes: 75 additions & 0 deletions unit-tests/pyth/ptgm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
});

0 comments on commit 697dd5f

Please sign in to comment.