Skip to content

Commit b48bc74

Browse files
authored
add pyth-connecor example (#57)
* add pyth-connecor example Signed-off-by: master_jedy <yura.zarudny@gmail.com> * update readme & fix comment Signed-off-by: master_jedy <yura.zarudny@gmail.com> * debug proxy op, add proxy op test, update readme Signed-off-by: master_jedy <yura.zarudny@gmail.com> --------- Signed-off-by: master_jedy <yura.zarudny@gmail.com>
1 parent 4490fc7 commit b48bc74

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+9660
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.env
2+
yarn.lock
3+
node_modules
4+
build
5+
dist
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
build/
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Pyth-connector example
2+
This example provides two main usage patterns for interacting with Pyth:
3+
4+
- **On-chain getter:**
5+
`User → User JW → App JW → App → Pyth → App → ...`
6+
7+
- **Proxy call:**
8+
`User → Pyth → App → ...`
9+
10+
You can use this example as a standalone module for sandbox testing. It exports functions to deploy and configure a local Pyth contract, making it easy to experiment without connecting to real on-chain contracts on testnet or mainnet.
11+
12+
This example uses a patched Pyth contract intended only for testing purposes in a local development environment. It accepts simplified prices without requiring a Merkle trie proof, so price authenticity is not verified.
13+
14+
<div style="border-radius: 8px; border: 1px solid #ffd700; background: #fffbe6; padding: 16px; margin: 16px 0;">
15+
<strong>Important:</strong> This patched contract is for <strong>testing purposes only</strong>. For production, always use the official Pyth contract deployed on mainnet.
16+
</div>
17+
18+
The demonstration is fully sandboxed. You do not need the Hermes client, prices can be generated locally, for example: **`{ TON: 3.456, USDC: 0.998, USDT: 0.999 }`**
19+
20+
## Project Structure
21+
22+
- `contracts/` — Source code of the smart contracts and their dependencies.
23+
- `wrappers/` — TypeScript contract wrappers (implementing `Contract` from ton-core), including serialization/deserialization and compilation utilities.
24+
- `tests/` — Unit and integration tests for the contracts.
25+
- `scripts/` — Deployment and utility scripts.
26+
27+
28+
## How to use
29+
30+
1. **Install Node.js version 22**
31+
It is recommended to use [nvm](https://github.com/nvm-sh/nvm) for managing Node.js versions.
32+
If you don't have nvm installed, run:
33+
```bash
34+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
35+
source ~/.bashrc
36+
```
37+
Then install and use Node.js 22:
38+
```bash
39+
nvm install 22
40+
nvm use 22
41+
```
42+
43+
2. **Install Yarn** (if not already installed):
44+
```bash
45+
npm install -g yarn
46+
```
47+
48+
3. **Install project dependencies**
49+
In the project root directory, run:
50+
```bash
51+
yarn install
52+
```
53+
54+
After these steps, you are ready to build, test, and use the example. See the sections below for more commands.
55+
56+
### Build
57+
to build the module you can run:
58+
```bash
59+
yarn build
60+
```
61+
62+
### Contracts
63+
To prebuild contracts run:
64+
```bash
65+
yarn contracts
66+
```
67+
68+
### Test
69+
```bash
70+
yarn test:unit
71+
```
72+
73+
### Deploy
74+
You don't need to deploy this example's contracts to testnet/mainnet,
75+
76+
## Important Note on Message Handling
77+
78+
When using the Pyth price feed in the recommended flow (User/App -> Pyth -> Protocol), be aware that:
79+
80+
### Security Warning ⚠️
81+
82+
**CRITICAL**: Integrators MUST validate the sender address in their receive function to ensure messages are coming from the Pyth Oracle contract. Failure to do so could allow attackers to:
83+
84+
- Send invalid price responses
85+
- Impersonate users via the sender_address and custom_payload fields
86+
- Potentially drain the protocol
87+
88+
### Message Bouncing Behavior
89+
90+
- If the target protocol bounces the message (e.g., due to invalid custom payload or other errors), the forwarded TON will remain in the Pyth contract and will not be automatically refunded to the original sender.
91+
- This could be significant when dealing with large amounts of TON (e.g., in DeFi operations).
92+
- Integrators should implement proper error handling and refund mechanisms in their applications.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#include "imports/stdlib.fc";
2+
#include "common/errors.fc";
3+
#include "common/storage.fc";
4+
#include "common/op.fc";
5+
#include "Wormhole.fc";
6+
#include "Pyth.fc";
7+
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+
21+
;; 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
27+
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
28+
if (in_msg_body.slice_empty?()) { ;; ignore empty messages
29+
return ();
30+
}
31+
32+
;; * A 32-bit (big-endian) unsigned integer `op`, identifying the `operation` to be performed, or the `method` of the smart contract to be invoked.
33+
int op = in_msg_body~load_uint(32);
34+
cell data = in_msg_body~load_ref();
35+
slice data_slice = data.begin_parse();
36+
37+
;; Get sender address from message
38+
slice cs = in_msg_full.begin_parse();
39+
int flags = cs~load_uint(4);
40+
if (flags & 1) { ;; ignore all bounced messages
41+
return ();
42+
}
43+
slice sender_address = cs~load_msg_addr(); ;; load sender address
44+
45+
;; * The remainder of the message body is specific for each supported value of `op`.
46+
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
49+
update_guardian_set(data_slice);
50+
} 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
54+
update_price_feeds(msg_value, data_slice);
55+
} 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
58+
execute_governance_action(data_slice);
59+
} elseif (op == OP_UPGRADE_CONTRACT) {
60+
;; @notice Upgrades the contract code
61+
;; @param data - Cell containing the new contract code
62+
execute_upgrade_contract(data);
63+
} 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
73+
cell price_ids_cell = in_msg_body~load_ref();
74+
slice price_ids_slice = price_ids_cell.begin_parse();
75+
int min_publish_time = in_msg_body~load_uint(64);
76+
int max_publish_time = in_msg_body~load_uint(64);
77+
slice target_address = in_msg_body~load_msg_addr();
78+
cell custom_payload_cell = in_msg_body~load_ref();
79+
slice custom_payload = custom_payload_cell.begin_parse();
80+
parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address, target_address, custom_payload);
81+
} 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_val; @notice Parses unique price feed updates (only the latest for each price ID) and returns the results to the caller
84+
;; @param msg_value - Amount of TON sent with the message (used for fee calculation)
85+
;; @param data_slice - Slice containing the price feed update data
86+
;; @param price_ids_slice - Slice containing the price IDs to filter for
87+
;; @param publish_time - Target publish time for price updates
88+
;; @param max_staleness - Maximum allowed staleness of price updates (in seconds)
89+
;; @param sender_address - Address of the sender (for response)
90+
;; @param target_address - Address to send the response to
91+
;; @param custom_payload - Custom payload to include in the response
92+
cell price_ids_cell = in_msg_body~load_ref();
93+
slice price_ids_slice = price_ids_cell.begin_parse();
94+
int publish_time = in_msg_body~load_uint(64);
95+
int max_staleness = in_msg_body~load_uint(64);
96+
slice target_address = in_msg_body~load_msg_addr();
97+
cell custom_payload_cell = in_msg_body~load_ref();
98+
slice custom_payload = custom_payload_cell.begin_parse();
99+
parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address, target_address, custom_payload);
100+
} else {
101+
throw(0xffff); ;; Throw exception for unknown op
102+
}
103+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{-
2+
This test contract serves two main purposes:
3+
1. It allows testing of non-getter functions in FunC without requiring specific opcodes for each function.
4+
2. It provides access to internal functions through wrapper getter functions.
5+
6+
This approach is common in FunC development, where a separate test contract is used for unit testing.
7+
It enables more comprehensive testing of the contract's functionality, including internal operations
8+
that are not directly accessible through standard getter methods.
9+
-}
10+
{-
11+
The only difference from the Main.fc is that it uses patched Pyth functions,
12+
which don't verify prices sources and allow to run tests with locally generated prices.
13+
-}
14+
#include "imports/stdlib.fc";
15+
#include "tests/PythNoCheck.fc";
16+
#include "Wormhole.fc";
17+
#include "common/op.fc";
18+
19+
() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
20+
if (in_msg_body.slice_empty?()) {
21+
return ();
22+
}
23+
24+
;; Get sender address from message
25+
slice cs = in_msg_full.begin_parse();
26+
int flags = cs~load_uint(4); ;; load flags
27+
if (flags & 1) {
28+
return ();
29+
}
30+
slice sender_address = cs~load_msg_addr(); ;; load sender address
31+
32+
int op = in_msg_body~load_uint(32);
33+
cell data = in_msg_body~load_ref();
34+
slice data_slice = data.begin_parse();
35+
36+
if (op == OP_UPDATE_GUARDIAN_SET) {
37+
update_guardian_set(data_slice);
38+
} elseif (op == OP_UPDATE_PRICE_FEEDS) {
39+
update_price_feeds(msg_value, data_slice);
40+
} elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
41+
execute_governance_action(data_slice);
42+
} elseif (op == OP_UPGRADE_CONTRACT) {
43+
execute_upgrade_contract(data);
44+
} elseif (op == OP_PARSE_PRICE_FEED_UPDATES) {
45+
cell price_ids_cell = in_msg_body~load_ref();
46+
slice price_ids_slice = price_ids_cell.begin_parse();
47+
int min_publish_time = in_msg_body~load_uint(64);
48+
int max_publish_time = in_msg_body~load_uint(64);
49+
slice target_address = in_msg_body~load_msg_addr();
50+
cell custom_payload_cell = in_msg_body~load_ref();
51+
slice custom_payload = custom_payload_cell.begin_parse();
52+
parse_price_feed_updates(msg_value, data_slice, price_ids_slice, min_publish_time, max_publish_time, sender_address, target_address, custom_payload);
53+
} elseif (op == OP_PARSE_UNIQUE_PRICE_FEED_UPDATES) {
54+
cell price_ids_cell = in_msg_body~load_ref();
55+
slice price_ids_slice = price_ids_cell.begin_parse();
56+
int publish_time = in_msg_body~load_uint(64);
57+
int max_staleness = in_msg_body~load_uint(64);
58+
slice target_address = in_msg_body~load_msg_addr();
59+
cell custom_payload_cell = in_msg_body~load_ref();
60+
slice custom_payload = custom_payload_cell.begin_parse();
61+
parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address, target_address, custom_payload);
62+
} else {
63+
throw(0xffff); ;; Throw exception for unknown op
64+
}
65+
}

0 commit comments

Comments
 (0)