Skip to content

Commit e82158d

Browse files
authored
feat(target_chains/ton): wormhole contract (#1814)
* initialize ton contracts * precommit * add wormhole contract * precommit * temp * add unit test for parse_encoded_upgrade * update build script * Rename pyth.fc to Pyth.fc * update import * fix bug in getParseAndVerifyWormholeVm * fix ci * remove code for update_guardian_sets * update verify_signatures * add update_guardian_set (#1840) * address comments * address comments * address comments * address comments * address comments * address comments * update pnpm-lock.yaml * address comments * address comments * address comments * add invalid test cases
1 parent 43bf766 commit e82158d

19 files changed

+1978
-6026
lines changed

pnpm-lock.yaml

Lines changed: 945 additions & 98 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ packages:
2424
- "target_chains/sui/cli"
2525
- "target_chains/solana/sdk/js/solana_utils"
2626
- "target_chains/solana/sdk/js/pyth_solana_receiver"
27+
- "target_chains/ton/contracts"
2728
- "contract_manager"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#include "imports/stdlib.fc";
2+
#include "Wormhole.fc";
3+
4+
;; Opcodes
5+
const int OP_UPDATE_GUARDIAN_SET = 1;
6+
const int OP_EXECUTE_GOVERNANCE_ACTION = 2;
7+
8+
;; Internal message handler
9+
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
10+
if (in_msg_body.slice_empty?()) { ;; ignore empty messages
11+
return ();
12+
}
13+
14+
;; * A 32-bit (big-endian) unsigned integer `op`, identifying the `operation` to be performed, or the `method` of the smart contract to be invoked.
15+
int op = in_msg_body~load_uint(32);
16+
;; * 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.
17+
int query_id = in_msg_body~load_uint(64);
18+
19+
;; * The remainder of the message body is specific for each supported value of `op`.
20+
if (op == OP_UPDATE_GUARDIAN_SET) {
21+
update_guardian_set(in_msg_body);
22+
} elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
23+
execute_governance_action(in_msg_body);
24+
} else {
25+
throw(0xffff); ;; Throw exception for unknown op
26+
}
27+
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
#include "imports/stdlib.fc";
2+
#include "imports/errors.fc";
3+
#include "imports/utils.fc";
4+
#include "imports/storage.fc";
5+
6+
;; Signature verification function
7+
;; ECRECOVER: Recovers the signer's address from the signature
8+
;; It returns 1 value (0) on failure and 4 values on success
9+
;; NULLSWAPIFNOT and NULLSWAPIFNOT2: Ensure consistent return of 4 values
10+
;; These opcodes swap nulls onto the stack if ECRECOVER fails, maintaining the 4-value return
11+
(int, int, int, int) check_sig (int hash, int v, int r, int s) asm
12+
"ECRECOVER" ;; Attempt to recover the signer's address
13+
"NULLSWAPIFNOT" ;; If recovery failed, insert null under the top of the stack
14+
"NULLSWAPIFNOT2"; ;; If recovery failed, insert two more nulls under the top of the stack
15+
16+
;; Constants
17+
const int GUARDIAN_SET_EXPIRY = 86400; ;; 1 day in seconds
18+
const int UPGRADE_MODULE = 0x0000000000000000000000000000000000000000000000000000000000436f7265; ;; "Core" (left-padded to 256 bits) in hex
19+
20+
;; For troubleshooting purposes
21+
() dump_guardian_sets(cell keys) impure {
22+
int key = -1;
23+
do {
24+
(key, slice value, int found) = keys.udict_get_next?(32, key);
25+
if (found) {
26+
~dump(key);
27+
~dump(value);
28+
}
29+
} until (~ found);
30+
}
31+
32+
33+
;; Internal helper methods
34+
(int, cell, int) parse_guardian_set(slice guardian_set) {
35+
slice cs = guardian_set~load_ref().begin_parse();
36+
int expiration_time = cs~load_uint(64);
37+
;; slice keys = cs~load_ref().begin_parse();
38+
cell keys_dict = cs~load_dict();
39+
int key_count = 0;
40+
int key = -1;
41+
do {
42+
(key, slice address, int found) = keys_dict.udict_get_next?(8, key);
43+
if (found) {
44+
key_count += 1;
45+
}
46+
} until (~ found);
47+
48+
return (expiration_time, keys_dict, key_count);
49+
}
50+
51+
(int, cell, int) get_guardian_set_internal(int index) {
52+
(slice guardian_set, int found?) = guardian_sets.udict_get?(32, index);
53+
throw_unless(ERROR_GUARDIAN_SET_NOT_FOUND, found?);
54+
(int expiration_time, cell keys, int key_count) = parse_guardian_set(guardian_set);
55+
return (expiration_time, keys, key_count);
56+
}
57+
58+
;; store_data stores data in the contract
59+
() store_data() impure inline_ref {
60+
begin_cell()
61+
.store_uint(current_guardian_set_index, 32)
62+
.store_dict(guardian_sets)
63+
.store_uint(chain_id, 16)
64+
.store_uint(governance_chain_id, 16)
65+
.store_uint(governance_contract, 256)
66+
.store_dict(consumed_governance_actions)
67+
.end_cell()
68+
.set_data();
69+
}
70+
71+
;; load_data populates storage variables using stored data
72+
() load_data() impure inline_ref {
73+
var ds = get_data().begin_parse();
74+
current_guardian_set_index = ds~load_uint(32);
75+
guardian_sets = ds~load_dict();
76+
(int expiration_time, cell keys, int key_count) = get_guardian_set_internal(current_guardian_set_index);
77+
chain_id = ds~load_uint(16);
78+
governance_chain_id = ds~load_uint(16);
79+
governance_contract = ds~load_uint(256);
80+
consumed_governance_actions = ds~load_dict();
81+
ds.end_parse();
82+
}
83+
84+
85+
;; Get methods
86+
int get_current_guardian_set_index() method_id {
87+
return current_guardian_set_index;
88+
}
89+
90+
(int, cell, int) get_guardian_set(int index) method_id {
91+
return get_guardian_set_internal(index);
92+
}
93+
94+
int get_chain_id() method_id {
95+
return chain_id;
96+
}
97+
98+
int get_governance_chain_id() method_id {
99+
return governance_chain_id;
100+
}
101+
102+
int get_governance_contract() method_id {
103+
return governance_contract;
104+
}
105+
106+
int governance_action_is_consumed(int hash) method_id {
107+
(_, int found?) = consumed_governance_actions.udict_get?(256, hash);
108+
return found?;
109+
}
110+
111+
112+
() verify_signatures(int hash, slice signatures, int signers_length, cell guardian_set_keys, int guardian_set_size) impure {
113+
slice cs = signatures;
114+
int i = 0;
115+
int valid_signatures = 0;
116+
117+
while (i < signers_length) {
118+
int guardian_index = cs~load_uint(8);
119+
(_, int found?) = guardian_sets.udict_get?(32, guardian_index);
120+
throw_unless(ERROR_GUARDIAN_SET_NOT_FOUND, found?);
121+
int r = cs~load_uint(256);
122+
int s = cs~load_uint(256);
123+
int v = cs~load_uint(8);
124+
(_, int x1, int x2, int valid) = check_sig(hash, v >= 27 ? v - 27 : v, r, s);
125+
throw_unless(ERROR_INVALID_SIGNATURES, valid);
126+
int parsed_address = pubkey_to_eth_address(x1, x2);
127+
(slice guardian_key, int found?) = guardian_set_keys.udict_get?(8, guardian_index);
128+
int guardian_address = guardian_key~load_uint(160);
129+
throw_unless(ERROR_INVALID_GUARDIAN_ADDRESS, parsed_address == guardian_address);
130+
valid_signatures += 1;
131+
i += 1;
132+
}
133+
134+
;; Check quorum (2/3 + 1)
135+
;; We're using a fixed point number transformation with 1 decimal to deal with rounding.
136+
throw_unless(ERROR_NO_QUORUM, valid_signatures >= (((guardian_set_size * 10) / 3) * 2) / 10 + 1);
137+
}
138+
139+
(int, int, int, int, int, int, int, int, slice, int) parse_and_verify_wormhole_vm(slice in_msg_body) impure {
140+
;; Parse VM fields
141+
int version = in_msg_body~load_uint(8);
142+
throw_unless(ERROR_INVALID_VERSION, version == 1);
143+
int vm_guardian_set_index = in_msg_body~load_uint(32);
144+
;; Verify and check if guardian set is valid
145+
(int expiration_time, cell keys, int key_count) = get_guardian_set_internal(vm_guardian_set_index);
146+
throw_if(ERROR_INVALID_GUARDIAN_SET_KEYS_LENGTH, cell_null?(keys));
147+
throw_unless(ERROR_INVALID_GUARDIAN_SET,
148+
(current_guardian_set_index == vm_guardian_set_index) &
149+
((expiration_time == 0) | (expiration_time > now()))
150+
);
151+
int signers_length = in_msg_body~load_uint(8);
152+
;; Calculate signatures_size in bits (66 bytes per signature: 1 (guardianIndex) + 32 (r) + 32 (s) + 1 (v))
153+
int signatures_size = signers_length * 66 * 8;
154+
155+
;; Load signatures
156+
(cell signatures, slice remaining_body) = read_and_store_large_data(in_msg_body, signatures_size);
157+
in_msg_body = remaining_body;
158+
159+
;; Calculate total body length across all references
160+
int body_length = 0;
161+
int continue? = -1; ;; -1 is true
162+
do {
163+
body_length += remaining_body.slice_bits();
164+
if (remaining_body.slice_refs_empty?()) {
165+
continue? = 0;
166+
} else {
167+
remaining_body = remaining_body~load_ref().begin_parse();
168+
}
169+
} until (~ continue?);
170+
171+
;; Load body
172+
(cell body_cell, _) = read_and_store_large_data(in_msg_body, body_length);
173+
174+
int hash = hash_vm_body(body_cell.begin_parse());
175+
;; Verify signatures
176+
verify_signatures(hash, signatures.begin_parse(), signers_length, keys, key_count);
177+
178+
slice body_slice = body_cell.begin_parse();
179+
int timestamp = body_slice~load_uint(32);
180+
int nonce = body_slice~load_uint(32);
181+
int emitter_chain_id = body_slice~load_uint(16);
182+
int emitter_address = body_slice~load_uint(256);
183+
int sequence = body_slice~load_uint(64);
184+
int consistency_level = body_slice~load_uint(8);
185+
slice payload = body_slice;
186+
187+
return (
188+
version,
189+
vm_guardian_set_index,
190+
timestamp,
191+
nonce,
192+
emitter_chain_id,
193+
emitter_address,
194+
sequence,
195+
consistency_level,
196+
payload,
197+
hash
198+
);
199+
}
200+
201+
(int, int, int, cell, int) parse_encoded_upgrade(int current_guardian_set_index, slice payload) impure {
202+
int module = payload~load_uint(256);
203+
throw_unless(ERROR_INVALID_MODULE, module == UPGRADE_MODULE);
204+
205+
int action = payload~load_uint(8);
206+
throw_unless(ERROR_INVALID_GOVERNANCE_ACTION, action == 2);
207+
208+
int chain = payload~load_uint(16);
209+
int new_guardian_set_index = payload~load_uint(32);
210+
throw_unless(ERROR_NEW_GUARDIAN_SET_INDEX_IS_INVALID, new_guardian_set_index == (current_guardian_set_index + 1));
211+
212+
int guardian_length = payload~load_uint(8);
213+
cell new_guardian_set_keys = new_dict();
214+
int key_count = 0;
215+
while (key_count < guardian_length) {
216+
builder key = begin_cell();
217+
int key_bits_loaded = 0;
218+
while (key_bits_loaded < 160) {
219+
int bits_to_load = min(payload.slice_bits(), 160 - key_bits_loaded);
220+
key = key.store_slice(payload~load_bits(bits_to_load));
221+
key_bits_loaded += bits_to_load;
222+
if (key_bits_loaded < 160) {
223+
throw_unless(ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH, ~ payload.slice_refs_empty?());
224+
payload = payload~load_ref().begin_parse();
225+
}
226+
}
227+
slice key_slice = key.end_cell().begin_parse();
228+
new_guardian_set_keys~udict_set(8, key_count, key_slice);
229+
key_count += 1;
230+
}
231+
throw_unless(ERROR_GUARDIAN_SET_KEYS_LENGTH_NOT_EQUAL, key_count == guardian_length);
232+
throw_unless(ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH, payload.slice_empty?());
233+
234+
return (action, chain, module, new_guardian_set_keys, new_guardian_set_index);
235+
}
236+
237+
() update_guardian_set(slice in_msg_body) impure {
238+
;; Verify governance VM
239+
(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);
240+
241+
;; Verify the emitter chain and address
242+
int governance_chain_id = get_governance_chain_id();
243+
throw_unless(ERROR_INVALID_GOVERNANCE_CHAIN, emitter_chain_id == governance_chain_id);
244+
int governance_contract_address = get_governance_contract();
245+
throw_unless(ERROR_INVALID_GOVERNANCE_CONTRACT, emitter_address == governance_contract_address);
246+
247+
;; Check if the governance action has already been consumed
248+
throw_if(ERROR_GOVERNANCE_ACTION_ALREADY_CONSUMED, governance_action_is_consumed(hash));
249+
250+
;; Parse the new guardian set from the payload
251+
(int action, int chain, int module, cell new_guardian_set_keys, int new_guardian_set_index) = parse_encoded_upgrade(current_guardian_set_index, payload);
252+
253+
;; Set expiry if current GuardianSet exists
254+
(slice current_guardian_set, int found?) = guardian_sets.udict_get?(32, current_guardian_set_index);
255+
if (found?) {
256+
(int expiration_time, cell keys, int key_count) = parse_guardian_set(current_guardian_set);
257+
cell updated_guardian_set = begin_cell()
258+
.store_uint(now() + GUARDIAN_SET_EXPIRY, 64) ;; expiration time
259+
.store_dict(keys) ;; keys
260+
.end_cell();
261+
guardian_sets~udict_set(32, current_guardian_set_index, updated_guardian_set.begin_parse());
262+
}
263+
264+
;; Store the new guardian set
265+
cell new_guardian_set = begin_cell()
266+
.store_uint(0, 64) ;; expiration_time, set to 0 initially
267+
.store_ref(new_guardian_set_keys)
268+
.end_cell();
269+
guardian_sets~udict_set(32, new_guardian_set_index, new_guardian_set.begin_parse());
270+
271+
;; Update the current guardian set index
272+
current_guardian_set_index = new_guardian_set_index;
273+
274+
;; Mark the governance action as consumed
275+
consumed_governance_actions~udict_set(256, hash, begin_cell().store_int(true, 1).end_cell().begin_parse());
276+
}
277+
278+
() execute_governance_action(slice in_msg_body) impure {
279+
;; TODO: Implement
280+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
;; Error codes enum
2+
const int ERROR_INVALID_GUARDIAN_SET = 1000;
3+
const int ERROR_INVALID_VERSION = 1001;
4+
const int ERROR_GUARDIAN_SET_NOT_FOUND = 1002;
5+
const int ERROR_GUARDIAN_SET_EXPIRED = 1003;
6+
const int ERROR_INVALID_SIGNATURES = 1004;
7+
const int ERROR_INVALID_EMITTER_ADDRESS = 1005;
8+
const int ERROR_GOVERNANCE_ACTION_ALREADY_CONSUMED = 1006;
9+
const int ERROR_INVALID_GUARDIAN_SET_KEYS_LENGTH = 1007;
10+
const int ERROR_INVALID_SIGNATURE_LENGTH = 1008;
11+
const int ERROR_SIGNATURE_INDICES_NOT_ASCENDING = 1009;
12+
const int ERROR_NO_QUORUM = 1010;
13+
const int ERROR_INVALID_MODULE = 1011;
14+
const int ERROR_INVALID_GOVERNANCE_ACTION = 1012;
15+
const int ERROR_NEW_GUARDIAN_SET_INDEX_IS_INVALID = 1013;
16+
const int ERROR_GUARDIAN_SET_KEYS_LENGTH_NOT_EQUAL = 1014;
17+
const int ERROR_INVALID_GUARDIAN_SET_UPGRADE_LENGTH = 1015;
18+
const int ERROR_INVALID_GOVERNANCE_CHAIN = 1016;
19+
const int ERROR_INVALID_GOVERNANCE_CONTRACT = 1017;
20+
const int ERROR_INVALID_GUARDIAN_ADDRESS = 1018;

target_chains/ton/contracts/contracts/imports/stdlib.fc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ slice begin_parse(cell c) asm "CTOS";
290290
;;; Preloads the first reference from the slice.
291291
cell preload_ref(slice s) asm "PLDREF";
292292

293-
{- Functions below are commented because are implemented on compilator level for optimisation -}
293+
{- Functions below are commented because are implemented on compilator level for optimisation -}
294294

295295
;;; Loads a signed [len]-bit integer from a slice [s].
296296
;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX";
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
global int current_guardian_set_index;
2+
;; GuardianSet struct: {expiration_time: int, keys: cell}
3+
;; The 'keys' cell is a dictionary with the following structure:
4+
;; - Key: 8-bit unsigned integer (guardian index)
5+
;; - Value: 160-bit unsigned integer (guardian address)
6+
global cell guardian_sets;
7+
global int chain_id;
8+
global int governance_chain_id;
9+
;; GovernanceContract struct: {chain_id: int, address: slice}
10+
global int governance_contract;
11+
global cell consumed_governance_actions;

0 commit comments

Comments
 (0)