Saltar al contenido principal

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

En Solidity, los contratos inteligentes pueden heredar propiedades y funciones de otros contratos. Esto se logra utilizando la palabra clave is.

Aquí hay un ejemplo de un contrato padre que define una función llamada messageFromParent que devuelve una cadena:

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

En este ejemplo, el contrato Child hereda la función messageFromParent del contrato Parent. El contrato Child puede entonces llamar a la función messageFromParent directamente.

Interfaces

Las interfaces son similares a los contratos, pero no pueden tener implementaciones de funciones. Solo contienen firmas de funciones. Los contratos pueden implementar interfaces utilizando la palabra clave is, similar a la herencia.

Aquí hay un ejemplo de una interfaz que define una función llamada doSomething que retorna un 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;
}
}

En este ejemplo, el contrato SomeContract implementa la interfaz SomeInterface. Su implementación devuelve un u256 que se incrementa cada vez que se llama a la función doSomething.

Bibliotecas

Las bibliotecas son similares a los contratos, pero no pueden tener variables de estado. Se utilizan para almacenar código reutilizable que puede ser utilizado por otros contratos. Las bibliotecas se implementan una vez y pueden ser usadas por múltiples contratos. Se definen utilizando la palabra clave library. Se invocan utilizando la palabra clave using.

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

En este ejemplo, la biblioteca SafeMath se utiliza en la función increment. La función increment utiliza la función add de la biblioteca SafeMath para incrementar la variable value.

Modificadores

Los modificadores se utilizan para cambiar el comportamiento de las funciones de manera declarativa. Se definen utilizando la palabra clave modifier. Los modificadores pueden ser utilizados para realizar chequeos comunes tales como validar entradas, verificar permisos, y más.

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
}
}

En este ejemplo, el modificador onlyOwner se utiliza para restringir el acceso a la función doSomething. La función doSomething solo puede ser llamada por el owner del contrato que fue definido durante la implementación como msg.sender.

Conceptos Avanzados de Rust

Crates

Un crate en Rust es una colección de programas precompilados, scripts o rutinas que pueden ser fácilmente reutilizados por programadores cuando escriben código. Esto les permite evitar reinventar la rueda al no tener que implementar la misma lógica o programa múltiples veces. Hay dos tipos de crates en Rust: Crates Binarios y Crates de Biblioteca.

Los crates binarios son crates que pueden ser ejecutados como programas independientes. Los crates de biblioteca son crates que están destinados a ser utilizados por otros programas. Los crates de biblioteca pueden ser importados en otros programas usando la palabra clave use.

Aquí hay un ejemplo de un flujo de trabajo que implementa la lógica de asignación (alloc) dentro de un contrato inteligente:

Primero, un usuario incluiría el crate alloc en su archivo Cargo.toml:

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

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

Luego, importaría el crate alloc en su contrato inteligente:

// 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
}
}

En este ejemplo, el crate alloc es importado en el contrato inteligente usando la declaración extern crate alloc;. El crate alloc se utiliza para crear un vector temporal que contiene valores de 0 a count. Los valores en el vector son luego sumados y devueltos.

Para más detalles sobre cómo usar el crate alloc, incluyendo un ejercicio práctico, visita el contrato de ejemplo de alloc.

Heredando Funcionalidad de Otros Crates

Podemos ilustrar otro ejemplo de herencia al importar funcionalidad a un crate desde otros crates en el mismo proyecto en el siguiente ejemplo:

A continuación se muestra una función del archivo event.rs de nuestro ejemplo Token.

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

Esta función publicará un evento de acuñación en la blockchain con la siguiente salida:

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

También utilizaremos una función de nuestro archivo admin.rs.

// 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)?)
}

Esta función devuelve un objeto Result que contiene la dirección del administrador.

Por último, implementaremos una función de nuestro archivo balance.rs.

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

Esta función escribe un monto en el saldo de una dirección.

El event.rs es importado en el archivo contract.rs que contiene la lógica para nuestro contrato de token.

//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);
}
}

Como puedes ver, los archivos event.rs, admin.rs, y balance.rs son importados en el archivo contract.rs usando la palabra clave use. Esto nos permite usar las funciones de esos archivos en la función mint de nuestro archivo contract.rs.

Heredando Funcionalidad usando contractimport!

El SDK de Soroban Rust proporciona un poderoso macro, contractimport, que permite a un usuario importar un contrato desde su archivo Wasm, generando un cliente, tipos y constantes que contienen el archivo del contrato.

Aquí hay un ejemplo de cómo usar el macro contractimport tomado del archivo token.rs de nuestro ejemplo de Liquidity Pool:

