5.7 调用合约(Calling Contracts)

Smart contracts can be called by other contracts or scripts. In the FuelVM, this is done primarily with the call instruction.

智能合约可以被其他合约或脚本调用。在FuelVM中,这主要是通过call指令完成的。

Sway provides a nice way to manage callable interfaces with its abi system. The Fuel ABI specification can be found here.

Sway提供了一个很好的方法来管理其abi系统的可调用接口。Fuel ABI规范可以在这里找到。

示例(Example)

Here is an example of a contract calling another contract in Sway. A script can call a contract in the same way.

下面是一个合约调用另一个合约的例子,在Sway中。脚本也可以用同样的方式调用一个合约。

// ./contract_a.sw
contract;

abi ContractA {
    fn receive(field_1: bool, field_2: u64) -> u64;
}

impl ContractA for Contract {
    fn receive(field_1: bool, field_2: u64) -> u64 {
        assert(field_1 == true);
        assert(field_2 > 0);
        return_45()
    }
}

fn return_45() -> u64 {
  45
}
// ./contract_b.sw
contract;

use contract_a::ContractA;

abi ContractB {
    fn make_call();
}

const contract_id = 0x79fa8779bed2f36c3581d01c79df8da45eee09fac1fd76a5a656e16326317ef0;

impl ContractB for Contract {
    fn make_call() {
      let x = abi(ContractA, contract_id);
      let return_value = x.receive(true, 3); // will be 45
    }
}

Note: The ABI is for external calls only therefore you cannot define a method in the ABI and call it in the same contract. If you want to define a function for a contract, but keep it private so that only your contract can call it, you can define it outside of the impl and call it inside the contract, similar to the return_45() function above. 注意: ABI只适用于外部调用,因此你不能在ABI中定义一个方法并在同一个合约中调用它。如果你想为一个合约定义一个函数,但要保持它的私密性,以便只有你的合约可以调用它,你可以在impl之外定义它,并在合约中调用它,类似于上面的return_45()函数。

高级调用 (Advanced Calls)

All calls forward a gas stipend, and may additionally forward one native asset with the call.

所有的调用都会转发一个gas津贴,并且可以额外转发一个本地资产的调用。

Here is an example of how to specify the amount of gas (gas), the asset ID of the native asset (asset_id), and the amount of the native asset (coins) to forward:

下面是一个例子,说明如何指定气体的数量(gas),本地资产的资产ID(asset_id),以及要转发的本地资产的数量(coins):

script;

abi MyContract {
    fn foo(field_1: bool, field_2: u64);
}

fn main() {
    let x = abi(MyContract, 0x79fa8779bed2f36c3581d01c79df8da45eee09fac1fd76a5a656e16326317ef0);
    let asset_id = 0x7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777_7777;
    x.foo {
        gas: 5000, asset_id: asset_id, coins: 5000
    }
    (true, 3);
}

处理重入问题 (Handling Re-entrancy)

A common attack vector for smart contracts is re-entrancy. Similar to the EVM, the FuelVM allows for re-entrancy.

智能合约的一个常见攻击媒介是重入。与EVM类似,FuelVM允许重入。

A stateless re-entrancy guard is included in the sway-libs library. The guard will panic (revert) at run time if re-entrancy is detected.

sway-libs库中包含了一个_无状态_ 重入防护。如果检测到重入,该防护措施将在运行时恐慌(恢复)。

contract;

use reentrancy::reentrancy_guard;

abi MyContract {
    fn some_method();
}

impl ContractB for Contract {
    fn some_method() {
        reentrancy_guard();
        // do something
    }
}

CEI模式违反静态分析 (CEI pattern violation static analysis)

Another way of avoiding re-entrancy-related attacks is to follow the so-called CEI pattern. CEI stands for "Checks, Effects, Interactions", meaning that the contract code should first perform safety checks, also known as "pre-conditions", then perform effects, i.e. modify or read the contract storage and execute external contract calls (interaction) only at the very end of the function/method.

