揭开Ton智能合约的神秘面纱:关键特性与隐藏的安全陷阱

探索TON智能合约的架构与潜在威胁,确保区块链应用安全的关键指南

Posted by Yewbs on September 11, 2024

Title: A Burial at Ornans
Creator Lifespan: 1819 - 1877
Creator Nationality: French
Creator Gender: Male
Date Created: 1849 - 1850
Provenance: Gift of Miss Juliette Courbet, 1877
Physical Dimensions: w6680 x h3150 mm
Painter: Gustave Courbet
Original Title: Un enterrement à Ornans, dit aussi Tableau de figures humaines, historique d’un enterrement à Ornans
Type: Oil on canvas

前言

在区块链技术快速发展的今天,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 的数量。

ton 图源:https://frontierlabzh.medium.com/ton-web3世界的weixin-e1d3ae3b3574

在Ton中,智能合约通过发送和接收消息进行交互。这些消息可以是内部消息(一般来说是智能合约互相交互所发送的消息)或外部消息(由外部来源发送的消息)。消息的传递过程不需要等待目标合约的立即响应,发送方可以继续执行其余的逻辑代码。

这种异步消息传递机制相较于以太坊的同步调用,提供了更高的灵活性和扩展性,减少了因等待响应导致的性能瓶颈,同时也带来了处理并发和竞争条件的挑战。

消息格式与结构

在Ton中,消息通常包含发件人、收件人、金额、消息体等信息。消息体可以是函数调用、数据传输或其他自定义内容。

Ton使用的消息格式可以灵活定义和扩展,使得不同合约之间能够高效传递各种类型的信息。

1
2
3
4
5
6
7
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树进行组织,以确保状态的完整性和安全性。这种设计还支持状态的高效查询和验证,尤其是在跨分片交易的场景中。

帐户或智能合约状态通常包含以下内容:

  1. 基础货币的余额
  2. 其他货币的余额
  3. 智能合约代码(或其哈希)
  4. 智能合约的持久化数据(或其Merkle哈希)
  5. 有关持久化存储单元数和使用的原始字节数的统计信息
  6. 智能合约持久存储的付款的最近时间(实际上是主链块号)
  7. 转移货币并从此帐户发送消息所需的公钥(可选; 默认情况下等于 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 智能合约易忽略漏洞

在Beosin公众号上发表的TON的安全分析文章中已经详细介绍了Ton生态的常规安全漏洞

下表是我总结的Ton的常见漏洞:

漏洞分类 漏洞名称
基础漏洞 交易部分执行的风险
  交易顺序依赖
  时间戳依赖
  整数上溢/下溢
  舍入误差
  拒绝服务攻击
  访问控制漏洞
  中心化风险
  业务逻辑漏洞
  Gas计算错误
  存储管理漏洞
  未经检查的返回值
  并发消息调用和锁的安全性
  cell 数据解析错误
  消息失败的非正确处理
  消息反弹
业务风险 费用计算错误
  任意铸币风险
  余额计算错误
  错误的多签设置
  传递参数的顺序不正确
  参数可控
  消息格式的正确性未验证
  消息无法正常调用
  奖励或费用计算错误
  交易费用或奖励无法提取
  奖励不足
  全局变量未及时更新
  抢占式初始化
  返回数据错误

本文将重点介绍我们团队总结出的TON合约中容易被忽略的漏洞点

代码可读性优化

在TON的智能合约中,会使用数字来存储消息发送的相关数据,例如下面的代码中,多次使用数字来表示对应的标识和数据存储长度,这样大大降低降低代码的可读性和可维护性。其他开发者在阅读这些代码时,很难理解这些数字的意义和用途。为了提高代码的可读性和可维护性,建议将关键的数字值定义为具名常量,例如:0x18定义为NON_BOUNCEABLE。

1
2
3
4
5
6
7
8
check_std_addr(address);
var msg = begin_cell()
    store_uint(0x10, 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);

另外,在合约判断条件中的错误提示信息,同样建议定义对应的变量替换错误码。

1
throw_unless(705, equal_slices(owner_address, sender_address));

使用end_parse()确保数据完整性

在TON合约中,数据解析遵循固定的顺序,从原始数据中逐步加载指定类型的数据。这种解析方式确保了数据的一致性和准确性。如下所示:

1
2
3
4
5
6
7
8
() 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(),可以检查是否解析过程中数据有遗漏或异常。

数据记载和存储类型不匹配引发的异常

这里主要需要说明的是int和uint的存取类型匹配,如下所示的代码中,数据存储时使用了store_int()来存储int类型的值为-42,但是却使用了load_uint()来加载这个值,这里就可能出现异常。

1
2
3
4
5
6
7
() 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);
}

inline_ref和inline修饰符的合理使用

首先,需要阐述一下inline_ref和inline修饰符的区别:

  • Inline:使用inline修饰符的函数,其代码会在每次调用时被直接插入到调用位置。也就是说,每次调用函数时,函数的实际代码会被复制到调用的位置,而不是像普通函数那样通过跳转到函数体执行。

  • inline_ref:使用 inline_ref修饰符的函数,其代码存储在一个独立的cell中。每次调用函数时,TVM通过CALLREF命令来执行存储在cell中的代码,而不是在调用位置插入函数代码。

所以,inline修饰符适用于简单函数,减少函数调用开销,但可能导致合约代码重复;而inline_ref 修饰符适用于较复杂或被多次调用的函数,通过将函数代码存储在单独的cell中来提高效率,避免了代码重复。

那么可以总结为:当函数较大或被多个地方调用时,建议使用inline_ref;反之,则建议使用inline。

确定正确的工作链

TON允许创建多达2^32条工作链,每条工作链则可以细分为多达2^60个分片,前只有2个工作链:主链(-1)和基本链(0)。合约中计算目标地址时,必须明确指定目标地址所属的链ID,以确保生成的钱包地址位于正确的工作链上。为了避免生成错误地址,建议使用 force_chain() 强制指定链ID。

避免错误码冲突

合约设计中,为了确保规范性和避免混淆,错误码的管理非常关键。

对于TON智能合约,首先应确保每个错误码在合约中是唯一的,避免在同一个合约中定义重复的错误码,以防止错误码的混淆和信息的不明确;

其次TON平台或底层系统已经定义了一些标准的错误码,应避免与这些系统错误码冲突,例如333错误码表示的链ID不匹配。

所以建议合约的错误码最好在400到1000之间。

操作完成后需要存储数据和调用return()

在TON智能合约中,消息处理会根据op-code选择不同的逻辑。完成对应业逻辑后,还需完成两项操作:

首先,如果涉及数据更改,必须调用save_data()以确保数据被存储,否则更改将无效;

其次,必须调用return()以表示该操作完成,否则将触发throw(0xffff)异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
() 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);
}

备注:文章微改后发表于「Beosin」公众号