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] := v
或 delete 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,使其对合约所有者和用户更加明显。