Skip to content

TEP: 85 - SBT 合约

摘要

灵魂绑定代币(SBT)是一种特殊类型的 NFT,不能被转移。它包括可选的证书机制,由授权机构撤销,并提供链上所有权证明。持有人可以随时销毁其 SBT。

动机

有一种有用的代币类型,可以给予用户社会权限/角色或证书。例如,它可以被市场用来给 SBT 持有者提供折扣,或被大学用来颁发以 SBT 形式的认证证书。所有权证明机制允许轻松地向任何合约证明你是某个 SBT 的所有者。

规范

SBT 实现了 NFT 标准接口,但 transfer 应始终被拒绝。

1. prove_ownership

入站消息的 TL-B 方案:

prove_ownership#04ded148 query_id:uint64 dest:MsgAddress 
forward_payload:^Cell with_content:Bool = InternalMsgBody;

query_id - 任意请求号。

dest - 目标合约的地址,SBT 的所有权应向其证明。

forward_payload - 目标合约所需的任意数据。

with_content - 如果为真,SBT 的内容单元将包含在发送给合约的消息中。

应被拒绝如果:

  • 发送地址不是所有者地址。

否则应: 发送消息给 dest 合约,TL-B 方案如下:

ownership_proof#0524c7ae query_id:uint64 item_id:uint256 owner:MsgAddress 
data:^Cell revoked_at:uint64 content:(Maybe ^Cell) = InternalMsgBody;

query_id - 在 prove_ownership 中传递的请求号。

item_id - NFT 的 ID。

owner - 当前所有者地址。

data - 在 prove_ownership 中传递的数据单元。

revoked_at - SBT 被撤销的 Unix 时间,如果未被撤销则为 0。

content - NFT 的内容,如果 prove_ownership 中的 with_content 为真,则传递。

2. request_owner

入站消息的 TL-B 方案:

request_owner#d0c3bfea query_id:uint64 dest:MsgAddress 
forward_payload:^Cell with_content:Bool = InternalMsgBody;

query_id - 任意请求号。

dest - 目标合约的地址,SBT 的所有权应向其证明。

forward_payload - 目标合约所需的任意数据。

with_content - 如果为真,SBT 的内容单元将包含在发送给合约的消息中。

应:

发送消息给 dest 合约,TL-B 方案如下:

owner_info#0dd607e3 query_id:uint64 item_id:uint256 initiator:MsgAddress owner:MsgAddress 
data:^Cell revoked_at:uint64 content:(Maybe ^Cell) = InternalMsgBody;

query_id - 在 prove_ownership 中传递的请求号。

item_id - NFT 的 ID。

initiator - 请求发起者的地址。

owner - 当前所有者地址。

data - 在 prove_ownership 中传递的数据单元。

revoked_at - SBT 被撤销的 Unix 时间,如果未被撤销则为 0。

content - SBT 的内容,如果 request_owner 中的 with_content 为真,则传递。

3. destroy

内部消息的 TL-B 方案:

destroy#1f04537a query_id:uint64 = InternalMsgBody;

query_id - 任意请求号。

应被拒绝如果:

  • 发送地址不是所有者地址。

否则应:

  • 将所有者地址和授权地址设为空。
  • 向发送者发送消息,TL-B 方案为 excesses#d53276db query_id:uint64 = InternalMsgBody;,将传递合约的余额。
4. revoke

入站消息的 TL-B 方案:

revoke#6f89f5e3 query_id:uint64 = InternalMsgBody;

query_id - 任意请求号。

应被拒绝如果:

  • 发送地址不是授权地址。
  • 已经被撤销。

否则应:revoked_at 设置为当前 Unix 时间。

GET 方法

  1. get_nft_data() - 与 NFT 标准 相同。
  2. get_authority_address() - 返回授权地址的 slice。授权可以撤销 SBT。 此方法对于 SBT 是强制性的,如果没有授权应返回 addr_none(2 个零位)
  3. get_revoked_time() - 返回撤销时间的 int,即被撤销的 Unix 时间。如果未被撤销则为 0。

实现示例

https://github.com/getgems-io/nft-contracts/blob/main/packages/contracts/sources/sbt-item.fc

指南

铸造

可以使用基本的 NFT 集合进行铸造,SBT 应作为一个项目。在铸造消息中应传递授权地址,在内容之后

在铸造之前,建议发行者检查钱包代码并确认它是标准化钱包,而不是可以转让给第三方的合约。

向合约证明所有权

SBT 合约具有一种功能,允许通过链上证明实现与合约的有趣机制。

