开发
资产与代币
Ksana 有内置资产,由 VM 记录;也有代币,由合约按两种标准记录:KS-20 用于同质化代币,KS-721 用于独一无二的代币。它们有什么区别、该选哪一种,以及所有 KS-20 代币和 KS-721 合集的共同之处。
三种类型
| 内置资产 | KS-20 代币 | KS-721 合集 | |
|---|---|---|---|
| 是什么 | KSA,或合约用 asset_issue 或 asset_create 创建的资产 |
记录一本数额账本的合约 | 记录每个独一无二的代币归谁所有的合约 |
| 余额记在哪里 | VM 的余额表,和每个账户的 KSA 记在一起 | 代币合约的存储 | 合集合约的存储 |
| 怎样转移 | 一个 AssetTransfer 操作,或附带在调用上 |
调用代币合约:transfer、transfer_and_call |
调用合集合约 |
| 谁来读取 | 节点的 ksana_getAccount 会列出它 |
对代币合约的 balance_of 做只读调用 |
对 owner_of 做只读调用 |
| 自己的规则 | 只有发行方创建它时从 mint、freeze、allowlist、pause 中声明的能力 |
任意规则,由代币在 capabilities() 里报告 |
任意规则,同样报告 |
| 一次转账花费 | 10,000 gas | 参考代币约 84,000 gas | 测试合集约 176,000 gas |
每个内置资产都有名称、符号、小数位数和一个 32 字节的 ID:KSA 的 ID 是 32 个零字节,发行的资产的 ID 是它的合约 ID 与一个子 ID 的 SHA-256(账户讲了余额)。代币以它的合约 ID 来指称。这两种 ID 看起来一样,所以接受其中任一种的合约会要求一个 AssetRef,而 Playground 在部署任何东西之前都会检查每一个 ID。
选哪一种
| 需要 | 用 |
|---|---|
| 一种货币、一个积分余额、一种按数量计的物品 | 内置资产:钱包、区块浏览器和 RPC 都认识它,它可以附带在调用上,一次转账花费 10,000 gas |
| 一种发行方必须能冻结账户、只允许名单内的账户持有或暂停所有转账的货币,比如支付用的稳定币 | 用 asset_create 带着这些能力创建的内置资产:钱包会显示这些能力,发行方可以放弃它们,但永远不能获得新的能力,也不能拿走任何余额 |
| 带有自己逻辑的余额:转账手续费、会生息的份额、投票快照、锁仓释放、分红、每个持有者的数据、会读写其他合约的转账 | KS-20 代币 |
| 每件都独一无二的物品:收藏品、门票、名称、仓位凭证 | KS-721 合集 |
| 由一个合约发出的多种同质化物品 | 内置资产:一个合约可以发行任意多种,每种有自己的子 ID |
一个品牌只发行一种形式。当它需要进入只接受另一种形式的合约时,比如让一个 KS-20 代币进入只接受内置资产的市场,可以用一个包装合约一比一地锁住代币,并发行一种名称表明这一点的内置资产,比如 wXYZ。
合约怎样声明标准
代币或合集自己声明它的标准。它的 init 先发出事件 standards(ids),再发出 metadata,都在任何 transfer 之前;它的只读方法 standards() 在整个生命周期里都返回同一个列表:先是标准("KS-20" 或 "KS-721"),然后是它实现的扩展。索引器把一个合约的第一个 standards 事件当作它的声明,所以只凭回执就能索引代币;钱包和其他合约随时可以调用这个只读方法;在合约里,SDK 的 standard::standards_of(contract) 和 standard::declares(contract, id) 会去问它。
声明只是一种主张,不是保证:一个合约可以声明 KS-20 却违反它的规则,发出虚假的事件,给出虚假的余额,不过只能是它自己的。钱包信任的是合约的身份,即它的 ID 和代码哈希,除非合约委托它的代码,否则两者都不会变。钱包可以标记代码哈希与某个已知构建相同的合约:Playground 的编译服务在任何机器上都把同一份源码构建成同一个模块。
能力
capabilities() -> Vec<String> 报告合约的规则此刻允许别人做什么,让钱包可以给代币加上标记:
| 名称 | 有人可以 |
|---|---|
mint |
创造更多代币:设有增发者,或者合集还能铸造 |
freeze |
让某个账户的代币无法转移 |
allowlist |
限制谁可以持有或接收这些代币 |
pause |
停止所有转账 |
clawback |
不经持有者同意,从持有者那里拿走代币 |
fee |
对转账收取手续费 |
upgrade |
通过委托代码,改变合约的代码 |
列表里每个名称只出现一次,按这个顺序排列,标准没有定义的名称排在它们后面。已经结束的能力不再报告:增发者放弃了增发的代币,就不再报告 mint。
内置资产的发行能力用的是前四个名称:发行合约在创建资产时声明它们,之后只能放弃。ksana_getAsset 按同样的顺序列出它们,所以钱包可以用同一种方式标记两类资产。内置资产永远没有 clawback、fee 和 upgrade,它的任何能力都不能移动余额(规则见宿主函数)。
一个参数接受两种资产
在一个参数里接受任何同质化资产的合约,比如资金池或市场,使用 SDK 的 standard::AssetRef:
enum AssetRef { Token(ContractId), Asset([u8; 32]) } // 一个 KS-20 代币;一个内置资产(KSA 是 32 个零字节)它的 Borsh 编码是一个表示变体的字节,Token 为 00,Asset 为 01,再加上 32 字节的 ID:01 加 32 个零字节就是 KSA。示例 AMM 就这样接受它的两侧。在合约里,AssetRef::balance_of 读取代币的 balance_of 或内置余额,AssetRef::pay 用代币的 transfer 或 asset_transfer 从合约自己的余额里付款。
拒绝原因
拒绝就是方法返回的错误文本,回执里显示为 revert: <原因>。标准把这些原因固定为英文短语,让钱包可以翻译它们,合约可以比较它们:
| 情形 | 原因 |
|---|---|
| 合约没有的方法 | unknown method |
| 无法解码的参数,或解码后还剩下字节 | malformed arguments |
| 给不接受资金的方法附带了资金 | unexpected attached funds |
| 数额为 0(KS-20) | zero amount |
| 以零账户作为收款方、被授权方、操作员或增发者 | zero account |
| 以代币或合集自身作为收款方 | recipient is this contract |
| 余额不足(KS-20) | insufficient balance |
| 授权额度不足(KS-20) | insufficient allowance |
| 授权额度已过期(KS-20) | allowance expired |
给自己 approve(KS-20) |
spender is the caller |
| 不存在的代币(KS-721) | unknown token |
所有者以外的人调用 transfer 或 transfer_and_call(KS-721) |
not the owner |
无权的账户调用 transfer_from、approve 或 burn(KS-721) |
not authorized |
transfer_from 写的 from 不是所有者(KS-721) |
wrong owner |
| 转给所有者自己(KS-721) | already the owner |
| 以所有者作为被授权的账户(KS-721) | spender is the owner |
给自己 set_operator(KS-721) |
operator is the caller |
| 铸造一个已有或曾有代币的 ID(KS-721) | token exists、token was burned |
收款方的返回值不是 Bytes |
malformed return value |
增发者以外的人调用 mint 或 set_minter(KS-20-mintable) |
not the minter,没有增发者之后为 minting has ended |
| 增发超过上限(KS-20-mintable) | cap exceeded |
| 合约自己的规则拒绝 | rejected: <原因> |
transfer_and_call 的收款方拒绝时,回执显示收款方的原因。合约自己的 init 和限制可以使用其他原因。
安全
- 冒充:任何人都可以部署一个名叫 KSA 的代币。钱包会清理名称和符号,拿它们和 KSA、BTC 比较,对未经验证的代币显示完整的 ID,并把没人要的代币折叠起来。
- 授权额度:授权额度让被授权方随时可以拿走额度以内的代币。只授权一次操作需要的数额,并设好有效期,撤销不再使用的授权,优先使用不需要授权的
transfer_and_call。 - 回调:接收代币的合约要检查
caller(),因为任何人都可以直接调用on_token_received或on_nft_received。代币先转移,再调用;调用最多嵌套 8 层。 - 卡住的代币:代币永远不会转到零账户或合约自身,不接受资金的方法会拒绝资金。用普通的
transfer转给一个无法转出代币的合约,代币就卡在那里了,所以收款方是合约时,钱包会建议用transfer_and_call。 - 垃圾转账:数额为 0 的转账会被拒绝,所以没人能用它们塞满一个账户的记录。
- 可升级的代币:委托代码的合约可以改变它的行为;它会报告
upgrade,而参考实现什么也不委托。 - 会拒绝的规则:代币自己的规则,以及内置资产的能力,都可能以
rejected:开头的原因拒绝一笔转账。资金池或市场对待这种拒绝,就像对待余额不足一样。 - NFT 媒体:钱包只用
<img>显示代币的图片,从不把它当作会运行脚本的文档;远程媒体通过代理加载,或者等你打开时才加载。