Sway is a new smart contract language for the Fuel blockchain, inspired by Rust. For those familiar with Solidity, getting started with Sway is relatively easy, as many concepts in Sway also exist in Solidity, and the syntax is quite similar with Rust.
Contracts are similar to Scripts or Predicates at the language level. The main feature that distinguishes smart contracts from scripts or predicates is that they are callable and stateful.
Libraries in Sway are files used to define new common behaviors. They are defined using the library
keyword at the beginning of a file:
library;
// library code
A script is runnable bytecode on the chain that executes once to perform a specific task. It does not represent ownership of any resources and cannot be called by a contract. A script can return a single value of any type.
Scripts are state-aware in that, while they have no persistent storage (as they only exist during the transaction), they can call contracts and act based on the returned values and results.
script;
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: b256::zero(),
}(amount_to_send, recipient_address);
}
From the perspective of Sway, predicates are programs that return a Boolean value and represent ownership of some resource when executed as true. They have no access to contract storage.
Here is an example:
predicate;
// All predicates require a main function that returns a Boolean value.
fn main() -> bool {
true
}
The address of this predicate is 0xd19a5fe4cb9baf41ad9813f1a6fef551107c8e8e3f499a6e32bccbb954a74764
. Any assets sent to this address can be unlocked or claimed by executing the predicate above, as it always evaluates to true.
It does not need to be deployed to a blockchain because it only exists during a transaction. However, the predicate address is on-chain as the owner of one or more UTXOs.
msg.sender
, but there is a msg_sender()
function, which is similar to msg.sender
and is part of the Sway standard library.storage
, which is similar to a global storage module. All variables that need to be stored persistently in the contract can be defined here, as shown below: storage {
total_supply: u64 = 0,
owner: State = State::Uninitialized,
}
public
, private
, or other access controls, but they do have the concept of Purity. Functions are pure by default but can opt into impurity via the storage
function attribute. The storage
attribute may take read
and/or write
arguments indicating which type of access the function requires #[storage(read)]
fn get_amount() -> u64 {
...
}
#[storage(read, write)]
fn increment_amount(increment: u64) -> u64 {
...
}
fn a_pure_function() {
...
}
#[storage()]
fn also_a_pure_function() {
...
}
If you are familiar with Rust, you will recognize that this feature is borrowed from Rust. In Solidity, there is no such fine-grained control mechanism for storage operations in the EVM, which is one reason many attacks can succeed. Imagine if there were such fine-grained control over storage; even if there were a vulnerability in the contract, if the storage could not be written to, the attack would not succeed.
Variables in Sway are immutable by default. This means that, by default, once a variable is declared, its value cannot change. This is one of the ways Sway encourages safety.
let mut foo = 5;
foo = 6;
#[test]
fn test_test_function() {
// Deploy the contract
let contract_id = abi(MyContract, CONTRACT_ID);
// Test case 1: 2 + 3 should equal 5
let result1 = contract_id.test_function(2, 3);
assert(result1 == 5);
// Test case 2: 0 + 0 should equal 0
let result2 = contract_id.test_function(0, 0);
assert(result2 == 0);
// Test case 3: 100 + 200 should equal 300
let result3 = contract_id.test_function(0xffffffff, 0);
log(result3);
// assert(result3 == 300);
}
When running the code, it will return an error:
Bytecode size: 4712 bytes
Running 1 test, filtered 0 tests
test test_test_function ... FAILED (482.315µs, 6149 gas)
failures:
test test_test_function, "ccc/src/main.sw":16
revert code: 0
Logs: []
Result: FAILED. 0 passed. 1 failed. Finished in 482.315µs.
error: Some tests failed.
Delegate call is a low-level function in Solidity that allows a contract to execute code from another contract while keeping the caller’s context. This feature can be useful for code reuse and contract composition, but it can also introduce security risks, as seen in the Parity Wallet Hack. In Sway, there is no delegate call opcode, fundamentally preventing such vulnerabilities.
The self-destruct function in Solidity can be used to attack smart contracts if not used correctly. Attackers often use self-destruct to destroy malicious contracts after an attack. In Sway, there is no self-destruct instruction, eliminating this security hazard.
Reentrancy has led to billions of dollars lost in Solidity, as seen in the famous DAO attack and many DeFi projects. Sway contracts can also be vulnerable to reentrancy attacks. Developers need to be vigilant. To prevent and mitigate reentrancy attacks, Fuel uses an anti-reentrancy library, but it should be noted that it still cannot mitigate cross-contract reentrancy vulnerabilities. More information can be found here.
Sway’s official anti-reentry libriary is:
use sway_libs::reentrancy::;
Demo:
use sway_libs::reentrancy::reentrancy_guard;
abi MyContract {
fn my_non_reentrant_function();
}
impl MyContract for Contract {
fn my_non_reentrant_function() {
reentrancy_guard();
// my code here
}
}
According to our analysis, Sway contracts support flash loans, which inherently carries the risk of flash loan attacks.
Like Solidity, Sway can also have business logic bugs, such as uninitialized storage slots and denial of service (DoS) vulnerabilities etc.
Unlike the EVM, the contract address in FuelVM is not bound to a token. One contract address may have multiple assets, increasing security risks. When users transfer funds, they need to pay attention not only to the contract address but also to ensure the asset ID is correct.
In FuelVM, the Asset ID determines the uniqueness of the asset, and a contract can have multiple assets, which is very different from the EVM. An attacker can create the same Asset ID through brute force:
let my_contract_id: ContractId = ContractId::from(0x1000000000000000000000000000000000000000000000000000000000000000);
let my_sub_id: SubId = 0x2000000000000000000000000000000000000000000000000000000000000000;
let asset_id: AssetId = AssetId::new(my_contract_id, my_sub_id);
The Asset ID is determined by the contract ID and sub ID, allowing an attacker to create a malicious contract and brute force the sub ID to match the target Asset ID. If the victim only sees that the Asset ID is the same and does not notice the inconsistent contract ID, they may be deceived.
In general, Sway is much more secure than Solidity, eliminating many language-level security issues. However, vulnerabilities such as reentrancy, flash loan attacks, and other security concerns still exist. Due to the characteristics of Sway, it also introduces some new security risks that require developers to be particularly vigilant.
In short, Sway is an excellent and highly secure contract language that deserves recognition. As a leading Web3 security company, ExVul will continue to research the security of the Sway language. Based on our in-depth research, we are pleased to announce the launch of our Sway contract code audit service to provide our leading security audit capabilities for the Fuel ecosystem and protect the security of developers and user assets!