Skip to content
🎉 Welcome to the new Aptos Docs! Click here to submit an issue.

Account Abstraction

Account Abstraction (AA) on Aptos enables custom transaction authentication logic through Move modules, allowing accounts to define their own rules beyond native cryptographic schemes.

Core Concepts

FunctionInfo

A struct defining the authentication function to be invoked.

struct FunctionInfo has copy, drop, store {
    module_address: address,
    module_name: String,
    function_name: String
}

The authentication function is responsible for defining the authentication logic using Move. It should return a signer if authentication is successful, otherwise it aborts the transaction. The only accepted authentication function signature that can be added onto an account is the following:

// The function can return a signer if authentication is successful, otherwise it aborts the transaction.
public fun authenticate(account: signer, auth_data: AbstractionAuthData): signer;

Example (Move)

module deployer::authenticator {
    use aptos_framework::auth_data::{AbstractionAuthData};
 
    public fun authenticate(account: signer, auth_data: AbstractionAuthData): signer {
        // ... authentication logic ...
        account
    }
}

Example (Typescript)

const authenticationFunction = `${deployer}::authenticator:authenticate`;

AbstractionAuthData

An enum variant defining the authentication data to be passed to the authentication function. It contains:

  • digest: The sha256 hash of the signing message.
  • authenticator: Abstract bytes that will be passed to the authentication function that will be used to verify the transaction.
enum AbstractionAuthData has copy, drop {
    V1 { 
        digest: vector<u8>,       // SHA3-256 hash of the signing message
        authenticator: vector<u8> // Custom auth data (e.g., signatures)
    },
}

Why is the digest important?

The digest is checked by the MoveVM to ensure that the signing message of the transaction being submitted is the same as the one presented in the AbstractionAuthData. This is important because it allows the authentication function to verify signatures with respect to the correct transaction.

For example, if you want to permit a public key to sign transactions on behalf of the user, you can permit the public key to sign a transaction with a specific payload. However, if a malicious user sends a signature for the correct public key but a different payload from the digest, the signature will not be valid.

Example (Move)

This example demonstrates a simple authentication logic that checks if the authenticator is equal to "hello world".

module deployer::hello_world_authenticator {
    use aptos_framework::auth_data::{Self, AbstractionAuthData};
 
    public fun authenticate(
        account: signer,
        auth_data: AbstractionAuthData
    ): signer {
        let authenticator = *auth_data::authenticator(&auth_data);
        assert!(authenticator == b"hello world", 1);
        account
    }
}

Example (Typescript)

const abstractedAccount = new AbstractedAccount({
  /**
   * The result of the signer function will be available as the `authenticator` field in the `AbstractionAuthData` enum variant.
   */
  signer: () => new TextEncoder().encode("hello world"),
  /**
   * The authentication function to be invoked.
   */
  authenticationFunction: `${deployer}::hello_world_authenticator:authenticate`,
});

Minimal Step-by-Step Guide

1. Deploy Authentication Module

In this example, we will deploy the hello_world_authenticator module. The authenticate function takes an AbstractionAuthData and returns a signer if the authentication is successful, otherwise it aborts the transaction. The authentication logic will only allow transactions that have an authenticator equal to "hello world".

module deployer::hello_world_authenticator {
    use aptos_framework::auth_data::{Self, AbstractionAuthData};
    use std::bcs;
 
    public fun authenticate(
        account: signer,
        auth_data: AbstractionAuthData
    ): signer {
        let authenticator = *auth_data::authenticator(&auth_data);
        assert!(authenticator == b"hello world", 1);
        account
    }
}

To deploy the module, you can use the following commands from the Aptos CLI. We assume that you already have set up a workspace with aptos init and declared the named addresses in your Move.toml file.

aptos move publish --named-addresses deployer=0x1234567890123456789012345678901234567890

2. Setup your Environment

Once deployed, you can setup your environment. In this example, we will use Devnet and create an account named alice which will act as our user.

