English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french

简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE คนไทย Français русский

回答

收藏

8.4 存储管理

开源社区 开源社区 7264 人阅读 | 0 人回复 | 2025-03-16

  • 在以太坊虚拟机合约中,您可以单独访问和修改每个存储元素。
  • 而在 TON 中,存储空间是通过 get_data()/set_data() 访问的(c4 存储器持有Cell袋的引用)。
  • 这就要求开发人员手动 "管理 "存储空间

    + R$ ]" n' ]1 R$ O5 c% v! C
6 P5 n/ W0 N. X  q% V& w  H
典型的信息处理程序
TON 中典型的信息处理程序就是采用这种方法:
  1. () handle_something(...) impure {, |! A: R" _% I3 u" a
  2.     (int total_supply, <a lot of vars>) = load_data();
    8 `$ `* j' m+ U" o! w- ?
  3. + c: s- a! S  S5 a
  4.     ... ;; do something, change data
    . _& ?  E* T4 c8 k$ n4 l
  5. 3 g5 B4 B) L- x  e  a
  6.     save_data(total_supply, <a lot of vars>);8 M9 u/ |* L- Y% s8 F3 b+ ~) U) M+ ]2 h
  7. }
复制代码
不幸的是,我们注意到了一种趋势: <a lot of vars> 是对所有合约数据字段的真正枚举。例如
  1. (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' _
  2. slice admin_address, slice router_address, slice jettonA_address, slice jettonA_wallet_address, , b' X( {3 z( j5 n- U( X
  3. int jettonA_balance, int jettonA_pending_balance, slice jettonB_address, slice jettonB_wallet_address,
    , J. x# m2 F/ G- k+ u
  4. int jettonB_balance, int jettonB_pending_balance, int mining_amount, int datetime_amount, int minable_time, 0 o' E$ `9 `9 u% e
  5. 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
  6. 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 审核中,我们注意到开发人员在某些地方混淆了两个参数,并写道:
  1. save_data(total_supply, min_amount, swap_fee, ...
    1 X. N. u* T2 q! x+ [
  2. instead of  t& i' |5 b4 W, w
  3.     save_data(total_supply, swap_fee, min_amount, ...
复制代码
在没有专家团队进行外部审计的情况下,很难发现这样的漏洞。出现错误的函数很少被使用,两个混淆的参数值通常都为零。要发现这样的错误,你必须知道自己在寻找什么。
4 s* @  Q5 Y$ t6 _问题 2:命名空间污染
  • 读取当前命名空间的所有存储字段会污染命名空间。
    6 b  h/ k8 }: d( M0 l
其次是 "命名空间污染"。让我们用审计中的另一个例子来解释问题所在。在函数的中间部分,输入参数为
  1. (int total_supply, int swap_fee, int min_amount, <a lot of vars>) = load_data();0 L: G, x6 i: J- m' M; O
  2. ...' y4 K- h& W$ K9 H0 X5 L  e3 F5 p
  3. int min_amount = in_msg_body~load_coins();3 J9 Y: o  I/ p; C
  4. ...0 f4 A& p8 n; ~% i
  5. save_data(total_supply, swap_fee, min_amount, <a lot of vars>);
复制代码
也就是说,局部变量对存储字段进行了阴影处理,在函数结束时,这个被替换的值被存储在存储空间中。攻击者有机会覆盖合约的状态。
  • FunC 允许重新声明变量。
    & ?% y3 T7 m; w8 @7 W
问题 3:GAS成本增加
最后
  • 解析整个存储空间并在每次调用每个函数时将其打包回去,会增加GAS成本。
    7 \! g! l! ^8 f1 D
解决方案 1: 使用全局变量
在原型设计阶段,由于合约中存储的内容并不完全明确,因此可以使用全局变量。
  1. global int var1;& m& d6 Y4 `2 t( v' H- h9 [
  2. global cell var2;6 o& X6 }5 I. ]' D
  3. global slice var3;3 l  V, `1 z; f

  4. 6 ^" p4 l/ \7 l* P4 R( W
  5. () load_data() impure {0 M; N2 Q1 u5 f' R9 |7 Z8 r" @
  6.     var cs = get_data().begin_parse();
    6 b9 O: i$ |5 R, d. d9 r
  7.     var1 = cs~load_coins();
    % v: u% b! F  ~0 \% f; T" q: r
  8.     var2 = cs~load_ref();
    # b" h$ C  G) T7 p6 G
  9.     var3 = cs~load_bits(512);
    ; n8 t, ^* q/ A/ g8 t% y& ~
  10. }
    , H# s0 J" W5 f2 y' Q
  11. 2 A5 a7 O; G# I6 l8 i
  12. () save_data() impure {) b. i% V& z% T0 n: ?/ e
  13.     set_data(7 |' u  I1 ^8 Y+ n. K3 O
  14.         begin_cell()
    - T: f. C4 C' O: f
  15.             .store_coins(var1)+ Q2 j& d* N' @8 }1 S
  16.             .store_ref(var2)
    : \/ K; O& {& g# \$ i$ }) @2 H
  17.             .store_bits(var3)( `% |7 o; P9 I5 `7 e
  18.             .end_cell()
    5 l4 R& z9 \# h5 u& V
  19.         );
    4 Y2 l+ n; L/ g+ C# L7 Z% H
  20. }
复制代码
这样,如果发现需要另一个变量,只需添加一个新的全局变量并修改 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:使用 "嵌套 "存储
原型设计完成后,我们建议采用这种存储组织方法:
  1. () handle_something(...) impure {5 w& A4 w, F5 ?  |8 T8 \
  2.     (slice swap_data, cell liquidity_data, cell mining_data, cell discovery_data) = load_data();% c- w, n  t1 H$ k- \$ z9 `
  3.     (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 }
  4.     …  d5 h/ g6 k3 h: a9 g
  5.     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
  6.     save_data(swap_data, liquidity_data, mining_data, discovery_data);
    1 J8 c- u' E6 ^# x
  7. }
复制代码
  • 如果变量经常使用(如 is_paused),则由 load_data() 立即提供。
  • 如果只在一种情况下需要使用参数组,就不要拆包。
    - ?; ~7 A( S0 \
存储空间由相关数据块组成。如果每个函数都使用一个参数,例如 is_paused,那么  load_data() 会立即提供该参数。如果一个参数组只在一种情况下需要使用,那么它就不需要解压缩,也不需要打包,更不会污染命名空间。
  • 如果添加了新变量,需要更新的代码片段就会减少。

    % _/ [: K8 K) x7 }
如果存储结构需要更改(通常是添加一个新字段),那么需要进行的编辑就会大大减少。
  • 嵌套变量可以再嵌套。

    6 B8 _* S3 v0 s2 R/ R
此外,这种方法还可以重复使用。如果我们的合约中有 30 个存储字段,那么一开始你可以得到四组,然后从第一组中得到几个变量和另一个子组。最主要的是不要做得太过分。
  • Cell最多可存储 1023 个比特和 4 个参考点。无论如何,你都会分片。
    , E" J7 L# T( |% d+ ^$ q$ X  K
请注意,由于一个Cell最多可存储 1023 位数据和 4 个引用,因此无论如何都必须将数据拆分到不同的Cell中。
分层数据是 TON 的主要功能之一,让我们将其用于预期目的。
使用 end_parse()
  • 从存储器和信息有效载荷读取数据时,尽可能使用 end_parse()。
    6 V3 e+ ~& i6 @' x
由于 TON 使用的是可变数据格式的位(bit)流,因此确保读取的数据量与写入的数据量相等是很有帮助的。这样可以节省一个小时的调试时间。
使用辅助函数,避免幻数
这段代码来自一个真实的项目。由于有大量幻数,即使是经验丰富的开发人员也会被吓到
  1. var msg = begin_cell()9 ]0 ?: p( p" ?7 y9 ^4 B* G
  2.     .store_uint(0xc4ff, 17)         ;; 0 11000100 0xff2 L8 b* |3 f8 }' E: w  g
  3.     .store_uint(config_addr, 256)& W0 z, Y5 ?$ e$ K' Y5 V
  4.     .store_grams(1 << 30)           ;; ~1 gram of value- M1 j% C. j, P* V
  5.     .store_uint(0, 107)
    2 N# A6 X  {; \6 }9 C$ t6 j
  6.     .store_uint(0x4e565354, 32)2 ?* E) ~) _; {& f7 E
  7.     .store_uint(query_id, 64)
    7 [9 k3 l! d' C$ |
  8.     .store_ref(vset);
    ( x/ q; @9 `) y- q/ i6 o
  9.    
    4 W4 [) Y/ ^$ z% }$ R
  10. send_raw_message(msg.end_cell(), 1
复制代码
根据需要引入尽可能多的常量和封装器,以实现代码的表现力

8 i, q) b8 F% U% g' p1 W
  1.     var msg = begin_cell()
      T+ U; J/ p+ U6 B" Q& ~
  2.         .store_msg_flags(BOUNCEABLE)
    ( t% n0 X) L+ V1 C, x
  3.         .store_slice(to_wallet_address)( x: Z  y% m& d" c# N( {  {
  4.         .store_coins(amount)
      r$ ]/ ?3 y6 M) c
  5.         .store_msgbody_prefix_stateinit()1 V/ A2 P7 [  T  n
  6.         .store_ref(state_init)
    , W" o' B6 Z- C; X! z  _
  7.         .store_ref(master_msg);  Z+ p% l' i  \) ~1 P% D' B

  8. ) P/ @. c# ]* \* V8 j
  9.     send_raw_message(msg.end_cell(), SEND_MODE_PAY_FEES_SEPARETELY);
复制代码
错误示例
不要忘记所有与 TON 无关的传统陷阱和潜在错误。下面是一个实际项目中的例子。
  1. () handle_transfer(...) impure {6 ^3 q, E# c5 [7 q9 h
  2.   ...( }' A' D& @. O! x. R% @9 q( O
  3.   (slice from_user_info, int from_flag) = user_info_dict.udict_get?(256, from_addr_hash);
    ! f% Z5 c/ f9 e  y1 ]* k$ a
  4.   int from_balance = from_user_info~load_coins();
    / E9 o, ^% l4 B0 a) R0 \
  5.   ...$ x- i3 f4 }; Q$ F
  6.   (slice to_user_info, int to_flag) = user_info_dict.udict_get?(256, to_addr_hash);) u$ X, O: J7 H5 L  A
  7.   int to_balance = to_user_info~load_coins();' P- z- N& j  P9 l
  8.   ...* M% {0 k. d, \+ P/ X
  9.   ;; save decreased from_balance to user_info_dict0 e! o9 ], x! I5 X
  10.   ;; save increased to_balance to user_info_dict
    ; t3 j$ @1 @, e+ Y* ], i
  11. }
复制代码
由于 to_balance 会覆盖已清零的 from_balance,因此向同一地址转账实际上会使余额翻倍。
8 l* R( M# p* c! k/ [# y
摘要
  • 使用 "嵌套 "存储组织和全局变量,保持存储操作的稳健性。
  • 编写封装程序并声明常量,以保持代码的表达能力。
  • 测试你的代码。
  • 进行彻底审核,避免损失。

    / ^; O/ {' B; R8 v4 M) a& @

  |+ 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
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则