Skip to content

Commit d3d0f9f

Browse files
authored
feat(target_chains/ton): add parse_price_feed_updates (#2099)
* add parse_price_feed_updates * remove unnecessary comments * fix parse_price_feed_updates * uncomment test * add more tests * address comment * address comments * address comments
1 parent 9a36898 commit d3d0f9f

File tree

12 files changed

+933
-36
lines changed

12 files changed

+933
-36
lines changed

target_chains/ton/contracts/contracts/Main.fc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
cell data = in_msg_body~load_ref();
1717
slice data_slice = data.begin_parse();
1818

19+
;; Get sender address from message
20+
slice cs = in_msg_full.begin_parse();
21+
cs~skip_bits(4); ;; skip flags
22+
slice sender_address = cs~load_msg_addr(); ;; load sender address
23+
1924
;; * The remainder of the message body is specific for each supported value of `op`.
2025
if (op == OP_UPDATE_GUARDIAN_SET) {
2126
update_guardian_set(data_slice);
@@ -25,6 +30,18 @@
2530
execute_governance_action(data_slice);
2631
} elseif (op == OP_UPGRADE_CONTRACT) {
2732
execute_upgrade_contract(data);
33+
} elseif (op == OP_PARSE_PRICE_FEED_UPDATES) {
34+
cell price_ids_cell = in_msg_body~load_ref();
35+
slice price_ids_slice = price_ids_cell.begin_parse();
36+
int min_publish_time = in_msg_body~load_uint(64);
37+
int max_publish_time = in_msg_body~load_uint(64);
38+
parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address);
39+
} elseif (op == OP_PARSE_UNIQUE_PRICE_FEED_UPDATES) {
40+
cell price_ids_cell = in_msg_body~load_ref();
41+
slice price_ids_slice = price_ids_cell.begin_parse();
42+
int publish_time = in_msg_body~load_uint(64);
43+
int max_staleness = in_msg_body~load_uint(64);
44+
parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address);
2845
} else {
2946
throw(0xffff); ;; Throw exception for unknown op
3047
}

target_chains/ton/contracts/contracts/Pyth.fc

Lines changed: 242 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "common/merkle_tree.fc";
77
#include "common/governance_actions.fc";
88
#include "common/gas.fc";
9+
#include "common/op.fc";
910
#include "./Wormhole.fc";
1011