const DEPLOYER = "0x<hello_world_authenticator_deployer>"
 
const aptos = new Aptos(new AptosConfig({ network: Network.DEVNET }));
 
const alice = Account.generate();
 
const authenticationFunctionInfo = `${deployer}::hello_world_authenticator:authenticate`;

3. (Optional) Check if Account Abstraction is Enabled

Before you ask them to enable account abstraction, you can check if the account has account abstraction enabled by calling the isAccountAbstractionEnabled function. This will return a boolean value indicating if the account has account abstraction enabled.

const accountAbstractionStatus = await aptos.account.isAccountAbstractionEnabled({
    accountAddress: alice.accountAddress,
    authenticationFunction,
});
 
console.log("Account Abstraction status: ", accountAbstractionStatus);

4. Enable the Authentication Function

Assuming that the account does not have account abstraction enabled, you need to enable the authentication function for the account. This can be done by calling the enableAccountAbstractionTransaction function. This creates a raw transaction that needs to be signed and submitted to the network. In this example, alice will be the account that will be enabled.

const transaction = aptos.abstraction.enableAccountAbstractionTransaction({
  accountAddress: alice.accountAddress,
  authenticationFunction: `${deployer}::hello_world_authenticator:authenticate`,
});
 
const pendingTransaction = await aptos.signAndSubmitTransaction({
  transaction,
  signer: alice.signer,
});
 
await aptos.waitForTransaction({ hash: pendingTransaction.hash });
 
console.log("Account Abstraction enabled for account: ", alice.accountAddress);
Wallet Adapter Example
💡

If you are using the wallet adapter, you can use the signTransaction function to sign the transaction before submitting it to the network.

export default function useEnableAbstraction() {
  const { account, signTransaction } = useWallet();
 
  return {
    enableAbstraction: async () => {
      if (!account) return;
 
      // Note: The Aptos client must be defined somewhere in the application.
      const transaction = aptos.abstraction.enableAccountAbstractionTransaction({
        accountAddress: account.address,
        authenticationFunction: `${deployer}::hello_world_authenticator:authenticate`,
      });
 
      const senderAuthenticator = await signTransaction(txn);
 
      const pendingTxn = await aptos.transaction.submit.simple({
        transaction: txn,
        senderAuthenticator,
      });
 
      return await aptos.waitForTransaction({ hash: pendingTxn.hash });
    }
  }
}

5. Create an Abstracted Account

Once the authentication function is enabled, you can create an abstracted account object for signing transactions. You must provide the authentication function that will be used to verify the transaction and a signer function that will be used to sign the transaction. The signer function is responsible for generating the authenticator that will be passed to the authentication function.

const abstractedAccount = new AbstractedAccount({
  accountAddress: alice.accountAddress,
  signer: () => new TextEncoder().encode("hello world"),
  authenticationFunction: `${deployer}::hello_world_authenticator:authenticate`,
});

6. Sign and Submit a Transaction using the Abstracted Account

Once you have created the abstracted account, you can use it to sign transactions normally. It is important that the sender field in the transaction is the same as the abstracted account’s address.

const coinTransferTransaction = await aptos.transaction.build.simple({
  sender: abstractedAccount.accountAddress,
  data: {
    function: "0x1::coin::transfer",
    typeArguments: ["0x1::aptos_coin::AptosCoin"],
    functionArguments: [alice.accountAddress, 100],
  },
});
 
const pendingCoinTransferTransaction = await aptos.transaction.signAndSubmitTransaction({
  transaction: coinTransferTransaction,
  signer: abstractedAccount,
});
 
await aptos.waitForTransaction({ transactionHash: pendingCoinTransferTransaction.hash });
 
console.log("Coin transfer transaction submitted! ", pendingCoinTransferTransaction.hash);

7. Conclusion

To verify that you have successfully sign and submitted the transaction using the abstracted account, you can use the explorer to check the transaction. If the transaction signature contains a function_info and auth_data field, it means you succesfully used account abstraction! The full E2E demo can be found here.

