Skip to content

Commit

Permalink
Feature Proposal program
Browse files Browse the repository at this point in the history
  • Loading branch information
mvines authored and mergify[bot] committed Nov 14, 2020
1 parent 83096bc commit c4ec3b3
Show file tree
Hide file tree
Showing 12 changed files with 1,034 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"examples/rust/logging",
"examples/rust/sysvar",
"examples/rust/transfer-lamports",
"feature-proposal/program",
"memo/program",
"shared-memory/program",
"stake-pool/cli",
Expand Down
31 changes: 31 additions & 0 deletions feature-proposal/program/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "spl-feature-proposal"
version = "1.0.0-pre1"
description = "Solana Program Library Feature Proposal Program"
authors = ["Solana Maintainers <[email protected]>"]
repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0"
edition = "2018"

[features]
no-entrypoint = []
test-bpf = []

[dependencies]
borsh = "0.7.1"
borsh-derive = "0.7.1"
solana-program = "1.4.5"
spl-token = { version = "3.0", path = "../../token/program", features = ["no-entrypoint"] }


[dev-dependencies]
futures = "0.3"
solana-program-test = "1.4.5"
solana-sdk = "1.4.5"
tokio = { version = "0.3", features = ["macros"]}

[lib]
crate-type = ["cdylib", "lib"]

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
2 changes: 2 additions & 0 deletions feature-proposal/program/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
1 change: 1 addition & 0 deletions feature-proposal/program/program-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
badkenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8kn
15 changes: 15 additions & 0 deletions feature-proposal/program/run-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash

set -ex
cd "$(dirname "$0")"
cargo fmt -- --check
cargo clippy
cargo build
cargo build-bpf

if [[ $1 = -v ]]; then
export RUST_LOG=solana=debug
fi

cargo test
cargo test-bpf
55 changes: 55 additions & 0 deletions feature-proposal/program/src/borsh_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! Borsh utils
use borsh::schema::{BorshSchema, Declaration, Definition, Fields};
use std::collections::HashMap;

/// Get packed length for the given BorchSchema Declaration
fn get_declaration_packed_len(
declaration: &str,
definitions: &HashMap<Declaration, Definition>,
) -> usize {
match definitions.get(declaration) {
Some(Definition::Array { length, elements }) => {
*length as usize * get_declaration_packed_len(elements, definitions)
}
Some(Definition::Enum { variants }) => {
1 + variants
.iter()
.map(|(_, declaration)| get_declaration_packed_len(declaration, definitions))
.max()
.unwrap_or(0)
}
Some(Definition::Struct { fields }) => match fields {
Fields::NamedFields(named_fields) => named_fields
.iter()
.map(|(_, declaration)| get_declaration_packed_len(declaration, definitions))
.sum(),
Fields::UnnamedFields(declarations) => declarations
.iter()
.map(|declaration| get_declaration_packed_len(declaration, definitions))
.sum(),
Fields::Empty => 0,
},
Some(Definition::Sequence {
elements: _elements,
}) => panic!("Missing support for Definition::Sequence"),
Some(Definition::Tuple { elements }) => elements
.iter()
.map(|element| get_declaration_packed_len(element, definitions))
.sum(),
None => match declaration {
"u8" | "i8" => 1,
"u16" | "i16" => 2,
"u32" | "i32" => 2,
"u64" | "i64" => 8,
"u128" | "i128" => 16,
"nil" => 0,
_ => panic!("Missing primitive type: {}", declaration),
},
}
}

/// Get the worst-case packed length for the given BorshSchema
pub fn get_packed_len<S: BorshSchema>() -> usize {
let schema_container = S::schema_container();
get_declaration_packed_len(&schema_container.declaration, &schema_container.definitions)
}
16 changes: 16 additions & 0 deletions feature-proposal/program/src/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! Program entrypoint
#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))]

use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
};

entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
crate::processor::process_instruction(program_id, accounts, instruction_data)
}
216 changes: 216 additions & 0 deletions feature-proposal/program/src/instruction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
//! Program instructions
use crate::{state::AcceptanceCriteria, *};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
info,
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
program_pack::{Pack, Sealed},
pubkey::Pubkey,
sysvar,
};

