Calling Contracts
Smart contracts can be called by other contracts or scripts. In the FuelVM, this is done primarily with the call
instruction.
Sway provides a nice way to manage callable interfaces with its abi
system. The Fuel ABI specification can be found here.
Example
Here is an example of a contract calling another contract in Sway. A script can call a contract in the same way.
// ./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 thereturn_45()
function above.
Advanced Calls
All calls forward a gas stipend, and may additionally forward one native asset with the call.
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:
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.
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.
contract;
use reentrancy::reentrancy_guard;
abi MyContract {
fn some_method();
}
impl ContractB for Contract {
fn some_method() {
reentrancy_guard();
// do something
}
}
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.
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.
For example, in the following contract the CEI pattern is violated, because an external contract call is executed before a storage write.
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).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:
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.
In addition to storage reads and writes after an interaction, the CEI analyzer reports analogous warnings about:
- balance tree updates, i.e. balance tree reads with subsequent writes, which may be produced by the
tr
andtro
asm instructions or library functions using them under the hood; - balance trees reads with
bal
instruction; - changes to the output messages that can be produced by the
__smo
intrinsic function or thesmo
asm instruction.
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:
-
Native assets: FuelVM calls can forward any native asset not just base asset.
-
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.