在为以太坊虚拟机开发合约时,为了方便起见,通常会将项目拆分成几个合约。在某些情况下,可以在一个合约中实现所有功能,即使在有必要拆分合约的情况下(例如,自动做市商中的流动性对),也不会导致任何特殊困难。 在 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 中,如果收件人的钱包无法接受任何代币(这取决于接收逻辑),那么发件人的钱包就会处理退回的信息,并将代币返还到自己的余额中。 - () on_bounce (slice in_msg_body) impure {
% C! D% C9 T1 D- _ - in_msg_body~skip_bits(32); ;;0xFFFFFFFF
k3 p* P$ C+ R - . g5 r" G5 A; m8 M7 D+ T% w7 K
- (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();6 e+ j7 b/ ^: R* S" D
- a: K1 Y( D$ [0 U' ?, F2 Z- int op = in_msg_body~load_op();# {, G9 D8 j! {! P5 ~: b
- 4 x; \8 L6 \8 D* n
- throw_unless(error::unknown_op, (op == op::internal_transfer) | (op == op::burn_notification));
; a S& O: C# Z% C& t6 k; d M( P - # W1 w% L9 W) r, \
- int query_id = in_msg_body~load_query_id();6 L5 ~& L# V, C& k* v+ G
- int jetton_amount = in_msg_body~load_coins();) d* J2 H' I# l d- j
$ {% o3 U* M. K/ H* T {- balance += jetton_amount;$ b' C' ] z8 }4 F7 A
- save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
: r* @1 |* }9 s, q3 v) d# V - }
复制代码一般来说,我们建议对退回的信息进行处理,但这并不能完全防止信息处理失败和执行不完整。 首先,发送被退回的信息并对其进行处理需要消耗GAS,如果发件人没有提供足够的GAS,那么就没有被退回的信息。 无法 "重新反弹 "信息
8 v9 X$ {' t; A: E" Z) }3 j8 ~ ) b: b" [- K% s, z7 x
其次,TON 不提供跳转链。这意味着被退回的信息无法重新退回。例如,如果目的地无法处理 op::transfer_notification,那么发送方钱包就无法通过被退回的信息发现这一点。 信息流中的中间人也就是说,如果在一开始就检查了某个属性(例如用户是否有足够的代币),就不要假设在同一合约的第三阶段它们仍然满足该属性。 使用携带值模式我们得出了最重要的建议:使用携带值模式。 TON Jetton》就证明了这一点: 反过来 无法在链上获得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
|