Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions target_chains/ton/contracts/contracts/Main.fc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
cell data = in_msg_body~load_ref();
slice data_slice = data.begin_parse();

;; Get sender address from message
slice cs = in_msg_full.begin_parse();
cs~skip_bits(4); ;; skip flags
slice sender_address = cs~load_msg_addr(); ;; load sender address

;; * The remainder of the message body is specific for each supported value of `op`.
if (op == OP_UPDATE_GUARDIAN_SET) {
update_guardian_set(data_slice);
Expand All @@ -25,6 +30,18 @@
execute_governance_action(data_slice);
} elseif (op == OP_UPGRADE_CONTRACT) {
execute_upgrade_contract(data);
} elseif (op == OP_PARSE_PRICE_FEED_UPDATES) {
cell price_ids_cell = in_msg_body~load_ref();
slice price_ids_slice = price_ids_cell.begin_parse();
int min_publish_time = in_msg_body~load_uint(64);
int max_publish_time = in_msg_body~load_uint(64);
parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address);
} elseif (op == OP_PARSE_UNIQUE_PRICE_FEED_UPDATES) {
cell price_ids_cell = in_msg_body~load_ref();
slice price_ids_slice = price_ids_cell.begin_parse();
int publish_time = in_msg_body~load_uint(64);
int max_staleness = in_msg_body~load_uint(64);
parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address);
} else {
throw(0xffff); ;; Throw exception for unknown op
}
Expand Down
264 changes: 242 additions & 22 deletions target_chains/ton/contracts/contracts/Pyth.fc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "common/merkle_tree.fc";
#include "common/governance_actions.fc";
#include "common/gas.fc";
#include "common/op.fc";
#include "./Wormhole.fc";