Transaction Signature

Complex Step-by-Step Guide

Now that you have a basic understanding of how account abstraction works, let’s dive into a more complex example.

In this example, we will create an authenticator that allows users to permit certain public keys to sign transactions on behalf of the abstracted account.

1. Create an Authenticator module

We will deploy the public_key_authenticator module that does two things:

  • Allow users to permit and/or revoke public keys from signing on behalf of the user.
  • Allow users to authenticate on behalf of somebody else using account abstraction.
module deployer::public_key_authenticator {
    use std::signer;
    use aptos_std::smart_table::{Self, SmartTable};
    use aptos_std::ed25519::{
        Self,
        new_signature_from_bytes,
        new_unvalidated_public_key_from_bytes,
        unvalidated_public_key_to_bytes
    };
    use aptos_framework::bcs_stream::{Self, deserialize_u8};
    use aptos_framework::auth_data::{Self, AbstractionAuthData};
 
    // ====== Error Codes ====== //
 
    const EINVALID_PUBLIC_KEY: u64 = 0x20000;
    const EPUBLIC_KEY_NOT_PERMITTED: u64 = 0x20001;
    const EENTRY_ALREADY_EXISTS: u64 = 0x20002;
    const ENO_PERMISSIONS: u64 = 0x20003;
    const EINVALID_SIGNATURE: u64 = 0x20004;
 
    // ====== Data Structures ====== //
 
    struct PublicKeyPermissions has key {
        public_key_table: SmartTable<vector<u8>, bool>,
    }
 
    // ====== Authenticator ====== //
 
    public fun authenticate(
        account: signer,
        auth_data: AbstractionAuthData
    ): signer acquires PublicKeyPermissions {
        let account_addr = signer::address_of(&account);
        assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);
        let permissions = borrow_global<PublicKeyPermissions>(account_addr);
 
        // Extract the public key and signature from the authenticator
        let authenticator = *auth_data::authenticator(&auth_data);
        let stream = bcs_stream::new(authenticator);
        let public_key = new_unvalidated_public_key_from_bytes(
            bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
        );
        let signature = new_signature_from_bytes(
            bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
        );
 
        // Check if the public key is permitted
        assert!(smart_table::contains(&permissions.public_key_table, unvalidated_public_key_to_bytes(&public_key)), EPUBLIC_KEY_NOT_PERMITTED);
 
        // Verify the signature
        let digest = *auth_data::digest(&auth_data);
        assert!(ed25519::signature_verify_strict(&signature, &public_key, digest), EINVALID_SIGNATURE);
 
        account
    }
 
    // ====== Core Functionality ====== //
 
    public entry fun permit_public_key(
        signer: &signer,
        public_key: vector<u8>
    ) acquires PublicKeyPermissions {
        let account_addr = signer::address_of(signer);
        assert!(std::vector::length(&public_key) == 32, EINVALID_PUBLIC_KEY);
        
        if (!exists<PublicKeyPermissions>(account_addr)) {
            move_to(signer, PublicKeyPermissions {
                public_key_table: smart_table::new(),
            });
        };
 
        let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
        assert!(
            !smart_table::contains(&permissions.public_key_table, public_key), 
            EENTRY_ALREADY_EXISTS
        );
 
        smart_table::add(&mut permissions.public_key_table, public_key, true);
    
    }
 
    public entry fun revoke_public_key(
        signer: &signer,
        public_key: vector<u8>
    ) acquires PublicKeyPermissions {
        let account_addr = signer::address_of(signer);
        
        assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);
 
        let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
        smart_table::remove(&mut permissions.public_key_table, public_key);
    }
 
}

Let’s break down the module…

Storing Public Keys

The PublicKeyPermissions struct is a key that contains a SmartTable of public keys that determines whether a public key is permitted to sign transactions on behalf of the user.

module deployer::public_key_authenticator {
  // ...
 