Primero, vemos que el archivo wasm del ejemplo de token construído previamente es importado en el archivo token.rs usando el macro contractimport:

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

Luego vemos que nuestro contrato de token es importado en nuestro archivo lib.rs y se genera un Client para que podamos acceder a la funcionalidad del contrato de token:

//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));
}

En el ejemplo anterior, usamos contractimport para interactuar con el archivo de token a través de un Client que fue generado para el módulo token. Este Client fue creado usando un trait de contrato que coincide con la interfaz del contrato, una estructura ContractClient que contiene funciones para cada función en el contrato, y tipos para todos los tipos de contrato definidos en el contrato.

Una Nota sobre Herencia y Composición

Aunque hemos estado usando el término "herencia" para ayudar a hacer la transición de Solidity más suave, aclaremos un aspecto importante de Rust: no soporta la herencia como la entendemos tradicionalmente. En su lugar, Rust practica la "composición", lo que significa que utiliza funciones de diferentes crates, que son similares a paquetes, de manera modular. Así que, cuando hablamos de contractimport!, en realidad estamos observando la composición en acción, no "herencia". Rust no fomenta la relación "es un" inherente en los lenguajes OOP. En su lugar, permite reutilizar y ensamblar código de manera efectiva a través de diferentes ámbitos. Esta es una verdad técnica que es importante entender; sin embargo, vale la pena señalar que este hecho no impacta el uso práctico de Soroban a lo largo de esta guía.

Módulos

En Rust, los módulos consisten en un conjunto cohesivo de funciones relacionadas y tipos que a menudo están organizados juntos para una mejor organización y reutilización. Estos módulos pueden ser reutilizados en múltiples proyectos al publicarlos como crates.

Aquí hay un ejemplo de un módulo que implementa la lógica de SafeMath con una función add:

#![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;

Nota que usamos la función checked_add de la biblioteca estándar para asegurarnos de que la suma no se desborde. Esto es importante porque si la suma se desborda, podría llevar a un comportamiento inesperado en el contrato.

Incluso cuando el código Rust es compilado con el flag #![no_std], todavía es posible utilizar algunas características de la biblioteca estándar, como la función checked_add. Esto se debe a que Rust ofrece la opción de importar selectivamente módulos y funciones de la biblioteca estándar, lo que permite a los desarrolladores usar solo las características específicas que necesitan.

Traits

Rust no tiene un sistema de modificadores incorporado como Solidity. Sin embargo, puedes lograr una funcionalidad similar usando traits y sus implementaciones.

En el ejemplo a continuación, ilustraremos la herencia de traits usando el trait Ownable.

#![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;

Aquí hay un desglose del código anterior:

  • Primero, definimos el trait Ownable, que define un único método llamado is_owner. Este método toma un Address como argumento y devuelve un valor booleano que indica si la dirección es o no el propietario del contrato.
  • A continuación, implementamos el trait Ownable para la estructura OwnableContract. Esto nos permite usar el método is_owner en instancias de la estructura OwnableContract.
  • Luego, definimos un "modificador" llamado only_owner que toma una instancia de la estructura OwnableContract y una Address como argumentos. Este "modificador" devuelve un valor booleano que indica si la dirección es o no el propietario del contrato.
  • Finalmente, implementamos el contrato para la estructura OwnableContract. Esto permite que solo el owner del contrato use el método change_number en instancias de la estructura OwnableContract.

Es importante mencionar que el SDK de Soroban Rust viene con varios requisitos incorporados que los desarrolladores pueden usar, como el método require_auth provisto por la estructura Address.

Interfaces

Las interfaces son una parte esencial de la construcción de contratos inteligentes con Soroban.

Hay muchos tipos de interfaces de contratos inteligentes, y cada una tiene un propósito específico. Un ejemplo de una interfaz construida con Soroban es la Interfaz de Token. Esta interfaz asegura que los tokens desplegados en Soroban sean interoperables con los tokens incorporados de Soroban (como el Contrato de Activos Stellar). La Interfaz de Token consiste en tres requerimientos de compatibilidad:

  • function interface
  • authorization
  • events

Para más información sobre las interfaces de contratos inteligentes construidas con Soroban, incluyendo la Interfaz de Token, visita la sección de tokens de la documentación.

Escribir Código Seguro y Eficiente en Rust para Contratos Inteligentes

Al escribir código Rust para contratos inteligentes, es importante enfocarse en la seguridad y la eficiencia. Algunos consejos incluyen:

  • Usa el tipo Result para manejar errores de una manera segura y predecible. En los contratos inteligentes, es importante evitar pánicos, ya que esto puede llevar a comportamientos impredecibles. En su lugar, Result puede ser utilizado para manejar errores y asegurar que el contrato se comporte como se espera.

