#![deny(rustdoc::all)]
#![allow(rustdoc::missing_doc_code_examples)]
#![deny(clippy::unwrap_used)]
use anchor_lang::prelude::*;
use anchor_lang::solana_program;
use vipers::prelude::*;
mod events;
mod instructions;
mod state;
mod validators;
pub use events::*;
pub use instructions::*;
pub use state::*;
pub const SECONDS_PER_DAY: i64 = 60 * 60 * 24;
pub const MAX_DELAY_SECONDS: i64 = 365 * SECONDS_PER_DAY;
pub const DEFAULT_GRACE_PERIOD: i64 = 14 * SECONDS_PER_DAY;
pub const NO_ETA: i64 = -1;
declare_id!("GokivDYuQXPZCWRkwMhdH2h91KpDQXBEmpgBgs55bnpH");
#[program]
#[deny(missing_docs)]
pub mod smart_wallet {
use super::*;
#[access_control(ctx.accounts.validate())]
pub fn create_smart_wallet(
ctx: Context<CreateSmartWallet>,
_bump: u8,
max_owners: u8,
owners: Vec<Pubkey>,
threshold: u64,
minimum_delay: i64,
) -> Result<()> {
invariant!(minimum_delay >= 0, "delay must be positive");
invariant!(minimum_delay < MAX_DELAY_SECONDS, DelayTooHigh);
invariant!((max_owners as usize) >= owners.len(), "max_owners");
let smart_wallet = &mut ctx.accounts.smart_wallet;
smart_wallet.base = ctx.accounts.base.key();
smart_wallet.bump = *unwrap_int!(ctx.bumps.get("smart_wallet"));
smart_wallet.threshold = threshold;
smart_wallet.minimum_delay = minimum_delay;
smart_wallet.grace_period = DEFAULT_GRACE_PERIOD;
smart_wallet.owner_set_seqno = 0;
smart_wallet.num_transactions = 0;
smart_wallet.owners = owners.clone();
emit!(WalletCreateEvent {
smart_wallet: ctx.accounts.smart_wallet.key(),
owners,
threshold,
minimum_delay,
timestamp: Clock::get()?.unix_timestamp
});
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn set_owners(ctx: Context<Auth>, owners: Vec<Pubkey>) -> Result<()> {
let smart_wallet = &mut ctx.accounts.smart_wallet;
if (owners.len() as u64) < smart_wallet.threshold {
smart_wallet.threshold = owners.len() as u64;
}
smart_wallet.owners = owners.clone();
smart_wallet.owner_set_seqno = unwrap_int!(smart_wallet.owner_set_seqno.checked_add(1));
emit!(WalletSetOwnersEvent {
smart_wallet: ctx.accounts.smart_wallet.key(),
owners,
timestamp: Clock::get()?.unix_timestamp
});
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn change_threshold(ctx: Context<Auth>, threshold: u64) -> Result<()> {
invariant!(
threshold <= ctx.accounts.smart_wallet.owners.len() as u64,
InvalidThreshold
);
let smart_wallet = &mut ctx.accounts.smart_wallet;
smart_wallet.threshold = threshold;
emit!(WalletChangeThresholdEvent {
smart_wallet: ctx.accounts.smart_wallet.key(),
threshold,
timestamp: Clock::get()?.unix_timestamp
});
Ok(())
}
pub fn create_transaction(
ctx: Context<CreateTransaction>,
bump: u8,
instructions: Vec<TXInstruction>,
) -> Result<()> {
create_transaction_with_timelock(ctx, bump, instructions, NO_ETA)
}
#[access_control(ctx.accounts.validate())]
pub fn create_transaction_with_timelock(
ctx: Context<CreateTransaction>,
_bump: u8,
instructions: Vec<TXInstruction>,
eta: i64,
) -> Result<()> {
let smart_wallet = &ctx.accounts.smart_wallet;
let owner_index = smart_wallet.try_owner_index(ctx.accounts.proposer.key())?;
let clock = Clock::get()?;
let current_ts = clock.unix_timestamp;
if smart_wallet.minimum_delay != 0 {
invariant!(
eta >= unwrap_int!(current_ts.checked_add(smart_wallet.minimum_delay as i64)),
InvalidETA
);
}
if eta != NO_ETA {
invariant!(eta >= 0, "ETA must be positive");
let delay = unwrap_int!(eta.checked_sub(current_ts));
invariant!(delay >= 0, "ETA must be in the future");
invariant!(delay <= MAX_DELAY_SECONDS, DelayTooHigh);
}
let owners = &smart_wallet.owners;
let mut signers = Vec::new();
signers.resize(owners.len(), false);
signers[owner_index] = true;
let index = smart_wallet.num_transactions;
let smart_wallet = &mut ctx.accounts.smart_wallet;
smart_wallet.num_transactions = unwrap_int!(smart_wallet.num_transactions.checked_add(1));
let tx = &mut ctx.accounts.transaction;
tx.smart_wallet = smart_wallet.key();
tx.index = index;
tx.bump = *unwrap_int!(ctx.bumps.get("transaction"));
tx.proposer = ctx.accounts.proposer.key();
tx.instructions = instructions.clone();
tx.signers = signers;
tx.owner_set_seqno = smart_wallet.owner_set_seqno;
tx.eta = eta;
tx.executor = Pubkey::default();
tx.executed_at = -1;
emit!(TransactionCreateEvent {
smart_wallet: ctx.accounts.smart_wallet.key(),
transaction: ctx.accounts.transaction.key(),
proposer: ctx.accounts.proposer.key(),
instructions,
eta,
timestamp: Clock::get()?.unix_timestamp
});
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn approve(ctx: Context<Approve>) -> Result<()> {
instructions::approve::handler(ctx)
}
#[access_control(ctx.accounts.validate())]
pub fn unapprove(ctx: Context<Approve>) -> Result<()> {
instructions::unapprove::handler(ctx)
}
#[access_control(ctx.accounts.validate())]
pub fn execute_transaction(ctx: Context<ExecuteTransaction>) -> Result<()> {
let smart_wallet = &ctx.accounts.smart_wallet;
let wallet_seeds: &[&[&[u8]]] = &[&[
b"GokiSmartWallet" as &[u8],
&smart_wallet.base.to_bytes(),
&[smart_wallet.bump],
]];
do_execute_transaction(ctx, wallet_seeds)
}
#[access_control(ctx.accounts.validate())]
pub fn execute_transaction_derived(
ctx: Context<ExecuteTransaction>,
index: u64,
bump: u8,
) -> Result<()> {
let smart_wallet = &ctx.accounts.smart_wallet;
let wallet_seeds: &[&[&[u8]]] = &[&[
b"GokiSmartWalletDerived" as &[u8],
&smart_wallet.key().to_bytes(),
&index.to_le_bytes(),
&[bump],
]];
do_execute_transaction(ctx, wallet_seeds)
}
#[access_control(ctx.accounts.validate())]
pub fn owner_invoke_instruction(
ctx: Context<OwnerInvokeInstruction>,
index: u64,
bump: u8,
ix: TXInstruction,
) -> Result<()> {
let smart_wallet = &ctx.accounts.smart_wallet;
let invoker_seeds: &[&[&[u8]]] = &[&[
b"GokiSmartWalletOwnerInvoker" as &[u8],
&smart_wallet.key().to_bytes(),
&index.to_le_bytes(),
&[bump],
]];
solana_program::program::invoke_signed(
&(&ix).into(),
ctx.remaining_accounts,
invoker_seeds,
)?;
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn owner_invoke_instruction_v2(
ctx: Context<OwnerInvokeInstruction>,
index: u64,
bump: u8,
invoker: Pubkey,
data: Vec<u8>,
) -> Result<()> {
let smart_wallet = &ctx.accounts.smart_wallet;
let invoker_seeds: &[&[&[u8]]] = &[&[
b"GokiSmartWalletOwnerInvoker" as &[u8],
&smart_wallet.key().to_bytes(),
&index.to_le_bytes(),
&[bump],
]];
let program_id = ctx.remaining_accounts[0].key();
let accounts: Vec<AccountMeta> = ctx.remaining_accounts[1..]
.iter()
.map(|v| AccountMeta {
pubkey: *v.key,
is_signer: if v.key == &invoker { true } else { v.is_signer },
is_writable: v.is_writable,
})
.collect();
let ix = &solana_program::instruction::Instruction {
program_id,
accounts,
data,
};
solana_program::program::invoke_signed(ix, ctx.remaining_accounts, invoker_seeds)?;
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn create_subaccount_info(
ctx: Context<CreateSubaccountInfo>,
_bump: u8,
subaccount: Pubkey,
smart_wallet: Pubkey,
index: u64,
subaccount_type: SubaccountType,
) -> Result<()> {
let (address, _derived_bump) = match subaccount_type {
SubaccountType::Derived => Pubkey::find_program_address(
&[
b"GokiSmartWalletDerived" as &[u8],
&smart_wallet.to_bytes(),
&index.to_le_bytes(),
],
&crate::ID,
),
SubaccountType::OwnerInvoker => Pubkey::find_program_address(
&[
b"GokiSmartWalletOwnerInvoker" as &[u8],
&smart_wallet.to_bytes(),
&index.to_le_bytes(),
],
&crate::ID,
),
};
invariant!(address == subaccount, SubaccountOwnerMismatch);
let info = &mut ctx.accounts.subaccount_info;
info.smart_wallet = smart_wallet;
info.subaccount_type = subaccount_type;
info.index = index;
Ok(())
}
}
#[derive(Accounts)]
#[instruction(bump: u8, max_owners: u8)]
pub struct CreateSmartWallet<'info> {
pub base: Signer<'info>,
#[account(
init,
seeds = [
b"GokiSmartWallet".as_ref(),
base.key().to_bytes().as_ref()
],
bump,
payer = payer,
space = SmartWallet::space(max_owners),
)]
pub smart_wallet: Account<'info, SmartWallet>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Auth<'info> {
#[account(mut, signer)]
pub smart_wallet: Account<'info, SmartWallet>,
}
#[derive(Accounts)]
#[instruction(bump: u8, instructions: Vec<TXInstruction>)]
pub struct CreateTransaction<'info> {
#[account(mut)]
pub smart_wallet: Account<'info, SmartWallet>,
#[account(
init,
seeds = [
b"GokiTransaction".as_ref(),
smart_wallet.key().to_bytes().as_ref(),
smart_wallet.num_transactions.to_le_bytes().as_ref()
],
bump,
payer = payer,
space = Transaction::space(instructions),
)]
pub transaction: Account<'info, Transaction>,
pub proposer: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct ExecuteTransaction<'info> {
pub smart_wallet: Account<'info, SmartWallet>,
#[account(mut)]
pub transaction: Account<'info, Transaction>,
pub owner: Signer<'info>,
}
#[derive(Accounts)]
pub struct OwnerInvokeInstruction<'info> {
pub smart_wallet: Account<'info, SmartWallet>,
pub owner: Signer<'info>,
}
#[derive(Accounts)]
#[instruction(bump: u8, subaccount: Pubkey)]
pub struct CreateSubaccountInfo<'info> {
#[account(
init,
seeds = [
b"GokiSubaccountInfo".as_ref(),
&subaccount.to_bytes()
],
bump,
payer = payer,
space = 8 + SubaccountInfo::LEN
)]
pub subaccount_info: Account<'info, SubaccountInfo>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
fn do_execute_transaction(ctx: Context<ExecuteTransaction>, seeds: &[&[&[u8]]]) -> Result<()> {
for ix in ctx.accounts.transaction.instructions.iter() {
solana_program::program::invoke_signed(&(ix).into(), ctx.remaining_accounts, seeds)?;
}
let tx = &mut ctx.accounts.transaction;
tx.executor = ctx.accounts.owner.key();
tx.executed_at = Clock::get()?.unix_timestamp;
emit!(TransactionExecuteEvent {
smart_wallet: ctx.accounts.smart_wallet.key(),
transaction: ctx.accounts.transaction.key(),
executor: ctx.accounts.owner.key(),
timestamp: Clock::get()?.unix_timestamp
});
Ok(())
}
#[error_code]
pub enum ErrorCode {
#[msg("The given owner is not part of this smart wallet.")]
InvalidOwner,
#[msg("Estimated execution block must satisfy delay.")]
InvalidETA,
#[msg("Delay greater than the maximum.")]
DelayTooHigh,
#[msg("Not enough owners signed this transaction.")]
NotEnoughSigners,
#[msg("Transaction is past the grace period.")]
TransactionIsStale,
#[msg("Transaction hasn't surpassed time lock.")]
TransactionNotReady,
#[msg("The given transaction has already been executed.")]
AlreadyExecuted,
#[msg("Threshold must be less than or equal to the number of owners.")]
InvalidThreshold,
#[msg("Owner set has changed since the creation of the transaction.")]
OwnerSetChanged,
#[msg("Subaccount does not belong to smart wallet.")]
SubaccountOwnerMismatch,
#[msg("Buffer already finalized.")]
BufferFinalized,
#[msg("Buffer bundle not found.")]
BufferBundleNotFound,
#[msg("Buffer index specified is out of range.")]
BufferBundleOutOfRange,
#[msg("Buffer has not been finalized.")]
BufferBundleNotFinalized,
#[msg("Buffer bundle has already been executed.")]
BufferBundleExecuted,
}