diff --git a/target_chains/ton/contracts/contracts/Main.fc b/target_chains/ton/contracts/contracts/Main.fc index faf534be7c..edb3123062 100644 --- a/target_chains/ton/contracts/contracts/Main.fc +++ b/target_chains/ton/contracts/contracts/Main.fc @@ -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 (); @@ -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); @@ -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); diff --git a/target_chains/ton/contracts/contracts/Pyth.fc b/target_chains/ton/contracts/contracts/Pyth.fc index a34138d37c..15d7a2e00f 100644 --- a/target_chains/ton/contracts/contracts/Pyth.fc +++ b/target_chains/ton/contracts/contracts/Pyth.fc @@ -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) { @@ -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 { diff --git a/target_chains/ton/contracts/contracts/common/error_handling.fc b/target_chains/ton/contracts/contracts/common/error_handling.fc new file mode 100644 index 0000000000..17e345cbef --- /dev/null +++ b/target_chains/ton/contracts/contracts/common/error_handling.fc @@ -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); +} diff --git a/target_chains/ton/contracts/contracts/common/op.fc b/target_chains/ton/contracts/contracts/common/op.fc index 2d15b5cc2f..6d066b1037 100644 --- a/target_chains/ton/contracts/contracts/common/op.fc +++ b/target_chains/ton/contracts/contracts/common/op.fc @@ -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; diff --git a/target_chains/ton/contracts/tests/PythTest.spec.ts b/target_chains/ton/contracts/tests/PythTest.spec.ts index 6e8cf25aef..3a3ae4216b 100644 --- a/target_chains/ton/contracts/tests/PythTest.spec.ts +++ b/target_chains/ton/contracts/tests/PythTest.spec.ts @@ -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 () => { @@ -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 () => { @@ -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 () => {