链上抽奖的种子从哪个对象来
Sui 的 Move 合约要取随机值,用的是一批保留在固定地址上的能力:Random 对象的地址是 0x8。合约函数拿到它的不可变引用,调用 new_generator 造出一个 RandomGenerator,再用 generate_u128、generate_u8_in_range 这类方法取值。官方文档把这个发生器明确描述成伪随机发生器,不是真随机源,这一点是所有后续风险的源头。
还有一个容易被忽略的结构特征:Random 虽然是共享对象,但不允许任何可变操作——任何试图修改它的事务都会失败。它是纯粹的被读取对象,合约只能通过它派生自己的发生器,不能往里写状态、不能自己维护一个计数器把它当抽奖池。
第一个坑:资源是有限的

文档专门警告了一类设计层面的陷阱:每笔交易可用的资源有上限,而攻击者可以刻意控制你的函数在资源耗尽的那一刻执行。gas 是最典型的一种,但不是唯一一种。文档列出的同类受限资源还包括新建对象的数量、可被使用的对象数量(包含动态字段)、发出的事件数量,以及 UID 生成、删除与转移的次数,具体阈值都写在协议的 ProtocolConfig 限制表里。它同时点明:Random 这套 API 本身不会自动替你挡住这种攻击,必须在合约设计阶段自己考虑。
这类攻击的实际后果不是“随机数变得不好”,而是交易在随机的地方失败——如果你把付费和取值写在一起,失败的那一笔可能把该收的钱也带回去。
第二个坑:输了就回退
文档给了一个骰子的例子。合约函数先收取一笔费用转给创建者,再掷骰判断输赢。攻击者另写一个函数调用它,拿到结果后判断:如果没中,就抛断言让整笔交易回退。由于回退把前面的费用转账一起撤销,攻击者相当于白玩无数次,只在赢的那一次真正付钱。
官方给的防线是把函数定义成私有的 entry 函数,让别的模块无法调用。文档还指出这一条是被编译器强制的:把 Random 作为参数出现在 public 函数上,Move 编译器会直接拒绝。换句话说,这里不是风格建议,而是编译期规则。
第三个坑:可编程交易块的原子性
即使函数已经是私有 entry,还有第二种攻击。Sui 的可编程交易块(PTB)把多条命令打包成一笔原子执行的交易:块里先跑掷骰命令,再把结果喂给攻击者自己的命令,一旦判定失败,整笔交易连同前面的付费一起回退。文档强调,逐笔重发还能让每次攻击拿到不同的随机值,因此单纯拆成多笔并不能解决。
协议层面的防御是硬性限制:如果在以 Random 为输入的 MoveCall 之后,PTB 里跟着的不是 TransferObjects 或 MergeCoins 这类命令,这笔交易会被拒绝。这就堵住了“把用了随机数的结果继续往下串”的通道。
第四个坑:发生器不能由别人递给你
文档还写明:RandomGenerator 只有在由真正使用它的模块自己创建时才安全。如果它作为参数从外部传进来,调用方就可能预测这个实例后续的每一个输出——办法是把发生器对象用 bcs::to_bytes 序列化出来,直接解析它的内部状态。官方的强制手段同样是编译器的:把 RandomGenerator 作为参数出现在 public 函数上会被拒绝。
需要公平揭示时的两种做法
对于抽奖选个中奖者这类场景,代码逻辑与随机值本身无关,上面的攻击不构成问题。但对于下注、开箱这类“先看到结果再决定要不要付费”的流程,文档建议把逻辑拆到两笔交易里:第一笔取到随机值后,把它存进一个在同一笔交易里别人读不到的对象(例如转给调用者,或者记下这笔交易的摘要并在读取时校验不一致);第二笔再读出来完成结算。文档同时提醒两个前提:第二笔的输入必须在第一笔之后不可再被修改,否则攻击者看到随机值后照样能改;还要为“第二笔永远没来”设计兜底,一种简单做法是在第一步就把费用收掉。
该看什么、不能推出什么
判断一个用到链上随机的合约是否稳,可以按这几条问:发生器是自己模块创建的还是别人传进来的;用随机数的那笔是否会被塞进 PTB 串到别的命令后面;付费与取值之间有没有可被回退的窗口;资源受限项的失败路径是不是把费用一起退回去了。这些问题在官方文档里都有对应的机制答案,而具体项目有没有做,得去看它的 Move 源码。
以上都是机制与防御层面的说明,不涉及任何资产的价值判断或收益承诺,本文不构成投资建议。所有函数名、地址与规则描述以 Sui 官方文档当前记载为准。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。