Skip to content
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

feat(target_chains/ton): wormhole contract #1814

Merged
merged 25 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 22 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
1,043 changes: 945 additions & 98 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ packages:
- "target_chains/sui/cli"
- "target_chains/solana/sdk/js/solana_utils"
- "target_chains/solana/sdk/js/pyth_solana_receiver"
- "target_chains/ton/contracts"
- "contract_manager"
27 changes: 27 additions & 0 deletions target_chains/ton/contracts/contracts/Pyth.fc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#include "imports/stdlib.fc";
#include "Wormhole.fc";

;; Opcodes
const int OP_UPDATE_GUARDIAN_SET = 1;
const int OP_EXECUTE_GOVERNANCE_ACTION = 2;

;; Internal message handler
() 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);
;; * A 64-bit (big-endian) unsigned integer `query_id`, used in all query-response internal messages to indicate that a response is related to a query (the `query_id` of a response must be equal to the `query_id` of the corresponding query). If `op` is not a query-response method (e.g., it invokes a method that is not expected to send an answer), then `query_id` may be omitted.
int query_id = in_msg_body~load_uint(64);
cctdaniel marked this conversation as resolved.
Show resolved Hide resolved

;; * The remainder of the message body is specific for each supported value of `op`.
if (op == OP_UPDATE_GUARDIAN_SET) {
update_guardian_set(in_msg_body);
} elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
execute_governance_action(in_msg_body);
} else {
throw(0xffff); ;; Throw exception for unknown op
}
}
280 changes: 280 additions & 0 deletions target_chains/ton/contracts/contracts/Wormhole.fc
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
#include "imports/stdlib.fc";
#include "imports/errors.fc";
#include "imports/utils.fc";
#include "imports/storage.fc";

;; Signature verification function
;; ECRECOVER: Recovers the signer's address from the signature
;; It returns 1 value (0) on failure and 4 values on success
;; NULLSWAPIFNOT and NULLSWAPIFNOT2: Ensure consistent return of 4 values
;; These opcodes swap nulls onto the stack if ECRECOVER fails, maintaining the 4-value return
(int, int, int, int) check_sig (int hash, int v, int r, int s) asm
"ECRECOVER" ;; Attempt to recover the signer's address
"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 {
int key = -1;
do {
(key, slice value, int found) = keys.udict_get_next?(32, key);
cctdaniel marked this conversation as resolved.
Show resolved Hide resolved
if (found) {
~dump(key);
~dump(value);
}
} until (~ found);
}


;; Internal helper methods
(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;
do {
(key, slice address, int found) = keys_dict.udict_get_next?(8, key);
if (found) {
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);
}

;; store_data stores data in the contract
() store_data() impure inline_ref {
begin_cell()
.store_uint(current_guardian_set_index, 32)
.store_dict(guardian_sets)
.store_uint(chain_id, 16)
.store_uint(governance_chain_id, 16)
.store_uint(governance_contract, 256)
.store_dict(consumed_governance_actions)
.end_cell()
.set_data();
}

;; load_data populates storage variables using stored data
() load_data() impure inline_ref {
var ds = get_data().begin_parse();
current_guardian_set_index = ds~load_uint(32);
guardian_sets = ds~load_dict();
(int expiration_time, cell keys, int key_count) = get_guardian_set_internal(current_guardian_set_index);
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();
ds.end_parse();
}


;; Get methods
int get_current_guardian_set_index() method_id {
return current_guardian_set_index;
}

(int, cell, int) get_guardian_set(int 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 {
return governance_chain_id;
}

int get_governance_contract() method_id {
return governance_contract;
}

int governance_action_is_consumed(int hash) method_id {
(_, 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;
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 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);
(slice guardian_key, int found?) = guardian_set_keys.udict_get?(8, guardian_index);
int guardian_address = guardian_key~load_uint(160);
throw_unless(ERROR_INVALID_GUARDIAN_ADDRESS, parsed_address == guardian_address);
valid_signatures += 1;
i += 1;
}

;; Check quorum (2/3 + 1)
;; We're using a fixed point number transformation with 1 decimal to deal with rounding.
throw_unless(ERROR_NO_QUORUM, valid_signatures >= (((guardian_set_size * 10) / 3) * 2) / 10 + 1);
}

(int, int, int, int, int, int, int, int, slice, int) parse_and_verify_wormhole_vm(slice in_msg_body) impure {
;; Parse VM fields
int version = in_msg_body~load_uint(8);
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));
throw_unless(ERROR_INVALID_GUARDIAN_SET,
(current_guardian_set_index == vm_guardian_set_index) &
((expiration_time == 0) | (expiration_time > now()))
);
int signers_length = in_msg_body~load_uint(8);
;; Calculate signatures_size in bits (66 bytes per signature: 1 (guardianIndex) + 32 (r) + 32 (s) + 1 (v))
int signatures_size = signers_length * 66 * 8;

