Getting Started with Volta on EVM
Volta is an ERC-4337 smart account that provides multi-signature ownership, session keys with granular permissions, and gas sponsorship on EVM-compatible chains.
Prerequisites
- Node.js (v18+) and npm/yarn
- A funded EOA wallet on your target chain (testnet for development)
- Basic familiarity with ERC-4337 account abstraction concepts
- Access to a bundler service (e.g., Stackup, Pimlico, Alchemy)
Key Concepts
Account Abstraction (ERC-4337)
Instead of sending transactions directly, users submit UserOperations to a bundler. The ERC-4337 EntryPoint contract validates signatures and executes operations on behalf of the smart account.
Multi-Signature Ownership
Volta implements M-of-N multi-sig where: - N = total number of non-Volta owners - M = minQuorum (minimum signatures required) - The first owner is always the Volta platform address
For example, a minQuorum=2 account with 3 non-Volta owners means any 2 of those 3 must sign.
Session Keys
Session keys are delegated signers with scoped permissions. They can: - Be restricted to specific target contracts - Be limited to specific function selectors - Have parameter-level constraints (e.g., "transfer amount <= 100 USDC") - Have time bounds (valid only within a time window) - Have ETH value limits
Step 1: Install Dependencies
Step 2: Compute the Account Address
Before deploying, you can compute the deterministic address:
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://your-rpc-url');
const FACTORY_ABI = [
'function getAddress(address accountImplementation, uint256 salt) view returns (address)',
];
const factory = new ethers.Contract(
'0xFactoryAddress...',
FACTORY_ABI,
provider
);
const accountAddress = await factory.getAddress(
'0xImplementationAddress...',
42n // your salt
);
console.log('Account will be deployed at:', accountAddress);
Step 3: Deploy the Account
Account deployment is done through the VoltaFactory. The factory requires a signature from its owner to authorize account creation.
const FACTORY_ABI = [
'function createAccount(address accountImplementation, uint256 salt, address sessionKeyValidator, address executor, address[] owners, uint256 minQuorum, address entryPoint, bytes signature) returns (address)',
'function buildSignatureDigest(address accountImplementation, uint256 salt, address sessionKeyValidator, address executor, address[] owners, uint256 minQuorum, address entryPoint) view returns (bytes32)',
];
const factory = new ethers.Contract(
'0xFactoryAddress...',
FACTORY_ABI,
provider
);
// Parameters
const implementationAddress = '0x...'; // VoltaAccount implementation
const salt = 42n;
const sessionKeyValidator = '0x...'; // SessionKeyValidator deployment
const executor = '0x...'; // DefaultExecutor deployment
const owners = [
'0xVolta...', // Volta platform address (must be first)
'0xOwner1...', // First owner
'0xOwner2...', // Second owner
];
const minQuorum = 1; // Require 1 non-Volta owner signature
const entryPoint = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'; // v0.6
Requirements:
- At least 3 addresses in owners (Volta + 2 non-Volta owners minimum)
- minQuorum >= 1 and minQuorum <= owners.length - 1
- All addresses must be unique and non-zero
Step 4: Verify the Account
Check that the account was deployed correctly:
const ACCOUNT_ABI = [
'function getDeposit() view returns (uint256)',
'function entryPoint() view returns (address)',
'function accountImplementation() view returns (address)',
'function sessionKeyValidator() view returns (address)',
'function executor() view returns (address)',
];
const account = new ethers.Contract(accountAddress, ACCOUNT_ABI, provider);
console.log('EntryPoint:', await account.entryPoint());
console.log('Implementation:', await account.accountImplementation());
console.log('Session Key Validator:', await account.sessionKeyValidator());
console.log('Executor:', await account.executor());
console.log('Deposit:', ethers.formatEther(await account.getDeposit()));
Step 5: Fund the Account
The account needs ETH for gas (or use a paymaster). Deposit to the EntryPoint:
// Option 1: Send ETH directly to the account
const tx = await wallet.sendTransaction({
to: accountAddress,
value: ethers.parseEther('0.01'),
});
await tx.wait();
// Option 2: Deposit to the EntryPoint via a UserOperation
// (requires the account to already have some gas or use a paymaster)
Step 6: Send Your First UserOperation
Build and submit a simple ETH transfer:
// Build the execute calldata
const accountInterface = new ethers.Interface([
'function execute(address dest, uint256 value, bytes data)',
]);
const callData = accountInterface.encodeFunctionData('execute', [
'0xRecipient...', // destination
ethers.parseEther('0.001'), // value
'0x', // no calldata (simple transfer)
]);
// Build the UserOperation (simplified - use a bundler SDK in production)
const userOp = {
sender: accountAddress,
nonce: '0x0', // get from EntryPoint
initCode: '0x',
callData,
callGasLimit: '0x50000',
verificationGasLimit: '0x50000',
preVerificationGas: '0x50000',
maxFeePerGas: '0x...',
maxPriorityFeePerGas: '0x...',
paymasterAndData: '0x',
signature: '0x', // placeholder
};
// Sign the UserOperation
const userOpHash = '...'; // computed by the bundler or EntryPoint
const signature = await ownerWallet.signMessage(ethers.getBytes(userOpHash));
userOp.signature = signature;
// Submit via bundler
// await bundler.sendUserOperation(userOp, entryPointAddress);
Account Functions Reference
| Function | Description | Who Can Call | Volta Required? |
|---|---|---|---|
execute |
Execute a single transaction | Via EntryPoint | No |
executeBatch |
Execute multiple transactions atomically | Via EntryPoint | No |
enable |
Enable session keys with permissions | Via EntryPoint | Yes |
disable |
Disable session keys | Via EntryPoint | Yes |
updateOwners |
Change owners and quorum | Via EntryPoint | Yes |
upgradeSessionKeyValidator |
Replace validator module | Via EntryPoint | Yes |
upgradeExecutor |
Replace executor module | Via EntryPoint | Yes |
multiUpgrade |
Upgrade both modules | Via EntryPoint | Yes |
updateEntryPoint |
Change EntryPoint | Via EntryPoint | Yes |
upgradeTo |
Upgrade account implementation | Via EntryPoint | Yes |
getDeposit |
Check EntryPoint deposit | Anyone | No |
isValidSignature |
ERC-1271 verification | Anyone | No |
Security Considerations
-
Protect owner keys: Each owner key contributes to the quorum. Secure them with hardware wallets or secure key management.
-
Volta must sign privileged operations: Upgrades, owner changes, and session key management require Volta's signature to prevent unauthorized modifications.
-
Session keys limit blast radius: Use session keys for routine operations instead of owner keys. If compromised, damage is limited to the session key's permissions.
-
Use time-bounded session keys: Always set
validUntilto minimize risk from key compromise. -
Test on testnets first: Always verify UserOperation construction and signing on a testnet.