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
45 changes: 45 additions & 0 deletions target_chains/ton/contracts/contracts/Main.fc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,25 @@
#include "Wormhole.fc";
#include "Pyth.fc";

;; @title Pyth Network Price Oracle Contract for TON
;; @notice This contract serves as the main entry point for the Pyth Network price oracle on TON.
;; @dev The contract handles various operations including:
;; - Updating guardian sets for Wormhole message verification
;; - Updating price feeds with the latest price data
;; - Executing governance actions
;; - Upgrading the contract code
;; - Parsing price feed updates for clients
;;
;; The contract uses Wormhole's cross-chain messaging protocol to verify price updates
;; and governance actions. It maintains a dictionary of price feeds indexed by price ID.
;; Each price feed contains the current price, confidence interval, exponent, and publish time.

;; Internal message handler
;; @param my_balance - Current contract balance
;; @param msg_value - Amount of TON sent with the message
;; @param in_msg_full - Full incoming message cell
;; @param in_msg_body - Message body as a slice
;; @returns () - No return value
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) { ;; ignore empty messages
return ();
Expand All @@ -26,14 +44,32 @@

;; * The remainder of the message body is specific for each supported value of `op`.
if (op == OP_UPDATE_GUARDIAN_SET) {
;; @notice Updates the guardian set based on a Wormhole VAA
;; @param data_slice - Slice containing the VAA with guardian set update information
update_guardian_set(data_slice);
} elseif (op == OP_UPDATE_PRICE_FEEDS) {
;; @notice Updates price feeds with the latest price data
;; @param msg_value - Amount of TON sent with the message (used for fee calculation)
;; @param data_slice - Slice containing the price feed update data
update_price_feeds(msg_value, data_slice);
} elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
;; @notice Executes a governance action based on a Wormhole VAA
;; @param data_slice - Slice containing the VAA with governance action information
execute_governance_action(data_slice);
} elseif (op == OP_UPGRADE_CONTRACT) {
;; @notice Upgrades the contract code
;; @param data - Cell containing the new contract code
execute_upgrade_contract(data);
} elseif (op == OP_PARSE_PRICE_FEED_UPDATES) {
;; @notice Parses price feed updates and returns the results to the caller
;; @param msg_value - Amount of TON sent with the message (used for fee calculation)
;; @param data_slice - Slice containing the price feed update data
;; @param price_ids_slice - Slice containing the price IDs to filter for
;; @param min_publish_time - Minimum publish time for price updates to be considered
;; @param max_publish_time - Maximum publish time for price updates to be considered
;; @param sender_address - Address of the sender (for response)
;; @param target_address - Address to send the response to
;; @param custom_payload - Custom payload to include in the response
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);
Expand All @@ -43,6 +79,15 @@
slice custom_payload = custom_payload_cell.begin_parse();
parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address, target_address, custom_payload);
} elseif (op == OP_PARSE_UNIQUE_PRICE_FEED_UPDATES) {
;; @notice Parses unique price feed updates (only the latest for each price ID) and returns the results to the caller
;; @param msg_value - Amount of TON sent with the message (used for fee calculation)
;; @param data_slice - Slice containing the price feed update data
;; @param price_ids_slice - Slice containing the price IDs to filter for
;; @param publish_time - Target publish time for price updates
;; @param max_staleness - Maximum allowed staleness of price updates (in seconds)
;; @param sender_address - Address of the sender (for response)
;; @param target_address - Address to send the response to
;; @param custom_payload - Custom payload to include in the response
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);
Expand Down
54 changes: 34 additions & 20 deletions target_chains/ton/contracts/contracts/Pyth.fc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "common/governance_actions.fc";
#include "common/gas.fc";
#include "common/op.fc";
#include "common/error_handling.fc";
#include "./Wormhole.fc";

cell store_price(int price, int conf, int expo, int publish_time) {
Expand Down Expand Up @@ -369,33 +370,46 @@ cell create_price_feed_cell_chain(tuple price_feeds) {
}

() 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, slice target_address, slice custom_payload) impure {
load_data();
try {
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);
}

;; 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, min_publish_time, max_publish_time, false);
send_price_feeds_response(price_feeds, msg_value, OP_PARSE_PRICE_FEED_UPDATES,
sender_address, target_address, custom_payload);
} catch (_, error_code) {
;; Handle any unexpected errors
emit_error(error_code, OP_PARSE_PRICE_FEED_UPDATES,
sender_address, begin_cell().store_slice(custom_payload).end_cell());
}

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, target_address, custom_payload);
}

() 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, slice target_address, slice custom_payload) impure {
load_data();
try {
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);
}

;; 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, target_address, custom_payload);
} catch (_, error_code) {
;; Handle any unexpected errors
emit_error(error_code, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES,
sender_address, begin_cell().store_slice(custom_payload).end_cell());
}

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, target_address, custom_payload);
}

