Skip to content

Commit

Permalink
fix(target_chains/ton): soft throw instead of revert (#2413)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cctdaniel authored Feb 27, 2025
1 parent 6e4589d commit 176ba64
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 29 deletions.
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

0 comments on commit 176ba64

Please sign in to comment.