  struct PublicKeyPermissions has key {
      public_key_table: SmartTable<vector<u8>, bool>,
  } 
  
}

Permitting and Revoking Public Keys

We define two entry functions to permit and revoke public keys. These functions are used to add and remove public keys from the PublicKeyPermissions struct.

module deployer::public_key_authenticator {
  // ...
 
      public entry fun permit_public_key(
        signer: &signer,
        public_key: vector<u8>
    ) acquires PublicKeyPermissions {
        let account_addr = signer::address_of(signer);
        assert!(std::vector::length(&public_key) == 32, EINVALID_PUBLIC_KEY);
        
        if (!exists<PublicKeyPermissions>(account_addr)) {
            move_to(signer, PublicKeyPermissions {
                public_key_table: smart_table::new(),
            });
        };
 
        let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
        assert!(
            !smart_table::contains(&permissions.public_key_table, public_key), 
            EENTRY_ALREADY_EXISTS
        );
 
        smart_table::add(&mut permissions.public_key_table, public_key, true);
    
    }
 
    public entry fun revoke_public_key(
        signer: &signer,
        public_key: vector<u8>
    ) acquires PublicKeyPermissions {
        let account_addr = signer::address_of(signer);
        
        assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);
 
        let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
        smart_table::remove(&mut permissions.public_key_table, public_key);
    }
}

Authenticating on behalf of somebody else

The authenticate function is the main function that allows users to authenticate on behalf of somebody else using account abstraction. The authenticator will contain the public key and a signature of the user. We will verify that the public key is permitted and that the signature is valid.

The signature is the result of signing the digest. The digest is the sha256 hash of the signing message which contains information about the transaction. By signing the digest, we confirm that the user has approved the specific transaction that was submitted.

module deployer::public_key_authenticator {
    // ...
 
    public fun authenticate(
        account: signer,
        auth_data: AbstractionAuthData
    ): signer acquires PublicKeyPermissions {
        let account_addr = signer::address_of(&account);
        assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);
        let permissions = borrow_global<PublicKeyPermissions>(account_addr);
 
        // Extract the public key and signature from the authenticator
        let authenticator = *auth_data::authenticator(&auth_data);
        let stream = bcs_stream::new(authenticator);
        let public_key = new_unvalidated_public_key_from_bytes(
            bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
        );
        let signature = new_signature_from_bytes(
            bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
        );
 
        // Check if the public key is permitted
        assert!(smart_table::contains(&permissions.public_key_table, unvalidated_public_key_to_bytes(&public_key)), EPUBLIC_KEY_NOT_PERMITTED);
 
        // Verify the signature
        let digest = *auth_data::digest(&auth_data);
        assert!(ed25519::signature_verify_strict(&signature, &public_key, digest), EINVALID_SIGNATURE);
 
        account
    }
}

To deploy the module, you can use the following commands from the Aptos CLI. We assume that you already have set up a workspace with aptos init and declared the named addresses in your Move.toml file.

aptos move publish --named-addresses deployer=0x1234567890123456789012345678901234567890

2. Setup your Environment

Once deployed, you can setup your environment. In this example, we will use Devnet and create an account named alice as the user that will be authenticated on behalf of and bob as the user that will be permitted to sign transactions on behalf of alice.

const DEPLOYER = "0x<public_key_authenticator_deployer>"
 
const aptos = new Aptos(new AptosConfig({ network: Network.DEVNET }));
 
const alice = Account.generate();
const bob = Account.generate();
 
const authenticationFunctionInfo = `${deployer}::public_key_authenticator:authenticate`;

3. (Optional) Check if Account Abstraction is Enabled

Before we enable the authentication function, we can check if the account has account abstraction enabled by calling the isAccountAbstractionEnabled function. This will return a boolean value indicating if the account has account abstraction enabled.

const accountAbstractionStatus = await aptos.account.isAccountAbstractionEnabled({
    accountAddress: alice.accountAddress,
    authenticationFunction,
});
 
