Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 56 additions & 24 deletions target_chains/ton/contracts/contracts/Pyth.fc
Original file line number Diff line number Diff line change
Expand Up @@ -156,50 +156,88 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {
return payload~load_uint(160); ;; Return root_digest
}


() update_price_feeds(int msg_value, slice data) impure {
load_data();
tuple parse_price_feeds_from_data(slice data) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tuple is how we deal with arrays in func

slice cs = read_and_verify_header(data);

int wormhole_proof_size_bytes = cs~load_uint(16);
(cell wormhole_proof, slice new_cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8);
cs = new_cs;

int num_updates = cs~load_uint(8);
int update_fee = single_update_fee * num_updates;
int compute_fee = get_compute_fee(
WORKCHAIN,
UPDATE_PRICE_FEEDS_BASE_GAS + (UPDATE_PRICE_FEEDS_PER_UPDATE_GAS * num_updates)
);
throw_unless(ERROR_INSUFFICIENT_GAS, msg_value >= compute_fee);
int remaining_msg_value = msg_value - compute_fee;

;; Check if the sender has sent enough TON to cover the update_fee
throw_unless(ERROR_INSUFFICIENT_FEE, remaining_msg_value >= update_fee);

(_, _, _, _, int emitter_chain_id, int emitter_address, _, _, slice payload, _) = parse_and_verify_wormhole_vm(wormhole_proof.begin_parse());

;; Check if the data source is valid
cell data_source = begin_cell()
.store_uint(emitter_chain_id, 16)
.store_uint(emitter_address, 256)
.end_cell();
.end_cell();

;; Dictionary doesn't support cell as key, so we use cell_hash to create a 256-bit integer key
int data_source_key = cell_hash(data_source);

(slice value, int found?) = is_valid_data_source.udict_get?(256, data_source_key);
throw_unless(ERROR_UPDATE_DATA_SOURCE_NOT_FOUND, found?);
int valid = value~load_int(1);
throw_unless(ERROR_INVALID_UPDATE_DATA_SOURCE, valid);


int root_digest = parse_pyth_payload_in_wormhole_vm(payload);

;; Create tuple to store price feeds
tuple price_feeds = empty_tuple();

repeat(num_updates) {
(int price_id, int price, int conf, int expo, int publish_time, _, int ema_price, int ema_conf, slice new_cs) = read_and_verify_message(cs, root_digest);
cs = new_cs;

;; Create price feed cell containing both current and EMA prices
cell price_feed_cell = begin_cell()
.store_ref(store_price(price, conf, expo, publish_time))
.store_ref(store_price(ema_price, ema_conf, expo, publish_time))
.end_cell();

tuple price_feed = empty_tuple();
price_feed~tpush(price_id);
price_feed~tpush(price_feed_cell);
price_feeds~tpush(price_feed);
}

throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, ~ cs.slice_empty?());

return price_feeds;
}

;; Returns a tuple of price feeds (max 255 due to TON limitations of tuple size)
tuple parse_price_feed_updates(slice data) method_id {
load_data();
tuple price_feeds = parse_price_feeds_from_data(data);
return price_feeds;
}

() update_price_feeds(int msg_value, slice data) impure {
load_data();
tuple price_feeds = parse_price_feeds_from_data(data);
int num_updates = price_feeds.tlen();
int update_fee = single_update_fee * num_updates;
int compute_fee = get_compute_fee(
WORKCHAIN,
UPDATE_PRICE_FEEDS_BASE_GAS + (UPDATE_PRICE_FEEDS_PER_UPDATE_GAS * num_updates)
);
throw_unless(ERROR_INSUFFICIENT_GAS, msg_value >= compute_fee);
int remaining_msg_value = msg_value - compute_fee;

;; Check if the sender has sent enough TON to cover the update_fee
throw_unless(ERROR_INSUFFICIENT_FEE, remaining_msg_value >= update_fee);

int i = 0;
while(i < num_updates) {
tuple price_feed = price_feeds.at(i);
int price_id = price_feed.at(0);
cell price_feed_cell = price_feed.at(1);
slice price_feed = price_feed_cell.begin_parse();
slice price = price_feed~load_ref().begin_parse();
slice ema_price = price_feed~load_ref().begin_parse();
(_, _, _, int publish_time) = parse_price(price);

(slice latest_price_info, int found?) = latest_price_feeds.udict_get?(256, price_id);
int latest_publish_time = 0;
if (found?) {
Expand All @@ -213,17 +251,11 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {
}

if (publish_time > latest_publish_time) {
cell price_feed = begin_cell()
.store_ref(store_price(price, conf, expo, publish_time))
.store_ref(store_price(ema_price, ema_conf, expo, publish_time))
.end_cell();

latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed).end_cell().begin_parse());
latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed_cell).end_cell().begin_parse());
}
i += 1;
}

throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, ~ cs.slice_empty?());

store_data();
}

Expand Down
1 change: 1 addition & 0 deletions target_chains/ton/contracts/contracts/common/utils.fc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
;; Built-in assembly functions
int keccak256(slice s) asm "1 PUSHINT HASHEXT_KECCAK256"; ;; Keccak-256 hash function
int keccak256_tuple(tuple t) asm "DUP TLEN EXPLODEVAR HASHEXT_KECCAK256";
int tlen(tuple t) asm "TLEN";

const MAX_BITS = 1016;

Expand Down
4 changes: 4 additions & 0 deletions target_chains/ton/contracts/contracts/tests/PythTest.fc
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@
(int) test_get_is_valid_data_source(cell data_source) method_id {
return get_is_valid_data_source(data_source);
}

(tuple) test_parse_price_feed_updates(slice data) method_id {
return parse_price_feed_updates(data);
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
return get_is_valid_data_source(data_source);
}

(tuple) test_parse_price_feed_updates(slice data) method_id {
return parse_price_feed_updates(data);
}

;; Add a new function to demonstrate the upgrade
(int) test_new_function() method_id {
return 1;
Expand Down
77 changes: 64 additions & 13 deletions target_chains/ton/contracts/tests/PythTest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ import {
HERMES_ETH_PRICE,
HERMES_ETH_PUBLISH_TIME,
HERMES_BTC_PUBLISH_TIME,
HERMES_BTC_CONF,
HERMES_BTC_EXPO,
HERMES_BTC_EMA_CONF,
HERMES_BTC_EMA_EXPO,
HERMES_BTC_EMA_PRICE,
HERMES_BTC_EMA_PUBLISH_TIME,
HERMES_ETH_CONF,
HERMES_ETH_EMA_CONF,
HERMES_ETH_EMA_EXPO,
HERMES_ETH_EMA_PRICE,
HERMES_ETH_EMA_PUBLISH_TIME,
HERMES_ETH_EXPO,
} from "./utils/pyth";
import { GUARDIAN_SET_0, MAINNET_UPGRADE_VAAS } from "./utils/wormhole";
import { DataSource } from "@pythnetwork/xc-admin-common";
Expand Down Expand Up @@ -376,19 +388,6 @@ describe("PythTest", () => {
const updateData = Buffer.from(HERMES_BTC_ETH_UPDATE, "hex");

let result = await pythTest.sendUpdatePriceFeeds(
deployer.getSender(),
updateData,
toNano("0.1") // Insufficient gas
);

expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: pythTest.address,
success: false,
exitCode: 3000, // ERROR_INSUFFICIENT_GAS
});

result = await pythTest.sendUpdatePriceFeeds(
deployer.getSender(),
updateData,
calculateUpdatePriceFeedsFee(1n) // Send enough gas for 1 update instead of 2
Expand Down Expand Up @@ -974,4 +973,56 @@ describe("PythTest", () => {
// Verify that the contract has not been upgraded by attempting to call the new method
await expect(pythTest.getNewFunction()).rejects.toThrow();
});

it("should successfully parse price feed updates", async () => {
await deployContract();
await updateGuardianSets(pythTest, deployer);

const result = await pythTest.getParsePriceFeedUpdates(
Buffer.from(HERMES_BTC_ETH_UPDATE, "hex")
);
expect(result).toEqual([
{
priceId: BTC_PRICE_FEED_ID,
price: {
price: HERMES_BTC_PRICE,
conf: HERMES_BTC_CONF,
expo: HERMES_BTC_EXPO,
publishTime: HERMES_BTC_PUBLISH_TIME,
},
emaPrice: {
price: HERMES_BTC_EMA_PRICE,
conf: HERMES_BTC_EMA_CONF,
expo: HERMES_BTC_EMA_EXPO,
publishTime: HERMES_BTC_EMA_PUBLISH_TIME,
},
},
{
priceId: ETH_PRICE_FEED_ID,
price: {
price: HERMES_ETH_PRICE,
conf: HERMES_ETH_CONF,
expo: HERMES_ETH_EXPO,
publishTime: HERMES_ETH_PUBLISH_TIME,
},
emaPrice: {
price: HERMES_ETH_EMA_PRICE,
conf: HERMES_ETH_EMA_CONF,
expo: HERMES_ETH_EMA_EXPO,
publishTime: HERMES_ETH_EMA_PUBLISH_TIME,
},
},
]);
});

it("should fail to parse invalid price feed updates", async () => {
await deployContract();
await updateGuardianSets(pythTest, deployer);

const invalidUpdateData = Buffer.from("invalid data");

await expect(
pythTest.getParsePriceFeedUpdates(invalidUpdateData)
).rejects.toThrow("Unable to execute get method. Got exit_code: 2002"); // ERROR_INVALID_MAGIC = 2002
});
});
15 changes: 15 additions & 0 deletions target_chains/ton/contracts/tests/utils/pyth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,25 @@ export const HERMES_BTC_ETH_UPDATE =
"504e41550100000003b801000000040d00a0bb18e08c0a4152eba8293e88d0ed43084dfd4677fd5dc0ff48b05d065e25511ea12181325e92541290f28e00487a7ed852fdecbee414cab803dbe1dac2392201023e177888eba8922eac9b0668566f15c61e945cd47c10fa4ca2e4d472d7d216f149f3378b4edc5f8d802c3ef9b8156ca53c9ae2d4f75dd91f7713946b4108c5910003af26c2426a1bf19f24a171bcc990dad056b670f76894e3bdb9925b21b40b3904757d8e6175133b8608431d7435e29c5fcc2912349c2c8b5588803c06f203c73401048a30050ebafd161c3cfa5531896040b6da88502734c8e42ca3197d52ea08f6ec785a24f24bc9325c16ee7b6a9791bc523523f9086162ed4ccf746b55e1b0f192010886c5256df6ca2719fe97c10b79a4a8c8574fb70add9bfe0d879ae5f6c69b2459360b50b58c43a65e881081174cce56827880e0c330b5c5681294dc3fcb78a86d010a4e0ebb1992f0e48263f6188cb5f8e871cdcd7879f54fe7ad53bbd28d5e7ff70e73441836f0d11076bd7a791aceb05d501500f6878cf26e641fffa7c8fd143371000b3ad70bd80e52a82cd740ffbd4a080bd22523bc7ac2b1242169516d7aaf2753cd7ee5b500134ef32c02284e9d806fbeab2e055ea4a94be9cbfcfbc39b249b5e6b010c97d60c0f15b18c8fb2f36331ab0a1ce0efa13e9f2118c32140bd2118823d50f12deffc40b5d0c9642b4e44a6bd1cf4f38de471536a6610e698a942f049abef35010da2004619d8b31e33037ffed4afdd97459a241dfc7fa3bcc426f175461c938a182db560547dfcdd8ede345f0cc69da33fd588c30e912b7521c3ac1b0455882628000e81868d37eb16e1988451c26cfea8bb7969ce11c89488cedea30c80e3416dd2147c0554e9a9cce1a864eb0db625baa2cbb226ae2c2f1051f84b0a711c4bf69647010f02f18088ddbabd7c4528a1f7582f5fb11e60c5e434e9fd4ca2b33d6646e2ac6e6459c651778d1531711b44d2a1204a0d9c17e218aba5e60800e80aade9f1d90400108c783ad40f93184ad4f7e84229b207b17099e78b8bd93ddf2434cba21c99b4a904d74555ced9977e6becc34fa346c3cca9332b3598e66e58eb56f9ac700074e0001270a31d95bd5426ffe943dcc2b93f05b93f8301848f0b19c56e0dea51b7742c467b6bb557f6fc6762ef4600988c2dbcad0a2be84d4c6839fbae05d227e30ce5f50166cec2c600000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71000000000493073601415557560000000000099e556100002710c0905b1576f0bb86fe861a51273f2bcc43d12dd702005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000005634a12c5d50000000096b3a0ebfffffff80000000066cec2c60000000066cec2c600000566c715d5a000000000895abaa20a83db6da7dfbe7cc34f56265123320d7765dda3ae132c1518c53ead6cde500c139f68a894f564d817c0dfaeefa80d4ed93d36b82f7fcfe80e4092bb54d4dae770124803c592f17cb918c9ac381ce82bd6817041aa5ae95d917d75687b7a389a188846dd79bd55cb6cb9b9d0e1c0c040f1110362a8e1e87c74887326d66b213d19f7dcd1766b6f8505be50f5c98783c07ec08f913cbe38c20a31e440e42bb5a8883356dd19288e618e938ae4e7031d52d684f7bd1ddcf0d4ff63844800e14ff0c6888ff26626ea9874005500ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace000000396f6987dd00000000052fbd87fffffff80000000066cec2c60000000066cec2c6000000398c4513280000000006c9a0a10a5307d6b780c8c5579b4b61de5fe0f12a789efdc628f14603df77ba31734c5611291df9a86e51430b243b3b61ee716bd758360783bdb7ef2f96ab8e92d509d85940a832b3de426d243a6d52ccf1e538af48b1bfad061dec1e4293898ca9ca9f37d0050d7b9e6a54357e77e7d79156fa1c70d54484ce52df49e79ab3fde203359f760fd9cf78be10644a43b52a171026829814590e8ba4853f6c8f0a16ce0fe3b532bcc96ea72723ab7d19f700c153e7cfb950f4aaa691e731e6f1de4edf43a6b9dd532a8ee785f084";

