Interfaz de Token
Los contratos de token, incluyendo el Contrato de Activo Stellar y ejemplos de implementaciones de tokens, exponen la siguiente interfaz común.
Los tokens desplegados en Soroban pueden implementar cualquier interfaz que elijan, sin embargo, deberían cumplir con la siguiente interfaz para ser interoperables con contratos construidos para admitir los tokens integrados de Soroban.
Nota que, en casos específicos, la interfaz no tiene que ser implementada completamente. Por ejemplo, el token personalizado puede no implementar la interfaz administrativa compatible con el Contrato de Activo Stellar; esto no impedirá que sea utilizable en los contratos que solo realizan las operaciones de usuario regulares (transferencias, asignaciones, balances, etc.).
Requisitos de Compatibilidad
Para cualquier función de contrato dada, hay 3 requisitos que deberían ser consistentes con la interfaz descrita aquí:
- Interfaz de función (nombre y argumentos) - si no es consistente, entonces los usuarios simplemente no podrán usar la función en absoluto. Este es el requisito firme.
- Autorización - los usuarios tienen que autorizar las llamadas a las funciones del token con todos los argumentos de la invocación (ver los comentarios de la interfaz). Si esto es inconsistente, entonces el token personalizado puede tener incidencias para obtener las firmas correctas de los usuarios y también puede confundir el software de billetera.
- Eventos - el token tiene que emitir los eventos en el formato especificado. Si es inconsistente, entonces el token puede no ser manejado correctamente por los sistemas posteriores, como los exploradores de bloques.
Código
La interfaz a continuación utiliza el Rust soroban-sdk para declarar un rasgo que cumple con la interfaz de token SEP-41.
pub trait TokenInterface {
/// Returns the allowance for `spender` to transfer from `from`.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens to be drawn from.
/// * `spender` - The address spending the tokens held by `from`.
fn allowance(env: Env, from: Address, spender: Address) -> i128;
/// Set the allowance by `amount` for `spender` to transfer/burn from
/// `from`.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens to be drawn from.
/// * `spender` - The address being authorized to spend the tokens held by
/// `from`.
/// * `amount` - The tokens to be made available to `spender`.
/// * `expiration_ledger` - The ledger number where this allowance expires. Cannot
/// be less than the current ledger number unless the amount is being set to 0.
/// An expired entry (where expiration_ledger < the current ledger number)
/// should be treated as a 0 amount allowance.
///
/// # Events
///
/// Emits an event with topics `["approve", from: Address,
/// spender: Address], data = [amount: i128, expiration_ledger: u32]`
fn approve(env: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32);
/// Returns the balance of `id`.
///
/// # Arguments
///
/// * `id` - The address for which a balance is being queried. If the
/// address has no existing balance, returns 0.
fn balance(env: Env, id: Address) -> i128;
/// Transfer `amount` from `from` to `to`.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens which will be
/// withdrawn from.
/// * `to` - The address which will receive the transferred tokens.
/// * `amount` - The amount of tokens to be transferred.
///
/// # Events
///
/// Emits an event with topics `["transfer", from: Address, to: Address],
/// data = amount: i128`
fn transfer(env: Env, from: Address, to: Address, amount: i128);
/// Transfer `amount` from `from` to `to`, consuming the allowance of
/// `spender`. Authorized by spender (`spender.require_auth()`).
///
/// # Arguments
///
/// * `spender` - The address authorizing the transfer, and having its
/// allowance consumed during the transfer.
/// * `from` - The address holding the balance of tokens which will be
/// withdrawn from.
/// * `to` - The address which will receive the transferred tokens.
/// * `amount` - The amount of tokens to be transferred.
///
/// # Events
///
/// Emits an event with topics `["transfer", from: Address, to: Address],
/// data = amount: i128`
fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128);
/// Burn `amount` from `from`.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens which will be
/// burned from.
/// * `amount` - The amount of tokens to be burned.
///
/// # Events
///
/// Emits an event with topics `["burn", from: Address], data = amount:
/// i128`
fn burn(env: Env, from: Address, amount: i128);
/// Burn `amount` from `from`, consuming the allowance of `spender`.
///
/// # Arguments
///
/// * `spender` - The address authorizing the burn, and having its allowance
/// consumed during the burn.
/// * `from` - The address holding the balance of tokens which will be
/// burned from.
/// * `amount` - The amount of tokens to be burned.
///
/// # Events
///
/// Emits an event with topics `["burn", from: Address], data = amount:
/// i128`
fn burn_from(env: Env, spender: Address, from: Address, amount: i128);
/// Returns the number of decimals used to represent amounts of this token.
///
/// # Panics
///
/// If the contract has not yet been initialized.
fn decimals(env: Env) -> u32;
/// Returns the name for this token.
///
/// # Panics
///
/// If the contract has not yet been initialized.
fn name(env: Env) -> String;
/// Returns the symbol for this token.
///
/// # Panics
///
/// If the contract has not yet been initialized.
fn symbol(env: Env) -> String;
}
La función approve
sobrescribe el valor anterior con amount
, por lo que es posible que la asignación anterior sea gastada en una transacción anterior antes de que amount
sea escrito en una transacción posterior. El resultado de esto es que spender
puede gastar más de lo previsto. Este problema puede evitarse estableciendo primero la asignación a 0, verificando que el spender no haya gastado ninguna parte de la asignación anterior, y luego estableciendo la asignación al nuevo monto deseado. Puedes leer más sobre este problema aquí - https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729.
Metadatos
Otro requisito para cumplir con la interfaz de token es escribir los metadatos estándar (decimal
, name
, y symbol
) para el token en un formato específico. Este formato permite a los usuarios leer datos constantes directamente del ledger en lugar de invocar una función de Wasm. El ejemplo de token demuestra cómo usar el Rust soroban-token-sdk para escribir los metadatos, y alentamos encarecidamente a las implementaciones de tokens a seguir este enfoque.
Manejo de Condiciones de Fallo
En la interfaz de token, hay varias instancias donde las llamadas a funciones pueden fallar por varias razones, como la falta de autorización adecuada, asignación insuficiente o balance, etc. Para manejar estas condiciones de fallo, es importante especificar el comportamiento esperado cuando surgen tales situaciones.
Es importante notar que la interfaz de token no solo incorpora el concepto de autorización para igualar la autorización de activos en Stellar Classic, sino que también utiliza el mecanismo de autorización de Soroban. Así que, si intentas hacer una llamada de token y falla, podría deberse a alguno de los procesos de autorización de token.
Para proporcionar más contexto, cuando usas la interfaz de token, hay una función llamada authorized
que devuelve "true" si una dirección tiene autorización de token.
Más detalles sobre la autorización se pueden encontrar aquí.
Para las funciones en la interfaz de token, se debe utilizar trapping como la forma estándar de manejar condiciones de fallo, ya que la interfaz no está diseñada para devolver códigos de error. Esto significa que cuando una función encuentra un error, se detendrá la ejecución y se revertirán cualquier cambio de estado que ocurriera durante la llamada a la función.
Condiciones de Fallo
Aquí hay una lista de condiciones de fallo básicas y su comportamiento esperado para funciones en la interfaz de token:
Funciones de administrador:
- Si el administrador no autorizó la llamada, la función debería interrumpirse.
- Si el administrador intenta realizar una acción no válida (por ejemplo, acuñar una cantidad negativa), la función debería interrumpirse.
Funciones de token:
- Si el llamador no está autorizado para realizar la acción (por ejemplo, transferir tokens sin la autorización adecuada), la función debería interrumpirse.
- Si la acción resultaría en un estado no válido (por ejemplo, transferir más tokens que los disponibles en el balance o asignación), la función debería interrumpirse.
Ejemplo: Manejo de Asignación Insuficiente en la función burn_from
En la función burn_from
, el contrato de token debería verificar si el spender tiene suficiente asignación para quemar la cantidad especificada de tokens de la dirección from
. Si la asignación es insuficiente, la función debería interrumpirse, deteniendo la ejecución y revirtiendo cualquier cambio de estado.
Aquí hay un ejemplo de cómo se puede modificar la función burn_from
para manejar esta condición de fallo:
fn burn_from(
env: soroban_sdk::Env,
spender: Address,
from: Address,
amount: i128,
) {
// Check if the spender has enough allowance
let current_allowance = allowance(env, from, spender);
if current_allowance < amount {
// Trap if the allowance is insufficient
panic!("Insufficient allowance");
}
// Proceed with burning tokens
// ...
}
Al delinear claramente cómo manejar los fallos e incorporar las técnicas adecuadas de gestión de errores en la interfaz de token, podemos hacer que los contratos de token sean más robustos y seguros.