What is a Smart Contract?
A smart contract is no different than a script or predicate in that it is a piece of bytecode that is deployed to the blockchain via a transaction. The main features of a smart contract that differentiate it from scripts or predicates are that it is callable and stateful. Put another way, a smart contract is analogous to a deployed API with some database state. The interface of a smart contract, also just called a contract, must be defined strictly with an ABI declaration. See this contract for an example.
Syntax of a Smart Contract
As with any Sway program, the program starts with a declaration of what program type it is. A contract must also either define or import an ABI declaration and implement it. It is considered good practice to define your ABI in a separate library and import it into your contract. This allows callers of your contract to simply import the ABI directly and use it in their scripts to call your contract. Let's take a look at an ABI declaration in a library:
library wallet_abi;
abi Wallet {
#[storage(read, write)]
fn receive_funds();
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address);
}
Let's focus on the ABI declaration and inspect it line-by-line.
The ABI Declaration
abi Wallet {
#[storage(read, write)]
fn receive_funds();
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address);
}
In the first line, abi Wallet {
, we declare the name of this Application Binary Interface, or ABI. We are naming this ABI Wallet
. To import this ABI into either a script for calling or a contract for implementing, you would use
use wallet_abi::Wallet;
In the second line,
#[storage(read, write)]
fn receive_funds();
we are declaring an ABI method called receive_funds
which, when called, should receive funds into this wallet. Note that we are simply defining an interface here, so there is no function body or implementation of the function. We only need to define the interface itself. In this way, ABI declarations are similar to trait declarations. This particular ABI method does not take any parameters.
In the third line,
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address);
we are declaring another ABI method, this time called send_funds
. It takes two parameters: the amount to send, and the address to send the funds to.
Note: The ABI methods
receive_funds
andsend_funds
also require the annotation#[storage(read, write)]
because their implementations require reading and writing a storage variable that keeps track of the wallet balance, as we will see shortly. Refer to Purity for more information on storage annotations.
Implementing an ABI for a Smart Contract
Now that we've discussed how to define the interface, let's discuss how to use it. We will start by implementing the above ABI for a specific contract.
Implementing an ABI for a contract is accomplished with impl <ABI name> for Contract
syntax. The for Contract
syntax can only be used to implement an ABI for a contract; implementing methods for a struct should use impl Foo
syntax.
impl Wallet for Contract {
#[storage(read, write)]
fn receive_funds() {
if msg_asset_id() == BASE_ASSET_ID {
// If we received `BASE_ASSET_ID` then keep track of the balance.
// Otherwise, we're receiving other native assets and don't care
// about our balance of tokens.
storage.balance += msg_amount();
}
}
#[storage(read, write)]
fn send_funds(amount_to_send: u64, recipient_address: Address) {
// Note: The return type of `msg_sender()` can be inferred by the
// compiler. It is shown here for explicitness.
let sender: Result<Identity, AuthError> = msg_sender();
match sender.unwrap() {
Identity::Address(addr) => assert(addr == OWNER_ADDRESS),
_ => revert(0),
};
let current_balance = storage.balance;
assert(current_balance >= amount_to_send);
storage.balance = current_balance - amount_to_send;
// Note: `transfer_to_address()` is not a call and thus not an
// interaction. Regardless, this code conforms to
// checks-effects-interactions to avoid re-entrancy.
transfer_to_address(amount_to_send, BASE_ASSET_ID, recipient_address);
}
}
You may notice once again the similarities between traits and ABIs. And, indeed, as a bonus, you can specify methods in addition to the interface surface of an ABI, just like a trait. By implementing the methods in the interface surface, you get the extra method implementations For Freeā¢.
Note that the above implementation of the ABI follows the Checks, Effects, Interactions pattern.
Calling a Smart Contract from a Script
Note: In most cases, calling a contract should be done from the Rust SDK or the TypeScript SDK which provide a more ergonomic UI for interacting with a contract. However, there are situations where manually writing a script to call a contract is required.
Now that we have defined our interface and implemented it for our contract, we need to know how to actually call our contract. Let's take a look at a contract call:
script;
use std::constants::ZERO_B256;
use wallet_abi::Wallet;
fn main() {
let contract_address = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b;
let caller = abi(Wallet, contract_address);
let amount_to_send = 200;
let recipient_address = ~Address::from(0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b);
caller.send_funds {
gas: 10000,
coins: 0,
asset_id: ZERO_B256,
}(amount_to_send, recipient_address);
}
The main new concept is the abi cast: abi(AbiName, contract_address)
. This returns a ContractCaller
type which can be used to call contracts. The methods of the ABI become the methods available on this contract caller: send_funds
and receive_funds
. We then directly call the contract ABI method as if it was just a regular method. You also have the option of specifying the following special parameters inside curly braces right before the main list of parameters:
gas
: au64
that represents the gas being forwarded to the contract when it is called.coins
: au64
that represents how many coins are being forwarded with this call.asset_id
: ab256
that represents the ID of the asset type of the coins being forwarded.
Each special parameter is optional and assumes a default value when skipped:
- The default value for
gas
is the context gas (i.e. the content of the special register$cgas
). Refer to the FuelVM specifications for more information about context gas. - The default value for
coins
is 0. - The default value for
asset_id
isZERO_B256
.