Authentication
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 Passkeys here.
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 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. For details on how to integrate as third-parties as main signer, 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 Passkeys as the main signer, while offering third-parties as recovery options via the on-chain Guardian Recovery. This approach ensures progressive self-custody, allowing users to modify 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 |
---|---|---|---|---|
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
You can allow users to use MetaMask, or any other EOA wallet as the signer to the Smart Account. EOAs exposes a JavaScript Ethereum Provider API.
Setup Account Owner
- ethers example
- viem example
import { SafeAccountV0_2_0 as SafeAccount } 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_2_0 as SafeAccount } from "abstractionkit";
import { createWalletClient, custom } from "viem";
const client = createWalletClient({
transport: custom(window.ethereum!),
});
const signerAddresses = await client.requestAddresses();
const signerAddress = signerAddresses[0];
const smartAccount = SafeAccount.initializeNewAccount([signerAddress]);
Sign UserOperation
Format the signature to a userOperation signature using formatEip712SignaturesToUseroperationSignature.
- ethers
- viem
let userOperation = ... // Use createUserOperation() to help you construct the userOp below
const domain = {
chainId: process.env.CHAIN_ID,
verifyingContract: smartAccount.safe4337ModuleAddress,
};
const types = SafeAccount.EIP712_SAFE_OPERATION_TYPE;
// formate according to EIP712 Safe Operation Type
const { sender, ...userOp } = userOperation;
const safeUserOperation = {
...userOp,
safe: userOperation.sender,
validUntil: BigInt(0),
validAfter: BigInt(0),
entryPoint: smartAccount.entrypointAddress,
};
const signature = await signer.signTypedData(domain, types, safeUserOperation);
const formatedSig = SafeAccount.formatEip712SignaturesToUseroperationSignature([signer.address], [signature]);
userOperation.signature = formatedSig;
let userOperation = ... // Use createUserOperation() to help you construct the userOp below
const domain = {
chainId: process.env.CHAIN_ID,
verifyingContract: smartAccount.safe4337ModuleAddress,
};
const types = SafeAccount.EIP712_SAFE_OPERATION_TYPE;
// formate according to EIP712 Safe Operation Type
const { sender, ...userOp } = userOperation;
const safeUserOperation = {
...userOp,
safe: userOperation.sender,
validUntil: BigInt(0),
validAfter: BigInt(0),
entryPoint: smartAccount.entrypointAddress,
};
const signature = await signer.signTypedData({
domain,
types,
primaryType: 'SafeOp',
message: safeUserOperation,
});
const formatedSig = SafeAccount.formatEip712SignaturesToUseroperationSignature([signer.address], [signature]);
userOperation.signature = formatedSig;
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_2_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,
)