En Ethereum, las cuentas son Cuentas de Propiedad Externa (EOAs, por sus siglas en inglés) por defecto. Cada cuenta está controlada por una clave privada, y si esta se ve comprometida, no hay forma de limitar el daño o revocar el acceso. Si la clave se pierde, la cuenta se pierde para siempre.
Las EOAs también son rígidas en su forma de operar. No hay soporte nativo para agrupar múltiples operaciones en una sola transacción. Los usuarios deben tener el token nativo de gas para pagar las tarifas, independientemente del token que estén usando realmente. Y no hay una forma integrada de añadir reglas como la aprobación multipartita, la recuperación de claves, etc.
Starknet no tiene EOAs. Cada cuenta es un contrato inteligente, conocido como contrato de cuenta.
Hacer que cada cuenta sea un contrato inteligente es la Abstracción de Cuentas (Account Abstraction, AA), y resuelve las limitaciones anteriores al hacer que la lógica de la cuenta sea programable.
En este artículo, aprenderemos qué es la Abstracción de Cuentas, cómo se compara el enfoque de Starknet con el de Ethereum, qué características permite y cómo se construye un contrato de cuenta en Starknet.
¿Qué es la Abstracción de Cuentas?
La Abstracción de Cuentas (AA) es un patrón de diseño de blockchain que hace que las cuentas sean programables. En lugar de que el protocolo dicte cómo se validan y ejecutan las transacciones, la propia cuenta define esa lógica. En Starknet, esto significa que todas las transacciones deben pasar por un contrato de cuenta para su validación antes de la ejecución.
Debido a que la lógica de la cuenta es programable, características como opciones de recuperación, procesamiento por lotes de transacciones, pago de gas con cualquier token y permisos granulares se vuelven posibles.
La Abstracción de Cuentas se considera nativa cuando la cuenta programable se implementa directamente a nivel del protocolo. Alternativamente, puede superponerse sobre protocolos existentes, pero dichas implementaciones no ofrecen todos los beneficios de la Abstracción de Cuentas nativa. Starknet adopta el enfoque nativo, integrado en el protocolo desde cero.
Cómo se diferencia la Abstracción de Cuentas de las EOAs
Con las EOAs, la clave privada (firmante) y la cuenta están estrechamente acopladas. La dirección de la cuenta se deriva de la clave privada, y esa misma clave es la única forma de firmar transacciones. La clave privada es la cuenta.
Con la Abstracción de Cuentas, el firmante y el contrato de cuenta están desacoplados. Esta separación es lo que hace que la cuenta sea programable: debido a que la cuenta ya no está ligada a la clave privada, la lógica sobre quién puede autorizar transacciones y cómo se ejecutan puede ser personalizada. En Starknet, esto funciona de la siguiente manera:
- El firmante es la clave privada, mantenida por el software de la billetera en el dispositivo del usuario, utilizada para firmar transacciones cuando el usuario las aprueba.
- El contrato de cuenta es un contrato inteligente desplegado onchain que custodia los activos del usuario y define la lógica para validar y ejecutar transacciones.
Cuando un usuario inicia una acción, la billetera usa la clave privada para firmar los detalles de la transacción off-chain y produce una firma que demuestra que el usuario autorizó la transacción. La transacción junto con esta firma se envía luego al sequencer (el nodo que ordena las transacciones y construye los bloques en Starknet), que la reenvía al contrato de cuenta del usuario. El contrato de cuenta verifica la firma comparándola con los detalles de la transacción y, si es válida, ejecuta la transacción.
Dado que la lógica de validación reside dentro del contrato de cuenta y no a nivel del protocolo, el propietario de la cuenta es libre de definir qué significa “válido”. El contrato de cuenta podría requerir la firma de una sola clave, firmas de múltiples claves, o cualquier lógica de verificación personalizada que el desarrollador elija implementar.
Ethereum también tiene Abstracción de Cuentas
La Abstracción de Cuentas no es nueva. Ya en 2017, Vitalik Buterin propuso los light account contracts, sugiriendo que las cuentas podrían ser contratos con su propia lógica de validación en lugar de depender únicamente de comprobaciones de firmas a nivel del protocolo.
Desde entonces, ha ganado tracción en el ecosistema de Ethereum. La implementación más madura hoy en día es EIP-4337, la cual introduce billeteras de contratos inteligentes sin requerir cambios en el propio protocolo de Ethereum. EIP-4337 está desplegada en mainnet, con varias implementaciones listas para producción.
Más recientemente, EIP-7702, introducida como parte de la actualización Pectra, acercó la Abstracción de Cuentas al nivel del protocolo. Permite que las EOAs apunten a un contrato inteligente ya desplegado, otorgándoles capacidades de cuentas inteligentes como el procesamiento por lotes de transacciones y la abstracción de gas sin necesidad de migrar a una nueva dirección. Este proceso, conocido como delegación, es opcional y persiste hasta que el usuario lo elimina delegando a la dirección cero.
Qué ocurre cuando creas una cuenta en Starknet
Cada vez que creas una nueva cuenta con el software de tu billetera, la billetera despliega un nuevo contrato de cuenta. Como cubrimos en el capítulo “Understanding Starknet’s Contract Deployment Model”, se pueden crear múltiples instancias de contratos a partir de la misma clase, compartiendo el mismo código pero cada una con su propia dirección y almacenamiento. Los proveedores de billeteras declaran su clase de cuenta una vez en Starknet, y cada nueva billetera creada es simplemente una nueva instancia de esa clase.
El constructor del contrato de cuenta se inicializa con parámetros como la clave pública del usuario, y la dirección de cuenta resultante es determinista: se deriva de varias entradas incluyendo el class hash, un salt, la dirección del desplegador y la calldata del constructor.
Recuerda que la cuenta puede recibir activos incluso antes de su despliegue porque su dirección es determinista, pero no puede ejecutar ninguna transacción hasta que se despliegue el contrato de cuenta.
Una vez desplegado, el contrato de cuenta queda vinculado a su lógica, y cambiar a un contrato de cuenta diferente significa crear una cuenta completamente nueva.
Cambiar entre contratos de cuenta
En teoría, cualquiera es libre de implementar su propio contrato de cuenta. A nivel del protocolo, los requisitos son:
- Implementar
__validate__y__execute__, los entrypoints que el protocolo llama durante el ciclo de vida de la transacción - Implementar
__validate_declare__o__validate_deploy__, dependiendo de las capacidades deseadas - Seguir el estándar SNIP-6, que añade la función
is_valid_signaturepara la interoperabilidad con dApps y la verificación de firmas off-chain
Explicaremos cada una de estas funciones en la siguiente sección.
Sin embargo, en la práctica, es difícil usar una implementación personalizada de contrato de cuenta. Incluso si sigues el estándar y todo funciona técnicamente, aún tienes que usar el contrato de cuenta a través de algún software de billetera.
Dado que cualquiera es libre de añadir funcionalidad sobre los requisitos mínimos de la Abstracción de Cuentas, la mayoría de las billeteras populares (como Ready) tienen sus propias implementaciones de contratos de cuenta. Estas no son intercambiables. Esto hace que sea difícil cambiar entre ellas o usar tu propia implementación.
Anatomía de un contrato de cuenta
Veamos una implementación muy mínima de un contrato de cuenta. Está deliberadamente simplificada y es altamente insegura, ya que aprueba todas las transacciones y firmas sin ninguna comprobación. El objetivo es ilustrar la estructura mínima requerida para un contrato de cuenta de Starknet, y se proporciona solo con fines de demostración, no para uso en producción.
Crea un nuevo proyecto en Scarb y navega hasta él:
scarb new aa
cd aa
Primero, importamos el struct Call en src/lib.cairo. Este struct representa una única operación en una transacción. Cada Call contiene una dirección de contrato de destino, un selector de función y la calldata a pasar. Nuestro contrato de cuenta lo usará para saber a qué contrato llamar y con qué datos.
use starknet::account::Call;
A continuación, definimos la interfaz SNIP-6 (ISRC6) que se espera que todo contrato de cuenta implemente. Tiene tres funciones:
__validate__para la validación de transacciones,__execute__para la ejecución de transacciones, yis_valid_signaturepara la verificación de firmas off-chain:
use starknet::account::Call;
//////NEWLY ADDED///////
#[starknet::interface]
trait ISRC6<T> {
fn __validate__(self: @T, calls: Array<Call>) -> felt252;
fn __execute__(ref self: T, calls: Array<Call>) -> Array<Span<felt252>>;
fn is_valid_signature(self: @T, hash: felt252, signature: Array<felt252>) -> felt252;
}
Ahora definamos el contrato de cuenta en sí. El atributo #[starknet::contract(account)] le dice al compilador que este es un contrato de cuenta, lo que permite al protocolo llamar a sus entrypoints __validate__ y __execute__ durante el procesamiento de la transacción. Sin este atributo, el contrato sería tratado como un contrato inteligente regular y no podría ser utilizado como una cuenta. También importamos call_contract_syscall que usaremos en __execute__ para hacer llamadas a otros contratos, y SyscallResultTrait para manejar el resultado de esas llamadas:
use starknet::account::Call;
#[starknet::interface]
trait ISRC6<T> {
fn __validate__(self: @T, calls: Array<Call>) -> felt252;
fn __execute__(ref self: T, calls: Array<Call>) -> Array<Span<felt252>>;
fn is_valid_signature(self: @T, hash: felt252, signature: Array<felt252>) -> felt252;
}
//////NEWLY ADDED///////
#[starknet::contract(account)]
mod Account {
use starknet::SyscallResultTrait;
use starknet::syscalls::call_contract_syscall;
use super::Call;
#[storage]
struct Storage {}
}
Ahora implementemos cada función.
La función __validate__
El protocolo llama a esta función antes de que se ejecute una transacción. En un contrato de cuenta de producción, aquí es donde se verifica la firma de la transacción para confirmar que el llamador está autorizado. En nuestra implementación, simplemente devuelve 'VALID' para cada transacción sin ninguna comprobación:
#[abi(embed_v0)]
impl AccountImpl of super::ISRC6<ContractState> {
fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 {
'VALID'
}
}
La función __execute__
El protocolo llama a esta función después de que __validate__ pase con éxito. Recibe un array de llamadas y las ejecuta secuencialmente, en el orden en que aparecen, usando call_contract_syscall. Esto es lo que habilita la multicall, la capacidad de ejecutar múltiples operaciones en una sola transacción:
fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> {
let mut results = ArrayTrait::new();
for call in calls {
let result = call_contract_syscall(call.to, call.selector, call.calldata)
.unwrap_syscall();
results.append(result);
}
results
}
La función is_valid_signature
Esta función no es llamada por el protocolo. Existe para la verificación de firmas off-chain y permite a las dApps confirmar que una firma pertenece a esta cuenta. Aquí devuelve 'VALID' sin comprobar nada:
fn is_valid_signature(
self: @ContractState, hash: felt252, signature: Array<felt252>,
) -> felt252 {
'VALID'
}
Este contrato de ejemplo omite toda validación y nunca debe usarse en producción. Los contratos de cuenta en producción deben implementar validaciones adecuadas y comprobaciones de firmas para proteger los activos y el acceso del usuario.
Aquí está el código completo del contrato Account:
use starknet::account::Call;
#[starknet::interface]
trait ISRC6<T> {
fn __validate__(self: @T, calls: Array<Call>) -> felt252;
fn __execute__(ref self: T, calls: Array<Call>) -> Array<Span<felt252>>;
fn is_valid_signature(self: @T, hash: felt252, signature: Array<felt252>) -> felt252;
}
#[starknet::contract(account)]
mod Account {
use starknet::SyscallResultTrait;
use starknet::syscalls::call_contract_syscall;
use super::Call;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl AccountImpl of super::ISRC6<ContractState> {
fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 {
'VALID'
}
fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> {
let mut results = ArrayTrait::new();
for call in calls {
let result = call_contract_syscall(call.to, call.selector, call.calldata)
.unwrap_syscall();
results.append(result);
}
results
}
fn is_valid_signature(
self: @ContractState, hash: felt252, signature: Array<felt252>,
) -> felt252 {
'VALID'
}
}
}
Vamos a usar el contrato Account en una devnet local para ver cómo interactúa con otros contratos. Más adelante, en la sección “Cómo se llama al contrato de Cuenta (AA)”, explicaremos qué hace el protocolo tras bambalinas durante cada transacción.
Desplegando e interactuando con nuestro contrato de cuenta
Un contrato de cuenta mínimo es capaz de invocar transacciones dirigidas a otros contratos. Sin embargo, no es capaz de declarar y desplegar otros contratos. Dado que el flujo de este tutorial implica declarar y desplegar un contrato separado, necesitamos añadir dos funciones de validación más:
__validate_declare__: llamada por el protocolo cuando el usuario quiere declarar una nueva clase de contrato.__validate_deploy__: llamada por el protocolo durante una transacciónDEPLOY_ACCOUNT, cuando se está desplegando un nuevo contrato de cuenta por primera vez.
Así como __validate__ valida las transacciones invoke antes de la ejecución, __validate_declare__ hace lo mismo para las transacciones declare y __validate_deploy__ para las transacciones de despliegue de cuenta.
Añade estas dos funciones a la interfaz:
fn __validate_declare__(self: @T, class_hash: felt252) -> felt252;
fn __validate_deploy__(
self: @T, class_hash: felt252, contract_address_salt: felt252, public_key: felt252,
) -> felt252;
También añade sus implementaciones en el contrato:
fn __validate_declare__(self: @ContractState, class_hash: felt252) -> felt252 {
return 'VALID';
}
fn __validate_deploy__(
self: @ContractState,
class_hash: felt252,
contract_address_salt: felt252,
public_key: felt252,
) -> felt252 {
return 'VALID';
}
Con la Abstracción de Cuentas nativa de Starknet, los diferentes proveedores de billeteras pueden personalizar sus funciones de validación manteniendo la compatibilidad. Por ejemplo, el contrato de cuenta de Ready define __validate_deploy__ con parámetros personalizados:
// Ready's customized validation function
__validate_deploy__(
class_hash: felt252,
contract_address_salt: felt252,
owner: Signer, // uses Signer type
guardian: Option<Signer> // adds guardian support
) -> felt252
Nota que la versión de Ready toma owner: Signer y guardian: Option<Signer> en lugar del parámetro estándar public_key: felt252. Esta personalización le permite a Ready:
- Usar su tipo personalizado
Signeren lugar de una simple clave pública, el cual puede representar diferentes esquemas de firmas - Añadir funcionalidad de guardián para recuperación social. Un guardián es una parte de confianza (como otra billetera, un amigo o el propio proveedor de la billetera) que puede ayudar a recuperar el acceso a la cuenta si se pierde el firmante principal.
A pesar de estos parámetros diferentes, la función __validate_deploy__ de Ready sigue llamando a la misma lógica de validación subyacente que comprueba las firmas y asegura que el despliegue está autorizado. Los parámetros personalizados simplemente permiten a Ready pasar información adicional (como las claves del guardián) a través de su proceso de validación.
Esta flexibilidad para personalizar los parámetros de validación mientras se sigue la interfaz de validación que Starknet espera es uno de los beneficios que permite la Abstracción de Cuentas. Permite a los proveedores de billeteras añadir características como soporte multifirma, claves de sesión o recuperación social sin desviarse del protocolo.
Aquí está el código completo del contrato Account con __validate_declare__ y __validate_deploy__ añadidas:
use starknet::account::Call;
#[starknet::interface]
trait ISRC6<T> {
fn __validate__(self: @T, calls: Array<Call>) -> felt252;
fn __execute__(ref self: T, calls: Array<Call>) -> Array<Span<felt252>>;
fn is_valid_signature(self: @T, hash: felt252, signature: Array<felt252>) -> felt252;
fn __validate_declare__(self: @T, class_hash: felt252) -> felt252;
fn __validate_deploy__(
self: @T, class_hash: felt252, contract_address_salt: felt252, public_key: felt252,
) -> felt252;
}
#[starknet::contract(account)]
mod Account {
use starknet::SyscallResultTrait;
use starknet::syscalls::call_contract_syscall;
use super::Call;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl AccountImpl of super::ISRC6<ContractState> {
fn __validate__(self: @ContractState, calls: Array<Call>) -> felt252 {
'VALID'
}
fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> {
let mut results = ArrayTrait::new();
for call in calls {
let result = call_contract_syscall(call.to, call.selector, call.calldata)
.unwrap_syscall();
results.append(result);
}
results
}
fn is_valid_signature(
self: @ContractState, hash: felt252, signature: Array<felt252>,
) -> felt252 {
'VALID'
}
fn __validate_declare__(self: @ContractState, class_hash: felt252) -> felt252 {
return 'VALID';
}
fn __validate_deploy__(
self: @ContractState,
class_hash: felt252,
contract_address_salt: felt252,
public_key: felt252,
) -> felt252 {
return 'VALID';
}
}
}
Reemplaza el contenido de src/lib.cairo con el contrato Account de arriba.
Añadir un contrato con el cual interactuar
Dado que queremos demostrar cómo se utiliza un contrato de cuenta, necesitamos tener algún otro contrato con el cual interactuar. Añadamos un contrato Counter simple al mismo archivo src/lib.cairo:
#[starknet::interface]
pub trait ICounter<TContractState> {
fn increase_counter(ref self: TContractState, amount: felt252);
fn get_counter(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod Counter {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
counter: felt252,
}
#[abi(embed_v0)]
impl CounterImpl of super::ICounter<ContractState> {
fn increase_counter(ref self: ContractState, amount: felt252) {
self.counter.write(self.counter.read() + amount);
}
fn get_counter(self: @ContractState) -> felt252 {
self.counter.read()
}
}
}
Ejecuta scarb build para compilar los contratos.
Iniciar una devnet con nuestro contrato de cuenta
Si instalaste tus herramientas de desarrollo de Starknet usando starkup, starknet-devnet ya está instalado. Puedes verificarlo ejecutando:
starknet-devnet --version
En el momento de escribir esto, la versión esperada es 0.7.2. Si no lo tienes instalado, primero añade el plugin, luego instálalo:
asdf plugin add starknet-devnet
asdf install starknet-devnet 0.7.2
asdf set starknet-devnet 0.7.2
Ahora iniciemos la devnet. Por defecto, starknet-devnet predespliega un conjunto de cuentas financiadas utilizando un contrato de cuenta estándar. Queremos que en su lugar utilicen nuestro contrato de cuenta, así que lo pasamos como un flag:
starknet-devnet --seed 1 --account-class-custom target/dev/aa_Account.contract_class.json
- El parámetro
--seedasegura que se generen las mismas cuentas predesplegadas (direcciones y claves) cada vez que se reinicia la devnet, para que no tengas que volver a importarlas después de cada reinicio. - El flag
--account-class-customle dice a la devnet que use nuestro contrato de cuenta compilado para las cuentas predesplegadas. El nombre del archivo combina el nombre de tu proyecto en Scarb (aa) y el nombre del contrato (Account).

Importar una cuenta para enviar transacciones
La devnet ahora tiene cuentas predesplegadas usando nuestro contrato de cuenta, pero sncast en nuestra máquina local aún no sabe de ellas. Necesitamos importar una para que sncast pueda usarla para enviar transacciones. Abre una nueva terminal (ya que la actual está ejecutando la devnet), luego copia la dirección y la clave privada de la primera cuenta que la devnet enumera y úsalas en el comando de importación:
sncast \
account import \
--url http://127.0.0.1:5050 \
--address <PREDEPLOYED_ACCOUNT_ADDRESS> \
--private-key <PREDEPLOYED_PRIVATE_KEY> \
--type oz
- El flag
--urlapunta al endpoint RPC de la devnet local. - Dado que nuestro contrato de cuenta no verifica las firmas, el valor de la clave privada no importa aquí. Sin embargo,
sncastla requiere como un campo obligatorio porque la mayoría de los contratos de cuenta dependen de las firmas para la autorización. - El flag
--typele dice asncastcómo formatear las transacciones para la cuenta. Las opciones disponibles sonready,braavos, ooz(OpenZeppelin). Dado que nuestra cuenta personalizada no coincide con ningún proveedor de billetera específico, usamosozcomo la opción genérica más cercana.
Después de ejecutar el comando, sncast preguntará si deseas hacer de esta cuenta la predeterminada. Elige la opción de predeterminada local para que la cuenta esté limitada solo a este proyecto.

La imagen a continuación muestra cómo la salida de la devnet (izquierda) se mapea con el comando de importación (derecha). La línea amarilla conecta la dirección de la cuenta, y la línea roja conecta la clave privada de la cuenta predesplegada de la devnet a los campos correspondientes en el comando sncast account import.

Toma nota del nombre de la cuenta en la salida de la importación y úsalo en lugar de
<ACCOUNT_NAME>en los comandos que siguen.
Si necesitas volver a importar la cuenta con diferentes detalles o limpiar después de las pruebas, puedes eliminarla con:
sncast \
account delete \
--url http://127.0.0.1:5050 \
--name <ACCOUNT_NAME>
Declarar y desplegar el contrato Counter
Ahora que tenemos un contrato de cuenta importado, usémoslo para declarar y desplegar nuestro contrato Counter.
Declara el contrato:
sncast \
--account <ACCOUNT_NAME> \
declare \
--url http://127.0.0.1:5050 \
--contract-name Counter

Toma nota del class hash resultante y reemplázalo en el siguiente comando para desplegar el contrato:
sncast \
--account <ACCOUNT_NAME> \
deploy \
--class-hash <CLASS_HASH> \
--url http://127.0.0.1:5050

Toma nota de la dirección del contrato resultante.
Interactuar con el contrato Counter
Ahora estamos listos para interactuar con el contrato Counter. Recuerda que la cuenta predesplegada que importamos está almacenada localmente bajo el <ACCOUNT_NAME> que especificamos durante el paso de importación.
Ejecuta el siguiente comando para incrementar el contador. Reemplaza <CONTRACT_ADDRESS> con la dirección del contrato de la salida del despliegue:
sncast \
--account <ACCOUNT_NAME> \
invoke \
--url http://127.0.0.1:5050 \
--contract-address <CONTRACT_ADDRESS> \
--function 'increase_counter' \
--arguments 23

Ahora podemos realizar una consulta para ver el nuevo valor del contador. Debería devolver 23 (codificado en hexadecimal):
sncast \
call \
--url http://127.0.0.1:5050 \
--contract-address <CONTRACT_ADDRESS> \
--function 'get_counter'

Cómo se llama al contrato de Cuenta (AA)
En el flujo de ejemplo anterior, nosotros:
- Iniciamos una devnet con nuestro contrato de cuenta como la implementación de cuenta predesplegada.
- Importamos una de las cuentas predesplegadas para que
sncastpudiera usarla localmente. - Declaramos un nuevo contrato en la devnet:
Counter. - Desplegamos el contrato
Counter. - Invocamos
increase_counteren el contrato Counter. - Llamamos a
get_counterpara leer el valor del contador.
Tras bambalinas, durante los pasos 3-6, el protocolo llamó a las funciones de validación y ejecución de nuestro contrato de cuenta en cada paso:
- Paso 3 - Declarar el contrato Counter: Esta es una transacción
DECLARE. El sequencer llamó a__validate_declare__en nuestro contrato de cuenta. Dado que devolvió'VALID', el sequencer registró la claseCounteren la red. - Paso 4 - Desplegar el contrato Counter: Esta no es una transacción
DEPLOY_ACCOUNTporque elCounteres un contrato regular, no una cuenta. Se procesa como una transacciónINVOKEa través del Universal Deployer Contract. El sequencer llamó a__validate__en nuestro contrato de cuenta, y después de que pasó la validación, llamó a__execute__para procesar el despliegue. - Paso 5 - Invocar
increase_counter: Esta es una transacciónINVOKE. El sequencer llamó a__validate__en nuestro contrato de cuenta, y después de que pasó la validación, llamó a__execute__lo cual reenvió la llamada al contratoCounter. - Paso 6 - Llamar a
get_counter: Esta es una llamada de solo lectura. No se envía ninguna transacción, no se paga gas, y nuestro contrato de cuenta no está involucrado en absoluto.
Nota que
__validate_deploy__nunca se activó en nuestro flujo. Esta función solo se llama durante una transacciónDEPLOY_ACCOUNT, la cual se utiliza al desplegar un contrato de cuenta desde cero (despliegue contrafactual). Dado que la devnet predesplegó las cuentas por nosotros, no hubo ninguna transacciónDEPLOY_ACCOUNT. Solo se llamaría si creáramos y desplegáramos una nueva cuenta.
Para ver la validación en acción, intenta cambiar el valor de retorno de __validate_declare__ en el contrato Account de 'VALID' a 'INVALID'. Recompila el contrato, reinicia la devnet con el contrato actualizado usando:
starknet-devnet --seed 1 --account-class-custom target/dev/aa_Account.contract_class.json
Luego importa la primera cuenta predesplegada como hicimos antes, e intenta declarar el contrato Counter nuevamente. Verás que la transacción falla con un error como este:
Error: Transaction execution error: The `validate` entry point should return
`VALID`. Got Retdata([0x494e56414c4944]).
Esto confirma que el protocolo comprueba el valor de retorno de las funciones de validación y rechaza cualquier transacción que no devuelva 'VALID'.
Recuperar una cuenta
Debido a que las direcciones EOA se derivan directamente de la clave privada, como discutimos anteriormente, recuperar una cuenta solo con la clave privada es sencillo. Sin embargo, si la clave se pierde, no hay opción de recuperación.
En Starknet, dado que el firmante y la cuenta están desacoplados, restaurar el acceso requiere tanto la clave privada como la dirección de la cuenta, ya que una no puede derivarse de la otra. Es por esto que importar una cuenta en billeteras como Ready requiere ambas cosas. Sin embargo, debido a que la cuenta es un contrato inteligente, los desarrolladores o proveedores de billeteras pueden implementar mecanismos de recuperación alternativos. Ready, por ejemplo, utiliza el sistema de guardián que discutimos antes para ayudar a los usuarios a recuperar el acceso a su cuenta si se pierde el firmante principal.
Características habilitadas por la Abstracción de Cuentas
La Abstracción de Cuentas introduce características que son difíciles o imposibles con las EOAs. Estas incluyen:
- Pago de gas con cualquier token. En lugar de pagar el gas con el token de gas nativo, un servicio de paymaster puede aceptar los tokens del usuario, intercambiarlos y cubrir la tarifa de gas en el token de gas requerido de forma invisible. El usuario sigue pagando, solo que con un token diferente.
- Transacciones patrocinadas. Un tercero puede patrocinar la totalidad de la tarifa de gas, permitiendo a los usuarios enviar transacciones de forma gratuita. Esto se usa comúnmente para el onboarding o para subsidiar el uso de una aplicación.
- Esquemas de firmas personalizados. Aunque la mayoría de las cuentas de Starknet utilizan firmas ECDSA, la función
__validate__puede implementar cualquier lógica de verificación, incluyendo diferentes esquemas criptográficos o incluso omitir las comprobaciones de firmas por completo en casos especiales. - Multifirma y control de acceso personalizado. Los contratos de cuenta pueden requerir que múltiples partes aprueben una transacción, imponer reglas basadas en el tiempo o implementar cualquier lógica de acceso personalizada.
- Recuperación de cuentas. Si falla el método de acceso principal, por ejemplo debido a la pérdida de claves, se pueden integrar opciones alternativas de recuperación en el contrato de cuenta.
- Cuentas con límite de uso (Rate-limited). Los contratos de cuenta pueden restringir el número de transacciones dentro de un período dado, lo cual es útil para cuentas patrocinadas con límites de uso.
Compensaciones de seguridad
Con la Abstracción de Cuentas, cada característica adicional (lógica personalizada, recuperación, multifirma, límites de uso) añade complejidad al contrato de cuenta. Errores o malas configuraciones en esta lógica pueden llevar a la pérdida irreversible de acceso o fondos.
Además, el protocolo de Starknet sigue evolucionando. Se están introduciendo nuevas características relacionadas con la Abstracción de Cuentas, y las mejores prácticas están cambiando activamente. Es importante mantenerse al día con los últimos desarrollos. Solo debes usar contratos de cuenta auditados.
Conclusión
La Abstracción de Cuentas hace que las cuentas sean programables. Habilita características como el pago de gas con cualquier token, transacciones patrocinadas, esquemas multifirma y métodos de recuperación personalizados que simplemente no son posibles con las EOAs. La implementación nativa de Starknet significa que estas características están disponibles para todas las cuentas por defecto.
Sin embargo, estos beneficios conllevan compensaciones. Los contratos de cuenta seguros son complejos de implementar, y cambiar entre las implementaciones de diferentes proveedores de billeteras sigue siendo difícil, como discutimos anteriormente.
En última instancia, la Abstracción de Cuentas trata de mejorar la experiencia del usuario. Elimina barreras como la necesidad de un token de gas específico o la gestión directa de claves privadas, lo que hace que el onboarding sea más fluido para los recién llegados.