La extensión interest-bearing de Token-2022 permite que un token mint acumule intereses automáticamente en todas las cuentas de tokens para ese mint específico. Utiliza una tasa anual definida en la configuración on-chain del mint.
Sin embargo, este interés es una vista calculada: el balance real de tokens nunca cambia on-chain. En su lugar, las wallets y aplicaciones aplican una fórmula de interés compuesto continuo al balance on-chain de cada usuario para calcular su balance neto de los intereses acumulados. El desarrollador puede combinar el interés calculado y el balance on-chain en un solo valor o mostrarlos por separado.
Esta extensión proporciona el mecanismo contable para la acumulación de intereses; depende de aplicaciones DeFi separadas para proporcionar un respaldo económico a dichos intereses.
En este artículo, desglosaremos la arquitectura de la extensión interest-bearing, explicaremos las matemáticas detrás de los cálculos, cubriremos la compatibilidad con wallets y recorreremos un ejemplo práctico de implementación con Anchor.
Arquitectura de la extensión interest-bearing
Como discutimos en nuestro artículo sobre Token-2022, las extensiones de tokens son características modulares que se añaden a un mint o a una cuenta de tokens. El diseño (layout) base de una cuenta mint es de 82 bytes, y el de una cuenta de tokens es de 165 bytes. Los datos de la extensión se añaden después de esos tamaños base, por lo que debes asignar un espacio igual al tamaño base más el tamaño de cualquier extensión habilitada antes de crear el mint o la cuenta de tokens.
El espacio asignado para la extensión interest-bearing almacena los datos de la extensión, incluyendo un campo de cuenta de autoridad que puede actualizar las tasas de interés. Si el campo de la cuenta de autoridad es todo ceros, se trata como None, lo que significa que la tasa de interés permanece inmutable. En la práctica, la autoridad se puede configurar a una aplicación DeFi, la cual luego ajusta la tasa de interés para reflejar la actividad económica real en la aplicación.
Además del campo de autoridad, el struct de Rust a continuación (el cual hemos tomado directamente del código fuente de la extensión) define los datos completos de la extensión interest-bearing:
- El timestamp de inicialización (
initialization_timestamp), que sirve como el tiempo de inicio para todos los cálculos de intereses - La tasa de interés promedio (
pre_update_average_rate) desde la inicialización hasta la última vez que se actualizó la tasa. - El timestamp del último cambio en la tasa de interés (
last_update_timestamp), utilizado para calcular los intereses acumulados - La tasa de interés actual (
current_rate) aplicada desde el último timestamp de actualización.
/// Annual interest rate, expressed as basis points
pub type BasisPoints = PodI16;
const ONE_IN_BASIS_POINTS: f64 = 10_000.;
const SECONDS_PER_YEAR: f64 = 60. * 60. * 24. * 365.24;
pub struct InterestBearingConfig {
/// Authority that can set the interest rate and authority
pub rate_authority: OptionalNonZeroPubkey,
/// Timestamp of initialization, from which to base interest calculations
pub initialization_timestamp: UnixTimestamp,
/// Average rate from initialization until the last time the rate was updated
pub pre_update_average_rate: BasisPoints,
/// Timestamp of the last update, used to calculate the total amount accrued
pub last_update_timestamp: UnixTimestamp,
/// Current rate, since the last update
pub current_rate: BasisPoints,
}
Diseño Type-Length-Value (TLV) de la extensión interest-bearing
Todas las extensiones de Token-2022 siguen un formato Type-Length-Value (TLV), el cual permite a los programas leer y omitir fácilmente los diferentes datos de extensión almacenados en una cuenta.
La entrada TLV de InterestBearingConfig se codifica como:
- T (
type):0x0A(el identificador de tipo paraInterestBearingConfig) - L (
length):0x34(u8, valor = 52 en decimal) - V (
value): campos serializados concatenados en orden:rate_authority(32 bytes)initialization_timestamp(8 bytes)pre_update_average_rate(2 bytes)last_update_timestamp(8 bytes)current_rate(2 bytes)
Los campos pre_update_average_rate y current_rate no se almacenan como números de punto flotante. En su lugar, se almacenan como puntos básicos.
1 punto básico = 1/100 (0.01%)
Entonces, para representar una tasa de interés anual del 2.50%, almacenarías el número entero 250 (porque 250 puntos básicos equivalen a 250*1/100=2.5%) en el campo current_rate. Para convertir de un porcentaje a puntos básicos, simplemente divide entre 0.01 (o equivalentemente, multiplica por 100). En este ejemplo, 2.5 / 0.01 = 250.
En el diseño TLV de la extensión InterestBearingConfig, la porción V es el valor serializado de todos los campos de la extensión concatenados en orden, tal como se explicó anteriormente en nuestro artículo sobre Token-2022.
Por ejemplo, supongamos que la extensión contiene los siguientes valores:
rate_authority:7xKXtg2CW87d9LN6HBUtjQVSiJ9MCrgdGubbyiTZRjrwb(32 bytes)initialization_timestamp:1672531200(1 de enero de 2023; 8 bytes)pre_update_average_rate:500(5.00% en puntos básicos; 2 bytes)last_update_timestamp:1704067200(1 de enero de 2024; 8 bytes)current_rate:500(5.00% en puntos básicos; 2 bytes)
Cuando concatenamos los campos anteriores como una secuencia continua y única de bytes, la porción V (hex) del TLV es:
0x689536DF68C2FB0A61A08DEDC9797145C969328A05D68A2A8C06E15A3AB6BD5200CDB06300000000F4018000926500000000F401
La entrada completa del TLV es T(0x0A) | L(0x34) | V(...) y se adjunta inmediatamente después de los datos de la cuenta Mint.

