use borsh::BorshSerialize;
use eyre::{Result, eyre};
use sha2::{Digest, Sha256};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use std::str::FromStr;
#[cfg(feature = "client")]
pub mod client;
pub const SYSTEM_PROGRAM_ID: Pubkey = Pubkey::new_from_array([0u8; 32]);
pub const SPL_TOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array([
0x06, 0xdd, 0xf6, 0xe1, 0xd7, 0x65, 0xa1, 0x93, 0xd9, 0xcb, 0xe1, 0x46, 0xce, 0xeb, 0x79, 0xac,
0x1c, 0xb4, 0x85, 0xed, 0x5f, 0x5b, 0x37, 0x91, 0x3a, 0x8c, 0xf5, 0x85, 0x7e, 0xff, 0x00, 0xa9,
]);
pub const WSOL_MINT: &str = "So11111111111111111111111111111111111111112";
pub fn is_wsol_mint(mint: &str) -> bool {
mint == WSOL_MINT
}
pub fn system_transfer_ix(from: &Pubkey, to: &Pubkey, lamports: u64) -> Instruction {
let mut data = Vec::with_capacity(12);
data.extend_from_slice(&2u32.to_le_bytes());
data.extend_from_slice(&lamports.to_le_bytes());
Instruction {
program_id: SYSTEM_PROGRAM_ID,
accounts: vec![AccountMeta::new(*from, true), AccountMeta::new(*to, false)],
data,
}
}
pub fn sync_native_ix(ata: &Pubkey) -> Instruction {
Instruction {
program_id: SPL_TOKEN_PROGRAM_ID,
accounts: vec![AccountMeta::new(*ata, false)],
data: vec![17],
}
}
pub fn close_token_account_ix(ata: &Pubkey, dest: &Pubkey, owner: &Pubkey) -> Instruction {
Instruction {
program_id: SPL_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*ata, false),
AccountMeta::new(*dest, false),
AccountMeta::new_readonly(*owner, true),
],
data: vec![9],
}
}
pub mod seeds {
pub const FACTORY_SEED: &[u8] = b"factory";
pub const INSTANCE_SEED: &[u8] = b"instance";
pub const BALANCE_SEED: &[u8] = b"balance";
pub const INSTANCE_VAULT_SEED: &[u8] = b"instance_vault";
pub const WITHDRAW_NONCE_SEED: &[u8] = b"withdraw_nonce";
pub const FEE_ACCRUAL_SEED: &[u8] = b"fee_accrual";
pub const WITHDRAW_EPOCH_SEED: &[u8] = b"withdraw_epoch";
}
pub fn sysvar_rent_id() -> Pubkey {
Pubkey::from_str("SysvarRent111111111111111111111111111111111")
.expect("SysvarRent id is a well-known constant; parse must succeed")
}
pub fn sysvar_instructions_id() -> Pubkey {
Pubkey::from_str("Sysvar1nstructions1111111111111111111111111")
.expect("Sysvar Instructions id is a well-known constant; parse must succeed")
}
pub fn ata_program_id() -> Pubkey {
Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
.expect("ATA program id is a well-known constant; parse must succeed")
}
pub fn ed25519_program_id() -> Pubkey {
Pubkey::from_str("Ed25519SigVerify111111111111111111111111111")
.expect("Ed25519 precompile id is a well-known constant; parse must succeed")
}
fn anchor_ix_discriminator(method: &str) -> [u8; 8] {
let mut h = Sha256::new();
h.update(format!("global:{method}").as_bytes());
let digest = h.finalize();
let mut out = [0u8; 8];
out.copy_from_slice(&digest[..8]);
out
}
fn encode_ix<A: BorshSerialize>(method: &str, args: &A) -> Result<Vec<u8>> {
let disc = anchor_ix_discriminator(method);
let body = borsh::to_vec(args).map_err(|e| eyre!("borsh encode {}: {}", method, e))?;
let mut data = Vec::with_capacity(8 + body.len());
data.extend_from_slice(&disc);
data.extend_from_slice(&body);
Ok(data)
}
pub fn derive_factory_pda(program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[seeds::FACTORY_SEED], program_id)
}
pub fn derive_instance_pda(
factory: &Pubkey,
instance_id: u64,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
seeds::INSTANCE_SEED,
factory.as_ref(),
&instance_id.to_le_bytes(),
],
program_id,
)
}
pub fn derive_user_balance_pda(
instance: &Pubkey,
user: &Pubkey,
mint: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
seeds::BALANCE_SEED,
instance.as_ref(),
user.as_ref(),
mint.as_ref(),
],
program_id,
)
}
pub fn derive_fee_accrual_pda(
instance: &Pubkey,
mint: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[seeds::FEE_ACCRUAL_SEED, instance.as_ref(), mint.as_ref()],
program_id,
)
}
pub fn derive_withdraw_epoch_pda(
instance: &Pubkey,
mint: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[seeds::WITHDRAW_EPOCH_SEED, instance.as_ref(), mint.as_ref()],
program_id,
)
}
pub fn derive_instance_vault(
instance: &Pubkey,
mint: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[seeds::INSTANCE_VAULT_SEED, instance.as_ref(), mint.as_ref()],
program_id,
)
}
pub fn derive_vault_authority(instance: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[seeds::INSTANCE_VAULT_SEED, instance.as_ref()], program_id)
}
pub fn derive_associated_token_account(owner: &Pubkey, mint: &Pubkey) -> Pubkey {
let seeds = &[owner.as_ref(), SPL_TOKEN_PROGRAM_ID.as_ref(), mint.as_ref()];
let (ata, _bump) = Pubkey::find_program_address(seeds, &ata_program_id());
ata
}
pub fn create_idempotent_ata_ix(
payer: &Pubkey,
owner: &Pubkey,
mint: &Pubkey,
ata: &Pubkey,
) -> Instruction {
Instruction {
program_id: ata_program_id(),
accounts: vec![
AccountMeta::new(*payer, true),
AccountMeta::new(*ata, false),
AccountMeta::new_readonly(*owner, false),
AccountMeta::new_readonly(*mint, false),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
],
data: vec![1], }
}
#[derive(BorshSerialize)]
struct AmountArgs {
amount: u64,
}
pub fn deposit_ix(
program_id: &Pubkey,
instance: &Pubkey,
user: &Pubkey,
mint: &Pubkey,
user_token_account: &Pubkey,
amount: u64,
) -> Result<Instruction> {
let (user_balance, _) = derive_user_balance_pda(instance, user, mint, program_id);
let (instance_vault, _) = derive_instance_vault(instance, mint, program_id);
let (vault_authority, _) = derive_vault_authority(instance, program_id);
let data = encode_ix("deposit", &AmountArgs { amount })?;
Ok(Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new_readonly(*instance, false),
AccountMeta::new_readonly(*mint, false),
AccountMeta::new(user_balance, false),
AccountMeta::new(*user_token_account, false),
AccountMeta::new(instance_vault, false),
AccountMeta::new_readonly(vault_authority, false),
AccountMeta::new(*user, true),
AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
AccountMeta::new_readonly(sysvar_rent_id(), false),
],
data,
})
}
pub fn derive_withdraw_nonce_pda(
instance: &Pubkey,
account: &Pubkey,
nonce: u64,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
seeds::WITHDRAW_NONCE_SEED,
instance.as_ref(),
account.as_ref(),
&nonce.to_le_bytes(),
],
program_id,
)
}
#[derive(borsh::BorshSerialize, Debug)]
pub struct WithdrawalVoucherPayload {
pub instance: Pubkey,
pub account: Pubkey,
pub mint: Pubkey,
pub amount: u64,
pub nonce: u64,
pub deadline: u64,
}
#[derive(borsh::BorshSerialize, Debug)]
pub struct WithdrawVoucherArgs {
pub amount: u64,
pub nonce: u64,
pub deadline: u64,
pub signature: [u8; 64],
}
pub fn withdrawal_voucher_signing_message(
instance: &Pubkey,
account: &Pubkey,
mint: &Pubkey,
amount: u64,
nonce: u64,
deadline: u64,
) -> Result<Vec<u8>> {
let payload = WithdrawalVoucherPayload {
instance: *instance,
account: *account,
mint: *mint,
amount,
nonce,
deadline,
};
borsh::to_vec(&payload).map_err(|e| eyre!("borsh encode WithdrawalVoucherPayload: {}", e))
}
#[allow(clippy::too_many_arguments)]
pub fn withdraw_voucher_ix(
program_id: &Pubkey,
instance: &Pubkey,
account: &Pubkey,
mint: &Pubkey,
user_token_account: &Pubkey,
payer: &Pubkey,
args: &WithdrawVoucherArgs,
) -> Result<Instruction> {
let (user_balance, _) = derive_user_balance_pda(instance, account, mint, program_id);
let (instance_vault, _) = derive_instance_vault(instance, mint, program_id);
let (vault_authority, _) = derive_vault_authority(instance, program_id);
let (used_nonce, _) = derive_withdraw_nonce_pda(instance, account, args.nonce, program_id);
let (withdraw_epoch, _) = derive_withdraw_epoch_pda(instance, mint, program_id);
let data = encode_ix("withdraw_voucher", args)?;
Ok(Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new_readonly(*instance, false),
AccountMeta::new_readonly(*mint, false),
AccountMeta::new(user_balance, false),
AccountMeta::new(*user_token_account, false),
AccountMeta::new(instance_vault, false),
AccountMeta::new_readonly(vault_authority, false),
AccountMeta::new(used_nonce, false),
AccountMeta::new(withdraw_epoch, false),
AccountMeta::new_readonly(*account, false),
AccountMeta::new(*payer, true),
AccountMeta::new_readonly(sysvar_instructions_id(), false),
AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
],
data,
})
}
#[derive(borsh::BorshSerialize, Debug)]
pub struct SetWithdrawEpochCapArgs {
pub cap: u64,
}
pub fn set_withdraw_epoch_cap_ix(
program_id: &Pubkey,
instance: &Pubkey,
mint: &Pubkey,
operator_admin: &Pubkey,
cap: u64,
) -> Result<Instruction> {
let (withdraw_epoch, _) = derive_withdraw_epoch_pda(instance, mint, program_id);
let data = encode_ix("set_withdraw_epoch_cap", &SetWithdrawEpochCapArgs { cap })?;
Ok(Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new_readonly(*instance, false),
AccountMeta::new_readonly(*mint, false),
AccountMeta::new(withdraw_epoch, false),
AccountMeta::new(*operator_admin, true),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
],
data,
})
}
pub fn ed25519_verify_ix(pubkey: &[u8; 32], signature: &[u8; 64], message: &[u8]) -> Instruction {
let signature_offset: u16 = 16;
let public_key_offset: u16 = 16 + 64;
let message_offset: u16 = 16 + 64 + 32;
let message_size: u16 = message.len() as u16;
let mut data = Vec::with_capacity(16 + 64 + 32 + message.len());
data.push(1); data.push(0); data.extend_from_slice(&signature_offset.to_le_bytes());
data.extend_from_slice(&u16::MAX.to_le_bytes()); data.extend_from_slice(&public_key_offset.to_le_bytes());
data.extend_from_slice(&u16::MAX.to_le_bytes());
data.extend_from_slice(&message_offset.to_le_bytes());
data.extend_from_slice(&message_size.to_le_bytes());
data.extend_from_slice(&u16::MAX.to_le_bytes());
data.extend_from_slice(signature);
data.extend_from_slice(pubkey);
data.extend_from_slice(message);
Instruction {
program_id: ed25519_program_id(),
accounts: vec![],
data,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discriminator_is_deterministic() {
let a = anchor_ix_discriminator("deposit");
let b = anchor_ix_discriminator("deposit");
assert_eq!(a, b);
let mut h = Sha256::new();
h.update(b"global:deposit");
assert_eq!(&a[..], &h.finalize()[..8]);
}
#[test]
fn deposit_has_signer_at_user_slot() {
let pid = Pubkey::new_from_array([1; 32]);
let inst = Pubkey::new_from_array([2; 32]);
let user = Pubkey::new_from_array([3; 32]);
let mint = Pubkey::new_from_array([4; 32]);
let ata = Pubkey::new_from_array([5; 32]);
let dep = deposit_ix(&pid, &inst, &user, &mint, &ata, 100).unwrap();
assert!(dep.accounts.iter().any(|a| a.is_signer && a.pubkey == user));
}
#[test]
fn spl_token_program_id_is_canonical() {
assert_eq!(
SPL_TOKEN_PROGRAM_ID.to_string(),
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);
}
#[test]
fn withdraw_voucher_accounts_match_program() {
let pid = Pubkey::new_from_array([1; 32]);
let instance = Pubkey::new_from_array([2; 32]);
let account = Pubkey::new_from_array([3; 32]);
let mint = Pubkey::new_from_array([4; 32]);
let user_token_account = Pubkey::new_from_array([5; 32]);
let payer = Pubkey::new_from_array([6; 32]);
let args = WithdrawVoucherArgs {
amount: 1,
nonce: 7,
deadline: 99,
signature: [0; 64],
};
let ix = withdraw_voucher_ix(
&pid,
&instance,
&account,
&mint,
&user_token_account,
&payer,
&args,
)
.expect("build withdraw_voucher ix");
let (user_balance, _) = derive_user_balance_pda(&instance, &account, &mint, &pid);
let (instance_vault, _) = derive_instance_vault(&instance, &mint, &pid);
let (vault_authority, _) = derive_vault_authority(&instance, &pid);
let (used_nonce, _) = derive_withdraw_nonce_pda(&instance, &account, args.nonce, &pid);
let (withdraw_epoch, _) = derive_withdraw_epoch_pda(&instance, &mint, &pid);
let expected: &[(&str, Pubkey, bool, bool)] = &[
("instance", instance, false, false),
("mint", mint, false, false),
("user_balance", user_balance, true, false),
("user_token_account", user_token_account, true, false),
("instance_vault", instance_vault, true, false),
("vault_authority", vault_authority, false, false),
("used_nonce", used_nonce, true, false),
("withdraw_epoch", withdraw_epoch, true, false),
("account", account, false, false),
("payer", payer, true, true),
("instructions", sysvar_instructions_id(), false, false),
("token_program", SPL_TOKEN_PROGRAM_ID, false, false),
("system_program", SYSTEM_PROGRAM_ID, false, false),
];
assert_eq!(
ix.accounts.len(),
expected.len(),
"withdraw_voucher account COUNT drifted from the program's \
WithdrawVoucher struct (expected {}, got {})",
expected.len(),
ix.accounts.len()
);
for (i, (name, pubkey, writable, signer)) in expected.iter().enumerate() {
let got = &ix.accounts[i];
assert_eq!(got.pubkey, *pubkey, "account {i} should be `{name}`");
assert_eq!(got.is_writable, *writable, "`{name}` writable flag");
assert_eq!(got.is_signer, *signer, "`{name}` signer flag");
}
}
#[test]
fn set_withdraw_epoch_cap_accounts_match_program() {
let pid = Pubkey::new_from_array([1; 32]);
let instance = Pubkey::new_from_array([2; 32]);
let mint = Pubkey::new_from_array([3; 32]);
let operator_admin = Pubkey::new_from_array([4; 32]);
let ix = set_withdraw_epoch_cap_ix(&pid, &instance, &mint, &operator_admin, 1_000)
.expect("build set_withdraw_epoch_cap ix");
let (withdraw_epoch, _) = derive_withdraw_epoch_pda(&instance, &mint, &pid);
let expected: &[(&str, Pubkey, bool, bool)] = &[
("instance", instance, false, false),
("mint", mint, false, false),
("withdraw_epoch", withdraw_epoch, true, false),
("operator_admin", operator_admin, true, true),
("system_program", SYSTEM_PROGRAM_ID, false, false),
];
assert_eq!(
ix.accounts.len(),
expected.len(),
"set_withdraw_epoch_cap account COUNT drifted from the program's \
SetWithdrawEpochCap struct (expected {}, got {})",
expected.len(),
ix.accounts.len()
);
for (i, (name, pubkey, writable, signer)) in expected.iter().enumerate() {
let got = &ix.accounts[i];
assert_eq!(got.pubkey, *pubkey, "account {i} should be `{name}`");
assert_eq!(got.is_writable, *writable, "`{name}` writable flag");
assert_eq!(got.is_signer, *signer, "`{name}` signer flag");
}
assert_eq!(
&ix.data[..8],
&[251, 61, 122, 122, 154, 228, 208, 222],
"discriminator drifted from the IDL's set_withdraw_epoch_cap"
);
assert_eq!(&ix.data[8..], &1_000u64.to_le_bytes());
assert_eq!(ix.program_id, pid);
}
#[test]
fn set_withdraw_epoch_cap_encodes_zero_as_unlimited() {
let pid = Pubkey::new_from_array([1; 32]);
let ix = set_withdraw_epoch_cap_ix(
&pid,
&Pubkey::new_from_array([2; 32]),
&Pubkey::new_from_array([3; 32]),
&Pubkey::new_from_array([4; 32]),
0,
)
.expect("build set_withdraw_epoch_cap ix with cap=0");
assert_eq!(&ix.data[8..], &0u64.to_le_bytes());
}
#[test]
fn well_known_program_ids_parse() {
assert_eq!(
sysvar_instructions_id().to_string(),
"Sysvar1nstructions1111111111111111111111111"
);
assert_eq!(
ed25519_program_id().to_string(),
"Ed25519SigVerify111111111111111111111111111"
);
assert_eq!(
ata_program_id().to_string(),
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
);
}
#[test]
fn ed25519_verify_ix_has_no_accounts_and_targets_precompile() {
let ix = ed25519_verify_ix(&[0; 32], &[0; 64], b"hi");
assert!(ix.accounts.is_empty());
assert_eq!(ix.program_id, ed25519_program_id());
assert_eq!(ix.data.len(), 16 + 64 + 32 + 2);
}
#[test]
fn create_idempotent_ata_ix_layout() {
let payer = Pubkey::new_from_array([1; 32]);
let owner = Pubkey::new_from_array([2; 32]);
let mint = Pubkey::new_from_array([3; 32]);
let ata = Pubkey::new_from_array([4; 32]);
let ix = create_idempotent_ata_ix(&payer, &owner, &mint, &ata);
assert_eq!(ix.program_id, ata_program_id());
assert_eq!(ix.data, vec![1], "CreateIdempotent discriminant");
let a = &ix.accounts;
assert_eq!(a.len(), 6);
assert_eq!(a[0].pubkey, payer);
assert!(
a[0].is_signer && a[0].is_writable,
"payer signs + funds rent"
);
assert_eq!(a[1].pubkey, ata);
assert!(
a[1].is_writable && !a[1].is_signer,
"ata is created (writable)"
);
assert_eq!(a[2].pubkey, owner);
assert_eq!(a[3].pubkey, mint);
assert_eq!(a[4].pubkey, SYSTEM_PROGRAM_ID);
assert_eq!(a[5].pubkey, SPL_TOKEN_PROGRAM_ID);
assert!(
a[2..].iter().all(|m| !m.is_signer && !m.is_writable),
"owner/mint/programs are readonly"
);
}
#[test]
fn system_transfer_ix_matches_wire_format() {
let from = Pubkey::new_from_array([1; 32]);
let to = Pubkey::new_from_array([2; 32]);
let ix = system_transfer_ix(&from, &to, 1_234_567);
assert_eq!(ix.program_id, SYSTEM_PROGRAM_ID);
let mut expected = 2u32.to_le_bytes().to_vec();
expected.extend_from_slice(&1_234_567u64.to_le_bytes());
assert_eq!(ix.data, expected);
assert_eq!(ix.accounts.len(), 2);
assert!(ix.accounts[0].is_signer && ix.accounts[0].is_writable);
assert!(!ix.accounts[1].is_signer && ix.accounts[1].is_writable);
}
#[test]
fn wsol_wrap_unwrap_ixs_are_exact() {
let wsol = Pubkey::from_str(WSOL_MINT).expect("WSOL mint parses");
assert!(is_wsol_mint(WSOL_MINT));
assert!(!is_wsol_mint("So11111111111111111111111111111111111111111"));
let ata = Pubkey::new_from_array([3; 32]);
let dest = Pubkey::new_from_array([4; 32]);
let owner = Pubkey::new_from_array([5; 32]);
let sync = sync_native_ix(&ata);
assert_eq!(sync.program_id, SPL_TOKEN_PROGRAM_ID);
assert_eq!(sync.data, vec![17]);
assert_eq!(sync.accounts.len(), 1);
assert_eq!(sync.accounts[0].pubkey, ata);
assert!(sync.accounts[0].is_writable && !sync.accounts[0].is_signer);
let close = close_token_account_ix(&ata, &dest, &owner);
assert_eq!(close.program_id, SPL_TOKEN_PROGRAM_ID);
assert_eq!(close.data, vec![9]);
assert_eq!(close.accounts.len(), 3);
assert_eq!(close.accounts[0].pubkey, ata);
assert!(close.accounts[0].is_writable && !close.accounts[0].is_signer);
assert_eq!(close.accounts[1].pubkey, dest);
assert!(close.accounts[1].is_writable && !close.accounts[1].is_signer);
assert_eq!(close.accounts[2].pubkey, owner);
assert!(!close.accounts[2].is_writable && close.accounts[2].is_signer);
let _ = wsol; }
#[test]
fn pdas_are_stable() {
let program_id = Pubkey::new_from_array([9; 32]);
let (factory_a, _) = derive_factory_pda(&program_id);
let (factory_b, _) = derive_factory_pda(&program_id);
assert_eq!(factory_a, factory_b);
let (inst, _) = derive_instance_pda(&factory_a, 1, &program_id);
let user = Pubkey::new_from_array([7; 32]);
let mint = Pubkey::new_from_array([8; 32]);
let (bal, _) = derive_user_balance_pda(&inst, &user, &mint, &program_id);
assert_ne!(bal, inst);
}
}