1112
cell store_price(int price, int conf, int expo, int publish_time) {
@@ -156,16 +157,7 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {
156157
return payload~load_uint(160); ;; Return root_digest
157158
}
158159

159-
160-
() update_price_feeds(int msg_value, slice data) impure {
161-
load_data();
162-
slice cs = read_and_verify_header(data);
163-
164-
int wormhole_proof_size_bytes = cs~load_uint(16);
165-
(cell wormhole_proof, slice new_cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8);
166-
cs = new_cs;
167-
168-
int num_updates = cs~load_uint(8);
160+
() calculate_and_validate_fees(int msg_value, int num_updates) impure {
169161
int update_fee = single_update_fee * num_updates;
170162
int compute_fee = get_compute_fee(
171163
WORKCHAIN,
@@ -176,30 +168,264 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {
176168

177169
;; Check if the sender has sent enough TON to cover the update_fee
178170
throw_unless(ERROR_INSUFFICIENT_FEE, remaining_msg_value >= update_fee);
171+
}
172+
173+
(int) find_price_id_index(tuple price_ids, int price_id) {
174+
int len = price_ids.tlen();
175+
int i = 0;
176+
while (i < len) {
177+
if (price_ids.at(i) == price_id) {
178+
return i;
179+
}
180+
i += 1;
181+
}
182+
return -1; ;; Not found
183+
}
184+
185+
186+
tuple parse_price_feeds_from_data(int msg_value, slice data, tuple price_ids, int min_publish_time, int max_publish_time, int unique) {
187+
slice cs = read_and_verify_header(data);
188+
189+
int wormhole_proof_size_bytes = cs~load_uint(16);
190+
(cell wormhole_proof, slice new_cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8);
191+
cs = new_cs;
192+
193+
int num_updates = cs~load_uint(8);
194+
195+
calculate_and_validate_fees(msg_value, num_updates);
179196

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

182199
;; Check if the data source is valid
183200
cell data_source = begin_cell()
184201
.store_uint(emitter_chain_id, 16)
185202
.store_uint(emitter_address, 256)
186-
.end_cell();
203+
.end_cell();
187204

188205
;; Dictionary doesn't support cell as key, so we use cell_hash to create a 256-bit integer key
189206
int data_source_key = cell_hash(data_source);
190-
191207
(slice value, int found?) = is_valid_data_source.udict_get?(256, data_source_key);
192208
throw_unless(ERROR_UPDATE_DATA_SOURCE_NOT_FOUND, found?);
193209
int valid = value~load_int(1);
194210
throw_unless(ERROR_INVALID_UPDATE_DATA_SOURCE, valid);
195211

196-
197212
int root_digest = parse_pyth_payload_in_wormhole_vm(payload);
198213

214+
;; Create dictionary to store price feeds in order (dict has a udict_get_next? method which returns the next key in order)
215+
cell ordered_feeds = new_dict();
216+
;; Track which price IDs we've found
217+
cell found_price_ids = new_dict();
218+
219+
int index = 0;
220+
199221
repeat(num_updates) {
200-
(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);
222+
(int price_id, int price, int conf, int expo, int publish_time, int prev_publish_time, int ema_price, int ema_conf, slice new_cs) = read_and_verify_message(cs, root_digest);
201223
cs = new_cs;
202224

225+
int price_ids_len = price_ids.tlen();
226+
227+
;; Check if we've already processed this price_id to avoid duplicates
228+
(_, int already_processed?) = found_price_ids.udict_get?(256, price_id);
229+
if (~ already_processed?) { ;; Only process if we haven't seen this price_id yet
230+
int should_include = (price_ids_len == 0)
231+
| ((price_ids_len > 0)
232+
& (publish_time >= min_publish_time)
233+
& (publish_time <= max_publish_time)
234+
& ((unique == 0) | (min_publish_time > prev_publish_time)));
235+
236+
if (should_include) {
237+
;; Create price feed cell containing both current and EMA prices
238+
cell price_feed_cell = begin_cell()
239+
.store_ref(store_price(price, conf, expo, publish_time))
240+
.store_ref(store_price(ema_price, ema_conf, expo, publish_time))
241+
.end_cell();
242+
243+
if (price_ids_len == 0) {
244+
ordered_feeds~udict_set(8, index, begin_cell()
245+
.store_uint(price_id, 256)
246+
.store_ref(price_feed_cell)
247+
.end_cell().begin_parse());
248+
index += 1;
249+
} else {
250+
index = find_price_id_index(price_ids, price_id);
251+
if (index >= 0) {
252+
ordered_feeds~udict_set(8, index, begin_cell()
253+
.store_uint(price_id, 256)
254+
.store_ref(price_feed_cell)
255+
.end_cell().begin_parse());
256+
}
257+
}
258+
259+
;; Mark this price ID as found
260+
found_price_ids~udict_set(256, price_id, begin_cell().store_int(true, 1).end_cell().begin_parse());
261+
}
262+
}
263+
}
264+
265+
throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, ~ cs.slice_empty?());
266+
267+
;; Verify all requested price IDs were found
268+
if (price_ids.tlen() > 0) {
269+
int i = 0;
270+
repeat(price_ids.tlen()) {
271+
int requested_id = price_ids.at(i);
272+
(_, int found?) = found_price_ids.udict_get?(256, requested_id);
273+
throw_unless(ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE, found?);
274+
i += 1;
275+
}
276+
}
277+
278+
;; Create final ordered tuple from dictionary
279+
tuple price_feeds = empty_tuple();
280+
int index = -1;
281+
do {
282+
(index, slice value, int success) = ordered_feeds.udict_get_next?(8, index);
283+
if (success) {
284+
tuple price_feed = empty_tuple();
285+
price_feed~tpush(value~load_uint(256)); ;; price_id
286+
price_feed~tpush(value~load_ref()); ;; price_feed_cell
287+
price_feeds~tpush(price_feed);
288+
}
289+
} until (~ success);
290+
291+
return price_feeds;
292+
}
293+
294+
;; Creates a chain of cells from price feeds, with each cell containing exactly one price_id (256 bits)
295+
;; and one ref to the price feed cell. Returns the head of the chain.
296+
;; Each cell now contains exactly:
297+
;; - One price_id (256 bits)
298+
;; - One ref to price_feed_cell
299+
;; - One optional ref to next cell in chain
300+
;; This approach is:
301+
;; - More consistent with TON's cell model
302+
;; - Easier to traverse and read individual price feeds
303+
;; - Cleaner separation of data
304+
;; - More predictable in terms of cell structure
305+
cell create_price_feed_cell_chain(tuple price_feeds) {
306+
cell result = null();
307+
308+
int i = price_feeds.tlen() - 1;
309+
while (i >= 0) {
310+
tuple price_feed = price_feeds.at(i);
311+
int price_id = price_feed.at(0);
312+
cell price_feed_cell = price_feed.at(1);
313+
314+
;; Create new cell with single price feed and chain to previous result
315+
builder current_builder = begin_cell()
316+
.store_uint(price_id, 256) ;; Store price_id
317+
.store_ref(price_feed_cell); ;; Store price data ref
318+
319+
;; Chain to previous cells if they exist
320+
if (~ cell_null?(result)) {
321+
current_builder = current_builder.store_ref(result);
322+
}
323+
324+
result = current_builder.end_cell();
325+
i -= 1;
326+
}
327+
328+
return result;
329+
}
330+
331+
() send_price_feeds_response(tuple price_feeds, int msg_value, int op, slice sender_address) impure {
332+
;; Build response cell with price feeds
333+
builder response = begin_cell()
334+
.store_uint(op, 32) ;; Response op
335+
.store_uint(price_feeds.tlen(), 8); ;; Number of price feeds
336+
337+
;; Create and store price feed cell chain
338+
cell price_feeds_cell = create_price_feed_cell_chain(price_feeds);
339+
response = response.store_ref(price_feeds_cell);
340+
341+
;; Build the complete message cell (https://docs.ton.org/v3/documentation/smart-contracts/message-management/sending-messages#message-layout)
342+
cell msg = begin_cell()
343+
.store_uint(0x18, 6)
344+
.store_slice(sender_address)
345+
.store_coins(0) ;; Will fill in actual amount after fee calculations
346+
.store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
347+
.store_ref(response.end_cell())
348+
.end_cell();
349+
350+
int num_price_feeds = price_feeds.tlen();
351+
352+
;; Number of cells in the message
353+
;; - 2 cells: msg + response
354+
int cells = 2 + num_price_feeds;
355+
356+
;; Bit layout per TL-B spec (https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb):
357+
;; - 6 bits: optimized way of serializing the tag and the first 4 fields
358+
;; - 256 bits: owner address
359+
;; - 128 bits: coins (VarUInteger 16) from grams$_ amount:(VarUInteger 16) = Grams
360+
;; - 107 bits: other data (extra_currencies + ihr_fee + fwd_fee + lt of transaction + unixtime of transaction + no init-field flag + inplace message body flag)
361+
;; - PRICE_FEED_BITS * num_price_feeds: space for each price feed
362+
int bits = 6 + 256 + 128 + 107 + (PRICE_FEED_BITS * num_price_feeds);
363+
int fwd_fee = get_forward_fee(cells, bits, WORKCHAIN);
364+
365+
;; Calculate all fees
366+
int compute_fee = get_compute_fee(WORKCHAIN, get_gas_consumed());
367+
int update_fee = single_update_fee * price_feeds.tlen();
368+
369+
;; Calculate total fees and remaining excess
370+
int total_fees = compute_fee + update_fee + fwd_fee;
371+
int excess = msg_value - total_fees;
372+
373+
;; Send response message back to sender with exact excess amount
374+
send_raw_message(begin_cell()
375+
.store_uint(0x18, 6)
376+
.store_slice(sender_address)
377+
.store_coins(excess)
378+
.store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
379+
.store_ref(response.end_cell())
380+
.end_cell(),
381+
0);
382+
}
383+
384+
() parse_price_feed_updates(int msg_value, slice update_data_slice, slice price_ids_slice, int min_publish_time, int max_publish_time, slice sender_address) impure {
385+
load_data();
386+
387+
;; Load price_ids tuple
388+
int price_ids_len = price_ids_slice~load_uint(8);
389+
tuple price_ids = empty_tuple();
390+
repeat(price_ids_len) {
391+
int price_id = price_ids_slice~load_uint(256);
392+
price_ids~tpush(price_id);
393+
}
394+
395+
tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, min_publish_time, max_publish_time, false);
396+
send_price_feeds_response(price_feeds, msg_value, OP_PARSE_PRICE_FEED_UPDATES, sender_address);
397+
}
398+
399+
() parse_unique_price_feed_updates(int msg_value, slice update_data_slice, slice price_ids_slice, int publish_time, int max_staleness, slice sender_address) impure {
400+
load_data();
401+
402+
;; Load price_ids tuple
403+
int price_ids_len = price_ids_slice~load_uint(8);
404+
tuple price_ids = empty_tuple();
405+
repeat(price_ids_len) {
406+
int price_id = price_ids_slice~load_uint(256);
407+
price_ids~tpush(price_id);
408+
}
409+
410+
tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, publish_time, publish_time + max_staleness, true);
411+
send_price_feeds_response(price_feeds, msg_value, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES, sender_address);
412+
}
413+
414+
() update_price_feeds(int msg_value, slice data) impure {
415+
load_data();
416+
tuple price_feeds = parse_price_feeds_from_data(msg_value, data, empty_tuple(), 0, 0, false);
417+
int num_updates = price_feeds.tlen();
418+
419+
int i = 0;
420+
while(i < num_updates) {
421+
tuple price_feed = price_feeds.at(i);
422+
int price_id = price_feed.at(0);
423+
cell price_feed_cell = price_feed.at(1);
424+
slice price_feed = price_feed_cell.begin_parse();
425+
slice price = price_feed~load_ref().begin_parse();
426+
slice ema_price = price_feed~load_ref().begin_parse();
427+
(int price_, int conf, int expo, int publish_time) = parse_price(price);
428+
203429
(slice latest_price_info, int found?) = latest_price_feeds.udict_get?(256, price_id);
204430
int latest_publish_time = 0;
205431
if (found?) {
@@ -213,17 +439,11 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {
213439
}
214440

215441
if (publish_time > latest_publish_time) {
216-
cell price_feed = begin_cell()
217-
.store_ref(store_price(price, conf, expo, publish_time))
218-
.store_ref(store_price(ema_price, ema_conf, expo, publish_time))
219-
.end_cell();
220-
221-
latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed).end_cell().begin_parse());
442+
latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed_cell).end_cell().begin_parse());
222443
}
444+
i += 1;
223445
}
224446