另一种避免重入相关攻击的方法是遵循所谓的 CEI 模式。CEI是 "检查、影响、交互 "的缩写,意思是说,合约代码应该首先执行安全检查,也称为 "前置条件",然后执行影响,即修改或读取合约存储,并且只在函数/方法的最末端执行外部合约调用(交互)。

Please see this blog post for more detail on some vulnerabilities in case of storage modification after interaction and this blog post for more information on storage reads after interaction.

请看这篇博文,了解在交互后修改存储的情况下的一些漏洞,以及这篇博文,了解关于交互后读取存储的更多信息。

The Sway compiler implements a check that the CEI pattern is not violated in the user contract and issues warnings if that's the case.

Sway编译器实现了对CEI模式在用户合约中是否被违反的检查,如果是这样就会发出警告。

For example, in the following contract the CEI pattern is violated, because an external contract call is executed before a storage write.

例如,在下面的合约中,CEI模式被违反了,因为一个外部合约调用在存储写入之前被执行。

contract;

mod other_contract;

use other_contract::*;

use std::auth::msg_sender;

abi MyContract {
    #[storage(read, write)]
    fn withdraw(external_contract_id: ContractId);
}

storage {
    balances: StorageMap<Identity, u64> = StorageMap {},
}

impl MyContract for Contract {
    #[storage(read, write)]
    fn withdraw(external_contract_id: ContractId) {
        let sender = msg_sender().unwrap();
        let bal = storage.balances.get(sender).try_read().unwrap_or(0);

        assert(bal > 0);

        // External call
        let caller = abi(OtherContract, external_contract_id.into());
        caller.external_call { coins: bal }();

        // Storage update _after_ external call
        storage.balances.insert(sender, 0);
    }
}

Here, other_contract is defined as follows:

library;

abi OtherContract {
    #[payable]
    fn external_call();
}

The CEI pattern analyzer issues a warning as follows, pointing to the interaction before a storage modification:

CEI模式分析器发出如下警告,指向存储修改前的交互:

warning
  --> /path/to/contract/main.sw:28:9
   |
26 |
27 |           let caller = abi(OtherContract, external_contract_id.into());
28 |           caller.external_call { coins: bal }();
   |  _________-
29 | |
30 | |         // Storage update _after_ external call
31 | |         storage.balances.insert(sender, 0);
   | |__________________________________________- Storage write after external contract interaction in function or method "withdraw". Consider making all storage writes before calling another contract
32 |       }
33 |   }
   |
____

In case there is a storage read after an interaction, the CEI analyzer will issue a similar warning.

如果在交互之后有一个存储读取,CEI分析器将发出类似的警告。

In addition to storage reads and writes after an interaction, the CEI analyzer reports analogous warnings about:

除了交互后的存储读和写,CEI分析器还报告类似的警告:

  • balance tree updates, i.e. balance tree reads with subsequent writes, which may be produced by the tr and tro asm instructions or library functions using them under the hood; 平衡树更新,即平衡树的读和随后的写,这可能是由trtro asm指令或,在当时条件下下使用它们的库函数产生的;

  • balance trees reads with bal instruction; 用bal指令读取平衡树;

  • changes to the output messages that can be produced by the __smo intrinsic function or the smo asm instruction. 对输出信息的改变,可以由__smo内在函数或smo asm指令产生。

与EVM的不同之处 (Differences from the EVM)

While the Fuel contract calling paradigm is similar to the EVM's (using an ABI, forwarding gas and data), it differs in two key ways:

虽然Fuel合约的调用范式与EVM的相似(使用ABI,转发气体和数据),但它在_两个关键方面有所不同:

  1. Native assets: FuelVM calls can forward any native asset not just base asset. 本地资产: FuelVM调用可以转发任何本地资产,而不仅仅是基础资产。

  2. No data serialization: Contract calls in the FuelVM do not need to serialize data to pass it between contracts; instead they simply pass a pointer to the data. This is because the FuelVM has a shared global memory which all call frames can read from. 无数据序列化: FuelVM中的合约调用不需要将数据序列化以在合约之间传递;相反,他们只是传递一个数据指针。这是因为FuelVM有一个共享的全局内存,所有的调用框架都可以从中读取。

Last updated