En el ejemplo a continuación, la función add regresa un tipo Result, que puede ser Ok o Err. Si la suma no desborda, la función devuelve Ok, de lo contrario, devuelve Err.

pub fn add(a: u32, b: u32) -> Result<u32, &'static str> {
a.checked_add(b).ok_or("Addition overflow")
}
  • Usa la familia de funciones checked_, como checked_add, checked_sub, etc., para realizar operaciones aritméticas de manera segura y eficiente. Estas funciones verifican si hay desbordamientos y subdesbordamientos y devuelven un error si ocurre uno.

En el ejemplo a continuación, la función add utiliza la función checked_add para realizar la suma. Si la suma se desborda, la función devuelve un error.

pub fn add(a: u32, b: u32) -> u32 {
a.checked_add(b).expect("Addition overflow")
}
  • Usa cargo y clippy para exigir la calidad, el estilo y la eficiencia del código en Rust. cargo es el administrador de paquetes de Rust y proporciona una serie de herramientas para construir y probar el código Rust. clippy es un linter que puede ayudar a identificar problemas potenciales en el código, como variables no utilizadas o funciones que podrían ser optimizadas.

Para usar clippy con cargo, primero necesitarás instalarlo. Puedes hacerlo ejecutando el siguiente comando en tu terminal:

cargo install clippy

Una vez que clippy esté instalado, puedes ejecutarlo ejecutando el siguiente comando en tu terminal:

cargo clippy

Esto ejecutará clippy en todo tu proyecto, revisando posibles problemas y proporcionando sugerencias para mejorar. Clippy mostrará cualquier problema que encuentre, junto con sugerencias sobre cómo corregirlos.

  • Usa cargo y rustfmt para exigir el estilo del código. rustfmt es una herramienta que puede formatear automáticamente el código Rust de acuerdo con la guía de estilo de Rust. Esto puede ayudar a asegurar que el código sea consistente y fácil de leer.

Para usar rustfmt con cargo, primero necesitarás instalarlo. Puedes hacerlo ejecutando el siguiente comando en tu terminal:

cargo install rustfmt

Una vez que rustfmt esté instalado, puedes ejecutarlo ejecutando el siguiente comando en tu terminal:

cargo fmt

Antes:

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

Después:

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

Solidity a Soroban: Conceptos Comunes y Prácticas Recomendadas

En esta sección exploraremos conceptos clave de Solidity y proporcionaremos sus equivalentes en Soroban. Discutiremos los siguientes temas:

  • Propiedades del Mensaje
  • Manejo de Errores
  • Funcionalidad relacionada con Direcciones
  • Especificadores de visibilidad de funciones
  • Variables basadas en tiempo

Propiedades del Mensaje

El SDK de Soroban Rust y Solidity proporcionan una serie de propiedades de mensaje que pueden ser utilizadas para acceder a información sobre la transacción actual. Estas propiedades incluyen:

Solidity

  • msg.sender: La dirección de la cuenta que envió la transacción.
  • msg.value: La cantidad de Ether enviada con la transacción.
  • msg.data: Los datos enviados con la transacción.

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;
}
}

Estas son parte de las variables globales de Solidity, que son accesibles desde cualquier función en el contrato.

Soroban

A diferencia de las variables globales de Solidity, Soroban se basa en pasar un argumento Env a todas las funciones que proporciona acceso al entorno en el que se está ejecutando el contrato.

El Env proporciona acceso a la información sobre el contrato que se está ejecutando actualmente, quién lo invocó, datos del contrato, funciones para firmar, hashear, etc.

Por ejemplo, usarías env.storage().persistent().get(key) para acceder a un valor de objetivo persistent del almacenamiento del contrato. Lee más sobre los diferentes tipos de almacenamiento aquí.

  • env.storage() se usa para obtener una estructura para acceder y actualizar datos de contrato que han sido almacenados.
  • Usado como env.storage().persistent().get() o env.persistent().storage().set().
  • Adicionalmente, utilizamos el método clone(), una característica prevalente en Rust que permite la duplicación explícita de un objeto.

Consulta el ejemplo a continuación para implementaciones de env.storage() y 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
}

Manejo de Errores

El SDK de Soroban Rust y Solidity proporcionan varias maneras de manejar errores. Estas incluyen:

Solidity

Solidity proporciona una función require que se puede utilizar para verificar ciertas condiciones y revertir la transacción si no se cumplen. Por ejemplo, el siguiente código establece un valor mínimo para la cantidad de Ether enviado con la transacción:


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

