A Solana smart wallet program with social recovery features, built using the Anchor framework. This program enables users to create secure smart accounts protected by guardians who can help recover access if the owner loses their keys.
- Overview
- Features
- Architecture
- Program Flow
- Setup & Installation
- Deployment
- Usage Guide
- Testing
- Security Considerations
Cron Wallet is a smart wallet program that provides:
- Smart Account Management: Create and manage PDA-based accounts that hold SOL and SPL tokens
- Social Recovery: Designate trusted guardians who can help recover your account
- Multi-signature Recovery: Set approval thresholds to prevent single-guardian attacks
- Payment Operations: Send both SOL and SPL tokens from your smart wallet
Program ID: 4TUaUyAqjbZr4vPCm8bq2Qojy7pswQaWJgoA6AB5m6uW
-
Smart Account Initialization
- Create a smart wallet with a unique 16-byte identifier
- Automatically initialize an associated guardian registry
- Set yourself as the initial owner
-
Guardian Management
- Add up to 4 guardians to protect your account
- Set approval threshold (minimum guardians needed for recovery)
- Only the owner can add guardians or change the threshold
-
Social Recovery
- Any guardian can propose a recovery to a new owner
- Multiple guardians must approve (based on threshold)
- Anyone can execute once the threshold is met
- Owner can cancel recovery proposals at any time
-
Payment Operations
- Send SOL from your smart account
- Transfer SPL tokens to other accounts
- Payments blocked when recovery is active (account locked)
The program uses three main account types:
pub struct SmartAccount {
pub smart_account_id: [u8; 16], // Unique identifier
pub guardian_registry: Pubkey, // Associated guardian registry
pub owner: Pubkey, // Current owner
pub is_locked: bool, // True during recovery
pub bump: u8, // PDA bump
}- PDA Seeds:
["smart_account", smart_account_id] - Size: 90 bytes
pub struct GuardianRegistry {
pub smart_account: Pubkey, // Associated smart account
pub guardians: Vec<Pubkey>, // List of guardians (max 4)
pub threshold: u8, // Required approvals
pub bump: u8, // PDA bump
}- PDA Seeds:
["guardian_registry", smart_account_pubkey] - Size: 170 bytes (supports up to 4 guardians)
pub struct RecoveryProposal {
pub smart_account: Pubkey, // Smart account being recovered
pub new_owner: Pubkey, // Proposed new owner
pub approvals: Vec<Pubkey>, // Guardians who approved
pub proposed_at: i64, // Timestamp
pub bump: u8, // PDA bump
}- PDA Seeds:
["recovery_proposal", smart_account_pubkey] - Size: 209 bytes
┌─────────────────────────────────────────────────────────────┐
│ SMART ACCOUNT LIFECYCLE │
└─────────────────────────────────────────────────────────────┘
[Uninitialized]
│
│ init_smart_account()
↓
[Active - is_locked: false] ←─────────────────────┐
│ │
│ • pay_with_sol() │
│ • pay_with_spl() │
│ • add_guardian() cancel_recovery()
│ • set_threshold() │
│ │
│ propose_recovery() │
↓ │
[Locked - is_locked: true] ─────────────────────────┘
│ ↑
│ approve_recovery() │ (by owner)
│ (by guardians) │
↓ │
[Threshold Met] │
│ │
│ execute_recovery() │
↓ │
[Ownership Transferred] ─────┘
Step 1: Initialize Smart Account
├─ Owner signs transaction
├─ Creates SmartAccount PDA
├─ Creates GuardianRegistry PDA
└─ Sets owner and initializes empty guardian list
Step 2: Add Guardians (repeat 1-4 times)
├─ Owner signs transaction
├─ Adds guardian public key to registry
└─ Maximum 4 guardians allowed
Step 3: Set Recovery Threshold
├─ Owner signs transaction
├─ Sets number of approvals needed (1-4)
└─ Must be ≤ total number of guardians
Payment with SOL:
├─ Owner signs transaction
├─ Checks account is not locked
├─ Verifies sufficient balance
└─ Transfers lamports directly from PDA
Payment with SPL Tokens:
├─ Owner signs transaction
├─ Checks account is not locked
├─ Uses PDA as token account authority
└─ Executes CPI to token program
Scenario: Owner loses access to their keys
Step 1: Propose Recovery
├─ ANY guardian initiates proposal
├─ Specifies new owner address
├─ Locks the smart account (is_locked = true)
└─ Creates RecoveryProposal PDA
Step 2: Gather Approvals
├─ Other guardians approve the proposal
├─ Each guardian can only approve once
└─ Approvals tracked in proposal.approvals[]
Step 3: Execute Recovery
├─ Anyone can execute when threshold met
├─ Ownership transferred to new owner
├─ Account unlocked (is_locked = false)
└─ RecoveryProposal PDA closed
Alternative: Cancel Recovery
├─ Original owner can cancel at any time
├─ Account unlocked
└─ RecoveryProposal PDA closed
- Rust: 1.70.0 or later
- Solana CLI: 1.18.0 or later
- Anchor CLI: 0.31.1 or later
- Node.js: 18.0.0 or later
- Yarn: 1.22.0 or later
- Clone the repository
git clone <repository-url>
cd cron-wallet-program- Install dependencies
# Install Rust dependencies
cargo build
# Install Node.js dependencies
yarn install- Configure Anchor
# Generate a new keypair (if needed)
solana-keygen new
# Update Anchor.toml with your wallet path
# wallet = "~/.config/solana/id.json"- Build the program
anchor build- Start local validator
solana-test-validator- Deploy the program
anchor deploy- Run tests
anchor test --skip-local-validator- Configure for Devnet
# Update Anchor.toml
[provider]
cluster = "devnet"
# Configure Solana CLI
solana config set --url devnet- Airdrop SOL for deployment
solana airdrop 2- Deploy
anchor deploy --provider.cluster devnet- Configure for Mainnet
# Update Anchor.toml
[provider]
cluster = "mainnet"
# Configure Solana CLI
solana config set --url mainnet-beta- Deploy (ensure you have sufficient SOL)
anchor deploy --provider.cluster mainnet-betaThe program automatically generates TypeScript types after building. Here's how to use them:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { CronWalletProgram } from "../target/types/cron_wallet_program";
import { uuid } from "uuidv4";
// Initialize connection
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.CronWalletProgram as Program<CronWalletProgram>;
// 1. Initialize Smart Account
const smartAccountId = Buffer.from(uuid().replace(/-/g, ""), "hex");
const [smartAccountPDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("smart_account"), smartAccountId],
program.programId
);
const [guardianRegistryPDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("guardian_registry"), smartAccountPDA.toBuffer()],
program.programId
);
await program.methods
.initSmartAccount(Array.from(smartAccountId))
.accounts({
smartAccount: smartAccountPDA,
guardianRegistry: guardianRegistryPDA,
authority: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// 2. Add Guardian
const guardianPubkey = new anchor.web3.PublicKey("Guardian123...");
await program.methods
.addGuardian()
.accounts({
smartAccount: smartAccountPDA,
guardianRegistry: guardianRegistryPDA,
newGuardian: guardianPubkey,
owner: provider.wallet.publicKey,
})
.rpc();
// 3. Set Threshold
await program.methods
.setThreshold(2) // Require 2 guardians to approve recovery
.accounts({
smartAccount: smartAccountPDA,
guardianRegistry: guardianRegistryPDA,
owner: provider.wallet.publicKey,
})
.rpc();
// 4. Pay with SOL
const recipient = new anchor.web3.PublicKey("Recipient123...");
await program.methods
.payWithSol(new anchor.BN(1_000_000)) // Amount in lamports
.accounts({
smartAccount: smartAccountPDA,
dstSolAccount: recipient,
owner: provider.wallet.publicKey,
})
.rpc();
// 5. Propose Recovery (as guardian)
const newOwner = new anchor.web3.PublicKey("NewOwner123...");
const [recoveryProposalPDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("recovery_proposal"), smartAccountPDA.toBuffer()],
program.programId
);
await program.methods
.proposeRecovery()
.accounts({
smartAccount: smartAccountPDA,
guardianRegistry: guardianRegistryPDA,
recoveryProposal: recoveryProposalPDA,
newOwner: newOwner,
guardian: guardianWallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([guardianWallet])
.rpc();
// 6. Approve Recovery (as other guardians)
await program.methods
.approveRecovery()
.accounts({
smartAccount: smartAccountPDA,
guardianRegistry: guardianRegistryPDA,
recoveryProposal: recoveryProposalPDA,
guardian: anotherGuardianWallet.publicKey,
})
.signers([anotherGuardianWallet])
.rpc();
// 7. Execute Recovery (anyone can execute once threshold is met)
await program.methods
.executeRecovery()
.accounts({
smartAccount: smartAccountPDA,
guardianRegistry: guardianRegistryPDA,
recoveryProposal: recoveryProposalPDA,
executor: provider.wallet.publicKey,
})
.rpc();
// 8. Cancel Recovery (only original owner)
await program.methods
.cancelRecovery()
.accounts({
smartAccount: smartAccountPDA,
recoveryProposal: recoveryProposalPDA,
owner: provider.wallet.publicKey,
})
.rpc();You can also interact with the program using the Solana CLI with the generated IDL:
# Get the program's IDL
anchor idl init <PROGRAM_ID> -f target/idl/cron_wallet_program.json
# Call instructions
solana program invoke <PROGRAM_ID> --data <BASE58_ENCODED_INSTRUCTION_DATA># Build and test in one command
anchor test
# Or separately
anchor build
anchor test --skip-build
# Test on specific network
anchor test --skip-local-validator # Uses existing validatorThe test suite (tests/cron-wallet-program.ts) covers:
- ✅ Smart account initialization
- ✅ Guardian management (add/remove)
- ✅ Threshold configuration
- ✅ Payment operations (SOL and SPL)
- ✅ Full recovery flow
- ✅ Recovery cancellation
- ✅ Edge cases and error conditions
# Run with verbose output
yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts --grep "test_name"-
PDA Security
- All accounts use Program Derived Addresses (PDAs)
- Seeds are deterministic and collision-resistant
- No user-controlled seeds in critical paths
-
Access Control
- Owner-only operations validated with
has_oneconstraints - Guardian verification in recovery operations
- Double-spend prevention via account locking
- Owner-only operations validated with
-
Recovery Safety
- Account locks during active recovery proposals
- Threshold prevents single guardian attacks
- Owner can always cancel malicious recovery attempts
- No funds can leave while account is locked
-
Arithmetic Safety
- All arithmetic uses checked operations
- Overflow/underflow protection
- Balance verification before transfers
-
Guardian Selection
- Choose trustworthy individuals you know personally
- Use diverse guardians (different geographies, relationships)
- Consider using 3-4 guardians with threshold of 2-3
- Regularly communicate with guardians
-
Threshold Configuration
- Too low: Single compromised guardian is dangerous
- Too high: May be impossible to recover if guardians unavailable
- Recommended:
threshold = (num_guardians / 2) + 1
-
Operational Security
- Keep your owner keypair secure
- Monitor your smart account for unauthorized recovery proposals
- Set up notifications for guardian events
- Test recovery process on devnet first
-
Recovery Process
- Always verify the new owner address multiple times
- Use a secure channel to communicate with guardians
- Consider time-delayed recovery (future enhancement)
- Document your recovery plan
- Maximum 4 Guardians: Current implementation limits to 4 guardians due to account size constraints
- No Time Lock: Recovery can execute immediately after threshold is met
- No Guardian Removal: Once added, guardians cannot be removed (only threshold can change)
- No Multi-Step Auth: Single signature transactions (no session keys or delegated signing)
The program emits events for important state changes:
// Smart account created
pub struct SmartAccountInitialized {
pub smart_account: Pubkey,
pub smart_account_id: [u8; 16],
pub guardian_registry: Pubkey,
pub owner: Pubkey,
}
// Recovery proposed
pub struct RecoveryProposed {
pub smart_account: Pubkey,
pub recovery_proposal: Pubkey,
pub new_owner: Pubkey,
pub proposed_at: i64,
}
// Guardian approved recovery
pub struct RecoveryApproved {
pub smart_account: Pubkey,
pub recovery_proposal: Pubkey,
pub guardian: Pubkey,
pub approvals_count: u8,
}
// Recovery executed
pub struct RecoveryExecuted {
pub smart_account: Pubkey,
pub old_owner: Pubkey,
pub new_owner: Pubkey,
}
// Recovery cancelled
pub struct RecoveryCancelled {
pub smart_account: Pubkey,
pub recovery_proposal: Pubkey,
}
// Token transfer
pub struct TokenTransfer {
pub token_mint: Pubkey,
pub amount: u64,
pub sender: Pubkey,
pub recipient: Pubkey,
}Monitor these events to track account activity and security events.
| Code | Error | Description |
|---|---|---|
| 6000 | InvalidOwner | Only the owner can execute this operation |
| 6001 | InsufficientFunds | Insufficient funds for transfer |
| 6002 | Overflow | Arithmetic overflow |
| 6003 | AccountLocked | Smart account is locked during recovery |
| 6004 | InvalidGuardian | Only a guardian can execute this operation |
| 6005 | RecoveryProposalExists | Recovery proposal already exists |
| 6006 | NoActiveRecoveryProposal | No active recovery proposal |
| 6007 | GuardianAlreadyApproved | Guardian has already approved this recovery |
| 6008 | ThresholdNotMet | Threshold not met for recovery execution |
| 6009 | InvalidRecoveryProposal | Recovery proposal does not match smart account |
Potential improvements for future versions:
- Time-locked Recovery: Add delay between proposal and execution
- Guardian Rotation: Allow adding/removing guardians
- Session Keys: Enable temporary spending limits without full ownership
- Multi-signature Payments: Require guardian approval for large transfers
- Recovery History: Track past recovery attempts
- Emergency Freeze: Allow guardians to freeze account in case of compromise
- Scheduled Payments: Enable recurring or scheduled transactions
- Multi-owner Accounts: Support shared ownership models
- Integration with DeFi: Direct integration with lending/staking protocols
- Mobile SDK: Native mobile integration
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Write comprehensive tests for new features
- Update documentation for any API changes
- Follow Rust and Anchor best practices
- Ensure all tests pass before submitting PR
- Add events for significant state changes
ISC
For questions, issues, or feature requests:
- Open an issue on GitHub
- Review the test files for usage examples
- Check the Anchor documentation: https://www.anchor-lang.com/
Built with:
- Anchor Framework - Solana smart contract framework
- Solana - High-performance blockchain
- SPL Token - Solana token standard