Skip to content

Commit 176ba64

Browse files
authored
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
1 parent 6e4589d commit 176ba64

File tree

5 files changed

+217
-29
lines changed

5 files changed

+217
-29
lines changed

target_chains/ton/contracts/contracts/Main.fc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,25 @@
55
#include "Wormhole.fc";
66
#include "Pyth.fc";
77

8+
;; @title Pyth Network Price Oracle Contract for TON
9+
;; @notice This contract serves as the main entry point for the Pyth Network price oracle on TON.
10+
;; @dev The contract handles various operations including:
11+
;; - Updating guardian sets for Wormhole message verification
12+
;; - Updating price feeds with the latest price data
13+
;; - Executing governance actions
14+
;; - Upgrading the contract code
15+
;; - Parsing price feed updates for clients
16+
;;
17+
;; The contract uses Wormhole's cross-chain messaging protocol to verify price updates
18+
;; and governance actions. It maintains a dictionary of price feeds indexed by price ID.
19+
;; Each price feed contains the current price, confidence interval, exponent, and publish time.
20+
821
;; Internal message handler
22+
;; @param my_balance - Current contract balance
23+
;; @param msg_value - Amount of TON sent with the message
24+
;; @param in_msg_full - Full incoming message cell
25+
;; @param in_msg_body - Message body as a slice
26+
;; @returns () - No return value
927
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
1028
if (in_msg_body.slice_empty?()) { ;; ignore empty messages
1129
return ();
@@ -26,14 +44,32 @@
2644

2745
;; * The remainder of the message body is specific for each supported value of `op`.
2846
if (op == OP_UPDATE_GUARDIAN_SET) {
47+
;; @notice Updates the guardian set based on a Wormhole VAA
48+
;; @param data_slice - Slice containing the VAA with guardian set update information
2949
update_guardian_set(data_slice);
3050
} elseif (op == OP_UPDATE_PRICE_FEEDS) {
51+
;; @notice Updates price feeds with the latest price data
52+
;; @param msg_value - Amount of TON sent with the message (used for fee calculation)
53+
;; @param data_slice - Slice containing the price feed update data
3154
update_price_feeds(msg_value, data_slice);
3255
} elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
56+
;; @notice Executes a governance action based on a Wormhole VAA
57+
;; @param data_slice - Slice containing the VAA with governance action information
3358
execute_governance_action(data_slice);
3459
} elseif (op == OP_UPGRADE_CONTRACT) {
60+
;; @notice Upgrades the contract code
61+
;; @param data - Cell containing the new contract code
3562
execute_upgrade_contract(data);
3663
} elseif (op == OP_PARSE_PRICE_FEED_UPDATES) {
64+
;; @notice Parses price feed updates and returns the results to the caller
65+
;; @param msg_value - Amount of TON sent with the message (used for fee calculation)
66+
;; @param data_slice - Slice containing the price feed update data
67+
;; @param price_ids_slice - Slice containing the price IDs to filter for
68+
;; @param min_publish_time - Minimum publish time for price updates to be considered
69+
;; @param max_publish_time - Maximum publish time for price updates to be considered
70+
;; @param sender_address - Address of the sender (for response)
71+
;; @param target_address - Address to send the response to
72+
;; @param custom_payload - Custom payload to include in the response
3773
cell price_ids_cell = in_msg_body~load_ref();
3874
slice price_ids_slice = price_ids_cell.begin_parse();
3975
int min_publish_time = in_msg_body~load_uint(64);
@@ -43,6 +79,15 @@
4379
slice custom_payload = custom_payload_cell.begin_parse();
4480
parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address, target_address, custom_payload);
4581
} elseif (op == OP_PARSE_UNIQUE_PRICE_FEED_UPDATES) {
82+
;; @notice Parses unique price feed updates (only the latest for each price ID) and returns the results to the caller
83+
;; @param msg_value - Amount of TON sent with the message (used for fee calculation)
84+
;; @param data_slice - Slice containing the price feed update data
85+
;; @param price_ids_slice - Slice containing the price IDs to filter for
86+
;; @param publish_time - Target publish time for price updates
87+
;; @param max_staleness - Maximum allowed staleness of price updates (in seconds)
88+
;; @param sender_address - Address of the sender (for response)
89+
;; @param target_address - Address to send the response to
90+
;; @param custom_payload - Custom payload to include in the response
4691
cell price_ids_cell = in_msg_body~load_ref();
4792
slice price_ids_slice = price_ids_cell.begin_parse();
4893
int publish_time = in_msg_body~load_uint(64);

target_chains/ton/contracts/contracts/Pyth.fc

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "common/governance_actions.fc";
88
#include "common/gas.fc";
99
#include "common/op.fc";
10+
#include "common/error_handling.fc";
1011
#include "./Wormhole.fc";
1112

1213
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) {
369370
}
370371