console.log("Account Abstraction status: ", accountAbstractionStatus);

4. Enable the Authentication Function

Assuming that the account does not have account abstraction enabled, we need to enable the authentication function for the account. This can be done by calling the enableAccountAbstractionTransaction function. This creates a raw transaction that needs to be signed and submitted to the network. In this example, alice will be the account that will be enabled.

const transaction = await aptos.abstraction.enableAccountAbstractionTransaction({
  accountAddress: alice.accountAddress,
  authenticationFunction,
});
 
const pendingTransaction = await aptos.signAndSubmitTransaction({
  transaction,
  signer: alice.signer,
});
 
await aptos.waitForTransaction({ hash: pendingTransaction.hash });
 
console.log("Account Abstraction enabled for account: ", alice.accountAddress);

5. Permit Bob’s Public Key

Now that we have enabled the authentication function, we can permit bob’s public key to sign transactions on behalf of alice.

const enableBobPublicKeyTransaction = await aptos.transaction.build.simple({
    sender: alice.accountAddress,
    data: {
      function: `${alice.accountAddress}::public_key_authenticator::permit_public_key`,
      typeArguments: [],
      functionArguments: [bob.publicKey.toUint8Array()],
    },
  });
 
const pendingEnableBobPublicKeyTransaction = await aptos.signAndSubmitTransaction({
  signer: alice,
  transaction: enableBobPublicKeyTransaction,
});
 
await aptos.waitForTransaction({ hash: pendingEnableBobPublicKeyTransaction.hash });
 
console.log(`Enable Bob's public key transaction hash: ${pendingEnableBobPublicKeyTransaction.hash}`);

6. Create an Abstracted Account

Now that we have permitted bob’s public key, we can create an abstracted account that will be used to sign transactions on behalf of alice. Notice that the signer function uses bob’s signer.

const abstractedAccount = new AbstractedAccount({
  accountAddress: alice.accountAddress,
  signer: (digest) => {
      const serializer = new Serializer();
      bob.publicKey.serialize(serializer);
      bob.sign(digest).serialize(serializer);
      return serializer.toUint8Array();
  },
  authenticationFunction,
});

7. Sign and Submit a Transaction using the Abstracted Account

Now that we have created the abstracted account, we can use it to sign transactions normally. It is important that the sender field in the transaction is the same as the abstracted account’s address.

const coinTransferTransaction = new aptos.transaction.build.simple({
  sender: abstractedAccount.accountAddress,
  data: {
    function: "0x1::coin::transfer",
    typeArguments: ["0x1::aptos_coin::AptosCoin"],
    functionArguments: [alice.accountAddress, 100],
  },
});
 
const pendingCoinTransferTransaction = await aptos.transaction.signAndSubmitTransaction({
  transaction: coinTransferTransaction,
  signer: abstractedAccount,
});
 
await aptos.waitForTransaction({ hash: pendingCoinTransferTransaction.hash });
 
console.log("Coin transfer transaction submitted! ", pendingCoinTransferTransaction.hash);

8. Conclusion

To verify that you have successfully sign and submitted the transaction using the abstracted account, you can use the explorer to check the transaction. If the transaction signature contains a function_info and auth_data field, it means you succesfully used account abstraction! The full E2E demo can be found here

Transaction Signature

Management Operations

If you want to disable account abstraction for an account, you can use the disableAccountAbstractionTransaction. If you do not specify an authentication function, the transaction will disable all authentication functions for the account.

const transaction = aptos.abstraction.disableAccountAbstractionTransaction({
  accountAddress: alice.accountAddress,
  /**
   * The authentication function to be disabled. If left `undefined`, all authentication functions will be disabled.
  */
  authenticationFunction,
});

Application User Experience

Applications that want to leverage account abstraction will want to provide a user experience that allows users to check if the account has account abstraction enabled, and to enable it, if it is not enabled.

Below is a diagram of the UX flow for enabling account abstraction.

Account Abstraction UX