撰文:Beosin
在区块链技术快速发展的今天,TON (The Open Network) 作为一款高效且灵活的区块链平台,正受到越来越多开发者的关注。TON 的独特架构和特性为去中心化应用的开发提供了强大的工具和丰富的可能性。
然而,随着功能和复杂性的增加,智能合约的安全性也变得越来越重要。FunC 作为 TON 上的智能合约编程语言,以其灵活性和高效性著称,但同时也带来了许多潜在的风险和挑战。编写安全可靠的智能合约,需要开发者深刻理解 FunC 语言的特性以及可能存在的风险。
本文将详细分析在 TON 区块链上的一些与智能合约有关的特性,以及 TON 上智能合约容易被忽略的漏洞点。
Ton 异步特性与账户机制解析
智能合约异步调用
网络分片与异步通信
TON 区块链在设计上分为三种链:主链 (Masterchain),工作链 (Workingchains)和分片链 (Shardchains)。
主链是整个网络的核心,负责存储全网的元数据和共识机制。它记录所有工作链和分片链的状态,并确保全网的一致性和安全性。工作链是独立的区块链,最多有 2^32 条,负责处理特定类型的交易和智能合约。每个工作链可以有自己的规则和特性,以满足不同的应用需求。分片链是工作链的子链,用于进一步分割工作链的负载,提升处理能力和扩展性。每个工作链最多拆分为 2^60 个 shard chain,分片链独立处理部分交易,从而实现高效的并行处理。
理论上每一个账户都可以独占一个 shard chain,每一个账户独立维护自己的 COIN/TOKEN 余额,每一个账户间的交易都可以完全并行。账户与账户间通过异步消息进行传递,消息在 shard chain 间传递的路径为 log_16(N) - 1,其中 N 为 shard chain 的数量。
图源:https://frontierlabzh.medium.com/ton-web3 世界的 weixin-e1d3ae3b3574
在 Ton 中,智能合约通过发送和接收消息进行交互。这些消息可以是内部消息(一般来说是智能合约互相交互所发送的消息)或外部消息(由外部来源发送的消息)。消息的传递过程不需要等待目标合约的立即响应,发送方可以继续执行其余的逻辑代码。这种异步消息传递机制相较于以太坊的同步调用,提供了更高的灵活性和扩展性,减少了因等待响应导致的性能瓶颈,同时也带来了处理并发和竞争条件的挑战。
消息格式与结构
在 Ton 中,消息通常包含发件人、收件人、金额、消息体等信息。消息体可以是函数调用、数据传输或其他自定义内容。Ton 使用的消息格式可以灵活定义和扩展,使得不同合约之间能够高效传递各种类型的信息。
cell msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(message_body)
.end_cell();
消息队列与状态处理
每个合约都维护一个消息队列,存储尚未处理的消息。合约在执行过程中,会根据队列中的消息逐个处理。由于消息处理是异步的,合约的状态在收到消息之前不会立即更新。
异步消息传递的优势
高效的分片机制:Ton 的异步机制与其分片设计高度契合。每个分片独立处理合约的消息和状态变化,避免了跨分片同步通信带来的延迟问题。这种设计提升了整个网络的吞吐量和可扩展性。
降低资源消耗:由于异步消息不要求即时响应,Ton 的合约执行可以分散在多个区块内完成,避免了单个区块内资源的过度消耗。这使得 Ton 能够支持更为复杂和资源密集型的智能合约。
容错性与可靠性:异步消息的传递机制使得系统更具容错性。例如,如果某个合约由于资源限制或其他原因无法及时响应消息,发送方仍然可以继续处理其他逻辑,系统不会因为单个合约的延迟而停滞。
异步合约设计的挑战
状态一致性问题:由于消息传递是异步的,合约的状态在不同时刻可能会接收到不同的消息,这需要开发者特别注意状态一致性问题。在设计合约时,必须考虑到不同消息顺序可能带来的状态变化,确保系统在任何情况下都能保持一致性。
竞争条件与防护:异步消息处理带来了潜在的竞争条件问题,多个消息可能同时尝试修改合约状态。开发者需要引入适当的锁机制或使用事务性操作来防止状态冲突。
安全性考量:异步合约在处理跨合约通信时,容易受到中间人攻击或重放攻击。因此,在设计异步合约时,必须考虑到这些潜在的安全风险,并采取措施防止它们发生,如使用时间戳、随机数或多重签名等手段。
账本模型
Ton(The Open Network)在设计其区块链基础设施时,采用了一种独特的账户抽象和账本模型。这个模型的灵活性体现在它如何处理账户的状态、消息传递以及合约的执行。
账户抽象
Ton 的账户模型采用了一种基于合约的抽象,每个账户都可以视为一个合约,这与以太坊的账户抽象模型有一些相似之处,但更加灵活和通用。在 Ton 中,账户不仅仅是持有资产的容器,它们还包含了合约代码和状态数据。每个账户都由其代码(Code)、数据(Data)和消息处理(Message Handling)逻辑组成。
账户结构:每个 Ton 账户都有一个唯一的地址,该地址是由账户代码的哈希值、部署时的初始数据以及一些其他参数组合而成的。这意味着同样的代码和初始数据部署在不同的环境下(例如,不同的区块链或分片)可能会生成不同的地址。
灵活性:由于每个账户都可以运行自己的合约代码,因此 Ton 的账户可以实现非常复杂的逻辑。账户不仅仅是简单的余额持有者,还可以处理复杂的状态转移、跨账户的消息通信、甚至是基于特定条件的自动化操作。这使得 Ton 的账户模型比传统区块链上的账户模型更具扩展性和灵活性。
账本结构
Ton 的账本结构设计为高效处理大规模并发交易,支持异步消息传递和多分片操作。每个账户的状态保存在 Merkle 树结构中,这使得 Ton 的账本具有高效的状态验证能力。
状态存储
账户的状态信息被存储在持久化存储中,并通过 Merkle 树进行组织,以确保状态的完整性和安全性。这种设计还支持状态的高效查询和验证,尤其是在跨分片交易的场景中。
帐户或智能合约状态通常包含以下内容:
基础货币的余额
其他货币的余额
智能合约代码(或其哈希)
智能合约的持久化数据(或其 Merkle 哈希)
有关持久化存储单元数和使用的原始字节数的统计信息
智能合约持久存储的付款的最近时间(实际上是主链块号)
转移货币并从此帐户发送消息所需的公钥(可选; 默认情况下等于 account_id 本身)。在某些情况下,类似于比特币交易输出所做的,可以在此处找到更复杂的签名检查代码; 然后 account_id 将等于此代码的哈希值。
并非所有的信息都是每个帐户必须需要的。例如,智能合约代码仅适用于智能合约,但不适用于「简单」账户。此外,虽然任何账户必须具有主要货币的非零余额(例如,基本工作链的主链和分片链的 Gram),但其它货币的余额可能为零。为了避免保留未使用的数据,在工作链的创建期间定义了一个 sum-product 类型,它使用不同的标记字节来区分不同的「够造函数」。最终,帐户状态本身被保存为 TVM 持久化存储的单元集合。
消息传递与处理
Ton 的账本结构内置了对异步消息传递的支持,每个账户可以独立处理接收到的消息并更新其状态。这种异步消息机制允许账户之间进行复杂的交互,而不会因为某个操作的延迟而影响其他账户的正常运行。
Gas 模型
Ton(The Open Network)区块链通过其独特的 Gas 费模型大幅优化了智能合约的执行效率。Gas 费模型在区块链中用于衡量和限制智能合约执行过程中消耗的资源。与传统区块链(如以太坊)的 Gas 模型相比,Ton 的模型设计更为复杂且高效,能够更精确地管理合约执行过程中的资源消耗。
细化的 Gas 消耗测量
Ton 的 Gas 模型能够精确测量智能合约在执行过程中消耗的计算资源、存储操作以及消息传递成本。通过对计算、存储和消息传递等资源的细化测量,Ton 的 Gas 模型能够防止某些复杂度过高的操作占用过多的资源。通过限制 Gas 消耗,Ton 确保了网络的每个节点都能公平地分配计算资源,避免单一合约或操作对网络资源的过度消耗。
并行处理与 Gas 优化
Ton 支持智能合约的并行处理,这使得多个合约能够同时在不同的分片上运行,而不会相互阻塞。在这种设计下,其 Gas 模型与其并行执行和分片机制紧密结合,通过在多个分片上并行处理合约,Ton 可以将 Gas 的计算和支付分散到不同的节点和链上,避免了网络拥堵,同时最大化了资源利用率。
动态 Gas 调整机制
Ton 的 Gas 模型中包含了动态调整机制,允许根据网络的实时负载情况对 Gas 费进行调整。这意味着在网络负载较低时,用户可以以较低的 Gas 费执行合约,从而鼓励在低负载时段进行操作,平衡网络的资源使用。这种机制不仅提升了用户体验,还通过市场化的方式控制了资源的使用峰值。
Ton 智能合约易忽略漏洞
在我们上一篇 TON 的安全分析文章中已经详细介绍了 Ton 生态的常规安全漏洞,也可参考下表:
本文将重点介绍我们团队总结出的 TON 合约中容易被忽略的漏洞点:
(1) 代码可读性优化
在 TON 的智能合约中,会使用数字来存储消息发送的相关数据,例如下面的代码中,多次使用数字来表示对应的标识和数据存储长度,这样大大降低降低代码的可读性和可维护性。其他开发者在阅读这些代码时,很难理解这些数字的意义和用途。为了提高代码的可读性和可维护性,建议将关键的数字值定义为具名常量,例如:0x18 定义为 NON_BOUNCEABLE。
check_std_addr(address);var msg = begin_cell() store_uint(0x18, 6) ;; nobounce store_slice(address) store_coins(amount) store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) end_cell();send_raw_message(msg, 1);
另外,在合约判断条件中的错误提示信息,同样建议定义对应的变量替换错误码。
throw_unless(705, equal_slices(owner_address, sender_address));
(2) 使用 end_parse() 确保数据完整性
在 TON 合约中,数据解析遵循固定的顺序,从原始数据中逐步加载指定类型的数据。这种解析方式确保了数据的一致性和准确性。如下所示:
() load_data() impure {
slice ds = get_data().begin_parse();
storage::owner = ds~load_msg_addr();
storage::amount = ds~load_uint(256);
storage::data = ds~load_ref();
storage::api_data = ds~load_ref();
ds.end_parse();
}
注意这里的 end_parse() 用于检查数据切片(slice)是否为空,如果切片不为空,函数会抛出一个异常。这样可确保数据的格式和内容都是符合预期的。如果 end_parse() 函数发现数据切片中仍然有剩余的数据,这可能表明数据解析没有完全按照预期进行,或者数据的格式存在问题。因此,通过调用 end_parse(),可以检查是否解析过程中数据有遗漏或异常。
(3) 数据记载和存储类型不匹配引发的异常
这里主要需要说明的是 int 和 uint 的存取类型匹配,如下所示的代码中,数据存储时使用了 store_int() 来存储 int 类型的值为 -42,但是却使用了 load_uint() 来加载这个值,这里就可能出现异常。
() Test_Fuction() {
var cell = begin_cell();
cell = cell.store_int(-42, 32);
var my_cell = cell.end_cell();
slice s = my_cell.begin_parse();
var result = s.load_uint(32);
}
(4)inline_ref 和 inline 修饰符的合理使用
首先,需要阐述一下 inline_ref 和 inline 修饰符的区别:
lInline:使用 inline 修饰符的函数,其代码会在每次调用时被直接插入到调用位置。也就是说,每次调用函数时,函数的实际代码会被复制到调用的位置,而不是像普通函数那样通过跳转到函数体执行。
linline_ref:使用 inline_ref 修饰符的函数,其代码存储在一个独立的 cell 中。每次调用函数时,TVM 通过 CALLREF 命令来执行存储在 cell 中的代码,而不是在调用位置插入函数代码。
所以,inline 修饰符适用于简单函数,减少函数调用开销,但可能导致合约代码重复;而 inline_ref 修饰符适用于较复杂或被多次调用的函数,通过将函数代码存储在单独的 cell 中来提高效率,避免了代码重复。那么可以总结为:当函数较大或被多个地方调用时,建议使用 inline_ref;反之,则建议使用 inline。
(5) 确定正确的工作链
TON 允许创建多达 2^32 条工作链,每条工作链则可以细分为多达 2^60 个分片,前只有 2 个工作链:主链 (-1) 和基本链 (0)。合约中计算目标地址时,必须明确指定目标地址所属的链 ID,以确保生成的钱包地址位于正确的工作链上。为了避免生成错误地址,建议使用 force_chain() 强制指定链 ID。
(6) 避免错误码冲突
合约设计中,为了确保规范性和避免混淆,错误码的管理非常关键。对于 TON 智能合约,首先应确保每个错误码在合约中是唯一的,避免在同一个合约中定义重复的错误码,以防止错误码的混淆和信息的不明确;其次 TON 平台或底层系统已经定义了一些标准的错误码,应避免与这些系统错误码冲突,例如 333 错误码表示的链 ID 不匹配。所以建议合约的错误码最好在 400 到 1000 之间。
(7) 操作完成后需要存储数据和调用 return()
在 TON 智能合约中,消息处理会根据 op-code 选择不同的逻辑。完成对应业逻辑后,还需完成两项操作:首先,如果涉及数据更改,必须调用 save_data() 以确保数据被存储,否则更改将无效;其次,必须调用 return() 以表示该操作完成,否则将触发 throw(0xffff) 异常。
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
int flags = cs~load_uint(4);
if (flags & 1) {
;; ignore all bounced messages
return ();
}
slice sender_address = cs~load_msg_addr();
load_data();
int op = in_msg_body~load_op();
if ((op == op::op_1())) {
handle_op1();
save_data();
return ();
}
if ((op == op::op_2())) {
handle_op2();
save_data();
return ();
}
if ((op == op::op_3())) {
handle_op3();
save_data();
return ();
}
throw(0xffff);
}
综上所述,TON 区块链凭借其创新的架构和灵活的开发环境,正逐渐成为去中心化应用开发者的理想平台。然而,随着智能合约在 TON 生态系统中扮演越来越重要的角色,合约安全性问题也不容忽视。开发者应深入了解 TON 生态的特性,严格遵循最佳实践,强化安全审计流程,确保合约的稳健性与安全性。只有这样,才能充分发挥 TON 平台的优势,构建更加安全可靠的去中心化应用,为整个生态系统的健康发展保驾护航。
目前 TON 生态正在快速发展,吸引了大量的资金与活跃用户。然而,随之而来的安全问题也不容忽视。