Skip to main content

Advanced Smart Contract Concepts with Solidity and Rust

In this tutorial, we will cover advanced Solidity and Rust concepts such as inheritance, interfaces, libraries, and modifiers. Additionally, we will learn how to write safe and efficient Rust code for smart contracts. Finally, we will learn how to convert common Solidity concepts to Rust.

Table of Contents

  1. Advanced Solidity Concepts
  2. Advanced Rust Concepts
  3. Writing Safe and Efficient Rust Code for Smart Contracts
  4. 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 the is keyword.

Here is an example of a parent contract that defines a function called messageFromParent that returns a string:

contract Parent {
function messageFromParent() public pure returns (string memory) {
return "Hello from Parent";
}
}
contract Child is Parent {
function messageFromChild(string memory newMessage) public pure returns (string memory) {
string memory messageFromParent = messageFromParent();
return string(abi.encodePacked(messageFromParent,', ', newMessage));
}
}

In this example, the 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 the is keyword, similar to inheritance.

Here is an example of an interface that defines a function called doSomething that returns a uint256:

interface SomeInterface {
function doSomething() external returns (uint256);
}

contract SomeContract is SomeInterface {
uint256 private counter;

function doSomething() external override returns (uint256) {
counter += 1;
return counter;
}
}

In this example, the 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 the library keyword. They are invoked by using the using keyword.

library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "Addition overflow");

return c;
}
}

contract MyContract {
using SafeMath for uint256;

uint256 public value;

function increment(uint256 amount) public {
value = value.add(amount);
}
}

In this example, the 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 the modifier keyword. Modifiers can be used to perform common checks such as validating inputs, checking permissions, and more.

contract Ownable {
address public owner;

constructor() {
owner = msg.sender;
}

modifier onlyOwner() {
require(msg.sender == owner, "Caller is not the owner");
_;
}
}

contract MyContract is Ownable {
function doSomething() public onlyOwner {
// This function can only be called by the owner of the contract
}
}

In this example, the 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 the use 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:

[dependencies]
soroban-sdk = { workspace = true, features = ["alloc"] }

[dev_dependencies]
soroban-sdk = { workspace = true, features = ["testutils", "alloc"] }

Then they would import the alloc crate into their smart contract:

// Imports
#![no_std]
use soroban_sdk::{contractimpl, Env};

extern crate alloc;

#[contract]
pub struct AllocContract;

#[contractimpl]
impl AllocContract {
/// Allocates a temporary vector holding values (0..count), then computes and returns their sum.
pub fn sum(_env: Env, count: u32) -> u32 {
let mut v1 = alloc::vec![];
(0..count).for_each(|i| v1.push(i));

let mut sum = 0;
for i in v1 {
sum += i;
}

sum
}
}

In this example, the 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 section of the documentation.

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 the event.rs file from our Token example.

use soroban_sdk::{Address, Env, Symbol};
...
pub(crate) fn mint(e: &Env, admin: Address, to: Address, amount: i128) {
let topics = (symbol_short!("mint"), admin, to);
e.events().publish(topics, amount);
}

This function will publish a mint event to the blockchain with the following output:

Emit event with topics = ["mint", admin: Address, to: Address], data = [amount: i128]

We'll also use a function from our admin.rs file.

// Metering: covered by components
pub fn read_administrator(e: &Host) -> Result<Address, HostError> {
let key = DataKey::Admin;
let rv = e.get_contract_data(key.try_into_val(e)?)?;
Ok(rv.try_into_val(e)?)
}

This function returns a Result object that contains the administrator's address.

Lastly, we'll implement a function from our balance.rs file.

pub fn receive_balance(e: &Env, addr: Address, amount: i128) {
let balance = read_balance(e, addr.clone());
if !is_authorized(e, addr.clone()) {
panic!("can't receive when deauthorized");
}
write_balance(e, addr, balance + amount);
}

This function writes an amount to an address' balance.

The event.rs is imported into the contract.rs file which holds the logic for our token contract.

//contract.rs

//imports
use crate::event;
use crate::admin::{read_administrator};
use crate::balance::{receive_balance};