export const HERMES_BTC_PRICE = 5924002645461;
export const HERMES_BTC_CONF = 2528354539;
export const HERMES_BTC_EXPO = -8;
export const HERMES_BTC_PUBLISH_TIME = 1724826310;

export const HERMES_BTC_EMA_PRICE = 5938984900000;
export const HERMES_BTC_EMA_CONF = 2304424610;
export const HERMES_BTC_EMA_EXPO = -8;
export const HERMES_BTC_EMA_PUBLISH_TIME = 1724826310;

export const HERMES_ETH_PRICE = 246682322909;
export const HERMES_ETH_CONF = 87014791;
export const HERMES_ETH_EXPO = -8;
export const HERMES_ETH_PUBLISH_TIME = 1724826310;

export const HERMES_ETH_EMA_PRICE = 247166473000;
export const HERMES_ETH_EMA_CONF = 113877153;
export const HERMES_ETH_EMA_EXPO = -8;
export const HERMES_ETH_EMA_PUBLISH_TIME = 1724826310;

export const BTC_PRICE_FEED_ID =
"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43";

Expand Down
44 changes: 44 additions & 0 deletions target_chains/ton/contracts/wrappers/PythTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Cell,
contractAddress,
ContractProvider,
parseTuple,
Sender,
SendMode,
toNano,
Expand Down Expand Up @@ -196,6 +197,49 @@ export class PythTest extends BaseWrapper {
return parseDataSources(result.stack.readCell());
}

