diff --git a/programs/conditional_vault/src/error.rs b/programs/conditional_vault/src/error.rs index e5e970e0..8c621c50 100644 --- a/programs/conditional_vault/src/error.rs +++ b/programs/conditional_vault/src/error.rs @@ -10,6 +10,8 @@ pub enum VaultError { InsufficientConditionalTokens, #[msg("This `vault_underlying_token_account` is not this vault's `underlying_token_account`")] InvalidVaultUnderlyingTokenAccount, + #[msg("This `vault_underlying_token_mint` is not this vault's `underlying_token_mint`")] + InvalidVaultUnderlyingTokenMint, #[msg("This conditional token mint is not this vault's conditional token mint")] InvalidConditionalTokenMint, #[msg("Question needs to be resolved before users can redeem conditional tokens for underlying tokens")] @@ -28,6 +30,8 @@ pub enum VaultError { BadConditionalTokenAccount, #[msg("User conditional token account mint does not match conditional mint")] ConditionalTokenMintMismatch, + #[msg("Vault or user token account mint does not match underlying token mint")] + UnderlyingTokenMintMismatch, #[msg("Payouts must sum to 1 or more")] PayoutZero, #[msg("Question already resolved")] diff --git a/programs/conditional_vault/src/instructions/add_metadata_to_conditional_tokens.rs b/programs/conditional_vault/src/instructions/add_metadata_to_conditional_tokens.rs index af38ce6f..65f17159 100644 --- a/programs/conditional_vault/src/instructions/add_metadata_to_conditional_tokens.rs +++ b/programs/conditional_vault/src/instructions/add_metadata_to_conditional_tokens.rs @@ -1,50 +1,92 @@ use super::*; +/// Module containing proph3t's public key +/// This is used for authorization in production pub mod proph3t_deployer { use anchor_lang::declare_id; declare_id!("HfFi634cyurmVVDr9frwu4MjGLJzz9XbAJz981HdVaNz"); } +/// Arguments required for adding metadata to conditional tokens. +/// This metadata helps identify and display token information in wallets and GUIs. #[derive(AnchorSerialize, AnchorDeserialize)] pub struct AddMetadataToConditionalTokensArgs { + /// The display name of the conditional token + /// Example: "YES_SOL_ABOVE_50K_DEC2024" pub name: String, + + /// The trading symbol for the conditional token + /// Example: "YSOL50K" pub symbol: String, + + /// URI pointing to the token's metadata JSON + /// This should contain extended information about the token, + /// such as description, images, and additional attributes pub uri: String, } #[event_cpi] #[derive(Accounts)] pub struct AddMetadataToConditionalTokens<'info> { + /// The account paying for the metadata account creation + /// In production, this must be proph3t - the authorized deployer #[account(mut)] pub payer: Signer<'info>, + + /// The vault account that controls the conditional tokens + /// Must be in Active status to modify metadata #[account(mut)] pub vault: Account<'info, ConditionalVault>, + + /// The mint account of the conditional token + /// Must have the vault as its mint authority #[account( mut, mint::authority = vault, )] pub conditional_token_mint: Account<'info, Mint>, + + /// The account that will store the token metadata + /// Must be empty before initialization /// CHECK: verified via cpi into token metadata #[account(mut)] pub conditional_token_metadata: AccountInfo<'info>, + + /// The Token Metadata Program that will create and store the metadata pub token_metadata_program: Program<'info, Metadata>, + + /// The System Program Account pub system_program: Program<'info, System>, + + /// The Rent Sysvar account for rent calculations pub rent: Sysvar<'info, Rent>, } impl AddMetadataToConditionalTokens<'_> { + /// Validates the preconditions for adding metadata to conditional tokens + /// + /// # Checks + /// * Verifies the metadata account is empty (prevents overwriting) + /// * In production, ensures only proph3t (the authorized deployer) can add metadata + /// + /// # Returns + /// * `Ok(())` if all validations pass + /// * `Err(VaultError::ConditionalTokenMetadataAlreadySet)` if metadata exists pub fn validate(&self) -> Result<()> { + // Commented out for reference: // require!( // self.vault.status == VaultStatus::Active, // VaultError::VaultAlreadySettled // ); + // Ensure we're not overwriting existing metadata require!( self.conditional_token_metadata.data_is_empty(), VaultError::ConditionalTokenMetadataAlreadySet ); + // In production builds, restrict access to authorized deployer #[cfg(feature = "production")] require_eq!( self.payer.key(), proph3t_deployer::ID @@ -53,12 +95,36 @@ impl AddMetadataToConditionalTokens<'_> { Ok(()) } + /// Handles the addition of metadata to conditional tokens + /// + /// # Arguments + /// * `ctx` - The context containing all required accounts + /// * `args` - The metadata arguments (name, symbol, URI) + /// + /// # Steps + /// 1. Generates vault PDA seeds for signing + /// 2. Creates metadata account via CPI to Token Metadata Program + /// 3. Increments vault sequence number + /// 4. Emits event with metadata details + /// + /// # Example + /// ```ignore + /// let args = AddMetadataToConditionalTokensArgs { + /// name: "YES_SOL_ABOVE_50K".to_string(), + /// symbol: "YBTC50K".to_string(), + /// uri: "https://metadao.fi/token/123".to_string(), + /// }; + /// add_metadata_to_conditional_tokens(ctx, args)?; + /// ``` pub fn handle(ctx: Context, args: AddMetadataToConditionalTokensArgs) -> Result<()> { + // Generate vault PDA seeds for signing metadata transactions let seeds = generate_vault_seeds!(ctx.accounts.vault); let signer_seeds = &[&seeds[..]]; + // Prepare CPI to token metadata program let cpi_program = ctx.accounts.token_metadata_program.to_account_info(); + // Setup accounts for metadata creation let cpi_accounts = CreateMetadataAccountsV3 { metadata: ctx.accounts.conditional_token_metadata.to_account_info(), mint: ctx.accounts.conditional_token_mint.to_account_info(), @@ -69,24 +135,27 @@ impl AddMetadataToConditionalTokens<'_> { rent: ctx.accounts.rent.to_account_info(), }; + // Create metadata account with provided details create_metadata_accounts_v3( CpiContext::new(cpi_program, cpi_accounts).with_signer(signer_seeds), DataV2 { name: args.name.clone(), symbol: args.symbol.clone(), uri: args.uri.clone(), - seller_fee_basis_points: 0, - creators: None, - collection: None, - uses: None, + seller_fee_basis_points: 0, // No fees for conditional tokens + creators: None, // No creator royalties + collection: None, // Not part of a collection + uses: None, // No uses metadata }, - false, - true, - None, + false, // Is mutable + true, // Update authority is signer + None, // Collection details )?; + // Increment vault sequence number for tracking ctx.accounts.vault.seq_num += 1; + // Emit event for indexing and tracking let clock = Clock::get()?; emit_cpi!(AddMetadataToConditionalTokensEvent { common: CommonFields { diff --git a/programs/conditional_vault/src/instructions/common.rs b/programs/conditional_vault/src/instructions/common.rs index 330d1f8b..4f86bfbf 100644 --- a/programs/conditional_vault/src/instructions/common.rs +++ b/programs/conditional_vault/src/instructions/common.rs @@ -1,39 +1,134 @@ use super::*; +/// Account context for interacting with a conditional vault. +/// This structure validates and processes all accounts needed for vault operations +/// such as minting, burning, or settling conditional tokens. #[event_cpi] #[derive(Accounts)] pub struct InteractWithVault<'info> { + /// The non-mutable question account associated with this vault + /// Contains outcome information and oracle details pub question: Account<'info, Question>, + + /// The vault account being interacted with + /// Must be associated with the provided question account + /// Marked as mutable to allow updating vault state #[account(mut, has_one = question)] pub vault: Account<'info, ConditionalVault>, + + /// The mint account for the underlying token used in this vault. + /// This account is critical for token validation and decimal precision checking + /// during transfer operations. + /// + /// # Validation + /// - Must match the mint stored in vault account (`vault.underlying_token_mint`) + /// - Must be a valid SPL token mint account + /// - Used to verify decimal precision in token operations + /// + /// # Security Considerations + /// 1. Address validation ensures we're using the correct underlying token + /// 2. Prevents token substitution attacks + /// 3. Enables decimal verification in transfer_checked operations + /// 4. Guards against incorrect token account associations + /// + /// # Usage in Operations + /// - Transfer validation: Used in transfer_checked for decimal verification + /// - Decimal consistency: Ensures all operations maintain correct precision + /// - Token validation: Verifies token account relationships + /// + /// # Example + /// ```ignore + /// token::transfer_checked( + /// ctx, + /// amount, + /// vault_underlying_token_mint.decimals, // Decimal verification + /// )?; + /// ``` + /// + /// # Error Scenarios + /// Returns `VaultError::UnderlyingTokenMintMismatch` when: + /// - Provided mint doesn't match vault's recorded mint + /// - Incorrect mint account is supplied + /// - Mint address validation fails + #[account(address = vault.underlying_token_mint @ VaultError::InvalidVaultUnderlyingTokenMint)] + pub vault_underlying_token_mint: Account<'info, Mint>, + + /// The vault's underlying token account + /// Holds the collateral tokens backing the conditional tokens + /// Must match the account stored in the vault's state #[account( mut, constraint = vault_underlying_token_account.key() == vault.underlying_token_account @ VaultError::InvalidVaultUnderlyingTokenAccount )] pub vault_underlying_token_account: Account<'info, TokenAccount>, + + /// The authority (user) initiating the interaction + /// Must sign the transaction to authorize token transfers pub authority: Signer<'info>, + + /// The user's token account for the underlying token + /// Used for depositing/withdrawing underlying tokens/collateral + /// Must be owned by the authority and match the vault's underlying token mint #[account( mut, token::authority = authority, token::mint = vault.underlying_token_mint )] pub user_underlying_token_account: Account<'info, TokenAccount>, + + /// The SPL Token program account + /// Required for token operations pub token_program: Program<'info, Token>, } impl<'info, 'c: 'info> InteractWithVault<'info> { + /// Retrieves and validates conditional token mints and their corresponding user accounts + /// from the remaining accounts in the transaction. + /// + /// # Arguments + /// * `ctx` - The context containing all transaction accounts including remaining accounts + /// + /// # Returns + /// * `Result<(Vec>, Vec>)>`: + /// - First vector contains validated conditional token mint accounts + /// - Second vector contains validated user token accounts for each conditional token + /// + /// # Validation Steps + /// 1. Verifies correct number of remaining accounts (2 accounts per outcome) + /// 2. Validates conditional token mint addresses match vault's records + /// 3. Ensures all mint accounts are valid + /// 4. Verifies user token accounts correspond to correct mints + /// + /// # Errors + /// * `VaultError::InvalidConditionals` - Incorrect number of remaining accounts + /// * `VaultError::ConditionalMintMismatch` - Mint address doesn't match vault records + /// * `VaultError::BadConditionalMint` - Invalid mint account + /// * `VaultError::BadConditionalTokenAccount` - Invalid token account + /// * `VaultError::ConditionalTokenMintMismatch` - Token account mint mismatch + /// + /// # Example + /// ```ignore + /// let (mints, user_accounts) = InteractWithVault::get_mints_and_user_token_accounts(&ctx)?; + /// // mints[0] is the mint for first outcome + /// // user_accounts[0] is user's token account for first outcome + /// ``` pub fn get_mints_and_user_token_accounts( ctx: &Context<'_, '_, 'c, 'info, Self>, ) -> Result<(Vec>, Vec>)> { + // Get iterator over remaining accounts let remaining_accs = &mut ctx.remaining_accounts.iter(); + // Calculate expected number of accounts based on outcomes let expected_num_conditional_tokens = ctx.accounts.question.num_outcomes(); + + // Verify we have correct number of accounts (2 per outcome: mint + token account) require_eq!( remaining_accs.len(), expected_num_conditional_tokens * 2, VaultError::InvalidConditionals ); + // Initialize vectors to store validated accounts let mut conditional_token_mints = vec![]; let mut user_conditional_token_accounts = vec![]; @@ -45,20 +140,25 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { VaultError::ConditionalMintMismatch ); - // really, this should never fail because we initialize mints when we initialize the vault + // Validate and convert to Mint account + // Note: This should never fail as mints are initialized with vault conditional_token_mints.push( Account::::try_from(conditional_token_mint) .or(Err(VaultError::BadConditionalMint))?, ); } + // Second pass: validate and collect all user token accounts for i in 0..expected_num_conditional_tokens { + // Get next account info let user_conditional_token_account = next_account_info(remaining_accs)?; + // Validate and convert to TokenAccount let user_conditional_token_account = Account::::try_from(user_conditional_token_account) .or(Err(VaultError::BadConditionalTokenAccount))?; + // Verify token account's mint matches corresponding conditional token mint require_eq!( user_conditional_token_account.mint, conditional_token_mints[i].key(), diff --git a/programs/conditional_vault/src/instructions/initialize_conditional_vault.rs b/programs/conditional_vault/src/instructions/initialize_conditional_vault.rs index 325ba5b6..2b4accab 100644 --- a/programs/conditional_vault/src/instructions/initialize_conditional_vault.rs +++ b/programs/conditional_vault/src/instructions/initialize_conditional_vault.rs @@ -3,9 +3,32 @@ use super::*; use anchor_lang::system_program; use anchor_spl::token; +/// Structure defining the accounts required for initializing a conditional vault. +/// This represents the core setup for creating a vault that can manage conditional tokens +/// based on a specific question's outcomes. + #[event_cpi] #[derive(Accounts)] pub struct InitializeConditionalVault<'info> { + /// The vault account that will be initialized and serve as the central + /// authority for this prediction market instance. + /// + /// # PDA Derivation + /// Seeds: + /// - PDA seeds: ["conditional_vault", question key, underlying token mint key] + /// - PREFIX: "conditional_vault" (static identifier) + /// - question.key(): Links vault to specific question + /// - underlying_token_mint.key(): Associates with underlying token type/ collateral type + /// + /// # Space Allocation + /// - 8 bytes: Discriminator + /// - ConditionalVault size: Core struct data + /// - Dynamic size: 32 bytes per outcome for mint addresses + /// + /// # Authorization + /// - PDA acts as authority for all conditional token mints + /// - Controls underlying token account + /// - Manages token minting and burning operations #[account( init, payer = payer, @@ -18,43 +41,184 @@ pub struct InitializeConditionalVault<'info> { bump )] pub vault: Box>, + + /// The question account defining the prediction market parameters. + /// + /// # Requirements + /// - Must be initialized before vault creation + /// - Determines number of conditional tokens + /// - Defines resolution criteria + /// + /// # Validation + /// - Account must exist and be valid + /// - Number of outcomes must be valid (≥2) + /// - Cannot be modified after vault creation pub question: Account<'info, Question>, + + /// The mint account for the underlying token used as collateral. + /// This is the token type that will be used for deposits and redemptions + /// # Characteristics + /// - Determines decimal precision for all conditional tokens + /// - Must be a valid SPL token mint + /// - Can be any fungible token + /// + /// # Implications + /// - All conditional tokens will share same decimal precision + /// - Affects minimum transaction amounts + /// - Influences precision of payouts pub underlying_token_mint: Account<'info, Mint>, + + /// Associated Token Account for the vault's underlying tokens (collateral). + /// + /// # Terminology + /// - Underlying tokens = Collateral tokens + /// Examples: + /// - USDC as underlying/collateral for a USDC-backed prediction market + /// - SOL as underlying/collateral for a SOL-backed prediction market + /// + /// # Configuration + /// - Authority: Vault PDA (derived from question and underlying_token_mint) + /// - Mint: Must match underlying_token_mint + /// + /// # Account Properties + /// - Associated Token Account (ATA) derived deterministically + /// - Stores the underlying tokens (collateral) that back conditional tokens + /// - Balance must satisfy invariant checks + /// + /// # Usage Flow + /// 1. Split Operation: + /// - User deposits underlying tokens (collateral) + /// - Vault mints conditional tokens + /// + /// 2. Merge Operation: + /// - User burns equal amounts of all conditional tokens + /// - Vault returns underlying tokens (collateral) + /// + /// 3. Redemption (after resolution): + /// - User burns conditional tokens + /// - Vault pays out underlying tokens based on resolution ratios + /// + /// # Invariant Requirements + /// For unresolved questions: + /// - Collateral must cover the highest conditional token supply + /// Example: If YES=1000, NO=2000, collateral must be ≥ 2000 + /// + /// For resolved questions: + /// - Collateral must cover weighted sum of all payouts + /// Example: If supplies=[1000,2000] with 60/40 split + /// Required collateral = (1000×0.6) + (2000×0.4) = 1400 + /// + /// # Security Considerations + /// - Only vault PDA can authorize transfers + /// - Collateral balance checked before every operation + /// - Cannot be closed while conditional tokens exist + /// - Must maintain solvency ratio at all times #[account( associated_token::authority = vault, associated_token::mint = underlying_token_mint )] pub vault_underlying_token_account: Box>, + + /// Account that will pay for the initialization costs + /// + /// # Responsibilities + /// - Funds vault account creation + /// - Pays for conditional token mint accounts + /// - Covers rent exemption costs + /// + /// # Requirements + /// - Must be a signer + /// - Must have sufficient SOL for all operations #[account(mut)] pub payer: Signer<'info>, + + /// SPL Token program account for token operations. + /// Required for: + /// - Mint initialization + /// - Token account creation + /// - Transfer operations pub token_program: Program<'info, Token>, + + /// Associated Token Program for ATA operations. + /// Used for: + /// - ATA validation + /// - ATA creation (if needed) + /// - ATA constraint checking pub associated_token_program: Program<'info, AssociatedToken>, + + /// System Program for account operations. + /// Required for: + /// - Account creation + /// - Lamport transfers + /// - Space allocation pub system_program: Program<'info, System>, } impl<'info, 'c: 'info> InitializeConditionalVault<'info> { + /// Handles the initialization of a new conditional vault in the prediction market system. + /// This is a complex, multi-step process that sets up all necessary components for + /// a functioning prediction market vault. + /// + /// # Initialization Process Overview + /// 1. Vault Account Setup + /// 2. Conditional Token Mint Creation + /// 3. State Initialization + /// 4. Event Emission + /// + /// # Security Considerations + /// - All PDAs are verified for correctness + /// - Rent exemption is guaranteed for all accounts + /// - Authority relationships are properly established + /// - No gaps in sequence numbers + /// + /// # Error Handling + /// - Account validation errors + /// - Insufficient funds errors + /// - Space allocation errors + /// - CPI (Cross-Program Invocation) errors + /// + /// # Returns + /// * `Ok(())` if initialization succeeds + /// * `Err(...)` if any step fails pub fn handle(ctx: Context<'_, '_, 'c, 'info, Self>) -> Result<()> { + // Get mutable reference to the vault account being initialized let vault = &mut ctx.accounts.vault; + // Get the decimals from the underlying token mint + // This will be used for all conditional token mints let decimals = ctx.accounts.underlying_token_mint.decimals; + // Setup iterator for remaining accounts (conditional token mints) let remaining_accs = &mut ctx.remaining_accounts.iter(); + // Get the number of outcomes from the question account + // This determines how many conditional tokens we need to create let expected_num_conditional_tokens = ctx.accounts.question.num_outcomes(); + + // Initialize vector to store conditional token mint addresses let mut conditional_token_mints = vec![]; + // Calculate minimum lamports needed for rent exemption of a mint account + // This ensures accounts won't be purged let mint_lamports = Rent::get()?.minimum_balance(Mint::LEN); + + // Create a mint for each possible outcome for i in 0..expected_num_conditional_tokens { + // Generate PDA for the conditional token mint + // Seeds: ["conditional_token", vault key, outcome index] let (conditional_token_mint_address, pda_bump) = Pubkey::find_program_address( &[b"conditional_token", vault.key().as_ref(), &[i as u8]], ctx.program_id, ); + // Get the next account from remaining accounts and verify it matches expected PDA let conditional_token_mint = next_account_info(remaining_accs)?; require_eq!(conditional_token_mint.key(), conditional_token_mint_address); + // Store the mint address in our vector conditional_token_mints.push(conditional_token_mint_address); + // Transfer lamports to the mint account for rent exemption let cpi_accounts = system_program::Transfer { from: ctx.accounts.payer.to_account_info(), to: conditional_token_mint.to_account_info(), @@ -63,6 +227,7 @@ impl<'info, 'c: 'info> InitializeConditionalVault<'info> { CpiContext::new(ctx.accounts.system_program.to_account_info(), cpi_accounts); system_program::transfer(cpi_ctx, mint_lamports)?; + // Setup PDA signer seeds for the mint account let vault_key = vault.key(); let seeds = &[ b"conditional_token", @@ -72,6 +237,7 @@ impl<'info, 'c: 'info> InitializeConditionalVault<'info> { ]; let signer = &[&seeds[..]]; + // Allocate space for the mint account let cpi_accounts = system_program::Allocate { account_to_allocate: conditional_token_mint.to_account_info(), }; @@ -79,6 +245,7 @@ impl<'info, 'c: 'info> InitializeConditionalVault<'info> { CpiContext::new(ctx.accounts.system_program.to_account_info(), cpi_accounts); system_program::allocate(cpi_ctx.with_signer(signer), Mint::LEN as u64)?; + // Assign the mint account to the token program let cpi_accounts = system_program::Assign { account_to_assign: conditional_token_mint.to_account_info(), }; @@ -86,15 +253,40 @@ impl<'info, 'c: 'info> InitializeConditionalVault<'info> { CpiContext::new(ctx.accounts.system_program.to_account_info(), cpi_accounts); system_program::assign(cpi_ctx.with_signer(signer), ctx.accounts.token_program.key)?; + // Initialize the mint account with proper decimals and authority let cpi_program = ctx.accounts.token_program.to_account_info(); let cpi_accounts = token::InitializeMint2 { mint: conditional_token_mint.to_account_info(), }; let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); - token::initialize_mint2(cpi_ctx, decimals, &vault.key(), None)?; + // When initializing conditional token mints, we explicitly set no freeze authority + // to improve user experience in wallets like Phantom. Without this, users would see + // a warning message about tokens being freezable. + + // Technical Details + // - Mint authority: Set to vault PDA (required for minting/burning) + // - Freeze authority: Set to None (prevents freezing capability) + // - Decimals: Matched with underlying token + + // User Experience Impact + // - Removes "asset can be frozen" warning in Phantom wallet + // - Increases user trust in conditional tokens + // - Maintains all necessary vault functionality + + // Security Considerations + // - Vault retains mint/burn authority (required for core functionality) + // - No capability to freeze tokens (improves user trust) + // - No impact on vault solvency guarantees + token::initialize_mint2( + cpi_ctx, + decimals, + &vault.key(), // mint authority = vault + None, // freeze authority = none + )?; } + // Initialize the vault account with all collected data vault.set_inner(ConditionalVault { question: ctx.accounts.question.key(), underlying_token_mint: ctx.accounts.underlying_token_mint.key(), @@ -105,6 +297,7 @@ impl<'info, 'c: 'info> InitializeConditionalVault<'info> { seq_num: 0, }); + // Emit initialization event with timestamp and vault details let clock = Clock::get()?; emit_cpi!(InitializeConditionalVaultEvent { common: CommonFields { diff --git a/programs/conditional_vault/src/instructions/initialize_question.rs b/programs/conditional_vault/src/instructions/initialize_question.rs index a4bf82e4..26a57be2 100644 --- a/programs/conditional_vault/src/instructions/initialize_question.rs +++ b/programs/conditional_vault/src/instructions/initialize_question.rs @@ -1,16 +1,42 @@ use super::*; +/// Arguments required for initializing a new question in the prediction market. +/// This structure defines the core parameters that uniquely identify and configure +/// a question instance. #[derive(AnchorSerialize, AnchorDeserialize)] pub struct InitializeQuestionArgs { + /// A unique 32-byte identifier for the question + /// This can be derived from a hash of the question text or other unique parameters pub question_id: [u8; 32], + + /// The public key of the oracle authority + /// This account will have the exclusive permission to resolve the question + /// by setting the final outcome values pub oracle: Pubkey, + + /// The number of possible outcomes for this question + /// For example: + /// - Binary questions (Yes/No) would have 2 outcomes + /// - Non binary questions might have 3+ outcomes + /// Must be >= 2 to be valid pub num_outcomes: u8, } +/// Instruction context for initializing a new question in the prediction market system. +/// This structure validates and processes all accounts needed for question creation. #[event_cpi] #[derive(Accounts)] #[instruction(args: InitializeQuestionArgs)] pub struct InitializeQuestion<'info> { + /// The question account to be initialized + /// Space calculation breakdown: + /// - 8 bytes: Account discriminator + /// - 32 bytes: question_id ([u8; 32]) + /// - 32 bytes: oracle (Pubkey) + /// - 1 byte: padding + /// - 4 bytes: Vec prefix for payout_numerators + /// - (num_outcomes * 4) bytes: payout_numerators (Vec) + /// - 4 bytes: payout_denominator (u32) #[account( init, payer = payer, @@ -24,27 +50,65 @@ pub struct InitializeQuestion<'info> { bump )] pub question: Box>, + + /// The account that will pay for account initialization + /// Must be a signer and must have sufficient SOL to cover rent #[account(mut)] pub payer: Signer<'info>, + + /// The system program account + /// Required for account initialization pub system_program: Program<'info, System>, } impl InitializeQuestion<'_> { + /// Handles the initialization of a new question in the prediction market. + /// + /// # Arguments + /// + /// * `ctx` - The context containing all relevant accounts for initialization + /// * `args` - The initialization arguments containing question parameters + /// + /// # Steps + /// 1. Validates the number of outcomes is at least 2 + /// 2. Initializes the question account with default values + /// 3. Emits an event logging the question creation + /// + /// # Example + /// ```ignore + /// let args = InitializeQuestionArgs { + /// question_id: [0; 32], // Unique identifier + /// oracle: oracle_pubkey, // Oracle authority + /// num_outcomes: 2, // Binary question (Yes/No) + /// }; + /// initialize_question(ctx, args)?; + /// ``` + /// + /// # Errors + /// Returns `VaultError::InsufficientNumConditions` if num_outcomes < 2 pub fn handle(ctx: Context, args: InitializeQuestionArgs) -> Result<()> { + // Validate that there are at least 2 possible outcomes + // This ensures the question is meaningful and can be resolved require_gte!( args.num_outcomes, 2, VaultError::InsufficientNumConditions ); + // Get mutable reference to the question account let question = &mut ctx.accounts.question; + // Destructure the initialization arguments for clarity let InitializeQuestionArgs { question_id, oracle, num_outcomes, } = args; + + // Initialize the question account with default values: + // - payout_numerators initialized to zero for each outcome + // - payout_denominator set to 0 indicating unresolved state question.set_inner(Question { question_id, oracle, @@ -52,6 +116,8 @@ impl InitializeQuestion<'_> { payout_denominator: 0, }); + // Emit an event to log the question initialization + // This helps with indexing and tracking question creation let clock = Clock::get()?; emit_cpi!(InitializeQuestionEvent { common: CommonFields { diff --git a/programs/conditional_vault/src/instructions/merge_tokens.rs b/programs/conditional_vault/src/instructions/merge_tokens.rs index c82fa173..7afe149f 100644 --- a/programs/conditional_vault/src/instructions/merge_tokens.rs +++ b/programs/conditional_vault/src/instructions/merge_tokens.rs @@ -1,12 +1,37 @@ use super::*; impl<'info, 'c: 'info> InteractWithVault<'info> { + /// Handles the merging of conditional tokens back into underlying tokens. + /// This process burns equal amounts of all conditional tokens to reclaim the underlying token. + /// + /// # Lifecycle + /// 1. Validates all account relationships and token balances + /// 2. Burns conditional tokens from user accounts + /// 3. Transfers underlying tokens from vault to user + /// 4. Verifies all balance changes + /// 5. Ensures vault remains solvent + /// 6. Emits event with updated state + /// + /// # Arguments + /// * `ctx` - Context containing all required accounts and program state + /// * `amount` - Amount of each conditional token to merge/burn + /// + /// # Account Requirements + /// * All conditional token accounts must belong to user + /// * User must have sufficient balance in each conditional token + /// * Vault must have sufficient underlying tokens + /// + /// # Returns + /// * `Ok(())` on successful merge + /// * `Err(VaultError)` on validation or execution failure pub fn handle_merge_tokens(ctx: Context<'_, '_, 'c, 'info, Self>, amount: u64) -> Result<()> { let accs = &ctx.accounts; + // Get and validate all conditional token mints and user token accounts let (mut conditional_token_mints, mut user_conditional_token_accounts) = Self::get_mints_and_user_token_accounts(&ctx)?; + // Verify user has sufficient balance in all conditional tokens for conditional_token_account in user_conditional_token_accounts.iter() { require!( conditional_token_account.amount >= amount, @@ -16,9 +41,12 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { let vault = &accs.vault; + // Store initial balances for later verification let pre_user_underlying_balance = accs.user_underlying_token_account.amount; let pre_vault_underlying_balance = accs.vault_underlying_token_account.amount; + // Calculate expected balances after operation + // This helps verify the operation completed correctly let expected_future_balances: Vec = user_conditional_token_accounts .iter() .map(|account| account.amount - amount) @@ -28,9 +56,11 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { .map(|mint| mint.supply - amount) .collect(); + // Generate vault PDA seeds for signing transfers let seeds = generate_vault_seeds!(vault); let signer = &[&seeds[..]]; + // Burn equal amounts of each conditional token for (conditional_mint, user_conditional_token_account) in conditional_token_mints .iter() .zip(user_conditional_token_accounts.iter()) @@ -48,23 +78,44 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { )?; } + // Validate token accounts and decimals + require_eq!( + accs.user_underlying_token_account.mint, + accs.vault_underlying_token_mint.key(), + VaultError::UnderlyingTokenMintMismatch + ); + require_eq!( + accs.vault_underlying_token_account.mint, + accs.vault_underlying_token_mint.key(), + VaultError::UnderlyingTokenMintMismatch + ); + require_eq!( + accs.vault_underlying_token_mint.decimals, + accs.vault.decimals, + VaultError::AssertFailed + ); + // Transfer `amount` from vault to user - token::transfer( + token::transfer_checked( CpiContext::new_with_signer( accs.token_program.to_account_info(), - Transfer { + TransferChecked { from: accs.vault_underlying_token_account.to_account_info(), + mint: accs.vault_underlying_token_mint.to_account_info(), // Added mint account to: accs.user_underlying_token_account.to_account_info(), authority: accs.vault.to_account_info(), }, signer, ), amount, + accs.vault_underlying_token_mint.decimals, // Use decimals from mint account )?; + // Reload account states to verify changes ctx.accounts.user_underlying_token_account.reload()?; ctx.accounts.vault_underlying_token_account.reload()?; + // Verify underlying token balances changed correctly require_eq!( ctx.accounts.user_underlying_token_account.amount, pre_user_underlying_balance + amount, @@ -76,6 +127,7 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { VaultError::AssertFailed ); + // Verify all conditional token supplies decreased correctly for (mint, expected_supply) in conditional_token_mints .iter_mut() .zip(expected_future_supplies.iter()) @@ -84,6 +136,7 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { require_eq!(mint.supply, *expected_supply, VaultError::AssertFailed); } + // Verify all user conditional token balances decreased correctly for (account, expected_balance) in user_conditional_token_accounts .iter_mut() .zip(expected_future_balances.iter()) @@ -92,6 +145,7 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { require_eq!(account.amount, *expected_balance, VaultError::AssertFailed); } + // Verify vault remains solvent after operation ctx.accounts.vault.invariant( &ctx.accounts.question, conditional_token_mints @@ -101,8 +155,10 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { ctx.accounts.vault_underlying_token_account.amount, )?; + // Increment vault sequence number for tracking ctx.accounts.vault.seq_num += 1; + // Emit event with updated state emit_cpi!(MergeTokensEvent { common: CommonFields { slot: Clock::get()?.slot, diff --git a/programs/conditional_vault/src/instructions/redeem_tokens.rs b/programs/conditional_vault/src/instructions/redeem_tokens.rs index 2325cf6b..25c5e096 100644 --- a/programs/conditional_vault/src/instructions/redeem_tokens.rs +++ b/programs/conditional_vault/src/instructions/redeem_tokens.rs @@ -1,6 +1,12 @@ use super::*; impl<'info, 'c: 'info> InteractWithVault<'info> { + /// Validates whether conditional tokens can be redeemed + /// This check ensures the question has been resolved before allowing redemption + /// + /// # Returns + /// * `Ok(())` if the question is resolved + /// * `Err(VaultError::CantRedeemConditionalTokens)` if question isn't resolved pub fn validate_redeem_tokens(&self) -> Result<()> { require!( self.question.is_resolved(), @@ -10,14 +16,37 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { Ok(()) } + /// Handles the redemption of conditional tokens after a question is resolved. + /// Users can redeem their conditional tokens for underlying tokens based on + /// the outcome payouts defined in the question. + /// + /// # Mathematical Model + /// For each conditional token type i: + /// redeemable_amount_i = user_balance_i * payout_numerator_i / payout_denominator + /// total_redeemable = sum(redeemable_amount_i) + /// + /// # Lifecycle + /// 1. Validates all accounts and balances + /// 2. Calculates redemption amounts based on payouts + /// 3. Burns all conditional tokens + /// 4. Transfers underlying tokens to user + /// 5. Verifies all state changes + /// 6. Ensures vault remains solvent + /// + /// # Arguments + /// * `ctx` - Context containing all required accounts and program state + /// + /// # Returns + /// * `Ok(())` on successful redemption + /// * `Err(VaultError)` on validation or execution failure pub fn handle_redeem_tokens(ctx: Context<'_, '_, 'c, 'info, Self>) -> Result<()> { let accs = &ctx.accounts; + // Get and validate all conditional token mints and user token accounts let (mut conditional_token_mints, mut user_conditional_token_accounts) = Self::get_mints_and_user_token_accounts(&ctx)?; - // calculate the expected future supplies of the conditional token mints - // as current supply - user balance + // Calculate expected future supplies after burning all user tokens let expected_future_supplies: Vec = conditional_token_mints .iter() .zip(user_conditional_token_accounts.iter()) @@ -27,12 +56,16 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { let vault = &accs.vault; let question = &accs.question; + // Generate vault PDA seeds for signing transfers let seeds = generate_vault_seeds!(vault); let signer = &[&seeds[..]]; + // Store initial balances for verification let user_underlying_balance_before = accs.user_underlying_token_account.amount; let vault_underlying_balance_before = accs.vault_underlying_token_account.amount; - // safe because there is always at least two conditional tokens and thus + + // Find maximum amount user could receive (used for safety check) + // Safe because we validate there are at least two conditional tokens and thus // at least two user conditional token accounts let max_redeemable = user_conditional_token_accounts .iter() @@ -40,23 +73,28 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { .max() .unwrap(); + // Calculate and execute redemption for each conditional token let mut total_redeemable = 0; for (conditional_mint, user_conditional_token_account) in conditional_token_mints .iter() .zip(user_conditional_token_accounts.iter()) { - // this is safe because we check that every conditional mint is a part of the vault + // Find index of this mint in vault's records for matching payout + // Safe because we validate all mints belong to vault let payout_index = vault .conditional_token_mints .iter() .position(|mint| mint == &conditional_mint.key()) .unwrap(); + // Calculate redeemable amount based on payouts + // Uses u128 for intermediate calculation to prevent overflow total_redeemable += ((user_conditional_token_account.amount as u128 * question.payout_numerators[payout_index] as u128) / question.payout_denominator as u128) as u64; + // Burn all conditional tokens of this type token::burn( CpiContext::new( accs.token_program.to_account_info(), @@ -70,41 +108,67 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { )?; } - token::transfer( + // Validate token accounts and decimals + require_eq!( + accs.user_underlying_token_account.mint, + accs.vault_underlying_token_mint.key(), + VaultError::UnderlyingTokenMintMismatch + ); + require_eq!( + accs.vault_underlying_token_account.mint, + accs.vault_underlying_token_mint.key(), + VaultError::UnderlyingTokenMintMismatch + ); + require_eq!( + accs.vault_underlying_token_mint.decimals, + accs.vault.decimals, + VaultError::AssertFailed + ); + + // Transfer redeemable underlying tokens to user + token::transfer_checked( CpiContext::new_with_signer( accs.token_program.to_account_info(), - Transfer { + TransferChecked { from: accs.vault_underlying_token_account.to_account_info(), + mint: accs.vault_underlying_token_mint.to_account_info(), // Added mint account to: accs.user_underlying_token_account.to_account_info(), authority: accs.vault.to_account_info(), }, signer, ), total_redeemable, + accs.vault_underlying_token_mint.decimals, // Use decimals from mint account )?; + // Safety check: Total redeemable should never exceed max possible payout require_gte!(max_redeemable, total_redeemable, VaultError::AssertFailed); + // Reload accounts and verify all balance changes ctx.accounts.user_underlying_token_account.reload()?; ctx.accounts.vault_underlying_token_account.reload()?; + // Verify user received correct amount require_eq!( ctx.accounts.user_underlying_token_account.amount, user_underlying_balance_before + total_redeemable, VaultError::AssertFailed ); + // Verify vault balance decreased correctly require_eq!( ctx.accounts.vault_underlying_token_account.amount, vault_underlying_balance_before - total_redeemable, VaultError::AssertFailed ); + // Verify all conditional tokens were burned for acc in user_conditional_token_accounts.iter_mut() { acc.reload()?; require_eq!(acc.amount, 0, VaultError::AssertFailed); } + // Verify all mint supplies decreased correctly for (mint, expected_supply) in conditional_token_mints .iter_mut() .zip(expected_future_supplies.iter()) @@ -113,6 +177,7 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { require_eq!(mint.supply, *expected_supply, VaultError::AssertFailed); } + // Verify vault remains solvent after redemption ctx.accounts.vault.invariant( &ctx.accounts.question, conditional_token_mints @@ -122,8 +187,10 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { ctx.accounts.vault_underlying_token_account.amount, )?; + // Increment vault sequence number ctx.accounts.vault.seq_num += 1; + // Emit event with updated state let clock = Clock::get()?; emit_cpi!(RedeemTokensEvent { common: CommonFields { diff --git a/programs/conditional_vault/src/instructions/resolve_question.rs b/programs/conditional_vault/src/instructions/resolve_question.rs index 9294f76a..c05111e3 100644 --- a/programs/conditional_vault/src/instructions/resolve_question.rs +++ b/programs/conditional_vault/src/instructions/resolve_question.rs @@ -1,36 +1,100 @@ use super::*; +/// Arguments required for resolving a question. +/// Contains the payout ratios for each possible outcome. #[derive(AnchorSerialize, AnchorDeserialize)] pub struct ResolveQuestionArgs { + /// Vector of payout numerators for each outcome + /// Length must match number of outcomes in question + /// Sum of numerators becomes the denominator + /// Example: [70, 30] for a 70/30 split pub payout_numerators: Vec, } + +/// Account context for resolving a question in the prediction market. +/// Only the designated oracle can resolve the question. #[event_cpi] #[derive(Accounts)] #[instruction(args: ResolveQuestionArgs)] pub struct ResolveQuestion<'info> { + /// The question account to be resolved + /// Must be mutable to update resolution state + /// Must be owned by the oracle attempting resolution #[account(mut, has_one = oracle)] pub question: Account<'info, Question>, + + /// The oracle account authorized to resolve this question + /// Must be a signer to prove authority pub oracle: Signer<'info>, } impl ResolveQuestion<'_> { + /// Handles the resolution of a question by setting final outcome payouts. + /// This is a critical operation that determines how conditional tokens + /// can be redeemed for underlying tokens. + /// + /// # Mathematical Model + /// For each outcome i: + /// payout_ratio_i = payout_numerator_i / sum(payout_numerators) + /// redemption_value = token_amount * payout_ratio + /// + /// # Arguments + /// * `ctx` - Context containing question and oracle accounts + /// * `args` - Resolution arguments containing payout numerators + /// + /// # Validation Steps + /// 1. Ensures question hasn't been previously resolved + /// 2. Validates number of payout numerators matches outcomes + /// 3. Verifies non-zero total payout + /// + /// # Example Resolutions + /// ```ignore + /// // Binary outcome (70/30 split): + /// ResolveQuestionArgs { + /// payout_numerators: vec![70, 30] // Denominator becomes 100 + /// } + /// + /// // Three-way split (50/30/20): + /// ResolveQuestionArgs { + /// payout_numerators: vec![50, 30, 20] // Denominator becomes 100 + /// } + /// + /// // Scalar outcome (partial correctness): + /// ResolveQuestionArgs { + /// payout_numerators: vec![75, 25] // 75% correct prediction + /// } + /// ``` + /// + /// # Errors + /// * `VaultError::QuestionAlreadyResolved` - Question was previously resolved + /// * `VaultError::InvalidNumPayoutNumerators` - Wrong number of payouts + /// * `VaultError::PayoutZero` - Sum of payouts is zero pub fn handle(ctx: Context, args: ResolveQuestionArgs) -> Result<()> { let question = &mut ctx.accounts.question; + // Verify question hasn't been resolved already + // payout_denominator == 0 indicates unresolved state require_eq!(question.payout_denominator, 0, VaultError::QuestionAlreadyResolved); + // Ensure number of payout numerators matches number of outcomes require_eq!( args.payout_numerators.len(), question.num_outcomes(), VaultError::InvalidNumPayoutNumerators ); + // Set resolution values: + // - denominator becomes sum of all numerators + // - store provided numerators question.payout_denominator = args.payout_numerators.iter().sum(); question.payout_numerators = args.payout_numerators.clone(); + // Ensure total payout is non-zero + // This prevents division by zero in redemption calculations require_gt!(question.payout_denominator, 0, VaultError::PayoutZero); + // Emit resolution event for indexing and tracking let clock = Clock::get()?; emit_cpi!(ResolveQuestionEvent { common: CommonFields { diff --git a/programs/conditional_vault/src/instructions/split_tokens.rs b/programs/conditional_vault/src/instructions/split_tokens.rs index c835feb0..a7b38338 100644 --- a/programs/conditional_vault/src/instructions/split_tokens.rs +++ b/programs/conditional_vault/src/instructions/split_tokens.rs @@ -1,44 +1,93 @@ use super::*; impl<'info, 'c: 'info> InteractWithVault<'info> { + /// Handles the splitting of underlying tokens into conditional tokens. + /// This operation creates equal amounts of each conditional token type + /// in exchange for underlying tokens, enabling participation in the prediction market. + /// + /// # Mathematical Model + /// For amount X of underlying tokens: + /// - Vault receives: X underlying tokens + /// - User receives: X tokens of each conditional outcome + /// - Conservation: Total value is preserved across split + /// + /// # Arguments + /// * `ctx` - Context containing all required accounts + /// * `amount` - Amount of underlying tokens to split into conditional tokens + /// + /// # Example + /// ```ignore + /// // Splitting 100 tokens in a binary outcome market: + /// // Input: 100 underlying tokens + /// // Output: 100 YES tokens AND 100 NO tokens + /// handle_split_tokens(ctx, 100)?; + /// ``` pub fn handle_split_tokens(ctx: Context<'_, '_, 'c, 'info, Self>, amount: u64) -> Result<()> { let accs = &ctx.accounts; + // Get and validate all conditional token mints and user token accounts let (mut conditional_token_mints, mut user_conditional_token_accounts) = Self::get_mints_and_user_token_accounts(&ctx)?; + // Record initial state for verification let pre_vault_underlying_balance = accs.vault_underlying_token_account.amount; + // Store initial balances of user's conditional tokens let pre_conditional_user_balances = user_conditional_token_accounts .iter() .map(|acc| acc.amount) .collect::>(); + // Store initial supplies of conditional tokens let pre_conditional_mint_supplies = conditional_token_mints .iter() .map(|mint| mint.supply) .collect::>(); + // Verify user has sufficient underlying tokens require!( accs.user_underlying_token_account.amount >= amount, VaultError::InsufficientUnderlyingTokens ); + // Generate vault PDA seeds for signing mint operations let vault = &accs.vault; let seeds = generate_vault_seeds!(vault); let signer = &[&seeds[..]]; - token::transfer( + // Validate token accounts and decimals + require_eq!( + accs.user_underlying_token_account.mint, + accs.vault_underlying_token_mint.key(), + VaultError::UnderlyingTokenMintMismatch + ); + require_eq!( + accs.vault_underlying_token_account.mint, + accs.vault_underlying_token_mint.key(), + VaultError::UnderlyingTokenMintMismatch + ); + require_eq!( + accs.vault_underlying_token_mint.decimals, + accs.vault.decimals, + VaultError::AssertFailed + ); + + // Transfer underlying tokens from user to vault with decimal verification + token::transfer_checked( CpiContext::new( accs.token_program.to_account_info(), - Transfer { + TransferChecked { from: accs.user_underlying_token_account.to_account_info(), + mint: accs.vault_underlying_token_mint.to_account_info(), to: accs.vault_underlying_token_account.to_account_info(), authority: accs.authority.to_account_info(), }, ), amount, + accs.vault_underlying_token_mint.decimals, )?; + // Mint conditional tokens to user + // Creates equal amounts of each type for (conditional_mint, user_conditional_token_account) in conditional_token_mints .iter() .zip(user_conditional_token_accounts.iter()) @@ -57,6 +106,9 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { )?; } + // === Verify State Changes === + + // Verify vault received underlying tokens ctx.accounts.vault_underlying_token_account.reload()?; require_eq!( ctx.accounts.vault_underlying_token_account.amount, @@ -64,16 +116,19 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { VaultError::AssertFailed ); + // Verify conditional token supplies increased correctly for (i, mint) in conditional_token_mints.iter_mut().enumerate() { mint.reload()?; require_eq!(mint.supply, pre_conditional_mint_supplies[i] + amount, VaultError::AssertFailed); } + // Verify user received correct amounts of conditional tokens for (i, acc) in user_conditional_token_accounts.iter_mut().enumerate() { acc.reload()?; require_eq!(acc.amount, pre_conditional_user_balances[i] + amount, VaultError::AssertFailed); } + // Verify vault remains solvent after split operation ctx.accounts.vault.invariant( &ctx.accounts.question, conditional_token_mints @@ -83,8 +138,10 @@ impl<'info, 'c: 'info> InteractWithVault<'info> { ctx.accounts.vault_underlying_token_account.amount, )?; + // Increment vault sequence number for tracking ctx.accounts.vault.seq_num += 1; + // Emit event with updated state let clock = Clock::get()?; emit_cpi!(SplitTokensEvent { common: CommonFields { diff --git a/programs/conditional_vault/src/lib.rs b/programs/conditional_vault/src/lib.rs index 521f85d1..3ef68a74 100644 --- a/programs/conditional_vault/src/lib.rs +++ b/programs/conditional_vault/src/lib.rs @@ -5,7 +5,7 @@ use anchor_spl::metadata::{ }; use anchor_spl::{ associated_token::AssociatedToken, - token::{self, Burn, Mint, MintTo, Token, TokenAccount, Transfer}, + token::{self, Burn, Mint, MintTo, Token, TokenAccount, TransferChecked}, }; pub mod error; diff --git a/programs/conditional_vault/src/state/conditional_vault.rs b/programs/conditional_vault/src/state/conditional_vault.rs index 58a0b1a1..dd4eab51 100644 --- a/programs/conditional_vault/src/state/conditional_vault.rs +++ b/programs/conditional_vault/src/state/conditional_vault.rs @@ -1,20 +1,70 @@ use super::*; +/// Represents the operational status of a conditional vault. +/// +/// # States +/// * `Active` - Normal operational state, allows token minting/burning +/// * `Finalized` - Completed state, no further modifications allowed +/// * `Reverted` - Cancelled state, indicates vault termination +/// +/// # State Transitions +/// ```text +/// Active → Finalized (After successful resolution) +/// Active → Reverted (In case of cancellation) +/// ``` +/// +/// # Usage +/// Used to control vault operations and permissions. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq)] pub enum VaultStatus { + /// Vault is operational Active, + /// Vault has been completed Finalized, + /// Vault has been reversed/cancelled Reverted, } +/// The structure that represents a conditional vault in the prediction market. +/// Manages the relationship between underlying tokens and conditional tokens +/// representing different outcomes. +/// +/// # Architecture +/// The vault serves as an escrow system that: +/// 1. Holds underlying tokens as collateral +/// 2. Issues conditional tokens for different outcomes +/// 3. Maintains solvency through invariant checks +/// 4. Handles redemptions after question resolution +/// +/// # Account Structure +/// * `question` - Link to the associated question account +/// * `underlying_token_mint` - The collateral token's mint +/// * `underlying_token_account` - Vault's underlying token account/ collateral storage +/// * `conditional_token_mints` - Outcome token mints +/// * `pda_bump` - For PDA derivation +/// * `decimals` - Precision level for all tokens +/// * `seq_num` - Operation sequence tracker +/// +/// # Security Model +/// * PDA-based authorization +/// * Invariant-based solvency protection +/// * Atomic operations +/// * Balance verification #[account] pub struct ConditionalVault { + /// Public key of the associated question pub question: Pubkey, + /// Public key of the main token mint pub underlying_token_mint: Pubkey, + /// Public key of the token account pub underlying_token_account: Pubkey, + /// Vector of public keys for conditional tokens pub conditional_token_mints: Vec, + /// Program Derived Address bump seed pub pda_bump: u8, + /// Token decimal places pub decimals: u8, + /// Sequence number pub seq_num: u64, } @@ -25,23 +75,64 @@ impl ConditionalVault { /// /// `conditional_token_supplies` should be in the same order as /// `vault.conditional_token_mints`. + /// + /// Typically, it validates the vault's solvency by ensuring assets cover potential liabilities. + /// This critical safety check prevents the vault from becoming insolvent. + /// + /// # Arguments + /// * `question` - Reference to associated question account + /// * `conditional_token_supplies` - Vector of current token supplies + /// * `vault_underlying_balance` - Current vault underlying token balance/ collateral balance + /// + /// # Behaviors + /// ## Unresolved Question State + /// * Maximum liability equals highest individual token supply + /// * Any outcome could potentially win with 100% payout + /// * Must cover worst-case scenario + /// + /// ## Resolved Question State + /// * Liability calculated using actual payout ratios + /// * Weighted sum of token supplies * payout ratios + /// * Must cover exact payout requirements + /// + /// # Examples + /// ## Unresolved Binary Market + /// ```ignore + /// // Market: "Will SOL > $50k?" + /// let supplies = vec![1000, 2000]; // [YES, NO] tokens + /// let balance = 2000; // Underlying tokens + /// + /// // Safe because balance covers max supply (2000) + /// vault.invariant(&question, supplies, balance)?; + /// ``` + /// + /// ## Resolved Three-Way Split + /// ```ignore + /// let supplies = vec![1000, 2000, 3000]; + /// let payouts = vec![30, 50, 20]; // 30%, 50%, 20% + /// let denominator = 100; + /// + /// // Liability calculation: + /// // (1000 * 0.3) + (2000 * 0.5) + (3000 * 0.2) = 1900 + /// ``` + /// + /// # Security Considerations + /// * Uses u128 for intermediate calculations to prevent overflow + /// * Checks performed before any mint/burn operations + /// * Maintains safety margin in unresolved state + /// * Prevents double-redemption through supply tracking pub fn invariant( &self, question: &Question, conditional_token_supplies: Vec, vault_underlying_balance: u64, ) -> Result<()> { - // if the question isn't resolved, the vault should have more underlying - // tokens than ANY conditional token mint's supply - - // if the question is resolved, the vault should have more underlying - // tokens than the sum of the conditional token mint's supplies multiplied - // by their respective payouts - let max_possible_liability = if !question.is_resolved() { + // Unresolved state: Use maximum supply as liability // safe because conditional_token_supplies is non-empty *conditional_token_supplies.iter().max().unwrap() } else { + // Resolved state: Calculate weighted sum of liabilities conditional_token_supplies .iter() .enumerate() @@ -52,12 +143,31 @@ impl ConditionalVault { .sum::() as u64 }; + // Ensures vault has enough underlying tokens to cover obligations require_gte!(vault_underlying_balance, max_possible_liability, VaultError::AssertFailed); Ok(()) } } +/// Generates deterministic seeds for vault PDA derivation. +/// +/// # Seeds Structure +/// 1. `b"conditional_vault"` - Program identifier +/// 2. `question.key()` - Links to specific question +/// 3. `underlying_token_mint.key()` - Links to token type +/// 4. `bump` - Ensures valid PDA +/// +/// # Usage +/// ```ignore +/// let seeds = generate_vault_seeds!(vault); +/// let signer_seeds = &[&seeds[..]]; +/// ``` +/// +/// # Security +/// * Deterministic generation prevents address collisions +/// * Incorporates all relevant identifiers +/// * Used for signing vault operations #[macro_export] macro_rules! generate_vault_seeds { ($vault:expr) => {{ diff --git a/programs/conditional_vault/src/state/question.rs b/programs/conditional_vault/src/state/question.rs index c5d68173..2db9e1c2 100644 --- a/programs/conditional_vault/src/state/question.rs +++ b/programs/conditional_vault/src/state/question.rs @@ -1,5 +1,6 @@ use super::*; +/// # Summary /// Questions represent statements about future events. /// /// These statements include: @@ -20,19 +21,259 @@ use super::*; /// outcome would resolve to 0.3. /// /// Once resolved, the sum of all outcome resolutions is exactly 1. + +/// # More Details +/// A Question represents a prediction market's core inquiry and its resolution parameters. +/// +/// # Overview +/// Questions are statements about future events that can resolve to multiple outcomes, +/// each with an associated payout weight. The payout system uses fractions to represent +/// outcome values, enabling precise calculations without floating-point arithmetic. +/// +/// # Structure +/// * `question_id`: [u8; 32] - Unique identifier for the question +/// * `oracle`: Pubkey - Authorized resolver's public key +/// * `payout_numerators`: Vec - Outcome weights (numerators for payout fractions) +/// * `payout_denominator`: u32 - Shared denominator for payout calculations +/// +/// # Payout Mechanics +/// Each outcome's value is calculated as: `payout_numerator / payout_denominator` +/// +/// ## Resolution States +/// * Unresolved: `payout_denominator = 0` +/// * Resolved: `payout_denominator > 0` +/// +/// ## Constraints +/// * Sum of all payouts must equal 1 (or denominator in fractional form) +/// * Minimum of 2 outcomes required +/// * All numerators must be less than or equal to denominator +/// +/// # Examples +/// +/// ## Binary Outcome (Yes/No) +/// ```ignore +/// let binary_question = Question { +/// question_id: [0; 32], +/// oracle: oracle_pubkey, +/// payout_numerators: vec![1, 0], // Yes = 1, No = 0, Therefore, Yes wins (100%) +/// payout_denominator: 1, +/// }; +/// ``` +/// The outcome is rigid, with only two options: full payout (100%) or no payout (0%). +/// +/// ## Percentage Split +/// ```ignore +/// let percentage_question = Question { +/// question_id: [0; 32], +/// oracle: oracle_pubkey, +/// payout_numerators: vec![70, 30], // 70/30 split +/// payout_denominator: 100, +/// }; +/// ``` +/// Calculations: +/// Outcome 1 value = 30/100 = 0.3 +/// Outcome 2 value = 70/100 = 0.7 +/// +/// The outcome weighting is flexible, allowing each outcome to represent a percentage of the payout. +/// +/// ## Multiple Outcomes +/// ```ignore +/// let multi_outcome = Question { +/// question_id: [0; 32], +/// oracle: oracle_pubkey, +/// payout_numerators: vec![50, 30, 20], // Three-way split +/// payout_denominator: 100, +/// }; +/// ``` +/// +/// # Real-World Applications +/// +/// ## Election Predictions +/// Question: "Who will win the election - Alice, Bob, or Carol?" +/// +/// ```ignore +/// let election = Question { +/// payout_numerators: vec![60, 40, 0], // Alice 60%, Bob 40%, Carol 0% +/// payout_denominator: 100, +/// // ... other fields ... +/// }; +/// ``` +/// Calculations: +/// Alice's outcome = 60/100 = 0.6 (60%) +/// Bob's outcome = 40/100 = 0.4 (40%) +/// Carol's outcome = 0/100 = 0 (0%) +/// +/// ## Grant Effectiveness +/// ```ignore +/// let grant = Question { +/// payout_numerators: vec![20, 45, 35], // Low/Medium/High effectiveness +/// payout_denominator: 100, +/// // ... other fields ... +/// }; +/// ``` +/// +/// # Liability Calculation +/// The question's payout structure determines potential liabilities in the vault system. +/// +/// ## Formula +/// For each outcome i: +/// ```text +/// liability_i = token_supply_i * (payout_numerator_i / payout_denominator) +/// total_liability = sum(liability_i) +/// ``` +/// +/// ## Example Calculation +/// Given: +/// * Token supplies: [1000, 2000, 3000] +/// * Payout numerators: [20, 30, 50] +/// * Payout denominator: 100 +/// +/// Liabilities: +/// ```text +/// 1000 * (20/100) = 200 +/// 2000 * (30/100) = 600 +/// 3000 * (50/100) = 1500 +/// Total = 2300 +/// ``` +/// +/// # Security Considerations +/// * Oracle has exclusive resolution authority +/// * Payout values are immutable after resolution +/// * Denominator of 0 prevents premature redemptions +/// * Integer arithmetic prevents rounding errors +/// +/// # Implementation Note +/// All calculations use integer arithmetic to maintain precision. +/// Intermediate calculations use u128 to prevent overflow. #[account] pub struct Question { - pub question_id: [u8; 32], - pub oracle: Pubkey, - pub payout_numerators: Vec, - pub payout_denominator: u32, + /// 32 byte unique identifier for the question + pub question_id: [u8; 32], + /// the public key of the oracle authorized to resolve the question + pub oracle: Pubkey, + /// vector of unsigned 32-bit integers (numerators) representing outcome weights + pub payout_numerators: Vec, + /// the unsigned 32-bit integer (denominator) used to calculate final outcome values + pub payout_denominator: u32, } +/// The payout system uses a fraction-based approach. +/// Each outcome's value = payout_numerators/payout_denominator +/// This allows for precise representation of decimal values without floating-point math. +/// For example, a 70% resolution might use `numerator = 70`, `denominator = 100`. +/// +/// Examples +/// +/// Definitive "Yes" (100%) or "No" (0%) payout: +/// Question: "Did the project meet its goals?" +/// ```ignore +/// let project_question = Question { +/// payout_numerators: vec![0, 1], // No = 0, Yes = 1 +/// payout_denominator: 1 +/// }; +/// ``` +/// +/// The outcome is rigid, with only two options: full payout (100%) or no payout (0%). +/// +/// Two Outcomes: +/// ```ignore +/// let question = Question { +/// question_id: [0; 32], +/// oracle: some_pubkey, +/// payout_numerators: vec![30, 70], +/// payout_denominator: 100 +/// }; +/// ``` +/// +/// Calculations: +/// Outcome 1 value = 30/100 = 0.3 +/// Outcome 2 value = 70/100 = 0.7 +/// +/// The outcome weighting is flexible, allowing each outcome to represent a percentage of the payout. +/// +/// Three Outcomes: +/// ```rust +/// let question = Question { +/// question_id: [0; 32], +/// oracle: some_pubkey, +/// payout_numerators: vec![50, 25, 25], // Three outcomes +/// payout_denominator: 100 +/// }; +/// ``` +/// +/// Calculations: +/// Outcome 1 value = 50/100 = 0.5 +/// Outcome 2 value = 25/100 = 0.25 +/// Outcome 3 value = 25/100 = 0.25 + +/// Real Scenarios: +/// Question: "Who will win the election - Alice, Bob, or Carol?" +/// Alice 60%, Bob 40%, Carol 0% +/// ```rust +/// let election_question = Question { +/// payout_numerators: vec![60, 40, 0], +/// payout_denominator: 100 +/// }; +/// ``` + +/// Calculations: +/// Alice's outcome = 60/100 = 0.6 (60%) +/// Bob's outcome = 40/100 = 0.4 (40%) +/// Carol's outcome = 0/100 = 0 (0%) + +/// Question: "How effective was this grant?" +/// ```rust +/// let grant_question = Question { +/// payout_numerators: vec![20, 45, 35], // Low, Medium, High effectiveness +/// payout_denominator: 100 +/// }; +/// ``` + +/// Calculations: +/// Low effectiveness = 20/100 = 0.2 (20%) +/// Medium effectiveness = 45/100 = 0.45 (45%) +/// High effectiveness = 35/100 = 0.35 (35%) + + impl Question { + /// Returns the number of possible outcomes for this question. + /// + /// # Returns + /// * `usize` - The number of outcomes, derived from payout_numerators length + /// + /// # Validation + /// The implementation ensures: + /// * Minimum of 2 outcomes + /// * Matches the number of conditional tokens in associated vaults pub fn num_outcomes(&self) -> usize { self.payout_numerators.len() } + /// Checks if the question has been resolved by the oracle. + /// + /// # Returns + /// * `bool` - true if resolved (denominator > 0), false otherwise + /// + /// # Resolution States + /// * Unresolved: `payout_denominator = 0` + /// * Resolved: `payout_denominator > 0` + /// + /// # Example + /// ```ignore + /// // Unresolved state + /// assert_eq!(Question { + /// payout_numerators: vec![0, 0], + /// payout_denominator: 0, + /// // ... other fields ... + /// }.is_resolved(), false); + /// + /// // Resolved state + /// assert_eq!(Question { + /// payout_numerators: vec![1, 0], + /// payout_denominator: 1, + /// // ... other fields ... + /// }.is_resolved(), true); + /// ``` pub fn is_resolved(&self) -> bool { self.payout_denominator != 0 }