cell store_price(int price, int conf, int expo, int publish_time) {
Expand Down Expand Up @@ -156,16 +157,7 @@ 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();
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);
() calculate_and_validate_fees(int msg_value, int num_updates) impure {
int update_fee = single_update_fee * num_updates;
int compute_fee = get_compute_fee(
WORKCHAIN,
Expand All @@ -176,30 +168,264 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {

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

(int) find_price_id_index(tuple price_ids, int price_id) {
int len = price_ids.tlen();
int i = 0;
while (i < len) {
if (price_ids.at(i) == price_id) {
return i;
}
i += 1;
}
return -1; ;; Not found
}


tuple parse_price_feeds_from_data(int msg_value, slice data, tuple price_ids, int min_publish_time, int max_publish_time, int unique) {
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);

calculate_and_validate_fees(msg_value, num_updates);

(_, _, _, _, 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 dictionary to store price feeds in order (dict has a udict_get_next? method which returns the next key in order)
cell ordered_feeds = new_dict();
;; Track which price IDs we've found
cell found_price_ids = new_dict();

int index = 0;

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);
(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);
cs = new_cs;

int price_ids_len = price_ids.tlen();

;; Check if we've already processed this price_id to avoid duplicates
(_, int already_processed?) = found_price_ids.udict_get?(256, price_id);
if (~ already_processed?) { ;; Only process if we haven't seen this price_id yet
int should_include = (price_ids_len == 0)
| ((price_ids_len > 0)
& (publish_time >= min_publish_time)
& (publish_time <= max_publish_time)
& ((unique == 0) | (min_publish_time > prev_publish_time)));

if (should_include) {
;; 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();

if (price_ids_len == 0) {
ordered_feeds~udict_set(8, index, begin_cell()
.store_uint(price_id, 256)
.store_ref(price_feed_cell)
.end_cell().begin_parse());
index += 1;
} else {
Comment on lines +243 to +249
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is probably when having a top level if to return in this case makes the code cleaner.

index = find_price_id_index(price_ids, price_id);
if (index >= 0) {
ordered_feeds~udict_set(8, index, begin_cell()
.store_uint(price_id, 256)
.store_ref(price_feed_cell)
.end_cell().begin_parse());
}
}

;; Mark this price ID as found
found_price_ids~udict_set(256, price_id, begin_cell().store_int(true, 1).end_cell().begin_parse());
}
}
}

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

;; Verify all requested price IDs were found
if (price_ids.tlen() > 0) {
int i = 0;
repeat(price_ids.tlen()) {
int requested_id = price_ids.at(i);
(_, int found?) = found_price_ids.udict_get?(256, requested_id);
throw_unless(ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE, found?);
i += 1;
}
}

;; Create final ordered tuple from dictionary
tuple price_feeds = empty_tuple();
int index = -1;
do {
(index, slice value, int success) = ordered_feeds.udict_get_next?(8, index);
if (success) {
tuple price_feed = empty_tuple();
price_feed~tpush(value~load_uint(256)); ;; price_id
price_feed~tpush(value~load_ref()); ;; price_feed_cell
price_feeds~tpush(price_feed);
}
} until (~ success);

return price_feeds;
}

;; Creates a chain of cells from price feeds, with each cell containing exactly one price_id (256 bits)
;; and one ref to the price feed cell. Returns the head of the chain.
;; Each cell now contains exactly:
;; - One price_id (256 bits)
;; - One ref to price_feed_cell
;; - One optional ref to next cell in chain
;; This approach is:
;; - More consistent with TON's cell model
;; - Easier to traverse and read individual price feeds
;; - Cleaner separation of data
;; - More predictable in terms of cell structure
cell create_price_feed_cell_chain(tuple price_feeds) {
cell result = null();

int i = price_feeds.tlen() - 1;
while (i >= 0) {
tuple price_feed = price_feeds.at(i);
int price_id = price_feed.at(0);
Copy link
Contributor Author

@cctdaniel cctdaniel Nov 11, 2024

Choose a reason for hiding this comment

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

0 is price_id, 1 is price_feed_cell

cell price_feed_cell = price_feed.at(1);

;; Create new cell with single price feed and chain to previous result
builder current_builder = begin_cell()
.store_uint(price_id, 256) ;; Store price_id
.store_ref(price_feed_cell); ;; Store price data ref

;; Chain to previous cells if they exist
if (~ cell_null?(result)) {
current_builder = current_builder.store_ref(result);
}

result = current_builder.end_cell();
i -= 1;
}

return result;
}

() send_price_feeds_response(tuple price_feeds, int msg_value, int op, slice sender_address) impure {
;; Build response cell with price feeds
builder response = begin_cell()
.store_uint(op, 32) ;; Response op
.store_uint(price_feeds.tlen(), 8); ;; Number of price feeds

;; Create and store price feed cell chain
cell price_feeds_cell = create_price_feed_cell_chain(price_feeds);
response = response.store_ref(price_feeds_cell);

;; Build the complete message cell (https://docs.ton.org/v3/documentation/smart-contracts/message-management/sending-messages#message-layout)
cell msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(sender_address)
.store_coins(0) ;; Will fill in actual amount after fee calculations
.store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_ref(response.end_cell())
.end_cell();

int num_price_feeds = price_feeds.tlen();

;; Number of cells in the message
;; - 2 cells: msg + response
int cells = 2 + num_price_feeds;

;; Bit layout per TL-B spec (https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb):
;; - 6 bits: optimized way of serializing the tag and the first 4 fields
;; - 256 bits: owner address
;; - 128 bits: coins (VarUInteger 16) from grams$_ amount:(VarUInteger 16) = Grams
;; - 107 bits: other data (extra_currencies + ihr_fee + fwd_fee + lt of transaction + unixtime of transaction + no init-field flag + inplace message body flag)
;; - PRICE_FEED_BITS * num_price_feeds: space for each price feed
int bits = 6 + 256 + 128 + 107 + (PRICE_FEED_BITS * num_price_feeds);
int fwd_fee = get_forward_fee(cells, bits, WORKCHAIN);

;; Calculate all fees
int compute_fee = get_compute_fee(WORKCHAIN, get_gas_consumed());
int update_fee = single_update_fee * price_feeds.tlen();

;; Calculate total fees and remaining excess
int total_fees = compute_fee + update_fee + fwd_fee;
int excess = msg_value - total_fees;

;; Send response message back to sender with exact excess amount
send_raw_message(begin_cell()
.store_uint(0x18, 6)
.store_slice(sender_address)
.store_coins(excess)
.store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_ref(response.end_cell())
.end_cell(),
0);
}

() 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 {
load_data();

;; Load price_ids tuple
int price_ids_len = price_ids_slice~load_uint(8);

Choose a reason for hiding this comment

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

Should price_ids_len == 0 be supported? This will then be treated like a call from update_price_feeds within parse_price_feeds_from_data and everything will be included, meaning min_publish_time and max_publish_time are ignored.

tuple price_ids = empty_tuple();
repeat(price_ids_len) {
int price_id = price_ids_slice~load_uint(256);
price_ids~tpush(price_id);
}

tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, min_publish_time, max_publish_time, false);
send_price_feeds_response(price_feeds, msg_value, OP_PARSE_PRICE_FEED_UPDATES, sender_address);
}

() 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 {
load_data();

;; Load price_ids tuple
int price_ids_len = price_ids_slice~load_uint(8);
tuple price_ids = empty_tuple();
repeat(price_ids_len) {
int price_id = price_ids_slice~load_uint(256);
price_ids~tpush(price_id);
}

tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, publish_time, publish_time + max_staleness, true);
send_price_feeds_response(price_feeds, msg_value, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES, sender_address);
}

