转账失败前先拿到答案:ERC-1592 把转账规则拆成独立合约 图 1
转账失败前先拿到答案:ERC-1592 把转账规则拆成独立合约 · 图 1

转账失败前先拿到答案:ERC-1592 把转账规则拆成独立合约

一只代币拒绝了你的转账,链上只会告诉你交易回滚了,不会告诉你是因为地址黑名单、锁定期还是白名单。ERC-1592 想让“为什么不行”在发交易之前就能查到。摘要的框架是给代币及其交互平台(交易所、钱包、dapp)一套统一的转账规则标准,规则与存储外置以复用和省钱。文档创建于 2018 年 11 月 9 日,仓库记录状态为 Stagnant,属于长期无人推进的归档方案。动机部分还有一句定位很关键:当时已经很难知道一笔 ERC-20 转账为何失败,等大量代币各自长出私规之后只会更难,所以标准必须做到——发交易之前就能判断这笔转账会成功还是失败、以及为什么。

规则是最小合约,问题只有两个

规范刻意把规则压小:每条规则逻辑越简单越好,因为这段逻辑会在每笔转账里执行;原子性还是组合的前提,靠知道哪条规则触发,被拒转账的原因就能被完整还原。落地形态是一个叫 IRule 的接口,只有两个只读函数:isAddressValid(address _address) 回答某个地址本身是否可参与转账,isTransferValid(address _from, address _to, uint256 _amount) 回答一笔具体转账是否合法。白名单、黑名单、锁定期、额度阈值,全部写成实现这两个函数的小合约,各自独立部署。

这里藏着两个容易被跳过的接口细节。其一,摘要对规则能力的概括是“依据发送方、目标地址与金额行事,按业务逻辑触发并拒绝转账”——落到 IRule 签名上就是 _from_to_amount 三个参数,规则没有第四个信息来源,任何声称复杂合规逻辑的实现最终都要压缩成这三元组的布尔判断。其二,规范给了适配指引:两个方法若有一个不适用,直接让它恒返回 true 即可;isTransferValid 里用不到的参数应当用注释符注释掉名字而不是删除——保持签名一致,引擎才能统一调用。这两条合起来定义了“规则”二字的极限:原子、无状态、只看转账三要素。

转账失败前先拿到答案:ERC-1592 把转账规则拆成独立合约 图 2
转账失败前先拿到答案:ERC-1592 把转账规则拆成独立合约 · 图 2

规则引擎与挂载顺序

代币侧集成靠 IWithRules 接口:ruleLength() 返回挂了几条规则,rule(uint256 _ruleId) 按序号取规则合约地址,validateAddress(address)validateTransfer(address _from, address _to, uint256 _amount) 提供预演式查询——钱包在发起前调用后者就能拿到合法与否及其位置,失败原因不再需要事后猜。唯一的批量写入函数是 defineRules(IRule[] _rules),一次交易按指定顺序挂上全部规则;换规则再调一次,规范要求触发事件 RulesDefined(uint256 count),这条事件就是“规则被改过”的可监测信号——动机部分写的“规则改了要能被轻易察觉”正是由它兑现。顺序是语义的一部分:规则按序求值,短路顺序不同,拒绝理由就不同。

引擎侧提供两个修饰符 whenAddressRulesAreValidwhenTransferRulesAreValid,代币合约把它们套在 transfer、transferFrom 及任何需要遵守白名单或复杂规则的路径上,转账执行与规则核验从此共用同一份规则合约——预演讲一遍和实际执行用的是同一批合约,这正是“发交易前就能知道结果”的实现基础。

归档方案的三个遗产问题

文档末尾给了规则模板和实现示例的链接,举例包括冻结规则这类黑名单实现。这份 Stagnant 文本留下三个至今仍在的问题作为排查清单:规则住在独立合约意味着规则本身可被换掉或含漏洞,读一只受限代币时应当把每条规则的部署地址与源码都过一遍,而不是只看代币主合约;预演接口 validateTransfer 和真实执行之间隔着区块状态,预演通过不等于随后那笔一定成功;改了规则只发事件不改调用方,钱包若不监听 RulesDefined,用户手里的“能否转”缓存就是过期的。答案为否的集成方,都只是把失败推迟到弹窗而已。本文为机制说明,不构成任何投资建议。