Skip to main content

Design Principles for Third-Party Developers

Account Abstraction RC

Account Abstraction is currently a release candidate and, therefore, only available on Testnet and Devnet.
If you test it out, please share your feedback with us in the IOTA Builders Discord.

Authenticator functions are read-only by construction

The type system prevents an authenticator function from modifying any state. All its inputs are immutable. This is intentional: authentication must be a direct decision, it reads the ledger state and says yes or no. If you need mutable state during authentication (for example, to record that a one-time code has been used), that state must be managed through the account object itself, which can be done during the execution of the AA transaction.

Account state lives as dynamic fields

The best approach to design abstract accounts is typically to use stateless structs and store meaningful data as dynamic fields. This allows the account struct itself to be minimal while allowing arbitrary extensions/plug-ins that enable additional authenticator functions. When defining an abstract account, store authentication-relevant state, member lists, key stores, approval tables, nonces, as dynamic fields under the account's UID. The authenticator function receives a read-only reference to the account and can traverse those dynamic fields.

The AuthContext enables intent-aware authentication

The tx_inputs and tx_commands fields of AuthContext expose the full PTB. Authenticator functions can use this to inspect what the AA transaction will do and enforce policies over it. This makes authentication aware of intent, not just identity: rather than simply verifying who is sending the transaction, the authenticator can also verify what the AA transaction does, checking which functions are called, which objects are involved, and what values are passed, before deciding whether to allow execution.

An Abstract Account's transactions can be sponsored — paid for by an account other than the sender — and the sponsor itself can be either kind of account:

  • A standard protocol-created account (a regular keypair, multisig, passkey, …). Sponsorship is authorized by a standard GenericSignature over the transaction; the protocol's signature check is the only validation the sponsor goes through.
  • An Abstract Account whose Move authenticator authorizes the sponsorship. This authenticator runs the same way it would for a sender — receiving the full AuthContext — and can therefore inspect the PTB and the sender's authentication info before deciding to pay.

A timing asymmetry shapes what each sponsor kind can verify. The protocol runs sponsor verification (the standard signature check or the sponsor's AA Move authenticator) at both the pre-consensus and the post-consensus phases. The sender's Move authenticator, by contrast, runs only at post-consensus, and only when the transaction is sponsored — on the sponsor's gas budget. So by the time the sponsor's pre-consensus approval lands, the sponsor has already committed to pay for whatever sender-authentication code path runs after consensus.

That timing gap is the root of the sender-identity binding issue, and it manifests most sharply for a standard protocol-created sponsor: such a sponsor can only ever sign the transaction digest, which commits to the sender's address but not to which authenticator function or auth instance the sender will use at execution time. Between sponsor-signing and submission, the sender can rotate their on-chain authenticator function to a far more expensive variant — and the sponsor's signature still verifies, draining the budget. A well-designed Sponsor AA closes the gap because the sender-identity check is part of the AA's pre-consensus authenticator itself, not retrofitted to an off-chain signature. The two complementary patterns:

  • Policy-driven sponsor AAs keep on-chain whitelists (accepted sender authenticator functions, per-user gas allowances, …) and consult them at sponsor-validation time. No per-transaction signature from the sponsor is required; the policy is the sponsor's standing approval. Sender identity is bound intrinsically — the sender's authenticator function must be whitelisted, the gas budget must fit a per-user cap, and any per-tx debit happens in a separate PTB command that the sponsor's authenticator verifies by scanning tx_commands. See Sponsor Transactions with a Whitelist and Per-User Gas Allowances for an end-to-end example.

  • Signature-driven sponsor AAs require a per-transaction signature from a key the sponsor controls. Because the signature is verified inside the AA's authenticator, the AA can require it to commit to more than the transaction digest — at minimum, the sender's auth digest and (when present) the sender's authenticator function identity. See Binding Sponsor Approval to Sender Identity for the threat model and the canonical helper that gets the byte layout right.

Standard-protocol sponsors don't have these levers — their signature inherently covers only ctx.digest(). The mitigation there is to delegate sponsorship to a sponsor AA that can enforce sender-identity binding, or to restrict standard-sponsor flows to senders whose authenticator function cannot rotate (e.g. immutable accounts via create_immutable_account_v1).

Authentication is signaled by execution, not return value

The authenticator function has no return type. Returning successfully means "authenticated"; aborting means "rejected". Use assert! and standard Move abort patterns to express rejection conditions.

Avoid silent failures: if authentication should fail, abort with a descriptive error.

Authenticator references are version-aware but decoupled from compile-time dependencies

AuthenticatorFunctionRefV1 stores a runtime reference to a package, module, and function. This reference is not a compile-time import. When you rotate to a new authenticator version, you create a new AuthenticatorFunctionRefV1 pointing to the new package. Old transactions that were authorized by the old authenticator remain valid; new transactions go through the new one. This decoupling means you can upgrade authentication logic without redeploying the account type itself.

However, this also means that an upgrade to any authenticator function will not be automatically enforced for all accounts using it. Users MUST opt-in to use the new authenticator function version by rotating the AuthenticatorFunctionRefV1.

Immutable accounts have fixed authenticators

iota::account::create_immutable_account_v1 creates an account whose object can never change. This means the AuthenticatorFunctionRefV1 is permanently fixed. Use this only when your authentication logic is entirely stateless and needs no future update path. This can be used, for instance, for throwaway accounts. Mutable shared accounts created with iota::account::create_account_v1 are the typical choice for any production use case.

The bytecode verifier enforces module-local account creation

The IOTA Move bytecode verifier requires that the Account type argument to iota::account::create_account_v1 be defined in the same module that calls it. This cannot be circumvented. It ensures that no module can create an abstract account impersonating a type defined elsewhere, and that account creation is always an authorized operation within the module that owns the type.