// trait logic
pub trait TokenTrait {
fn mint(e: Env, to: Address, amount: i128);
}

// struct logic
#[contract]
pub struct Token;

// impl logic

#[contractimpl]
impl TokenTrait for Token {
fn mint(e: Env, to: Address, amount: i128) {
check_nonnegative_amount(amount);
let admin = read_administrator(&e);
admin.require_auth();
receive_balance(&e, to.clone(), amount);
event::mint(&e, admin, to, amount);
}
}

As you can see, the 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:

//token.rs
soroban_sdk::contractimport!(
file = "../token/target/wasm32-unknown-unknown/release/soroban_token_contract.wasm"
);

We see then that our token contract is imported into our lib.rs file and a Client is generated for us to access functionality from the token contract:

//lib.rs
mod token;

fn get_balance(e: &Env, contract_id: BytesN<32>) -> i128 {
token::Client::new(e, &contract_id).balance(&e.current_contract_address())
}

fn transfer(e: &Env, contract_id: BytesN<32>, to: Address, amount: i128) {
token::Client::new(e, &contract_id).transfer(&e.current_contract_address(), &to, &amount);
}

struct LiquidityPool;

#[contractimpl]
impl LiquidityPoolTrait for LiquidityPool {
let token_a_client = token::Client::new(&e, &get_token_a(&e));
}

In the above example, we use 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 discuss contractimport!, 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 implements SafeMath logic with an add function:

#![no_std]

mod safe_math

// mod safe_math {
// pub fn add(a: u32, b: u32) -> Result<u32, &'static str> {
// a.checked_add(b).ok_or("Addition overflow")
// }
// }


// Imports
use soroban_sdk::{contractimpl, Env};
use safe_math::add;

pub trait MathContract {
fn add(&self, env: Env, a: u32, b: u32) -> u32;
}

#[contract]
pub struct Adder;

impl MathContract for Adder {
fn add(&self, _env: Env, a: u32, b: u32) -> u32 {
add(a, b).unwrap()
}
}

#[contractimpl]
impl Adder {}

// test module
#[cfg(test)]
mod test;

Notice that we use the 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 using traits and their implementations.

In the example below, we will illustrate the inheritance of traits using the Ownable trait.

#![no_std]

// Imports
use soroban_sdk::{contracttype, Address};

// Define the `Ownable` trait
trait Ownable {
fn is_owner(&self, owner: &Address) -> bool;
}

// Implement the `Ownable` trait for the `OwnableContract` struct
impl Ownable for OwnableContract {
fn is_owner(&self, owner: &Address) -> bool {
self.owner == *owner
}
}

// Define a modifier that requires the caller to be the owner of the contract
fn only_owner(contract: &OwnableContract, owner: &Address) -> bool {
contract.is_owner(owner)
}

// Implement the contract for the `OwnableContract` struct
#[contracttype]

// Define the `OwnableContract` struct
pub struct OwnableContract {
owner: Address,
number: u32,
}

impl OwnableContract {
// Define a public method that requires the caller to be the owner of the contract
pub fn change_number(&mut self, new_number: u32) {
if only_owner(self, &self.owner) {
self.number = new_number;
}
}
}

#[cfg(test)]
mod test;

Here's a breakdown of the code above:

  • First, we define the Ownable trait, which defines a single method called is_owner. This method takes an Address as an argument and returns a boolean value indicating whether or not the address is the owner of the contract.
  • Next, we implement the Ownable trait for the OwnableContract struct. This allows us to use the is_owner method on instances of the OwnableContract struct.
  • Then, we define a "modifier" called only_owner that takes an instance of the OwnableContract struct and an Address as 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 OwnableContract struct. This allows only the owner of the contract to use the change_number method on instances of the OwnableContract struct.

It's worth mentioning that the Soroban Rust SDK comes with several built-in requirements that developers can use, such as the 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 interface
  • authorization
  • events

For more information on smart contract interfaces built with Soroban, including the Token Interface, visit the tokens section of the documentation.

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 Result type 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.

