Smart Contract Development with Soroban and Hardhat
In this tutorial, we will discover the similarities in smart contract deployment by examining workflows with Soroban and Hardhat. We will dive into the intricacies of each framework, learn to write secure and efficient smart contract code, and harness the power of Rust and Soroban to create customized contract logic.
Table of Contents
- Soroban and Hardhat Comparison
- Hardhat vs Soroban SDKs
- Using Rust and Soroban for Smart Contract Development
- Vault Contract Deployment and Interaction
Soroban and Hardhat Comparison
Introduction
Soroban and Hardhat are both frameworks that enable developers to build, test, and deploy smart contracts. In this section, we will delve into the similarities and distinctions between these two frameworks.
Soroban Framework
Soroban es un marco basado en Rust diseñado para desarrollar contratos inteligentes en la red Stellar. Diseñado como un marco ligero, con herramientas para admitir a los desarrolladores, Soroban permite a los desarrolladores crear contratos inteligentes a través de un flujo de trabajo simple e intuitivo.
Hardhat
Hardhat sirve como un entorno de desarrollo para compilar, desplegar, probar y depurar contratos inteligentes para la EVM. Asiste a los desarrolladores en la gestión y automatización de tareas recurrentes inherentes a la construcción de contratos inteligentes.
Similaridades
Soroban y Hardhat son marcos potentes diseñados para agilizar el proceso de construir, probar y desplegar contratos inteligentes. Equipadas con una suite integral de herramientas, estos marcos facilitan el desarrollo de contratos inteligentes y su despliegue en sus respectivas máquinas virtuales.
Diferencias
Soroban, con su diseño ligero, ofrece a los desarrolladores una plataforma excepcional para escribir contratos inteligentes basados en Rust y desplegarlos sin esfuerzo en la red Stellar. En contraste, Hardhat sirve principalmente como un entorno de desarrollo diseñado para la Máquina Virtual de Ethereum, proporcionando un enfoque y audiencia objetivo diferentes.
Hardhat vs. Soroban SDKs
Hardhat ofrece un flujo de trabajo optimizado para desplegar contratos inteligentes en la Máquina Virtual de Ethereum, con componentes clave como ethers.js
, scripts
, y testing
desempeñando roles cruciales.
Por otro lado, Soroban presenta una alternativa convincente, con potentes SDKs que facilitan el desarrollo y despliegue de contratos inteligentes. En la próxima sección, profundizaremos en los SDKs de Soroban, comparando los componentes de Hardhat y destacando las ventajas únicas que cada plataforma aporta.
Ethers.js
Ethers.js
es una biblioteca de JavaScript
ampliamente utilizada diseñada para la interacción sin problemas con la EVM. Ofrece una interfaz amigable que simplifica la conexión a nodos de Ethereum, la gestión de cuentas y el envío de transacciones. Además, Ethers.js
proporciona una API robusta para la comunicación eficiente con contratos inteligentes. Esta biblioteca es un componente central del marco Hardhat y puede ser importada en scripts para agilizar el despliegue de contratos inteligentes.
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
}
Soroban Client
Soroban ofrece una biblioteca comparable, stellar-sdk
, que permite la interacción sin problemas con contratos inteligentes desplegados en la red Stellar. Esta biblioteca proporciona una API de capa de red integral para métodos RPC de Stellar, así como la API tradicional de Horizon, simplificando el proceso de construcción y firma de transacciones. Además, stellar-sdk
agiliza la comunicación con instancias RPC y admite enviar transacciones o consultar el estado de la red con facilidad.
Scripts
Los scripts de Hardhat agilizan la automatización de tareas rutinarias, como desplegar y gestionar contratos inteligentes. Los desarrolladores pueden crear estos scripts usando JavaScript o TypeScript, adaptándose a su estilo de programación preferido. Se almacenan en el directorio scripts
de un proyecto Hardhat y pueden ejecutarse utilizando el comando npx hardhat run
.
// scripts/deploy.js
async function main() {
// Compile and deploy the smart contract
const MyContract = await ethers.getContractFactory("MyContract");
const myContract = await MyContract.deploy();
console.log("MyContract deployed to:", myContract.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Soroban Scripts
Soroban ofrece una amplia colección de SDKs que incluyen capacidades de scripting, asegurando un flujo de trabajo fluido para desplegar y gestionar contratos inteligentes. Los desarrolladores pueden automatizar tareas como compilar, desplegar e interactuar con contratos inteligentes utilizando una variedad de SDKs que admiten scripting en lenguajes como JavaScript
, TypeScript
, Python
, y otros.
# This example shows how to deploy a compiled contract to the Stellar network.
# https://github.com/stellar/soroban-quest/blob/main/quests/6-asset-interop/py-scripts/deploy-contract.py
import time
from stellar_sdk import Network, Keypair, TransactionBuilder
from stellar_sdk import xdr as stellar_xdr
from stellar_sdk.soroban import SorobanServer
from stellar_sdk.soroban.soroban_rpc import TransactionStatus
# TODO: You need to replace the following parameters according to the actual situation
secret = "SAAPYAPTTRZMCUZFPG3G66V4ZMHTK4TWA6NS7U4F7Z3IMUD52EK4DDEV"
rpc_server_url = "https://soroban-testnet.stellar.org"
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
contract_file_path = "/path/to/compiled/soroban_contract.wasm"
kp = Keypair.from_secret(secret)
soroban_server = SorobanServer(rpc_server_url)
print("installing contract...")
source = soroban_server.load_account(kp.public_key)
# with open(contract_file_path, "rb") as f:
# contract_bin = f.read()
tx = (
TransactionBuilder(source, network_passphrase)
.set_timeout(300)
.append_install_contract_code_op(
contract=contract_file_path, # the path to the contract, or binary data
source=kp.public_key,
)
.build()
)
...
Testing
Hardhat proporciona un marco de pruebas que permite a los desarrolladores escribir pruebas para sus contratos inteligentes. Estas pruebas pueden escribirse en JavaScript o TypeScript y ejecutarse utilizando el comando npx hardhat test
.
// test/my-contract.js
const { expect } = require("chai");
describe("MyContract", function () {
it("Should return the correct name", async function () {
const MyContract = await ethers.getContractFactory("MyContract");
const myContract = await MyContract.deploy();
await myContract.deployed();
expect(await myContract.name()).to.equal("MyContract");
});
});
Soroban Testing
Soroban permite a los usuarios aprovechar el poder del marco de pruebas de Rust para escribir pruebas para sus contratos inteligentes. Estas pruebas pueden escribirse en Rust y ejecutarse usando el comando cargo test
.
#![cfg(test)]
use super::*;
use soroban_sdk::{vec, Env, Symbol, symbol_short};
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register_contract(None, HelloContract);
let client = HelloContractClient::new(&env, &contract_id);
let words = client.hello(&symbol_short!("Dev"));
assert_eq!(
words,
vec![&env, symbol_short!("Hello"), symbol_short!("Dev"),]
);
}
En resumen, mientras Hardhat proporciona un excelente entorno para desplegar contratos inteligentes en la EVM, el marco basado en Rust de Soroban ofrece ventajas significativas en términos de rendimiento, lo que lo convierte en una opción ideal para construir contratos inteligentes seguros y eficientes.
Desarrollar Contratos Inteligentes con Rust y Soroban
Introducción
Ahora que hemos examinado el flujo de trabajo de despliegue con Hardhat, exploremos el desarrollo y despliegue de contratos inteligentes con Rust y Soroban. La principal ventaja de utilizar Soroban es su capacidad para aprovechar las características de seguridad y rendimiento de Rust, lo que lo convierte en una excelente opción para desarrollar contratos inteligentes seguros y eficientes.
Hemos aprendido que los contratos inteligentes son contratos autoejecutables que pueden programarse para hacer cumplir automáticamente las reglas y regulaciones de un acuerdo particular. Son un componente central de las aplicaciones descentralizadas (dApps) y la tecnología blockchain. En esta sección, aprenderemos cómo usar Rust y Soroban para desarrollar y desplegar lógica de contrato inteligente personalizada.
Configuración
Si aún no has configurado el entorno de desarrollo para Soroban, puedes comenzar siguiendo los pasos en la Página de Configuración.
Este proyecto requiere el uso del archivo soroban_token_contract.wasm
que necesitarás importar manualmente.
Primero, necesitarás clonar la etiqueta v22.0.1
del repositorio soroban-examples
:
git clone -b v22.0.1 https://github.com/stellar/soroban-examples
Luego, navega al directorio soroban-examples/token
cd soroban-examples/token
A continuación, construye el contrato Token usando el siguiente comando:
soroban contract build
Esto construirá el archivo soroban_token_contract.wasm
que necesitarás importar en tu proyecto. El archivo soroban_token_contract.wasm
se encuentra en el directorio soroban-examples/target/wasm32-unknown-unknown/release
.
soroban-examples
├── target
│ └── wasm32-unknown-unknown
│ └── release
│ └── soroban_token_contract.wasm
└──
Una vez que tengamos el Token, vamos a crear un nuevo contrato inteligente que lo use.
Escribir un Contrato Inteligente
Comencemos escribiendo un ejemplo simple de un contrato de vault que permite a los usuarios depositar fondos y retirar sus fondos con rendimiento generado.
Aquí hay un desglose de la mecánica del contrato
- Las acciones son creadas cuando un usuario realiza un depósito.
- El protocolo DeFi utiliza los depósitos de los usuarios para generar rendimiento.
- El usuario quema acciones para retirar sus tokens + rendimiento.
En una nueva terminal, vamos a crear un nuevo proyecto en Rust ejecutando el siguiente comando:
cargo new --lib vault
Esto creará un nuevo proyecto de Rust llamado vault
.
Ahora vamos a agregar el archivo soroban_token_contract.wasm
al proyecto vault
. Para hacer esto, podemos arrastrar y soltar el archivo en el directorio del proyecto vault
.
A continuación, necesitaremos agregar el SDK de Soroban como una dependencia. Para hacer esto, abre el archivo Cargo.toml
en tu proyecto y asegúrate de que coincide con lo siguiente:
[package]
name = "vault"
version = "0.0.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
soroban-sdk = { version = "20.0.0" }
num-integer = { version = "0.1.45", default-features = false, features = ["i128"] }
[dev_dependencies]
soroban-sdk = { version = "20.0.0", features = ["testutils"] }
[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true
[profile.release-with-logs]
inherits = "release"
debug-assertions = true
En este proyecto necesitaremos crear 3 archivos:
src/lib.rs
- Aquí es donde escribiremos nuestra lógica de contrato inteligente de vault.src/test.rs
- Aquí es donde escribiremos nuestras pruebas.src/token.rs
- Este archivo hereda el contrato de token que importamos anteriormente. También es donde escribiremos nuestra lógica de creación de tokens.
Para interactuar con el contrato de token, utilizaremos una interfaz incorporada que puedes encontrar en la pestaña token_interface.rs
. Esta interfaz incluye las funciones initialize
y mint
que utilizaremos para crear y acuñar tokens para usar en nuestro contrato de vault. Si quieres ver el código completo del contrato de token, puedes verlo aquí.
- src/lib.rs
- src/test.rs
- src/token.rs
- src/token_interface.rs
#![no_std]
mod test;
mod token;
use soroban_sdk::{
contract, contractimpl, contractmeta, Address, BytesN, ConversionError, Env, IntoVal,
TryFromVal, Val,
};
use token::create_contract;
#[derive(Clone, Copy)]
#[repr(u32)]
pub enum DataKey {
Token = 0,
TokenShare = 1,
TotalShares = 2,
Reserve = 3,
}
impl TryFromVal<Env, DataKey> for Val {
type Error = ConversionError;
fn try_from_val(_env: &Env, v: &DataKey) -> Result<Self, Self::Error> {
Ok((*v as u32).into())
}
}
fn get_token(e: &Env) -> Address {
e.storage().instance().get(&DataKey::Token).unwrap()
}
fn get_token_share(e: &Env) -> Address {
e.storage().instance().get(&DataKey::TokenShare).unwrap()
}
fn get_total_shares(e: &Env) -> i128 {
e.storage().instance().get(&DataKey::TotalShares).unwrap()
}
fn get_reserve(e: &Env) -> i128 {
e.storage().instance().get(&DataKey::Reserve).unwrap()
}
fn get_balance(e: &Env, contract: Address) -> i128 {
token::Client::new(e, &contract).balance(&e.current_contract_address())
}
fn get_token_balance(e: &Env) -> i128 {
get_balance(e, get_token(e))
}
fn get_balance_shares(e: &Env) -> i128 {
get_balance(e, get_token_share(e))
}
fn put_token(e: &Env, contract: Address) {
e.storage().instance().set(&DataKey::Token, &contract);
}
fn put_token_share(e: &Env, contract: Address) {
e.storage().instance().set(&DataKey::TokenShare, &contract);
}
fn put_total_shares(e: &Env, amount: i128) {
e.storage().instance().set(&DataKey::TotalShares, &amount)
}
fn put_reserve(e: &Env, amount: i128) {
e.storage().instance().set(&DataKey::Reserve, &amount)
}
fn burn_shares(e: &Env, amount: i128) {
let total = get_total_shares(e);
let share_contract_id = get_token_share(e);
token::Client::new(e, &share_contract_id).burn(&e.current_contract_address(), &amount);
put_total_shares(e, total - amount);
}
fn mint_shares(e: &Env, to: Address, amount: i128) {
let total = get_total_shares(e);
let share_contract_id = get_token_share(e);
token::Client::new(e, &share_contract_id).mint(&to, &amount);
put_total_shares(e, total + amount);
}
// Metadata that is added on to the Wasm custom section
contractmeta!(
key = "Description",
val = "A Vault with a 1% return on investment per deposit."
);
pub trait VaultTrait {
// Sets the token contract addresses for this vault
fn initialize(e: Env, token_wasm_hash: BytesN<32>, token: Address);
// Returns the token contract address for the vault share token
fn share_id(e: Env) -> Address;
// Deposits token. Also mints vault shares for the `from` Identifier. The amount minted
// is determined based on the difference between the reserves stored by this contract, and
// the actual balance of token for this contract.
fn deposit(e: Env, from: Address, amount: i128);
// transfers `amount` of vault share tokens to this contract, burns all pools share tokens in this contracts, and sends the
// corresponding amount of token to `to`.
// Returns amount of token withdrawn
fn withdraw(e: Env, to: Address, amount: i128) -> i128;
fn get_rsrvs(e: Env) -> i128;
}
#[contract]
struct Vault;
#[contractimpl]
impl VaultTrait for Vault {
fn initialize(e: Env, token_wasm_hash: BytesN<32>, token: Address) {
let share_contract_id = create_contract(&e, token_wasm_hash, &token);
token::Client::new(&e, &share_contract_id).initialize(
&e.current_contract_address(),
&7u32,
&"Vault Share Token".into_val(&e),
&"VST".into_val(&e),
);
put_token(&e, token);
put_token_share(&e, share_contract_id.try_into().unwrap());
put_total_shares(&e, 0);
put_reserve(&e, 0);
}
fn share_id(e: Env) -> Address {
get_token_share(&e)
}
fn deposit(e: Env, from: Address, amount: i128) {
// Depositor needs to authorize the deposit
from.require_auth();
let token_client = token::Client::new(&e, &get_token(&e));
token_client.transfer(&from, &e.current_contract_address(), &amount);
let balance = get_token_balance(&e);
mint_shares(&e, from, amount);
put_reserve(&e, balance);
}
fn withdraw(e: Env, to: Address, amount: i128) -> i128 {
to.require_auth();
// First transfer the vault shares that need to be redeemed
let share_token_client = token::Client::new(&e, &get_token_share(&e));
share_token_client.transfer(&to, &e.current_contract_address(), &amount);
let token_client = token::Client::new(&e, &get_token(&e));
token_client.transfer(
&e.current_contract_address(),
&to,
&(&amount + (&amount / &100)),
);
let balance = get_token_balance(&e);
let balance_shares = get_balance_shares(&e);
burn_shares(&e, balance_shares);
put_reserve(&e, balance - amount);
amount
}
fn get_rsrvs(e: Env) -> i128 {
get_reserve(&e)
}
}
#![cfg(test)]
extern crate std;
use crate::{token, VaultClient};
use soroban_sdk::{
symbol_short,
testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation},
Address, BytesN, Env, IntoVal,
};
fn create_token_contract<'a>(e: &Env, admin: &Address) -> token::Client<'a> {
token::Client::new(e, &e.register_stellar_asset_contract_v2(admin.clone()).address())
}
fn create_vault_contract<'a>(
e: &Env,
token_wasm_hash: &BytesN<32>,
token: &Address,
) -> VaultClient<'a> {
let vault = VaultClient::new(e, &e.register_contract(None, crate::Vault {}));
vault.initialize(token_wasm_hash, token);
vault
}
fn install_token_wasm(e: &Env) -> BytesN<32> {
soroban_sdk::contractimport!(file = "./soroban_token_contract.wasm");
e.deployer().upload_contract_wasm(WASM)
}
#[test]
fn test() {
let e = Env::default();
e.mock_all_auths();
let admin1 = Address::random(&e);
let token = create_token_contract(&e, &admin1);
let user1 = Address::random(&e);
let vault = create_vault_contract(&e, &install_token_wasm(&e), &token.address);
let contract_share = token::Client::new(&e, &vault.share_id());
let token_share = token::Client::new(&e, &contract_share.address);
token.mint(&user1, &200);
assert_eq!(token.balance(&user1), 200);
token.mint(&vault.address, &100);
assert_eq!(token.balance(&vault.address), 100);
vault.deposit(&user1, &100);
assert_eq!(
e.auths(),
std::vec![(
user1.clone(),
AuthorizedInvocation {
function: AuthorizedFunction::Contract((
vault.address.clone(),
symbol_short!("deposit"),
(&user1, 100_i128).into_val(&e)
)),
sub_invocations: std::vec![AuthorizedInvocation {
function: AuthorizedFunction::Contract((
token.address.clone(),
symbol_short!("transfer"),
(&user1, &vault.address, 100_i128).into_val(&e)
)),
sub_invocations: std::vec![]
}]
}
)]
);
assert_eq!(token_share.balance(&user1), 100);
assert_eq!(token_share.balance(&vault.address), 0);
assert_eq!(token.balance(&user1), 100);
assert_eq!(token.balance(&vault.address), 200);
e.budget().reset_unlimited();
vault.withdraw(&user1, &100);
assert_eq!(
e.auths(),
std::vec![(
user1.clone(),
AuthorizedInvocation {
function: AuthorizedFunction::Contract((
vault.address.clone(),
symbol_short!("withdraw"),
(&user1, 100_i128).into_val(&e)
)),
sub_invocations: std::vec![AuthorizedInvocation {
function: AuthorizedFunction::Contract((
token_share.address.clone(),
symbol_short!("transfer"),
(&user1, &vault.address, 100_i128).into_val(&e)
)),
sub_invocations: std::vec![]
}]
}
)]
);
assert_eq!(token.balance(&user1), 201);
assert_eq!(token_share.balance(&user1), 0);
assert_eq!(token.balance(&vault.address), 99);
assert_eq!(token_share.balance(&vault.address), 0);
}
#![allow(unused)]
use soroban_sdk::{xdr::ToXdr, Address, Bytes, BytesN, Env};
soroban_sdk::contractimport!(file = "./soroban_token_contract.wasm");
pub fn create_contract(e: &Env, token_wasm_hash: BytesN<32>, token: &Address) -> Address {
let mut salt = Bytes::new(e);
salt.append(&token.to_xdr(e));
let salt = e.crypto().sha256(&salt);
e.deployer()
.with_current_contract(salt)
.deploy(token_wasm_hash)
}
//! This contract demonstrates a sample implementation of the Soroban token
//! interface.
use crate::admin::{has_administrator, read_administrator, write_administrator};
use crate::allowance::{read_allowance, spend_allowance, write_allowance};
use crate::balance::{is_authorized, write_authorization};
use crate::balance::{read_balance, receive_balance, spend_balance};
use crate::event;
use crate::metadata::{read_decimal, read_name, read_symbol, write_metadata};
use crate::storage_types::INSTANCE_TTL_EXTEND_AMOUNT;
use soroban_sdk::{contract, contractimpl, Address, Env, String};
use soroban_token_sdk::TokenMetadata;
pub trait TokenTrait {
fn initialize(e: Env, admin: Address, decimal: u32, name: String, symbol: String);
fn allowance(e: Env, from: Address, spender: Address) -> i128;
fn approve(e: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32);
fn balance(e: Env, id: Address) -> i128;
fn spendable_balance(e: Env, id: Address) -> i128;
fn authorized(e: Env, id: Address) -> bool;
fn transfer(e: Env, from: Address, to: Address, amount: i128);
fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128);
fn burn(e: Env, from: Address, amount: i128);
fn burn_from(e: Env, spender: Address, from: Address, amount: i128);
fn clawback(e: Env, from: Address, amount: i128);
fn set_authorized(e: Env, id: Address, authorize: bool);
fn mint(e: Env, to: Address, amount: i128);
fn set_admin(e: Env, new_admin: Address);
fn decimals(e: Env) -> u32;
fn name(e: Env) -> String;
fn symbol(e: Env) -> String;
}
fn check_nonnegative_amount(amount: i128) {
if amount < 0 {
panic!("negative amount is not allowed: {}", amount)
}
}
#[contract]
pub struct Token;
#[contractimpl]
impl TokenTrait for Token {
fn initialize(e: Env, admin: Address, decimal: u32, name: String, symbol: String) {
if has_administrator(&e) {
panic!("already initialized")
}
write_administrator(&e, &admin);
if decimal > 18 {
panic!("Decimal must not be greater than 18");
}
write_metadata(
&e,
TokenMetadata {
decimal,
name,
symbol,
},
)
}
fn allowance(e: Env, from: Address, spender: Address) -> i128 {
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
read_allowance(&e, from, spender).amount
}
fn approve(e: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32) {
from.require_auth();
check_nonnegative_amount(amount);
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
write_allowance(&e, from.clone(), spender.clone(), amount, expiration_ledger);
event::approve(&e, from, spender, amount, expiration_ledger);
}
fn balance(e: Env, id: Address) -> i128 {
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
read_balance(&e, id)
}
fn spendable_balance(e: Env, id: Address) -> i128 {
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
read_balance(&e, id)
}
fn authorized(e: Env, id: Address) -> bool {
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
is_authorized(&e, id)
}
fn transfer(e: Env, from: Address, to: Address, amount: i128) {
from.require_auth();
check_nonnegative_amount(amount);
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
spend_balance(&e, from.clone(), amount);
receive_balance(&e, to.clone(), amount);
event::transfer(&e, from, to, amount);
}
fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128) {
spender.require_auth();
check_nonnegative_amount(amount);
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
spend_allowance(&e, from.clone(), spender, amount);
spend_balance(&e, from.clone(), amount);
receive_balance(&e, to.clone(), amount);
event::transfer(&e, from, to, amount)
}
fn burn(e: Env, from: Address, amount: i128) {
from.require_auth();
check_nonnegative_amount(amount);
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
spend_balance(&e, from.clone(), amount);
event::burn(&e, from, amount);
}
fn burn_from(e: Env, spender: Address, from: Address, amount: i128) {
spender.require_auth();
check_nonnegative_amount(amount);
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
spend_allowance(&e, from.clone(), spender, amount);
spend_balance(&e, from.clone(), amount);
event::burn(&e, from, amount)
}
fn clawback(e: Env, from: Address, amount: i128) {
check_nonnegative_amount(amount);
let admin = read_administrator(&e);
admin.require_auth();
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
spend_balance(&e, from.clone(), amount);
event::clawback(&e, admin, from, amount);
}
fn set_authorized(e: Env, id: Address, authorize: bool) {
let admin = read_administrator(&e);
admin.require_auth();
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
write_authorization(&e, id.clone(), authorize);
event::set_authorized(&e, admin, id, authorize);
}
fn mint(e: Env, to: Address, amount: i128) {
check_nonnegative_amount(amount);
let admin = read_administrator(&e);
admin.require_auth();
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
receive_balance(&e, to.clone(), amount);
event::mint(&e, admin, to, amount);
}
fn set_admin(e: Env, new_admin: Address) {
let admin = read_administrator(&e);
admin.require_auth();
e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT);
write_administrator(&e, &new_admin);
event::set_admin(&e, admin, new_admin);
}
fn decimals(e: Env) -> u32 {
read_decimal(&e)
}
fn name(e: Env) -> String {
read_name(&e)
}
fn symbol(e: Env) -> String {
read_symbol(&e)
}
}
Now that we've added these files to our project, let's break down what happens in the lib.rs
file above and discover how "yield" is generated from our vault contract
Primero, echemos un vistazo a lo que sucede cuando un usuario deposita tokens en el contrato de vault.
fn deposit(e: Env, from: Address, amount: i128) {
// Depositor needs to authorize the deposit
from.require_auth();
let token = token::Client::new(&e, &get_token(&e));
token.transfer(&from, &e.current_contract_address(), &amount);
// Now calculate how many new vault shares to mint
let balance = get_token_balance(&e);
let shares = amount;
mint_shares(&e, from, shares);
put_reserve(&e, balance + shares);
}
- La función
deposit
es llamada por el depositante para depositar tokens en el contrato de vault. - El método
transfer
de la instanciatoken_client
transfiere tokens del depositante al contrato de vault. - El saldo actual de tokens y el total de acciones emitidas por el contrato de vault se obtienen utilizando las funciones
get_token_balance
yget_total_shares
, respectivamente. mint_shares
se llama para emitir nuevas acciones al depositante y actualiza el total de acciones emitidas por el contrato de vault.put_reserve
almacena el saldo actual de tokens en una ubicación reservada.
Si el usuario llamara al método deposit
con 100 tokens, sucedería lo siguiente:
- 100 tokens se transferirían del depositante al contrato de vault.
- El saldo actual de tokens se almacenaría en una ubicación reservada.
- El total de acciones emitidas por el contrato de vault se actualizaría a 100.
- 100 acciones se emitirían al depositante.
Ahora veamos qué sucede cuando un usuario retira tokens del vault.
fn withdraw(e: Env, to: Address, amount: i128) -> i128 {
to.require_auth();
// First transfer the vault shares that need to be redeemed
let share_token_client = token::Client::new(&e, &get_token_share(&e));
share_token_client.transfer(&to, &e.current_contract_address(), &amount);
// Calculate total amount including yield
let total_amount = amount + (amount / 100);
let token_client = token::Client::new(&e, &get_token(&e));
token_client.transfer(&e.current_contract_address(), &to, &total_amount);
let balance = get_token_balance(&e);
let balance_shares = get_balance_shares(&e);
burn_shares(&e, balance_shares);
put_reserve(&e, balance); // Update the reserve with the actual balance
total_amount
}
- La función
withdraw
es llamada por el retirador para retirar tokens del contrato de vault. - El método
transfer
de la instanciashare_token_client
transfiere acciones del retirador al contrato de vault. - El método
transfer_token
de la instanciatoken_client
transfiere tokens del contrato de vault al retirador. burn_shares
se llama para quemar las acciones que fueron transferidas al contrato de vault.put_reserve
almacena el saldo actual de tokens en una ubicación reservada.- Devuelve el total de tokens retirados por el usuario.
Nota : En la función de retirada, notarás que la cantidad de transferencia se define como
&(&amount + (&amount / &100))
. Este es un cálculo de rendimiento simple que asume que el rendimiento es el 1% de la cantidad que se está retirando. Sin embargo, es importante notar que este es un enfoque muy simplista y puede no ser adecuado para sistemas de producción. En realidad, los cálculos de rendimiento son más complejos e involucran varios factores como condiciones de mercado, gestión de riesgos y tarifas.
Si el usuario llamara al método withdraw
con 100 acciones, sucedería lo siguiente:
- 100 acciones se transferirían del retirador al contrato de vault.
- El saldo actual de tokens se almacenaría en una ubicación reservada.
- 100 acciones se quemarían.
- 100 + (100/100) tokens se transferirían del contrato de vault al retirador.
Testing
Para probar el contrato de vault, podemos simplemente ejecutar el siguiente comando en nuestra terminal desde el directorio de nuestro contrato de vault:
#cd vault
cargo test
Esto ejecutará las pruebas que hemos escrito en el archivo src/test.rs
.
running 1 test
test test::test ... ok
Despliegue e Interacción del Contrato de Vault
Ahora que tenemos un contrato de vault operativo, podemos desplegarlo en una red e interactuar con él.
Esta sección requiere que tengas un Keypair financiado para usar con el Testnet de Stellar. Puedes crear y financiar uno usando el Stellar Lab.
A continuación, encontrarás una serie de comandos que te ayudarán a construir, desplegar e interactuar con los contratos de vault y de token. Puedes usarlos para seguir mientras pasamos por el proceso de construir, desplegar e interactuar con los contratos. Puede serte útil mantener estos comandos en un directorio scripts
en tu proyecto. De esta manera, puedes ejecutarlos fácilmente desde tu terminal.
Nota : Si decides usar scripts, asegúrate de verificar tus rutas de importación.
- build.sh
- deploy_token.sh
- initialize_token.sh
- mint.sh
- balance.sh
- install.sh
- deploy_vault.sh
- initialize_vault.sh
- share_id.sh
- deposit.sh
- get_rsrv.sh
- withdraw.sh
soroban contract build
soroban contract deploy \
--wasm soroban_token_contract.wasm \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015'
soroban contract invoke \
--id <TOKEN_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
initialize \
--admin <USER_ADDRESS> \
--decimal 18 \
--name <TOKEN_NAME> \
--symbol <TOKEN_SYMBOL>
soroban contract invoke \
--id <TOKEN_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
mint \
--to <USER_OR_VAULT_ADDRESS> \
--amount 100
soroban contract invoke \
--id <TOKEN_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
balance \
--id <USER_OR_VAULT_ADDRESS>
soroban contract install \
--wasm soroban_token_contract.wasm \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015'
soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/vault.wasm \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015'
soroban contract invoke \
--id <VAULT_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
initialize \
--token_wasm_hash 73593275ee3bcacc2aef8d641a1d5108618064bdfff84a826576b8caff395add \
--token <TOKEN_CONTRACT_ADDRESS>
soroban contract invoke \
--id <VAULT_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
share_id
soroban contract invoke \
--id <VAULT_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
deposit \
--from <USER_ADDRESS> \
--amount 100
soroban contract invoke \
--id <VAULT_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
get_rsrvs
soroban contract invoke \
--id <VAULT_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
withdraw \
--to <USER_ADDRESS> \
--amount 100
Primero, necesitamos construir el contrato de vault. Podemos hacer esto ejecutando el script build.sh
desde nuestro directorio de vault.
##cd vault
soroban contract build
A continuación, necesitamos desplegar el contrato de token. Podemos hacer esto ejecutando el script deploy_token.sh
.
soroban contract deploy \
--wasm soroban_token_contract.wasm \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015'
Deberíamos recibir una salida con el ID del contrato de token. Necesitaremos este ID para el siguiente paso.
CBYMG7OPIT67AG4S2FZU7LAYCXUSXEHRGHLDE6H26VCVWNOV7QUQTGNU
A continuación, necesitamos inicializar el contrato de token. Podemos hacer esto ejecutando el script initialize_token.sh
.
soroban contract invoke \
--id <TOKEN_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
initialize \
--admin <USER_ADDRESS> \
--decimal 18 \
--name <TOKEN_NAME> \
--symbol <TOKEN_SYMBOL>
A continuación, necesitamos desplegar el contrato de vault. Podemos hacer esto ejecutando el script deploy_vault.sh
.
soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/vault.wasm \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015'
Deberíamos recibir una salida con el ID del contrato del vault. Necesitaremos este ID para el siguiente paso.
CBBPLE6TGYOMO5HUF2AMYLSYYXM2VYZVAVYI5QCCM5OCFRZPBE2XA53F
Ahora necesitamos obtener el hash Wasm del contrato del token. Podemos hacer esto ejecutando el script get_token_wasm_hash.sh
.
soroban contract install \
--wasm soroban_token_contract.wasm \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015'
Deberíamos recibir el hash Wasm del contrato del token.
6b7e4bfbf47157a12e24e564efc1f9ac237e7ae6d7056b6c2ab47178b9e7a510
Ahora necesitamos inicializar el contrato del vault. Podemos hacer esto ejecutando el script initialize_vault.sh
y pasando el ID del contrato del token y el hash Wasm del contrato del token.
soroban contract invoke \
--id <VAULT_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
initialize \
--token_wasm_hash 6b7e4bfbf47157a12e24e564efc1f9ac237e7ae6d7056b6c2ab47178b9e7a510 \
--token <TOKEN_CONTRACT_ADDRESS>
Después de recibir que la transacción ha sido enviada, mintamos algunos tokens a ambas nuestras cuentas de usuario y direcciones del contrato del vault. Podemos hacer esto ejecutando el script mint.sh
.
soroban contract invoke \
--id <TOKEN_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
mint \
--to <USER_OR_VAULT_ADDRESS> \
--amount 100
Después de enviar la transacción, podemos verificar el saldo de la cuenta. Podemos hacer esto ejecutando el script balance.sh
.
soroban contract invoke \
--id <TOKEN_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
balance \
--id <USER_ADDRESS>
Deberíamos recibir una salida con el saldo de la cuenta.
100
Ahora podemos depositar algunos tokens en nuestro vault. Podemos hacer esto ejecutando el script deposit.sh
.
soroban contract invoke \
--id <VAULT_CONTRACT_ID> \
--source <ACCOUNT_SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
deposit \
--from <USER_ADDRESS> \
--amount 100
Después de enviar la transacción, podemos verificar las reservas del vault. Podemos hacer esto ejecutando el script reserves.sh
.
soroban contract invoke \
--id <VAULT_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
get_rsrvs
Deberíamos recibir una salida con las reservas del vault.
"200"
100 de la depósito y 100 de la mint.
Ahora podemos retirar algunos tokens del vault. Podemos hacer esto ejecutando el script withdraw.sh
.
soroban contract invoke \
--id <VAULT_CONTRACT_ID> \
--source <SECRET_KEY> \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase 'Test SDF Network ; September 2015' \
-- \
withdraw \
--to <USER_ADDRESS> \
--amount 100
Deberíamos recibir una salida con el monto de la retirada.
"100"
Ahora es un buen momento para verificar nuestro saldo de cuenta de nuevo. Podemos hacer esto ejecutando el script balance.sh
.
Deberíamos ver que nuestro saldo ha aumentado el monto que retiramos más el rendimiento (monto/100) o %1 de nuestro monto de retiro.
101
Y finalmente, podemos verificar las reservas del vault de nuevo. Podemos hacer esto ejecutando el script get_rsrv.sh
.
Deberíamos ver que las reservas del vault han disminuido por el monto que retiramos + rendimiento.
"99"
¡Y ahí lo tienes! ¡Has desplegado e interactuado con éxito con el contrato del vault!
Es importante notar que este no es un contrato listo para producción y solo está destinado a demostrar las capacidades de la plataforma de contratos inteligentes Soroban. ¡Esperamos ver contratos de rendimiento mucho más complejos desplegados con Soroban en el futuro, y esperamos que tú seas parte de ello!