Bladeren bron

fix(target_chains/ton): soft throw instead of revert (#2413)

* soft throw instead of revert

* add soft throw for parse_unique_price_feed_updates

* revert test

* feat: enhance documentation for Pyth Network Price Oracle contract operations
Daniel Chew 8 maanden geleden
bovenliggende
commit
176ba64b6e

+ 45 - 0
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);

+ 34 - 20
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 {

+ 44 - 0
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);
+}

+ 4 - 0
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;

+ 90 - 9
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 () => {