371372
() 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 {
372-
load_data();
373+
try {
374+
load_data();
375+
376+
;; Load price_ids tuple
377+
int price_ids_len = price_ids_slice~load_uint(8);
378+
tuple price_ids = empty_tuple();
379+
repeat(price_ids_len) {
380+
int price_id = price_ids_slice~load_uint(256);
381+
price_ids~tpush(price_id);
382+
}
373383

374-
;; Load price_ids tuple
375-
int price_ids_len = price_ids_slice~load_uint(8);
376-
tuple price_ids = empty_tuple();
377-
repeat(price_ids_len) {
378-
int price_id = price_ids_slice~load_uint(256);
379-
price_ids~tpush(price_id);
384+
tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, min_publish_time, max_publish_time, false);
385+
send_price_feeds_response(price_feeds, msg_value, OP_PARSE_PRICE_FEED_UPDATES,
386+
sender_address, target_address, custom_payload);
387+
} catch (_, error_code) {
388+
;; Handle any unexpected errors
389+
emit_error(error_code, OP_PARSE_PRICE_FEED_UPDATES,
390+
sender_address, begin_cell().store_slice(custom_payload).end_cell());
380391
}
381-
382-
tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, min_publish_time, max_publish_time, false);
383-
send_price_feeds_response(price_feeds, msg_value, OP_PARSE_PRICE_FEED_UPDATES, sender_address, target_address, custom_payload);
384392
}
385393

386394
() 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 {
387-
load_data();
395+
try {
396+
load_data();
397+
398+
;; Load price_ids tuple
399+
int price_ids_len = price_ids_slice~load_uint(8);
400+
tuple price_ids = empty_tuple();
401+
repeat(price_ids_len) {
402+
int price_id = price_ids_slice~load_uint(256);
403+
price_ids~tpush(price_id);
404+
}
388405

389-
;; Load price_ids tuple
390-
int price_ids_len = price_ids_slice~load_uint(8);
391-
tuple price_ids = empty_tuple();
392-
repeat(price_ids_len) {
393-
int price_id = price_ids_slice~load_uint(256);
394-
price_ids~tpush(price_id);
406+
tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, publish_time, publish_time + max_staleness, true);
407+
send_price_feeds_response(price_feeds, msg_value, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES, sender_address, target_address, custom_payload);
408+
} catch (_, error_code) {
409+
;; Handle any unexpected errors
410+
emit_error(error_code, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES,
411+
sender_address, begin_cell().store_slice(custom_payload).end_cell());
395412
}
396-
397-
tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, publish_time, publish_time + max_staleness, true);
398-
send_price_feeds_response(price_feeds, msg_value, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES, sender_address, target_address, custom_payload);
399413
}
400414

401415
() update_price_feeds(int msg_value, slice data) impure {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#include "op.fc";
2+
#include "errors.fc";
3+
#include "constants.fc";
4+
5+
() emit_error(int error_code, int op, slice sender_address, cell custom_payload) impure inline {
6+
;; Create error message cell with context
7+
cell msg = begin_cell()
8+
.store_uint(OP_RESPONSE_ERROR, 32)
9+
.store_uint(error_code, 32)
10+
.store_uint(op, 32)
11+
.store_ref(custom_payload)
12+
.end_cell();
13+
14+
;; Send error response back to sender
15+
var msg = begin_cell()
16+
.store_uint(0x18, 6) ;; nobounce
17+
.store_slice(sender_address) ;; to_addr
18+
.store_coins(0) ;; value
19+
.store_uint(1, MSG_SERIALIZE_BITS) ;; msg header
20+
.store_ref(msg) ;; error info
21+
.end_cell();
22+
23+
send_raw_message(msg, 64);
24+
}
25+
26+
() emit_success(slice sender_address, cell result, cell custom_payload) impure inline {
27+
;; Create success message cell
28+
cell msg = begin_cell()
29+
.store_uint(OP_RESPONSE_SUCCESS, 32)
30+
.store_ref(result) ;; Result data
31+
.store_ref(custom_payload) ;; Original custom payload
32+
.end_cell();
33+
34+
;; Send success response
35+
var msg = begin_cell()
36+
.store_uint(0x18, 6) ;; nobounce
37+
.store_slice(sender_address) ;; to_addr
38+
.store_coins(0) ;; value
39+
.store_uint(1, MSG_SERIALIZE_BITS) ;; msg header
40+
.store_ref(msg) ;; success info
41+
.end_cell();
42+
43+
send_raw_message(msg, 64);
44+
}

target_chains/ton/contracts/contracts/common/op.fc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ const int OP_EXECUTE_GOVERNANCE_ACTION = 3;
44
const int OP_UPGRADE_CONTRACT = 4;
55
const int OP_PARSE_PRICE_FEED_UPDATES = 5;
66
const int OP_PARSE_UNIQUE_PRICE_FEED_UPDATES = 6;
7+
8+
;; Response op codes
9+
const int OP_RESPONSE_SUCCESS = 0x10001;
10+
const int OP_RESPONSE_ERROR = 0x10002;

target_chains/ton/contracts/tests/PythTest.spec.ts

Lines changed: 90 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,13 +1247,40 @@ describe("PythTest", () => {
12471247
CUSTOM_PAYLOAD
12481248
);
12491249

1250-
// Verify transaction success and message count
1250+
// Verify transaction success but error response sent
12511251
expect(result.transactions).toHaveTransaction({
12521252
from: deployer.address,
12531253
to: pythTest.address,
1254-
success: false,
1255-
exitCode: 2002, // ERROR_INVALID_MAGIC
1254+
success: true,
12561255
});
1256+
1257+
// Find the error response message - it's in the second transaction's outMessages
1258+
const errorTx = result.transactions[1]; // The PythTest contract transaction
1259+
expect(errorTx.outMessages.values().length).toBeGreaterThan(0);
1260+
1261+
const errorMessage = errorTx.outMessages.values()[0];
1262+
expect(errorMessage).toBeDefined();
1263+
1264+
const cs = errorMessage.body.beginParse();
1265+
1266+
// Verify error response format
1267+
const op = cs.loadUint(32);
1268+
expect(op).toBe(0x10002); // OP_RESPONSE_ERROR
1269+
1270+
const errorCode = cs.loadUint(32);
1271+
expect(errorCode).toBe(2002); // ERROR_INVALID_MAGIC
1272+
1273+
const originalOp = cs.loadUint(32);
1274+
expect(originalOp).toBe(5); // OP_PARSE_PRICE_FEED_UPDATES
1275+
1276+
// Verify custom payload is preserved
1277+
const customPayloadCell = cs.loadRef();
1278+
const customPayloadSlice = customPayloadCell.beginParse();
1279+
expect(
1280+
Buffer.from(
1281+
customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length)
1282+
).toString("hex")
1283+
).toBe(CUSTOM_PAYLOAD.toString("hex"));
12571284
});
12581285

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

