freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Tact智能合约安全实践:TON生态系统中的常见错误
2024-12-19 19:10:40
所属地 海外

TON(The Open Network)以其创新特性和强大的智能合约性能,不断拓宽区块链技术的边界。基于早期的区块链平台(如以太坊等)的经验与教训,TON为开发者提供了一个更加高效且灵活的开发环境。其中推动这一进步的关键要素之一便是Tact编程语言。

Tact是专为TON链设计的一种全新编程语言,以高效与简洁为核心目标。它易于学习和使用,并与智能合约完美契合。Tact是一种静态类型语言,拥有简单的语法和强大的类型系统。

尽管如此,开发者在使用FunC时遇到的许多问题,在Tact开发中仍然存在。以下将结合审计实践案例,分析Tact开发中的一些常见错误。

数据结构

可选地址

Tact语言简化了声明、解码和编码的数据结构。然而,开发者仍需保持谨慎。我们来看一个例子:

这是根据TEP-74[1]标准用于转移jetton的内部传输(InternalTransfer)消息声明。请注意response_destination的声明,它是一个地址(Address)类型。在Tact中,要求地址必须是非零地址。然而,jetton标准的参考实现[2]允许零地址(addr_none),它由两个零位表示。这意味着用户或其他合约可能会尝试发送带有零响应地址的jetton,而该操作会意外失败。

此外,如果用户发送给其钱包的Transfer消息允许设置response_destination,而从发送方钱包到接收方钱包的InternalTransfer消息却不支持该参数,那么jetton将会“飞出”,意味着jetton无法到达目标地址,最终导致丢失。稍后,我们将讨论一种例外情况,即如何正确处理被退回的消息。息会被妥善处理。

在这种情况下,允许零地址的更好结构声明应为Address?,但在Tact中,将可选地址传递到下一条消息目前较为繁琐。

数据序列化

在Tact中,开发者可以指定字段的序列化方式。

本例中,totalAmount将序列化为coins,而releasedAmount将序列化为int257(默认为Int)。releasedAmount可以是负值,并且将占用257位。在大多数情况下,省略序列化类型不会带来问题;然而,如果数据涉及到通信,这就变得至关重要。

以下是我们审计的项目中的一个例子:

该数据结构是由NFT项目用作对链上get_static_data[3]请求的回复。根据标准,回复应该是:

上述索引是uint256(而不是int257),这意味着返回的数据将被调用者错误解读,从而导致不可预测的结果。很可能的结果是report_static_data处理程序会发生回滚,消息流也会因此中断。这些例子说明了为什么即使在使用Tact时,考虑数据序列化也是至关重要的。

有符号整数

不指定Int的序列化类型可能会导致比上述示例更严重的后果。与coins不同,int257可以为负值,这常常会让程序员感到意外。例如,在Tact的实时合约中,看到amount: Int是极其常见的。

这种写法本身并不一定意味着存在漏洞,因为该金额(amount)通常会被编码到JettonTransfer消息中,或传递到send(SendParameters{value: amount}),后者使用的是coins类型,不允许负值。然而,在一个案例中,我们发现一个拥有大量余额的合约,它允许用户将所有值设置为负数,包括奖励、手续费、金额、价格等。因此,恶意行为者可能利用这一漏洞进行攻击。

并发

以太坊链的开发者必须注意重入攻击,即在当前函数执行完成之前,能够再次调用同一个合约的函数。而在TON链上,重入攻击是不可能的。

由于TON是一个支持异步和并行智能合约调用的系统,追踪处理动作的顺序可能变得更加困难。任何内部消息都会被目标账户接收,交易结果会在交易本身之后处理,但并没有其他保证(有关消息传递的更多信息请参见相关文档[4])。

我们无法预测消息3或消息4哪个会先被送达。

在这种情况下,中间人攻击(Man-in-the-Middle Attack)[5]在消息流中是高发的攻击类型。为了确保安全,开发者应该设定每条消息的传递时间为1到100秒,在此期间,任何其他消息都有可能被传递。以下是一些可以提高安全性的其他注意事项:

1.不要检查或更新合约状态以供消息流的后续步骤使用。

2.使用携带值模式(carry-value pattern)[6]。不要直接发送有关值的信息,而是与消息一起发送。

以下是一个存在漏洞的真实例子:

在上述示例中,发生了以下步骤:

1.用户通过collection_jetton_wallet向NftCollection发送jetton。

2.TransferNotification被发送到NftCollection合约,合约记录了received_jetton_amount。

3.合约将jetton转发给NftCollection的所有者。

4.向NftCollection发送Excesses消息,作为response_destination。

5.NftItem在Excesses处理程序中部署,使用received_jetton_amount。

这里有几个问题需要注意:

首先,Excesses消息并不能保证按照jetton标准被送达。如果没有足够的gas费来发送Excesses消息,它将被跳过,消息流将停止。

其次,更新received_jetton_amount并在后续使用它会使系统容易受到并发执行的影响。其他用户可能会同时发送另一个金额并覆盖已保存的金额,这也可能会被恶意利用以从中获利。

在并发的情况下,TON与传统的中心化多线程系统相似。

处理退回消息

许多合约忽视了退回消息的处理。然而,Tact使这一过程变得简单明了:

要决定消息是否应以可退回模式发送,可以考虑两个因素:

1.如果消息失败,谁应该收到附加的Toncoin?如果目标应该接收这些资金,而不是发送合约,那么就以非可退回模式[7]发送消息。

2.如果下一个消息被拒绝,消息流会发生什么?如果通过处理退回的消息可以恢复一致的状态,那么最好进行处理。如果不能恢复,最好修改消息流。

