Request an Audit
Back
  • 08/23/2024

Introduction to the Sway Language Security Audit

What is the Sway Language?

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.

Four Types of Programs in Sway

  • Contracts

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

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
  • Scripts

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);
 }
  • Predicates

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.

Differences Between EVM and FuelVM

  • In FuelVM, all assets are native, and the process for sending any native asset is the same.
  • No Token ApprovalAn advantage of native assets is that there is no need for token approvals, unlike Ether on the EVM. With millions of dollars hacked every year due to misused token approvals, this is a significant security improvement. In FuelVM, there is no approval mechanism.
  • In FuelVM, all NFTs are treated the same as any other native asset. The ERC-721 equivalent is a native asset where each asset has a supply of one. This is defined in the SRC-20: Native Asset Standard.
  • There is no concept of msg.sender, but there is a msg_sender() function, which is similar to msg.sender and is part of the Sway standard library.
  • Sway has the keyword 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,
 }
  • Functions do not have 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.

  • Variable Declaration

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;
  • No Integer Overflow
 #[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.
 ​
  • No Delegate Call

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.

  • No Self-Destruct

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.

Sway’s Security Risk Points

  • Reentrancy Vulnerability Exists

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
      }
 }
  • Is There a Flash Loan Attack?

According to our analysis, Sway contracts support flash loans, which inherently carries the risk of flash loan attacks.

  • Some solidity business logic bugs also exist in sway

Like Solidity, Sway can also have business logic bugs, such as uninitialized storage slots and denial of service (DoS) vulnerabilities etc.

New Risk Points of Sway Contracts

  • Same Contract ID and Different Asset ID

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.

  • Same Asset ID and Different Contract ID

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.

Summary

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!

References