// in state.rspub struct State {// The last time we chose winners, used to compute how soon we can choose againpub prev_selection_time: i64,// the number of active bids in the system up to MAX_BIDDERSpub n_bidders: u16,// the sum of all the current bidspub total_bid_amount : u64,// for each bid we track the key, amount and timepub bid_keys : [Pubkey; MAX_BIDDERS],pub bid_amounts: [u64; MAX_BIDDERS],pub bid_times: [i64; MAX_BIDDERS],// the number of winners to be chosen, up to MAX_WINNERS, and their keyspub n_winners : u8,pub winners: [Pubkey; MAX_WINNERS],// summary of the charity stats for the auctionpub charity_data : CharityData}
state.rs
at no point do we ever try and access the whole state on chain, it is there primarily as a reference. The final entry charity_data
is the same struct that we used in our charity token launch post to store the summary statistics of the donations made and the breakdown per charity.enum
, which includes an entry for each item in State
. For the vectors the entries take an additional index
argument, which is used to specify which element of the vector we want to access:// in state.rs// we can't unpack the whole state in one go due to stack limits on chain.// we create an enum to make it easier to access elements within the statepub enum StateEnum {PrevSelectionTime,NBidders,TotalBidAmount,BidKeys{index: usize},BidAmounts{index: usize},BidTimes{index: usize},NWinners,Winners{index: usize},CharityData}
enum
we then define the following function, get_state_index
, which takes an entry from StateEnum
and returns a tuple that provides the start and end bytes for that element of the state.// in state.rspub fn get_state_index(element: StateEnum) -> (usize, usize) {match element {// the unix timestamp that winners were last selected, 8 bytesStateEnum::PrevSelectionTime => {(0, 8)}// the number of bidders, 2 bytesStateEnum::NBidders => {(8, 10)}// the total amount bid currently in the ladder, 8 bytesStateEnum::TotalBidAmount => {(10, 18)},// the list of bidder pubkeys, each is 32 bytesStateEnum::BidKeys{index} => {(18 + index * 32, 18 + (index + 1) * 32)},// the list of corresponding bid amounts, each is 8 bytesStateEnum::BidAmounts{index} => {(32786 + index * 8, 32786 + (index + 1) * 8)},// the list of corresponding bid amounts, each is 8 bytesStateEnum::BidTimes{index} => {(40978 + index * 8, 40978 + (index + 1) * 8)},// the number of winners selected, 1 byteStateEnum::NWinners => {(49170, 49171)},// pubkeys of the selected winners, each is 32 bytesStateEnum::Winners{index} => {(49171 + index * 32, 49171 + (index + 1) * 32)},// the Charity data is 80 bytesStateEnum::CharityData => {(49299, 49379)}}}
State
in the code we can simply ask for the byte range of that item by doing something like:let key_idx = get_state_index(StateEnum::BidKeys{index: 0});
key_idx.0
and key_idx.1
without having to repeatedly hard code the values. If we change the contents of State
we just update StateEnum
and get_state_index
and the rest of the code will work without change.pub struct BidValues {pub bid_amounts: [u64; BID_BLOCK],}pub struct BidTimes {pub bid_times: [i64; BID_BLOCK],}pub struct WinnersKeys {pub keys: [Pubkey; MAX_WINNERS],}
state.rs
we define BID_BLOCK
to be 64, so BidValues
and BidTimes
are each 512 bytes. Although this is well inside the 4kb limit, due to how these structures are accessed within the code trying to make them any larger led to the program violating the stack size limit.create_program_account
, create_token_account
and transfer_tokens
functions described in that post.create_with_seed
function to determine the address of a data account using a seed phrase and a base_key
. These are passed as arguments along with the public key of the owner (the program in this case):let program_data_account = Pubkey::create_with_seed(&daoplays,"data_account",&program,)?;
create_account_with_seed
function, along with the size of the account and the balance that needs to be transferred to ensure it is rent exempt:let data_size: usize = 49381;let space : u64 = data_size.try_into().unwrap();let lamports = rent::Rent::default().minimum_balance(data_size);let instruction = solana_sdk::system_instruction::create_account_with_seed(&wallet.pubkey(),&data_account,&wallet.pubkey(),"data_account",lamports,space,&program,);
process_place_bid
function, which handles new bids from the auction's participants, has the following overall flow:charity_data
in the program's State
State
with the new bidPubkey::find_program_address(&[&bidder_account_info.key.to_bytes()], &program_id);
BidderData
struct, defined below:// in state.rspub struct BidderData {pub index : u16}
State
(bid_keys
, bid_amounts
, and bid_times
) that are associated with this participant's most recent bid. Whenever a new bid is made we can check if the participant has an existing bid by just checking the public key in the program's bid_keys
array at position index
, rather than searching through all the keys in the array, which is extremely inefficient....// get the bid index from the bidders accountlet bidder_data = BidderData::try_from_slice(&bidder_data_account_info.data.borrow()[..])?;// when adding the bid to the program state we have three possibilities:// i) there is already a bid and we just accumulate// ii) there is no bid but there is an empty spot// iii) there is no bid and no empty spot, so we replace the oldest bid// start by checking if a bid existslet mut bidders_index = bidder_data.index as usize;// check the public key that is present in the data account at bid_indexlet key_idx = get_state_index(StateEnum::BidKeys{index: bidders_index});let key = Pubkey::try_from_slice(&program_data_account_info.data.borrow()[key_idx.0..key_idx.1])?;...
...// if the keys match then we accumulate the bid// otherwise it must be a new bidif key == *bidder_token_account_info.key {// get the old bidlet old_bid_idx = get_state_index(StateEnum::BidAmounts{index: bidders_index});let old_bid = u64::try_from_slice(&program_data_account_info.data.borrow()[old_bid_idx.0..old_bid_idx.1])?;new_bid += old_bid;}...
State
, or if there are no empty slots then we are going to need to replace the oldest bid.bid_times
array to find the first element that is zero, which indicates an empty slot. As we iterate through we also track oldest_bid_index
, which if we make it all the way through the loop without finding an empty slot will point to the entry in the data that contains the oldest bid. Note in the below we are making use of our BidTimes
helper struct, so that we can deserialize in chunks of BID_BLOCK
entries and still remain in the 4kb stack frame limit....// if they were a new bidder add their bid to the ladder, first just try and find the first open spotlet mut found_space = false;// if there isn't a space we will want to replace the oldest bid// so we find that in the same looplet mut oldest_bid_index : usize = 0;let mut oldest_time = i64::MAX;for i in 0..N_BID_BLOCKS {let time_idx = get_state_index(StateEnum::BidTimes {index: i * BID_BLOCK});let times = BidTimes::try_from_slice(&program_data_account_info.data.borrow()[time_idx.0..time_idx.0 + BID_BLOCK * 8])?;for j in 0..BID_BLOCK {let total_index = i * BID_BLOCK + j;// if the bid time is zero that indicates we have found an empty slot, so break out of the loopif times.bid_times[j] == 0 {bidders_index = total_index;found_space = true;break}// otherwise check if this is older than the oldest known bid so farif times.bid_times[j] < oldest_time {oldest_bid_index = total_index;oldest_time = times.bid_times[j];}}if found_space {break;}}...
oldest_bid_index
. We therefore first have to check what bid was present at that position and subtract its value from bid_total
which tracks the total quantity currently in the program's State
, and also reduce n_bidders
, which tracks the number of active bids in the State
, by one....// if there was no open spot we overwrite the oldest bidif !found_space {bidders_index = oldest_bid_index;// if we are overwriting we need to subtract bid_amount and reduce n_bidders by onelet existing_bid_idx = get_state_index(StateEnum::BidAmounts{index: bidders_index});let existing_bid = u64::try_from_slice(&program_data_account_info.data.borrow()[existing_bid_idx.0..existing_bid_idx.1])?;total_bid -= existing_bid;n_bidders -=1;}...
BidderData
:...// update their bid datalet new_bidder_data = BidderData {index: bidders_index as u16};new_bidder_data.serialize(&mut &mut bidder_data_account_info.data.borrow_mut()[..])?;...
total_bid
and n_bidders
data as well....msg!("update bid details for position {}", bidders_index);// insert the new bid, key and time into the program datalet new_bid_idx = get_state_index(StateEnum::BidAmounts{index: bidders_index});let new_key_idx = get_state_index(StateEnum::BidKeys{index: bidders_index});let new_time_idx = get_state_index(StateEnum::BidTimes{index: bidders_index});let bidder_token_pubkey = *bidder_token_account_info.key;// serialize the new datanew_bid.serialize(&mut &mut program_data_account_info.data.borrow_mut()[new_bid_idx.0..new_bid_idx.1])?;bidder_token_pubkey.serialize(&mut &mut program_data_account_info.data.borrow_mut()[new_key_idx.0..new_key_idx.1])?;current_time.serialize(&mut &mut program_data_account_info.data.borrow_mut()[new_time_idx.0..new_time_idx.1])?;// update total bidtotal_bid.serialize(&mut &mut program_data_account_info.data.borrow_mut()[total_bid_idx.0..total_bid_idx.1])?;// update n_biddersn_bidders += 1;n_bidders.serialize(&mut &mut program_data_account_info.data.borrow_mut()[n_bidders_idx.0..n_bidders_idx.1])?;
n_winner
random numbersState
with the new winners, and other relevant dataselect_winners
function, which in this example we have set to two seconds. We therefore define the following function, get_bid_state
, which takes as an argument max_time
, which is the latest time for a bid that we will consider in order to include it in this round of the auction. At this stage all we are interested in doing is finding both the total amount bid prior to that time, and the number of valid bids, so we simply iterate through the data, checking that the time of the bid is prior to max_time
and returning these quantities at the end:pub fn get_bid_state(max_time : i64, program_data_account_info : &AccountInfo) -> Result<(u16, u64), ProgramError> {// calculate the total bid amount and number of bidders at this timelet mut total_bid : u64 = 0;let mut n_bidders : u16 = 0;for idx in 0..N_BID_BLOCKS {let bid_idx = get_state_index(StateEnum::BidAmounts {index: idx*BID_BLOCK});let time_idx = get_state_index(StateEnum::BidTimes {index: idx*BID_BLOCK});let bids = BidValues::try_from_slice(&program_data_account_info.data.borrow()[bid_idx.0..bid_idx.0 + BID_BLOCK*8])?;let times = BidTimes::try_from_slice(&program_data_account_info.data.borrow()[time_idx.0..time_idx.0 + BID_BLOCK*8])?;for jdx in 0..BID_BLOCK {if times.bid_times[jdx] < max_time && bids.bid_amounts[jdx] > 0 {total_bid += bids.bid_amounts[jdx];n_bidders += 1;}}}Ok((n_bidders, total_bid))}
check_winners_state
, which starts by just checking whether there are any valid bids at all:pub fn check_winners_state<'a>(n_bidders : u16,program_data_account_info : &AccountInfo<'a>,program_token_account_info : &AccountInfo<'a>) -> Result<u8, ProgramError> {// if there are no bidders then we have no-one to chooseif n_bidders == 0 {msg!("no bidders to be able to select winners");return Ok(0);}...
amount
field of the token account:...// if there aren't enough tokens available then we can't choose winnerslet min_tokens: u64 = TOKENS_WON;let program_token_account = spl_token::state::Account::unpack_unchecked(&program_token_account_info.try_borrow_data()?)?;let token_balance = program_token_account.amount;if token_balance < min_tokens {msg!("insufficient tokens in program account to select new winners: {} < {}", token_balance, min_tokens);return Ok(0);}...
n_winners
for this round:...let max_token_blocks = token_balance / TOKENS_WON;// set the number of winners to the max and check if we should decrease from therelet mut n_winners = MAX_WINNERS as u8;// check if we have enough token blocks for this manyif n_winners as u64 > max_token_blocks {n_winners = max_token_blocks as u8;}// finally check if we have enough bidders for thislet max_winners_from_bidders = n_bidders / 64 + 1;if n_winners as u16 > max_winners_from_bidders {n_winners = max_winners_from_bidders as u8;}...
select_winners
, but already by the time there are ten bidders, new winners can be selected every 30 seconds. We also remove this limitation entirely once the expected time is below a few seconds, so that if more than one hundred bidders are in the pool new winners can be drawn immediately one after the other:...let prev_time_idx = get_state_index(StateEnum::PrevSelectionTime);let prev_time_selected = i64::try_from_slice(&program_data_account_info.data.borrow()[prev_time_idx.0..prev_time_idx.1])?;let clock = Clock::get()?;let current_time = clock.unix_timestamp;let time_passed = (current_time - prev_time_selected) as f64;// on average we expect a single bidder to wait 5 minutes before being selected// we therefore calculate time_per_bidder based on the number of bidders, and number of winners being selected// if this is below 10 seconds we just allow new winners to be selected so that there is less friction with large// numbers of bidderslet time_per_bidder = (5.0 * 60.0) / ((n_bidders as f64) / (n_winners as f64));msg!("time_per_bidder {} time_passed: {} n_bidders {} token_balance {} max_blocks {}", time_per_bidder, time_passed, n_bidders, token_balance, max_token_blocks);if time_per_bidder > 3.0 && time_passed < time_per_bidder {return Ok(0);}msg!("Selecting {} new winners! ({} {})", n_winners, max_token_blocks, max_winners_from_bidders);Ok(n_winners)}
n_winner
values we need. As a final step we then sort the vector of random numbers in order to increase the efficiency with which we can then search through the bid data and actually find the winning keys:...// generate the seed for selecting winnerslet mut pyth_random = randoms::generate_seed(btc_account_info,eth_account_info,sol_account_info);let mut ran_vec : Vec<f64> = Vec::new();for _winner in 0..n_winners {pyth_random = randoms::shift_seed(pyth_random);let random_f64 = randoms::generate_random(pyth_random);ran_vec.push(random_f64);}ran_vec.sort_by(|a, b| a.partial_cmp(b).unwrap());...
valid_total_bid
has been initialized to the value returned by get_bid_state
, and so represents the sum of the bids that occurred more than two seconds prior to the time we entered this function. As before when iterating through all the bids, they will be split up into N_BID_BLOCK
chunks each of size BID_BLOCK
.valid_total_bid
by these numbers we get the point in the cumulative distribution at which a winner will be chosen, which we define as the threshold
in the code snippet below. As we iterate through the bids we will be tracking the cumulative bid amount, and comparing this to the threshold
. The bid that causes the cumulative total to exceed the threshold will then be chosen as the next winner. As these random values were sorted into numerical order we can just iterate through N_BID_BLOCK
in the outer loop, and only deserialize each block once in order to increase the computational efficiency of selecting winners....// initialize the starting statelet mut cumulative_total : u64 = 0;let mut winners_found : [bool; MAX_WINNERS] = [false; MAX_WINNERS];for idx in 0..N_BID_BLOCKS {let bid_idx = get_state_index(StateEnum::BidAmounts {index: idx*BID_BLOCK});let mut bids = BidValues::try_from_slice(&program_data_account_info.data.borrow()[bid_idx.0..bid_idx.0 + BID_BLOCK*8])?;let time_idx = get_state_index(StateEnum::BidTimes {index: idx*BID_BLOCK});let times = BidTimes::try_from_slice(&program_data_account_info.data.borrow()[time_idx.0..time_idx.0 + BID_BLOCK*8])?;for current_winner in 0..n_winners {if winners_found[current_winner as usize] {continue;}// update the thresholdlet random_f64 = ran_vec[current_winner as usize];let threshold = ((valid_total_bid as f64) * random_f64) as u64;...
...let mut sub_total : u64 = cumulative_total;for bid_index in 0..BID_BLOCK {// check if this is within the time thresholdif times.bid_times[bid_index] >= threshold_time {continue;}let current_bid = bids.bid_amounts[bid_index];sub_total += current_bid;if sub_total > threshold {winners_found[current_winner as usize] = true;...
winners
array of the program's State
, and then clear their entries in the bid_amounts
, bid_times
, and bid_keys
arrays:...let winner_index = idx * BID_BLOCK + bid_index;// get the winners key from the program data accountlet key_idx = get_state_index(StateEnum::BidKeys{index: winner_index});let winners_key = Pubkey::try_from_slice(&program_data_account_info.data.borrow()[key_idx.0..key_idx.1])?;// and insert it into the winners arraylet winner_idx = get_state_index(StateEnum::Winners{index: current_winner as usize});winners_key.serialize(&mut &mut program_data_account_info.data.borrow_mut()[winner_idx.0..winner_idx.1])?;// now clear the winners data in the program data account// start by zero'ing their bidlet win_bid_idx = get_state_index(StateEnum::BidAmounts {index: winner_index});0u64.serialize(&mut &mut program_data_account_info.data.borrow_mut()[win_bid_idx.0..win_bid_idx.1])?;// then the bid timelet win_time_idx = get_state_index(StateEnum::BidTimes{index: winner_index});0i64.serialize(&mut &mut program_data_account_info.data.borrow_mut()[win_time_idx.0..win_time_idx.1])?;// and then clear their keysolana_program::system_program::id().serialize(&mut &mut program_data_account_info.data.borrow_mut()[key_idx.0..key_idx.1])?;...
bid_amounts
array, so that it won't get included as we continue to loop through for the next winner. If we finish the loop through the current block without having found a winner we update the cumulative total bid with the sum from this block, and move on to the next....// finally decrement the number of bidders, and the total bid amountvalid_n_bidders -= 1;valid_total_bid -= current_bid;n_bidders -= 1;total_bid -= current_bid;bids.bid_amounts[bid_index] = 0;break;}}// if this winner wasn't found in this block, move onto the next blockif winners_found[current_winner as usize] == false {cumulative_total = sub_total;break;}}}...
total_bid
and n_bidders
, and to set the last time winners were selected to the current time:...// update number of bidderslet n_bidders_idx = get_state_index(StateEnum::NBidders);n_bidders.serialize(&mut &mut program_data_account_info.data.borrow_mut()[n_bidders_idx.0..n_bidders_idx.1])?;// update total_bid_amountlet total_bid_idx = get_state_index(StateEnum::TotalBidAmount);total_bid.serialize(&mut &mut program_data_account_info.data.borrow_mut()[total_bid_idx.0..total_bid_idx.1])?;let prev_time_idx = get_state_index(StateEnum::PrevSelectionTime);current_time.serialize(&mut &mut program_data_account_info.data.borrow_mut()[prev_time_idx.0..prev_time_idx.1])?;
n_winners
value from the program data account. We do this rather than passing the value as an argument to the program to ensure that users can't pass erroneous values. As this function can be called at any time, at this stage we also simply check that this value is greater than zero. If there are no winners we can exit immediately.// now check how many winners we expect and make sure the keys match the program datalet n_winners_idx = get_state_index(StateEnum::NWinners);let n_winners = u8::try_from_slice(&program_data_account_info.data.borrow()[n_winners_idx.0..n_winners_idx.1])?;if n_winners == 0 {msg!("No winners selected, exiting send_tokens");return Ok(());}msg!("have {} winners to send tokens to", n_winners);
AccountInfo
objects, and push the winners onto that from the account iterator. We make use of the peekable
functionality of iterators in order to check whether the correct number of accounts have been passed to the function:...// get the winner's account infolet mut winners_account_info : Vec<&AccountInfo> = Vec::new();for _w_idx in 0..n_winners {if account_info_iter.peek().is_some() {winners_account_info.push(next_account_info(account_info_iter)?);}else {msg!("n_winners {} exceeds the number of accounts passed", n_winners);return Ok(());}}// check that was the last accountif account_info_iter.peek().is_some() {msg!("n_winners {} is less than the number of accounts passed", n_winners);return Ok(());}...
...let winners_key_idx = get_state_index(StateEnum::Winners { index: 0 });let expected_winners = WinnersKeys::try_from_slice(&program_data_account_info.data.borrow()[winners_key_idx.0..winners_key_idx.0 + 32 * MAX_WINNERS])?;// check the winners sent are what we expectfor w_idx in 0..(n_winners as usize) {msg!("winner {} : {}", w_idx, expected_winners.keys[w_idx].to_string());if expected_winners.keys[w_idx as usize] != *winners_account_info[w_idx].key {msg!("expected winner {} to have key {}", w_idx, winners_account_info[w_idx].key);return Err(ProgramError::InvalidAccountData);}// also check none of the winners are the system program which would indicate we have arrived here too earlyif *winners_account_info[w_idx].key == solana_program::system_program::id() {msg!("winner {} has system program key {}", w_idx, winners_account_info[w_idx].key);return Err(ProgramError::InvalidAccountData);}}...
n_winners
variable must not have been set to the right value. Although this shouldn't ever happen, we check anyway:...// finally check that the remaining entries in the winners data vec are the system program idfor w_idx in (n_winners as usize)..MAX_WINNERS {msg!("winner {} : {}", w_idx, expected_winners.keys[w_idx as usize].to_string());if expected_winners.keys[w_idx] != solana_program::system_program::id() {msg!("expected winner {} to have key {}", w_idx, solana_program::system_program::id());return Err(ProgramError::InvalidAccountData);}}...
transfer_tokens
function to send the winning amount to each of the accounts in turn. Here that amount is defined as the constant TOKENS_WON
, which in this example we have set to one hundred:// now we can transfer the tokensfor w_idx in 0..(n_winners as usize) {utils::transfer_tokens(TOKENS_WON,program_token_account_info,winners_account_info[w_idx],program_derived_account_info,token_program_account_info,bump_seed)?;}
n_winners
back to zero, so that the program can start selecting new winners again.// finally just reset the n_winners value to zero so we can select new winners again// and reset all the winners keys to their defaultfor current_winner in 0..MAX_WINNERS {let winner_idx = get_state_index(StateEnum::Winners{index: current_winner});solana_program::system_program::id().serialize(&mut &mut program_data_account_info.data.borrow_mut()[winner_idx.0..winner_idx.1])?;}0u8.serialize(&mut &mut program_data_account_info.data.borrow_mut()[n_winners_idx.0..n_winners_idx.1])?;
Overview
Account Info