async getParsePriceFeedUpdates(provider: ContractProvider, vm: Buffer) {
const result = await provider.get("test_parse_price_feed_updates", [
{ type: "slice", cell: createCellChain(vm) },
]);
const tuple = result.stack.readTuple();
const prices = [];

// Get tuple length and iterate
const size = tuple.remaining;
for (let i = 0; i < size; i++) {
const item = tuple.readTuple();
const priceId =
"0x" + item.readBigNumber().toString(16).padStart(64, "0");
const priceFeedCell = item.readCell();
const priceFeedCellSlice = priceFeedCell.beginParse();

const priceSlice = priceFeedCellSlice.loadRef().beginParse();
const price = priceSlice.loadInt(64);
const conf = priceSlice.loadUint(64);
const expo = priceSlice.loadInt(32);
const publishTime = priceSlice.loadUint(64);

const emaPriceSlice = priceFeedCellSlice.loadRef().beginParse();
const emaPrice = emaPriceSlice.loadInt(64);
const emaConf = emaPriceSlice.loadUint(64);
const emaExpo = emaPriceSlice.loadInt(32);
const emaPublishTime = emaPriceSlice.loadUint(64);

prices.push({
priceId,
price: { price, conf, expo, publishTime },
emaPrice: {
price: emaPrice,
conf: emaConf,
expo: emaExpo,
publishTime: emaPublishTime,
},
});
}

return prices;
}

async getNewFunction(provider: ContractProvider) {
const result = await provider.get("test_new_function", []);
return result.stack.readNumber();
Expand Down
Loading