;; Load signatures
(cell signatures, slice remaining_body) = read_and_store_large_data(in_msg_body, signatures_size);
in_msg_body = remaining_body;

;; Calculate total body length across all references
int body_length = 0;
int continue? = -1; ;; -1 is true
do {
body_length += remaining_body.slice_bits();
if (remaining_body.slice_refs_empty?()) {
continue? = 0;
} else {
remaining_body = remaining_body~load_ref().begin_parse();
}
} until (~ continue?);

;; Load body
(cell body_cell, _) = read_and_store_large_data(in_msg_body, body_length);

int hash = hash_vm_body(body_cell.begin_parse());
;; Verify signatures
verify_signatures(hash, signatures.begin_parse(), signers_length, keys, key_count);

slice body_slice = body_cell.begin_parse();
int timestamp = body_slice~load_uint(32);
int nonce = body_slice~load_uint(32);
int emitter_chain_id = body_slice~load_uint(16);
int emitter_address = body_slice~load_uint(256);
int sequence = body_slice~load_uint(64);
int consistency_level = body_slice~load_uint(8);
slice payload = body_slice;

return (
version,
vm_guardian_set_index,
timestamp,
nonce,
emitter_chain_id,
emitter_address,
sequence,
consistency_level,
payload,
hash
);
}

(int, int, int, cell, int) parse_encoded_upgrade(int current_guardian_set_index, slice payload) impure {
int module = payload~load_uint(256);
throw_unless(ERROR_INVALID_MODULE, module == UPGRADE_MODULE);

int action = payload~load_uint(8);
throw_unless(ERROR_INVALID_GOVERNANCE_ACTION, action == 2);

int chain = payload~load_uint(16);
int new_guardian_set_index = payload~load_uint(32);
throw_unless(ERROR_NEW_GUARDIAN_SET_INDEX_IS_INVALID, new_guardian_set_index > current_guardian_set_index);
cctdaniel marked this conversation as resolved.
Show resolved Hide resolved

int guardian_length = payload~load_uint(8);
cell new_guardian_set_keys = new_dict();
int key_count = 0;
while (key_count < guardian_length) {
builder key = begin_cell();
int key_bits_loaded = 0;
while (key_bits_loaded < 160) {
int bits_to_load = min(payload.slice_bits(), 160 - key_bits_loaded);
key = key.store_slice(payload~load_bits(bits_to_load));
key_bits_loaded += bits_to_load;
if (key_bits_loaded < 160) {
throw_unless(ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH, ~ payload.slice_refs_empty?());
payload = payload~load_ref().begin_parse();
}
}
slice key_slice = key.end_cell().begin_parse();
new_guardian_set_keys~udict_set(8, key_count, key_slice);
key_count += 1;
}
throw_unless(ERROR_GUARDIAN_SET_KEYS_LENGTH_NOT_EQUAL, key_count == guardian_length);
throw_unless(ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH, payload.slice_empty?());

return (action, chain, module, new_guardian_set_keys, new_guardian_set_index);
}