Soroban

El macro panic! actúa como el mecanismo de manejo de errores de Rust, que se asemeja estrechamente a la función require en Solidity.

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

Funcionalidad Relacionada con Direcciones

Tanto Soroban como Solidity proporcionan varias funciones para trabajar con direcciones. Estas funciones incluyen:

Solidity

  • address(this): Devuelve la dirección del contrato actual.
  • address payable(this): Devuelve la dirección del contrato actual como una dirección pagadera.
  • address(address): Devuelve la dirección de la cuenta especificada.
  • address payable(address): Devuelve la dirección de la cuenta especificada como una dirección pagadera.

A continuación se muestra un ejemplo de un contrato inteligente que ilustra cómo se pueden recuperar contratos

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

No habría diferencia en la apariencia entre una dirección normal y una dirección pagadera en Solidity.

Soroban

  • e.current_contract_address(): Devuelve el objeto Address correspondiente al contrato que se está ejecutando actualmente.

El Env no solo proporciona información esencial sobre el contrato que se está ejecutando actualmente y su invocador, sino que también ofrece acceso a datos de contratos y funciones para firmar, hash, y más. La construcción o conversión de la mayoría de los tipos en Soroban requiere acceso a una instancia de Env.

Aquí hay un ejemplo de un contrato inteligente que ilustra cómo se pueden recuperar contratos:

#![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
}
}

Por qué Soroban es Diferente

Soroban tiene algunas diferencias con Solidity en términos de direcciones y otras funcionalidades. Estas diferencias surgen debido a los principios de diseño y objetivos de Soroban.

Una diferencia significativa es el uso del objeto Env en Soroban. El objeto Env encapsula diversas funcionalidades relacionadas con la ejecución de contratos, acceso a datos, y más. Proporciona una interfaz unificada para interactuar con el entorno de Soroban dentro del contexto de un contrato. Al utilizar el objeto Env, Soroban permite un enfoque más modular y flexible para el desarrollo de contratos.

Para explicar más, el tipo Env ofrece una puerta de entrada al entorno en el que opera el contrato. Proporciona información sobre el contrato en curso, la entidad que lo invoca, los datos del contrato, y funciones para firmar, hacer hash, etc. La mayoría de los tipos demandan acceso a un Env para su construcción o conversión.

Mientras tanto, el objeto Address sirve como una herramienta potente para la autenticación y autorización. Por ejemplo, se puede usar para autorizar transferencias de tokens, actuando como un guardián de seguridad dentro del sistema. Esta característica amplifica la funcionalidad de las direcciones en Soroban, haciéndolas no solo un medio de identificación o almacenamiento, sino también un actor clave en la verificación y autorización de transacciones.

Especificadores de Visibilidad de Funciones

El SDK de Soroban Rust y Solidity proporcionan varios especificadores de visibilidad de funciones que se pueden utilizar para controlar quién puede llamar a una función. Estos especificadores incluyen:

Solidity

  • public: Cualquiera puede llamar a la función.
  • external: Solo otros contratos pueden llamar a la función.
  • internal: Solo el contrato actual y los contratos que heredan de él pueden llamar a la función.
  • private: Solo el contrato actual puede llamar a la función.

Aquí hay un ejemplo de un contrato inteligente que ilustra cómo se utiliza la visibilidad de funciones en Solidity:

pragma solidity ^0.8.0;

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

Soroban

  • pub: El ítem (función, estructura, etc.) es accesible desde cualquier módulo o ámbito.
  • pub(crate): El ítem es accesible solo dentro del crate actual.
  • pub(super): El ítem es accesible solo dentro de su módulo padre.
  • pub(in path::to::module): El ítem es accesible solo dentro de la ruta de módulo especificada.
  • private: El ítem no está marcado como pub y, por lo tanto, es privado para su propio módulo, lo que significa que solo se puede acceder dentro del mismo módulo y no es accesible desde fuera del módulo.

Aquí hay un ejemplo de un módulo que ilustra cómo se utiliza la visibilidad de funciones en 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()
}
}

Variables Basadas en el Tiempo

El SDK de Soroban Rust y Solidity proporcionan varias variables basadas en el tiempo que se pueden utilizar para acceder a información sobre el bloque actual(EVM) o ledger(Soroban). Estas variables incluyen:

Solidity

  • block.timestamp: La marca de tiempo del bloque actual.
  • block.number: El número del bloque actual.

Aquí hay un ejemplo de un contrato inteligente que ilustra cómo se utilizan las variables basadas en el tiempo en 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(): Devuelve una marca de tiempo unix para cuando se cerró el ledger más reciente.
  • env.ledger().sequence(): Devuelve el número de secuencia del ledger cerrado más recientemente.