225-
throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, ~ cs.slice_empty?());
226-
227447
store_data();
228448
}
229449

target_chains/ton/contracts/contracts/common/constants.fc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ const int WORMHOLE_MERKLE_UPDATE_TYPE = 0;
1515

1616
const int PRICE_FEED_MESSAGE_TYPE = 0;
1717

18+
;; Structure:
19+
;; - 256 bits: price_id
20+
;; Price:
21+
;; - 64 bits: price
22+
;; - 64 bits: confidence
23+
;; - 32 bits: exponent
24+
;; - 64 bits: publish_time
25+
;; EMA Price:
26+
;; - 64 bits: price
27+
;; - 64 bits: confidence
28+
;; - 32 bits: exponent
29+
;; - 64 bits: publish_time
30+
const int PRICE_FEED_BITS = 256 + 224 + 224;
31+
1832
{-
1933
The main workchain ID in TON. Currently, TON has two blockchains:
2034
1. Masterchain: Used for system-level operations and consensus.

target_chains/ton/contracts/contracts/common/errors.fc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const int ERROR_INVALID_GOVERNANCE_MAGIC = 2016;
4343
const int ERROR_INVALID_GOVERNANCE_MODULE = 2017;
4444
const int ERROR_INVALID_CODE_HASH = 2018;
4545
const int ERROR_INVALID_PAYLOAD_LENGTH = 2019;
46+
const int ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE = 2020;
4647

4748
;; Common
4849
const int ERROR_INSUFFICIENT_GAS = 3000;

target_chains/ton/contracts/contracts/common/gas.fc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE";
2+
int get_gas_consumed() asm "GASCONSUMED";
3+
int get_forward_fee(int cells, int bits, int workchain) asm(cells bits workchain) "GETFORWARDFEE";
4+
25

36
;; 1 update: 262,567 gas
47
;; 2 updates: 347,791 (+85,224)

target_chains/ton/contracts/contracts/common/op.fc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ const int OP_UPDATE_GUARDIAN_SET = 1;
22
const int OP_UPDATE_PRICE_FEEDS = 2;
33
const int OP_EXECUTE_GOVERNANCE_ACTION = 3;
44
const int OP_UPGRADE_CONTRACT = 4;
5+
const int OP_PARSE_PRICE_FEED_UPDATES = 5;
6+
const int OP_PARSE_UNIQUE_PRICE_FEED_UPDATES = 6;

target_chains/ton/contracts/contracts/common/utils.fc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
;; Built-in assembly functions
44
int keccak256(slice s) asm "1 PUSHINT HASHEXT_KECCAK256"; ;; Keccak-256 hash function
55
int keccak256_tuple(tuple t) asm "DUP TLEN EXPLODEVAR HASHEXT_KECCAK256";
6+
int tlen(tuple t) asm "TLEN";
67

78
const MAX_BITS = 1016;
89

0 commit comments

Comments
 (0)