In the example below, the 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.

pub fn add(a: u32, b: u32) -> Result<u32, &'static str> {
a.checked_add(b).ok_or("Addition overflow")
}
  • Use the checked_ family of functions, such as checked_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.

In the example below, the add function uses the checked_add function to perform the addition. If the addition overflows, the function returns an error.

pub fn add(a: u32, b: u32) -> u32 {
a.checked_add(b).expect("Addition overflow")
}
  • Use cargo and clippy to enforce code quality, style, and efficiency in Rust. cargo is Rust's package manager and provides a number of tools for building and testing Rust code. clippy is a linter that can help identify potential issues in the code, such as unused variables or functions that could be optimized.

To use clippy with cargo, you'll first need to install it. You can do this by running the following command in your terminal:

cargo install clippy

Once clippy is installed, you can run it by running the following command in your terminal:

cargo clippy

This will run clippy on your entire project, checking for potential issues and providing suggestions for improvement. Clippy will output any issues it finds, along with suggestions for how to fix them.

  • Use cargo and rustfmt to enforce code style. rustfmt is 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.

To use rustfmt with cargo, you'll first need to install it. You can do this by running the following command in your terminal:

cargo install rustfmt

Once rustfmt is installed, you can run it by running the following command in your terminal:

cargo fmt

Before:

fn main()
{
let x=5;
if x==5 {
println!("Hello, world!");
}
}

After:

fn main() {
let x = 5;
if x == 5 {
println!("Hello, world!");
}
}

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.

Here's a simple example of a smart contract that demonstrates the use of each

pragma solidity ^0.8.0;

contract SimpleContract {
address public sender;
uint public value;
bytes public data;

// Caller must send Ether and data to this function.
// This function will store the sender, value, and data.
function sendData(bytes calldata _data) external payable {
sender = msg.sender;
value = msg.value;
data = _data;
}
}

These are a part of Solidity's global variables, which are accessible from any function in the contract.

Soroban

In contrast to Solidity's global variables, Soroban relies on passing an Env 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() or env.persistent().storage().set().
  • Additionally, we utilize the clone() method, a prevalent trait in Rust that allows for the explicit duplication of an object.

See the example below for implementations of env.storage() and clone()

  • env.storage().persistent().set()
use soroban_sdk::{Env, Symbol};

pub fn set_storage(env: Env) {
let key = symbol_short!("key");
let value = symbol_short!("value");
env.storage().persistent().set(&key, &value);
}
  • env.storage().persistent().get()
use soroban_sdk::{Env};

pub fn get_storage(env: Env) -> value {
env.storage().persistent().get(&key);
}
  • clone()
use soroban_sdk::{Env, Address};

pub fn return_user(user: Address) -> Address {
let user_address: Address = user.clone();
user_address
}

Error Handling

The Soroban Rust SDK and Solidity provide a number of ways to handle errors. These include:

Solidity

Solidity provides a require 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:


function deposit() public payable {
require(msg.value >= 1 ether, "Not enough Ether sent");
// ...
}

Soroban

The panic! macro serves as Rust's error-handling mechanism, which closely resembles the require function in Solidity.

pub fn simple_deposit(amount: u32) {
if amount < 1_000_000 {
panic!("amount too low");
}
// ...
}

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.

Below is an example of a smart contract that illustrates how contracts can be retrieved

pragma solidity ^0.8.0;

contract SimpleContract {
address public contractAddress = address(this);
address public randomAddress = 0x1234567890123456789012345678901234567890;

address public payableAddress = payable(address(this));
address public payableRandomAddress = payable(0x1234567890123456789012345678901234567890);
}

There would be no difference in appearance between a regular address and a payable address in Solidity.

Soroban

  • e.current_contract_address(): Returns the Address object corresponding to the current executing contract.

The 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:

#![no_std]
use soroban_sdk::{contractimpl, log, Address, Env, Symbol};

#[contract]
pub struct SimpleContract;

