基本概念,substrate 合约与以太坊合约的一些联系与区别,上传合约代码 put_code 和实例化合约 instantiate 两个外部接口的实现。ChainX团队的这篇文章已经阐述过了: https://www.jianshu.com/p/ad1320ef9904
本文将介绍合约模块的第三个外部接口合约调用 call 的基本逻辑,并且会详细介绍下 substrate 关于合约收费的设计。
call :调用合约
首先是调用了账户查找lookup,返回给dest的是AccountId。
接着主要是调用了bare_call这个函数,和instantiate一样走得是execute_wasm。ctx.call实际上调用的是 ExecutionContext::call。

在
ExecutionContext::call中,首先判断了调用深度,然后收取调用合约的gas费用,接着调用pay_rent收取存储空间的租用费用。代码在 srml/contracts/src/rent.rs,try_evict和pay_rent都是走try_evict_or_and_pay_rent。该函数涉及费用计算,因此在合约收费中介绍。支付rent费用后,若合约状态变更为tomestone,则直接返回错误信息。
如果value不为0的话,会调用transfer将value传给dest账户(不一定是合约账户 )。
然后调用nested.overlay.get_code_hash(account)得到合约账户对应的合约哈希,根据dest_code_hash执行合约。nested.loader.load_main(&dest_code_hash)会得到包含合约的调用接口call的executable:load_main调用了load_code,它会比较 schedule 的版本,之前在 put_code的最后是写入了两个存储,一个是原始代码,一个是原始代码预处理后的 prefab_module。如果当前版本大于已经预处理好的版本, 那么需要重新预处理,否则直接返回已经存储的 prefab_module。load_init 最终返回 WasmExecutable 结构体 executable。
然后与合约部署时一样,将返回的 executable放到 WasmVm 执行 execute。通过call执行完相应函数后,进行合约账户的余额检查,如果低于账户存在的最小额,将合约删除。
如果一切顺利,OverlayAccountDb 进行 commit并且将延迟操作存入nested.deferred中。注意这里还没有正式写入存储。回到最外层的 execute_wasm,如果这里执行正确,DirectAccountDb 进行 commit,这里才是真正写到存储里面。然后又是正常的返回剩余 Gas,和执行延后的 runtime调用等等。
合约收费
存储收费
设计到存储收费的主要是在srml/contracts/src/rent.rs中的try_evict_or_and_pay_rent函数。
在instantiate时,我们传入了endowment这个值,令合约账户中拥有资产。根据合约账户的balance大小,可以根据RentDepositOffset计算得到一个免费的存储空间大小。计算公式为free_storage = balance / RentDepositOffset。
而如果合约大小超过了这个免费空间,那么需要按RentByteFee收费,该值的单位为每块每字节。所以需要支付的租用费用计算公式为:
rent = effective_storage_size(超过的空间) * RentByteFee * blocks_passed(距离上一次支付)。
可见,如果一个合约资产不足以完全存储合约代码,随着时间的流逝,最终它的资产会减少到无法维持Alive状态,从而变更为Tombstone,甚至被直接删除(太久未被调用)。这是一个合理的设计,使得runtime中不会存储太多合约,存储空间得到充分利用。
在我们instantiate_contract时,默认设置rent_allowance为balance能支持的最大值。该值会随着每次支付rent而逐渐减少。所以实际上,设置rent_allowance这个值也可以限制合约需要的资产门槛。
状态为tombstone的合约,在后面可以用状态为Alive的合约retore_to进行替换。注意,当合约状态变更为tombstone时,合约数据已经被删除,所以用该方法对依赖数据的合约进行更新是不可行的。
操作的gas费用
写在开头:所有的gas费用,在操作调用一开始,就根据两者(操作时传入的gas_limit和链设定的gas_price)的乘积,从发起交易者的账户中收取了,然后存在gas_meter这个gas“管家”中。当操作完成时,剩余的gas会调用refund_unused_gas(...)进行返还。
接下来对不同的操作,进行gas费用收取的剖析:
-
instantiate:根据schedule中设定的instantiate_base_cost。impl<T: Trait> Token<T> for ExecFeeToken { type Metadata = Config<T>; #[inline] fn calculate_amount(&self, metadata: &Config<T>) -> Gas { match *self { ExecFeeToken::Call => metadata.schedule.call_base_cost, ExecFeeToken::Instantiate => metadata.schedule.instantiate_base_cost, } } } gas_meter.charge(self.config, ExecFeeToken::Instantiate)
-
call:根据schedule中设定的call_base_cost。gas_meter.charge(self.config, ExecFeeToken::Call)
-
transfer:对不同的transfer_kind有不同的收取标准。费用标准在Config中设定,编写链的时候在impl contracts::Trait for Runtime{...}时写入相应的值。impl<T: Trait> Token<T> for TransferFeeToken<BalanceOf<T>> { type Metadata = Config<T>; #[inline] fn calculate_amount(&self, metadata: &Config<T>) -> Gas { let balance_fee = match self.kind { TransferFeeKind::ContractInstantiate => metadata.contract_account_instantiate_fee, TransferFeeKind::AccountCreate => metadata.account_create_fee, TransferFeeKind::Transfer => metadata.transfer_fee, }; approx_gas_for_balance(self.gas_price, balance_fee) } } // 对不同操作进行gas收费 if gas_meter.charge(ctx.config, token).is_out_of_gas() { return Err("not enough gas to pay transfer fee"); }
| Cause | 收取标准 |
|---|---|
部署合约(Instantiate) |
contract_account_instantiate_fee |
Call时,dest账户不存在 |
account_create_fee |
Call时,合约账户存在 |
transfer_fee |
注意,Instantiate和Call时创建的账户是不同的,Instantiate创建的是合约账户,Call创建的是普通账户。
另外,在instantiate和call中,都会调用transfer。