diff --git a/README.md b/README.md index 32ab827..d70fc5b 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ This Anchor program enables users to migrate their tokens from an old token mint - Node.js and Yarn - Rust and Cargo -- Solana CLI tools -- Anchor CLI +- Solana CLI (v2.3.0) tools +- Anchor CLI (v0.31.1) ### Installation diff --git a/programs/token-migrator/src/contexts/initialize.rs b/programs/token-migrator/src/contexts/initialize.rs index e187df3..831e6fa 100644 --- a/programs/token-migrator/src/contexts/initialize.rs +++ b/programs/token-migrator/src/contexts/initialize.rs @@ -6,11 +6,7 @@ use crate::state::{Strategy, Vault}; #[derive(Accounts)] #[instruction(mint_from: Pubkey, mint_to: Pubkey)] pub struct Initialize<'info> { - #[account( - mut, - // ℹ️ NOTE: Remove `address` constraint to make contract permissionless. - address = pubkey!("ELT1uRmtFvYP6WSrc4mCZaW7VVbcdkcKAj39aHSVCmwH") - )] + #[account(mut)] admin: Signer<'info>, // Ensure our vaults were initialized in preInstructions @@ -45,6 +41,8 @@ impl<'info> Initialize<'info> { strategy: Strategy, bump: [u8; 1], ) -> Result<()> { + strategy.validate()?; + self.vault.set_inner(Vault { admin: self.admin.key(), mint_from, diff --git a/programs/token-migrator/src/contexts/migrate.rs b/programs/token-migrator/src/contexts/migrate.rs index f627f15..8c4d09b 100644 --- a/programs/token-migrator/src/contexts/migrate.rs +++ b/programs/token-migrator/src/contexts/migrate.rs @@ -125,7 +125,7 @@ impl<'info> Migrate<'info> { /// # Supply From /// /// Calculates supply of `from` token to simulate a token burn. This is calculated by: - /// ``` + /// ```ignore /// let supply_from = mint_from.supply - vault_from_ata.amount /// ``` #[inline(always)] diff --git a/programs/token-migrator/src/errors.rs b/programs/token-migrator/src/errors.rs new file mode 100644 index 0000000..f6f8aa7 --- /dev/null +++ b/programs/token-migrator/src/errors.rs @@ -0,0 +1,7 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum MigratorError { + #[msg("Fixed strategy exponent out of range (|e| must be <= 19)")] + ExponentOutOfRange, +} diff --git a/programs/token-migrator/src/lib.rs b/programs/token-migrator/src/lib.rs index ab0b0c3..f4bea0b 100644 --- a/programs/token-migrator/src/lib.rs +++ b/programs/token-migrator/src/lib.rs @@ -9,6 +9,8 @@ pub use state::*; pub mod events; +pub mod errors; + #[cfg(not(feature = "no-entrypoint"))] use solana_security_txt::security_txt; diff --git a/programs/token-migrator/src/state.rs b/programs/token-migrator/src/state.rs index 4a5baf2..755d244 100644 --- a/programs/token-migrator/src/state.rs +++ b/programs/token-migrator/src/state.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; +use crate::errors::MigratorError; + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, InitSpace)] /// # Strategy /// @@ -12,7 +14,30 @@ pub enum Strategy { Fixed { e: i8 }, } +/// Maximum magnitude of the `Fixed` exponent. `10^19` is the largest power of +/// ten that fits in a `u64` (`10^20` overflows), so bounding `|e| <= 19` keeps +/// the `10u64.pow(|e|)` scaling in `withdraw_amount` from overflowing and rules +/// out the `i8::MIN` negation panic. +pub const MAX_FIXED_EXPONENT: u8 = 19; + impl Strategy { + /// # Validate + /// Ensures the strategy parameters are within safe bounds before the vault + /// is persisted. For `Fixed`, the exponent magnitude must be + /// `<= MAX_FIXED_EXPONENT` so the `10u64.pow(|e|)` scaling in + /// `withdraw_amount` cannot overflow. `ProRata` carries no parameters and is + /// always valid. + pub fn validate(&self) -> Result<()> { + if let Strategy::Fixed { e } = self { + require!( + e.unsigned_abs() <= MAX_FIXED_EXPONENT, + MigratorError::ExponentOutOfRange + ); + } + + Ok(()) + } + /// # Withdraw Amount /// Calculates the amount of tokens the user can withdraw based upon the `Strategy` implemented. #[inline(always)] @@ -94,4 +119,24 @@ mod tests { let strategy = Strategy::ProRata; assert_eq!(strategy.withdraw_amount(10, 100, 100).unwrap(), 10); } + + #[test] + fn test_validate_pro_rata_ok() { + assert!(Strategy::ProRata.validate().is_ok()); + } + + #[test] + fn test_validate_fixed_within_bounds_ok() { + assert!(Strategy::Fixed { e: 0 }.validate().is_ok()); + assert!(Strategy::Fixed { e: 19 }.validate().is_ok()); + assert!(Strategy::Fixed { e: -19 }.validate().is_ok()); + } + + #[test] + fn test_validate_fixed_out_of_bounds_err() { + assert!(Strategy::Fixed { e: 20 }.validate().is_err()); + assert!(Strategy::Fixed { e: -20 }.validate().is_err()); + assert!(Strategy::Fixed { e: i8::MAX }.validate().is_err()); + assert!(Strategy::Fixed { e: i8::MIN }.validate().is_err()); + } }