Skip to main content

spl_feature_proposal/
instruction.rs

1//! Program instructions
2
3use crate::{state::AcceptanceCriteria, *};
4use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
5use solana_program::{
6    info,
7    instruction::{AccountMeta, Instruction},
8    program_error::ProgramError,
9    program_pack::{Pack, Sealed},
10    pubkey::Pubkey,
11    sysvar,
12};
13
14/// Instructions supported by the Feature Proposal program
15#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)]
16pub enum FeatureProposalInstruction {
17    /// Propose a new feature.
18    ///
19    /// This instruction will create a variety of accounts to support the feature proposal, all
20    /// funded by account 0:
21    /// * A new token mint with a supply of `tokens_to_mint`, owned by the program and never
22    ///   modified again
23    /// * A new "distributor" token account that holds the total supply, owned by account 0.
24    /// * A new "acceptance" token account that holds 0 tokens, owned by the program.  Tokens
25    ///   transfers to this address are irrevocable and permanent.
26    /// * A new feature id account that has been funded and allocated (as described in
27    ///  `solana_program::feature`)
28    ///
29    /// On successful execution of the instruction, the feature proposer is expected to distribute
30    /// the tokens in the distributor token account out to all participating parties.
31    ///
32    /// Based on the provided acceptance criteria, if `AcceptanceCriteria::tokens_required`
33    /// tokens are transferred into the acceptance token account before
34    /// `AcceptanceCriteria::deadline` then the proposal is eligible to be accepted.
35    ///
36    /// The `FeatureProposalInstruction::Tally` instruction must be executed, by any party, to
37    /// complete the feature acceptance process.
38    ///
39    /// Accounts expected by this instruction:
40    ///
41    /// 0. `[writeable,signer]` Funding account (must be a system account)
42    /// 1. `[writeable,signer]` Unallocated feature proposal account to create
43    /// 2. `[writeable]` Token mint address from `get_mint_address`
44    /// 3. `[writeable]` Distributor token account address from `get_distributor_token_address`
45    /// 4. `[writeable]` Acceptance token account address from `get_acceptance_token_address`
46    /// 5. `[writeable]` Feature id account address from `get_feature_id_address`
47    /// 6. `[]` System program
48    /// 7. `[]` SPL Token program
49    /// 8. `[]` Rent sysvar
50    ///
51    Propose {
52        /// Total number of tokens to mint for this proposal
53        #[allow(dead_code)] // not dead code..
54        tokens_to_mint: u64,
55
56        /// Criteria for how this proposal may be activated
57        #[allow(dead_code)] // not dead code..
58        acceptance_criteria: AcceptanceCriteria,
59    },
60
61    /// `Tally` is a permission-less instruction to check the acceptance criteria for the feature
62    /// proposal, which may result in:
63    /// * No action
64    /// * Feature proposal acceptance
65    /// * Feature proposal expiration
66    ///
67    /// Accounts expected by this instruction:
68    ///
69    /// 0. `[writeable]` Feature proposal account
70    /// 1. `[]` Acceptance token account address from `get_acceptance_token_address`
71    /// 2. `[writeable]` Derived feature id account address from `get_feature_id_address`
72    /// 3. `[]` System program
73    /// 4. `[]` Clock sysvar
74    Tally,
75}
76
77impl Sealed for FeatureProposalInstruction {}
78impl Pack for FeatureProposalInstruction {
79    const LEN: usize = 25; // see `test_get_packed_len()` for justification of "18"
80
81    fn pack_into_slice(&self, dst: &mut [u8]) {
82        let data = self.pack_into_vec();
83        dst[..data.len()].copy_from_slice(&data);
84    }
85
86    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
87        let mut mut_src: &[u8] = src;
88        Self::deserialize(&mut mut_src).map_err(|err| {
89            info!(&format!(
90                "Error: failed to deserialize feature proposal instruction: {}",
91                err
92            ));
93            ProgramError::InvalidInstructionData
94        })
95    }
96}
97
98impl FeatureProposalInstruction {
99    fn pack_into_vec(&self) -> Vec<u8> {
100        self.try_to_vec().expect("try_to_vec")
101    }
102}
103
104/// Create a `FeatureProposalInstruction::Propose` instruction
105pub fn propose(
106    funding_address: &Pubkey,
107    feature_proposal_address: &Pubkey,
108    tokens_to_mint: u64,
109    acceptance_criteria: AcceptanceCriteria,
110) -> Instruction {
111    let mint_address = get_mint_address(feature_proposal_address);
112    let distributor_token_address = get_distributor_token_address(feature_proposal_address);
113    let acceptance_token_address = get_acceptance_token_address(feature_proposal_address);
114    let feature_id_address = get_feature_id_address(feature_proposal_address);
115
116    Instruction {
117        program_id: id(),
118        accounts: vec![
119            AccountMeta::new(*funding_address, true),
120            AccountMeta::new(*feature_proposal_address, true),
121            AccountMeta::new(mint_address, false),
122            AccountMeta::new(distributor_token_address, false),
123            AccountMeta::new(acceptance_token_address, false),
124            AccountMeta::new(feature_id_address, false),
125            AccountMeta::new_readonly(solana_program::system_program::id(), false),
126            AccountMeta::new_readonly(spl_token::id(), false),
127            AccountMeta::new_readonly(sysvar::rent::id(), false),
128        ],
129        data: FeatureProposalInstruction::Propose {
130            tokens_to_mint,
131            acceptance_criteria,
132        }
133        .pack_into_vec(),
134    }
135}
136
137/// Create a `FeatureProposalInstruction::Tally` instruction
138pub fn tally(feature_proposal_address: &Pubkey) -> Instruction {
139    let acceptance_token_address = get_acceptance_token_address(feature_proposal_address);
140    let feature_id_address = get_feature_id_address(feature_proposal_address);
141
142    Instruction {
143        program_id: id(),
144        accounts: vec![
145            AccountMeta::new(*feature_proposal_address, false),
146            AccountMeta::new_readonly(acceptance_token_address, false),
147            AccountMeta::new(feature_id_address, false),
148            AccountMeta::new_readonly(solana_program::system_program::id(), false),
149            AccountMeta::new_readonly(sysvar::clock::id(), false),
150        ],
151        data: FeatureProposalInstruction::Tally.pack_into_vec(),
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::borsh_utils;
159
160    #[test]
161    fn test_get_packed_len() {
162        assert_eq!(
163            FeatureProposalInstruction::get_packed_len(),
164            borsh_utils::get_packed_len::<FeatureProposalInstruction>()
165        )
166    }
167
168    #[test]
169    fn test_serialize_bytes() {
170        assert_eq!(
171            FeatureProposalInstruction::Tally.try_to_vec().unwrap(),
172            vec![1]
173        );
174
175        assert_eq!(
176            FeatureProposalInstruction::Propose {
177                tokens_to_mint: 42,
178                acceptance_criteria: AcceptanceCriteria {
179                    tokens_required: 0xdeadbeefdeadbeef,
180                    deadline: -1,
181                }
182            }
183            .try_to_vec()
184            .unwrap(),
185            vec![
186                0, 42, 0, 0, 0, 0, 0, 0, 0, 239, 190, 173, 222, 239, 190, 173, 222, 255, 255, 255,
187                255, 255, 255, 255, 255
188            ]
189        );
190    }
191
192    #[test]
193    fn test_serialize_large_slice() {
194        let mut dst = vec![0xff; 4];
195        FeatureProposalInstruction::Tally.pack_into_slice(&mut dst);
196
197        // Extra bytes (0xff) ignored
198        assert_eq!(dst, vec![1, 0xff, 0xff, 0xff]);
199    }
200
201    #[test]
202    fn state_deserialize_invalid() {
203        assert_eq!(
204            FeatureProposalInstruction::unpack_from_slice(&[1]),
205            Ok(FeatureProposalInstruction::Tally),
206        );
207
208        // Extra bytes (0xff) ignored...
209        assert_eq!(
210            FeatureProposalInstruction::unpack_from_slice(&[1, 0xff, 0xff, 0xff]),
211            Ok(FeatureProposalInstruction::Tally),
212        );
213
214        assert_eq!(
215            FeatureProposalInstruction::unpack_from_slice(&[2]),
216            Err(ProgramError::InvalidInstructionData),
217        );
218    }
219}