#[contractimpl]
impl SimpleContract {
///Example contract for returning a contract Address.
pub fn return_address(env: Env) -> Address {
let current_contract_address = env.current_contract_address();
current_contract_address
}
}

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 the Env 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.

Here is an example of a smart contract that illustrates how function visibility is used in Solidity:

pragma solidity ^0.8.0;

contract SimpleContract {
function publicFunction() public {}
function externalFunction() external {}
function internalFunction() internal {}
function privateFunction() private {}
}

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.

Here is an example of a module that illustrates how function visibility is used in Soroban:

#![no_std]
use soroban_sdk::{contractimpl, Env};

mod outer {
pub struct PublicStruct {
pub field: u32,
}

pub(crate) struct CrateStruct {
pub(crate) field: u32,
}

// This struct is private because it is not marked as `pub`.
struct PrivateStruct {
field: u32,
}

mod inner {
pub(super) struct SuperStruct {
pub(super) field: u32,
}
}

pub fn get_all_fields() -> u32 {
let public_struct = PublicStruct { field: 1 };
let crate_struct = CrateStruct { field: 2 };
let private_struct = PrivateStruct { field: 3 };
let super_struct = inner::SuperStruct { field: 4 };
public_struct.field + crate_struct.field + private_struct.field + super_struct.field
}
}

#[contract]
pub struct NewContract;
trait OuterTrait {
fn get_all_fields() -> u32;
fn get_public_fields() -> u32;
}

#[contractimpl]
impl OuterTrait for NewContract {
fn get_public_fields() -> u32 {
let public_struct = outer::PublicStruct { field: 1 };
let crate_struct = outer::CrateStruct { field: 2 };
// private structs cannot be accessed from outside the crate
public_struct.field + crate_struct.field
}

fn get_all_fields() -> u32 {
outer::get_all_fields()
}
}

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.

Here is an example of a smart contract that illustrates how time-based variables are used in Solidity:

pragma solidity ^0.8.0;

contract SimpleContract {
function getTimestamp() public view returns (uint256) {
return block.timestamp;
}

function getBlockNumber() public view returns (uint256) {
return block.number;
}
}

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.

Here is an example of a smart contract that illustrates how time-based variables are used in Rust:

#![no_std]
use soroban_sdk::{contractimpl, Address, Env};

#[contract]
pub struct SimpleContract;

#[contractimpl]
impl SimpleContract {
pub fn get_timestamp(env: Env) {
let timestamp = env.ledger().timestamp();
let sequence = env.ledger().sequence();
}
}

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 the approve 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:

pragma solidity ^0.8.0;

contract ERC20Token {

constructor() {
address owner = msg.sender;
ownerAddress = owner;
}

mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
mapping(address => bool) public isAuthorized;

address public ownerAddress;

function approve(address spender, uint256 amount) public returns (bool) {
allowances[msg.sender][spender] = amount;
return true;
}

function setAuthorization(address newAuth) public returns (bool) {
require(msg.sender == ownerAddress);
isAuthorized[newAuth] = true;
return true;
}

function transfer(address to, uint256 amount) public returns (bool) {
require(allowances[msg.sender][to] >= amount, "Not enough allowance");
require(isAuthorized[to] == true, "Not authorized");
balances[msg.sender] -= amount;
balances[to] += amount;
return true;
}
}

The approve function allows the token holder to authorize spender to spend amount tokens on their behalf. The transfer function then checks if the spender is authorized to spend the amount of tokens requested by the token holder. If so, the transfer is executed.

These Solidity examples illustrate some common authorization patterns used in Ethereum smart contracts. Soroban provides alternative approaches to achieve similar functionality, leveraging core functionality derived right from the soroban SDK.

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 as require_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:

#![no_std]
use soroban_sdk::{contractimpl, testutils::Address as _, Address, Symbol, Env, IntoVal};

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
pub fn transfer(env: Env, address: Address, amount: i128) {
address.require_auth();
}
pub fn transfer2(env: Env, address: Address, amount: i128) {
address.require_auth_for_args((amount / 2,).into_val(&env));
}
}

In this example, we have a Soroban contract that includes two public functions: 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 uses env 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.