From 4afb6e64d7d45dd1a9cf99dac83e886fd4ffe1ee Mon Sep 17 00:00:00 2001 From: Jon C Date: Thu, 21 Nov 2024 20:33:14 +0100 Subject: [PATCH] Add timestamp, rename to "multiplier" --- token/client/src/token.rs | 26 +-- .../tests/scaled_ui_amount.rs | 91 ++++++++--- .../extension/scaled_ui_amount/instruction.rs | 52 ++++-- .../src/extension/scaled_ui_amount/mod.rs | 148 +++++++++++++----- .../extension/scaled_ui_amount/processor.rs | 61 ++++++-- token/program-2022/src/processor.rs | 6 +- 6 files changed, 282 insertions(+), 102 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 960961af051..20cae87c1fe 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -191,7 +191,7 @@ pub enum ExtensionInitializationParams { }, ScaledUiAmountConfig { authority: Option, - scale: f64, + multiplier: f64, }, } impl ExtensionInitializationParams { @@ -322,9 +322,15 @@ impl ExtensionInitializationParams { authority, member_address, ), - Self::ScaledUiAmountConfig { authority, scale } => { - scaled_ui_amount::instruction::initialize(token_program_id, mint, authority, scale) - } + Self::ScaledUiAmountConfig { + authority, + multiplier, + } => scaled_ui_amount::instruction::initialize( + token_program_id, + mint, + authority, + multiplier, + ), } } } @@ -1814,23 +1820,25 @@ where .await } - /// Update scale - pub async fn update_scale( + /// Update multiplier + pub async fn update_multiplier( &self, authority: &Pubkey, - new_scale: f64, + new_multiplier: f64, + new_multiplier_effective_timestamp: i64, signing_keypairs: &S, ) -> TokenResult { let signing_pubkeys = signing_keypairs.pubkeys(); let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); self.process_ixs( - &[scaled_ui_amount::instruction::update_scale( + &[scaled_ui_amount::instruction::update_multiplier( &self.program_id, self.get_address(), authority, &multisig_signers, - new_scale, + new_multiplier, + new_multiplier_effective_timestamp, )?], signing_keypairs, ) diff --git a/token/program-2022-test/tests/scaled_ui_amount.rs b/token/program-2022-test/tests/scaled_ui_amount.rs index ad097fc6116..78fd4433735 100644 --- a/token/program-2022-test/tests/scaled_ui_amount.rs +++ b/token/program-2022-test/tests/scaled_ui_amount.rs @@ -33,7 +33,7 @@ use { #[tokio::test] async fn success_initialize() { - for (scale, authority) in [ + for (multiplier, authority) in [ (f64::MIN_POSITIVE, None), (f64::MAX, Some(Pubkey::new_unique())), ] { @@ -41,7 +41,7 @@ async fn success_initialize() { context .init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig { authority, - scale, + multiplier, }]) .await .unwrap(); @@ -50,19 +50,49 @@ async fn success_initialize() { let state = token.get_mint_info().await.unwrap(); let extension = state.get_extension::().unwrap(); assert_eq!(Option::::from(extension.authority), authority,); - assert_eq!(f64::from(extension.scale), scale); + assert_eq!(f64::from(extension.multiplier), multiplier); + assert_eq!(f64::from(extension.new_multiplier), multiplier); + assert_eq!(i64::from(extension.new_multiplier_effective_timestamp), 0); } } #[tokio::test] -async fn update_scale() { +async fn fail_initialize_with_interest_bearing() { + let authority = None; + let mut context = TestContext::new().await; + let err = context + .init_token_with_mint(vec![ + ExtensionInitializationParams::ScaledUiAmountConfig { + authority, + multiplier: 1.0, + }, + ExtensionInitializationParams::InterestBearingConfig { + rate_authority: None, + rate: 0, + }, + ]) + .await + .unwrap_err(); + assert_eq!( + err, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 3, + InstructionError::Custom(TokenError::InvalidExtensionCombination as u32) + ) + ))) + ); +} + +#[tokio::test] +async fn update_multiplier() { let authority = Keypair::new(); - let initial_scale = 5.0; + let initial_multiplier = 5.0; let mut context = TestContext::new().await; context .init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig { authority: Some(authority.pubkey()), - scale: initial_scale, + multiplier: initial_multiplier, }]) .await .unwrap(); @@ -70,22 +100,45 @@ async fn update_scale() { let state = token.get_mint_info().await.unwrap(); let extension = state.get_extension::().unwrap(); - assert_eq!(f64::from(extension.scale), initial_scale); + assert_eq!(f64::from(extension.multiplier), initial_multiplier); + assert_eq!(f64::from(extension.new_multiplier), initial_multiplier); // correct - let new_scale = 10.0; + let new_multiplier = 10.0; + token + .update_multiplier(&authority.pubkey(), new_multiplier, 0, &[&authority]) + .await + .unwrap(); + let state = token.get_mint_info().await.unwrap(); + let extension = state.get_extension::().unwrap(); + assert_eq!(f64::from(extension.multiplier), new_multiplier); + assert_eq!(f64::from(extension.new_multiplier), new_multiplier); + assert_eq!(i64::from(extension.new_multiplier_effective_timestamp), 0); + + // correct in the future + let newest_multiplier = 100.0; token - .update_scale(&authority.pubkey(), new_scale, &[&authority]) + .update_multiplier( + &authority.pubkey(), + newest_multiplier, + i64::MAX, + &[&authority], + ) .await .unwrap(); let state = token.get_mint_info().await.unwrap(); let extension = state.get_extension::().unwrap(); - assert_eq!(f64::from(extension.scale), new_scale); + assert_eq!(f64::from(extension.multiplier), new_multiplier); + assert_eq!(f64::from(extension.new_multiplier), newest_multiplier); + assert_eq!( + i64::from(extension.new_multiplier_effective_timestamp), + i64::MAX + ); // wrong signer let wrong_signer = Keypair::new(); let err = token - .update_scale(&wrong_signer.pubkey(), 1.0, &[&wrong_signer]) + .update_multiplier(&wrong_signer.pubkey(), 1.0, 0, &[&wrong_signer]) .await .unwrap_err(); assert_eq!( @@ -102,12 +155,12 @@ async fn update_scale() { #[tokio::test] async fn set_authority() { let authority = Keypair::new(); - let initial_scale = 500.0; + let initial_multiplier = 500.0; let mut context = TestContext::new().await; context .init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig { authority: Some(authority.pubkey()), - scale: initial_scale, + multiplier: initial_multiplier, }]) .await .unwrap(); @@ -132,11 +185,11 @@ async fn set_authority() { Some(new_authority.pubkey()).try_into().unwrap(), ); token - .update_scale(&new_authority.pubkey(), 10.0, &[&new_authority]) + .update_multiplier(&new_authority.pubkey(), 10.0, 0, &[&new_authority]) .await .unwrap(); let err = token - .update_scale(&authority.pubkey(), 100.0, &[&authority]) + .update_multiplier(&authority.pubkey(), 100.0, 0, &[&authority]) .await .unwrap_err(); assert_eq!( @@ -166,7 +219,7 @@ async fn set_authority() { // now all fail let err = token - .update_scale(&new_authority.pubkey(), 50.0, &[&new_authority]) + .update_multiplier(&new_authority.pubkey(), 50.0, 0, &[&new_authority]) .await .unwrap_err(); assert_eq!( @@ -179,7 +232,7 @@ async fn set_authority() { ))) ); let err = token - .update_scale(&authority.pubkey(), 5.5, &[&authority]) + .update_multiplier(&authority.pubkey(), 5.5, 0, &[&authority]) .await .unwrap_err(); assert_eq!( @@ -256,11 +309,11 @@ async fn amount_conversions() { context, token_context: None, }; - let initial_scale = 5.0; + let initial_multiplier = 5.0; context .init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig { authority: Some(authority.pubkey()), - scale: initial_scale, + multiplier: initial_multiplier, }]) .await .unwrap(); diff --git a/token/program-2022/src/extension/scaled_ui_amount/instruction.rs b/token/program-2022/src/extension/scaled_ui_amount/instruction.rs index c7e48db7f5c..9dc092714f9 100644 --- a/token/program-2022/src/extension/scaled_ui_amount/instruction.rs +++ b/token/program-2022/src/extension/scaled_ui_amount/instruction.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use { crate::{ check_program_account, - extension::scaled_ui_amount::PodF64, + extension::scaled_ui_amount::{PodF64, UnixTimestamp}, instruction::{encode_instruction, TokenInstruction}, }, bytemuck::{Pod, Zeroable}, @@ -41,23 +41,27 @@ pub enum ScaledUiAmountMintInstruction { /// Data expected by this instruction: /// `crate::extension::scaled_ui_amount::instruction::InitializeInstructionData` Initialize, - /// Update the scale. Only supported for mints that include the + /// Update the multiplier. Only supported for mints that include the /// `ScaledUiAmount` extension. /// + /// The authority provides a new multiplier and a unix timestamp on which + /// it should take effect. If the timestamp is before the current time, + /// immediately sets the multiplier. + /// /// Accounts expected by this instruction: /// /// * Single authority /// 0. `[writable]` The mint. - /// 1. `[signer]` The mint scale authority. + /// 1. `[signer]` The multiplier authority. /// /// * Multisignature authority /// 0. `[writable]` The mint. - /// 1. `[]` The mint's multisignature scale authority. + /// 1. `[]` The mint's multisignature multiplier authority. /// 2. `..2+M` `[signer]` M signer accounts. /// /// Data expected by this instruction: - /// `crate::extension::scaled_ui_amount::PodF64` - UpdateScale, + /// `crate::extension::scaled_ui_amount::instruction::UpdateMultiplierInstructionData` + UpdateMultiplier, } /// Data expected by `ScaledUiAmountMint::Initialize` @@ -66,10 +70,22 @@ pub enum ScaledUiAmountMintInstruction { #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct InitializeInstructionData { - /// The public key for the account that can update the scale + /// The public key for the account that can update the multiplier pub authority: OptionalNonZeroPubkey, - /// The initial scale - pub scale: PodF64, + /// The initial multiplier + pub multiplier: PodF64, +} + +/// Data expected by `ScaledUiAmountMint::UpdateMultiplier` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct UpdateMultiplierInstructionData { + /// The new multiplier + pub multiplier: PodF64, + /// Timestamp at which the new multiplier will take effect + pub effective_timestamp: UnixTimestamp, } /// Create an `Initialize` instruction @@ -77,7 +93,7 @@ pub fn initialize( token_program_id: &Pubkey, mint: &Pubkey, authority: Option, - scale: f64, + multiplier: f64, ) -> Result { check_program_account(token_program_id)?; let accounts = vec![AccountMeta::new(*mint, false)]; @@ -88,18 +104,19 @@ pub fn initialize( ScaledUiAmountMintInstruction::Initialize, &InitializeInstructionData { authority: authority.try_into()?, - scale: scale.into(), + multiplier: multiplier.into(), }, )) } -/// Create an `UpdateScale` instruction -pub fn update_scale( +/// Create an `UpdateMultiplier` instruction +pub fn update_multiplier( token_program_id: &Pubkey, mint: &Pubkey, authority: &Pubkey, signers: &[&Pubkey], - scale: f64, + multiplier: f64, + effective_timestamp: i64, ) -> Result { check_program_account(token_program_id)?; let mut accounts = vec![ @@ -113,7 +130,10 @@ pub fn update_scale( token_program_id, accounts, TokenInstruction::ScaledUiAmountExtension, - ScaledUiAmountMintInstruction::UpdateScale, - &PodF64::from(scale), + ScaledUiAmountMintInstruction::UpdateMultiplier, + &UpdateMultiplierInstructionData { + effective_timestamp: effective_timestamp.into(), + multiplier: multiplier.into(), + }, )) } diff --git a/token/program-2022/src/extension/scaled_ui_amount/mod.rs b/token/program-2022/src/extension/scaled_ui_amount/mod.rs index 5fe68327cce..3a6fb0bca38 100644 --- a/token/program-2022/src/extension/scaled_ui_amount/mod.rs +++ b/token/program-2022/src/extension/scaled_ui_amount/mod.rs @@ -4,7 +4,7 @@ use { crate::extension::{Extension, ExtensionType}, bytemuck::{Pod, Zeroable}, solana_program::program_error::ProgramError, - spl_pod::optional_keys::OptionalNonZeroPubkey, + spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodI64}, }; /// Scaled UI amount extension instructions @@ -13,6 +13,9 @@ pub mod instruction; /// Scaled UI amount extension processor pub mod processor; +/// `UnixTimestamp` expressed with an alignment-independent type +pub type UnixTimestamp = PodI64; + /// `f64` type that can be used in `Pod`s #[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde-traits", serde(from = "f64", into = "f64"))] @@ -44,17 +47,31 @@ pub struct ScaledUiAmountConfig { /// Authority that can set the scaling amount and authority pub authority: OptionalNonZeroPubkey, /// Amount to multiply raw amounts by, outside of the decimal - pub scale: PodF64, + pub multiplier: PodF64, + /// Unix timestamp at which `new_multiplier` comes into effective + pub new_multiplier_effective_timestamp: UnixTimestamp, + /// Next multiplier, once `new_multiplier_effective_timestamp` is reached + pub new_multiplier: PodF64, } impl ScaledUiAmountConfig { - fn total_scale(&self, decimals: u8) -> f64 { - f64::from(self.scale) / 10_f64.powi(decimals as i32) + fn total_multiplier(&self, decimals: u8, unix_timestamp: i64) -> f64 { + let multiplier = if unix_timestamp >= self.new_multiplier_effective_timestamp.into() { + self.new_multiplier + } else { + self.multiplier + }; + f64::from(multiplier) / 10_f64.powi(decimals as i32) } /// Convert a raw amount to its UI representation using the given decimals /// field Excess zeroes or unneeded decimal point are trimmed. - pub fn amount_to_ui_amount(&self, amount: u64, decimals: u8) -> Option { - let scaled_amount = (amount as f64) * self.total_scale(decimals); + pub fn amount_to_ui_amount( + &self, + amount: u64, + decimals: u8, + unix_timestamp: i64, + ) -> Option { + let scaled_amount = (amount as f64) * self.total_multiplier(decimals, unix_timestamp); Some(scaled_amount.to_string()) } @@ -64,11 +81,12 @@ impl ScaledUiAmountConfig { &self, ui_amount: &str, decimals: u8, + unix_timestamp: i64, ) -> Result { let scaled_amount = ui_amount .parse::() .map_err(|_| ProgramError::InvalidArgument)?; - let amount = scaled_amount / self.total_scale(decimals); + let amount = scaled_amount / self.total_multiplier(decimals, unix_timestamp); if amount > (u64::MAX as f64) || amount < (u64::MIN as f64) || amount.is_nan() { Err(ProgramError::InvalidArgument) } else { @@ -88,32 +106,62 @@ mod tests { const TEST_DECIMALS: u8 = 2; + #[test] + fn multiplier_choice() { + let multiplier = 5.0; + let new_multiplier = 10.0; + let new_multiplier_effective_timestamp = 1; + let config = ScaledUiAmountConfig { + authority: OptionalNonZeroPubkey::default(), + multiplier: PodF64::from(multiplier), + new_multiplier: PodF64::from(new_multiplier), + new_multiplier_effective_timestamp: UnixTimestamp::from( + new_multiplier_effective_timestamp, + ), + }; + assert_eq!( + config.total_multiplier(0, new_multiplier_effective_timestamp), + new_multiplier + ); + assert_eq!( + config.total_multiplier(0, new_multiplier_effective_timestamp - 1), + multiplier + ); + assert_eq!(config.total_multiplier(0, 0), multiplier); + assert_eq!(config.total_multiplier(0, i64::MIN), multiplier); + assert_eq!(config.total_multiplier(0, i64::MAX), new_multiplier); + } + #[test] fn specific_amount_to_ui_amount() { // 5x let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: PodF64::from(5.0), + multiplier: PodF64::from(5.0), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; - let ui_amount = config.amount_to_ui_amount(1, 0).unwrap(); + let ui_amount = config.amount_to_ui_amount(1, 0, 0).unwrap(); assert_eq!(ui_amount, "5"); // with 1 decimal place - let ui_amount = config.amount_to_ui_amount(1, 1).unwrap(); + let ui_amount = config.amount_to_ui_amount(1, 1, 0).unwrap(); assert_eq!(ui_amount, "0.5"); // with 10 decimal places - let ui_amount = config.amount_to_ui_amount(1, 10).unwrap(); + let ui_amount = config.amount_to_ui_amount(1, 10, 0).unwrap(); assert_eq!(ui_amount, "0.0000000005"); // huge amount with 10 decimal places - let ui_amount = config.amount_to_ui_amount(10_000_000_000, 10).unwrap(); + let ui_amount = config.amount_to_ui_amount(10_000_000_000, 10, 0).unwrap(); assert_eq!(ui_amount, "5"); // huge values let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: PodF64::from(f64::MAX), + multiplier: PodF64::from(f64::MAX), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; - let ui_amount = config.amount_to_ui_amount(u64::MAX, 0).unwrap(); + let ui_amount = config.amount_to_ui_amount(u64::MAX, 0, 0).unwrap(); assert_eq!(ui_amount, "inf"); } @@ -122,83 +170,97 @@ mod tests { // constant 5x let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: 5.0.into(), + multiplier: 5.0.into(), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; - let amount = config.try_ui_amount_into_amount("5.0", 0).unwrap(); + let amount = config.try_ui_amount_into_amount("5.0", 0, 0).unwrap(); assert_eq!(1, amount); // with 1 decimal place - let amount = config.try_ui_amount_into_amount("0.500000000", 1).unwrap(); + let amount = config + .try_ui_amount_into_amount("0.500000000", 1, 0) + .unwrap(); assert_eq!(amount, 1); // with 10 decimal places let amount = config - .try_ui_amount_into_amount("0.00000000050000000000000000", 10) + .try_ui_amount_into_amount("0.00000000050000000000000000", 10, 0) .unwrap(); assert_eq!(amount, 1); // huge amount with 10 decimal places let amount = config - .try_ui_amount_into_amount("5.0000000000000000", 10) + .try_ui_amount_into_amount("5.0000000000000000", 10, 0) .unwrap(); assert_eq!(amount, 10_000_000_000); // huge values let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: 5.0.into(), + multiplier: 5.0.into(), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; let amount = config - .try_ui_amount_into_amount("92233720368547758075", 0) + .try_ui_amount_into_amount("92233720368547758075", 0, 0) .unwrap(); assert_eq!(amount, u64::MAX); let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: f64::MAX.into(), + multiplier: f64::MAX.into(), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; // scientific notation "e" let amount = config - .try_ui_amount_into_amount("1.7976931348623157e308", 0) + .try_ui_amount_into_amount("1.7976931348623157e308", 0, 0) .unwrap(); assert_eq!(amount, 1); let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: 9.745314011399998e288.into(), + multiplier: 9.745314011399998e288.into(), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; let amount = config - .try_ui_amount_into_amount("1.7976931348623157e308", 0) + .try_ui_amount_into_amount("1.7976931348623157e308", 0, 0) .unwrap(); assert_eq!(amount, u64::MAX); // scientific notation "E" let amount = config - .try_ui_amount_into_amount("1.7976931348623157E308", 0) + .try_ui_amount_into_amount("1.7976931348623157E308", 0, 0) .unwrap(); assert_eq!(amount, u64::MAX); // this is unfortunate, but underflows can happen due to floats let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: 1.0.into(), + multiplier: 1.0.into(), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; assert_eq!( u64::MAX, config - .try_ui_amount_into_amount("18446744073709551616", 0) + .try_ui_amount_into_amount("18446744073709551616", 0, 0) .unwrap() // u64::MAX + 1 ); // overflow u64 fail let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: 0.1.into(), + multiplier: 0.1.into(), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; assert_eq!( Err(ProgramError::InvalidArgument), - config.try_ui_amount_into_amount("18446744073709551615", 0) // u64::MAX + 1 + config.try_ui_amount_into_amount("18446744073709551615", 0, 0) // u64::MAX + 1 ); for fail_ui_amount in ["-0.0000000000000000000001", "inf", "-inf", "NaN"] { assert_eq!( Err(ProgramError::InvalidArgument), - config.try_ui_amount_into_amount(fail_ui_amount, 0) + config.try_ui_amount_into_amount(fail_ui_amount, 0, 0) ); } } @@ -207,10 +269,14 @@ mod tests { fn specific_amount_to_ui_amount_no_scale() { let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: 1.0.into(), + multiplier: 1.0.into(), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; for (amount, expected) in [(23, "0.23"), (110, "1.1"), (4200, "42"), (0, "0")] { - let ui_amount = config.amount_to_ui_amount(amount, TEST_DECIMALS).unwrap(); + let ui_amount = config + .amount_to_ui_amount(amount, TEST_DECIMALS, 0) + .unwrap(); assert_eq!(ui_amount, expected); } } @@ -219,7 +285,9 @@ mod tests { fn specific_ui_amount_to_amount_no_scale() { let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: 1.0.into(), + multiplier: 1.0.into(), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; for (ui_amount, expected) in [ ("0.23", 23), @@ -233,14 +301,14 @@ mod tests { ("0", 0), ] { let amount = config - .try_ui_amount_into_amount(ui_amount, TEST_DECIMALS) + .try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, 0) .unwrap(); assert_eq!(expected, amount); } // this is invalid with normal mints, but rounding for this mint makes it ok let amount = config - .try_ui_amount_into_amount("0.111", TEST_DECIMALS) + .try_ui_amount_into_amount("0.111", TEST_DECIMALS, 0) .unwrap(); assert_eq!(11, amount); @@ -248,7 +316,7 @@ mod tests { for ui_amount in ["", ".", "0.t"] { assert_eq!( Err(ProgramError::InvalidArgument), - config.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS), + config.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, 0), ); } } @@ -262,9 +330,11 @@ mod tests { ) { let config = ScaledUiAmountConfig { authority: OptionalNonZeroPubkey::default(), - scale: scale.into(), + multiplier: scale.into(), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() }; - let ui_amount = config.amount_to_ui_amount(amount, decimals); + let ui_amount = config.amount_to_ui_amount(amount, decimals, 0); assert!(ui_amount.is_some()); } } diff --git a/token/program-2022/src/extension/scaled_ui_amount/processor.rs b/token/program-2022/src/extension/scaled_ui_amount/processor.rs index 5b04cf70096..1199a64c804 100644 --- a/token/program-2022/src/extension/scaled_ui_amount/processor.rs +++ b/token/program-2022/src/extension/scaled_ui_amount/processor.rs @@ -4,8 +4,11 @@ use { error::TokenError, extension::{ scaled_ui_amount::{ - instruction::{InitializeInstructionData, ScaledUiAmountMintInstruction}, - PodF64, ScaledUiAmountConfig, + instruction::{ + InitializeInstructionData, ScaledUiAmountMintInstruction, + UpdateMultiplierInstructionData, + }, + PodF64, ScaledUiAmountConfig, UnixTimestamp, }, BaseStateWithExtensionsMut, PodStateWithExtensionsMut, }, @@ -15,16 +18,18 @@ use { }, solana_program::{ account_info::{next_account_info, AccountInfo}, + clock::Clock, entrypoint::ProgramResult, msg, pubkey::Pubkey, + sysvar::Sysvar, }, spl_pod::optional_keys::OptionalNonZeroPubkey, }; -fn try_validate_scale(scale: &PodF64) -> ProgramResult { - let float_scale = f64::from(*scale); - if float_scale.is_sign_positive() && float_scale.is_normal() { +fn try_validate_multiplier(multiplier: &PodF64) -> ProgramResult { + let float_multiplier = f64::from(*multiplier); + if float_multiplier.is_sign_positive() && float_multiplier.is_normal() { Ok(()) } else { Err(TokenError::InvalidScale.into()) @@ -35,7 +40,7 @@ fn process_initialize( _program_id: &Pubkey, accounts: &[AccountInfo], authority: &OptionalNonZeroPubkey, - scale: &PodF64, + multiplier: &PodF64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let mint_account_info = next_account_info(account_info_iter)?; @@ -44,15 +49,18 @@ fn process_initialize( let extension = mint.init_extension::(true)?; extension.authority = *authority; - try_validate_scale(scale)?; - extension.scale = *scale; + try_validate_multiplier(multiplier)?; + extension.multiplier = *multiplier; + extension.new_multiplier_effective_timestamp = 0.into(); + extension.new_multiplier = *multiplier; Ok(()) } -fn process_update_scale( +fn process_update_multiplier( program_id: &Pubkey, accounts: &[AccountInfo], - new_scale: &PodF64, + new_multiplier: &PodF64, + effective_timestamp: &UnixTimestamp, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let mint_account_info = next_account_info(account_info_iter)?; @@ -73,8 +81,21 @@ fn process_update_scale( account_info_iter.as_slice(), )?; - try_validate_scale(new_scale)?; - extension.scale = *new_scale; + try_validate_multiplier(new_multiplier)?; + let clock = Clock::get()?; + extension.new_multiplier = *new_multiplier; + let int_effective_timestamp = i64::from(*effective_timestamp); + // just floor it to 0 + if int_effective_timestamp < 0 { + extension.new_multiplier_effective_timestamp = 0.into(); + } else { + extension.new_multiplier_effective_timestamp = *effective_timestamp; + } + // if the new effective timestamp has already passed, also set the old + // multiplier, just to be clear + if clock.unix_timestamp >= int_effective_timestamp { + extension.multiplier = *new_multiplier; + } Ok(()) } @@ -87,13 +108,19 @@ pub(crate) fn process_instruction( match decode_instruction_type(input)? { ScaledUiAmountMintInstruction::Initialize => { msg!("ScaledUiAmountMintInstruction::Initialize"); - let InitializeInstructionData { authority, scale } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, authority, scale) + let InitializeInstructionData { + authority, + multiplier, + } = decode_instruction_data(input)?; + process_initialize(program_id, accounts, authority, multiplier) } - ScaledUiAmountMintInstruction::UpdateScale => { + ScaledUiAmountMintInstruction::UpdateMultiplier => { msg!("ScaledUiAmountMintInstruction::UpdateScale"); - let new_scale = decode_instruction_data(input)?; - process_update_scale(program_id, accounts, new_scale) + let UpdateMultiplierInstructionData { + effective_timestamp, + multiplier, + } = decode_instruction_data(input)?; + process_update_multiplier(program_id, accounts, multiplier, effective_timestamp) } } } diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs index 5869708dff1..99a813e4775 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -1388,8 +1388,9 @@ impl Processor { .amount_to_ui_amount(amount, mint.base.decimals, unix_timestamp) .ok_or(ProgramError::InvalidArgument)? } else if let Ok(extension) = mint.get_extension::() { + let unix_timestamp = Clock::get()?.unix_timestamp; extension - .amount_to_ui_amount(amount, mint.base.decimals) + .amount_to_ui_amount(amount, mint.base.decimals, unix_timestamp) .ok_or(ProgramError::InvalidArgument)? } else { crate::amount_to_ui_amount_string_trimmed(amount, mint.base.decimals) @@ -1413,7 +1414,8 @@ impl Processor { let unix_timestamp = Clock::get()?.unix_timestamp; extension.try_ui_amount_into_amount(ui_amount, mint.base.decimals, unix_timestamp)? } else if let Ok(extension) = mint.get_extension::() { - extension.try_ui_amount_into_amount(ui_amount, mint.base.decimals)? + let unix_timestamp = Clock::get()?.unix_timestamp; + extension.try_ui_amount_into_amount(ui_amount, mint.base.decimals, unix_timestamp)? } else { crate::try_ui_amount_into_amount(ui_amount.to_string(), mint.base.decimals)? };