//! clud — an unframeworked solana program.
//! the state only rises, the record only lengthens, the weather only comes at a record.
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
program::{invoke, invoke_signed},
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
entrypoint!(process_instruction);
const STATE_SEED: &[u8] = b"clud";
const STAKE_SEED: &[u8] = b"stake";
const POOL_SEED: &[u8] = b"pool";
const SLOT_HASHES_ID: Pubkey = solana_program::pubkey!("SysvarS1otHashes111111111111111111111111111");
const TOKEN_PROGRAM_ID: Pubkey = solana_program::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
/// The observer's cut of the vault on a rain, in basis points.
const OBSERVER_BPS: u64 = 200; // 2%
/// SlotHashes entry: u64 slot + 32-byte hash.
const SH_ENTRY: usize = 40;
/// SlotHashes header: u64 length.
const SH_HEADER: usize = 8;
const STATE_LEN: usize = 88;
const STAKE_LEN: usize = 34;
struct State<'a> { data: &'a mut [u8] }
impl<'a> State<'a> {
fn load(a: &'a mut [u8]) -> Result<Self, ProgramError> {
if a.len() != STATE_LEN || a[0] != 1 {
return Err(ProgramError::InvalidAccountData);
}
Ok(Self { data: a })
}
fn level(&self) -> u16 { u16::from_le_bytes(self.data[2..4].try_into().unwrap()) }
fn set_level(&mut self, v: u16) { self.data[2..4].copy_from_slice(&v.to_le_bytes()) }
fn rains(&self) -> u32 { u32::from_le_bytes(self.data[4..8].try_into().unwrap()) }
fn set_rains(&mut self, v: u32) { self.data[4..8].copy_from_slice(&v.to_le_bytes()) }
fn observations(&self) -> u64 { u64::from_le_bytes(self.data[8..16].try_into().unwrap()) }
fn set_observations(&mut self, v: u64) { self.data[8..16].copy_from_slice(&v.to_le_bytes()) }
fn last_slot(&self) -> u64 { u64::from_le_bytes(self.data[16..24].try_into().unwrap()) }
fn set_last_slot(&mut self, v: u64) { self.data[16..24].copy_from_slice(&v.to_le_bytes()) }
fn unseen(&self) -> u64 { u64::from_le_bytes(self.data[24..32].try_into().unwrap()) }
fn set_unseen(&mut self, v: u64) { self.data[24..32].copy_from_slice(&v.to_le_bytes()) }
fn total_staked(&self) -> u64 { u64::from_le_bytes(self.data[32..40].try_into().unwrap()) }
fn set_total_staked(&mut self, v: u64) { self.data[32..40].copy_from_slice(&v.to_le_bytes()) }
fn reward_index(&self) -> u128 { u128::from_le_bytes(self.data[40..56].try_into().unwrap()) }
fn set_reward_index(&mut self, v: u128) { self.data[40..56].copy_from_slice(&v.to_le_bytes()) }
}
struct StakePos<'a> { data: &'a mut [u8] }
impl<'a> StakePos<'a> {
fn load(a: &'a mut [u8]) -> Result<Self, ProgramError> {
if a.len() != STAKE_LEN || a[0] != 1 {
return Err(ProgramError::InvalidAccountData);
}
Ok(Self { data: a })
}
fn amount(&self) -> u64 { u64::from_le_bytes(self.data[2..10].try_into().unwrap()) }
fn set_amount(&mut self, v: u64) { self.data[2..10].copy_from_slice(&v.to_le_bytes()) }
fn reward_debt(&self) -> u128 { u128::from_le_bytes(self.data[10..26].try_into().unwrap()) }
fn set_reward_debt(&mut self, v: u128) { self.data[10..26].copy_from_slice(&v.to_le_bytes()) }
fn pending(&self) -> u64 { u64::from_le_bytes(self.data[26..34].try_into().unwrap()) }
fn set_pending(&mut self, v: u64) { self.data[26..34].copy_from_slice(&v.to_le_bytes()) }
}
fn process_instruction(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
match data.first() {
Some(0) => init(program_id, accounts),
Some(1) => observe(program_id, accounts),
Some(2) => stake(program_id, accounts, &data[1..]),
Some(3) => unstake(program_id, accounts, &data[1..]),
Some(4) => claim(program_id, accounts),
_ => Err(ProgramError::InvalidInstructionData),
}
}
/// Callable once. Creates the state account and records the mint.
fn init(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let payer = &accounts[0];
let state_ai = &accounts[1];
let mint = &accounts[2];
let system = &accounts[4];
if !payer.is_signer { return Err(ProgramError::MissingRequiredSignature); }
let (state_key, bump) = Pubkey::find_program_address(&[STATE_SEED], program_id);
if state_key != *state_ai.key { return Err(ProgramError::InvalidSeeds); }
if !state_ai.data_is_empty() { return Err(ProgramError::AccountAlreadyInitialized); }
let rent = Rent::get()?;
invoke_signed(
&system_instruction::create_account(
payer.key, state_ai.key, rent.minimum_balance(STATE_LEN),
STATE_LEN as u64, program_id,
),
&[payer.clone(), state_ai.clone(), system.clone()],
&[&[STATE_SEED, &[bump]]],
)?;
let mut d = state_ai.try_borrow_mut_data()?;
d[0] = 1;
d[1] = bump;
d[56..88].copy_from_slice(mint.key.as_ref());
Ok(())
}
/// Permissionless. Counts the holes in the current view and rains at a record.
fn observe(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let state_ai = &accounts[0];
let sh = &accounts[1];
let caller = &accounts[2];
if state_ai.owner != program_id { return Err(ProgramError::IllegalOwner); }
if *sh.key != SLOT_HASHES_ID { return Err(ProgramError::InvalidArgument); }
if !caller.is_signer { return Err(ProgramError::MissingRequiredSignature); }
// The sysvar is a u64 length then (u64 slot, [u8;32]) entries, newest first.
// Three fixed offsets. The entries are never walked.
let shd = sh.try_borrow_data()?;
if shd.len() < SH_HEADER + SH_ENTRY { return Err(ProgramError::InvalidAccountData); }
let len = u64::from_le_bytes(shd[0..8].try_into().unwrap()) as usize;
if len < 2 || shd.len() < SH_HEADER + len * SH_ENTRY {
return Err(ProgramError::InvalidAccountData);
}
let newest = u64::from_le_bytes(shd[SH_HEADER..SH_HEADER + 8].try_into().unwrap());
let last_off = SH_HEADER + (len - 1) * SH_ENTRY;
let oldest = u64::from_le_bytes(shd[last_off..last_off + 8].try_into().unwrap());
drop(shd);
// Slots spanned minus entries present is the count of blocks never produced.
let span = newest.checked_sub(oldest).and_then(|d| d.checked_add(1))
.ok_or(ProgramError::ArithmeticOverflow)?;
let gaps = span.saturating_sub(len as u64);
let gaps_u16 = u16::try_from(gaps.min(u16::MAX as u64)).unwrap();
let mut sd = state_ai.try_borrow_mut_data()?;
let mut st = State::load(&mut sd)?;
// Weather that happened while nobody was looking. Recorded, never estimated.
let last = st.last_slot();
if last != 0 && last + 1 < oldest {
st.set_unseen(st.unseen().saturating_add(oldest - last - 1));
}
if gaps_u16 > st.level() {
let rent = Rent::get()?;
let floor = rent.minimum_balance(STATE_LEN);
let vapor = state_ai.lamports().saturating_sub(floor);
if vapor > 0 {
let to_caller = vapor * OBSERVER_BPS / 10_000;
let to_pool = vapor - to_caller;
**state_ai.try_borrow_mut_lamports()? -= to_caller;
**caller.try_borrow_mut_lamports()? += to_caller;
let staked = st.total_staked();
if staked > 0 {
let delta = ((to_pool as u128) << 64) / staked as u128;
st.set_reward_index(st.reward_index().wrapping_add(delta));
}
// If nothing is staked the pool share stays in the vault and joins
// the next rain. Nothing is stranded and nothing is kept.
}
st.set_level(gaps_u16);
st.set_rains(st.rains() + 1);
}
st.set_last_slot(newest);
st.set_observations(st.observations() + 1);
Ok(())
}
fn settle(st: &State, pos: &mut StakePos) {
let owed = ((st.reward_index().wrapping_sub(pos.reward_debt()))
.saturating_mul(pos.amount() as u128)) >> 64;
pos.set_pending(pos.pending().saturating_add(owed as u64));
pos.set_reward_debt(st.reward_index());
}
fn token_transfer<'a>(
from: &AccountInfo<'a>, to: &AccountInfo<'a>, auth: &AccountInfo<'a>,
token_prog: &AccountInfo<'a>, amount: u64, signer_seeds: Option<&[&[u8]]>,
) -> ProgramResult {
let mut data = Vec::with_capacity(9);
data.push(3u8);
data.extend_from_slice(&amount.to_le_bytes());
let ix = Instruction {
program_id: TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*from.key, false),
AccountMeta::new(*to.key, false),
AccountMeta::new_readonly(*auth.key, signer_seeds.is_none()),
],
data,
};
let infos = [from.clone(), to.clone(), auth.clone(), token_prog.clone()];
match signer_seeds {
Some(seeds) => invoke_signed(&ix, &infos, &[seeds]),
None => invoke(&ix, &infos),
}
}
fn stake_pda(program_id: &Pubkey, user: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[STAKE_SEED, user.as_ref()], program_id)
}
/// Coin in. No lockup begins, because there is none.
fn stake(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let amount = u64::from_le_bytes(
data.get(0..8).ok_or(ProgramError::InvalidInstructionData)?.try_into().unwrap());
if amount == 0 { return Err(ProgramError::InvalidInstructionData); }
let state_ai = &accounts[0];
let user = &accounts[1];
let pos_ai = &accounts[2];
let user_tok = &accounts[3];
let pool_tok = &accounts[4];
let token_prog = &accounts[5];
let system = &accounts[6];
if state_ai.owner != program_id { return Err(ProgramError::IllegalOwner); }
if !user.is_signer { return Err(ProgramError::MissingRequiredSignature); }
let (pos_key, pos_bump) = stake_pda(program_id, user.key);
if pos_key != *pos_ai.key { return Err(ProgramError::InvalidSeeds); }
let (pool_key, _) = Pubkey::find_program_address(&[POOL_SEED], program_id);
if pool_key != *pool_tok.key { return Err(ProgramError::InvalidSeeds); }
if pos_ai.data_is_empty() {
let rent = Rent::get()?;
invoke_signed(
&system_instruction::create_account(
user.key, pos_ai.key, rent.minimum_balance(STAKE_LEN),
STAKE_LEN as u64, program_id,
),
&[user.clone(), pos_ai.clone(), system.clone()],
&[&[STAKE_SEED, user.key.as_ref(), &[pos_bump]]],
)?;
let mut pd = pos_ai.try_borrow_mut_data()?;
pd[0] = 1;
pd[1] = pos_bump;
}
token_transfer(user_tok, pool_tok, user, token_prog, amount, None)?;
let mut sd = state_ai.try_borrow_mut_data()?;
let mut st = State::load(&mut sd)?;
let mut pd = pos_ai.try_borrow_mut_data()?;
let mut pos = StakePos::load(&mut pd)?;
settle(&st, &mut pos);
pos.set_amount(pos.amount().checked_add(amount).ok_or(ProgramError::ArithmeticOverflow)?);
st.set_total_staked(st.total_staked().checked_add(amount).ok_or(ProgramError::ArithmeticOverflow)?);
Ok(())
}
/// Coin out. No queue, no penalty, no waiting.
fn unstake(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let amount = u64::from_le_bytes(
data.get(0..8).ok_or(ProgramError::InvalidInstructionData)?.try_into().unwrap());
let state_ai = &accounts[0];
let user = &accounts[1];
let pos_ai = &accounts[2];
let user_tok = &accounts[3];
let pool_tok = &accounts[4];
let token_prog = &accounts[5];
if state_ai.owner != program_id || pos_ai.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
if !user.is_signer { return Err(ProgramError::MissingRequiredSignature); }
let (pos_key, _) = stake_pda(program_id, user.key);
if pos_key != *pos_ai.key { return Err(ProgramError::InvalidSeeds); }
let mut sd = state_ai.try_borrow_mut_data()?;
let mut st = State::load(&mut sd)?;
let mut pd = pos_ai.try_borrow_mut_data()?;
let mut pos = StakePos::load(&mut pd)?;
settle(&st, &mut pos);
if amount > pos.amount() { return Err(ProgramError::InsufficientFunds); }
pos.set_amount(pos.amount() - amount);
st.set_total_staked(st.total_staked() - amount);
drop(pd);
drop(sd);
let bump = { let sd = state_ai.try_borrow_data()?; sd[1] };
token_transfer(pool_tok, user_tok, state_ai, token_prog, amount,
Some(&[STATE_SEED, &[bump]]))
}
/// Accrued lamports, pro rata, constant time regardless of dry observations sat through.
fn claim(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let state_ai = &accounts[0];
let user = &accounts[1];
let pos_ai = &accounts[2];
if state_ai.owner != program_id || pos_ai.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
if !user.is_signer { return Err(ProgramError::MissingRequiredSignature); }
let (pos_key, _) = stake_pda(program_id, user.key);
if pos_key != *pos_ai.key { return Err(ProgramError::InvalidSeeds); }
let mut sd = state_ai.try_borrow_mut_data()?;
let st = State::load(&mut sd)?;
let mut pd = pos_ai.try_borrow_mut_data()?;
let mut pos = StakePos::load(&mut pd)?;
settle(&st, &mut pos);
let owed = pos.pending();
if owed == 0 { return Ok(()); }
pos.set_pending(0);
drop(pd);
drop(sd);
**state_ai.try_borrow_mut_lamports()? -= owed;
**user.try_borrow_mut_lamports()? += owed;
Ok(())
}