Inicialización de la extensión interest-bearing
La extensión interest-bearing se habilita e inicializa en una sola operación, la cual reserva el espacio y escribe el TLV de la extensión al mismo tiempo. Como discutimos previamente en el artículo de Token-2022,
otras extensiones generalmente requieren dos pasos:
- Habilitar las extensiones requeridas y reservar espacio para la extensión
- Y una instrucción de inicialización separada para configurarla.
Modelo de cálculo de intereses
Supón que tienes una cuenta de ahorros en el banco. Cuando depositas $1,000 a una tasa de interés anual del 3%, tu estado de cuenta no muestra a tu banco emitiendo nuevos dólares todos los días. En su lugar, el sistema del banco calcula cuánto habría crecido tu balance si se estuviera capitalizando (interés compuesto) y te muestra la cifra actualizada cada vez que inicias sesión.
La extensión interest-bearing hace que tu cuenta mint funcione de manera similar. Tu balance on-chain (el equivalente al balance de tu banco) nunca cambia de $1,000. Pero tu wallet (como la aplicación de banca en línea) utiliza una fórmula de interés compuesto para mostrar ****$1,015 después de seis meses o $1,030 después de un año.
Si posteriormente el banco aumenta tu tasa al 5% ****desde el ****3%, los intereses futuros se capitalizarán más rápido, pero tu año anterior al 3% todavía está “asegurado”, y el balance final reflejará adecuadamente la tasa de crecimiento del 3% para ese período de tiempo.
La extensión interest-bearing utiliza la fórmula del interés compuesto continuo () para calcular el interés y garantizar que tu rendimiento acumulado sea exacto, independientemente de si la tasa cambia o no (exploraremos esta fórmula en detalle más adelante).
La fórmula está integrada (hard-coded) en el programa Token-2022 en dos variaciones: before (antes) y after (después) de la actualización de tasa más reciente.
La variación before captura el crecimiento bajo la tasa anterior (en caso de que la autoridad actualice la tasa), mientras que la variación after captura el crecimiento bajo la tasa actual. Juntas, producen un factor de interés compuesto continuo ponderado por el tiempo que asegura que los balances se mantengan precisos a través de múltiples cambios de tasa.
A continuación, mostraremos cómo se calculan matemáticamente estas operaciones con un ejemplo.
Cómo la extensión interest-bearing utiliza la fórmula del interés compuesto continuo
En primer lugar, aquí hay una descripción de las variables de la fórmula:
- A = la cantidad final (capital principal + intereses)
- P = el principal (cantidad inicial)
- e = el número de Euler (aproximadamente 2.71828…)
- r = la tasa de interés anual (como decimal)
- t = el tiempo en años (Token-2022 trabaja internamente con segundos)
Donde SECONDS_PER_YEAR = 60 × 60 × 24 × 365.24
Toma el siguiente ejemplo para un escenario en el que la tasa no ha cambiado:
- Depositas 1000 tokens (P = 1000)
- La tasa de interés es del 5% (r = 0.05)
- Después de 1 año (t = 1).
- El balance mostrado se calcula de la siguiente manera:
Mostremos también el comportamiento cuando cambia la tasa
Ahora supón que la tasa de interés comienza en 3% pero aumenta al 5% después de los primeros 3 meses. Token-2022 maneja esto dividiendo el cálculo en segmentos.
Nota: En nuestro modelo matemático, representamos tres meses como 0.25 (calculado como 3 ÷ 12 = 0.25)
-
Crecimiento previo a la actualización (Pre-update):
Comencemos calculando el tiempo transcurrido (t).
Si sustituimos la t en la fórmula por 0.25 años según nuestro cálculo anterior, obtendremos el siguiente resultado:
-
Crecimiento posterior a la actualización (Post-update):
Nueva tasa = 5% (r₂ = 0.05)
Tiempo transcurrido restante = 9 meses, lo cual se calcula como 9/12 (t₂ = 0.75)
A partir de nuestro cálculo hasta ahora, notarás que el interés representa un rendimiento de un año. En los primeros tres meses (período de crecimiento previo a la actualización), el usuario ganó 7.53 tokens a una tasa del 3%, llevando su total a 1,007.53 tokens. Cuando la tasa aumentó al 5% durante los nueve meses restantes (el período de crecimiento posterior a la actualización), ganó 38.5 tokens adicionales, lo que resulta en un balance final de 1,046.03 tokens.
Manejo de múltiples actualizaciones de tasas de interés
Hemos visto cómo funciona este cálculo para dos períodos de tiempo, pero la extensión interest-bearing puede actualizar la tasa de interés varias veces durante la vida útil de un token. Cada actualización garantiza que el rendimiento acumulado siga siendo coherente con todos los cambios de tasa pasados y futuros.
Cuando se establece una nueva tasa, el programa actualiza los campos en la InterestBearingConfig de la siguiente manera:
- Recalcula
pre_update_average_ratecomo un promedio ponderado por el tiempo de todas las tasas anteriores, incluida la que acaba de ser reemplazada. - Adelanta el
last_update_timestampal tiempo de bloque actual (block time). - Configura
current_rateal nuevo valor de la tasa (por ejemplo,700puntos básicos para 7%).
Matemáticamente, podemos recalcular el nuevo promedio ponderado por el tiempo (pre_update_average_rate) de todas las tasas anteriores con la fórmula a continuación:
donde:
- r₁ →
pre_update_average_rate(la tasa promedio anterior) - t₁ →
last_update_timestamp - initialization_timestamp(tiempo transcurrido bajo todas las tasas anteriores) - r₂ →
current_rate(la tasa más reciente antes de la actualización) - t₂ →
current_timestamp - last_update_timestamp(tiempo transcurrido bajo la tasa actual)
Ejemplo de cálculo del promedio ponderado por el tiempo
Supongamos que comenzamos con:
initialization_timestamp = 0pre_update_average_rate = 300(3%)last_update_timestamp = 7889184segundos (~3 meses. La extensiónInterestBearingConfigalmacena marcas de tiempo Unix absolutas (timestamps), pero en este ejemplo, usamos una duración transcurrida de 3 meses en segundos para ilustrar el crecimiento de los intereses, ya que el interés depende únicamente del paso del tiempo y no de los valores específicos del timestamp)current_rate = 500(5%)current_timestamp = 31556736(≈ 1 año)new_rate = 700(7%)
Entonces:
Después de la actualización:
pre_update_average_rate = 450(4.50%)last_update_timestamp = 31556736current_rate = 700(7%)
Ilustración en código de la fórmula
El período de crecimiento previo a la actualización (pre-update) y el período de crecimiento posterior a la actualización (post-update) ****están implementados como las funciones pre_update_exp y post_update_exp en el código fuente de la extensión. La parte que define ambas funciones se muestra a continuación.
Las funciones pre_update_exp y post_update_exp implementan directamente la fórmula del interés compuesto continuo (), en específico, el factor de crecimiento del interés () para dos períodos de tiempo diferentes: antes y después de la actualización más reciente de la tasa de interés.
pre_update_expcalcula el crecimiento del interés compuesto por el tiempo transcurrido entre la inicialización del token y la última actualización de la tasa.- Multiplica la tasa promedio durante ese período (
pre_update_average_rate) por el tiempo transcurrido en segundos.
- Divide el numerador por ambos:
- el número de segundos en un año (
SECONDS_PER_YEAR) para convertir el tiempo de segundos a años, y - la constante
ONE_IN_BASIS_POINTS(que es igual a 10,000) para convertir la tasa de puntos básicos a decimal.
- el número de segundos en un año (
- Finalmente, calcula
exponent.exp(), que es el factor de crecimiento continuo (el número de Euler elevado a la potencia del exponente) para esa duración. Esto se representa matemáticamente como .
- Multiplica la tasa promedio durante ese período (
post_update_exprealiza el mismo cálculo pero utiliza la tasa actual (current_rate) y el tiempo transcurrido desde la actualización más reciente (post_update_timespan).
A continuación se encuentran las funciones pre_update_exp y post_update_exp de la base de código de Token-2022:
pub struct InterestBearingConfig {
/// Authority that can set the interest rate and authority
pub rate_authority: OptionalNonZeroPubkey,
/// Timestamp of initialization, from which to base interest calculations
pub initialization_timestamp: UnixTimestamp,
/// Average rate from initialization until the last time it was updated
pub pre_update_average_rate: BasisPoints,
/// Timestamp of the last update, used to calculate the total amount accrued
pub last_update_timestamp: UnixTimestamp,
/// Current rate, since the last update
pub current_rate: BasisPoints,
}
fn pre_update_exp(&self) -> Option<f64> {
let numerator = (i16::from(self.pre_update_average_rate) as i128)
.checked_mul(self.pre_update_timespan()? as i128)? as f64;
let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
Some(exponent.exp())
}
fn post_update_exp(&self, unix_timestamp: i64) -> Option<f64> {
let numerator = (i16::from(self.current_rate) as i128)
.checked_mul(self.post_update_timespan(unix_timestamp)? as i128)? as f64;
let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
Some(exponent.exp())
}
fn post_update_timespan(&self, unix_timestamp: i64) -> Option<i64> {
unix_timestamp.checked_sub(self.last_update_timestamp.into())
}
Para calcular de forma precisa el interés compuesto (sin importar si la tasa ha cambiado), la extensión interest-bearing utiliza la función total_scale.
Ésta multiplica los resultados de pre_update_exp y post_update_exp, los cuales representan los factores de crecimiento antes y después de la última actualización de la tasa, respectivamente.
El producto da el factor de crecimiento exponencial total a través de ambos segmentos de tiempo.
Finalmente, la función total_scale divide el resultado entre 10^decimals para escalar el valor a la precisión estándar del token. Por ejemplo, el token SOL tiene 9 decimales, por lo que el resultado de pre_update_exp y post_update_exp se dividirá entre 10^9.
El valor resultante de total_scale es el factor de escala (scaling factor) aplicado al balance on-chain para mostrar con precisión el interés compuesto continuo.
fn total_scale(&self, decimals: u8, unix_timestamp: i64) -> Option<f64> {
Some(
self.pre_update_exp()? * self.post_update_exp(unix_timestamp)?
/ 10_f64.powi(decimals as i32),
)
}
En otras palabras, el valor resultante de cada cálculo de intereses en la extensión interest-bearing es el producto del principal y total_scale.
Cómo la extensión interest-bearing aplica la fórmula internamente
Aquí están los tres campos importantes que hacen posible este cálculo internamente:
pre_update_average_rate: Almacena el factor de crecimiento acumulativo (el exponente de la fórmula) hasta la última actualización de la tasa. En nuestro ejemplo, después de 3 meses esto captura0.03 × 0.25 = 0.0075.last_update_timestamp: Marca la hora exacta en la que ocurrió la última actualización de la tasa. En el ejemplo, este es el timestamp en la marca de los 3 meses.current_rate: La tasa de interés vigente en la actualidad. En el ejemplo, esto cambia de0.03a0.05después de 3 meses.
Cuando una wallet o un programa consulta el balance, la extensión interest-bearing reconstruye la fórmula de la siguiente manera:
Esto es equivalente a lo que tenemos en la función total_scale que mencionamos anteriormente:
fn total_scale(&self, decimals: u8, unix_timestamp: i64) -> Option<f64> {
Some(
self.pre_update_exp()? * self.post_update_exp(unix_timestamp)?
/ 10_f64.powi(decimals as i32),
)
}
Funciones de conversión para mostrar balances en la interfaz de usuario (UI)
La extensión interest-bearing expone dos funciones que las wallets y aplicaciones utilizan para mostrar balances consistentemente off-chain:
- La función que convierte la cantidad cruda (raw amount) a la cantidad de UI (
amount_to_ui_amount), la cual primero calcula el interés acumulado (cantidad del token * total scale), luego formatea el resultado en un string con la precisión decimal dada y elimina los ceros innecesarios.
/// Convert a raw amount to its UI representation using the given decimals
/// field. Excess zeroes or unneeded decimal point are trimmed.
pub fn amount_to_ui_amount(
&self,
amount: u64,
decimals: u8,
unix_timestamp: i64,
) -> Option<String> {
let scaled_amount_with_interest =
(amount as f64) * self.total_scale(decimals, unix_timestamp)?;
let ui_amount = format!("{scaled_amount_with_interest:.*}", decimals as usize);
Some(trim_ui_amount_string(ui_amount, decimals))
}
- y la
try_ui_amount_into_amount, la cual convierte un balance de UI (el que incluye el interés calculado) de vuelta a la cantidad cruda (sin el interés) que se usa internamente. Aquí está la implementación original en el código fuente de interest-bearing.
/// Try to convert a UI representation of a token amount to its raw amount
/// using the given decimals field
pub fn try_ui_amount_into_amount(
&self,
ui_amount: &str,
decimals: u8,
unix_timestamp: i64,
) -> Result<u64, ProgramError> {
let scaled_amount = ui_amount
.parse::<f64>()
.map_err(|_| ProgramError::InvalidArgument)?;
let amount = scaled_amount
/ self
.total_scale(decimals, unix_timestamp)
.ok_or(ProgramError::InvalidArgument)?;
if amount > (u64::MAX as f64) || amount < (u64::MIN as f64) || amount.is_nan() {
Err(ProgramError::InvalidArgument)
} else {
// this is important, if you round earlier, you'll get wrong "inf"
// answers
Ok(amount.round() as u64)
}
}
Las funciones anteriores (amount_to_ui_amount y try_ui_amount_into_amount) no se ejecutan on-chain. Son helpers del lado del cliente (client-side helpers) implementados en el SDK de Rust (y también replicados en el SDK de TypeScript)
Compatibilidad con wallets
Las extensiones de Token-2022 todavía no tienen un soporte amplio por parte de las wallets en el ecosistema de Solana. Dado que el balance on-chain de un token que devenga intereses nunca cambia, una wallet debe detectar la extensión interest-bearing en el mint y aplicar la fórmula de interés compuesto antes de mostrar el balance del usuario. Sin esta lógica, la wallet siempre mostrará la cantidad principal cruda (raw) y omitirá el crecimiento acumulado.
Esta diferencia significa que dos wallets pueden mostrar la misma cuenta con diferentes resultados: una mostrando solo la cantidad fija almacenada on-chain, la otra mostrando el balance de capitalización continua derivado de los campos de la extensión. Hasta que las wallets actualicen su renderizado de cuentas para incluir las extensiones de Token-2022, las aplicaciones que dependan de tokens interest-bearing a menudo necesitarán calcular y mostrar los balances por sí mismas.
Conclusión
En el transcurso de este artículo, hemos discutido cómo funciona la extensión de token interest-bearing, cómo introduce una forma de representar el rendimiento directamente al nivel del mint del token sin requerir actualizaciones de balance on-chain o transacciones de distribución periódicas.
También discutimos cómo todas las acumulaciones ocurren a través de una fórmula determinista off-chain, haciendo que el sistema sea eficiente mientras sigue brindando a los usuarios la experiencia de un balance creciente.
Las matemáticas detrás de la visualización garantizan que el interés acumulado se capitalice correctamente, y las integraciones de wallets pueden depender de las funciones proporcionadas para mantener los balances consistentes.
El soporte entre wallets y exploradores sigue siendo limitado, por lo que por ahora las aplicaciones que adopten esta característica deben asumir la responsabilidad de mostrar los balances correctamente.
Este artículo es parte de una serie de tutoriales sobre Solana.