Fondo de Liquidez
El ejemplo de fondo de liquidez demuestra cómo escribir un contrato de fondo de liquidez de producto constante. Un fondo de liquidez es una forma automatizada de añadir liquidez para un conjunto de tokens que facilitará la conversión de activos entre ellos. Los usuarios pueden depositar una cierta cantidad de cada token en el fondo, recibiendo un número proporcional de "acciones de token." El usuario recibirá entonces una parte de las tarifas de conversión acumuladas cuando finalmente "cambie" sus acciones de token para recibir de vuelta sus tokens originales.
Los fondos de liquidez de Soroban son exclusivos de Soroban y no pueden interactuar con los fondos de liquidez AMM integrados de Stellar.
Implementar un fondo de liquidez personalizado debe hacerse con precaución. Los fondos de los usuarios están involucrados, así que se debe tener mucho cuidado para garantizar la seguridad y la transparencia. El ejemplo aquí no debe considerarse un contrato listo para usar. Por favor, utilízalo solo como referencia.
La red Stellar ya tiene funcionalidad de fondo de liquidez integrada en el protocolo base. Aprende más aquí.
Ejecutar el Ejemplo
Primero pasa por el proceso de Configuración para tener tu entorno de desarrollo configurado, luego clona la etiqueta v21.6.0
del repositorio soroban-examples
:
git clone -b v21.6.0 https://github.com/stellar/soroban-examples
O, salta la configuración del entorno de desarrollo y abre este ejemplo en Gitpod.
Para ejecutar las pruebas del ejemplo, navega al directorio liquidity_pool
y utiliza cargo test
.
cd liquidity_pool
cargo test
Deberías ver la salida:
running 1 test
test test::test ... ok
Código
Dado que nuestro fondo de liquidez emitirá su propio token para establecer el número de acciones que tiene el fondo, hemos creado un módulo token.rs
en este proyecto para contener la lógica que controla el contrato del token para esas acciones.
- lib.rs
- token.rs
#![no_std]
mod test;
mod token;
use num_integer::Roots;
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 {
TokenA = 0,
TokenB = 1,
TokenShare = 2,
TotalShares = 3,
ReserveA = 4,
ReserveB = 5,
}
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_a(e: &Env) -> Address {
e.storage().instance().get(&DataKey::TokenA).unwrap()
}
fn get_token_b(e: &Env) -> Address {
e.storage().instance().get(&DataKey::TokenB).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_a(e: &Env) -> i128 {
e.storage().instance().get(&DataKey::ReserveA).unwrap()
}
fn get_reserve_b(e: &Env) -> i128 {
e.storage().instance().get(&DataKey::ReserveB).unwrap()
}
fn get_balance(e: &Env, contract: Address) -> i128 {
token::Client::new(e, &contract).balance(&e.current_contract_address())
}
fn get_balance_a(e: &Env) -> i128 {
get_balance(e, get_token_a(e))
}
fn get_balance_b(e: &Env) -> i128 {
get_balance(e, get_token_b(e))
}
fn get_balance_shares(e: &Env) -> i128 {
get_balance(e, get_token_share(e))
}
fn put_token_a(e: &Env, contract: Address) {
e.storage().instance().set(&DataKey::TokenA, &contract);
}
fn put_token_b(e: &Env, contract: Address) {
e.storage().instance().set(&DataKey::TokenB, &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_a(e: &Env, amount: i128) {
e.storage().instance().set(&DataKey::ReserveA, &amount)
}
fn put_reserve_b(e: &Env, amount: i128) {
e.storage().instance().set(&DataKey::ReserveB, &amount)
}
fn burn_shares(e: &Env, amount: i128) {
let total = get_total_shares(e);
let share_contract = get_token_share(e);
token::Client::new(e, &share_contract).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);
}
fn transfer(e: &Env, token: Address, to: Address, amount: i128) {
token::Client::new(e, &token).transfer(&e.current_contract_address(), &to, &amount);
}
fn transfer_a(e: &Env, to: Address, amount: i128) {
transfer(e, get_token_a(e), to, amount);
}
fn transfer_b(e: &Env, to: Address, amount: i128) {
transfer(e, get_token_b(e), to, amount);
}
fn get_deposit_amounts(
desired_a: i128,
min_a: i128,
desired_b: i128,
min_b: i128,
reserve_a: i128,
reserve_b: i128,
) -> (i128, i128) {
if reserve_a == 0 && reserve_b == 0 {
return (desired_a, desired_b);
}
let amount_b = desired_a * reserve_b / reserve_a;
if amount_b <= desired_b {
if amount_b < min_b {
panic!("amount_b less than min")
}
(desired_a, amount_b)
} else {
let amount_a = desired_b * reserve_a / reserve_b;
if amount_a > desired_a || desired_a < min_a {
panic!("amount_a invalid")
}
(amount_a, desired_b)
}
}
// Metadata that is added on to the WASM custom section
contractmeta!(
key = "Description",
val = "Constant product AMM with a .3% swap fee"
);
pub trait LiquidityPoolTrait {
// Sets the token contract addresses for this pool
fn initialize(e: Env, token_wasm_hash: BytesN<32>, token_a: Address, token_b: Address);
// Returns the token contract address for the pool share token
fn share_id(e: Env) -> Address;
// Deposits token_a and token_b. Also mints pool shares for the "to" Identifier. The amount minted
// is determined based on the difference between the reserves stored by this contract, and
// the actual balance of token_a and token_b for this contract.
fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128);
// If "buy_a" is true, the swap will buy token_a and sell token_b. This is flipped if "buy_a" is false.
// "out" is the amount being bought, with in_max being a safety to make sure you receive at least that amount.
// swap will transfer the selling token "to" to this contract, and then the contract will transfer the buying token to "to".
fn swap(e: Env, to: Address, buy_a: bool, out: i128, in_max: i128);
// transfers share_amount of pool share tokens to this contract, burns all pools share tokens in this contracts, and sends the
// corresponding amount of token_a and token_b to "to".
// Returns amount of both tokens withdrawn
fn withdraw(e: Env, to: Address, share_amount: i128, min_a: i128, min_b: i128) -> (i128, i128);
fn get_rsrvs(e: Env) -> (i128, i128);
}
#[contract]
struct LiquidityPool;
#[contractimpl]
impl LiquidityPoolTrait for LiquidityPool {
fn initialize(e: Env, token_wasm_hash: BytesN<32>, token_a: Address, token_b: Address) {
if token_a >= token_b {
panic!("token_a must be less than token_b");
}
let share_contract = create_contract(&e, token_wasm_hash, &token_a, &token_b);
token::Client::new(&e, &share_contract).initialize(
&e.current_contract_address(),
&7u32,
&"Pool Share Token".into_val(&e),
&"POOL".into_val(&e),
);
put_token_a(&e, token_a);
put_token_b(&e, token_b);
put_token_share(&e, share_contract.try_into().unwrap());
put_total_shares(&e, 0);
put_reserve_a(&e, 0);
put_reserve_b(&e, 0);
}
fn share_id(e: Env) -> Address {
get_token_share(&e)
}
fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128) {
// Depositor needs to authorize the deposit
to.require_auth();
let (reserve_a, reserve_b) = (get_reserve_a(&e), get_reserve_b(&e));
// Calculate deposit amounts
let (amount_a, amount_b) =
get_deposit_amounts(desired_a, min_a, desired_b, min_b, reserve_a, reserve_b);
if amount_a <= 0 || amount_b <= 0 {
// If one of the amounts can be zero, we can get into a situation
// where one of the reserves is 0, which leads to a divide by zero.
panic!("both amounts must be strictly positive");
}
let token_a_client = token::Client::new(&e, &get_token_a(&e));
let token_b_client = token::Client::new(&e, &get_token_b(&e));
token_a_client.transfer(&to, &e.current_contract_address(), &amount_a);
token_b_client.transfer(&to, &e.current_contract_address(), &amount_b);
// Now calculate how many new pool shares to mint
let (balance_a, balance_b) = (get_balance_a(&e), get_balance_b(&e));
let total_shares = get_total_shares(&e);
let zero = 0;
let new_total_shares = if reserve_a > zero && reserve_b > zero {
let shares_a = (balance_a * total_shares) / reserve_a;
let shares_b = (balance_b * total_shares) / reserve_b;
shares_a.min(shares_b)
} else {
(balance_a * balance_b).sqrt()
};
mint_shares(&e, to, new_total_shares - total_shares);
put_reserve_a(&e, balance_a);
put_reserve_b(&e, balance_b);
}
fn swap(e: Env, to: Address, buy_a: bool, out: i128, in_max: i128) {
to.require_auth();
let (reserve_a, reserve_b) = (get_reserve_a(&e), get_reserve_b(&e));
let (reserve_sell, reserve_buy) = if buy_a {
(reserve_b, reserve_a)
} else {
(reserve_a, reserve_b)
};
// First calculate how much needs to be sold to buy amount out from the pool
let n = reserve_sell * out * 1000;
let d = (reserve_buy - out) * 997;
let sell_amount = (n / d) + 1;
if sell_amount > in_max {
panic!("in amount is over max")
}
// Transfer the amount being sold to the contract
let sell_token = if buy_a {
get_token_b(&e)
} else {
get_token_a(&e)
};
let sell_token_client = token::Client::new(&e, &sell_token);
sell_token_client.transfer(&to, &e.current_contract_address(), &sell_amount);
let (balance_a, balance_b) = (get_balance_a(&e), get_balance_b(&e));
// residue_numerator and residue_denominator are the amount that the invariant considers after
// deducting the fee, scaled up by 1000 to avoid fractions
let residue_numerator = 997;
let residue_denominator = 1000;
let zero = 0;
let new_invariant_factor = |balance: i128, reserve: i128, out: i128| {
let delta = balance - reserve - out;
let adj_delta = if delta > zero {
residue_numerator * delta
} else {
residue_denominator * delta
};
residue_denominator * reserve + adj_delta
};
let (out_a, out_b) = if buy_a { (out, 0) } else { (0, out) };
let new_inv_a = new_invariant_factor(balance_a, reserve_a, out_a);
let new_inv_b = new_invariant_factor(balance_b, reserve_b, out_b);
let old_inv_a = residue_denominator * reserve_a;
let old_inv_b = residue_denominator * reserve_b;
if new_inv_a * new_inv_b < old_inv_a * old_inv_b {
panic!("constant product invariant does not hold");
}
if buy_a {
transfer_a(&e, to, out_a);
} else {
transfer_b(&e, to, out_b);
}
let new_reserve_a = balance_a - out_a;
let new_reserve_b = balance_b - out_b;
if new_reserve_a <= 0 || new_reserve_b <= 0 {
panic!("new reserves must be strictly positive");
}
put_reserve_a(&e, new_reserve_a);
put_reserve_b(&e, new_reserve_b);
}
fn withdraw(e: Env, to: Address, share_amount: i128, min_a: i128, min_b: i128) -> (i128, i128) {
to.require_auth();
// First transfer the pool 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(), &share_amount);
let (balance_a, balance_b) = (get_balance_a(&e), get_balance_b(&e));
let balance_shares = get_balance_shares(&e);
let total_shares = get_total_shares(&e);
// Now calculate the withdraw amounts
let out_a = (balance_a * balance_shares) / total_shares;
let out_b = (balance_b * balance_shares) / total_shares;
if out_a < min_a || out_b <TabItem min_b {
panic!("min not satisfied");
}
burn_shares(&e, balance_shares);
transfer_a(&e, to.clone(), out_a);
transfer_b(&e, to, out_b);
put_reserve_a(&e, balance_a - out_a);
put_reserve_b(&e, balance_b - out_b);
(out_a, out_b)
}
fn get_rsrvs(e: Env) -> (i128, i128) {
(get_reserve_a(&e), get_reserve_b(&e))
}
}
#![allow(unused)]
use soroban_sdk::{xdr::ToXdr, Address, Bytes, BytesN, Env};
soroban_sdk::contractimport!(
file = "../token/target/wasm32-unknown-unknown/release/soroban_token_contract.wasm"
);
pub fn create_contract(
e: &Env,
token_wasm_hash: BytesN<32>,
token_a: &Address,
token_b: &Address,
) -> Address {
let mut salt = Bytes::new(e);
salt.append(&token_a.to_xdr(e));
salt.append(&token_b.to_xdr(e));
let salt = e.crypto().sha256(&salt);
e.deployer()
.with_current_contract(salt)
.deploy(token_wasm_hash)
}
Ref: https://github.com/stellar/soroban-examples/tree/v21.6.0/liquidity_pool
Cómo Funciona
Cada activo creado en Stellar comienza con cero liquidez. Lo mismo es cierto para los tokens creados en Soroban (a menos que un activo de Stellar con un token de liquidez existente sea "envuelto" para su uso en Soroban). En términos simples, "liquidez" significa cuántos de un activo en un mercado están disponibles para ser comprados o vendidos. En los "viejos tiempos," podías generar liquidez en un mercado creando órdenes de compra/venta en un libro de órdenes.
Los fondos de liquidez automatizan este proceso sustituyendo las órdenes con matemáticas. Los depositantes en el fondo de liquidez ganan tarifas de las transacciones de swap
. ¡No se requieren órdenes!
Abre el archivo liquidity_pool/src/lib.rs
o consulta el código anterior para seguir adelante.
Inicializar el Contrato
Cuando este contrato se despliega por primera vez, podría crear un fondo de liquidez para cualquier par de tokens disponibles en Soroban. Primero debe inicializarse con la siguiente información:
token_wasm_hash
: El contrato terminará creando su propio tokenPOOL
así como [interactuando con los contratos paratoken_a
ytoken_b
]. La forma en que funciona este ejemplo es usando el [contrato de ejemplotoken
] para ambos trabajos. Cuando se inicializa nuestro contrato de fondo de liquidez, desea que le pasemos el hash wasm del contrato de token ya instalado. Luego desplegará un contrato que ejecutará el bytecode WASM almacenado en ese hash como un nuevo contrato token para los tokensPOOL
.token_a
: LaDirección
del contrato para un token ya desplegado (o envuelto) que se mantendrá en reserva por el fondo de liquidez.token_b
: LaDirección
del contrato para un token ya desplegado (o envuelto) que se mantendrá en reserva por el fondo de liquidez.
Ten en cuenta que cuál token es token_a
y cuál es token_b
no es una distinción arbitraria. De acuerdo con los fondos de liquidez integrados en Stellar, este contrato puede hacer únicamente un fondo de liquidez para un conjunto dado de tokens. Por lo tanto, las direcciones de los tokens deben proporcionarse en orden lexicográfico en el momento de la inicialización.
fn initialize(e: Env, token_wasm_hash: BytesN<32>, taken_a: Address, token_b: Address) {
if token_a >= token_b {
panic!("token_a must be less than token_b");
}
// The initialization function also stores important information in the contract's instance storage
put_token_a(&e, token_a);
put_token_b(&e, token_b);
put_token_share(&e, share_contract.try_into().unwrap());
put_total_shares(&e, 0);
put_reserve_a(&e, 0);
put_reserve_b(&e, 0);
}
Un Fondo de Liquidez de "Producto Constante"
El tipo de fondo de liquidez que implementa este contrato de ejemplo se llama un fondo de liquidez de "producto constante". Aunque este no es el único tipo de fondo de liquidez que existe, es la variedad más común. Estos fondos de liquidez están diseñados para mantener el valor total de cada activo en equilibrio relativo. El "producto" en el producto constante (también llamado "invariante") cambiará cada vez que se interactúe con el fondo de liquidez (depósito, retiro o intercambios de tokens). Sin embargo, el invariante debe solo aumentar con cada interacción.
Durante un intercambio, lo que debe tenerse en cuenta es que por cada retiro del lado de token_a
, debes "rellenar" el lado de token_b
con una cantidad suficiente para mantener el precio del fondo de liquidez equilibrado. La matemática es predecible, pero no es lineal. Cuanto más tomes de un lado, más debes dar en el lado opuesto exponencialmente.
Dentro de la función swap
, la matemática se hace así (sin embargo, esta es una versión simplificada):
fn swap(e: Env, to: Address, buy_a: bool, out: i128, in_max: i128) {
// Get the current balances of both tokens in the liquidity pool
let (reserve_sell, reserve_buy) = (get_reserve_a(&e), get_reserve_b(&e));
// Calculate how much needs to be
let n = reserve_sell * out * 1000;
let d = (reserve_buy - out) * 997;
let sell_amount = (n / d) + 1;
}
Tenemos información mucho más detallada sobre cómo funciona este tipo de fondo de liquidez disponible en Stellar Quest: Serie 3, Misión 5. Esta es una forma muy útil e interactiva de aprender más sobre cómo funcionan los fondos de liquidez integrados en Stellar. Gran parte del conocimiento que podrías obtener de allí se traducirá fácilmente a este contrato de ejemplo.
Interactuar con contratos de tokens en otro contrato
Este contrato de fondo de liquidez funcionará con un total de tres tokens diferentes de Soroban:
POOL
: Este token es un token único que se otorga a los depositantes de activos a cambio de su depósito. Estos tokens son "cambiados" por el usuario cuando retira alguna cantidad de su depósito original (más cualquier tarifa de intercambio obtenida). Este contrato de ejemplo implementa el mismo [contrato de ejemplotoken
] para este token.token_a
ytoken_b
: Serán los dos "tokens en reserva" que los usuarios depositarán en el fondo. Estos podrían ser tokens "envueltos" de activos de Stellar preexistentes, o podrían ser tokens nativos de Soroban. A este contrato realmente no le importa, siempre que las funciones que necesita de la Interfaz de Token común estén disponibles en el contrato del token.
Creando un Token POOL
Personalizado para Acciones LP
Estamos utilizando el contrato de ejemplo token
compilado como nuestro contrato de activo para el token POOL
. Esto significa que sigue todas las convenciones de la Interfaz de Token, y puede ser tratado igual que cualquier otro token. Podrían ser transferidos, quemados, acuñados, etc. También significa que el desarrollador de LP podría aprovechar las características administrativas como la recuperación, autorización y más.
El archivo token.rs
contiene una función create_contract
que utilizaremos para desplegar este contrato token particular.
pub fn create_contract(
e: &Env,
token_wasm_hash: BytesN<32>,
token_a: &Address,
token_b: &Address,
) -> Address {
let mut salt = Bytes::new(e);
salt.append(&token_a.to_xdr(e));
salt.append(&token_b.to_xdr(e));
let salt = e.crypto().sha256(&salt);
e.deployer()
.with_current_contract(salt)
.deploy(token_wasm_hash)
}
Este contrato del token POOL
se crea dentro de la función initialize
.
fn initialize(e: Env, token_wasm_hash: BytesN<32>, token_a: Address, token_b: Address) {
let share_contract = create_contract(&e, token_wasm_hash, &token_a, &token_b);
token::Client::new(&e, &share_contract).initialize(
&e.current_contract_address(),
&7u32,
&"Pool Share Token".into_val(&e),
&"POOL".into_val(&e),
);
}
Luego, durante un deposito
, una cantidad calculada de tokens POOL
son mint
ed a la dirección depositante.
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);
}
¿Cómo se calcula ese número de acciones, preguntas? ¡Excelente pregunta! Si es el primer depósito (ver arriba), es solo la raíz cuadrada del producto de las cantidades de token_a
y token_b
depositados. Muy simple.
Sin embargo, si ya ha habido depósitos en el fondo de liquidez, y el usuario solo está añadiendo más tokens al fondo, hay un poco más de matemáticas. Sin embargo, el punto principal es que cada depositante recibe la misma proporción de tokens POOL
por su depósito que cada otro depositante.
fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128) {
let zero = 0;
let new_total_shares = if reserve_a > zero && reserve_b > zero {
// Note balance_a and balance_b at this point in the function include
// the tokens the user is currently depositing, whereas reserve_a and
// reserve_b do not yet.
let shares_a = (balance_a * total_shares) / reserve_a;
let shares_b = (balance_b * total_shares) / reserve_b;
shares_a.min(shares_b)
} else {
(balance_a * balance_b).sqrt()
};
}
Transferencias de Tokens de/a Contrato LP
Como ya hemos discutido, el contrato de fondo de liquidez utilizará la Interfaz de Token disponible en los contratos de tokens que se suministraron como argumentos token_a
y token_b
en el momento de la inicialización. A lo largo del resto del contrato, el fondo de liquidez utilizará esa interfaz para hacer transferencias de esos tokens de/a sí mismo.
Lo que sucede es que, a medida que un usuario deposita tokens en el fondo, el contrato invoca la función transfer
para mover los tokens de la dirección to
(el depositante) para ser mantenidos por la dirección del contrato. Los tokens POOL
son luego acuñados al depositante (ver sección anterior). ¡Bastante simple, verdad!?
fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128) {
// Depositor needs to authorize the deposit
to.require_auth();
let token_a_client = token::Client::new(&e, &get_token_a(&e));
let token_b_client = token::Client::new(&e, &get_token_b(&e));
token_a_client.transfer(&to, &e.current_contract_address(), &amounts.0);
token_b_client.transfer(&to, &e.current_contract_address(), &amounts.1);
mint_shares(&e, to, new_total_shares - total_shares);
}
En contraste, cuando un usuario retira sus tokens depositados, es un poco más complejo, y ocurre el siguiente procedimiento.
- Se transfiere una cierta cantidad del token
POOL
del depositante a la dirección del contrato. Esta es una forma temporal de rastrear cuántos tokensPOOL
están siendo canjeados. El contrato no mantendrá este saldo dePOOL
por mucho tiempo. - Los montos a retirar para los tokens en reserva se calculan en base al saldo actual de tokens
POOL
del contrato. - Los tokens
POOL
son destruidos ahora que se han calculado los montos a retirar, y ya no se necesitan. - Las cantidades respectivas de
token_a
ytoken_b
se transfieren desde la dirección del contrato a la direcciónto
(el depositante).
fn withdraw(e: Env, to: Address, share_amount: i128, min_a: i128, min_b: i128) -> (i128, i128) {
to.require_auth();
// First transfer the pool 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(), &share_amount);
// Now calculate the withdraw amounts
let out_a = (balance_a * balance_shares) / total_shares;
let out_b = (balance_b * balance_shares) / total_shares;
burn_shares(&e, balance_shares);
transfer_a(&e, to.clone(), out_a);
transfer_b(&e, to, out_b);
}
Notarás que al mantener el saldo de token_a
y token_b
en el contrato de fondo de liquidez, se facilita mucho realizar cualquier acción de la Interfaz de Token dentro del contrato. Como un bono, cualquier observador externo podría consultar los saldos de token_a
o token_b
mantenidos por el contrato para verificar que las reservas están realmente alineadas con los valores que el contrato informa cuando se invoca su propia función get_rsvs
.
Pruebas
Abre el archivo liquidity_pool/src/test.rs
para seguir adelante.
#![cfg(test)]
extern crate std;
use crate::{token, LiquidityPoolClient};
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_liqpool_contract<'a>(
e: &Env,
token_wasm_hash: &BytesN<32>,
token_a: &Address,
token_b: &Address,
) -> LiquidityPoolClient<'a> {
let liqpool = LiquidityPoolClient::new(e, &e.register_contract(None, crate::LiquidityPool {}));
liqpool.initialize(token_wasm_hash, token_a, token_b);
liqpool
}
fn install_token_wasm(e: &Env) -> BytesN<32> {
soroban_sdk::contractimport!(
file = "../token/target/wasm32-unknown-unknown/release/soroban_token_contract.wasm"
);
e.deployer().upload_contract_wasm(WASM)
}
#[test]
fn test() {
let e = Env::default();
e.mock_all_auths();
let mut admin1 = Address::random(&e);
let mut admin2 = Address::random(&e);
let mut token1 = create_token_contract(&e, &admin1);
let mut token2 = create_token_contract(&e, &admin2);
if &token2.address < &token1.address {
std::mem::swap(&mut token1, &mut token2);
std::mem::swap(&mut admin1, &mut admin2);
}
let user1 = Address::random(&e);
let liqpool = create_liqpool_contract(
&e,
&install_token_wasm(&e),
&token1.address,
&token2.address,
);
let token_share = token::Client::new(&e, &liqpool.share_id());
token1.mint(&user1, &1000);
assert_eq!(token1.balance(&user1), 1000);
token2.mint(&user1, &1000);
assert_eq!(token2.balance(&user1), 1000);
liqpool.deposit(&user1, &100, &100, &100, &100);
assert_eq!(
e.auths(),
std::vec![(
user1.clone(),
AuthorizedInvocation {
function: AuthorizedFunction::Contract((
liqpool.address.clone(),
symbol_short!("deposit"),
(&user1, 100_i128, 100_i128, 100_i128, 100_i128).into_val(&e)
)),
sub_invocations: std::vec![
AuthorizedInvocation {
function: AuthorizedFunction::Contract((
token1.address.clone(),
symbol_short!("transfer"),
(&user1, &liqpool.address, 100_i128).into_val(&e)
)),
sub_invocations: std::vec![]
},
AuthorizedInvocation {
function: AuthorizedFunction::Contract((
token2.address.clone(),
symbol_short!("transfer"),
(&user1, &liqpool.address, 100_i128).into_val(&e)
)),
sub_invocations: std::vec![]
}
]
}
)]
);
assert_eq!(token_share.balance(&user1), 100);
assert_eq!(token_share.balance(&liqpool.address), 0);
assert_eq!(token1.balance(&user1), 900);
assert_eq!(token1.balance(&liqpool.address), 100);
assert_eq!(token2.balance(&user1), 900);
assert_eq!(token2.balance(&liqpool.address), 100);
liqpool.swap(&user1, &false, &49, &100);
assert_eq!(
e.auths(),
std::vec![(
user1.clone(),
AuthorizedInvocation {
function: AuthorizedFunction::Contract((
liqpool.address.clone(),
symbol_short!("swap"),
(&user1, false, 49_i128, 100_i128).into_val(&e)
)),
sub_invocations: std::vec![AuthorizedInvocation {
function: AuthorizedFunction::Contract((
token1.address.clone(),
symbol_short!("transfer"),
(&user1, &liqpool.address, 97_i128).into_val(&e)
)),
sub_invocations: std::vec![]
}]
}
)]
);
assert_eq!(token1.balance(&user1), 803);
assert_eq!(token1.balance(&liqpool.address), 197);
assert_eq!(token2.balance(&user1), 949);
assert_eq!(token2.balance(&liqpool.address), 51);
e.budget().reset_unlimited();
liqpool.withdraw(&user1, &100, &197, &51);
assert_eq!(
e.auths(),
std::vec![(
user1.clone(),
AuthorizedInvocation {
function: AuthorizedFunction::Contract((
liqpool.address.clone(),
symbol_short!("withdraw"),
(&user1, 100_i128, 197_i128, 51_i128).into_val(&e)
)),
sub_invocations: std::vec![AuthorizedInvocation {
function: AuthorizedFunction::Contract((
token_share.address.clone(),
symbol_short!("transfer"),
(&user1, &liqpool.address, 100_i128).into_val(&e)
)),
sub_invocations: std::vec![]
}]
}
)]
);
assert_eq!(token1.balance(&user1), 1000);
assert_eq!(token2.balance(&user1), 1000);
assert_eq!(token_share.balance(&user1), 0);
assert_eq!(token1.balance(&liqpool.address), 0);
assert_eq!(token2.balance(&liqpool.address), 0);
assert_eq!(token_share.balance(&liqpool.address), 0);
}
En cualquier prueba, lo primero que siempre se requiere es un Env
, que es el entorno Soroban en el que se ejecutará el contrato.
let e = Env::default();
Simulamos cheques de autenticación en las pruebas, lo que permite que las pruebas avancen como si todos los usuarios/direcciones/contratos, etc. se hubieran autenticado con éxito.
e.mock_all_auths();
Hemos abstraído en algunas funciones las tareas de creación de contratos de tokens, desplegar un contrato de fondo de liquidez, e instalar el bytecode WASM de ejemplo de token en nuestro entorno de prueba. Cada una se utiliza luego dentro de la prueba.
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_liqpool_contract<'a>(
e: &Env,
token_wasm_hash: &BytesN<32>,
token_a: &Address,
token_b: &Address,
) -> LiquidityPoolClient<'a> {
let liqpool = LiquidityPoolClient::new(e, &e.register_contract(None, crate::LiquidityPool {}));
liqpool.initialize(token_wasm_hash, token_a, token_b);
liqpool
}
fn install_token_wasm(e: &Env) -> BytesN<32> {
soroban_sdk::contractimport!(
file = "../token/target/wasm32-unknown-unknown/release/soroban_token_contract.wasm"
);
e.deployer().upload_contract_wasm(WASM)
}
Todas las funciones públicas dentro de un bloque impl
que está anotado con el atributo #[contractimpl]
tienen una función correspondiente generada en un tipo de cliente generado. El tipo de cliente se nombrará igual que el tipo de contrato con Client
añadido. Por ejemplo, en nuestro contrato, el tipo de contrato es LiquidityPool
, y el cliente se llama LiquidityPoolClient
.
Estas pruebas examinan el uso "típico" de un fondo de liquidez, asegurando que los saldos, retornos, etc. sean apropiados en varios puntos durante la prueba.
- Primero, la prueba configura todo con un
Env
, dos direcciones administrativas, dos tokens en reserva, una dirección generada aleatoriamente para actuar como el usuario del fondo de liquidez, el fondo de liquidez mismo, un contrato de acciones de token del fondo, y acuña los activos de reserva a la dirección del usuario. - El usuario luego deposita algunos de cada activo en el fondo de liquidez. En este momento, se realizan las siguientes comprobaciones:
- existen autorizaciones apropiadas para depósitos y transferencias,
- los saldos se verifican para cada token (
token_a
,token_b
, yPOOL
) desde la perspectiva del usuario y de la perspectiva del contratoliqpool
- El usuario realiza un intercambio, comprando
token_b
a cambio detoken_a
. Ahora se realizan las mismas comprobaciones que en el paso anterior, exceptuando los saldos dePOOL
, ya que un intercambio no tiene efecto sobre los tokensPOOL
. - El usuario luego retira todos los depósitos que realizó, cambiando todos sus tokens
POOL
en el proceso. Aquí se realizan las mismas comprobaciones que se hicieron en el paso dedepósito
.
Crear el Contrato
Para crear el contrato, usa el comando stellar contract build
.
stellar contract build
Un archivo .wasm
debería ser generado en el directorio target
:
target/wasm32-unknown-unknown/release/soroban_liquidity_pool_contract.wasm
Ejecutar el Contrato
Si tienes stellar-cli
instalado, puedes invocar funciones del contrato usándolo.
- macOS/Linux
- Windows (PowerShell)
stellar contract invoke \
--wasm target/wasm32-unknown-unknown/release/soroban_liquidity_pool_contract.wasm \
--id 1 \
-- \
deposit \
--to GBZV3NONYSUDVTEHATQO4BCJVFXJO3XQU5K32X3XREVZKSMMOZFO4ZXR \
--desired_a 100 \
--min_a 98 \
--desired_be 200 \
--min_b 196
stellar contract invoke `
--wasm target/wasm32-unknown-unknown/release/soroban_liquidity_pool_contract.wasm `
--id 1 `
-- `
deposit `
--to GBZV3NONYSUDVTEHATQO4BCJVFXJO3XQU5K32X3XREVZKSMMOZFO4ZXR `
--desired_a 100 `
--min_a 98 `
--desired_be 200 `
--min_b 196