你可以向 SBT 发送消息,它会将消息代理到目标合约,并在消息体中包含其索引、所有者地址和发起者地址,以及对合约有用的任何有效负载, 这样目标合约可以知道你是与预期集合相关的 SBT 的所有者。合约可以通过使用代码和索引计算 SBT 的地址,并将其与发送者进行比较来知道 SBT 是否与集合相关。

有两种方法可以使用此功能,所有权证明所有权信息。 区别在于所有权证明只能由 SBT 所有者调用,因此在需要仅接受来自所有者的消息时(例如在 DAO 中的投票)优先使用。

所有权证明

SBT 所有者可以使用以下方案向 SBT 发送消息:

prove_ownership#04ded148 query_id:uint64 dest:MsgAddress 
forward_payload:^Cell with_content:Bool = InternalMsgBody;

之后,SBT 会向 dest 发送转移消息,方案如下:

ownership_proof#0524c7ae query_id:uint64 item_id:uint256 owner:MsgAddress 
data:^Cell revoked_at:uint64 content:(Maybe ^Cell)
所有权信息

任何人可以使用以下方案向 SBT 发送消息:

request_owner#d0c3bfea query_id:uint64 dest:MsgAddress 
forward_payload:^Cell with_content:Bool = InternalMsgBody;

之后,SBT 会向 dest 发送转移消息,方案如下:

owner_info#0dd607e3 query_id:uint64 item_id:uint256 initiator:MsgAddress owner:MsgAddress 
data:^Cell revoked_at:uint64 content:(Maybe ^Cell)
验证 SBT 合约示例
C
int op::ownership_proof() asm "0x0524c7ae PUSHINT";

int equal_slices (slice a, slice b) asm "SDEQ";

_ load_data() {
    slice ds = get_data().begin_parse();

    return (
        ds~load_msg_addr(),    ;; collection_addr
        ds~load_ref()          ;; sbt_code
    );
}

slice calculate_sbt_address(slice collection_addr, cell sbt_item_code, int wc, int index) {
  cell data = begin_cell().store_uint(index, 64).store_slice(collection_addr).end_cell();
  cell state_init = begin_cell().store_uint(0, 2).store_dict(sbt_item_code).store_dict(data).store_uint(0, 1).end_cell();

  return begin_cell().store_uint(4, 3)
                     .store_int(wc, 8)
                     .store_uint(cell_hash(state_init), 256)
                     .end_cell()
                     .begin_parse();
}


() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg) impure {
  slice cs = in_msg_full.begin_parse();
  int flags = cs~load_uint(4);

  slice sender_address = cs~load_msg_addr();

  int op = in_msg~load_uint(32);
  int query_id = in_msg~load_uint(64);

  if (op == op::ownership_proof()) {
    int id = in_msg~load_uint(256);

    (slice collection_addr, cell sbt_code) = load_data();
    throw_unless(403, equal_slices(sender_address, collection_addr.calculate_sbt_address(sbt_code, 0, id)));

    slice owner_addr = in_msg~load_msg_addr();
    cell payload = in_msg~load_ref();
    
    int revoked_at = in_msg~load_uint(64);
    throw_if(403, revoked_at > 0);
    
    int with_content = in_msg~load_uint(1);
    if (with_content != 0) {
        cell sbt_content = in_msg~load_ref();
    }
    
    ;;
    ;; sbt verified, do something
    ;;

    return ();
  }

  throw(0xffff);
}

理由和替代方案

  • 为什么这是在可能设计中最好的设计?

这个设计允许我们将 SBT 用作证书,具有撤销和链上证明功能,同时如果没有设置授权,也可以作为真正的 SBT。

  • 考虑了哪些其他设计,为什么不选择它们?

最初考虑了类似 ETH 的设计,即地址绑定的代币,但它被扩展为有用的链上证明和撤销选项。

  • 不这样做的影响是什么?

目前,TON 没有所有者绑定的代币标准,因此发行不能转让给第三方的代币是一个问题。因此,如果我们忽略这个或任何引入这种机制的类似标准,TON 可能会错过一些有趣和有前途的产品。

先前的工作

在 ETH 中(EIP-4973 ABT) - SBT 被做成一种完全不能在账户之间转移的 NFT。我们做了同样的事情,但扩展了逻辑,增加了链上证明和授权可以撤销的功能,因此 SBT 可以用作功能齐全的证书。

缺点

EIP-4973 ABT 具有装备/卸装机制,允许暂时显示/隐藏 SBT。在当前提案中,我们只能销毁 SBT。实际上不确定我们是否需要显示/隐藏逻辑。

未来的可能性

标准看起来已经定型。