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.
Service | Key Management Method | Authentication Methods | Plug-n-play Front End? | Guide |
---|---|---|---|---|
Dfns | HSM, MPC | Custom | No | |
Dynamic | MPC (Turnkey under the hood) | Email, Social, Wallets | Yes | |
Fireblocks | MPC | Custom | No | |
Lit Protocol | MPC | Email, Social, Phone | No | Add a Google Account as a Recovery Method using Lit |
Magic.link | AWS KMS | Email, Social | Yes | Add an Social Account as a Recovery Method using Magic |
Privy | MPC | Email, Social, Wallets | Yes | |
Turnkey | AWS KMS | Custom | No | Use Turnkey with AbstractionKit |
Web3Auth | MPC, key sharding | Social, Email | Yes |
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
- ethers example
- viem example
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]);
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";
import { createWalletClient, custom } from "viem";
const viemWalletClient = createWalletClient({
transport: custom(window.ethereum!),
});
const signerAddresses = await viemWalletClient.requestAddresses();
const signerAddress = signerAddresses[0];
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()
.
- 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()
.
- ethers
- viem
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]);
let userOperation = ... // Use createUserOperation() to help you construct the userOp below
const safeUserOpHash = SafeAccount.getUserOperationEip712Hash(
userOperation,
chainId
) as `0x${string}`;
const signature = await viemWalletClient.account.sign({ hash: safeUserOpHash });
userOperation.signature = SafeAccount.formatEip712SignaturesToUseroperationSignature(
[signer.address],
[signature]
);
- 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.
- ethers
- viem
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]
);
let userOperation = ... // Use createUserOperation() to help you construct the userOp below
const { domain, types, messageValue } = SafeAccount.getUserOperationEip712Data(userOperation, chainId);
const signedTypedData = await viemWalletClient.signTypedData({
domain,
types,
message: messageValue,
primaryType: "SafeOp",
} as any);
userOperation.signature = SafeAccount.formatEip712SignaturesToUseroperationSignature(
[viemWalletClient.account.address],
[signedTypedData],
);
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,
)