Skip to content

add pyth-connecor example #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions price_feeds/ton/pyth-connector/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.env
yarn.lock
node_modules
build
dist
1 change: 1 addition & 0 deletions price_feeds/ton/pyth-connector/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
53 changes: 53 additions & 0 deletions price_feeds/ton/pyth-connector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Pyth-connector example
Provides onchain-getter: **User -> User JettonWallet -> App -> Pyth -> App -> ...** and proxy call: **User -> Pyth -> App -> ...** pyth usage examples.

This example can be used as a standalone module that provides tools for sandbox testing by exporting functions for deploying and configuring a local Pyth contract.

It demonstrates techniques for using the Pyth oracle in financial applications.

The demonstration is fully sandboxed and does not require real on-chain contracts on either testnet or mainnet.
Using the Hermes client is also not required — prices can be generated locally, for example: **{TON: 3.12345, USDC: 0.998, USDT: 0.999}**.

This is achieved by using a patched Pyth contract that accepts simplified prices without a Merkle trie proof, and therefore does not verify the authenticity of the prices.
Important: This patched contract is intended for testing purposes only. The production version must use the authentic Pyth contract deployed on the mainnet.

## Project structure

- `contracts` - source code of all the smart contracts of the project and their dependencies.
- `wrappers` - wrapper classes (implementing `Contract` from ton-core) for the contracts, including any [de]serialization primitives and compilation functions.
- `tests` - tests for the contracts.
- `scripts` - scripts used by the project, mainly the deployment scripts.

## How to use
First you need to install dependencies, node v22 is required, you can use nvm to install it: `nvm use 22` .
Then install dependencies, just run `yarn`

### Build
to build the module you can run `yarn build`

### Contracts
To prebuild contracts run`yarn contracts`

### Test
`yarn test:unit`

### Deploy
You don't need to deploy this example's contracts to testnet/mainnet,

## Important Note on Message Handling

When using the Pyth price feed in the recommended flow (User/App -> Pyth -> Protocol), be aware that:

### Security Warning ⚠️

**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:

- Send invalid price responses
- Impersonate users via the sender_address and custom_payload fields
- Potentially drain the protocol

### Message Bouncing Behavior

- 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.
- This could be significant when dealing with large amounts of TON (e.g., in DeFi operations).
- Integrators should implement proper error handling and refund mechanisms in their applications.
103 changes: 103 additions & 0 deletions price_feeds/ton/pyth-connector/contracts/Pyth/Main.fc
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#include "imports/stdlib.fc";
#include "common/errors.fc";
#include "common/storage.fc";
#include "common/op.fc";
#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 ();
}

;; * A 32-bit (big-endian) unsigned integer `op`, identifying the `operation` to be performed, or the `method` of the smart contract to be invoked.
int op = in_msg_body~load_uint(32);
cell data = in_msg_body~load_ref();
slice data_slice = data.begin_parse();

;; Get sender address from message
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) { ;; ignore all bounced messages
return ();
}
slice sender_address = cs~load_msg_addr(); ;; load sender address

;; * 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);
int max_publish_time = in_msg_body~load_uint(64);
slice target_address = in_msg_body~load_msg_addr();
cell custom_payload_cell = in_msg_body~load_ref();
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_val; @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);
int max_staleness = in_msg_body~load_uint(64);
slice target_address = in_msg_body~load_msg_addr();
cell custom_payload_cell = in_msg_body~load_ref();
slice custom_payload = custom_payload_cell.begin_parse();
parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address, target_address, custom_payload);
} else {
throw(0xffff); ;; Throw exception for unknown op
}
}
65 changes: 65 additions & 0 deletions price_feeds/ton/pyth-connector/contracts/Pyth/MainNoCheck.fc
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{-
This test contract serves two main purposes:
1. It allows testing of non-getter functions in FunC without requiring specific opcodes for each function.
2. It provides access to internal functions through wrapper getter functions.

This approach is common in FunC development, where a separate test contract is used for unit testing.
It enables more comprehensive testing of the contract's functionality, including internal operations
that are not directly accessible through standard getter methods.
-}
{-
The only difference from the Main.fc is that it uses patched Pyth functions,
which don't verify prices sources and allow to run tests with locally generated prices.
-}
#include "imports/stdlib.fc";
#include "tests/PythNoCheck.fc";
#include "Wormhole.fc";
#include "common/op.fc";

() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) {
return ();
}

;; Get sender address from message
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4); ;; load flags
if (flags & 1) {
return ();
}
slice sender_address = cs~load_msg_addr(); ;; load sender address

int op = in_msg_body~load_uint(32);
cell data = in_msg_body~load_ref();
slice data_slice = data.begin_parse();

if (op == OP_UPDATE_GUARDIAN_SET) {
update_guardian_set(data_slice);
} elseif (op == OP_UPDATE_PRICE_FEEDS) {
update_price_feeds(msg_value, data_slice);
} elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
execute_governance_action(data_slice);
} elseif (op == OP_UPGRADE_CONTRACT) {
execute_upgrade_contract(data);
} elseif (op == OP_PARSE_PRICE_FEED_UPDATES) {
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);
int max_publish_time = in_msg_body~load_uint(64);
slice target_address = in_msg_body~load_msg_addr();
cell custom_payload_cell = in_msg_body~load_ref();
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) {
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);
int max_staleness = in_msg_body~load_uint(64);
slice target_address = in_msg_body~load_msg_addr();
cell custom_payload_cell = in_msg_body~load_ref();
slice custom_payload = custom_payload_cell.begin_parse();
parse_unique_price_feed_updates(msg_value, data_slice, price_ids_slice, publish_time, max_staleness, sender_address, target_address, custom_payload);
} else {
throw(0xffff); ;; Throw exception for unknown op
}
}
Loading