Aquí hay un ejemplo de un contrato inteligente que ilustra cómo se utilizan las variables basadas en el tiempo en 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();
}
}

Autorización

Soroban difiere de Solidity en su enfoque hacia la autorización y los modificadores. Mientras que Solidity tiene un sistema de modificadores incorporado, Soroban no. En su lugar, Soroban aprovecha traits, sus implementaciones, y características centrales para lograr una funcionalidad similar.

Solidity

En Solidity, el estándar de token ERC20 incluye la función approve, que permite a un tenedor de tokens autorizar a otra dirección a gastar una cierta cantidad de tokens en su nombre. Esta función se usa comúnmente en intercambios descentralizados y otros escenarios de transferencia de tokens. Además, nos aseguramos de que el gastador esté autorizado para gastar la cantidad de tokens solicitada por el tenedor de tokens.

Aquí hay un ejemplo de cómo la función approve actúa como un mecanismo de autorización en 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;
}
}

La función approve permite al tenedor de tokens autorizar al gastador para gastar tokens de cantidad en su nombre. La función transfer luego verifica si el gastador está autorizado para gastar la cantidad de tokens solicitada por el tenedor de tokens. Si es así, la transferencia se ejecuta.

Estos ejemplos de Solidity ilustran algunos patrones comunes de autorización utilizados en contratos inteligentes de Ethereum. Soroban proporciona enfoques alternativos para lograr una funcionalidad similar, aprovechando la funcionalidad central derivada directamente del SDK de Soroban.

Soroban

Los principios de diseño de Soroban priorizan la flexibilidad, seguridad y capacidad de prueba, lo que ha llevado a diferencias en cómo se maneja la autorización en comparación con Solidity.

Soroban proporciona funciones incorporadas como require_auth y require_auth_for_args a través de la estructura Address. Estas funciones ayudan a hacer cumplir las reglas de autorización dentro de los contratos. Durante la ejecución on-chain, el host de Soroban realiza la autenticación necesaria, incluyendo la verificación de firmas y asegurando la prevención de replays. Esto alivia la carga de autenticación de los mismos contratos, promoviendo la seguridad y reduciendo vulnerabilidades potenciales.

Aquí hay un ejemplo de un contrato inteligente que ilustra cómo se maneja la autorización en 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));
}
}

En este ejemplo, tenemos un contrato de Soroban que incluye dos funciones públicas: transfer y transfer2, ambas de las cuales implican verificaciones de autorización.

Dentro de la función Transfer, se invoca el método require_auth en el objeto de dirección. Este método asegura que el llamador del contrato tenga la autorización necesaria para ejecutar la transferencia.

La función transfer2 sigue un patrón similar, pero utiliza el método require_auth_for_args en su lugar. Toma los mismos parámetros que transfer, pero proporciona una tupla (cantidad / 2,) como argumento para require_auth_for_args. Este método verifica que el llamador haya autorizado la invocación del contrato con los argumentos específicos.

Al utilizar estos métodos de autorización proporcionados por el objeto Address del SDK de Soroban Rust, el contrato exige que solo los llamadores autorizados puedan realizar las transferencias. Este enfoque mejora la seguridad del contrato al asegurar que las operaciones sensibles solo pueden ser ejecutadas por partes autorizadas.

El enfoque de Soroban hacia la autorización en este ejemplo ofrece varias ventajas sobre el modelo de ERC20 de Solidity al eliminar la necesidad de gestionar aprobaciones por separado. En su lugar, las verificaciones de autorización se pueden incorporar directamente en cualquier función de Soroban. Esto simplifica la base de código del contrato y reduce la complejidad asociada con la gestión de estados de aprobación separados.

La autorización de Soroban proporciona autorización a nivel de contrato, funcionalidad de abstracción de cuentas, y más verificaciones avanzadas de autorización. Para aprender más sobre estas ventajas, visita la sección de Autorización de la documentación.

Resumen

En general, los equivalentes de Soroban de los conceptos de Solidity son muy similares. Sin embargo, hay diferencias notables que vale la pena resaltar. Soroban utiliza env en lugar de msg para acceder a información sobre todo el entorno de ejecución del contrato, incluyendo el estado del contrato, direcciones involucradas, y más. La autorización también se maneja de manera diferente en Soroban, ya que está integrada en la funcionalidad central del SDK y es más robusta que depender únicamente del código del contrato inteligente.

Para más información sobre los conceptos de Solidity y sus equivalentes en Soroban, se recomienda consultar tanto la documentación del SDK de Soroban Rust como la documentación de Solidity.