以下是jetton标准[8]中的一个例子:

1.Excesses消息以非可退回模式发送,因为合约不需要返回toncoins。

2.以非可退回模式发送TransferNotification消息,因为forward_ton_amount属于调用者,合约不会保留它。

3.相反,BurnNotification是以可退回模式发送,因为如果它被jetton主合约退回,钱包需要恢复其余额,以保持total_supply一致。

4.InternalTransfer也是可退回的。如果接收方拒绝资金,发送方的钱包必须更新余额。

请记住以下几点:

1.退回消息仅接收256位[9]的原始消息;在消息识别之后,有效数据仅有224位。因此,你将得到有限的关于失败操作的信息,通常是存储为coins的某个金额。

2.如果没有足够的gas费,退回的消息将无法送达。

3.退回消息本身无法再次被退回。

返回Jetton

在某些情况下,撤销和处理退回消息不是一个选项。最常见的例子是当你的合约收到TransferNotification关于到达的jetton时,退回该消息可能会导致jetton永远被锁定。相反,你应该使用try-catch块[10]来处理。

让我们来看一个例子。在EVM中,当一笔交易被撤销时,所有结果都会被回滚(除了gas——它会被矿工收取)。但在TVM中,“交易”被分解为一系列消息,因此只回滚其中一条消息很可能会导致“合约组”状态不一致。

为了解决这个问题,必须手动检查所有条件,并在紧急情况下来回发送修正消息。然而,由于在没有异常的情况下解析有效载荷非常繁琐,因此最好使用try-catch块。

下面是一个典型的Jetton接收代码示例:

请注意,如果gas费不足,即使是将jettons发送回去也无法正常工作。此外,需要注意的是,我们是通过sender()的“钱包”返还jetton,而不是通过我们合约的实际jetton钱包返还。这是因为任何人都可以手动发送TransferNotification消息来欺骗我们。

管理Gas费

在审计TON合约时,最常见的问题之一就是gas费管理问题。主要原因有两个:

1. 缺乏gas费控制可能导致以下问题:

消息流执行不完整:部分操作会生效,而另一部分由于gas不足而被回滚。例如,如果奖励获取操作在jetton钱包中完成,但销毁份额操作在jetton主合约中被忽略,那么整个合约组将变得不一致。

用户可以提取自己的合约余额:此外合约中可能会积累过多的Toncoin。

2. TON合约开发者难以管理和控制gas:Tact的开发者需要通过测试来获得gas消耗量,并在开发过程中每次更新消息流时都更新相应的数值。

我们建议的做法如下:

1.确定“入口点”:这些是所有可以接受来自“外部”消息的消息处理器,即来自终端用户或其他合约(如Jetton钱包)。

2.对于每个入口点,绘制所有可能的路径并计算gas消耗。使用printTransactionFees()(可在@ton/sandbox中找到,该工具随Blueprint[11]一起提供)。

3.如果可以在消息流过程中部署合约,则假设它将被部署。部署将消耗更多的gas费和存储费用。

4.在每个入口点,根据情况添加最低的gas要求。

5.如果处理器不发送更多消息(消息流在此终止),那么最好返回Excesses,如下所示:

不发送Excesses也是可以的,但对于像Jetton Master这样的高吞吐量合约,存在大量BurnNotification消息或大量传入转账的合约,累计金额可能会迅速增长。

6.如果处理器只发送一条消息——包括emit(),实际上是一个外部消息——最简单的方式是通过forward()传递剩余的gas费(见上文)。

7.如果处理器发送多条消息,或者如果通讯中涉及ton数量,那么计算应发送金额比计算应剩余金额要更容易。

在下一个例子中,假设合约希望将forwardAmount发送给两个子合约作为押金:

正如你所看到的,gas费管理需要高度关注,即使是在简单的情况下。请注意,如果你已经发送了消息,则不能在send()模式中使用SendRemainingValue标志,除非你故意想要从合约余额中支出资金。

结论

随着TON生态系统的发展,Tact智能合约的安全开发将变得越来越重要。虽然Tact提供了更高的效率和简洁性,但开发者必须保持警惕,避免常见的陷阱。通过了解常见错误并实施最佳实践,开发者可以充分开发Tact的潜力,创建强大而安全的智能合约。持续学习并遵循安全实践指南,将确保TON生态的创新能力得到安全有效地利用,从而为更安全、可信的区块链环境作出贡献。


[1] TEP-74: https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md#1-transfer

[2] 参考实现: https://github.com/ton-blockchain/token-contract/

[3] get_static_data: https://github.com/ton-blockchain/TEPs/blob/master/text/0062-nft-standard.md#2-get_static_data

[4] 相关文档: https://docs.ton.org/develop/smart-contracts/guidelines/message-delivery-guarantees#message-delivery

[5] 中间人攻击: https://docs.ton.org/develop/smart-contracts/security/secure-programming#3-expect-a-man-in-the-middle-of-the-message-flow

[6] 携带值模式: https://docs.ton.org/develop/smart-contracts/security/secure-programming#4-use-a-carry-value-pattern

[7] 非可退回模式: https://docs.ton.org/develop/smart-contracts/guidelines/non-bouncable-messages

[8] jetton标准: https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md#1-transfer

[9] 仅接收256位: https://docs.tact-lang.org/book/bounced/#caveats

[10] try-catch块: https://docs.tact-lang.org/book/statements#try-catch

[11] Blueprint: https://github.com/ton-org/blueprint?tab=readme-ov-file#overview

# 安全实践
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录