/// Instructions supported by the Feature Proposal program
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)]
pub enum FeatureProposalInstruction {
/// Propose a new feature.
///
/// This instruction will create a variety of accounts to support the feature proposal, all
/// funded by account 0:
/// * A new token mint with a supply of `tokens_to_mint`, owned by the program and never
/// modified again
/// * A new "delivery" token account that holds the total supply, owned by account 0.
/// * A new "acceptance" token account that holds 0 tokens, owned by the program. Tokens
/// transfers to this address are irrevocable and permanent.
/// * A new feature id account that has been funded and allocated (as described in
/// `solana_program::feature`)
///
/// On successful execution of the instruction, the feature proposer is expected to distribute
/// the tokens in the delivery token account out to all participating parties.
///
/// Based on the provided acceptance criteria, if `AcceptanceCriteria::tokens_required`
/// tokens are transferred into the acceptance token account before
/// `AcceptanceCriteria::deadline` then the proposal is eligible to be accepted.
///
/// The `FeatureProposalInstruction::Tally` instruction must be executed, by any party, to
/// complete the feature acceptance process.
///
/// Accounts expected by this instruction:
///
/// 0. `[writeable,signer]` Funding account (must be a system account)
/// 1. `[writeable,signer]` Unallocated feature proposal account to create
/// 2. `[writeable]` Token mint address from `get_mint_address`
/// 3. `[writeable]` Delivery token account address from `get_delivery_token_address`
/// 4. `[writeable]` Acceptance token account address from `get_acceptance_token_address`
/// 5. `[writeable]` Feature id account address from `get_feature_id_address`
/// 6. `[]` System program
/// 7. `[]` SPL Token program
/// 8. `[]` Rent sysvar
///
Propose {
/// Total number of tokens to mint for this proposal
#[allow(dead_code)] // not dead code..
tokens_to_mint: u64,

/// Criteria for how this proposal may be activated
#[allow(dead_code)] // not dead code..
acceptance_criteria: AcceptanceCriteria,
},

/// `Tally` is a permission-less instruction to check the acceptance criteria for the feature
/// proposal, which may result in:
/// * No action
/// * Feature proposal acceptance
/// * Feature proposal expiration
///
/// Accounts expected by this instruction:
///
/// 0. `[writeable]` Feature proposal account
/// 1. `[]` Acceptance token account address from `get_acceptance_token_address`
/// 2. `[writeable]` Derived feature id account address from `get_feature_id_address`
/// 3. `[]` System program
/// 4. `[]` Clock sysvar
Tally,
}

impl Sealed for FeatureProposalInstruction {}
impl Pack for FeatureProposalInstruction {
const LEN: usize = 26; // see `test_get_packed_len()` for justification of "18"

fn pack_into_slice(&self, dst: &mut [u8]) {
let data = self.pack_into_vec();
dst[..data.len()].copy_from_slice(&data);
}

fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
let mut mut_src: &[u8] = src;
Self::deserialize(&mut mut_src).map_err(|err| {
info!(&format!(
"Error: failed to deserialize feature proposal instruction: {}",
err
));
ProgramError::InvalidInstructionData
})
}
}

impl FeatureProposalInstruction {
fn pack_into_vec(&self) -> Vec<u8> {
self.try_to_vec().expect("try_to_vec")
}
}

/// Create a `FeatureProposalInstruction::Propose` instruction
pub fn propose(
funding_address: &Pubkey,
feature_proposal_address: &Pubkey,
tokens_to_mint: u64,
acceptance_criteria: AcceptanceCriteria,
) -> Instruction {
let mint_address = get_mint_address(feature_proposal_address);
let delivery_token_address = get_delivery_token_address(feature_proposal_address);
let acceptance_token_address = get_acceptance_token_address(feature_proposal_address);
let feature_id_address = get_feature_id_address(feature_proposal_address);

Instruction {
program_id: id(),
accounts: vec![
AccountMeta::new(*funding_address, true),
AccountMeta::new(*feature_proposal_address, true),
AccountMeta::new(mint_address, false),
AccountMeta::new(delivery_token_address, false),
AccountMeta::new(acceptance_token_address, false),
AccountMeta::new(feature_id_address, false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: FeatureProposalInstruction::Propose {
tokens_to_mint,
acceptance_criteria,
}
.pack_into_vec(),
}
}

/// Create a `FeatureProposalInstruction::Tally` instruction
pub fn tally(feature_proposal_address: &Pubkey) -> Instruction {
let acceptance_token_address = get_acceptance_token_address(feature_proposal_address);
let feature_id_address = get_feature_id_address(feature_proposal_address);

Instruction {
program_id: id(),
accounts: vec![
AccountMeta::new(*feature_proposal_address, false),
AccountMeta::new_readonly(acceptance_token_address, false),
AccountMeta::new(feature_id_address, false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
],
data: FeatureProposalInstruction::Tally.pack_into_vec(),
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::borsh_utils;

#[test]
fn test_get_packed_len() {
assert_eq!(
FeatureProposalInstruction::get_packed_len(),
borsh_utils::get_packed_len::<FeatureProposalInstruction>()
)
}

#[test]
fn test_serialize_bytes() {
assert_eq!(
FeatureProposalInstruction::Tally.try_to_vec().unwrap(),
vec![1]
);

assert_eq!(
FeatureProposalInstruction::Propose {
tokens_to_mint: 42,
acceptance_criteria: AcceptanceCriteria {
tokens_required: 0xdeadbeefdeadbeef,
deadline: None,
}
}
.try_to_vec()
.unwrap(),
vec![0, 42, 0, 0, 0, 0, 0, 0, 0, 239, 190, 173, 222, 239, 190, 173, 222, 0]
);
}

#[test]
fn test_serialize_large_slice() {
let mut dst = vec![0xff; 4];
FeatureProposalInstruction::Tally.pack_into_slice(&mut dst);

// Extra bytes (0xff) ignored
assert_eq!(dst, vec![1, 0xff, 0xff, 0xff]);
}

#[test]
fn state_deserialize_invalid() {
assert_eq!(
FeatureProposalInstruction::unpack_from_slice(&[1]),
Ok(FeatureProposalInstruction::Tally),
);

// Extra bytes (0xff) ignored...
assert_eq!(
FeatureProposalInstruction::unpack_from_slice(&[1, 0xff, 0xff, 0xff]),
Ok(FeatureProposalInstruction::Tally),
);

assert_eq!(
FeatureProposalInstruction::unpack_from_slice(&[2]),
Err(ProgramError::InvalidInstructionData),
);
}
}
Loading

0 comments on commit c4ec3b3

Please sign in to comment.