6 P5 n/ W0 N. X q% V& w H
典型的信息处理程序TON 中典型的信息处理程序就是采用这种方法: - () handle_something(...) impure {, |! A: R" _% I3 u" a
- (int total_supply, <a lot of vars>) = load_data();
8 `$ `* j' m+ U" o! w- ? - + c: s- a! S S5 a
- ... ;; do something, change data
. _& ? E* T4 c8 k$ n4 l - 3 g5 B4 B) L- x e a
- save_data(total_supply, <a lot of vars>);8 M9 u/ |* L- Y% s8 F3 b+ ~) U) M+ ]2 h
- }
复制代码不幸的是,我们注意到了一种趋势: <a lot of vars> 是对所有合约数据字段的真正枚举。例如 - (int total_supply, int swap_fee, int min_amount, int is_stopped, int user_count, int max_user_count, 3 `6 ^, j, d" v2 z- e6 z' _
- slice admin_address, slice router_address, slice jettonA_address, slice jettonA_wallet_address, , b' X( {3 z( j5 n- U( X
- int jettonA_balance, int jettonA_pending_balance, slice jettonB_address, slice jettonB_wallet_address,
, J. x# m2 F/ G- k+ u - int jettonB_balance, int jettonB_pending_balance, int mining_amount, int datetime_amount, int minable_time, 0 o' E$ `9 `9 u% e
- int half_life, int last_index, int last_mined, cell mining_rate_cell, cell user_info_dict, cell operation_gas, ; w/ T5 w- |) E( ^! ]( M
- cell content, cell lp_wallet_code) = load_data();
复制代码这种方法有许多缺点。
7 s# x2 ?% b' _7 H) v* r, u6 n问题 1:难以更新存储结构增加一个字段就需要更新整个合约。 . } G) j/ N g* H
首先,如果您决定添加另一个字段,例如 is_paused,那么您就需要更新整个合约中的 load_data()/save_data() 语句。这不仅耗费大量人力,还会导致难以捕捉的错误。 在最近的一次 CertiK 审核中,我们注意到开发人员在某些地方混淆了两个参数,并写道: - save_data(total_supply, min_amount, swap_fee, ...
1 X. N. u* T2 q! x+ [ - instead of t& i' |5 b4 W, w
- save_data(total_supply, swap_fee, min_amount, ...
复制代码 在没有专家团队进行外部审计的情况下,很难发现这样的漏洞。出现错误的函数很少被使用,两个混淆的参数值通常都为零。要发现这样的错误,你必须知道自己在寻找什么。
4 s* @ Q5 Y$ t6 _问题 2:命名空间污染读取当前命名空间的所有存储字段会污染命名空间。 6 b h/ k8 }: d( M0 l
其次是 "命名空间污染"。让我们用审计中的另一个例子来解释问题所在。在函数的中间部分,输入参数为 - (int total_supply, int swap_fee, int min_amount, <a lot of vars>) = load_data();0 L: G, x6 i: J- m' M; O
- ...' y4 K- h& W$ K9 H0 X5 L e3 F5 p
- int min_amount = in_msg_body~load_coins();3 J9 Y: o I/ p; C
- ...0 f4 A& p8 n; ~% i
- save_data(total_supply, swap_fee, min_amount, <a lot of vars>);
复制代码也就是说,局部变量对存储字段进行了阴影处理,在函数结束时,这个被替换的值被存储在存储空间中。攻击者有机会覆盖合约的状态。 FunC 允许重新声明变量。 & ?% y3 T7 m; w8 @7 W
问题 3:GAS成本增加最后 解决方案 1: 使用全局变量在原型设计阶段,由于合约中存储的内容并不完全明确,因此可以使用全局变量。 - global int var1;& m& d6 Y4 `2 t( v' H- h9 [
- global cell var2;6 o& X6 }5 I. ]' D
- global slice var3;3 l V, `1 z; f
6 ^" p4 l/ \7 l* P4 R( W- () load_data() impure {0 M; N2 Q1 u5 f' R9 |7 Z8 r" @
- var cs = get_data().begin_parse();
6 b9 O: i$ |5 R, d. d9 r - var1 = cs~load_coins();
% v: u% b! F ~0 \% f; T" q: r - var2 = cs~load_ref();
# b" h$ C G) T7 p6 G - var3 = cs~load_bits(512);
; n8 t, ^* q/ A/ g8 t% y& ~ - }
, H# s0 J" W5 f2 y' Q - 2 A5 a7 O; G# I6 l8 i
- () save_data() impure {) b. i% V& z% T0 n: ?/ e
- set_data(7 |' u I1 ^8 Y+ n. K3 O
- begin_cell()
- T: f. C4 C' O: f - .store_coins(var1)+ Q2 j& d* N' @8 }1 S
- .store_ref(var2)
: \/ K; O& {& g# \$ i$ }) @2 H - .store_bits(var3)( `% |7 o; P9 I5 `7 e
- .end_cell()
5 l4 R& z9 \# h5 u& V - );
4 Y2 l+ n; L/ g+ C# L7 Z% H - }
复制代码这样,如果发现需要另一个变量,只需添加一个新的全局变量并修改 load_data() 和 save_data()。整个合约无需任何改动。但是 全局变量不能超过 31 个。 全局变量比在堆栈中存储更昂贵。 ) i( N" e4 z' u$ K. M3 V: t
* I1 N3 I% n P5 h7 q0 K u9 F" S
解决方案 2:使用 "嵌套 "存储原型设计完成后,我们建议采用这种存储组织方法: - () handle_something(...) impure {5 w& A4 w, F5 ? |8 T8 \
- (slice swap_data, cell liquidity_data, cell mining_data, cell discovery_data) = load_data();% c- w, n t1 H$ k- \$ z9 `
- (int total_supply, int swap_fee, int min_amount, int is_stopped) = swap_data.parse_swap_data();
6 p/ m; H% S; z* ?2 I8 } - … d5 h/ g6 k3 h: a9 g
- swap_data = pack_swap_data(total_supply + lp_amount, swap_fee, min_amount, is_stopped);
7 \0 M* H9 r! r9 S; M+ x) i* x - save_data(swap_data, liquidity_data, mining_data, discovery_data);
1 J8 c- u' E6 ^# x - }
复制代码存储空间由相关数据块组成。如果每个函数都使用一个参数,例如 is_paused,那么 load_data() 会立即提供该参数。如果一个参数组只在一种情况下需要使用,那么它就不需要解压缩,也不需要打包,更不会污染命名空间。 如果添加了新变量,需要更新的代码片段就会减少。
% _/ [: K8 K) x7 }
如果存储结构需要更改(通常是添加一个新字段),那么需要进行的编辑就会大大减少。 嵌套变量可以再嵌套。
6 B8 _* S3 v0 s2 R/ R
此外,这种方法还可以重复使用。如果我们的合约中有 30 个存储字段,那么一开始你可以得到四组,然后从第一组中得到几个变量和另一个子组。最主要的是不要做得太过分。 请注意,由于一个Cell最多可存储 1023 位数据和 4 个引用,因此无论如何都必须将数据拆分到不同的Cell中。 分层数据是 TON 的主要功能之一,让我们将其用于预期目的。 使用 end_parse()由于 TON 使用的是可变数据格式的位(bit)流,因此确保读取的数据量与写入的数据量相等是很有帮助的。这样可以节省一个小时的调试时间。 使用辅助函数,避免幻数这段代码来自一个真实的项目。由于有大量幻数,即使是经验丰富的开发人员也会被吓到 - var msg = begin_cell()9 ]0 ?: p( p" ?7 y9 ^4 B* G
- .store_uint(0xc4ff, 17) ;; 0 11000100 0xff2 L8 b* |3 f8 }' E: w g
- .store_uint(config_addr, 256)& W0 z, Y5 ?$ e$ K' Y5 V
- .store_grams(1 << 30) ;; ~1 gram of value- M1 j% C. j, P* V
- .store_uint(0, 107)
2 N# A6 X {; \6 }9 C$ t6 j - .store_uint(0x4e565354, 32)2 ?* E) ~) _; {& f7 E
- .store_uint(query_id, 64)
7 [9 k3 l! d' C$ | - .store_ref(vset);
( x/ q; @9 `) y- q/ i6 o -
4 W4 [) Y/ ^$ z% }$ R - send_raw_message(msg.end_cell(), 1
复制代码根据需要引入尽可能多的常量和封装器,以实现代码的表现力
8 i, q) b8 F% U% g' p1 W- var msg = begin_cell()
T+ U; J/ p+ U6 B" Q& ~ - .store_msg_flags(BOUNCEABLE)
( t% n0 X) L+ V1 C, x - .store_slice(to_wallet_address)( x: Z y% m& d" c# N( { {
- .store_coins(amount)
r$ ]/ ?3 y6 M) c - .store_msgbody_prefix_stateinit()1 V/ A2 P7 [ T n
- .store_ref(state_init)
, W" o' B6 Z- C; X! z _ - .store_ref(master_msg); Z+ p% l' i \) ~1 P% D' B
) P/ @. c# ]* \* V8 j- send_raw_message(msg.end_cell(), SEND_MODE_PAY_FEES_SEPARETELY);
复制代码 错误示例不要忘记所有与 TON 无关的传统陷阱和潜在错误。下面是一个实际项目中的例子。 - () handle_transfer(...) impure {6 ^3 q, E# c5 [7 q9 h
- ...( }' A' D& @. O! x. R% @9 q( O
- (slice from_user_info, int from_flag) = user_info_dict.udict_get?(256, from_addr_hash);
! f% Z5 c/ f9 e y1 ]* k$ a - int from_balance = from_user_info~load_coins();
/ E9 o, ^% l4 B0 a) R0 \ - ...$ x- i3 f4 }; Q$ F
- (slice to_user_info, int to_flag) = user_info_dict.udict_get?(256, to_addr_hash);) u$ X, O: J7 H5 L A
- int to_balance = to_user_info~load_coins();' P- z- N& j P9 l
- ...* M% {0 k. d, \+ P/ X
- ;; save decreased from_balance to user_info_dict0 e! o9 ], x! I5 X
- ;; save increased to_balance to user_info_dict
; t3 j$ @1 @, e+ Y* ], i - }
复制代码由于 to_balance 会覆盖已清零的 from_balance,因此向同一地址转账实际上会使余额翻倍。 8 l* R( M# p* c! k/ [# y
摘要
|+ I. R3 Z9 _7 ]# o& h9 s5 _7 D7 ?1 L% M2 A; N
. s0 U" O# h( ?4 Y, [* N7 ^6 _
& @1 g$ D' }( {9 F |