Scilla注意事项及使用技巧

性能

映射大小

如果你的合约需要知道字段映射的大小,即字段变量是映射,那么将字段映射读入变量并应用内置 size 的明显实现(如以下代码片段所示)可能非常低效,因为它需要制作相关映射的副本。

field accounts : Map ByStr20 Uint128 = Emp ByStr20 Uint128

transition Foo()
  ...
  accounts_copy <- accounts;
  accounts_size = builtin size accounts_copy;
  ...
end

为了解决这个问题,可以跟踪相应字段变量中的大小信息,如下面的代码片段所示。 请注意,现在不是复制映射,而是从 accounts_size 字段变量中读取。

let uint32_one = Uint32 1

field accounts : Map ByStr20 Uint128 = Emp ByStr20 Uint128
field accounts_size : Uint32 = 0

transition Foo()
  ...
  num_of_accounts <- accounts_size;
  ...
end

现在,为了确保映射及其大小保持同步,在使用内置的就地语句时需要更新映射的大小,例如使用 m[k] := vdelete m[k] 时,或者更好地定义并使用系统化的程序来做到这一点。

这是更新帐户映射中的键/值对并相应地更改其大小的 procedure 定义。

procedure insert_to_accounts (key : ByStr20, value : Uint128)
  already_exists <- exists accounts[key];
  match already_exists with
  | True =>
      (* do nothing as the size does not change *)
  | False =>
      size <- accounts_size;
      new_size = builtin add size uint32_one;
      accounts_size := new_size
  end;
  accounts[key] := value
end

这是从帐户映射中删除键/值对并相应地更改其大小的 procedure 定义。

procedure delete_from_accounts (key : ByStr20)
  already_exists <- exists accounts[key];
  match already_exists with
  | False =>
    (* do nothing as the map and its size do not change *)
  | True =>
    size <- accounts_size;
    new_size = builtin sub size uint32_one;
    accounts_size := new_size
  end;
  delete accounts[key]
end

资金俗语

部分接受资金

假设你正在编写一个让人们互相提示的合约。 你很自然地希望避免一个人因为打字错误而给太多小费的情况。 要求 Scilla 部分接受传入的资金会很好,但是却没有内置 accept <cap>。 你可以完全不接受或完全接受资金。 我们可以通过完全接受传入资金,然后在小费超过某个上限时立即退还小费来解决此限制。

事实证明,我们可以将这种行为封装为一个可重用的 procedure。

procedure accept_with_cap (cap : Uint128)
  sent_more_than_necessary = builtin lt cap _amount;
  match sent_more_than_necessary with
  | True =>
      amount_to_refund = builtin sub _amount cap;
      accept;
      msg = { _tag : ""; _recipient: _sender; _amount: amount_to_refund };
      msgs = one_msg msg;
      send msgs
  | False =>
      accept
  end
end

现在,procedure accept_with_cap 可以如下使用。

<contract library and procedures here>

contract Tips (tip_cap : Uint128)

transition Tip (message_from_tipper : String)
  accept_with_cap tip_cap;
  e = { _eventname: "ThanksForTheTip" };
  event e
end

安全性

转让合约所有权

如果你的合约有一个所有者(通常意味着拥有管理员权限的人,例如添加/删除用户帐户或暂停/取消暂停合约)可以在运行时更改,那么乍一看,这可以在代码中形式化为字段 owner 和像 ChangeOwner 一样的 transition:

contract Foo (initial_owner : ByStr20)

field owner : ByStr20 = initial_owner

transition ChangeOwner(new_owner : ByStr20)
  (* continue executing the transition if _sender is the owner,
     throw exception and abort otherwise *)
  isOwner;
  owner := new_owner
end

但是,这可能会导致当前所有者无法控制合约的情况。 例如,当调用 transition ChangeOwner 时,由于地址参数中的拼写错误,当前所有者可能会将合约所有权转移到不存在的地址。 在这里,我们提供了一种设计模式来规避这个问题。

确保新的所有者活跃的一种方法是分两个阶段进行所有权转让:

  • 当前所有者提出将所有权转让给新的所有者,请注意,此时当前所有者仍然是合约所有者;

  • 未来的新所有者接受待处理的所有权转让并成为当前所有者;

  • 在未来的新所有者接受转让之前的任何时刻,当前所有者都可以中止所有权转让。

这是上述模式的可能实现(未展示 procedure isOwner 的代码):

contract OwnershipTransfer (initial_owner : ByStr20)

field owner : ByStr20 = initial_owner
field pending_owner : Option ByStr20 = None {ByStr20}

transition RequestOwnershipTransfer (new_owner : ByStr20)
  isOwner;
  po = Some {ByStr20} new_owner;
  pending_owner := po
end

transition ConfirmOwnershipTransfer ()
  optional_po <- pending_owner;
  match optional_po with
  | Some pend_owner =>
      caller_is_new_owner = builtin eq _sender pend_owner;
      match caller_is_new_owner with
      | True =>
          (* transfer ownership *)
          owner := pend_owner;
          none = None {ByStr20};
          pending_owner := none
      | False => (* the caller is not the new owner, do nothing *)
      end
  | None => (* ownership transfer is not in-progress, do nothing *)
  end
end

具有上述 transition 的合约的所有权转让应该如下所示发生:

  • 当前所有者使用新的所有者地址作为其唯一的显式参数调用 transition RequestOwnershipTransfer

  • 新所有者调用 ConfirmOwnershipTransfer

在所有权转换完成之前,当前所有者可以通过使用自己的地址调用 RequestOwnershipTransfer 来中止它。 可以添加(冗余)专用 transition,使其对合约所有者和用户更加明显。