() update_price_feeds(int msg_value, slice data) impure {
load_data();
tuple price_feeds = parse_price_feeds_from_data(msg_value, data, empty_tuple(), 0, 0, false);
int num_updates = price_feeds.tlen();

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 price_, int conf, int expo, 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 +439,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
14 changes: 14 additions & 0 deletions target_chains/ton/contracts/contracts/common/constants.fc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ const int WORMHOLE_MERKLE_UPDATE_TYPE = 0;

const int PRICE_FEED_MESSAGE_TYPE = 0;

;; Structure:
;; - 256 bits: price_id
;; Price:
;; - 64 bits: price
;; - 64 bits: confidence
;; - 32 bits: exponent
;; - 64 bits: publish_time
;; EMA Price:
;; - 64 bits: price
;; - 64 bits: confidence
;; - 32 bits: exponent
;; - 64 bits: publish_time
const int PRICE_FEED_BITS = 256 + 224 + 224;

{-
The main workchain ID in TON. Currently, TON has two blockchains:
1. Masterchain: Used for system-level operations and consensus.
Expand Down
1 change: 1 addition & 0 deletions target_chains/ton/contracts/contracts/common/errors.fc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const int ERROR_INVALID_GOVERNANCE_MAGIC = 2016;
const int ERROR_INVALID_GOVERNANCE_MODULE = 2017;
const int ERROR_INVALID_CODE_HASH = 2018;
const int ERROR_INVALID_PAYLOAD_LENGTH = 2019;
const int ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE = 2020;

;; Common
const int ERROR_INSUFFICIENT_GAS = 3000;
3 changes: 3 additions & 0 deletions target_chains/ton/contracts/contracts/common/gas.fc
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE";
int get_gas_consumed() asm "GASCONSUMED";
int get_forward_fee(int cells, int bits, int workchain) asm(cells bits workchain) "GETFORWARDFEE";


;; 1 update: 262,567 gas
;; 2 updates: 347,791 (+85,224)
Expand Down
2 changes: 2 additions & 0 deletions target_chains/ton/contracts/contracts/common/op.fc
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ const int OP_UPDATE_GUARDIAN_SET = 1;
const int OP_UPDATE_PRICE_FEEDS = 2;
const int OP_EXECUTE_GOVERNANCE_ACTION = 3;
const int OP_UPGRADE_CONTRACT = 4;
const int OP_PARSE_PRICE_FEED_UPDATES = 5;
const int OP_PARSE_UNIQUE_PRICE_FEED_UPDATES = 6;
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
Loading
Loading