smart_wallet/
lib.rs

1//! Multisig Solana wallet with Timelock capabilities.
2//!
3//! This program can be used to allow a smart wallet to govern anything a regular
4//! [Pubkey] can govern. One can use the smart wallet as a BPF program upgrade
5//! authority, a mint authority, etc.
6//!
7//! To use, one must first create a [SmartWallet] account, specifying two important
8//! parameters:
9//!
10//! 1. Owners - the set of addresses that sign transactions for the smart wallet.
11//! 2. Threshold - the number of signers required to execute a transaction.
12//! 3. Minimum Delay - the minimum amount of time that must pass before a [Transaction]
13//!                    can be executed. If 0, this is ignored.
14//!
15//! Once the [SmartWallet] account is created, one can create a [Transaction]
16//! account, specifying the parameters for a normal Solana instruction.
17//!
18//! To sign, owners should invoke the [smart_wallet::approve] instruction, and finally,
19//! [smart_wallet::execute_transaction], once enough (i.e. [SmartWallet::threshold]) of the owners have
20//! signed.
21#![deny(rustdoc::all)]
22#![allow(rustdoc::missing_doc_code_examples)]
23#![deny(clippy::unwrap_used)]
24
25use anchor_lang::prelude::*;
26use anchor_lang::solana_program;
27use vipers::prelude::*;
28
29mod events;
30mod instructions;
31mod state;
32mod validators;
33
34pub use events::*;
35pub use instructions::*;
36pub use state::*;
37
38/// Number of seconds in a day.
39pub const SECONDS_PER_DAY: i64 = 60 * 60 * 24;
40
41/// Maximum timelock delay.
42pub const MAX_DELAY_SECONDS: i64 = 365 * SECONDS_PER_DAY;
43
44/// Default number of seconds until a transaction expires.
45pub const DEFAULT_GRACE_PERIOD: i64 = 14 * SECONDS_PER_DAY;
46
47/// Constant declaring that there is no ETA of the transaction.
48pub const NO_ETA: i64 = -1;
49
50declare_id!("GokivDYuQXPZCWRkwMhdH2h91KpDQXBEmpgBgs55bnpH");
51
52#[program]
53#[deny(missing_docs)]
54/// Goki smart wallet program.
55pub mod smart_wallet {
56    use super::*;
57
58    /// Initializes a new [SmartWallet] account with a set of owners and a threshold.
59    #[access_control(ctx.accounts.validate())]
60    pub fn create_smart_wallet(
61        ctx: Context<CreateSmartWallet>,
62        _bump: u8,
63        max_owners: u8,
64        owners: Vec<Pubkey>,
65        threshold: u64,
66        minimum_delay: i64,
67    ) -> Result<()> {
68        invariant!(minimum_delay >= 0, "delay must be positive");
69        invariant!(minimum_delay < MAX_DELAY_SECONDS, DelayTooHigh);
70
71        invariant!((max_owners as usize) >= owners.len(), "max_owners");
72
73        let smart_wallet = &mut ctx.accounts.smart_wallet;
74        smart_wallet.base = ctx.accounts.base.key();
75        smart_wallet.bump = *unwrap_int!(ctx.bumps.get("smart_wallet"));
76
77        smart_wallet.threshold = threshold;
78        smart_wallet.minimum_delay = minimum_delay;
79        smart_wallet.grace_period = DEFAULT_GRACE_PERIOD;
80
81        smart_wallet.owner_set_seqno = 0;
82        smart_wallet.num_transactions = 0;
83
84        smart_wallet.owners = owners.clone();
85
86        emit!(WalletCreateEvent {
87            smart_wallet: ctx.accounts.smart_wallet.key(),
88            owners,
89            threshold,
90            minimum_delay,
91            timestamp: Clock::get()?.unix_timestamp
92        });
93        Ok(())
94    }
95
96    /// Sets the owners field on the smart_wallet. The only way this can be invoked
97    /// is via a recursive call from execute_transaction -> set_owners.
98    #[access_control(ctx.accounts.validate())]
99    pub fn set_owners(ctx: Context<Auth>, owners: Vec<Pubkey>) -> Result<()> {
100        let smart_wallet = &mut ctx.accounts.smart_wallet;
101        if (owners.len() as u64) < smart_wallet.threshold {
102            smart_wallet.threshold = owners.len() as u64;
103        }
104
105        smart_wallet.owners = owners.clone();
106        smart_wallet.owner_set_seqno = unwrap_int!(smart_wallet.owner_set_seqno.checked_add(1));
107
108        emit!(WalletSetOwnersEvent {
109            smart_wallet: ctx.accounts.smart_wallet.key(),
110            owners,
111            timestamp: Clock::get()?.unix_timestamp
112        });
113        Ok(())
114    }
115
116    /// Changes the execution threshold of the smart_wallet. The only way this can be
117    /// invoked is via a recursive call from execute_transaction ->
118    /// change_threshold.
119    #[access_control(ctx.accounts.validate())]
120    pub fn change_threshold(ctx: Context<Auth>, threshold: u64) -> Result<()> {
121        invariant!(
122            threshold <= ctx.accounts.smart_wallet.owners.len() as u64,
123            InvalidThreshold
124        );
125        let smart_wallet = &mut ctx.accounts.smart_wallet;
126        smart_wallet.threshold = threshold;
127
128        emit!(WalletChangeThresholdEvent {
129            smart_wallet: ctx.accounts.smart_wallet.key(),
130            threshold,
131            timestamp: Clock::get()?.unix_timestamp
132        });
133        Ok(())
134    }
135
136    /// Creates a new [Transaction] account, automatically signed by the creator,
137    /// which must be one of the owners of the smart_wallet.
138    pub fn create_transaction(
139        ctx: Context<CreateTransaction>,
140        bump: u8,
141        instructions: Vec<TXInstruction>,
142    ) -> Result<()> {
143        create_transaction_with_timelock(ctx, bump, instructions, NO_ETA)
144    }
145
146    /// Creates a new [Transaction] account with time delay.
147    #[access_control(ctx.accounts.validate())]
148    pub fn create_transaction_with_timelock(
149        ctx: Context<CreateTransaction>,
150        _bump: u8,
151        instructions: Vec<TXInstruction>,
152        eta: i64,
153    ) -> Result<()> {
154        let smart_wallet = &ctx.accounts.smart_wallet;
155        let owner_index = smart_wallet.try_owner_index(ctx.accounts.proposer.key())?;
156
157        let clock = Clock::get()?;
158        let current_ts = clock.unix_timestamp;
159        if smart_wallet.minimum_delay != 0 {
160            invariant!(
161                eta >= unwrap_int!(current_ts.checked_add(smart_wallet.minimum_delay as i64)),
162                InvalidETA
163            );
164        }
165        if eta != NO_ETA {
166            invariant!(eta >= 0, "ETA must be positive");
167            let delay = unwrap_int!(eta.checked_sub(current_ts));
168            invariant!(delay >= 0, "ETA must be in the future");
169            invariant!(delay <= MAX_DELAY_SECONDS, DelayTooHigh);
170        }
171
172        // generate the signers boolean list
173        let owners = &smart_wallet.owners;
174        let mut signers = Vec::new();
175        signers.resize(owners.len(), false);
176        signers[owner_index] = true;
177
178        let index = smart_wallet.num_transactions;
179        let smart_wallet = &mut ctx.accounts.smart_wallet;
180        smart_wallet.num_transactions = unwrap_int!(smart_wallet.num_transactions.checked_add(1));
181
182        // init the TX
183        let tx = &mut ctx.accounts.transaction;
184        tx.smart_wallet = smart_wallet.key();
185        tx.index = index;
186        tx.bump = *unwrap_int!(ctx.bumps.get("transaction"));
187
188        tx.proposer = ctx.accounts.proposer.key();
189        tx.instructions = instructions.clone();
190        tx.signers = signers;
191        tx.owner_set_seqno = smart_wallet.owner_set_seqno;
192        tx.eta = eta;
193
194        tx.executor = Pubkey::default();
195        tx.executed_at = -1;
196
197        emit!(TransactionCreateEvent {
198            smart_wallet: ctx.accounts.smart_wallet.key(),
199            transaction: ctx.accounts.transaction.key(),
200            proposer: ctx.accounts.proposer.key(),
201            instructions,
202            eta,
203            timestamp: Clock::get()?.unix_timestamp
204        });
205        Ok(())
206    }
207
208    /// Approves a transaction on behalf of an owner of the [SmartWallet].
209    #[access_control(ctx.accounts.validate())]
210    pub fn approve(ctx: Context<Approve>) -> Result<()> {
211        instructions::approve::handler(ctx)
212    }
213
214    /// Unapproves a transaction on behalf of an owner of the [SmartWallet].
215    #[access_control(ctx.accounts.validate())]
216    pub fn unapprove(ctx: Context<Approve>) -> Result<()> {
217        instructions::unapprove::handler(ctx)
218    }
219
220    /// Executes the given transaction if threshold owners have signed it.
221    #[access_control(ctx.accounts.validate())]
222    pub fn execute_transaction(ctx: Context<ExecuteTransaction>) -> Result<()> {
223        let smart_wallet = &ctx.accounts.smart_wallet;
224        let wallet_seeds: &[&[&[u8]]] = &[&[
225            b"GokiSmartWallet" as &[u8],
226            &smart_wallet.base.to_bytes(),
227            &[smart_wallet.bump],
228        ]];
229        do_execute_transaction(ctx, wallet_seeds)
230    }
231
232    /// Executes the given transaction signed by the given derived address,
233    /// if threshold owners have signed it.
234    /// This allows a Smart Wallet to receive SOL.
235    #[access_control(ctx.accounts.validate())]
236    pub fn execute_transaction_derived(
237        ctx: Context<ExecuteTransaction>,
238        index: u64,
239        bump: u8,
240    ) -> Result<()> {
241        let smart_wallet = &ctx.accounts.smart_wallet;
242        // Execute the transaction signed by the smart_wallet.
243        let wallet_seeds: &[&[&[u8]]] = &[&[
244            b"GokiSmartWalletDerived" as &[u8],
245            &smart_wallet.key().to_bytes(),
246            &index.to_le_bytes(),
247            &[bump],
248        ]];
249        do_execute_transaction(ctx, wallet_seeds)
250    }
251
252    /// Invokes an arbitrary instruction as a PDA derived from the owner,
253    /// i.e. as an "Owner Invoker".
254    ///
255    /// This is useful for using the multisig as a whitelist or as a council,
256    /// e.g. a whitelist of approved owners.
257    #[access_control(ctx.accounts.validate())]
258    pub fn owner_invoke_instruction(
259        ctx: Context<OwnerInvokeInstruction>,
260        index: u64,
261        bump: u8,
262        ix: TXInstruction,
263    ) -> Result<()> {
264        let smart_wallet = &ctx.accounts.smart_wallet;
265        // Execute the transaction signed by the smart_wallet.
266        let invoker_seeds: &[&[&[u8]]] = &[&[
267            b"GokiSmartWalletOwnerInvoker" as &[u8],
268            &smart_wallet.key().to_bytes(),
269            &index.to_le_bytes(),
270            &[bump],
271        ]];
272
273        solana_program::program::invoke_signed(
274            &(&ix).into(),
275            ctx.remaining_accounts,
276            invoker_seeds,
277        )?;
278
279        Ok(())
280    }
281
282    /// Invokes an arbitrary instruction as a PDA derived from the owner,
283    /// i.e. as an "Owner Invoker".
284    ///
285    /// This is useful for using the multisig as a whitelist or as a council,
286    /// e.g. a whitelist of approved owners.
287    ///
288    /// # Arguments
289    /// - `index` - The index of the owner-invoker.
290    /// - `bump` - Bump seed of the owner-invoker.
291    /// - `invoker` - The owner-invoker.
292    /// - `data` - The raw bytes of the instruction data.
293    #[access_control(ctx.accounts.validate())]
294    pub fn owner_invoke_instruction_v2(
295        ctx: Context<OwnerInvokeInstruction>,
296        index: u64,
297        bump: u8,
298        invoker: Pubkey,
299        data: Vec<u8>,
300    ) -> Result<()> {
301        let smart_wallet = &ctx.accounts.smart_wallet;
302        // Execute the transaction signed by the smart_wallet.
303        let invoker_seeds: &[&[&[u8]]] = &[&[
304            b"GokiSmartWalletOwnerInvoker" as &[u8],
305            &smart_wallet.key().to_bytes(),
306            &index.to_le_bytes(),
307            &[bump],
308        ]];
309
310        let program_id = ctx.remaining_accounts[0].key();
311        let accounts: Vec<AccountMeta> = ctx.remaining_accounts[1..]
312            .iter()
313            .map(|v| AccountMeta {
314                pubkey: *v.key,
315                is_signer: if v.key == &invoker { true } else { v.is_signer },
316                is_writable: v.is_writable,
317            })
318            .collect();
319        let ix = &solana_program::instruction::Instruction {
320            program_id,
321            accounts,
322            data,
323        };
324
325        solana_program::program::invoke_signed(ix, ctx.remaining_accounts, invoker_seeds)?;
326        Ok(())
327    }
328
329    /// Creates a struct containing a reverse mapping of a subaccount to a
330    /// [SmartWallet].
331    #[access_control(ctx.accounts.validate())]
332    pub fn create_subaccount_info(
333        ctx: Context<CreateSubaccountInfo>,
334        _bump: u8,
335        subaccount: Pubkey,
336        smart_wallet: Pubkey,
337        index: u64,
338        subaccount_type: SubaccountType,
339    ) -> Result<()> {
340        let (address, _derived_bump) = match subaccount_type {
341            SubaccountType::Derived => Pubkey::find_program_address(
342                &[
343                    b"GokiSmartWalletDerived" as &[u8],
344                    &smart_wallet.to_bytes(),
345                    &index.to_le_bytes(),
346                ],
347                &crate::ID,
348            ),
349            SubaccountType::OwnerInvoker => Pubkey::find_program_address(
350                &[
351                    b"GokiSmartWalletOwnerInvoker" as &[u8],
352                    &smart_wallet.to_bytes(),
353                    &index.to_le_bytes(),
354                ],
355                &crate::ID,
356            ),
357        };
358
359        invariant!(address == subaccount, SubaccountOwnerMismatch);
360
361        let info = &mut ctx.accounts.subaccount_info;
362        info.smart_wallet = smart_wallet;
363        info.subaccount_type = subaccount_type;
364        info.index = index;
365
366        Ok(())
367    }
368}
369
370/// Accounts for [smart_wallet::create_smart_wallet].
371#[derive(Accounts)]
372#[instruction(bump: u8, max_owners: u8)]
373pub struct CreateSmartWallet<'info> {
374    /// Base key of the SmartWallet.
375    pub base: Signer<'info>,
376
377    /// The [SmartWallet] to create.
378    #[account(
379        init,
380        seeds = [
381            b"GokiSmartWallet".as_ref(),
382            base.key().to_bytes().as_ref()
383        ],
384        bump,
385        payer = payer,
386        space = SmartWallet::space(max_owners),
387    )]
388    pub smart_wallet: Account<'info, SmartWallet>,
389
390    /// Payer to create the smart_wallet.
391    #[account(mut)]
392    pub payer: Signer<'info>,
393
394    /// The [System] program.
395    pub system_program: Program<'info, System>,
396}
397
398/// Accounts for [smart_wallet::set_owners] and [smart_wallet::change_threshold].
399#[derive(Accounts)]
400pub struct Auth<'info> {
401    /// The [SmartWallet].
402    #[account(mut, signer)]
403    pub smart_wallet: Account<'info, SmartWallet>,
404}
405
406/// Accounts for [smart_wallet::create_transaction].
407#[derive(Accounts)]
408#[instruction(bump: u8, instructions: Vec<TXInstruction>)]
409pub struct CreateTransaction<'info> {
410    /// The [SmartWallet].
411    #[account(mut)]
412    pub smart_wallet: Account<'info, SmartWallet>,
413    /// The [Transaction].
414    #[account(
415        init,
416        seeds = [
417            b"GokiTransaction".as_ref(),
418            smart_wallet.key().to_bytes().as_ref(),
419            smart_wallet.num_transactions.to_le_bytes().as_ref()
420        ],
421        bump,
422        payer = payer,
423        space = Transaction::space(instructions),
424    )]
425    pub transaction: Account<'info, Transaction>,
426    /// One of the owners. Checked in the handler via [SmartWallet::try_owner_index].
427    pub proposer: Signer<'info>,
428    /// Payer to create the [Transaction].
429    #[account(mut)]
430    pub payer: Signer<'info>,
431    /// The [System] program.
432    pub system_program: Program<'info, System>,
433}
434
435/// Accounts for [smart_wallet::execute_transaction].
436#[derive(Accounts)]
437pub struct ExecuteTransaction<'info> {
438    /// The [SmartWallet].
439    pub smart_wallet: Account<'info, SmartWallet>,
440    /// The [Transaction] to execute.
441    #[account(mut)]
442    pub transaction: Account<'info, Transaction>,
443    /// An owner of the [SmartWallet].
444    pub owner: Signer<'info>,
445}
446
447/// Accounts for [smart_wallet::owner_invoke_instruction].
448#[derive(Accounts)]
449pub struct OwnerInvokeInstruction<'info> {
450    /// The [SmartWallet].
451    pub smart_wallet: Account<'info, SmartWallet>,
452    /// An owner of the [SmartWallet].
453    pub owner: Signer<'info>,
454}
455
456/// Accounts for [smart_wallet::create_subaccount_info].
457#[derive(Accounts)]
458#[instruction(bump: u8, subaccount: Pubkey)]
459pub struct CreateSubaccountInfo<'info> {
460    /// The [SubaccountInfo] to create.
461    #[account(
462        init,
463        seeds = [
464            b"GokiSubaccountInfo".as_ref(),
465            &subaccount.to_bytes()
466        ],
467        bump,
468        payer = payer,
469        space = 8 + SubaccountInfo::LEN
470    )]
471    pub subaccount_info: Account<'info, SubaccountInfo>,
472    /// Payer to create the [SubaccountInfo].
473    #[account(mut)]
474    pub payer: Signer<'info>,
475    /// The [System] program.
476    pub system_program: Program<'info, System>,
477}
478
479fn do_execute_transaction(ctx: Context<ExecuteTransaction>, seeds: &[&[&[u8]]]) -> Result<()> {
480    for ix in ctx.accounts.transaction.instructions.iter() {
481        solana_program::program::invoke_signed(&(ix).into(), ctx.remaining_accounts, seeds)?;
482    }
483
484    // Burn the transaction to ensure one time use.
485    let tx = &mut ctx.accounts.transaction;
486    tx.executor = ctx.accounts.owner.key();
487    tx.executed_at = Clock::get()?.unix_timestamp;
488
489    emit!(TransactionExecuteEvent {
490        smart_wallet: ctx.accounts.smart_wallet.key(),
491        transaction: ctx.accounts.transaction.key(),
492        executor: ctx.accounts.owner.key(),
493        timestamp: Clock::get()?.unix_timestamp
494    });
495    Ok(())
496}
497
498/// Program errors.
499#[error_code]
500pub enum ErrorCode {
501    #[msg("The given owner is not part of this smart wallet.")]
502    InvalidOwner,
503    #[msg("Estimated execution block must satisfy delay.")]
504    InvalidETA,
505    #[msg("Delay greater than the maximum.")]
506    DelayTooHigh,
507    #[msg("Not enough owners signed this transaction.")]
508    NotEnoughSigners,
509    #[msg("Transaction is past the grace period.")]
510    TransactionIsStale,
511    #[msg("Transaction hasn't surpassed time lock.")]
512    TransactionNotReady,
513    #[msg("The given transaction has already been executed.")]
514    AlreadyExecuted,
515    #[msg("Threshold must be less than or equal to the number of owners.")]
516    InvalidThreshold,
517    #[msg("Owner set has changed since the creation of the transaction.")]
518    OwnerSetChanged,
519    #[msg("Subaccount does not belong to smart wallet.")]
520    SubaccountOwnerMismatch,
521    #[msg("Buffer already finalized.")]
522    BufferFinalized,
523    #[msg("Buffer bundle not found.")]
524    BufferBundleNotFound,
525    #[msg("Buffer index specified is out of range.")]
526    BufferBundleOutOfRange,
527    #[msg("Buffer has not been finalized.")]
528    BufferBundleNotFinalized,
529    #[msg("Buffer bundle has already been executed.")]
530    BufferBundleExecuted,
531}