diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd5b6bfff2..534290fb39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1573,49 +1573,6 @@ importers: specifier: ^1.0.0-rc.1 version: 1.1.3(prettier@2.8.8) - target_chains/ethereum/examples/coin_flip/app: - dependencies: - '@pythnetwork/pyth-evm-js': - specifier: workspace:* - version: link:../../../sdk/js - '@pythnetwork/pyth-sdk-solidity': - specifier: workspace:* - version: link:../../../sdk/solidity - '@truffle/hdwallet-provider': - specifier: ^2.1.15 - version: 2.1.15(@babel/core@7.24.7)(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) - '@types/jest': - specifier: ^27.5.2 - version: 27.5.2 - '@types/node': - specifier: ^16.11.64 - version: 16.18.60 - buffer: - specifier: ^6.0.3 - version: 6.0.3 - ethers: - specifier: ^5.7.2 - version: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) - prettier: - specifier: ^2.7.1 - version: 2.8.8 - typescript: - specifier: ^4.8.4 - version: 4.9.5 - web3: - specifier: ^1.8.1 - version: 1.10.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) - yargs: - specifier: ^17.7.2 - version: 17.7.2 - devDependencies: - '@types/yargs': - specifier: ^17.0.32 - version: 17.0.32 - ts-node: - specifier: ^10.9.1 - version: 10.9.1(@types/node@16.18.60)(typescript@4.9.5) - target_chains/ethereum/sdk/js: dependencies: '@pythnetwork/price-service-client': @@ -1948,6 +1905,9 @@ importers: '@pythnetwork/price-service-sdk': specifier: workspace:* version: link:../../../price_service/sdk/js + '@pythnetwork/xc-admin-common': + specifier: workspace:* + version: link:../../../governance/xc_admin/packages/xc_admin_common '@ton/blueprint': specifier: ^0.22.0 version: 0.22.0(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)(@ton/ton@13.11.2(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0))(@types/node@20.14.15)(encoding@0.1.13)(typescript@5.5.4) @@ -8460,9 +8420,6 @@ packages: '@types/node@16.18.11': resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==} - '@types/node@16.18.60': - resolution: {integrity: sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA==} - '@types/node@18.11.18': resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} @@ -33332,27 +33289,6 @@ snapshots: - supports-color - utf-8-validate - '@truffle/hdwallet-provider@2.1.15(@babel/core@7.24.7)(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': - dependencies: - '@ethereumjs/common': 2.6.5 - '@ethereumjs/tx': 3.5.2 - '@metamask/eth-sig-util': 4.0.1 - '@truffle/hdwallet': 0.1.4 - '@types/ethereum-protocol': 1.0.2 - '@types/web3': 1.0.20 - '@types/web3-provider-engine': 14.0.1 - ethereum-cryptography: 1.1.2 - ethereum-protocol: 1.0.1 - ethereumjs-util: 7.1.5 - web3: 1.10.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) - web3-provider-engine: 16.0.3(@babel/core@7.24.7)(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - '@babel/core' - - bufferutil - - encoding - - supports-color - - utf-8-validate - '@truffle/hdwallet-provider@2.1.5(@babel/core@7.24.7)(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@ethereumjs/common': 2.6.5 @@ -33805,8 +33741,6 @@ snapshots: '@types/node@16.18.11': {} - '@types/node@16.18.60': {} - '@types/node@18.11.18': {} '@types/node@18.15.13': {} @@ -43601,7 +43535,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.14.15 - ts-node: 10.9.2(@types/node@18.19.34)(typescript@4.9.5) + ts-node: 10.9.2(@types/node@20.14.2)(typescript@4.9.5) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -50529,24 +50463,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.1(@types/node@16.18.60)(typescript@4.9.5): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.9 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.3 - '@types/node': 16.18.60 - acorn: 8.11.3 - acorn-walk: 8.2.0 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 4.9.5 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - ts-node@10.9.1(@types/node@22.5.1)(typescript@4.9.5): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -51606,16 +51522,6 @@ snapshots: - supports-color - utf-8-validate - web3-bzz@1.10.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): - dependencies: - '@types/node': 12.20.55 - got: 12.1.0 - swarm-js: 0.1.42(bufferutil@4.0.8)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - web3-bzz@1.10.4(bufferutil@4.0.7)(utf-8-validate@6.0.3): dependencies: '@types/node': 12.20.55 @@ -52688,21 +52594,6 @@ snapshots: - supports-color - utf-8-validate - web3@1.10.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10): - dependencies: - web3-bzz: 1.10.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-core: 1.10.0(encoding@0.1.13) - web3-eth: 1.10.0(encoding@0.1.13) - web3-eth-personal: 1.10.0(encoding@0.1.13) - web3-net: 1.10.0(encoding@0.1.13) - web3-shh: 1.10.0(encoding@0.1.13) - web3-utils: 1.10.0 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - web3@1.10.4(bufferutil@4.0.7)(encoding@0.1.13)(utf-8-validate@6.0.3): dependencies: web3-bzz: 1.10.4(bufferutil@4.0.7)(utf-8-validate@6.0.3) diff --git a/target_chains/ton/contracts/contracts/Main.fc b/target_chains/ton/contracts/contracts/Main.fc index 1cbf22373e..571db4a3e9 100644 --- a/target_chains/ton/contracts/contracts/Main.fc +++ b/target_chains/ton/contracts/contracts/Main.fc @@ -2,6 +2,7 @@ #include "common/errors.fc"; #include "common/storage.fc"; #include "Wormhole.fc"; +#include "Pyth.fc"; ;; Opcodes const int OP_UPDATE_GUARDIAN_SET = 1; diff --git a/target_chains/ton/contracts/contracts/Pyth.fc b/target_chains/ton/contracts/contracts/Pyth.fc index ccf368684d..762e14768e 100644 --- a/target_chains/ton/contracts/contracts/Pyth.fc +++ b/target_chains/ton/contracts/contracts/Pyth.fc @@ -2,13 +2,20 @@ #include "common/errors.fc"; #include "common/storage.fc"; #include "common/utils.fc"; +#include "common/constants.fc"; +#include "common/merkle_tree.fc"; #include "./Wormhole.fc"; -const int ACCUMULATOR_MAGIC = 0x504e4155; ;; "PNAU" (Pyth Network Accumulator Update) -const int MAJOR_VERSION = 1; -const int MINIMUM_ALLOWED_MINOR_VERSION = 0; +cell store_price(int price, int conf, int expo, int publish_time) { + return begin_cell() + .store_int(price, 64) + .store_uint(conf, 64) + .store_int(expo, 32) + .store_uint(publish_time, 64) + .end_cell(); +} -slice verify_header(slice data) { +slice read_and_verify_header(slice data) { int magic = data~load_uint(32); throw_unless(ERROR_INVALID_MAGIC, magic == ACCUMULATOR_MAGIC); int major_version = data~load_uint(8); @@ -16,28 +23,64 @@ slice verify_header(slice data) { int minor_version = data~load_uint(8); throw_if(ERROR_INVALID_MINOR_VERSION, minor_version < MINIMUM_ALLOWED_MINOR_VERSION); int trailing_header_size = data~load_uint(8); - ;; skip trailing headers and update type (uint8) + ;; skip trailing headers data~skip_bits(trailing_header_size); - data~skip_bits(8); + int update_type = data~load_uint(8); + throw_unless(ERROR_INVALID_UPDATE_DATA_TYPE, update_type == WORMHOLE_MERKLE_UPDATE_TYPE); return data; } +(int, int, int, int, int, int, int, int, slice) read_and_verify_message(slice cs, int root_digest) impure { + int message_size = cs~load_uint(16); + (cell message, slice cs) = read_and_store_large_data(cs, message_size * 8); + slice message = message.begin_parse(); + slice cs = read_and_verify_proof(root_digest, message, cs); + + int message_type = message~load_uint(8); + throw_unless(ERROR_INVALID_MESSAGE_TYPE, message_type == 0); ;; 0 corresponds to PriceFeed + + int price_id = message~load_uint(256); + int price = message~load_int(64); + int conf = message~load_uint(64); + int expo = message~load_int(32); + int publish_time = message~load_uint(64); + int prev_publish_time = message~load_uint(64); + int ema_price = message~load_int(64); + int ema_conf = message~load_uint(64); + + return (price_id, price, conf, expo, publish_time, prev_publish_time, ema_price, ema_conf, cs); +} + +(int, int, int, int) parse_price(slice price_feed) { + int price = price_feed~load_int(64); + int conf = price_feed~load_uint(64); + int expo = price_feed~load_int(32); + int publish_time = price_feed~load_uint(64); + return (price, conf, expo, publish_time); +} + (int) get_update_fee(slice data) method_id { load_data(); - slice cs = verify_header(data); + slice cs = read_and_verify_header(data); int wormhole_proof_size_bytes = cs~load_uint(16); (cell wormhole_proof, slice cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8); int num_updates = cs~load_uint(8); return single_update_fee * num_updates; } +int get_governance_data_source_index() method_id { + load_data(); + return governance_data_source_index; +} + +cell get_governance_data_source() method_id { + load_data(); + return governance_data_source; +} -(int, int, int, int) parse_price(slice price_feed) { - int price = price_feed~load_int(256); - int conf = price_feed~load_uint(64); - int expo = price_feed~load_int(32); - int publish_time = price_feed~load_uint(64); - return (price, conf, expo, publish_time); +int get_last_executed_governance_sequence() method_id { + load_data(); + return last_executed_governance_sequence; } (int, int, int, int) get_price_unsafe(int price_feed_id) method_id { @@ -74,3 +117,92 @@ slice verify_header(slice data) { throw_if(ERROR_OUTDATED_PRICE, current_time - publish_time > time_period); return (price, conf, expo, publish_time); } + +(int, int) parse_data_source(cell data_source) { + slice ds = data_source.begin_parse(); + int emitter_chain = ds~load_uint(16); + int emitter_address = ds~load_uint(256); + return (emitter_chain, emitter_address); +} + +int parse_pyth_payload_in_wormhole_vm(slice payload) impure { + int accumulator_wormhole_magic = payload~load_uint(32); + throw_unless(ERROR_INVALID_MAGIC, accumulator_wormhole_magic == ACCUMULATOR_WORMHOLE_MAGIC); + + int update_type = payload~load_uint(8); + throw_unless(ERROR_INVALID_UPDATE_DATA_TYPE, update_type == WORMHOLE_MERKLE_UPDATE_TYPE); + + payload~load_uint(64); ;; Skip slot + payload~load_uint(32); ;; Skip ring_size + + return payload~load_uint(160); ;; Return root_digest +} + + +() update_price_feeds(int msg_value, slice data) impure { + load_data(); + slice cs = read_and_verify_header(data); + + int wormhole_proof_size_bytes = cs~load_uint(16); + (cell wormhole_proof, slice new_cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8); + cs = new_cs; + + int num_updates = cs~load_uint(8); + int fee = single_update_fee * num_updates; + + ;; Check if the sender has sent enough TON to cover the fee + throw_unless(ERROR_INSUFFICIENT_FEE, msg_value >= fee); + + (_, _, _, _, int emitter_chain_id, int emitter_address, _, _, slice payload, _) = parse_and_verify_wormhole_vm(wormhole_proof.begin_parse()); + + ;; Check if the data source is valid + cell data_source = begin_cell() + .store_uint(emitter_chain_id, 16) + .store_uint(emitter_address, 256) + .end_cell(); + + ;; Dictionary doesn't support cell as key, so we use cell_hash to create a 256-bit integer key + int data_source_key = cell_hash(data_source); + + (slice value, int found?) = is_valid_data_source.udict_get?(256, data_source_key); + throw_unless(ERROR_UPDATE_DATA_SOURCE_NOT_FOUND, found?); + int valid = value~load_int(1); + throw_unless(ERROR_INVALID_UPDATE_DATA_SOURCE, valid); + + + int root_digest = parse_pyth_payload_in_wormhole_vm(payload); + + repeat(num_updates) { + (int price_id, int price, int conf, int expo, int publish_time, int prev_publish_time, int ema_price, int ema_conf, slice new_cs) = read_and_verify_message(cs, root_digest); + cs = new_cs; + + (slice latest_price_info, int found?) = latest_price_feeds.udict_get?(256, price_id); + int latest_publish_time = 0; + if (found?) { + slice price_feed_slice = latest_price_info~load_ref().begin_parse(); + slice price_slice = price_feed_slice~load_ref().begin_parse(); + + price_slice~load_int(64); ;; Skip price + price_slice~load_uint(64); ;; Skip conf + price_slice~load_int(32); ;; Skip expo + latest_publish_time = price_slice~load_uint(64); + } + + if (publish_time > latest_publish_time) { + cell price_feed = begin_cell() + .store_ref(store_price(price, conf, expo, publish_time)) + .store_ref(store_price(ema_price, ema_conf, expo, publish_time)) + .end_cell(); + + latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed).end_cell().begin_parse()); + } + } + + throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, ~ cs.slice_empty?()); + + store_data(); +} + +() execute_governance_action(slice in_msg_body) impure { + ;; TODO: Implement +} diff --git a/target_chains/ton/contracts/contracts/Wormhole.fc b/target_chains/ton/contracts/contracts/Wormhole.fc index 86c352a3e4..b6e05548a8 100644 --- a/target_chains/ton/contracts/contracts/Wormhole.fc +++ b/target_chains/ton/contracts/contracts/Wormhole.fc @@ -2,6 +2,7 @@ #include "common/errors.fc"; #include "common/utils.fc"; #include "common/storage.fc"; +#include "common/constants.fc"; ;; Signature verification function ;; ECRECOVER: Recovers the signer's address from the signature @@ -13,15 +14,11 @@ "NULLSWAPIFNOT" ;; If recovery failed, insert null under the top of the stack "NULLSWAPIFNOT2"; ;; If recovery failed, insert two more nulls under the top of the stack -;; Constants -const int GUARDIAN_SET_EXPIRY = 86400; ;; 1 day in seconds -const int UPGRADE_MODULE = 0x0000000000000000000000000000000000000000000000000000000000436f7265; ;; "Core" (left-padded to 256 bits) in hex - ;; For troubleshooting purposes -() dump_guardian_sets(cell keys) impure { +() dump_guardian_sets(cell keys_dict) impure { int key = -1; do { - (key, slice value, int found) = keys.udict_get_next?(32, key); + (key, slice value, int found) = keys_dict.udict_get_next?(32, key); if (found) { ~dump(key); ~dump(value); @@ -34,7 +31,6 @@ const int UPGRADE_MODULE = 0x000000000000000000000000000000000000000000000000000 (int, cell, int) parse_guardian_set(slice guardian_set) { slice cs = guardian_set~load_ref().begin_parse(); int expiration_time = cs~load_uint(64); - ;; slice keys = cs~load_ref().begin_parse(); cell keys_dict = cs~load_dict(); int key_count = 0; int key = -1; @@ -44,19 +40,23 @@ const int UPGRADE_MODULE = 0x000000000000000000000000000000000000000000000000000 key_count += 1; } } until (~ found); - return (expiration_time, keys_dict, key_count); } (int, cell, int) get_guardian_set_internal(int index) { (slice guardian_set, int found?) = guardian_sets.udict_get?(32, index); throw_unless(ERROR_GUARDIAN_SET_NOT_FOUND, found?); - (int expiration_time, cell keys, int key_count) = parse_guardian_set(guardian_set); - return (expiration_time, keys, key_count); + (int expiration_time, cell keys_dict, int key_count) = parse_guardian_set(guardian_set); + return (expiration_time, keys_dict, key_count); } ;; Get methods +int get_chain_id() method_id { + load_data(); + return chain_id; +} + int get_current_guardian_set_index() method_id { load_data(); return current_guardian_set_index; @@ -67,36 +67,47 @@ int get_current_guardian_set_index() method_id { return get_guardian_set_internal(index); } -int get_chain_id() method_id { - return chain_id; -} - int get_governance_chain_id() method_id { + load_data(); return governance_chain_id; } int get_governance_contract() method_id { + load_data(); return governance_contract; } int governance_action_is_consumed(int hash) method_id { + load_data(); (_, int found?) = consumed_governance_actions.udict_get?(256, hash); return found?; } - -() verify_signatures(int hash, slice signatures, int signers_length, cell guardian_set_keys, int guardian_set_size) impure { - slice cs = signatures; +() verify_signatures(int hash, cell signatures, int signers_length, cell guardian_set_keys, int guardian_set_size) impure { + slice cs = signatures.begin_parse(); int i = 0; int valid_signatures = 0; while (i < signers_length) { - int guardian_index = cs~load_uint(8); - (_, int found?) = guardian_sets.udict_get?(32, guardian_index); - throw_unless(ERROR_GUARDIAN_SET_NOT_FOUND, found?); - int r = cs~load_uint(256); - int s = cs~load_uint(256); - int v = cs~load_uint(8); + int bits_to_load = 528; + builder sig_builder = begin_cell(); + + while (bits_to_load > 0) { + int available_bits = cs.slice_bits(); + int bits = min(bits_to_load, available_bits); + sig_builder = sig_builder.store_slice(cs~load_bits(bits)); + bits_to_load -= bits; + + if (bits_to_load > 0) { + cs = cs~load_ref().begin_parse(); + } + } + + slice sig_slice = sig_builder.end_cell().begin_parse(); + int guardian_index = sig_slice~load_uint(8); + int r = sig_slice~load_uint(256); + int s = sig_slice~load_uint(256); + int v = sig_slice~load_uint(8); (_, int x1, int x2, int valid) = check_sig(hash, v >= 27 ? v - 27 : v, r, s); throw_unless(ERROR_INVALID_SIGNATURES, valid); int parsed_address = pubkey_to_eth_address(x1, x2); @@ -119,8 +130,8 @@ int governance_action_is_consumed(int hash) method_id { throw_unless(ERROR_INVALID_VERSION, version == 1); int vm_guardian_set_index = in_msg_body~load_uint(32); ;; Verify and check if guardian set is valid - (int expiration_time, cell keys, int key_count) = get_guardian_set_internal(vm_guardian_set_index); - throw_if(ERROR_INVALID_GUARDIAN_SET_KEYS_LENGTH, cell_null?(keys)); + (int expiration_time, cell keys_dict, int key_count) = get_guardian_set_internal(vm_guardian_set_index); + throw_if(ERROR_INVALID_GUARDIAN_SET_KEYS_LENGTH, cell_null?(keys_dict)); throw_unless(ERROR_INVALID_GUARDIAN_SET, (current_guardian_set_index == vm_guardian_set_index) & ((expiration_time == 0) | (expiration_time > now())) @@ -135,11 +146,11 @@ int governance_action_is_consumed(int hash) method_id { ;; Calculate total body length across all references int body_length = 0; - int continue? = -1; ;; -1 is true + int continue? = true; do { body_length += remaining_body.slice_bits(); if (remaining_body.slice_refs_empty?()) { - continue? = 0; + continue? = false; } else { remaining_body = remaining_body~load_ref().begin_parse(); } @@ -148,9 +159,9 @@ int governance_action_is_consumed(int hash) method_id { ;; Load body (cell body_cell, _) = read_and_store_large_data(in_msg_body, body_length); - int hash = hash_vm_body(body_cell.begin_parse()); + int hash = keccak256_int(keccak256_slice(body_cell.begin_parse())); ;; Verify signatures - verify_signatures(hash, signatures.begin_parse(), signers_length, keys, key_count); + verify_signatures(hash, signatures, signers_length, keys_dict, key_count); slice body_slice = body_cell.begin_parse(); int timestamp = body_slice~load_uint(32); @@ -230,20 +241,20 @@ int governance_action_is_consumed(int hash) method_id { ;; Set expiry if current GuardianSet exists (slice current_guardian_set, int found?) = guardian_sets.udict_get?(32, current_guardian_set_index); if (found?) { - (int expiration_time, cell keys, int key_count) = parse_guardian_set(current_guardian_set); + (int expiration_time, cell keys_dict, int key_count) = parse_guardian_set(current_guardian_set); cell updated_guardian_set = begin_cell() .store_uint(now() + GUARDIAN_SET_EXPIRY, 64) ;; expiration time - .store_dict(keys) ;; keys + .store_dict(keys_dict) ;; keys .end_cell(); - guardian_sets~udict_set(32, current_guardian_set_index, updated_guardian_set.begin_parse()); + guardian_sets~udict_set(32, current_guardian_set_index, begin_cell().store_ref(updated_guardian_set).end_cell().begin_parse()); ;; store reference to updated_guardian_set because storeDict stores a dict into a ref } ;; Store the new guardian set cell new_guardian_set = begin_cell() .store_uint(0, 64) ;; expiration_time, set to 0 initially - .store_ref(new_guardian_set_keys) + .store_dict(new_guardian_set_keys) .end_cell(); - guardian_sets~udict_set(32, new_guardian_set_index, new_guardian_set.begin_parse()); + guardian_sets~udict_set(32, new_guardian_set_index, begin_cell().store_ref(new_guardian_set).end_cell().begin_parse()); ;; Update the current guardian set index current_guardian_set_index = new_guardian_set_index; @@ -252,7 +263,3 @@ int governance_action_is_consumed(int hash) method_id { consumed_governance_actions~udict_set(256, hash, begin_cell().store_int(true, 1).end_cell().begin_parse()); store_data(); } - -() execute_governance_action(slice in_msg_body) impure { - ;; TODO: Implement -} diff --git a/target_chains/ton/contracts/contracts/common/constants.fc b/target_chains/ton/contracts/contracts/common/constants.fc new file mode 100644 index 0000000000..44a4cba8fc --- /dev/null +++ b/target_chains/ton/contracts/contracts/common/constants.fc @@ -0,0 +1,9 @@ +const int ACCUMULATOR_MAGIC = 0x504e4155; ;; "PNAU" (Pyth Network Accumulator Update) +const int ACCUMULATOR_WORMHOLE_MAGIC = 0x41555756; ;; Stands for AUWV (Accumulator Update Wormhole Verficiation) +const int MAJOR_VERSION = 1; +const int MINIMUM_ALLOWED_MINOR_VERSION = 0; + +const int GUARDIAN_SET_EXPIRY = 86400; ;; 1 day in seconds +const int UPGRADE_MODULE = 0x0000000000000000000000000000000000000000000000000000000000436f7265; ;; "Core" (left-padded to 256 bits) in hex + +const int WORMHOLE_MERKLE_UPDATE_TYPE = 0; diff --git a/target_chains/ton/contracts/contracts/common/errors.fc b/target_chains/ton/contracts/contracts/common/errors.fc index e13bcfd7dc..9b5a15d6c0 100644 --- a/target_chains/ton/contracts/contracts/common/errors.fc +++ b/target_chains/ton/contracts/contracts/common/errors.fc @@ -27,3 +27,11 @@ const int ERROR_OUTDATED_PRICE = 1020; const int ERROR_INVALID_MAGIC = 1021; const int ERROR_INVALID_MAJOR_VERSION = 1022; const int ERROR_INVALID_MINOR_VERSION = 1023; +const int ERROR_UPDATE_DATA_SOURCE_NOT_FOUND = 1024; +const int ERROR_INVALID_UPDATE_DATA_SOURCE = 1025; +const int ERROR_DIGEST_MISMATCH = 1026; +const int ERROR_INVALID_UPDATE_DATA_LENGTH = 1027; +const int ERROR_INVALID_UPDATE_DATA_TYPE = 1028; +const int ERROR_INVALID_MESSAGE_TYPE = 1029; +const int ERROR_INSUFFICIENT_FEE = 1030; +const int ERROR_INVALID_PROOF_SIZE = 1031; diff --git a/target_chains/ton/contracts/contracts/common/merkle_tree.fc b/target_chains/ton/contracts/contracts/common/merkle_tree.fc new file mode 100644 index 0000000000..a6d91982fd --- /dev/null +++ b/target_chains/ton/contracts/contracts/common/merkle_tree.fc @@ -0,0 +1,42 @@ +#include "../imports/stdlib.fc"; +#include "utils.fc"; + +const int MERKLE_LEAF_PREFIX = 0; +const int MERKLE_NODE_PREFIX = 1; + +int leaf_hash(slice message) { + int hash = keccak256_slice(begin_cell() + .store_uint(MERKLE_LEAF_PREFIX, 8) + .store_slice(message) + .end_cell().begin_parse()); + return hash >> 96; +} + +int node_hash(int a, int b) { + int min_value = min(a, b); + int max_value = max(a, b); + int hash = keccak256_slice(begin_cell() + .store_uint(MERKLE_NODE_PREFIX, 8) + .store_uint(min_value, 160) + .store_uint(max_value, 160) + .end_cell().begin_parse()); + return hash >> 96; +} + +slice read_and_verify_proof(int root_digest, slice message, slice cs) impure { + int current_hash = leaf_hash(message); + int proof_size = cs~load_uint(8); + + repeat(proof_size) { + builder sibling_digest = begin_cell(); + (cell digest_cell, cs) = read_and_store_large_data(cs, 160); + slice digest_slice = digest_cell.begin_parse(); + sibling_digest = sibling_digest.store_slice(digest_slice); + slice sibling_digest_slice = sibling_digest.end_cell().begin_parse(); + int sibling_digest_int = sibling_digest_slice~load_uint(160); + current_hash = node_hash(current_hash, sibling_digest_int); + } + + throw_unless(ERROR_DIGEST_MISMATCH, root_digest == current_hash); + return cs; +} diff --git a/target_chains/ton/contracts/contracts/common/op.fc b/target_chains/ton/contracts/contracts/common/op.fc new file mode 100644 index 0000000000..7471081229 --- /dev/null +++ b/target_chains/ton/contracts/contracts/common/op.fc @@ -0,0 +1,2 @@ +const int OP_UPDATE_GUARDIAN_SET = 1; +const int OP_UPDATE_PRICE_FEEDS = 2; diff --git a/target_chains/ton/contracts/contracts/common/storage.fc b/target_chains/ton/contracts/contracts/common/storage.fc index f7830589b0..db87edf313 100644 --- a/target_chains/ton/contracts/contracts/common/storage.fc +++ b/target_chains/ton/contracts/contracts/common/storage.fc @@ -5,6 +5,13 @@ ;; PriceFeed struct: {price: Price, ema_price: Price} global cell latest_price_feeds; ;; Dictionary of PriceFeed structs, keyed by price_feed_id (256-bit) global int single_update_fee; +;; DataSource struct: (emitter_chain_id: int, emitter_address: int) +;; emitter_chain_id is a 16-bit unsigned integer +;; emitter_address is a 256-bit unsigned integer +global cell data_sources; ;; Dictionary of DataSource tuples, keyed by u8 +global int num_data_sources; +global cell is_valid_data_source; ;; Dictionary of int (0 as false, -1 as true), keyed by DataSource cell_hash + ;; Wormhole global int current_guardian_set_index; @@ -16,33 +23,77 @@ global cell guardian_sets; global int chain_id; global int governance_chain_id; global int governance_contract; -global cell consumed_governance_actions; +global cell consumed_governance_actions; ;; Dictionary of int (0 as false, -1 as true), keyed by int (hash of the governance action) +global cell governance_data_source; ;; Single DataSource tuple +global int last_executed_governance_sequence; ;; u64 +global int governance_data_source_index; ;; u32 () store_data() impure inline_ref { - begin_cell() + cell price_feeds_cell = begin_cell() .store_dict(latest_price_feeds) .store_uint(single_update_fee, 256) + .end_cell(); + + cell data_sources_cell = begin_cell() + .store_dict(data_sources) + .store_uint(num_data_sources, 32) + .store_dict(is_valid_data_source) + .end_cell(); + + cell guardian_set_cell = begin_cell() .store_uint(current_guardian_set_index, 32) .store_dict(guardian_sets) + .end_cell(); + + cell governance_cell = begin_cell() .store_uint(chain_id, 16) .store_uint(governance_chain_id, 16) .store_uint(governance_contract, 256) .store_dict(consumed_governance_actions) + .store_ref(governance_data_source) + .store_uint(last_executed_governance_sequence, 64) + .store_uint(governance_data_source_index, 32) + .end_cell(); + + begin_cell() + .store_ref(price_feeds_cell) + .store_ref(data_sources_cell) + .store_ref(guardian_set_cell) + .store_ref(governance_cell) .end_cell() .set_data(); } ;; load_data populates storage variables using stored data () load_data() impure inline_ref { - var ds = get_data().begin_parse(); - latest_price_feeds = ds~load_dict(); - single_update_fee = ds~load_uint(256); - current_guardian_set_index = ds~load_uint(32); - guardian_sets = ds~load_dict(); - chain_id = ds~load_uint(16); - governance_chain_id = ds~load_uint(16); - governance_contract = ds~load_uint(256); - consumed_governance_actions = ds~load_dict(); + slice ds = get_data().begin_parse(); + + cell price_feeds_cell = ds~load_ref(); + slice price_feeds_slice = price_feeds_cell.begin_parse(); + latest_price_feeds = price_feeds_slice~load_dict(); + single_update_fee = price_feeds_slice~load_uint(256); + + cell data_sources_cell = ds~load_ref(); + slice data_sources_slice = data_sources_cell.begin_parse(); + data_sources = data_sources_slice~load_dict(); + num_data_sources = data_sources_slice~load_uint(32); + is_valid_data_source = data_sources_slice~load_dict(); + + cell guardian_set_cell = ds~load_ref(); + slice guardian_set_slice = guardian_set_cell.begin_parse(); + current_guardian_set_index = guardian_set_slice~load_uint(32); + guardian_sets = guardian_set_slice~load_dict(); + + cell governance_cell = ds~load_ref(); + slice governance_slice = governance_cell.begin_parse(); + chain_id = governance_slice~load_uint(16); + governance_chain_id = governance_slice~load_uint(16); + governance_contract = governance_slice~load_uint(256); + consumed_governance_actions = governance_slice~load_dict(); + governance_data_source = governance_slice~load_ref(); + last_executed_governance_sequence = governance_slice~load_uint(64); + governance_data_source_index = governance_slice~load_uint(32); + ds.end_parse(); } diff --git a/target_chains/ton/contracts/contracts/common/utils.fc b/target_chains/ton/contracts/contracts/common/utils.fc index 96f6a16728..4c0c94d2c6 100644 --- a/target_chains/ton/contracts/contracts/common/utils.fc +++ b/target_chains/ton/contracts/contracts/common/utils.fc @@ -1,26 +1,38 @@ +#include "../imports/stdlib.fc"; + ;; Built-in assembly functions int keccak256(slice s) asm "1 PUSHINT HASHEXT_KECCAK256"; ;; Keccak-256 hash function -;; TODO: Implement a Fift-based solution for arbitrary-sized input. -int keccack256_4(slice s1, slice s2, slice s3, slice s4) asm "4 PUSHINT HASHEXT_KECCAK256"; +int keccak256_tuple(tuple t) asm "DUP TLEN EXPLODEVAR HASHEXT_KECCAK256"; const MAX_BITS = 1016; -int hash_vm_body(slice s) inline { - ;; Assumes the input slice 's' contains a VM body stored across 4 cells, - ;; each containing up to 1016 bits of data. - ;; The total size of the VM body should not exceed 4064 bits (4 * 1016). - slice s1 = s~load_bits(s.slice_bits()); - s = s~load_ref().begin_parse(); - slice s2 = s~load_bits(s.slice_bits()); - s = s~load_ref().begin_parse(); - slice s3 = s~load_bits(s.slice_bits()); - s = s~load_ref().begin_parse(); - slice s4 = s~load_bits(s.slice_bits()); - int hash = keccack256_4(s1, s2, s3, s4); +int keccak256_int(int hash) inline { slice hash_slice = begin_cell().store_uint(hash, 256).end_cell().begin_parse(); return keccak256(hash_slice); } +int keccak256_slice(slice s) inline { + tuple slices = empty_tuple(); + int continue = true; + + while (continue) { + if (~ s.slice_empty?()) { + slice current_slice = s~load_bits(s.slice_bits()); + slices~tpush(current_slice); + + if (s.slice_refs_empty?()) { + continue = false; + } else { + s = s~load_ref().begin_parse(); + } + } else { + continue = false; + } + } + + return keccak256_tuple(slices); +} + ;; Splits a slice into chunks of MAX_BITS bits or less, in reverse order (cell, slice) split_into_reverse_chunks(slice data, int size) { cell chunks = null(); @@ -62,7 +74,7 @@ Note: - The function uses a maximum of 1016 bits per cell (instead of 1023) to ensure byte alignment - If the input data exceeds 1016 bits, it is split into multiple cells linked by references -} -(cell, slice) read_and_store_large_data(slice in_msg_body, int size) impure { +(cell, slice) read_and_store_large_data(slice in_msg_body, int size) { (cell chunks, slice remaining) = split_into_reverse_chunks(in_msg_body, size); cell last_cell = null(); while (~ cell_null?(chunks)) { diff --git a/target_chains/ton/contracts/contracts/tests/PythTest.fc b/target_chains/ton/contracts/contracts/tests/PythTest.fc index 285710808c..20a330ef0e 100644 --- a/target_chains/ton/contracts/contracts/tests/PythTest.fc +++ b/target_chains/ton/contracts/contracts/tests/PythTest.fc @@ -1,8 +1,32 @@ +{- + 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. +-} + #include "../imports/stdlib.fc"; #include "../Pyth.fc"; +#include "../Wormhole.fc"; +#include "../common/op.fc"; () recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { - ;; nop; + if (in_msg_body.slice_empty?()) { + return (); + } + + int op = in_msg_body~load_uint(32); + cell data = in_msg_body~load_ref(); + if (op == OP_UPDATE_GUARDIAN_SET) { + update_guardian_set(data.begin_parse()); + } elseif (op == OP_UPDATE_PRICE_FEEDS) { + update_price_feeds(msg_value, data.begin_parse()); + } else { + throw(0xffff); ;; Throw exception for unknown op + } } (int, int, int, int) test_get_price_unsafe(int price_feed_id) method_id { @@ -24,3 +48,19 @@ (int) test_get_update_fee(slice in_msg_body) method_id { return get_update_fee(in_msg_body); } + +(int) test_get_chain_id() method_id { + return get_chain_id(); +} + +(int) test_get_last_executed_governance_sequence() method_id { + return get_last_executed_governance_sequence(); +} + +(int) test_get_governance_data_source_index() method_id { + return get_governance_data_source_index(); +} + +(cell) test_get_governance_data_source() method_id { + return get_governance_data_source(); +} diff --git a/target_chains/ton/contracts/contracts/tests/WormholeTest.fc b/target_chains/ton/contracts/contracts/tests/WormholeTest.fc index 6d3d2b5577..475ec1d584 100644 --- a/target_chains/ton/contracts/contracts/tests/WormholeTest.fc +++ b/target_chains/ton/contracts/contracts/tests/WormholeTest.fc @@ -1,8 +1,29 @@ +{- + 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. +-} + #include "../imports/stdlib.fc"; #include "../Wormhole.fc"; +#include "../common/op.fc"; () recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { - ;; nop; + if (in_msg_body.slice_empty?()) { + return (); + } + + int op = in_msg_body~load_uint(32); + cell data = in_msg_body~load_ref(); + if (op == OP_UPDATE_GUARDIAN_SET) { + update_guardian_set(data.begin_parse()); + } else { + throw(0xffff); ;; Throw exception for unknown op + } } (int, int, int, cell, int) test_parse_encoded_upgrade(int current_guardian_set_index, slice payload) method_id { @@ -13,13 +34,6 @@ return parse_and_verify_wormhole_vm(in_msg_body); } -(int) test_update_guardian_set(slice in_msg_body) method_id { - int old_guardian_set_index = get_current_guardian_set_index(); - update_guardian_set(in_msg_body); - int new_guardian_set_index = get_current_guardian_set_index(); - return new_guardian_set_index > old_guardian_set_index; -} - (int) test_get_current_guardian_set_index() method_id { return get_current_guardian_set_index(); } @@ -27,3 +41,19 @@ (int, cell, int) test_get_guardian_set(int index) method_id { return get_guardian_set(index); } + +(int) test_get_chain_id() method_id { + return get_chain_id(); +} + +(int) test_get_governance_chain_id() method_id { + return get_governance_chain_id(); +} + +(int) test_get_governance_contract() method_id { + return get_governance_contract(); +} + +(int) test_governance_action_is_consumed(int hash) method_id { + return governance_action_is_consumed(hash); +} diff --git a/target_chains/ton/contracts/package.json b/target_chains/ton/contracts/package.json index 845a52b254..7948c04a43 100644 --- a/target_chains/ton/contracts/package.json +++ b/target_chains/ton/contracts/package.json @@ -9,6 +9,7 @@ }, "devDependencies": { "@pythnetwork/price-service-sdk": "workspace:*", + "@pythnetwork/xc-admin-common": "workspace:*", "@ton/blueprint": "^0.22.0", "@ton/core": "~0", "@ton/crypto": "^3.2.0", diff --git a/target_chains/ton/contracts/tests/PythTest.spec.ts b/target_chains/ton/contracts/tests/PythTest.spec.ts index 56ab466a51..6db0e18245 100644 --- a/target_chains/ton/contracts/tests/PythTest.spec.ts +++ b/target_chains/ton/contracts/tests/PythTest.spec.ts @@ -2,16 +2,12 @@ import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; import { Cell, toNano } from "@ton/core"; import "@ton/test-utils"; import { compile } from "@ton/blueprint"; -import { - HexString, - parseAccumulatorUpdateData, - Price, -} from "@pythnetwork/price-service-sdk"; +import { HexString, Price } from "@pythnetwork/price-service-sdk"; import { PythTest, PythTestConfig } from "../wrappers/PythTest"; -import { HERMES_BTC_ETH_UPDATE } from "./utils/pyth"; +import { BTC_PRICE_FEED_ID, HERMES_BTC_ETH_UPDATE } from "./utils/pyth"; +import { GUARDIAN_SET_0, MAINNET_UPGRADE_VAAS } from "./utils/wormhole"; +import { DataSource } from "@pythnetwork/xc-admin-common"; -const PRICE_FEED_ID = - "0x0000000000000000000000000000000000000000000000000000000000000000"; const TIME_PERIOD = 60; const PRICE = new Price({ price: "1", @@ -26,6 +22,13 @@ const EMA_PRICE = new Price({ publishTime: 8, }); const SINGLE_UPDATE_FEE = 1; +const DATA_SOURCES: DataSource[] = [ + { + emitterChain: 26, + emitterAddress: + "e101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71", + }, +]; describe("PythTest", () => { let code: Cell; @@ -44,11 +47,17 @@ describe("PythTest", () => { }); async function deployContract( - priceFeedId: HexString = PRICE_FEED_ID, + priceFeedId: HexString = BTC_PRICE_FEED_ID, timePeriod: number = TIME_PERIOD, price: Price = PRICE, emaPrice: Price = EMA_PRICE, - singleUpdateFee: number = SINGLE_UPDATE_FEE + singleUpdateFee: number = SINGLE_UPDATE_FEE, + dataSources: DataSource[] = DATA_SOURCES, + guardianSetIndex: number = 0, + guardianSet: string[] = GUARDIAN_SET_0, + chainId: number = 1, + governanceChainId: number = 1, + governanceContract: string = "0000000000000000000000000000000000000000000000000000000000000004" ) { const config: PythTestConfig = { priceFeedId, @@ -56,6 +65,12 @@ describe("PythTest", () => { price, emaPrice, singleUpdateFee, + dataSources, + guardianSetIndex, + guardianSet, + chainId, + governanceChainId, + governanceContract, }; pythTest = blockchain.openContract(PythTest.createFromConfig(config, code)); @@ -76,7 +91,7 @@ describe("PythTest", () => { it("should correctly get price unsafe", async () => { await deployContract(); - const result = await pythTest.getPriceUnsafe(PRICE_FEED_ID); + const result = await pythTest.getPriceUnsafe(BTC_PRICE_FEED_ID); expect(result.price).toBe(1); expect(result.conf).toBe(2); expect(result.expo).toBe(3); @@ -91,11 +106,11 @@ describe("PythTest", () => { expo: 3, publishTime: timeNow, }); - await deployContract(PRICE_FEED_ID, TIME_PERIOD, price, EMA_PRICE); + await deployContract(BTC_PRICE_FEED_ID, TIME_PERIOD, price, EMA_PRICE); const result = await pythTest.getPriceNoOlderThan( TIME_PERIOD, - PRICE_FEED_ID + BTC_PRICE_FEED_ID ); expect(result.price).toBe(1); @@ -108,7 +123,7 @@ describe("PythTest", () => { await deployContract(); await expect( - pythTest.getPriceNoOlderThan(TIME_PERIOD, PRICE_FEED_ID) + pythTest.getPriceNoOlderThan(TIME_PERIOD, BTC_PRICE_FEED_ID) ).rejects.toThrow("Unable to execute get method. Got exit_code: 1020"); // ERROR_OUTDATED_PRICE = 1020 }); @@ -120,11 +135,11 @@ describe("PythTest", () => { expo: 7, publishTime: timeNow, }); - await deployContract(PRICE_FEED_ID, TIME_PERIOD, PRICE, emaPrice); + await deployContract(BTC_PRICE_FEED_ID, TIME_PERIOD, PRICE, emaPrice); const result = await pythTest.getEmaPriceNoOlderThan( TIME_PERIOD, - PRICE_FEED_ID + BTC_PRICE_FEED_ID ); expect(result.price).toBe(5); @@ -137,14 +152,14 @@ describe("PythTest", () => { await deployContract(); await expect( - pythTest.getEmaPriceNoOlderThan(TIME_PERIOD, PRICE_FEED_ID) + pythTest.getEmaPriceNoOlderThan(TIME_PERIOD, BTC_PRICE_FEED_ID) ).rejects.toThrow("Unable to execute get method. Got exit_code: 1020"); // ERROR_OUTDATED_PRICE = 1020 }); it("should correctly get ema price unsafe", async () => { await deployContract(); - const result = await pythTest.getEmaPriceUnsafe(PRICE_FEED_ID); + const result = await pythTest.getEmaPriceUnsafe(BTC_PRICE_FEED_ID); expect(result.price).toBe(5); expect(result.conf).toBe(6); @@ -161,4 +176,110 @@ describe("PythTest", () => { expect(result).toBe(2); }); + + it("should correctly update price feeds", async () => { + await deployContract(); + let result; + + const mainnet_upgrade_vaa_1 = MAINNET_UPGRADE_VAAS[0]; + result = await pythTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(mainnet_upgrade_vaa_1, "hex") + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + const mainnet_upgrade_vaa_2 = MAINNET_UPGRADE_VAAS[1]; + result = await pythTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(mainnet_upgrade_vaa_2, "hex") + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + const mainnet_upgrade_vaa_3 = MAINNET_UPGRADE_VAAS[2]; + result = await pythTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(mainnet_upgrade_vaa_3, "hex") + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + const mainnet_upgrade_vaa_4 = MAINNET_UPGRADE_VAAS[3]; + result = await pythTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(mainnet_upgrade_vaa_4, "hex") + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + const updateData = Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"); + const updateFee = await pythTest.getUpdateFee(updateData); + + result = await pythTest.sendUpdatePriceFeeds( + deployer.getSender(), + updateData, + toNano(updateFee) + ); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: pythTest.address, + success: true, + }); + + // Check if the price has been updated correctly + const updatedPrice = await pythTest.getPriceUnsafe(BTC_PRICE_FEED_ID); + expect(updatedPrice.price).not.toBe(Number(PRICE.price)); // Since we updated the price, it should not be the same as the initial price + expect(updatedPrice.publishTime).toBeGreaterThan(PRICE.publishTime); + }); + + it("should return the correct chain ID", async () => { + await deployContract(); + + const result = await pythTest.getChainId(); + expect(result).toEqual(1); + }); + + it("should return the correct last executed governance sequence", async () => { + await deployContract(); + + const result = await pythTest.getLastExecutedGovernanceSequence(); + expect(result).toEqual(0); // Initial value should be 0 + + // TODO: add more tests for other governance sequences + }); + + it("should return the correct governance data source index", async () => { + await deployContract(); + + const result = await pythTest.getGovernanceDataSourceIndex(); + expect(result).toEqual(0); // Initial value should be 0 + + // TODO: add more tests for other governance data source index + }); + + it("should return an empty cell for governance data source", async () => { + await deployContract(); + + const result = await pythTest.getGovernanceDataSource(); + // assert that the result is an empty cell initally + expect(result).toBeDefined(); + expect(result.bits.length).toBe(0); + expect(result.refs.length).toBe(0); + + // TODO: add more tests for other governance data source + }); }); diff --git a/target_chains/ton/contracts/tests/WormholeTest.spec.ts b/target_chains/ton/contracts/tests/WormholeTest.spec.ts index b8c211b8a6..547f34f133 100644 --- a/target_chains/ton/contracts/tests/WormholeTest.spec.ts +++ b/target_chains/ton/contracts/tests/WormholeTest.spec.ts @@ -10,6 +10,11 @@ import { MAINNET_UPGRADE_VAAS, } from "./utils/wormhole"; +const CHAIN_ID = 1; +const GOVERNANCE_CHAIN_ID = 1; +const GOVERNANCE_CONTRACT = + "0000000000000000000000000000000000000000000000000000000000000004"; + describe("WormholeTest", () => { let code: Cell; @@ -29,9 +34,9 @@ describe("WormholeTest", () => { async function deployContract( guardianSetIndex: number = 0, guardianSet: string[] = GUARDIAN_SET_0, - chainId: number = 1, - governanceChainId: number = 1, - governanceContract: string = "0000000000000000000000000000000000000000000000000000000000000004" + chainId: number = CHAIN_ID, + governanceChainId: number = GOVERNANCE_CHAIN_ID, + governanceContract: string = GOVERNANCE_CONTRACT ) { const config: WormholeTestConfig = { guardianSetIndex, @@ -139,20 +144,34 @@ describe("WormholeTest", () => { const mainnet_upgrade_vaa_1 = MAINNET_UPGRADE_VAAS[0]; - const getUpdateGuardianSetResult = await wormholeTest.getUpdateGuardianSet( + const getUpdateGuardianSetResult = await wormholeTest.sendUpdateGuardianSet( + deployer.getSender(), Buffer.from(mainnet_upgrade_vaa_1, "hex") ); - expect(getUpdateGuardianSetResult).toBe(-1); + expect(getUpdateGuardianSetResult.transactions).toHaveTransaction({ + from: deployer.address, + to: wormholeTest.address, + success: true, + }); + + const getCurrentGuardianSetIndexResult = + await wormholeTest.getCurrentGuardianSetIndex(); + expect(getCurrentGuardianSetIndexResult).toBe(1); }); it("should fail with wrong vaa", async () => { await deployContract(); const invalid_mainnet_upgrade_vaa = "00" + MAINNET_UPGRADE_VAAS[0].slice(2); - await expect( - wormholeTest.getUpdateGuardianSet( - Buffer.from(invalid_mainnet_upgrade_vaa, "hex") - ) - ).rejects.toThrow("Unable to execute get method. Got exit_code: 1001"); // ERROR_INVALID_VERSION = 1001 + const result = await wormholeTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(invalid_mainnet_upgrade_vaa, "hex") + ); + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: wormholeTest.address, + success: false, + exitCode: 1001, // ERROR_INVALID_VERSION = 1001 + }); }); it("should correctly get guardian set", async () => { @@ -161,4 +180,61 @@ describe("WormholeTest", () => { const getGuardianSetResult = await wormholeTest.getGuardianSet(0); expect(getGuardianSetResult.keys).toEqual(GUARDIAN_SET_0); }); + + it("should return the correct chain ID", async () => { + await deployContract(); + + const result = await wormholeTest.getChainId(); + expect(result).toEqual(CHAIN_ID); + }); + + it("should return the correct governance chain ID", async () => { + await deployContract(); + + const result = await wormholeTest.getGovernanceChainId(); + expect(result).toEqual(GOVERNANCE_CHAIN_ID); + }); + + it("should return the correct governance contract address", async () => { + await deployContract(); + + const result = await wormholeTest.getGovernanceContract(); + expect(result).toEqual(GOVERNANCE_CONTRACT); + }); + + it("should correctly check if a governance action is consumed", async () => { + await deployContract(); + + const hash = 12345n; + let getGovernanceActionIsConsumedResult = + await wormholeTest.getGovernanceActionIsConsumed(hash); + expect(getGovernanceActionIsConsumedResult).toEqual(false); + + const mainnet_upgrade_vaa_1 = MAINNET_UPGRADE_VAAS[0]; + + const getParseAndVerifyWormholeVmResult = + await wormholeTest.getParseAndVerifyWormholeVm( + Buffer.from(mainnet_upgrade_vaa_1, "hex") + ); + expect(getParseAndVerifyWormholeVmResult.hash).toBe( + "ed3a5600d44b9dcc889daf0178dd69ab1e9356308194ba3628a7b720ae48a8d5" + ); + + const sendUpdateGuardianSetResult = + await wormholeTest.sendUpdateGuardianSet( + deployer.getSender(), + Buffer.from(mainnet_upgrade_vaa_1, "hex") + ); + expect(sendUpdateGuardianSetResult.transactions).toHaveTransaction({ + from: deployer.address, + to: wormholeTest.address, + success: true, + }); + + getGovernanceActionIsConsumedResult = + await wormholeTest.getGovernanceActionIsConsumed( + BigInt("0x" + getParseAndVerifyWormholeVmResult.hash) + ); + expect(getGovernanceActionIsConsumedResult).toEqual(true); + }); }); diff --git a/target_chains/ton/contracts/tests/utils/pyth.ts b/target_chains/ton/contracts/tests/utils/pyth.ts index d079ee45f5..1983b5eaa7 100644 --- a/target_chains/ton/contracts/tests/utils/pyth.ts +++ b/target_chains/ton/contracts/tests/utils/pyth.ts @@ -33,3 +33,6 @@ */ export const HERMES_BTC_ETH_UPDATE = "504e41550100000003b801000000040d00a0bb18e08c0a4152eba8293e88d0ed43084dfd4677fd5dc0ff48b05d065e25511ea12181325e92541290f28e00487a7ed852fdecbee414cab803dbe1dac2392201023e177888eba8922eac9b0668566f15c61e945cd47c10fa4ca2e4d472d7d216f149f3378b4edc5f8d802c3ef9b8156ca53c9ae2d4f75dd91f7713946b4108c5910003af26c2426a1bf19f24a171bcc990dad056b670f76894e3bdb9925b21b40b3904757d8e6175133b8608431d7435e29c5fcc2912349c2c8b5588803c06f203c73401048a30050ebafd161c3cfa5531896040b6da88502734c8e42ca3197d52ea08f6ec785a24f24bc9325c16ee7b6a9791bc523523f9086162ed4ccf746b55e1b0f192010886c5256df6ca2719fe97c10b79a4a8c8574fb70add9bfe0d879ae5f6c69b2459360b50b58c43a65e881081174cce56827880e0c330b5c5681294dc3fcb78a86d010a4e0ebb1992f0e48263f6188cb5f8e871cdcd7879f54fe7ad53bbd28d5e7ff70e73441836f0d11076bd7a791aceb05d501500f6878cf26e641fffa7c8fd143371000b3ad70bd80e52a82cd740ffbd4a080bd22523bc7ac2b1242169516d7aaf2753cd7ee5b500134ef32c02284e9d806fbeab2e055ea4a94be9cbfcfbc39b249b5e6b010c97d60c0f15b18c8fb2f36331ab0a1ce0efa13e9f2118c32140bd2118823d50f12deffc40b5d0c9642b4e44a6bd1cf4f38de471536a6610e698a942f049abef35010da2004619d8b31e33037ffed4afdd97459a241dfc7fa3bcc426f175461c938a182db560547dfcdd8ede345f0cc69da33fd588c30e912b7521c3ac1b0455882628000e81868d37eb16e1988451c26cfea8bb7969ce11c89488cedea30c80e3416dd2147c0554e9a9cce1a864eb0db625baa2cbb226ae2c2f1051f84b0a711c4bf69647010f02f18088ddbabd7c4528a1f7582f5fb11e60c5e434e9fd4ca2b33d6646e2ac6e6459c651778d1531711b44d2a1204a0d9c17e218aba5e60800e80aade9f1d90400108c783ad40f93184ad4f7e84229b207b17099e78b8bd93ddf2434cba21c99b4a904d74555ced9977e6becc34fa346c3cca9332b3598e66e58eb56f9ac700074e0001270a31d95bd5426ffe943dcc2b93f05b93f8301848f0b19c56e0dea51b7742c467b6bb557f6fc6762ef4600988c2dbcad0a2be84d4c6839fbae05d227e30ce5f50166cec2c600000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71000000000493073601415557560000000000099e556100002710c0905b1576f0bb86fe861a51273f2bcc43d12dd702005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000005634a12c5d50000000096b3a0ebfffffff80000000066cec2c60000000066cec2c600000566c715d5a000000000895abaa20a83db6da7dfbe7cc34f56265123320d7765dda3ae132c1518c53ead6cde500c139f68a894f564d817c0dfaeefa80d4ed93d36b82f7fcfe80e4092bb54d4dae770124803c592f17cb918c9ac381ce82bd6817041aa5ae95d917d75687b7a389a188846dd79bd55cb6cb9b9d0e1c0c040f1110362a8e1e87c74887326d66b213d19f7dcd1766b6f8505be50f5c98783c07ec08f913cbe38c20a31e440e42bb5a8883356dd19288e618e938ae4e7031d52d684f7bd1ddcf0d4ff63844800e14ff0c6888ff26626ea9874005500ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace000000396f6987dd00000000052fbd87fffffff80000000066cec2c60000000066cec2c6000000398c4513280000000006c9a0a10a5307d6b780c8c5579b4b61de5fe0f12a789efdc628f14603df77ba31734c5611291df9a86e51430b243b3b61ee716bd758360783bdb7ef2f96ab8e92d509d85940a832b3de426d243a6d52ccf1e538af48b1bfad061dec1e4293898ca9ca9f37d0050d7b9e6a54357e77e7d79156fa1c70d54484ce52df49e79ab3fde203359f760fd9cf78be10644a43b52a171026829814590e8ba4853f6c8f0a16ce0fe3b532bcc96ea72723ab7d19f700c153e7cfb950f4aaa691e731e6f1de4edf43a6b9dd532a8ee785f084"; + +export const BTC_PRICE_FEED_ID = + "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; diff --git a/target_chains/ton/contracts/tests/utils/wormhole.ts b/target_chains/ton/contracts/tests/utils/wormhole.ts index ebfbb5406d..b5607caebd 100644 --- a/target_chains/ton/contracts/tests/utils/wormhole.ts +++ b/target_chains/ton/contracts/tests/utils/wormhole.ts @@ -1,4 +1,4 @@ -import { Cell } from "@ton/core"; +import { beginCell, Cell, Dictionary } from "@ton/core"; export const GUARDIAN_SET_0 = ["0x58CC3AE5C097B213CE3C81979E1B9F9570746AA5"]; @@ -137,6 +137,31 @@ export function createGuardianSetUpgradeBytes( return payload.subarray(0, offset); } +export function createGuardianSetsDict( + guardianSet: string[], + guardianSetIndex: number +): Dictionary { + const guardianSetDict = Dictionary.empty( + Dictionary.Keys.Uint(8), + Dictionary.Values.Buffer(20) + ); + guardianSet.forEach((key, index) => { + guardianSetDict.set(index, Buffer.from(key.slice(2), "hex")); + }); + + const guardianSets = Dictionary.empty( + Dictionary.Keys.Uint(32), + Dictionary.Values.Cell() + ); + const guardianSetCell = beginCell() + .storeUint(0, 64) // expiration_time, set to 0 for testing + .storeDict(guardianSetDict) + .endCell(); + guardianSets.set(guardianSetIndex, guardianSetCell); + + return guardianSets; +} + export function parseGuardianSetKeys(cell: Cell): string[] { const keys: string[] = []; diff --git a/target_chains/ton/contracts/wrappers/PythTest.ts b/target_chains/ton/contracts/wrappers/PythTest.ts index 78ac63d1db..ee9a977c20 100644 --- a/target_chains/ton/contracts/wrappers/PythTest.ts +++ b/target_chains/ton/contracts/wrappers/PythTest.ts @@ -8,9 +8,12 @@ import { Dictionary, Sender, SendMode, + toNano, } from "@ton/core"; import { HexString, Price } from "@pythnetwork/price-service-sdk"; import { createCellChain } from "../tests/utils"; +import { createGuardianSetsDict } from "../tests/utils/wormhole"; +import { DataSource } from "@pythnetwork/xc-admin-common"; export type PythTestConfig = { priceFeedId: HexString; @@ -18,6 +21,12 @@ export type PythTestConfig = { price: Price; emaPrice: Price; singleUpdateFee: number; + dataSources: DataSource[]; + guardianSetIndex: number; + guardianSet: string[]; + chainId: number; + governanceChainId: number; + governanceContract: string; }; export class PythTest implements Contract { @@ -36,7 +45,13 @@ export class PythTest implements Contract { config.timePeriod, config.price, config.emaPrice, - config.singleUpdateFee + config.singleUpdateFee, + config.dataSources, + config.guardianSetIndex, + config.guardianSet, + config.chainId, + config.governanceChainId, + config.governanceContract ); const init = { code, data }; return new PythTest(contractAddress(workchain, init), init); @@ -47,7 +62,13 @@ export class PythTest implements Contract { timePeriod: number, price: Price, emaPrice: Price, - singleUpdateFee: number + singleUpdateFee: number, + dataSources: DataSource[], + guardianSetIndex: number, + guardianSet: string[], + chainId: number, + governanceChainId: number, + governanceContract: string ): Cell { const priceDict = Dictionary.empty( Dictionary.Keys.BigUint(256), @@ -55,17 +76,14 @@ export class PythTest implements Contract { ); const priceCell = beginCell() - .storeInt(price.getPriceAsNumberUnchecked() * 10 ** -price.expo, 256) + .storeInt(price.getPriceAsNumberUnchecked() * 10 ** -price.expo, 64) .storeUint(price.getConfAsNumberUnchecked() * 10 ** -price.expo, 64) .storeInt(price.expo, 32) .storeUint(price.publishTime, 64) .endCell(); const emaPriceCell = beginCell() - .storeInt( - emaPrice.getPriceAsNumberUnchecked() * 10 ** -emaPrice.expo, - 256 - ) + .storeInt(emaPrice.getPriceAsNumberUnchecked() * 10 ** -emaPrice.expo, 64) .storeUint(emaPrice.getConfAsNumberUnchecked() * 10 ** -emaPrice.expo, 64) .storeInt(emaPrice.expo, 32) .storeUint(emaPrice.publishTime, 64) @@ -79,20 +97,63 @@ export class PythTest implements Contract { priceDict.set(BigInt(priceFeedId), priceFeedCell); + // Create a dictionary for data sources + const dataSourcesDict = Dictionary.empty( + Dictionary.Keys.Uint(32), + Dictionary.Values.Cell() + ); + // Create a dictionary for valid data sources + const isValidDataSourceDict = Dictionary.empty( + Dictionary.Keys.BigUint(256), + Dictionary.Values.Bool() + ); + + dataSources.forEach((source, index) => { + const sourceCell = beginCell() + .storeUint(source.emitterChain, 16) + .storeBuffer(Buffer.from(source.emitterAddress, "hex")) + .endCell(); + dataSourcesDict.set(index, sourceCell); + const cellHash = BigInt("0x" + sourceCell.hash().toString("hex")); + isValidDataSourceDict.set(cellHash, true); + }); + + // Group price feeds and update fee + const priceFeedsCell = beginCell() + .storeDict(priceDict) + .storeUint(singleUpdateFee, 256) + .endCell(); + + // Group data sources information + const dataSourcesCell = beginCell() + .storeDict(dataSourcesDict) + .storeUint(dataSources.length, 32) + .storeDict(isValidDataSourceDict) + .endCell(); + + // Group guardian set information + const guardianSetCell = beginCell() + .storeUint(guardianSetIndex, 32) + .storeDict(createGuardianSetsDict(guardianSet, guardianSetIndex)) + .endCell(); + + // Group chain and governance information + const governanceCell = beginCell() + .storeUint(chainId, 16) + .storeUint(governanceChainId, 16) + .storeBuffer(Buffer.from(governanceContract, "hex")) + .storeDict(Dictionary.empty()) // consumed_governance_actions + .storeRef(beginCell()) // governance_data_source, empty for initial state + .storeUint(0, 64) // last_executed_governance_sequence + .storeUint(0, 32) // governance_data_source_index + .endCell(); + + // Create the main cell with references to grouped data return beginCell() - .storeDict(priceDict) // latest_price_feeds - .storeUint(singleUpdateFee, 256) // single_update_fee - .storeUint(0, 32) - .storeDict(Dictionary.empty()) - .storeUint(0, 16) - .storeUint(0, 16) - .storeBuffer( - Buffer.from( - "0000000000000000000000000000000000000000000000000000000000000000", - "hex" - ) - ) - .storeDict(Dictionary.empty()) // consumed_governance_actions, + .storeRef(priceFeedsCell) + .storeRef(dataSourcesCell) + .storeRef(guardianSetCell) + .storeRef(governanceCell) .endCell(); } @@ -193,4 +254,65 @@ export class PythTest implements Contract { return result.stack.readNumber(); } + + async sendUpdatePriceFeeds( + provider: ContractProvider, + via: Sender, + updateData: Buffer, + updateFee: bigint + ) { + const messageBody = beginCell() + .storeUint(2, 32) // OP_UPDATE_PRICE_FEEDS + .storeRef(createCellChain(updateData)) + .endCell(); + + await provider.internal(via, { + value: updateFee, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + + async sendUpdateGuardianSet( + provider: ContractProvider, + via: Sender, + vm: Buffer + ) { + const messageBody = beginCell() + .storeUint(1, 32) // OP_UPDATE_GUARDIAN_SET + .storeRef(createCellChain(vm)) + .endCell(); + + await provider.internal(via, { + value: toNano("0.1"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + + async getChainId(provider: ContractProvider) { + const result = await provider.get("test_get_chain_id", []); + return result.stack.readNumber(); + } + + async getLastExecutedGovernanceSequence(provider: ContractProvider) { + const result = await provider.get( + "test_get_last_executed_governance_sequence", + [] + ); + return result.stack.readNumber(); + } + + async getGovernanceDataSourceIndex(provider: ContractProvider) { + const result = await provider.get( + "test_get_governance_data_source_index", + [] + ); + return result.stack.readNumber(); + } + + async getGovernanceDataSource(provider: ContractProvider) { + const result = await provider.get("test_get_governance_data_source", []); + return result.stack.readCell(); + } } diff --git a/target_chains/ton/contracts/wrappers/WormholeTest.ts b/target_chains/ton/contracts/wrappers/WormholeTest.ts index f5e6bf9fc3..8e161cc32d 100644 --- a/target_chains/ton/contracts/wrappers/WormholeTest.ts +++ b/target_chains/ton/contracts/wrappers/WormholeTest.ts @@ -8,9 +8,13 @@ import { Dictionary, Sender, SendMode, + toNano, } from "@ton/core"; import { createCellChain } from "../tests/utils"; -import { parseGuardianSetKeys } from "../tests/utils/wormhole"; +import { + createGuardianSetsDict, + parseGuardianSetKeys, +} from "../tests/utils/wormhole"; export type WormholeTestConfig = { guardianSetIndex: number; @@ -53,32 +57,42 @@ export class WormholeTest implements Contract { governanceChainId: number, governanceContract: string ): Cell { - const guardianSetDict = Dictionary.empty( - Dictionary.Keys.Uint(8), - Dictionary.Values.Buffer(20) - ); - guardianSet.forEach((key, index) => { - guardianSetDict.set(index, Buffer.from(key.slice(2), "hex")); - }); - const guardianSets = Dictionary.empty( - Dictionary.Keys.Uint(32), - Dictionary.Values.Cell() - ); - const guardianSetCell = beginCell() - .storeUint(0, 64) // expiration_time, set to 0 for testing - .storeDict(guardianSetDict) - .endCell(); - guardianSets.set(guardianSetIndex, guardianSetCell); - - return beginCell() + // Group price feeds and update fee (empty for Wormhole) + const priceFeedsCell = beginCell() .storeDict(Dictionary.empty()) // latest_price_feeds, empty for initial state .storeUint(0, 256) // single_update_fee, set to 0 for testing + .endCell(); + + // Group data sources information (empty for initial state) + const dataSourcesCell = beginCell() + .storeDict(Dictionary.empty()) // data_sources, empty for initial state + .storeUint(0, 32) // num_data_sources, set to 0 for initial state + .storeDict(Dictionary.empty()) // is_valid_data_source, empty for initial state + .endCell(); + + // Group guardian set information + const guardianSetCell = beginCell() .storeUint(guardianSetIndex, 32) - .storeDict(guardianSets) + .storeDict(createGuardianSetsDict(guardianSet, guardianSetIndex)) + .endCell(); + + // Group chain and governance information + const governanceCell = beginCell() .storeUint(chainId, 16) .storeUint(governanceChainId, 16) .storeBuffer(Buffer.from(governanceContract, "hex")) .storeDict(Dictionary.empty()) // consumed_governance_actions, empty for initial state + .storeRef(beginCell()) // governance_data_source, empty for initial state + .storeUint(0, 64) // last_executed_governance_sequence, set to 0 for initial state + .storeUint(0, 32) // governance_data_source_index, set to 0 for initial state + .endCell(); + + // Create the main cell with references to grouped data + return beginCell() + .storeRef(priceFeedsCell) + .storeRef(dataSourcesCell) + .storeRef(guardianSetCell) + .storeRef(governanceCell) .endCell(); } @@ -165,14 +179,6 @@ export class WormholeTest implements Contract { return result.stack.readNumber(); } - async getUpdateGuardianSet(provider: ContractProvider, vm: Buffer) { - const result = await provider.get("test_update_guardian_set", [ - { type: "slice", cell: createCellChain(vm) }, - ]); - - return result.stack.readNumber(); - } - async getGuardianSet(provider: ContractProvider, index: number) { const result = await provider.get("test_get_guardian_set", [ { type: "int", value: BigInt(index) }, @@ -188,4 +194,47 @@ export class WormholeTest implements Contract { keyCount, }; } + + async getGovernanceChainId(provider: ContractProvider) { + const result = await provider.get("test_get_governance_chain_id", []); + return result.stack.readNumber(); + } + + async getChainId(provider: ContractProvider) { + const result = await provider.get("test_get_chain_id", []); + return result.stack.readNumber(); + } + + async getGovernanceContract(provider: ContractProvider) { + const result = await provider.get("test_get_governance_contract", []); + const bigNumber = result.stack.readBigNumber(); + return bigNumber.toString(16).padStart(64, "0"); + } + + async getGovernanceActionIsConsumed( + provider: ContractProvider, + hash: bigint + ) { + const result = await provider.get("test_governance_action_is_consumed", [ + { type: "int", value: hash }, + ]); + return result.stack.readBoolean(); + } + + async sendUpdateGuardianSet( + provider: ContractProvider, + via: Sender, + vm: Buffer + ) { + const messageBody = beginCell() + .storeUint(1, 32) // OP_UPDATE_GUARDIAN_SET + .storeRef(createCellChain(vm)) + .endCell(); + + await provider.internal(via, { + value: toNano("0.1"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } }