human_escrow/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(clippy::all)]
3#![deny(clippy::integer_arithmetic)]
4#![cfg(not(feature = "no-entrypoint"))]
5
6mod error;
7
8use std::mem::{self};
9
10use borsh::{BorshDeserialize, BorshSerialize};
11use error::Error;
12use human_common::utils::{
13    next_atoken_wallet, next_expected_account, next_expected_token_wallet, next_signer_account,
14};
15use solana_program::{
16    account_info::{next_account_info, AccountInfo},
17    clock::{Clock, UnixTimestamp},
18    entrypoint,
19    entrypoint::ProgramResult,
20    msg,
21    program::{invoke, invoke_signed},
22    program_error::ProgramError,
23    program_memory::{sol_memcpy, sol_memset},
24    program_option::COption,
25    program_pack::Pack,
26    pubkey::Pubkey,
27    rent::Rent,
28    system_instruction, system_program,
29    sysvar::Sysvar,
30};
31use spl_associated_token_account::get_associated_token_address;
32use spl_token::instruction as token_inst;
33use spl_token::state as token_state;
34
35#[derive(Debug, BorshDeserialize, BorshSerialize)]
36enum Request {
37    /// Request with fixed amount
38    Funded(FundedRequest),
39    /// Request with variable amount
40    Unfunded(UnfundedRequest),
41}
42
43#[derive(Debug, BorshDeserialize, BorshSerialize)]
44struct FundedRequest {
45    // system account
46    author: Pubkey,
47}
48
49#[derive(Debug, BorshDeserialize, BorshSerialize)]
50struct UnfundedRequest {
51    /// amount of token accumulated. needed in case of refund
52    collected: u64,
53    deadline: Option<UnixTimestamp>,
54    accept_threshold: u64,
55}
56
57#[repr(u8)]
58#[derive(Debug, BorshDeserialize, BorshSerialize, PartialEq)]
59enum RequestStatus {
60    Unitialized,
61    Open,
62    Declined,
63    Accepted,
64}
65
66#[derive(Debug, BorshDeserialize, BorshSerialize)]
67struct State {
68    request_status: RequestStatus,
69    /// the moment request was created, so later we could add logic to close stale requests
70    created_at: UnixTimestamp,
71    /// temporary token wallet
72    wallet: Pubkey,
73    /// system account. if accepted, where tokens would go
74    destination: Pubkey,
75    /// account to refund SOL from closing state
76    payer: Pubkey,
77    /// request body
78    request: Request,
79}
80
81#[derive(Debug, BorshDeserialize, BorshSerialize, PartialEq)]
82struct Voucher {
83    /// state address will be used for reverse RPC lookup (getProgramAccounts)
84    state: Pubkey,
85    /// user (again, for reverse lookup)
86    user: Pubkey,
87    /// amount contributed by user
88    amount: u64,
89}
90
91impl State {
92    /// returns whether unfunded request if expired. funded requests cannot expire
93    fn expired(&self, now: UnixTimestamp) -> bool {
94        if !self.is_open() {
95            return false;
96        }
97
98        if let Request::Unfunded(UnfundedRequest {
99            deadline: Some(deadline),
100            collected,
101            accept_threshold,
102        }) = self.request
103        {
104            return now > deadline && collected < accept_threshold;
105        }
106
107        false
108    }
109
110    fn try_accept(&mut self) -> Result<(), ProgramError> {
111        if self.request_status != RequestStatus::Open {
112            return Err(ProgramError::Custom(0x16));
113        }
114
115        if let Request::Unfunded(request) = &self.request {
116            if request.collected < request.accept_threshold {
117                msg!("not enough collected to accept");
118                return Err(ProgramError::Custom(0x16));
119            }
120        }
121
122        self.request_status = RequestStatus::Accepted;
123
124        Ok(())
125    }
126
127    fn is_funded(&self) -> bool {
128        matches!(self.request, Request::Funded(_))
129    }
130
131    fn is_open(&self) -> bool {
132        self.request_status == RequestStatus::Open
133    }
134}
135
136#[derive(Debug, BorshDeserialize, BorshSerialize)]
137#[repr(u8)]
138#[non_exhaustive]
139enum Instruction {
140    Create(CreateInstruction),
141
142    // only for unfunded
143    // transfers all delegated balance as amount
144    // emits created token to atoken wallet where close_authority == our derived authority
145    // **discuss**: we can't automatically refund you (and receive our SOL's back) unless we are the owner of account
146    //      possible options:
147    //          require handing ownership (which kinda defeats all purpose of token)
148    //          creating another scheme for atoken wallets or new kind of account entirely
149    //          wait for all users to sign refund option (not happening)
150    Contribute(ContributeInstruction),
151
152    // refund contribution for contribution token. burn token and close account
153    // can also be used to close accounts of accepted requests
154    // if all collected == 0: erase state.
155    Refund,
156
157    // only for funded requests. made by author
158    // all funds go back to author. state erased
159    Cancel,
160
161    // for creator: receive all wallet funds.
162    // if funded: erase state
163    Accept,
164
165    // for creator: refund all tokens
166    // if funded: erase state
167    Decline,
168}
169
170#[derive(Debug, BorshDeserialize, BorshSerialize)]
171struct ContributeInstruction {
172    user: Pubkey,
173}
174
175#[derive(Debug, BorshDeserialize, BorshSerialize)]
176struct CreateInstruction {
177    dest: Pubkey,
178    payer: Pubkey,
179    rtype: CreateInstructionRequest,
180}
181
182#[derive(Debug, BorshDeserialize, BorshSerialize)]
183enum CreateInstructionRequest {
184    Funded {
185        author: Pubkey,
186    },
187    Unfunded {
188        deadline: Option<UnixTimestamp>,
189        accept_threshold: u64,
190    },
191}
192
193entrypoint!(process_instruction);
194pub fn process_instruction(
195    program_id: &Pubkey,
196    accounts: &[AccountInfo],
197    instruction_data: &[u8],
198) -> ProgramResult {
199    let instruction = Instruction::try_from_slice(instruction_data).map_err(|e| {
200        msg!("error parsing instruction: {}", e);
201        ProgramError::InvalidInstructionData
202    })?;
203
204    match instruction {
205        Instruction::Create(inst) => {
206            msg!("creating funded request");
207            process_create(program_id, accounts, inst)
208        }
209        Instruction::Contribute(ContributeInstruction { user }) => {
210            msg!("contributing to unfunded request");
211            process_contribute(program_id, accounts, &user)
212        }
213        Instruction::Refund => {
214            msg!("refunding request");
215            process_refund(program_id, accounts)
216        }
217
218        Instruction::Accept => {
219            msg!("accepting request");
220            process_accept(program_id, accounts)
221        }
222        Instruction::Decline => {
223            msg!("rejecting redeem request");
224            process_decline(program_id, accounts)
225        }
226        Instruction::Cancel => {
227            msg!("cancelling funded redeem request");
228            process_cancel(program_id, accounts)
229        }
230    }
231}
232
233pub const V1: &[u8] = b"HMN_R1";
234pub const AUTHORITY_SEED: &[u8] = b"A";
235
236#[macro_export]
237macro_rules! find_keyed_address {
238    ($program_id:expr, $seed:expr) => {
239        $crate::find_keyed_address!($program_id, $seed, &[])
240    };
241    ($program_id:expr, $seed:expr, $token:expr) => {{
242        let _: (&Pubkey, &[u8]) = ($program_id, $token);
243
244        let seeds = &[V1, $seed, ($token)];
245        let (addr, bump) = Pubkey::find_program_address(seeds, $program_id);
246
247        (addr, &[V1, $seed, $token, &[bump]])
248    }};
249}
250
251// mix state_addr into the mix, so same token can't be reused in different requests (generally a bad thing)
252#[macro_export]
253macro_rules! authority {
254    ($program_id:expr, $state_addr:expr) => {
255        $crate::find_keyed_address!($program_id, AUTHORITY_SEED, $state_addr.as_ref())
256    };
257}
258
259const STATE_SIZE: usize = 138;
260
261// [writable] new state account owned by this program
262// [writable] new wallet with owner and close authority set to derived authority with tokens already on it
263fn process_create(
264    program_id: &Pubkey,
265    accounts: &[AccountInfo],
266    args: CreateInstruction,
267) -> ProgramResult {
268    let account_info_iter = &mut accounts.iter();
269
270    let state_acc = next_account_info(account_info_iter)?;
271    let (wallet_addr, wallet) =
272        next_owned_token_wallet(account_info_iter, program_id, state_acc.key)?;
273
274    let rent = Rent::get()?;
275    let clock = Clock::get()?;
276
277    let mut state_data = state_acc.try_borrow_mut_data()?;
278
279    if !rent.is_exempt(state_acc.lamports(), STATE_SIZE) {
280        return Err(ProgramError::AccountNotRentExempt);
281    }
282
283    // sanity check
284    match args.rtype {
285        CreateInstructionRequest::Funded { .. } if wallet.amount == 0 => {
286            // should contain some funds (can't create empty request)
287            return Err(ProgramError::InsufficientFunds);
288        }
289        CreateInstructionRequest::Unfunded { .. } if wallet.amount != 0 => {
290            // should NOT contain any funds because we are unable to track them
291            return Err(ProgramError::InvalidAccountData);
292        }
293        _ => {}
294    }
295
296    let request = match args.rtype {
297        CreateInstructionRequest::Funded { author } => Request::Funded(FundedRequest { author }),
298        CreateInstructionRequest::Unfunded {
299            deadline,
300            accept_threshold,
301        } => Request::Unfunded(UnfundedRequest {
302            collected: 0,
303            deadline,
304            accept_threshold,
305        }),
306    };
307
308    let state = State {
309        request_status: RequestStatus::Open,
310        created_at: clock.unix_timestamp,
311        wallet: wallet_addr,
312        payer: args.payer,
313        destination: args.dest,
314        request,
315    };
316
317    write_state(&mut state_data, state)?;
318
319    Ok(())
320}
321
322fn write_state(data: &mut [u8], state: State) -> Result<(), ProgramError> {
323    if data.len() < STATE_SIZE {
324        return Err(ProgramError::AccountDataTooSmall);
325    }
326
327    // is this enough?
328    if matches!(State::try_from_slice(data), Ok(State {  request_status, .. }) if request_status != RequestStatus::Unitialized)
329    {
330        return Err(ProgramError::AccountAlreadyInitialized);
331    }
332
333    let state = state.try_to_vec()?;
334
335    copy_slice(data, &state);
336
337    Ok(())
338}
339
340pub fn next_owned_token_wallet<'a, 'b: 'a, I>(
341    i: &mut I,
342    program_id: &Pubkey,
343    state_addr: &Pubkey,
344) -> Result<(Pubkey, token_state::Account), ProgramError>
345where
346    I: Iterator<Item = &'a AccountInfo<'b>>,
347{
348    let wallet = next_account_info(i)?;
349
350    if !spl_token::check_id(wallet.owner) {
351        return Err(ProgramError::InvalidArgument);
352    }
353
354    let account = token_state::Account::unpack(&wallet.data.borrow())?;
355
356    let (derived_authority, _) = authority!(program_id, state_addr);
357    if account.owner != derived_authority {
358        return Err(ProgramError::IllegalOwner);
359    }
360
361    if account.close_authority.is_some() {
362        return Err(ProgramError::IllegalOwner);
363    }
364
365    Ok((*wallet.key, account))
366}
367
368fn next_state_account<'a, 'b, I: Iterator<Item = &'a AccountInfo<'b>>>(
369    i: &mut I,
370    program_id: &Pubkey,
371) -> Result<(State, &'a AccountInfo<'b>), ProgramError> {
372    let state_acc = next_account_info(i)?;
373
374    if state_acc.owner != program_id {
375        msg!(
376            "illegal state owner ({} != {})",
377            state_acc.owner,
378            program_id
379        );
380        return Err(ProgramError::IllegalOwner);
381    }
382
383    let data = state_acc.try_borrow_data()?;
384
385    let state = State::try_from_slice(&data)?;
386
387    if state.request_status == RequestStatus::Unitialized {
388        return Err(ProgramError::UninitializedAccount);
389    }
390
391    Ok((state, state_acc))
392}
393
394fn save_state(state: State, state_acc: &AccountInfo) -> Result<(), ProgramError> {
395    let serialized = state.try_to_vec()?;
396
397    let mut data = state_acc.try_borrow_mut_data()?;
398
399    copy_slice(&mut data[..], &serialized);
400
401    Ok(())
402}
403
404fn erase_state(
405    state: State,
406    state_acc: &AccountInfo,
407    payer_acc: &AccountInfo,
408) -> Result<(), ProgramError> {
409    // sanity check
410    assert_ne!(state.request_status, RequestStatus::Open);
411
412    if let Request::Unfunded(UnfundedRequest { collected, .. }) = state.request {
413        assert_eq!(collected, 0, "error: not all vouchers are refunded")
414    };
415
416    // withdraw all lamports from state
417    let lamports = state_acc.lamports();
418
419    **state_acc.try_borrow_mut_lamports()? = 0;
420    let mut payer_lamports = payer_acc.try_borrow_mut_lamports()?;
421    **payer_lamports = payer_lamports
422        .checked_add(lamports)
423        .ok_or(Error::Overflow)?;
424
425    let mut data = state_acc.try_borrow_mut_data()?;
426    let l = data.len();
427    sol_memset(&mut data, 0, l);
428
429    Ok(())
430}
431
432#[inline]
433fn copy_slice(dst: &mut [u8], src: &[u8]) {
434    sol_memcpy(dst, src, src.len());
435}
436
437// [writable] request state
438// [writable] request wallet
439// [writable] token wallet with delegated amount
440// [sign] delegate
441// [writable] voucher account
442// [] rent var
443// [] clock var
444fn process_contribute(
445    program_id: &Pubkey,
446    accounts: &[AccountInfo],
447    user: &Pubkey,
448) -> ProgramResult {
449    let account_info_iter = &mut accounts.iter();
450
451    let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
452    let _wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
453
454    let source_wallet = next_signer_account(account_info_iter, &state.destination)?;
455
456    if !spl_token::check_id(source_wallet.owner) {
457        return Err(ProgramError::IllegalOwner);
458    }
459
460    let source_wallet_data = token_state::Account::unpack(&source_wallet.try_borrow_data()?)?;
461
462    let amount = source_wallet_data.delegated_amount;
463    if amount == 0 {
464        return Err(ProgramError::InvalidArgument);
465    }
466
467    let delegate = source_wallet_data
468        .delegate
469        .ok_or(ProgramError::InvalidArgument)?;
470
471    next_signer_account(account_info_iter, &delegate)?;
472
473    let voucher_acc = next_account_info(account_info_iter)?;
474    let rent = Rent::from_account_info(next_account_info(account_info_iter)?)?;
475    let clock = Clock::from_account_info(next_account_info(account_info_iter)?)?;
476
477    if state.expired(clock.unix_timestamp) {
478        state.request_status = RequestStatus::Declined;
479        save_state(state, state_acc)?;
480        return Err(ProgramError::Custom(0x17));
481    }
482
483    if !state.is_open() {
484        return Err(ProgramError::Custom(0x12));
485    }
486
487    let mut unfunded_state = match state.request {
488        Request::Funded(_) => return Err(ProgramError::Custom(0x12)),
489        Request::Unfunded(ref mut s) => s,
490    };
491
492    // transfer user amount to
493    let inst = token_inst::transfer(
494        &spl_token::ID,
495        source_wallet.key,
496        &state.wallet,
497        &delegate,
498        &[],
499        amount,
500    )?;
501
502    invoke(&inst, accounts)?;
503
504    // issue voucher (or update amount on existing one)
505    let mut previous_amount = 0;
506
507    if let Some(v) = get_voucher(voucher_acc, program_id, &rent)? {
508        if v.user != *user {
509            msg!("refusing to override another user's voucher");
510            return Err(ProgramError::IllegalOwner);
511        }
512
513        previous_amount = v.amount
514    }
515
516    let voucher = Voucher {
517        state: *state_acc.key,
518        user: *user,
519        amount: previous_amount.checked_add(amount).ok_or(Error::Overflow)?,
520    }
521    .try_to_vec()?;
522
523    let mut data = voucher_acc.try_borrow_mut_data()?;
524    copy_slice(&mut data, &voucher);
525
526    // update total contributed counter
527    unfunded_state.collected = unfunded_state
528        .collected
529        .checked_add(amount)
530        .ok_or(Error::Overflow)?;
531
532    save_state(state, state_acc)?;
533
534    Ok(())
535}
536
537fn get_voucher(
538    voucher_acc: &AccountInfo,
539    program_id: &Pubkey,
540    rent: &Rent,
541) -> Result<Option<Voucher>, ProgramError> {
542    if !voucher_acc.is_writable {
543        return Err(ProgramError::InvalidArgument);
544    }
545
546    if voucher_acc.owner != program_id {
547        return Err(ProgramError::IncorrectProgramId);
548    }
549
550    if !rent.is_exempt(voucher_acc.lamports(), voucher_acc.data_len()) {
551        return Err(ProgramError::AccountNotRentExempt);
552    }
553
554    let data = voucher_acc.try_borrow_data()?;
555
556    match Voucher::try_from_slice(&data) {
557        Ok(v) if v.amount > 0 => Ok(Some(v)),
558        _ => Ok(None),
559    }
560}
561
562// [writable] request state
563// [writable] request wallet
564// [writable] payer
565// [] rent var
566// [] clock var
567// for n..10:
568// [writable] voucher
569// [writable] user atoken wallet
570fn process_refund(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
571    let account_info_iter = &mut accounts.iter().peekable();
572
573    let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
574    let wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
575
576    let payer = next_account_info(account_info_iter)?;
577    let rent = Rent::from_account_info(next_account_info(account_info_iter)?)?;
578    let _clock = Clock::from_account_info(next_account_info(account_info_iter)?)?;
579
580    if *payer.key != state.payer {
581        // not fair
582        return Err(ProgramError::IllegalOwner);
583    }
584
585    if state.is_open() {
586        return Err(ProgramError::InvalidArgument);
587    }
588
589    let request = match state.request {
590        Request::Funded(_) => return Err(ProgramError::Custom(0x12)),
591        Request::Unfunded(ref mut s) => s,
592    };
593
594    let (derived_authority, authority_seed) = authority!(program_id, state_acc.key);
595
596    let mut payer_lamports = payer.try_borrow_mut_lamports()?;
597
598    // get our sweet money back
599    while account_info_iter.peek().is_some() {
600        let voucher_acc = next_account_info(account_info_iter)?;
601
602        let voucher = get_voucher(voucher_acc, program_id, &rent)?
603            .ok_or(ProgramError::UninitializedAccount)?;
604
605        // close voucher
606        redeem_voucher(voucher_acc, &mut payer_lamports)?;
607
608        request.collected = request
609            .collected
610            .checked_sub(voucher.amount)
611            .ok_or(ProgramError::Custom(0x14))?;
612
613        let user_wallet = next_account_info(account_info_iter)?;
614
615        let derived_wallet = get_associated_token_address(&voucher.user, &wallet.mint);
616        if derived_wallet != *user_wallet.key {
617            return Err(ProgramError::InvalidArgument);
618        }
619
620        if state.request_status == RequestStatus::Accepted {
621            // nothing to refund
622            continue;
623        }
624
625        // refund user
626        let transfer = token_inst::transfer(
627            &spl_token::ID,
628            &state.wallet,
629            user_wallet.key,
630            &derived_authority,
631            &[],
632            voucher.amount,
633        )?;
634
635        invoke_signed(&transfer, accounts, &[authority_seed])?;
636    }
637
638    if request.collected > 0 {
639        // some refunding still required
640        return Ok(());
641    }
642
643    if wallet.amount != 0 {
644        msg!("sanity check failed: collected == 0 but token amount is still not zero");
645        return Err(ProgramError::Custom(0x15));
646    }
647
648    // all accounts closed and refunded
649    let close = token_inst::close_account(
650        &spl_token::ID,
651        &state.wallet,
652        payer.key,
653        &derived_authority,
654        &[],
655    )?;
656
657    invoke_signed(&close, accounts, &[authority_seed])?;
658
659    erase_state(state, state_acc, payer)?;
660
661    Ok(())
662}
663
664fn redeem_voucher(state_acc: &AccountInfo, payer_lamports: &mut u64) -> Result<(), ProgramError> {
665    // withdraw all lamports from state
666    *payer_lamports = payer_lamports
667        .checked_add(mem::take(*state_acc.try_borrow_mut_lamports()?))
668        .ok_or(Error::Overflow)?;
669
670    let mut data = state_acc.try_borrow_mut_data()?;
671    let l = data.len();
672    sol_memset(&mut data, 0, l);
673
674    Ok(())
675}
676
677// [writable] request state
678// [writable] request wallet
679// [sign] destination account
680// [writable] destination token wallet
681// [writable] payer
682// [] clock var
683// [] derived authority
684// [] token program
685fn process_accept(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
686    let account_info_iter = &mut accounts.iter();
687
688    let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
689    let wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
690
691    let _dest = next_signer_account(account_info_iter, &state.destination)?;
692    let destination_wallet = next_account_info(account_info_iter)?;
693
694    let payer = next_account_info(account_info_iter)?;
695    let clock = Clock::from_account_info(next_account_info(account_info_iter)?)?;
696
697    if *payer.key != state.payer {
698        // not fair
699        return Err(ProgramError::IllegalOwner);
700    }
701
702    if state.expired(clock.unix_timestamp) {
703        state.request_status = RequestStatus::Declined;
704        save_state(state, state_acc)?;
705        return Err(ProgramError::Custom(0x17));
706    }
707
708    if !state.is_open() {
709        return Err(ProgramError::Custom(0x10));
710    }
711
712    // sanity check
713    if wallet.amount == 0 {
714        return Err(ProgramError::Custom(0x11));
715    }
716
717    state.try_accept()?;
718
719    let (derived_authority, authority_seed) = authority!(program_id, state_acc.key);
720
721    // transfer tokens to dest
722    let transfer = token_inst::transfer(
723        &spl_token::ID,
724        &state.wallet,
725        destination_wallet.key,
726        &derived_authority,
727        &[],
728        wallet.amount,
729    )?;
730
731    invoke_signed(&transfer, accounts, &[authority_seed])?;
732
733    if state.is_funded() {
734        // close token wallet
735        let close = token_inst::close_account(
736            &spl_token::ID,
737            &state.wallet,
738            payer.key,
739            &derived_authority,
740            &[],
741        )?;
742
743        invoke_signed(&close, accounts, &[authority_seed])?;
744
745        // we're done here. return our lamports
746        erase_state(state, state_acc, payer)?;
747        return Ok(());
748    }
749
750    // unfunded requests stay until every single contributor closed his account
751    save_state(state, state_acc)?;
752
753    Ok(())
754}
755
756// [writable] request state
757// [writable] request wallet
758// [sign] author
759// [writable] atoken address of author
760// [writable] payer
761// [] derived authority
762fn process_cancel(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
763    let account_info_iter = &mut accounts.iter();
764
765    let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
766    let wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
767
768    let author = match state.request {
769        Request::Funded(FundedRequest { author }) => author,
770        Request::Unfunded { .. } => return Err(ProgramError::Custom(0x13)),
771    };
772
773    let _dest = next_signer_account(account_info_iter, &author)?;
774    let payer = next_expected_account(account_info_iter, &state.payer)?;
775
776    if !state.is_open() {
777        return Err(ProgramError::Custom(0x10));
778    }
779
780    state.request_status = RequestStatus::Declined;
781    let (derived_authority, authority_seed) = authority!(program_id, state_acc.key);
782    let _authority = next_expected_account(account_info_iter, &derived_authority)?;
783
784    if let COption::Some(rent_balance) = wallet.is_native {
785        let atoken_addr = get_associated_token_address(&author, &wallet.mint);
786        next_expected_account(account_info_iter, &atoken_addr)?;
787        next_expected_account(account_info_iter, &spl_token::ID)?;
788        next_expected_account(account_info_iter, &system_program::ID)?;
789
790        // close token wallet
791        let close = token_inst::close_account(
792            &spl_token::ID,
793            &state.wallet,
794            &derived_authority,
795            &derived_authority,
796            &[],
797        )?;
798        invoke_signed(&close, accounts, &[authority_seed])?;
799
800        // system transfer amount to author
801        msg!("transfer sol back to dest");
802        let transfer = system_instruction::transfer(&derived_authority, &author, wallet.amount);
803        invoke_signed(&transfer, accounts, &[authority_seed])?;
804
805        // transfer rent exemption to payer
806        msg!("transfer rent back to payer");
807        let transfer = system_instruction::transfer(&derived_authority, payer.key, rent_balance);
808        invoke_signed(&transfer, accounts, &[authority_seed])?;
809    } else {
810        let (author_wallet, _) = next_atoken_wallet(account_info_iter, &author, &wallet.mint)?;
811        next_expected_account(account_info_iter, &spl_token::ID)?;
812
813        // transfer tokens to dest
814        msg!("transfer tokens back to dest");
815        let transfer = token_inst::transfer(
816            &spl_token::ID,
817            &state.wallet,
818            &author_wallet,
819            &derived_authority,
820            &[],
821            wallet.amount,
822        )?;
823
824        invoke_signed(&transfer, accounts, &[authority_seed])?;
825
826        msg!("close token wallet");
827        // close token wallet
828        let close = token_inst::close_account(
829            &spl_token::ID,
830            &state.wallet,
831            payer.key,
832            &derived_authority,
833            &[],
834        )?;
835        invoke_signed(&close, accounts, &[authority_seed])?;
836    }
837    erase_state(state, state_acc, payer)?;
838
839    Ok(())
840}
841
842// [writable] request
843// [writable] request wallet
844// [sign] destination account
845// [writable] payer
846// [optional, writable] if request is funded, atoken adress of author
847// [writable] if request is funded, author address
848// [] authority
849// [] token prog
850fn process_decline(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
851    let account_info_iter = &mut accounts.iter();
852
853    let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
854    let wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
855
856    let _dest = next_signer_account(account_info_iter, &state.destination)?;
857    let payer = next_expected_account(account_info_iter, &state.payer)?;
858
859    if !state.is_open() {
860        return Err(ProgramError::Custom(0x10));
861    }
862
863    state.request_status = RequestStatus::Declined;
864
865    match state.request {
866        Request::Funded(ref r) => {
867            let (derived_authority, authority_seed) = authority!(program_id, state_acc.key);
868            //let _authority = next_expected_account(account_info_iter, &derived_authority)?;
869
870            if let COption::Some(rent_balance) = wallet.is_native {
871                let _author = next_expected_account(account_info_iter, &r.author)?;
872
873                // close token wallet
874                let close = token_inst::close_account(
875                    &spl_token::ID,
876                    &state.wallet,
877                    &derived_authority,
878                    &derived_authority,
879                    &[],
880                )?;
881                invoke_signed(&close, accounts, &[authority_seed])?;
882
883                next_expected_account(account_info_iter, &spl_token::ID)?; // 8
884                next_expected_account(account_info_iter, &system_program::ID)?; // 9
885
886                // system transfer amount to author
887                let transfer =
888                    system_instruction::transfer(&derived_authority, &r.author, wallet.amount);
889                invoke_signed(&transfer, accounts, &[authority_seed])?;
890
891                // transfer rent exemption to payer
892                let transfer =
893                    system_instruction::transfer(&derived_authority, payer.key, rent_balance);
894                invoke_signed(&transfer, accounts, &[authority_seed])?;
895            } else {
896                let (author_wallet, _) =
897                    next_atoken_wallet(account_info_iter, &r.author, &wallet.mint)?;
898
899                next_expected_account(account_info_iter, &r.author)?;
900
901                // transfer tokens to dest
902                let transfer = token_inst::transfer(
903                    &spl_token::ID,
904                    &state.wallet,
905                    &author_wallet,
906                    &derived_authority,
907                    &[],
908                    wallet.amount,
909                )?;
910
911                invoke_signed(&transfer, accounts, &[authority_seed])?;
912
913                // close token wallet
914                let close = token_inst::close_account(
915                    &spl_token::ID,
916                    &state.wallet,
917                    payer.key,
918                    &derived_authority,
919                    &[],
920                )?;
921
922                invoke_signed(&close, accounts, &[authority_seed])?;
923            }
924
925            // we're done here. return our lamports
926            erase_state(state, state_acc, payer)?;
927            Ok(())
928        }
929        Request::Unfunded(_) => {
930            // unfunded requests stay until every single contributor has closed an account
931            save_state(state, state_acc)?;
932            Ok(())
933        }
934    }
935}
936#[cfg(test)]
937mod tests {
938    use solana_program::pubkey::Pubkey;
939
940    use crate::{
941        Request::{self, *},
942        *,
943    };
944
945    #[test]
946    fn test_state_is_funded() {
947        assert!(
948            from_request(Unfunded(UnfundedRequest {
949                collected: 0,
950                deadline: None,
951                accept_threshold: 0,
952            }))
953            .is_funded()
954                == false
955        );
956
957        assert!(
958            from_request(Funded(FundedRequest {
959                author: Pubkey::default(),
960            }))
961            .is_funded()
962                == true
963        );
964    }
965
966    #[test]
967    fn test_state_expired() {
968        let now = 100000000;
969
970        let expired = from_request(Unfunded(UnfundedRequest {
971            collected: 1000,
972            deadline: Some(now - 100), // dealine has passed
973            accept_threshold: 100000,
974        }));
975
976        assert!(expired.expired(now) == true);
977
978        //
979        let funded_in_time = from_request(Unfunded(UnfundedRequest {
980            collected: 9999,
981            deadline: Some(now - 100), // dealine has passed but enough was collected
982            accept_threshold: 1000,
983        }));
984
985        assert!(funded_in_time.expired(now) == false);
986
987        let not_expired = from_request(Unfunded(UnfundedRequest {
988            collected: 1000,
989            deadline: Some(now + 100), // deadline has not yet passed
990            accept_threshold: 100000,
991        }));
992
993        assert!(not_expired.expired(now) == false);
994
995        let no_deadline = from_request(Unfunded(UnfundedRequest {
996            collected: 1000,
997            deadline: None, // no deadline
998            accept_threshold: 100000,
999        }));
1000
1001        assert!(no_deadline.expired(now) == false);
1002    }
1003
1004    #[test]
1005    fn test_state_try_accept() {
1006        let mut funded = from_request(Funded(FundedRequest {
1007            author: Pubkey::default(),
1008        }));
1009
1010        // funded can always be accepted
1011        funded.try_accept().unwrap();
1012        assert!(!funded.is_open());
1013
1014        let mut unfunded = from_request(Unfunded(UnfundedRequest {
1015            collected: 1000,
1016            deadline: None,
1017            accept_threshold: 100000,
1018        }));
1019
1020        unfunded.try_accept().unwrap_err();
1021
1022        assert_eq!(unfunded.request_status, RequestStatus::Open);
1023        assert!(unfunded.is_open());
1024
1025        let mut acceptable = from_request(Unfunded(UnfundedRequest {
1026            collected: 77777,
1027            deadline: None,
1028            accept_threshold: 10,
1029        }));
1030
1031        acceptable.try_accept().unwrap();
1032
1033        assert_eq!(acceptable.request_status, RequestStatus::Accepted);
1034        assert!(!acceptable.is_open());
1035    }
1036
1037    fn from_request(request: Request) -> State {
1038        let s = State {
1039            request_status: RequestStatus::Open,
1040            wallet: Pubkey::default(),
1041            destination: Pubkey::default(),
1042            payer: Pubkey::default(),
1043            created_at: 0,
1044            request,
1045        };
1046
1047        // assert_eq!(size_of::<State>(), 0);
1048        // assert_eq!(s.try_to_vec().unwrap().len(), 0);
1049
1050        s
1051    }
1052
1053    #[test]
1054    fn test_get_voucher() {
1055        let rent = Rent::with_slots_per_epoch(432000);
1056
1057        let data_len = 72;
1058        let mut data = vec![0; data_len];
1059        let mut lamports = rent.minimum_balance(data_len);
1060
1061        let key = Pubkey::new_from_array([1; 32]);
1062        let pid = Pubkey::new_from_array([23; 32]);
1063
1064        // not initialized
1065        {
1066            let acc = AccountInfo::new(&key, false, true, &mut lamports, &mut data, &pid, false, 0);
1067            assert!(get_voucher(&acc, &pid, &rent).unwrap().is_none());
1068        }
1069
1070        {
1071            // invalid pid
1072            let acc = AccountInfo::new(&key, false, true, &mut lamports, &mut data, &pid, false, 0);
1073            assert_eq!(
1074                get_voucher(&acc, &Pubkey::default(), &rent).unwrap_err(),
1075                ProgramError::IncorrectProgramId
1076            );
1077        }
1078
1079        {
1080            // not rent exempt
1081            let mut ins = 12;
1082
1083            let acc = AccountInfo::new(&key, false, true, &mut ins, &mut data, &pid, false, 0);
1084            assert_eq!(
1085                get_voucher(&acc, &pid, &rent).unwrap_err(),
1086                ProgramError::AccountNotRentExempt
1087            );
1088        }
1089
1090        {
1091            // happy path
1092            let state_addr = Pubkey::new_from_array([66; 32]);
1093            let user_addr = Pubkey::new_from_array([77; 32]);
1094
1095            let v = Voucher {
1096                state: state_addr,
1097                user: user_addr,
1098                amount: 123,
1099            };
1100
1101            let mut data = v.try_to_vec().unwrap();
1102
1103            let acc = AccountInfo::new(&key, false, true, &mut lamports, &mut data, &pid, false, 0);
1104            assert_eq!(get_voucher(&acc, &pid, &rent).unwrap().unwrap(), v);
1105        }
1106    }
1107}