Skip to content

Commit

Permalink
Merge pull request #25 from hirosystems/fix/secure-prices-updates
Browse files Browse the repository at this point in the history
fix: stale-price-threshold - access time
  • Loading branch information
Ludo Galabru authored Dec 2, 2023
2 parents 15c4f5b + 17b948c commit 8e780c1
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 40 deletions.
2 changes: 1 addition & 1 deletion contracts/pyth-store-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 39 additions & 6 deletions unit-tests/pyth/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
Expand Down Expand Up @@ -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
Expand Down
138 changes: 105 additions & 33 deletions unit-tests/pyth/pnau.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
},
],
]);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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),
}),
]),
);
});
Expand Down

0 comments on commit 8e780c1

Please sign in to comment.