Skip to main content

Authentication Methods in Smart Wallets

Smart Accounts, much like regular accounts (EOAs), depend on signatures for validating transactions and messages. What sets Smart Accounts apart is their ability to utilize various signature schemes, such as Passkeys and Multisig.

Passkeys

Candide supports on-chain Passkeys, allowing users to sign transactions directly from their devices. Using their default phone/computer auth system, such as PIN, biometrics, or FaceID, signatures are authenticated on-chain through smart contracts. All without reliance on centralized infrastructure.

See a demo on passkeys.candide.dev, and learn more on the Passkeys doc page.

Multisig

Candide supports on-chain multisig (multi-signature) accounts, which enhance security by requiring multiple approvals for transactions. This feature is ideal for implementing two-factor authentication (2FA) and is well-suited for wallets targeting companies or DAOs, where transactions often require multiple key approvals.

Visit the dedicated guide on Multisig.

Traditional Signers

Externally Owner Account (EOAs) utilize both a private and public key for security. Similarly, Smart Accounts can employ the same approach for securing funds. This can be achieved through various means such as a locally generated private key, utilizing a hosted MPC solution, or securing it with existing user wallets like MetaMask.

Social / Email

Developers can integrate third-party "Signer services" into their smart accounts, providing the benefits of traditional Web2 onboarding experiences. These services enable user authentication through familiar methods such as email magic links, social logins, or SMS.

  • Candide's AbstractionKit is highly adaptable, supporting any third-party Signer service. The process for assigning the smart account owner and signing a User Operation is consistent across all services. These services generate an EOA wallet for each user in their backend and provide authentication via social logins (e.g., Twitter), email, or a backend passkey.

  • Since the service provides the EOA public address, it can be easily assigned as the owner of the smart wallet in AbstractionKit. The authentication method does not affect integration, ensuring a consistent process for assigning smart account ownership and signing User Operations. For details on how to integrate as third-parties as main signer, you can refer to the guide for EOA wallets.

  • However, it is important to note that using third-party services as the primary signer of the account makes the wallet you are offering custodial. Candide recommends using onchain Safe Passkeys as the main signer, while offering third-parties as recovery options via the onchain Guardian Recovery. This approach ensures progressive self-custody, allowing users to modify their recovery options as needed.

Below are some third-party recovery options that you can consider. Candide does not endorse any of these options; the guides are provided for educational purposes only.

ServiceKey Management MethodAuthentication MethodsPlug-n-play Front End?Guide
DfnsHSM, MPCCustomNo
DynamicMPC (Turnkey under the hood)Email, Social, WalletsYes
FireblocksMPCCustomNo
Lit ProtocolMPCEmail, Social, PhoneNoAdd a Google Account as a Recovery Method using Lit
Magic.linkAWS KMSEmail, SocialYesAdd an Social Account as a Recovery Method using Magic
PrivyMPCEmail, Social, WalletsYes
TurnkeyAWS KMSCustomNoUse Turnkey with AbstractionKit
Web3AuthMPC, key shardingSocial, EmailYes

EOA Wallets & Third-party signers

You can allow users to use MetaMask, or any other EOA wallet, or third-party signers to be the owner of the Smart Account.EOAs exposes a JavaScript Ethereum Provider API.

Setup Account Owner

import { SafeAccountV0_3_0 as SafeAccount, EIP712_SAFE_OPERATION_V7_TYPE } from "abstractionkit";
import { BrowserProvider } from "ethers";

const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const signerAddress = await signer.getAddress();

const smartAccount = SafeAccount.initializeNewAccount([signerAddress]);

Signing a UserOperation with EIP-712

When signing a userOperation using abstractionkit, you have two options based on your use case. Regardless of the approach you choose, you must format the resulting signature into a userOperation compatible signature using formatEip712SignaturesToUserOperationSignature().

  1. Direct Signing of the EIP-712 Hash

Use this method if you won't displayed more information for the user to validate. This approach is quick using getUserOperationEip712Hash().

let userOperation = ... // Use createUserOperation() to help you construct the userOp below

const safeUserOpHash = SafeAccount.getUserOperationEip712Hash(userOperation, chainId);

const signature = signer.signingKey.sign(safeUserOpHash).serialized;

userOperation.signature = SafeAccount.formatEip712SignaturesToUseroperationSignature([signer.address], [signature]);
  1. Signing with EIP-712 Typed Data

This method involves retrieving the EIP-712 typed data using getUserOperationEip712Data(), displaying it to the user for review, and then signing the typed data. It allows the user to verify the data before signing.

let userOperation = ... // Use createUserOperation() to help you construct the userOp below

const { domain, types, messageValue } = SafeAccount.getUserOperationEip712Data(userOperation, chainId);

const signature = await wallet.signTypedData(domain, types, messageValue);
userOperation.signature = SafeAccount.formatEip712SignaturesToUseroperationSignature(
[signer.address],
[signature]
);

Private key Storage

Traditional wallets encrypts the private key in local storage. In this setup, the owner of the smart account is represented by it. Use signUserOperation.

Signup Account Owner

import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";

const ownerPublicAddress = process.env.PUBLIC_KEY;

const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);

Sign UserOperation

const chainId = BigInt(process.env.CHAIN_ID as string);
const ownerPrivateKey = process.env.PRIVATE_KEY as string;

const userOperation = ... // returned from createUserOperation()

userOperation.signature = smartAccount.signUserOperation(
userOperation,
[privateKey],
chainId,
)