1275-
// Verify transaction success and message count
1302+
// Verify transaction success but error response sent
12761303
expect(result.transactions).toHaveTransaction({
12771304
from: deployer.address,
12781305
to: pythTest.address,
1279-
success: false,
1280-
exitCode: 2020, // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE
1306+
success: true,
12811307
});
1308+
1309+
// Find the error response message - it's in the second transaction's outMessages
1310+
const errorTx = result.transactions[1]; // The PythTest contract transaction
1311+
expect(errorTx.outMessages.values().length).toBeGreaterThan(0);
1312+
1313+
const errorMessage = errorTx.outMessages.values()[0];
1314+
expect(errorMessage).toBeDefined();
1315+
1316+
const cs = errorMessage.body.beginParse();
1317+
1318+
// Verify error response format
1319+
const op = cs.loadUint(32);
1320+
expect(op).toBe(0x10002); // OP_RESPONSE_ERROR
1321+
1322+
const errorCode = cs.loadUint(32);
1323+
expect(errorCode).toBe(2020); // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE
1324+
1325+
const originalOp = cs.loadUint(32);
1326+
expect(originalOp).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES
1327+
1328+
// Verify custom payload is preserved
1329+
const customPayloadCell = cs.loadRef();
1330+
const customPayloadSlice = customPayloadCell.beginParse();
1331+
expect(
1332+
Buffer.from(
1333+
customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length)
1334+
).toString("hex")
1335+
).toBe(CUSTOM_PAYLOAD.toString("hex"));
12821336
});
12831337

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

1300-
// Verify transaction success and message count
1354+
// Verify transaction success but error response sent
13011355
expect(result.transactions).toHaveTransaction({
13021356
from: deployer.address,
13031357
to: pythTest.address,
1304-
success: false,
1305-
exitCode: 2020, // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE
1358+
success: true,
13061359
});
1360+
1361+
// Find the error response message - it's in the second transaction's outMessages
1362+
const errorTx = result.transactions[1]; // The PythTest contract transaction
1363+
expect(errorTx.outMessages.values().length).toBeGreaterThan(0);
1364+
1365+
const errorMessage = errorTx.outMessages.values()[0];
1366+
expect(errorMessage).toBeDefined();
1367+
1368+
const cs = errorMessage.body.beginParse();
1369+
1370+
// Verify error response format
1371+
const op = cs.loadUint(32);
1372+
expect(op).toBe(0x10002); // OP_RESPONSE_ERROR
1373+
1374+
const errorCode = cs.loadUint(32);
1375+
expect(errorCode).toBe(2020); // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE
1376+
1377+
const originalOp = cs.loadUint(32);
1378+
expect(originalOp).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES
1379+
1380+
// Verify custom payload is preserved
1381+
const customPayloadCell = cs.loadRef();
1382+
const customPayloadSlice = customPayloadCell.beginParse();
1383+
expect(
1384+
Buffer.from(
1385+
customPayloadSlice.loadBuffer(CUSTOM_PAYLOAD.length)
1386+
).toString("hex")
1387+
).toBe(CUSTOM_PAYLOAD.toString("hex"));
13071388
});
13081389

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

0 commit comments

Comments
 (0)