() update_price_feeds(int msg_value, slice data) impure {
Expand Down
44 changes: 44 additions & 0 deletions target_chains/ton/contracts/contracts/common/error_handling.fc
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include "op.fc";
#include "errors.fc";
#include "constants.fc";

() emit_error(int error_code, int op, slice sender_address, cell custom_payload) impure inline {
;; Create error message cell with context
cell msg = begin_cell()
.store_uint(OP_RESPONSE_ERROR, 32)
.store_uint(error_code, 32)
.store_uint(op, 32)
.store_ref(custom_payload)
.end_cell();

;; Send error response back to sender
var msg = begin_cell()
.store_uint(0x18, 6) ;; nobounce
.store_slice(sender_address) ;; to_addr
.store_coins(0) ;; value
.store_uint(1, MSG_SERIALIZE_BITS) ;; msg header
.store_ref(msg) ;; error info
.end_cell();

send_raw_message(msg, 64);
}

() emit_success(slice sender_address, cell result, cell custom_payload) impure inline {
;; Create success message cell
cell msg = begin_cell()
.store_uint(OP_RESPONSE_SUCCESS, 32)
.store_ref(result) ;; Result data
.store_ref(custom_payload) ;; Original custom payload
.end_cell();

;; Send success response
var msg = begin_cell()
.store_uint(0x18, 6) ;; nobounce
.store_slice(sender_address) ;; to_addr
.store_coins(0) ;; value
.store_uint(1, MSG_SERIALIZE_BITS) ;; msg header
.store_ref(msg) ;; success info
.end_cell();

send_raw_message(msg, 64);
}
4 changes: 4 additions & 0 deletions target_chains/ton/contracts/contracts/common/op.fc
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ 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;

;; Response op codes
const int OP_RESPONSE_SUCCESS = 0x10001;
const int OP_RESPONSE_ERROR = 0x10002;
99 changes: 90 additions & 9 deletions target_chains/ton/contracts/tests/PythTest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1247,13 +1247,40 @@ describe("PythTest", () => {
CUSTOM_PAYLOAD
);

// Verify transaction success and message count
// Verify transaction success but error response sent
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: pythTest.address,
success: false,
exitCode: 2002, // ERROR_INVALID_MAGIC
success: true,
});

// Find the error response message - it's in the second transaction's outMessages
const errorTx = result.transactions[1]; // The PythTest contract transaction
expect(errorTx.outMessages.values().length).toBeGreaterThan(0);

const errorMessage = errorTx.outMessages.values()[0];
expect(errorMessage).toBeDefined();

const cs = errorMessage.body.beginParse();

// Verify error response format
const op = cs.loadUint(32);
expect(op).toBe(0x10002); // OP_RESPONSE_ERROR

const errorCode = cs.loadUint(32);
expect(errorCode).toBe(2002); // ERROR_INVALID_MAGIC

const originalOp = cs.loadUint(32);
expect(originalOp).toBe(5); // OP_PARSE_PRICE_FEED_UPDATES

// Verify custom payload is preserved
const customPayloadCell = cs.loadRef();
const customPayloadSlice = customPayloadCell.beginParse();
expect(
Buffer.from(
customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length)
).toString("hex")
).toBe(CUSTOM_PAYLOAD.toString("hex"));
});

it("should fail to parse price feed updates within range", async () => {
Expand All @@ -1272,13 +1299,40 @@ describe("PythTest", () => {
CUSTOM_PAYLOAD
);

// Verify transaction success and message count
// Verify transaction success but error response sent
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: pythTest.address,
success: false,
exitCode: 2020, // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE
success: true,
});

// Find the error response message - it's in the second transaction's outMessages
const errorTx = result.transactions[1]; // The PythTest contract transaction
expect(errorTx.outMessages.values().length).toBeGreaterThan(0);

const errorMessage = errorTx.outMessages.values()[0];
expect(errorMessage).toBeDefined();

const cs = errorMessage.body.beginParse();

// Verify error response format
const op = cs.loadUint(32);
expect(op).toBe(0x10002); // OP_RESPONSE_ERROR

const errorCode = cs.loadUint(32);
expect(errorCode).toBe(2020); // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE

const originalOp = cs.loadUint(32);
expect(originalOp).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES

// Verify custom payload is preserved
const customPayloadCell = cs.loadRef();
const customPayloadSlice = customPayloadCell.beginParse();
expect(
Buffer.from(
customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length)
).toString("hex")
).toBe(CUSTOM_PAYLOAD.toString("hex"));
});

it("should fail to parse unique price feed updates", async () => {
Expand All @@ -1297,13 +1351,40 @@ describe("PythTest", () => {
CUSTOM_PAYLOAD
);

// Verify transaction success and message count
// Verify transaction success but error response sent
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: pythTest.address,
success: false,
exitCode: 2020, // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE
success: true,
});

// Find the error response message - it's in the second transaction's outMessages
const errorTx = result.transactions[1]; // The PythTest contract transaction
expect(errorTx.outMessages.values().length).toBeGreaterThan(0);

const errorMessage = errorTx.outMessages.values()[0];
expect(errorMessage).toBeDefined();

const cs = errorMessage.body.beginParse();

// Verify error response format
const op = cs.loadUint(32);
expect(op).toBe(0x10002); // OP_RESPONSE_ERROR

const errorCode = cs.loadUint(32);
expect(errorCode).toBe(2020); // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE

const originalOp = cs.loadUint(32);
expect(originalOp).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES

// Verify custom payload is preserved
const customPayloadCell = cs.loadRef();
const customPayloadSlice = customPayloadCell.beginParse();
expect(
Buffer.from(
customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length)
).toString("hex")
).toBe(CUSTOM_PAYLOAD.toString("hex"));
});

it("should successfully parse price feed updates in price ids order", async () => {
Expand Down
Loading