增强 UDT 标准
介绍
UDT标准是CKB社区长久以来的问题,之前有sUDT和xUDT两个标准,sUDT过于简单,xUDT的extension模式则因为很难与其他应用兼容导致几乎无人使用,此外,到目前为止,还没有一个在链上可以绑定UDT信息的标准。
一个实用的思路是,UDT应该像Solana的SPL代币标准一样专注于实现,而非像ERC20那样关注抽象接口。
为什么像xUDT这样,定义一个extension的抽象接口来扩展代币标准的努力难以推进呢?因为EVM是一个链上计算的模型,而Cell模型不是,任意实现扩展将使得其他应用难以对接。所以,合理的做法是,由UDT设计者决定对应的UDT脚本具有哪些功能,并提供链上脚本实现和链下SDK实现,这样才能保证诸多应用,如钱包,DEX,Lending在对接的时候不会遇到问题,代币发行方可以在UDT已有的功能集中选择需要的功能并组合使用。
而一旦对于某个新功能的需求足够强烈,同时已有的UDT未能提供所需功能,则可以设计一个新的UDT实现使用,该实现应尽可能保证向后兼容,使得在仅仅替换code_hash的前提下,大部分应用简单对接代币的转移。
Solana目前有两个代币标准:
-
SPL Token,具有的功能如下:
- Mint权限
- Freeze权限,即可以冻结账户
- decimals信息
- 元数据,name,decimal,uri
-
SPL Token 2022,在 SPL Token 的基础上新支持:
- 隐私转账,转账过程可以隐藏交易金额
- 转账hook:对于每笔转账,调用一个特定的程序
- 转账费用:可以对转账收税
- 关闭mint账户,收回里面的存储租金
- 计息代币:代币余额可以以一个比例增长
- 不可转移代币:只允许发行者变更代币所有者。
- 转账备注:用于合规,审计追溯
- 关闭mint权限
可以说,对代币标准的扩展需求大部分时候都不会超过上面所指出的集合。
而考虑到重要性以及在Cell模型上实现的难度,我认为先实现基础SPL Token的功能是合理的,而其他功能,在Cell模型上实现都很有难度,试举几例:
- 转账费用,需要考虑状态争用或者状态成本问题。
- 计息代币,需要考虑链上时间获取问题。
那么,对于一个简单且实用的UDT标准实现,其应具有以下功能,元数据绑定方便应用方对接,冻结权限方便中心化稳定币发布:
- mint权限的可编程性,AMM的LP代币,抵押稳定币等都依赖这一点。
- 元数据的链上绑定,即根据代币Cell,即可索引到代币的元数据信息
- 冻结权限,即将某个特定lockscript拉入黑名单的能力
- mint权限的可修改能力
- 元数据的可修改能力
- 冻结权限的可修改能力
- Supply控制能力
为了简化使用,并应对不同的场景,本方案将提出两个脚本实现,分别为 Enhanced sUDT 和 Enhanced xUDT。
其中 Enhanced sUDT,在 transfer 的使用将与 sUDT 一致,这使得它可以在不修改代码的时候与已有的应用和基础设施对接,而Enhanced xUDT将可以实施对 UDT Transfer 的额外约束,比如冻结功能,两种UDT标准的args都为32字节,以保证与当前UDT的完全兼容。
Enhanced sUDT,将包含以下功能:
- mint 权限的可编程性。
- 元数据的链上绑定
- mint权限的可修改能力
- 元数据的可修改能力
- Supply控制能力
Enhanced xUDT,将在Enhanced sUDT的基础上增加以下功能:
- 冻结权限
- 冻结权限的可修改能力
- 插件扩展能力
基础数据结构
struct ScriptAttr {
location: byte,
script_hash: Byte32,
script: ScriptOpt,
}
option ScriptAttrOpt (ScriptAttr);
vector ScriptAttrVec <ScriptAttr>;
ScriptAttr 是对链上权限的表示,其中Location的规定如下:
- 0,表示input_lock,即识别约束时应检查是否存在一input_cell的lock_hash满足规则。
- 1,表示 input_type,检查与上类似。
- 2,表示output_type,检查与上类似。
- 3,表示 dynamic linking,此script必须有值,根据 xUDT 的规则使用动态链接执行。
- 4,表示 spawn,此script必须有值,根据spawn执行对应脚本。(等待硬分叉后开启)
以上诸种类型,0,1,2在执行时必须依托在一个输入或者输出上,可能会有争用问题,而3,4则仅执行对应代码,不会有状态争用问题。
Enhanced sUDT
数据定义
在创建代币时,首先建立一个sUDT Meta Cell,其type script的code_hash为 sUDT Meta Type,args为type_id。
struct ScriptAttr {
location: byte,
script_hash: Byte32,
script: ScriptOpt,
}
option ScriptAttrOpt (ScriptAttr);
table sUDTMetadata {
flag: byte,
mint_authority: ScriptAttrOpt,
metadata_authority: ScriptAttrOpt,
supply: Uint128,
decimals: byte,
name: Bytes,
symbol: Bytes,
uri: Bytes,
extra_data: Bytes,
}
flag将指明该UDT使用的配置:
- 0号bit为1,代表开启supply功能。
- 其他bit保留,全部为0。
代币创建
代币创建者应先生成一个sUDT Meta Cell
,填好相应的信息,UDT Cell的type的code_hash为 Enhanced sUDT_code_hash
,args为 sUDT Meta type_hash
,即长度为 32byte。
代币发行
在发行代币时,存在两种情况:
- 如果开启了 Supply mode,需要将
sUDT Meta Cell
作为Input,并检查 mint_authority,并检查输出相对于输入增加或减少的UDT数量,并修改 supply 的值。代币发行者可以先发行总量代币,再把 mint_authority 设置为 None,即永不可再增发代币。 - 不开启 Supply mode,需要将
sUDT Meta Cell
作为 CellDeps,并检查 mint_authority。
代币转移与销毁
其代币转移与销毁与普通sUDT规则一样,完全由其lock控制,可随意操作。
Meta Cell 修改
mint_authority 验证通过后可以转移 mint 权限,metadata_authority 验证通过后可以转移 metadata修改权限,以及修改 name,symbol,uri,decimals。修改decimals可以理解为拆分代币或相反。
所有权限,一旦转移至 None,则无法再收回。
Enhanced xUDT
在创建代币时,首先建立一个xUDT Meta Cell,其type script的code_hash为 sUDT Meta Type,args为type_id。
struct ScriptAttr {
location: byte,
script_hash: Byte32,
script: ScriptOpt,
}
option ScriptAttrOpt (ScriptAttr);
vector ScriptAttrVec <ScriptAttr>;
table xUDTMetadata {
flag: byte,
paused: byte,
mint_authority: ScriptAttrOpt,
pause_authority: ScriptAttrOpt,
metadata_authority: ScriptAttrOpt,
freeze_authority: ScriptAttrOpt,
extensions: ScriptAttrVec,
supply: Uint128,
decimals: byte,
name: Bytes,
symbol: Bytes,
uri: Bytes,
extra_data: Bytes,
}
flag将指明该UDT使用的配置:
- 0号 bit 为 1,代表开启 supply 功能。
- 1号 bit 为 1,代表 freeze 功能。
- 其他bit保留,全部为0。
相比 Enhanced sUDT,Enhanced xUDT 的元数据多出了三项,即paused,pause_authority,freeze_authority 和 extensions,其中 paused指代合约是否暂停,pause_authority可以修改paused的值,仅限0或1,freeze_authority 拥有增删黑名单的权限,extensions则为针对每笔 UDT Transfer 施加的额外检查,可以不止一项。
代币创建
代币创建者应先生成一个xUDT Meta Cell
,填好相应的信息,UDT Cell的type的code_hash为 Enhanced xUDT_code_hash
,args为 xUDT Meta type_hash
,即长度为 32byte。
代币发行
在发行代币时,存在两种情况:
- 如果开启了 Supply mode,需要将
sUDT Meta Cell
作为Input,并检查 mint_authority,并检查输出相对于输入增加或减少的UDT数量,并修改 supply 的值。代币发行者可以先发行总量代币,再把 mint_authority 设置为 None,即永不可再增发代币。 - 不开启 Supply mode,需要将
sUDT Meta Cell
作为 CellDeps,并检查 mint_authority。
代币发行,也应该经过 extensions 检查,不过传入当前操作已通过mint_authority检查的信息,大部分extensions可以直接返回验证成功。代币发行时,paused应为0,否则不允许发行新代币。
Freeze 功能
代币的黑名单将使用链表实现,当 flag 表明对应代币是一个具有 freeze_authority 的代币时,需要保证同时创建一个链表cell,该链表cell包含的范围是000…000到FFF…FFF。
array BlackListRange [Byte32; 2];
table BlackListData {
range: BlackListRange,
blacklist: Byte32Vec,
}
每个blacklist的黑名单数据是一个Bytes32Vec,代表被冻结的lock_hash,具有四种操作:
- 插入一些在range范围的项。
- 删除一些在range范围的项。
- 拆分Cell,一个range变成两个range
- 合并Cell,两个range合并成一个range。
该链表 cell 的 typescript 的 code_hash 是 blacklist code_hash,args是 UDT Meta type_hash,当往链表中插入新的项,或者将链表cell从一个拆成多个时。
脚本执行检查,在 cellDeps 中读取一个 cell,其type_hash等于 blacklist_lock_script的args,并使用 UDTMetadata 解析,获取freeze_authority,然后验证权限,同时验证插入,删除,拆分,合并的正确性。
当 flag 存在 freeze 功能时,转账时,需要在cellDeps放置对应的blacklist_cell,其范围应涵盖所有输入以及输出的UDT Cell的 lockhash,如果分布在多个黑名单链表cell中,则需要放置多个cellDeps。
UDT需要检查所有涉及UDT的lockhash都不存在于blacklist_cell中,交易才能成功上链。
拥有freeze_authority的脚本,可以修改 UDTMetaData 中的 freeze_authority,即转移权限。
相比使用SMT的黑名单实现,链表是纯链上实现,一切信息在链上可查,并且增删黑名单也很简单。
同时,pause_authority可以直接将paused设置为1,使得任意转账都不被允许。
代币转移与销毁
其代币转移与销毁都需要经过黑名单和extensions的检查。
- 需要将
sUDT Meta Cell
作为 CellDeps。 - 如果元信息的flag表明开启了 freeze 模式,则需要检查从cellDeps读取所有的blacklist Cell,并检查输入输出中所有的lock_hash都不存在于blacklist中。
- 如果元信息中的 extensions 长度不为零,则逐一执行所有的extensions 检查。
- 如果meta中的paused等于1,则不允许发起任何转账。
Meta Cell 修改
mint_authority 验证通过后可以转移 mint 权限,同时可以修改extensions设置,比如增删 extensions。
freeze_authority 验证通过后可以转移 freeze权限,并增删 blacklist cell。
metadata_authority 验证通过后可以转移 metadata修改权限,以及修改 name,symbol,uri,decimals。修改decimals可以理解为拆分代币或相反。
所有权限,一旦转移至 None,则无法再收回。
脚本分析
对于 Enhanced sUDT,存在两个Script:
一个是 Meta type,其负责检查一些修改Meta信息的权限,以及格式的检查,由于MetaCell的所有检查均由type执行,建议其lock强制为 always_success,如 ckb-ecofund/ckb-proxy-locks 内的不可升级部署,建议 always_success 能有一个公认的部署。
另一个是 Enhanced sUDT type,其负责检查UDT转移,而对于mint,则通过获取 MetaCell 的信息,并进行代理检查。
这里存在一个依赖关系,由于 SupplyMode,Meta type需要能读取 UDT在输入输出的数量,所以它要在代码里硬编码 Enhanced sUDT type 的 code_hash和hash_type。
但是经过分析可以发现,只有 Meta type 需要知道 Enhanced sUDT type的信息,而Enhanced sUDT type是不需要直到 Meta type 的部署时信息的,其只需要获取自己的args,并在inputs或者celldeps找到一个type满足要求,并按照 sUDT Meta的格式读取其CellData并使用即可。
故两个脚本都可以使用data_hash的不可升级部署。
对于 Enhanced sUDT,存在三个Script:
一个是 Meta type,其负责检查一些修改Meta信息的权限,以及格式的检查,由于MetaCell的所有检查均由type执行,建议其lock强制为 always_success,如 ckb-ecofund/ckb-proxy-locks 内的不可升级部署,建议 always_success 能有一个公认的部署。
二是 Enhanced xUDT type,其负责检查UDT转移,而对于mint,则通过获取 MetaCell 的信息,并进行代理检查,同时还需要读取黑名单 Cell,判断交易是否合法。
三是 BlackList type,其负责检查 BlackList 的增删,Cell 的拆分以及合并。
在思考三个脚本的部署依赖关系时,首先 Meta type是依赖 Enhanced xUDT type 代码的部署的,同时 Enhanced xUDT type 会依赖 BlackList type 的部署,同时 Meta type 也是依赖 BlackList type 的部署。
因为 Enhanced xUDT 脚本在执行时,如何找到blacklist cell呢?一个简单的方法是,直接取出 blacklist code_hash和hash_type并拼接上自己的args,即可找到blacklist。
而 MetaCell 为何也需要依赖 BlackList Cell 的部署呢,因为在创建MetaCell时,如果开启了Freeze功能,则必须保证有一个覆盖全范围的BlackList被创建出来。
而 BlackList type 和 Enhanced xUDT type一样,只用通过args获取Meta Cell的信息,并解析读取,进行权限验证即可,所以也不存在循环依赖,可以全部用data部署,无非是按照依赖的顺序进行部署。
对于 MetaCell 和 Blacklist Cell,由于其约束全由type完成,lock的功能会影响功能,故可以在代码里硬编码 AlwaysSuccess 的脚本,并强制要求这两类Cell的lock必须为 AlwaysSuccess。
对比基于SMT的冻结机制
在 RFC: Regulation Compliance Extension - English / CKB Development & Technical Discussion - Nervos Talk 中,提出了一种基于SMT的冻结机制,其优点是链上状态占用极小,所以增加黑名单的成本极低。
缺点如下:
- 对于每个采用该机制的代币,所有涉及到的应用都需要索引并存储SMT树的最新状态,而基于链表的方案不需要维护链下状态,更易于对接。
- 对应用的侵入性强,转账交易需要在Witness内放置证明,那么当涉及到OTX交易时,由于用户签名包含对应的Witness,而最后的Aggregator可能一开始并不知道会涉及哪些lockscript,从而需要精心选择组合模式。而基于链表的方案,唯一增加的只是CellDeps,几乎完全不影响应用组合。
并且,基于extensions机制,如果未来黑名单的成本真的很高,比如说涉及一万个lock的黑名单,则需要 320000 CKB。
如果到时觉得这个成本过高,可以设计一个更易于组合的基于SMT的冻结方案,并从链表方案迁移过去,并收回所有的CKB。
迁移流程如下:
- mint_authority增加一个基于SMT冻结的插件,其args为 SMTRootCell 的 type_hash,执行该插件的流程为:
- 读取 type_hash 指向的 Cell,并读取其数据,数据中应包含两个信息,1:SMT Root;2:待检查的UDT type hash
- 从 Witness 中读取 SMT Proof
- 扫描输入输出对应 UDT Type hash 涉及到的lock。
- 根据SMT Proof,检查所有的lock符合规则。
- 首先将所有已有的黑名单lock增加进SMT的黑名单,此时每次转账都要经过两次检查,一次freeze,一次插件。
- 从blacklist cell中逐步清除合并所有的黑名单,并收回CKB,直到回到全范围的空链表。
- 此时CKB已收回,并且黑名单链表为空,相当于只受插件的黑名单检查。
关于权限管理的分析
根据 Principle of least privilege,关于UDT,尤其是合规的UDT,最终的实现中权限控制可以更精细,不限于本文的细节。
在本方案的Enhanced xUDT中,存在四种权限:
- mint_authority:铸造代币的权限
- pause_authority:暂停铸造和转移的权限
- metadata_authority:修改元数据权限
- freeze_authority:冻结账户权限
但有时可以增加更精细的分层权限控制,比如类似 USDC 在 ETH上的合约设计,存在一个 MasterMinter,可以增加minter,并给每个minter一定的mint限额,在每次铸造一定数量的代币之后,限额会随之减少,以隔离风险。
那么元数据可以变成接下来这样:
table xUDTMetadata {
flag: byte,
paused: byte,
master_mint_authority: ScriptAttrOpt,
minter: ScriptAttrVec,
mint_allowance: Uint128Vec,
pause_authority: ScriptAttrOpt,
metadata_authority: ScriptAttrOpt,
freeze_authority: ScriptAttrOpt,
extensions: ScriptAttrVec,
supply: Uint128,
decimals: byte,
name: Bytes,
symbol: Bytes,
uri: Bytes,
extra_data: Bytes,
}
当然,最好的办法是参考相关合规资产发行方的产品设计,并调研需求,以设计更通用普适的安全权限管理设计。