() update_guardian_set(slice in_msg_body) impure {
;; Verify governance VM
(int version, int vm_guardian_set_index, int timestamp, int nonce, int emitter_chain_id, int emitter_address, int sequence, int consistency_level, slice payload, int hash) = parse_and_verify_wormhole_vm(in_msg_body);

;; Verify the emitter chain and address
int governance_chain_id = get_governance_chain_id();
throw_unless(ERROR_INVALID_GOVERNANCE_CHAIN, emitter_chain_id == governance_chain_id);
int governance_contract_address = get_governance_contract();
throw_unless(ERROR_INVALID_GOVERNANCE_CONTRACT, emitter_address == governance_contract_address);

;; Check if the governance action has already been consumed
throw_if(ERROR_GOVERNANCE_ACTION_ALREADY_CONSUMED, governance_action_is_consumed(hash));

;; Parse the new guardian set from the payload
(int action, int chain, int module, cell new_guardian_set_keys, int new_guardian_set_index) = parse_encoded_upgrade(current_guardian_set_index, payload);

;; 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);
cell updated_guardian_set = begin_cell()
.store_uint(now() + GUARDIAN_SET_EXPIRY, 64) ;; expiration time
.store_dict(keys) ;; keys
.end_cell();
guardian_sets~udict_set(32, current_guardian_set_index, updated_guardian_set.begin_parse());
}

;; 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)
.end_cell();
guardian_sets~udict_set(32, new_guardian_set_index, new_guardian_set.begin_parse());

;; Update the current guardian set index
current_guardian_set_index = new_guardian_set_index;

;; Mark the governance action as consumed
consumed_governance_actions~udict_set(256, hash, begin_cell().store_int(true, 1).end_cell().begin_parse());
}

() execute_governance_action(slice in_msg_body) impure {
;; TODO: Implement
}
20 changes: 20 additions & 0 deletions target_chains/ton/contracts/contracts/imports/errors.fc
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these files moved in imports? i thought that was only external dependencies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I define imports as stuffs that need to be imported into the main contracts, we could also just keep it to stdlib if that's confusing

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
;; Error codes enum
const int ERROR_INVALID_GUARDIAN_SET = 1000;
const int ERROR_INVALID_VERSION = 1001;
const int ERROR_GUARDIAN_SET_NOT_FOUND = 1002;
const int ERROR_GUARDIAN_SET_EXPIRED = 1003;
const int ERROR_INVALID_SIGNATURES = 1004;
const int ERROR_INVALID_EMITTER_ADDRESS = 1005;
const int ERROR_GOVERNANCE_ACTION_ALREADY_CONSUMED = 1006;
const int ERROR_INVALID_GUARDIAN_SET_KEYS_LENGTH = 1007;
const int ERROR_INVALID_SIGNATURE_LENGTH = 1008;
const int ERROR_SIGNATURE_INDICES_NOT_ASCENDING = 1009;
const int ERROR_NO_QUORUM = 1010;
const int ERROR_INVALID_MODULE = 1011;
const int ERROR_INVALID_GOVERNANCE_ACTION = 1012;
const int ERROR_NEW_GUARDIAN_SET_INDEX_IS_INVALID = 1013;
const int ERROR_GUARDIAN_SET_KEYS_LENGTH_NOT_EQUAL = 1014;
const int ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH = 1015;
const int ERROR_INVALID_GOVERNANCE_CHAIN = 1016;
const int ERROR_INVALID_GOVERNANCE_CONTRACT = 1017;
const int ERROR_INVALID_GUARDIAN_ADDRESS = 1018;
2 changes: 1 addition & 1 deletion target_chains/ton/contracts/contracts/imports/stdlib.fc
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ slice begin_parse(cell c) asm "CTOS";
;;; Preloads the first reference from the slice.
cell preload_ref(slice s) asm "PLDREF";

{- Functions below are commented because are implemented on compilator level for optimisation -}
{- Functions below are commented because are implemented on compilator level for optimisation -}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably pre-commit


;;; Loads a signed [len]-bit integer from a slice [s].
;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX";
Expand Down
11 changes: 11 additions & 0 deletions target_chains/ton/contracts/contracts/imports/storage.fc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
global int current_guardian_set_index;
;; GuardianSet struct: {expiration_time: int, keys: cell}
cctdaniel marked this conversation as resolved.
Show resolved Hide resolved
;; The 'keys' cell is a dictionary with the following structure:
;; - Key: 8-bit unsigned integer (guardian index)
;; - Value: 160-bit unsigned integer (guardian address)
global cell guardian_sets;
global int chain_id;
global int governance_chain_id;
;; GovernanceContract struct: {chain_id: int, address: slice}
global int governance_contract;
global cell consumed_governance_actions;
Loading
Loading