Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit c4ec3b3

Browse files
mvinesmergify[bot]
authored andcommitted
Feature Proposal program
1 parent 83096bc commit c4ec3b3

File tree

12 files changed

+1034
-0
lines changed

12 files changed

+1034
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"examples/rust/logging",
77
"examples/rust/sysvar",
88
"examples/rust/transfer-lamports",
9+
"feature-proposal/program",
910
"memo/program",
1011
"shared-memory/program",
1112
"stake-pool/cli",

feature-proposal/program/Cargo.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "spl-feature-proposal"
3+
version = "1.0.0-pre1"
4+
description = "Solana Program Library Feature Proposal Program"
5+
authors = ["Solana Maintainers <[email protected]>"]
6+
repository = "https://github.com/solana-labs/solana-program-library"
7+
license = "Apache-2.0"
8+
edition = "2018"
9+
10+
[features]
11+
no-entrypoint = []
12+
test-bpf = []
13+
14+
[dependencies]
15+
borsh = "0.7.1"
16+
borsh-derive = "0.7.1"
17+
solana-program = "1.4.5"
18+
spl-token = { version = "3.0", path = "../../token/program", features = ["no-entrypoint"] }
19+
20+
21+
[dev-dependencies]
22+
futures = "0.3"
23+
solana-program-test = "1.4.5"
24+
solana-sdk = "1.4.5"
25+
tokio = { version = "0.3", features = ["macros"]}
26+
27+
[lib]
28+
crate-type = ["cdylib", "lib"]
29+
30+
[package.metadata.docs.rs]
31+
targets = ["x86_64-unknown-linux-gnu"]

feature-proposal/program/Xargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.bpfel-unknown-unknown.dependencies.std]
2+
features = []
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
badkenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8kn

feature-proposal/program/run-tests.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env bash
2+
3+
set -ex
4+
cd "$(dirname "$0")"
5+
cargo fmt -- --check
6+
cargo clippy
7+
cargo build
8+
cargo build-bpf
9+
10+
if [[ $1 = -v ]]; then
11+
export RUST_LOG=solana=debug
12+
fi
13+
14+
cargo test
15+
cargo test-bpf
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//! Borsh utils
2+
use borsh::schema::{BorshSchema, Declaration, Definition, Fields};
3+
use std::collections::HashMap;
4+
5+
/// Get packed length for the given BorchSchema Declaration
6+
fn get_declaration_packed_len(
7+
declaration: &str,
8+
definitions: &HashMap<Declaration, Definition>,
9+
) -> usize {
10+
match definitions.get(declaration) {
11+
Some(Definition::Array { length, elements }) => {
12+
*length as usize * get_declaration_packed_len(elements, definitions)
13+
}
14+
Some(Definition::Enum { variants }) => {
15+
1 + variants
16+
.iter()
17+
.map(|(_, declaration)| get_declaration_packed_len(declaration, definitions))
18+
.max()
19+
.unwrap_or(0)
20+
}
21+
Some(Definition::Struct { fields }) => match fields {
22+
Fields::NamedFields(named_fields) => named_fields
23+
.iter()
24+
.map(|(_, declaration)| get_declaration_packed_len(declaration, definitions))
25+
.sum(),
26+
Fields::UnnamedFields(declarations) => declarations
27+
.iter()
28+
.map(|declaration| get_declaration_packed_len(declaration, definitions))
29+
.sum(),
30+
Fields::Empty => 0,
31+
},
32+
Some(Definition::Sequence {
33+
elements: _elements,
34+
}) => panic!("Missing support for Definition::Sequence"),
35+
Some(Definition::Tuple { elements }) => elements
36+
.iter()
37+
.map(|element| get_declaration_packed_len(element, definitions))
38+
.sum(),
39+
None => match declaration {
40+
"u8" | "i8" => 1,
41+
"u16" | "i16" => 2,
42+
"u32" | "i32" => 2,
43+
"u64" | "i64" => 8,
44+
"u128" | "i128" => 16,
45+
"nil" => 0,
46+
_ => panic!("Missing primitive type: {}", declaration),
47+
},
48+
}
49+
}
50+
51+
/// Get the worst-case packed length for the given BorshSchema
52+
pub fn get_packed_len<S: BorshSchema>() -> usize {
53+
let schema_container = S::schema_container();
54+
get_declaration_packed_len(&schema_container.declaration, &schema_container.definitions)
55+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//! Program entrypoint
2+
3+
#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))]
4+
5+
use solana_program::{
6+
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
7+
};
8+
9+
entrypoint!(process_instruction);
10+
fn process_instruction(
11+
program_id: &Pubkey,
12+
accounts: &[AccountInfo],
13+
instruction_data: &[u8],
14+
) -> ProgramResult {
15+
crate::processor::process_instruction(program_id, accounts, instruction_data)
16+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//! Program instructions
2+
3+
use crate::{state::AcceptanceCriteria, *};
4+
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
5+
use 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)]
16+
pub 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 "delivery" 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 delivery 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]` Delivery token account address from `get_delivery_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+
77+
impl Sealed for FeatureProposalInstruction {}
78+
impl Pack for FeatureProposalInstruction {
79+
const LEN: usize = 26; // 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+
98+
impl 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
105+
pub 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 delivery_token_address = get_delivery_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(delivery_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
138+
pub 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)]
156+
mod 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: None,
181+
}
182+
}
183+
.try_to_vec()
184+
.unwrap(),
185+
vec![0, 42, 0, 0, 0, 0, 0, 0, 0, 239, 190, 173, 222, 239, 190, 173, 222, 0]
186+
);
187+
}
188+
189+
#[test]
190+
fn test_serialize_large_slice() {
191+
let mut dst = vec![0xff; 4];
192+
FeatureProposalInstruction::Tally.pack_into_slice(&mut dst);
193+
194+
// Extra bytes (0xff) ignored
195+
assert_eq!(dst, vec![1, 0xff, 0xff, 0xff]);
196+
}
197+
198+
#[test]
199+
fn state_deserialize_invalid() {
200+
assert_eq!(
201+
FeatureProposalInstruction::unpack_from_slice(&[1]),
202+
Ok(FeatureProposalInstruction::Tally),
203+
);
204+
205+
// Extra bytes (0xff) ignored...
206+
assert_eq!(
207+
FeatureProposalInstruction::unpack_from_slice(&[1, 0xff, 0xff, 0xff]),
208+
Ok(FeatureProposalInstruction::Tally),
209+
);
210+
211+
assert_eq!(
212+
FeatureProposalInstruction::unpack_from_slice(&[2]),
213+
Err(ProgramError::InvalidInstructionData),
214+
);
215+
}
216+
}

0 commit comments

Comments
 (0)