Table of Contents
- Advanced Solidity Concepts
- Advanced Rust Concepts
- Writing Safe and Efficient Rust Code for Smart Contracts
- Solidity to Soroban: Common Concepts and Best Practices
Advanced Solidity Concepts
Inheritance
In Solidity, smart contracts can inherit properties and functions from other contracts. This is achieved using theis keyword.
Here is an example of a parent contract that defines a function called messageFromParent that returns a string:
Child contract inherits the messageFromParent function from the Parent contract. The Child contract can then call the messageFromParent function directly.
Interfaces
Interfaces are similar to contracts, but they cannot have any function implementations. They only contain function signatures. Contracts can implement interfaces using theis keyword, similar to inheritance.
Here is an example of an interface that defines a function called doSomething that returns a uint256:
SomeContract contract implements the SomeInterface interface. Its implementation returns a u256 that is incremented each time the doSomething function is called.
Libraries
Libraries are similar to contracts, but they cannot have any state variables. They are used to store reusable code that can be used by other contracts. Libraries are deployed once and can be used by multiple contracts. They are defined using thelibrary keyword. They are invoked by using the using keyword.
SafeMath library is used in the increment function. The increment function uses the add function from the SafeMath library to increment the value variable.
Modifiers
Modifiers are used to change the behavior of functions in a declarative way. They are defined using themodifier keyword. Modifiers can be used to perform common checks such as validating inputs, checking permissions, and more.
onlyOwner modifier is used to restrict access to the doSomething function. The doSomething function can only be called by the owner of the contract which was defined during deployment as msg.sender.
Advanced Rust Concepts
Crates
A crate in Rust is a collection of precompiled programs, scripts, or routines that can be easily reused by programmers when writing code. This allows them to avoid reinventing the wheel by not having to implement the same logic or program multiple times. There are two types of crates in Rust: Binary crates and Library crates. Binary crates are crates that can be executed as standalone programs. Library crates are crates that are meant to be used by other programs. Library crates can be imported into other programs using theuse keyword.
Here is an example of a workflow that implements allocation (alloc) logic within a smart contract:
First a user would include the alloc crate in their Cargo.toml file:
alloc crate into their smart contract:
alloc crate is imported into the smart contract using the extern crate alloc; statement. The alloc crate is then used to create a temporary vector that holds values from 0 to count. The values in the vector are then summed and returned.
For more details on how to use the alloc crate, including a hands-on practical exercise, visit the alloc example contract.
Inheriting Functionality from Other Crates
We can illustrate another example of inheritance by importing functionality into a crate from other crates in the same project in the following example: Below is a function from theevent.rs file from our Token example.
admin.rs file.
Result object that contains the administrator’s address.
Lastly, we’ll implement a function from our balance.rs file.
event.rs is imported into the contract.rs file which holds the logic for our token contract.
event.rs, admin.rs, and balance.rs files are imported into the contract.rs file using the use keyword. This allows us to use the functions from those files in the mint function of our contract.rs file.
Inheriting Functionality using contractimport!
The Soroban Rust SDK provides a powerful macro, contractimport, which allows a user to import a contract from its Wasm file, generating a client, types, and constant holding the contract file.
Here is an example of how to use the contractimport macro taken from the token.rs file from our Liquidity Pool example:
First, we see that the wasm file from the previously built token example is imported into the token.rs file using the contractimport macro:
lib.rs file and a Client is generated for us to access functionality from the token contract:
contractimport to interact with the token file via a Client that was generated for the token module. This Client was created using a Contract trait that matches the interface of the contract, a ContractClient struct that contains functions for each function in the contract, and types for all contract types defined in the contract.
A Note on Inheritance and Composability
While we’ve been using the term “inheritance” to help make the transition from Solidity smoother, let’s clarify an important aspect of Rust: it does not support inheritance as we traditionally understand it. Instead, Rust practices “composability”, meaning it uses functions from different crates, which are akin to packages, in a modular fashion. So, when we discusscontractimport!, we’re actually observing composability in action, not “inheritance”. Rust does not foster the “is a” relationship inherent in OOP languages. Instead, it enables us to reuse and assemble code effectively across different scopes. This is a technical truth that is important to understand; however, it’s worth noting that this fact doesn’t impact the practical usage of Soroban throughout this guide.
Modules
In Rust, modules consist of a cohesive set of related functions and types that are often organized together for better organization and reusability. These modules can be reused across multiple projects by publishing them as crates. Here is an example of a module that implementsSafeMath logic with an add function:
checked_add function from the standard library to ensure that the addition does not overflow. This is important because if the addition overflows, it could lead to unexpected behavior in the contract.
Even when Rust code is compiled with the #![no_std] flag, it is still possible to use some of the standard library’s features, such as the checked_add function. This is because Rust provides the option to selectively import modules and functions from the standard library, allowing developers to use only the specific features they need.
Traits
Rust does not have a built-in modifier system like Solidity. However, you can achieve similar functionality usingtraits and their implementations.
In the example below, we will illustrate the inheritance of traits using the Ownable trait.
- First, we define the
Ownabletrait, which defines a single method calledis_owner. This method takes anAddressas an argument and returns a boolean value indicating whether or not the address is the owner of the contract. - Next, we implement the
Ownabletrait for theOwnableContractstruct. This allows us to use theis_ownermethod on instances of theOwnableContractstruct. - Then, we define a “modifier” called
only_ownerthat takes an instance of theOwnableContractstruct and anAddressas arguments. This “modifier” returns a boolean value indicating whether or not the address is the owner of the contract. - Finally, we implement the contract for the
OwnableContractstruct. This allows only theownerof the contract to use thechange_numbermethod on instances of theOwnableContractstruct.
require_auth method provided by the Address struct.
Interfaces
Interfaces are an essential part of building smart contracts with Soroban. There are many types of smart contract interfaces, and each has a specific purpose. One example of an interface built with Soroban is the Token Interface. This interface ensures that tokens deployed on Soroban are interoperable with Soroban’s built-in tokens (such as the Stellar Asset Contract). The Token Interface consists of three compatibility requirements:function interfaceauthorizationevents
Writing Safe and Efficient Rust Code for Smart Contracts
When writing Rust code for smart contracts, it’s important to focus on safety and efficiency. Some tips include:- Use the
Resulttype to handle errors in a safe and predictable way. In smart contracts, it’s important to avoid panicking, as this can lead to unpredictable behavior. Instead, Result can be used to handle errors and ensure that the contract behaves as expected.
add function returns a Result type, which can either be Ok or Err. If the addition does not overflow, the function returns Ok, otherwise it returns Err.
- Use the
checked_family of functions, such aschecked_add,checked_sub, etc., to perform arithmetic operations in a safe and efficient manner. These functions check for overflows and underflows and return an error if one occurs.
add function uses the checked_add function to perform the addition. If the addition overflows, the function returns an error.
- Use
cargoandclippyto enforce code quality, style, and efficiency in Rust.cargois Rust’s package manager and provides a number of tools for building and testing Rust code.clippyis a linter that can help identify potential issues in the code, such as unused variables or functions that could be optimized.
- Use
cargoandrustfmtto enforce code style.rustfmtis a tool that can automatically format Rust code according to the Rust style guide. This can help ensure that the code is consistent and easy to read.
Solidity to Soroban: Common Concepts and Best Practices
In this section we will explore key Solidity concepts and provide their Soroban equivalents. We will discuss the following topics:- Message Properties
- Error Handling
- Address-related functionality
- Function visibility specifiers
- Time-based variables
Message Properties
The Soroban Rust SDK and Solidity provide a number of message properties that can be used to access information about the current transaction. These properties include:Solidity
msg.sender: The address of the account that sent the transaction.msg.value: The amount of Ether sent with the transaction.msg.data: The data sent with the transaction.
Soroban
In contrast to Solidity’s global variables, Soroban relies on passing anEnv argument to all functions which provides access to the environment the contract is executing within.
The Env provides access to information about the currently executing contract, who invoked it, contract data, functions for signing, hashing, etc.
For instance, you would use env.storage().persistent().get(key) to access a persistent target value from the contract’s storage. Read more about the different storage types here.
env.storage()is used to get a struct for accessing and updating contract data that has been stored.- Used as
env.storage().persistent().get()orenv.persistent().storage().set(). - Additionally, we utilize the
clone()method, a prevalent trait in Rust that allows for the explicit duplication of an object.
env.storage() and clone()
env.storage().persistent().set()
env.storage().persistent().get()
clone()
Error Handling
The Soroban Rust SDK and Solidity provide a number of ways to handle errors. These include:Solidity
Solidity provides arequire function that can be used to check for certain conditions and revert the transaction if they are not met. For example, the following code sets a minimum value for the amount of Ether sent with the transaction:
Soroban
The panic! macro serves as Rust’s error-handling mechanism, which closely resembles therequire function in Solidity.
Address-Related Functionality
Both Soroban and Solidity provide provide a number of functions for working with addresses. These functions include:Solidity
address(this): Returns the address of the current contract.address payable(this): Returns the address of the current contract as a payable address.address(address): Returns the address of the specified account.address payable(address): Returns the address of the specified account as a payable address.
Soroban
e.current_contract_address(): Returns the Address object corresponding to the current executing contract.
Env not only provides essential information about the currently executing contract and its invoker, but also offers access to contract data and functions for signing, hashing, and more. The construction or conversion of most types in Soroban requires access to an Env instance.
Here is an example of a smart contract that illustrates how contracts can be retrieved:
Why Soroban Differs
Soroban has some differences from Solidity in terms of addresses and other functionalities. These differences arise due to the design principles and goals of Soroban. One significant difference is the use of theEnv object in Soroban. The Env object encapsulates various functionalities related to contract execution, data access, and more. It provides a unified interface for interacting with the Soroban environment within the context of a contract. By utilizing the Env object, Soroban enables a more modular and flexible approach to contract development.
To further explain, the Env type offers a gateway to the environment where the contract operates. It provides information about the ongoing contract, the entity invoking it, contract data, and functions for signing, hashing, and so forth. Most types demand access to an Env for their construction or conversion.
Meanwhile, the Address object serves as a potent tool for authentication and authorization. For instance, it can be used to authorize token transfers, acting as a security gatekeeper within the system. This feature amplifies the functionality of addresses in Soroban, making them not just a means of identification or storage, but also a key player in verifying and authorizing transactions.
Function Visibility Specifiers
The Soroban Rust SDK and Solidity provide a number of function visibility specifiers that can be used to control who can call a function. These specifiers include:Solidity
public: Anyone can call the function.external: Only other contracts can call the function.internal: Only the current contract and contracts that inherit from it can call the function.private: Only the current contract can call the function.
Soroban
pub: The item (function, struct, etc.) is accessible from any module or scope.pub(crate): The item is accessible only within the current crate.pub(super): The item is accessible only within its parent module.pub(in path::to::module): The item is accessible only within the specified module path.private: The item is not marked as pub and is therefore private to its own module, meaning it can only be accessed within the same module and is not accessible from outside the module.
Time-Based Variables
The Soroban Rust SDK and Solidity provide a number of time-based variables that can be used to access information about the current block(EVM) or ledger(Soroban). These variables include:Solidity
block.timestamp: The timestamp of the current block.block.number: The number of the current block.
Soroban
env.ledger().timestamp(): Returns a unix timestamp for when the most recent ledger was closed.env.ledger().sequence(): Returns the sequence number of the most recently closed ledger.
Authorization
Soroban differs from Solidity in its approach to authorization and modifiers. While Solidity has a built-in modifier system, Sorotban does not. Instead, Soroban leverages traits, their implementations, and core features to achieve similar functionality.Solidity
In Solidity, the ERC20 token standard includes theapprove function, which allows a token holder to authorize another address to spend a certain amount of tokens on their behalf. This function is commonly used in decentralized exchanges and other token transfer scenarios. Furthermore, we’re ensuring that the spender is authorized to spend the amount of tokens requested by the token holder.
Here’s an example of how the approve function acts as an authorization mechanism in Solidity:
Soroban
Soroban’s design principles prioritize flexibility, security, and testability, which have led to differences in how authorization is handled compared to Solidity. Soroban provides built-in functions such asrequire_auth and require_auth_for_args through the Address struct. These functions help enforce authorization rules within contracts. During on-chain execution, the Soroban host performs the necessary authentication, including verifying signatures and ensuring replay prevention. This alleviates the burden of authentication from the contracts themselves, promoting security and reducing potential vulnerabilities.
Here is an example of a smart contract that illustrates how authorization is handled in Soroban:
transfer and transfer2, both of which involve authorization checks.
Inside the Transfer function, the require_auth method is invoked on the address object. This method ensures that the caller of the contract has the necessary authorization to execute the transfer.
The transfer2 function follows a similar pattern but uses the require_auth_for_args method instead. It takes the same parameters as transfer but provides a tuple (amount / 2,) as the argument to require_auth_for_args. This method verifies that the caller has authorized the contract invocation with the specific arguments.
By utilizing these authorization methods provided by the Address object from the Soroban Rust SDK, the contract enforces that only authorized callers can perform the transfers. This approach enhances the security of the contract by ensuring that sensitive operations can only be executed by authorized parties.
Soroban’s approach to authorization in this example offers several advantages over Solidity’s model of ERC20 by eliminating the need for separate approval management. Instead, authorization checks can be directly incorporated into any Soroban function. This simplifies the contract codebase and reduces the complexity associated with managing separate approval states.
Soroban authorization provides Contract-level Authorization, Account Abstraction Functionality, and more advanced Authorization checks. To learn more about these advantages, visit the Authorization section of the documentation.
Summary
Overall the Soroban equivalents of Solidity concepts are very similar. However, there are notable differences worth highlighting. Soroban usesenv instead of msg to access information about the entire contract execution environment, including the state of the contract, addresses involved, and more. Authorization is also handled differently in Soroban, as it is built into the core functionality of the SDK and is more robust than relying on smart contract code alone.
For more information on Solidity concepts and their Soroban equivalents, it is recommended that one refer to both the Soroban Rust SDK documentation and the Solidity documentation.