aip | title | author | discussions-to (*optional) | Status | type | created | updated (*optional) |
---|---|---|---|---|---|---|---|
104 |
Account Abstraction |
lightmark, igor-aptos, davidiw |
Draft |
Framework |
10/01/2024 |
01/24/2025 |
Account Abstraction (AA) on Aptos allows any account to be authenticated through move code in addition to existing
authentication schemes supported by native code. This AIP will introduce a new authenticator called Abstraction
that
delegates the authentication verification to AptosVM running move-based authentication logic.
How to properly design an authentication function/module is out of scope.
The PayMaster
, aka, online gas payer, is not included in the design of AA in this AIP.
The core concept is to enable the move function to authenticate account signers, moving beyond the limitations of native Rust authenticators, which only support a restricted set of cryptographic schemes. To accomplish this, we propose transferring the authentication process from native Rust code to the AptosVM layer for each abstracted account. The solution we propose involves the following steps:
- Store the
FunctionInfo
of the customized Move authentication function in a new resource at the AA (Abstract Account) address. - When the account is configured to support an
AbstractionAuthenticator
, the Move function stored in step 1 will be executed (leveraging native dynamic dispatch) to perform the authentication check.
- No major security concerns: Users who opt for AA are responsible for managing their account’s security.
- Scalable: This approach can easily scale as it decouples authentication from rigid native cryptographic checks and supports multiple authentication functions.
- Audit-proof and safe: The dispatchable function mechanism has already been audited and is widely used in the FA standard, ensuring its security.
- Rich transaction context: The Transaction Context already provides comprehensive information about the current transaction, which can be leveraged for robust authentication.
This solution enhances flexibility and scalability while maintaining a secure and efficient authentication process.
Account Abstraction(AA) aims to support dispatchable authentication at the level of smart contract. The account can set up in such a way that the transaction can be authorized with complicated move smart contract instead of native account authenticators provided by Aptos blockchain core, enabling a series of possibilities:
- Multisig v2 account
- Sandbox or temporary account to isolate assets/risks, etc.
- Restricted permissions granted to trusted applications so users don’t need to sign every transaction.
- Delegation of asset management to a different account (semi-custodial model)
For Ethereum’s ERC-4337 solution, Aptos would need to implement a user operation pool, support contract wallets, and include an external Paymaster for handling authentication and payments. This would require bundling signatures and transactions into user operations, then verifying them through independent modules on-chain, adding significant complexity to development and interaction.
First and foremost, we will introduce a new native Authenticator that represents the authentication scheme specified by the current transaction is AA.
pub enum AccountAuthenticator {
//...
Abstraction {
function_info: FunctionInfo,
auth_data: AbstractionAuthData,
},
}
where function_info
indicates the function registered on the sender
account that will be called as authentication check
and auth_data
is enum type that includes all the necessary information as the input to the function with upgradabilty.
If the AccountAuthenticator
is not Abstraction
, the authentication_key
passed to the prologue will be Some(auth_key)
;
while if it is Abstraction
then authentication_key == None
At the framework level, we create a new resource, DispatchableAuthenticator
. If an account has DispatchableAuthenticator
,
it allows the account to be authenticated via the functions registered in the internal map.
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
/// The dispatchable authenticator that defines how to authenticates this account in the specified module.
/// An integral part of Account Abstraction.
enum DispatchableAuthenticator has key, copy, drop {
V1 { auth_functions: OrderedMap<FunctionInfo, bool> }
}
enum AbstractionAuthData has copy, drop {
V1 { digest: vector<u8>, authenticator: vector<u8> },
}
fun authenticate(account: signer, func_info: FunctionInfo, auth_data: AbstractionAuthData): signer acquires DispatchableAuthenticator {
let func_infos = dispatchable_authenticator_internal(signer::address_of(&account));
assert!(ordered_map::contains(func_infos, &func_info), error::not_found(EFUNCTION_INFO_EXISTENCE));
function_info::load_module_from_function(&func_info);
dispatchable_authenticate(account, auth_data, &func_info)
}
/// The native function to dispatch customized move authentication function.
native fun dispatchable_authenticate(account: signer, auth_data: AbstractionAuthData, function: &FunctionInfo): signer;
Any authentication function must have the same interface as the following function:
/// The native function to dispatch customized move authentication function.
public fun dispatchable_authenticate(account: signer, auth_data: AbstractionAuthData): signer;
The first parameter, signer
, represents the current account that needs authentication, and the function's return value is also a signer
. Typically, this function returns the input signer if the authentication check is successful. However, in some cases, a permissioned signer may be returned to control the resources the transaction can access.
auth_data
is an enum that contains all the data required to authenticate the transaction via account abstraction. The initial version just has two fields, where digest
is the sha3 digest of the corresponding transaction signing message and authenticator
is the attached byte array which serves as the proof of the authentication verification. Usually it encodes the signature over the digest at least.
Best Practice: To prevent replay attacks (both cross-chain), the authenticator
must depend on digest
.
In AptosVM, the following rust function dispatchable_authenticate
, will call the authenticate
function in the framework with function_info
and auth_data
specified in
the authenticator, who will dynamically call the function specified by function_info
to perform the authentication check
if the function is registered in the map. The return value is the serialized signer data which will be the input to the
transaction body.
fn dispatchable_authenticate(
session: &mut SessionExt,
gas_meter: &mut impl GasMeter,
account: AccountAddress,
function_info: FunctionInfo,
auth_data: &AbstractionAuthData,
traversal_context: &mut TraversalContext,
module_storage: &impl ModuleStorage,
) -> VMResult<Vec<u8>> {
let auth_data = bcs::to_bytes(auth_data).expect("from rust succeeds");
let mut params = serialize_values(&vec![
MoveValue::Signer(account),
function_info.as_move_value(),
]);
params.push(auth_data);
session
.execute_function_bypass_visibility(
&ACCOUNT_ABSTRACTION_MODULE,
AUTHENTICATE,
vec![],
params,
gas_meter,
traversal_context,
module_storage,
)
.map(|mut return_vals| {
assert!(
return_vals.mutable_reference_outputs.is_empty()
&& return_vals.return_values.len() == 1,
"Abstraction authentication function must only have 1 return value"
);
let (signer_data, signer_layout) = return_vals.return_values.pop().expect("Must exist");
assert_eq!(
signer_layout,
MoveTypeLayout::Signer,
"Abstraction authentication function returned non-signer."
);
signer_data
})
}
The AA authentication flow works as below:
- Transaction carries the Abstraction Authenticator.
- Rust authenticator will run and find it is Abstraction Authenticator so native authentication is noop. But
authentication_key
will be set toNone
. - When running prologue, if the
authentication_key
isNone
, it will bypass the authentication key check. - After the prologue, AptosVM will check each senders' whether their account authenticator is Abstraction Authenticator. If yes, AptosVM will run the move auth function specified in the
function_info
withauth_data
and expect asigner
to be returned. If any sender's auth function aborts, the authentication check for the whole transaction fails. It is noted that this function has a gas limit if the function call runs above the gas limit it will abort immediately. - Run the user transaction body as usual with the
signer
s just returned.
The feature flag for this feature is std::features::ACCOUNT_ABSTRACTION
.
The above PR has API e2e test the functionality of Account abstraction with various cases.
-
AA is hard to adopt w/o an auth function library. To make it easy for user to use, the framework should provide a bunch of commonly used authentication modules/functions in the future work for users to use. Otherwise, it is really hard for beginner to implement customized authentication logic in move.
-
The gas limit of the auth function is fixed so AA cannot support too complicated move functions.
- The first parameter to the auth function is account
signer
. We believe it is not a security concern since if you choose to delegate your account access to this function, you already agree to give all the access to your account to this function. - If the gas limit of the auth function is higher than necessary, it may become a dos attack vector.
We might want to find a way to cover the auth function gas in a way that does not have a fixed limit so abstracted auth function can have more complex logic.
Oct 2024
By the end of 2024.
By the end of 2024.