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

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

回答

收藏

8.2 携带值模式

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

在为以太坊虚拟机开发合约时,为了方便起见,通常会将项目拆分成几个合约。在某些情况下,可以在一个合约中实现所有功能,即使在有必要拆分合约的情况下(例如,自动做市商中的流动性对),也不会导致任何特殊困难。
  • 在 EVM 中,交易被完整地执行:要么一切顺利,要么一切回滚。

      m1 r* X0 s. [+ [6 F* g7 m6 I  E9 X, `
在 TON 中,强烈建议避免 "无限制的数据结构",并将单个逻辑合约分割成小块,每块管理少量数据。基本的例子就是 TON Jettons 的实现(这是以太坊 ERC-20 代币标准的 TON 版本)。
  • 在 TON 中,项目被分成几个小块。
    $ j$ ]* L4 T  b
合约分片
  • Jetton结构:
    • jetton-minter 储存 total_supply, minter_address, 元数据, jetton_wallet_code
    • 很多 jetton-wallet 储存所有者地址、余额、Jetton矿工地址, jetton_wallet_code

      + m& D# b3 V5 {) g

    ( c$ Z. G+ K( A
简而言之,我们有
一个 jetton-minter 用来存储总供应量(total_supply)、矿工地址(minter_address)和一些参考信息:通证描述(元数据)和 jetton_wallet_code。还有大量的 jetton 钱包,每个拥有者都有一个。每个钱包只存储所有者的地址、余额、jetton-minter 地址和 jetton_wallet_code 的链接。这样做的目的是在钱包之间直接传输Jetton币,而不会影响任何高负载地址,这对并行处理交易至关重要。
也就是说,准备好让你的合约变成一个 "合约群",它们将积极地相互影响。
由此得出什么结论呢?
可以部分执行交易
您的合约逻辑中出现了一个新的独特属性:部分执行交易。
例如,考虑一下标准 TON Jetton 的信息流:
下面是 TON Jetton 信息流。从图中可以看出
  • 发送方向其钱包 (sender_wallet) 发送 op::transfer 消息
  • 发送方钱包减少代币余额
  • 发送方钱包向接收方钱包(destination_wallet)发送 op::internal_transfer 消息
  • 目的地钱包增加其代币余额
  • destination_wallet 向其所有者(目的地)发送 op::transfer_notification 消息
  • destination_wallet 向响应目的地(通常是发送方)发送 op::excesses 消息,返回多余的GAS。
      O; z8 N  f. v. G
请注意,如果 destination_wallet 无法处理 op::internal_transfer 消息(出现异常或GAS耗尽),则不会执行本部分和后续步骤。
但第一步(减少发送者钱包中的余额)将会完成。结果是部分执行了交易,Jetton 的状态不一致,在这种情况下还会造成资金损失。
在最坏的情况下,所有代币都可能以这种方式被盗。试想一下,你先给用户累积奖金,然后向他们的 Jetton 钱包发送 op::burn 消息,但你不能保证 op::burn 会被成功处理。
始终绘制信息流程图
即使是在像 TON Jetton 这样的简单合约中,也已经有相当多的消息、发送方、接收方以及消息中包含的数据块。现在想象一下,当你在开发一些更复杂的东西时,比如去中心化交易所(DEX),一个工作流中的消息数量可能会超过十条,你会怎么想?
在 CertiK,我们在审核过程中使用 DOT 语言描述和更新此类图表。我们也建议开发人员这样做。
在以太坊智能合约中,外部调用必须在继续执行之前执行,而在 TON 中,外部调用是一条消息,将在一段时间后根据一些新条件进行交付和处理。这就需要开发者多加注意。

: @7 S8 k2 T! W3 Y1 ~避免失败并捕捉退回的消息
  • 确定 "合约组 "的 "入口"。
  • 检查有效载荷、GAS供应和信息来源,以最大限度地降低故障风险。
  • 在其他信息处理程序("结果")中检查信息来源。其他检查都是 "审计"。
  • 不能在 "结果 "中失败。如果可能失败--检查信息流程

    4 G7 n6 _3 E7 k) X6 m( P
使用信息流,首先定义入口。这是在您的合约组("后果")中启动一连串信息的信息。在这里需要检查所有内容(有效载荷、GAS供应等),以尽量减少后续阶段出现故障的可能性。
如果您不能确定是否能完成所有计划(例如,用户是否有足够的代币来完成交易),这意味着信息流的构建可能有误。
在后续的消息(结果)中,所有 throw_if()/throw_unless() 都将扮演审计的角色,而不是实际检查什么。
处理退回的信息
许多合约也会处理被退回的信息,以防万一。
例如,在 TON Jetton 中,如果收件人的钱包无法接受任何代币(这取决于接收逻辑),那么发件人的钱包就会处理退回的信息,并将代币返还到自己的余额中。
  1. () on_bounce (slice in_msg_body) impure {
    % C! D% C9 T1 D- _
  2.     in_msg_body~skip_bits(32);  ;;0xFFFFFFFF
      k3 p* P$ C+ R
  3. . g5 r" G5 A; m8 M7 D+ T% w7 K
  4.     (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();6 e+ j7 b/ ^: R* S" D

  5. - a: K1 Y( D$ [0 U' ?, F2 Z
  6.     int op = in_msg_body~load_op();# {, G9 D8 j! {! P5 ~: b
  7. 4 x; \8 L6 \8 D* n
  8.     throw_unless(error::unknown_op, (op == op::internal_transfer) | (op == op::burn_notification));
    ; a  S& O: C# Z% C& t6 k; d  M( P
  9. # W1 w% L9 W) r, \
  10.     int query_id = in_msg_body~load_query_id();6 L5 ~& L# V, C& k* v+ G
  11.     int jetton_amount = in_msg_body~load_coins();) d* J2 H' I# l  d- j

  12. $ {% o3 U* M. K/ H* T  {
  13.     balance += jetton_amount;$ b' C' ]  z8 }4 F7 A
  14.     save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
    : r* @1 |* }9 s, q3 v) d# V
  15. }
复制代码
一般来说,我们建议对退回的信息进行处理,但这并不能完全防止信息处理失败和执行不完整。
首先,发送被退回的信息并对其进行处理需要消耗GAS,如果发件人没有提供足够的GAS,那么就没有被退回的信息。
无法 "重新反弹 "信息
8 v9 X$ {' t; A: E" Z) }3 j8 ~) b: b" [- K% s, z7 x
其次,TON 不提供跳转链。这意味着被退回的信息无法重新退回。例如,如果目的地无法处理 op::transfer_notification,那么发送方钱包就无法通过被退回的信息发现这一点。
信息流中的中间人
  • 信息级联可在多个区块中处理。
  • 假设当一个信息流正在运行时,攻击者可以并行启动第二个信息流。

    2 t' J/ A- L' H* g" O
也就是说,如果在一开始就检查了某个属性(例如用户是否有足够的代币),就不要假设在同一合约的第三阶段它们仍然满足该属性。
使用携带值模式
我们得出了最重要的建议:使用携带值模式。
TON Jetton》就证明了这一点:
  • 发送方钱包减去余额后,将余额连同 op::internal_transfer 消息一起发送给目的地钱包、
    ! X- t; b  [3 G5 r* f, [
反过来
  • 接收到信息中的余额,并将其添加到自己的余额中(或弹回)。
  • 转移的是数值,而不是信息。
    . Q; C: C$ f) `
无法在链上获得Jetton余额
您可能已经注意到,Jetton 实施不允许您在链上查询 Jetton 余额。这是因为这样的问题不符合模式。当对 op::get_balance 消息的响应到达请求者时,余额可能已经被别人花掉了。
不过,让我们另辟蹊径:
  • 主账户向钱包发送信息 op::provide_balance
  • 钱包将其余额清零并发送回 op::take_balance
  • 主账户收到钱,判断是否足够,要么使用(借钱还钱),要么发送回钱包

    6 U# q4 `1 K. }2 f
所以,这有点像获取余额。
返回值代替拒绝
从携带值模式(Carry-Value Pattern)可以看出,你的合约组通常收到的不仅仅是一个请求,而是一个请求和一个值。因此,我们不能拒绝执行请求(通过 throw_unless()),而必须将 Jettons 发送回发送者。
例如,一个典型的流程启动:
  • 发送方通过 sender_wallet 向 your_contract_wallet 发送 op::transfer 消息,指定合约的 forward_ton_amount 和 forward_payload。
  • sender_wallet 向 your_contract_wallet 发送 op::internal_transfer 消息。
  • your_contract_wallet 向 your_contract 发送 op::transfer_notification 消息,同时发送 forward_ton_amount、forward_payload、sender_dress 和 jetton_amount。
  • 在你的合约中的 handle_transfer_notification()中,流程开始了。

    ' c1 ]5 H; e$ X# L3 Y$ N
在这里,您需要弄清楚这是一个什么样的请求,是否有足够的GAS来完成它,以及有效载荷中的所有内容是否正确。
在这一阶段,您不应该使用 throw(),因为这样Jetton就会丢失,请求也不会被执行。值得使用 try-catch 语句。如果出现不符合合约预期的情况,则必须退还 Jettons。
将 jettons 返回发件人地址
但要注意、
  • 将返回的 op::transfer 发送到 sender_address,而不是任何真正的 Jetton 钱包。
  • 你不知道你收到的 op::transfer_notification 是来自真正的钱包还是有人在开玩笑。
  • 如果您的合约收到了意外或未知的 Jetton,也必须将其退回。
    ' _( f7 e+ d. X; N7 P8 ]
摘要
  • TON 需要开发人员付出更多的设计努力,以避免 "无限制数据结构 "并允许 "无限分片模式"。
  • 即使设计正确并使用了 "携带值模式",TON 开发人员也必须考虑到 TON 信息的异步性质。
  • 接受 Jettons 需要手动仔细分析有效载荷并检查条件。
  • 由于信息可能是伪造的,因此应通过发送方地址返回值。

    + j9 I# B4 ~, I" J! H/ q) Z' f; {
. r  j$ I, Z( {# {+ q
: w& o% E+ H4 w4 ]+ F& U
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则