Skip to content

Commit

Permalink
token-2022: Add scaled amount extension
Browse files Browse the repository at this point in the history
#### Problem

The interest-bearing extension is useful for tokens that accrue in value
constantly, but many "rebasing" tokens on other blockchains employ a
different method of updating the number of tokens in accounts.

Rather than setting a rate and allowing the number to change
automatically over time, they set a scaling factor for the tokens by
hand.

#### Summary of changes

Add a new `ScaledUiAmount` extension to token-2022 for doing just that.
This is essentially a simplified version of the interest-bearing
extension, where someone just sets a scaling value into the mint
directly. The scale has no impact on the operation of the token, just on
the output of `amount_to_ui_amount` and `ui_amount_to_amount`.
  • Loading branch information
joncinque committed Nov 20, 2024
1 parent 0f54203 commit 772f34f
Show file tree
Hide file tree
Showing 10 changed files with 877 additions and 4 deletions.
36 changes: 34 additions & 2 deletions token/client/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ use {
ConfidentialTransferFeeConfig,
},
cpi_guard, default_account_state, group_member_pointer, group_pointer,
interest_bearing_mint, memo_transfer, metadata_pointer, transfer_fee, transfer_hook,
BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsOwned,
interest_bearing_mint, memo_transfer, metadata_pointer, scaled_ui_amount, transfer_fee,
transfer_hook, BaseStateWithExtensions, Extension, ExtensionType,
StateWithExtensionsOwned,
},
instruction, offchain,
solana_zk_sdk::{
Expand Down Expand Up @@ -188,6 +189,10 @@ pub enum ExtensionInitializationParams {
authority: Option<Pubkey>,
member_address: Option<Pubkey>,
},
ScaledUiAmountConfig {
authority: Option<Pubkey>,
scale: f64,
},
}
impl ExtensionInitializationParams {
/// Get the extension type associated with the init params
Expand All @@ -207,6 +212,7 @@ impl ExtensionInitializationParams {
}
Self::GroupPointer { .. } => ExtensionType::GroupPointer,
Self::GroupMemberPointer { .. } => ExtensionType::GroupMemberPointer,
Self::ScaledUiAmountConfig { .. } => ExtensionType::ScaledUiAmount,
}
}
/// Generate an appropriate initialization instruction for the given mint
Expand Down Expand Up @@ -316,6 +322,9 @@ impl ExtensionInitializationParams {
authority,
member_address,
),
Self::ScaledUiAmountConfig { authority, scale } => {
scaled_ui_amount::instruction::initialize(token_program_id, mint, authority, scale)
}
}
}
}
Expand Down Expand Up @@ -1805,6 +1814,29 @@ where
.await
}

/// Update scale
pub async fn update_scale<S: Signers>(
&self,
authority: &Pubkey,
new_scale: f64,
signing_keypairs: &S,
) -> TokenResult<T::Output> {
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(
&self.program_id,
self.get_address(),
authority,
&multisig_signers,
new_scale,
)?],
signing_keypairs,
)
.await
}

/// Update transfer hook program id
pub async fn update_transfer_hook_program_id<S: Signers>(
&self,
Expand Down
290 changes: 290 additions & 0 deletions token/program-2022-test/tests/scaled_ui_amount.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
#![cfg(feature = "test-sbf")]

mod program_test;
use {
program_test::{keypair_clone, TestContext, TokenContext},
solana_program_test::{
processor,
tokio::{self, sync::Mutex},
ProgramTest,
},
solana_sdk::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction, InstructionError},
msg,
program::{get_return_data, invoke},
program_error::ProgramError,
pubkey::Pubkey,
signature::Signer,
signer::keypair::Keypair,
transaction::{Transaction, TransactionError},
transport::TransportError,
},
spl_token_2022::{
error::TokenError,
extension::{scaled_ui_amount::ScaledUiAmountConfig, BaseStateWithExtensions},
instruction::{amount_to_ui_amount, ui_amount_to_amount, AuthorityType},
processor::Processor,
},
spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError},
std::{convert::TryInto, sync::Arc},
};

#[tokio::test]
async fn success_initialize() {
for (scale, authority) in [
(f64::MIN_POSITIVE, None),
(f64::MAX, Some(Pubkey::new_unique())),
] {
let mut context = TestContext::new().await;
context
.init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig {
authority,
scale,
}])
.await
.unwrap();
let TokenContext { token, .. } = context.token_context.unwrap();

let state = token.get_mint_info().await.unwrap();
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
assert_eq!(Option::<Pubkey>::from(extension.authority), authority,);
assert_eq!(f64::from(extension.scale), scale);
}
}

#[tokio::test]
async fn update_scale() {
let authority = Keypair::new();
let initial_scale = 5.0;
let mut context = TestContext::new().await;
context
.init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig {
authority: Some(authority.pubkey()),
scale: initial_scale,
}])
.await
.unwrap();
let TokenContext { token, .. } = context.token_context.take().unwrap();

let state = token.get_mint_info().await.unwrap();
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
assert_eq!(f64::from(extension.scale), initial_scale);

// correct
let new_scale = 10.0;
token
.update_scale(&authority.pubkey(), new_scale, &[&authority])
.await
.unwrap();
let state = token.get_mint_info().await.unwrap();
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
assert_eq!(f64::from(extension.scale), new_scale);

// wrong signer
let wrong_signer = Keypair::new();
let err = token
.update_scale(&wrong_signer.pubkey(), 1.0, &[&wrong_signer])
.await
.unwrap_err();
assert_eq!(
err,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
0,
InstructionError::Custom(TokenError::OwnerMismatch as u32)
)
)))
);
}

#[tokio::test]
async fn set_authority() {
let authority = Keypair::new();
let initial_scale = 500.0;
let mut context = TestContext::new().await;
context
.init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig {
authority: Some(authority.pubkey()),
scale: initial_scale,
}])
.await
.unwrap();
let TokenContext { token, .. } = context.token_context.take().unwrap();

// success
let new_authority = Keypair::new();
token
.set_authority(
token.get_address(),
&authority.pubkey(),
Some(&new_authority.pubkey()),
AuthorityType::ScaledUiAmount,
&[&authority],
)
.await
.unwrap();
let state = token.get_mint_info().await.unwrap();
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
assert_eq!(
extension.authority,
Some(new_authority.pubkey()).try_into().unwrap(),
);
token
.update_scale(&new_authority.pubkey(), 10.0, &[&new_authority])
.await
.unwrap();
let err = token
.update_scale(&authority.pubkey(), 100.0, &[&authority])
.await
.unwrap_err();
assert_eq!(
err,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
0,
InstructionError::Custom(TokenError::OwnerMismatch as u32)
)
)))
);

// set to none
token
.set_authority(
token.get_address(),
&new_authority.pubkey(),
None,
AuthorityType::ScaledUiAmount,
&[&new_authority],
)
.await
.unwrap();
let state = token.get_mint_info().await.unwrap();
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
assert_eq!(extension.authority, None.try_into().unwrap(),);

// now all fail
let err = token
.update_scale(&new_authority.pubkey(), 50.0, &[&new_authority])
.await
.unwrap_err();
assert_eq!(
err,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
0,
InstructionError::Custom(TokenError::NoAuthorityExists as u32)
)
)))
);
let err = token
.update_scale(&authority.pubkey(), 5.5, &[&authority])
.await
.unwrap_err();
assert_eq!(
err,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
0,
InstructionError::Custom(TokenError::NoAuthorityExists as u32)
)
)))
);
}

// test program to CPI into token to get ui amounts
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_input: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let mint_info = next_account_info(account_info_iter)?;
let token_program = next_account_info(account_info_iter)?;
// 10 tokens, with 9 decimal places
let test_amount = 10_000_000_000;
// "10" as an amount should be smaller than test_amount due to interest
invoke(
&ui_amount_to_amount(token_program.key, mint_info.key, "50")?,
&[mint_info.clone(), token_program.clone()],
)?;
let (_, return_data) = get_return_data().unwrap();
let amount = u64::from_le_bytes(return_data[0..8].try_into().unwrap());
msg!("amount: {}", amount);
if amount != test_amount {
return Err(ProgramError::InvalidInstructionData);
}

// test_amount as a UI amount should be larger due to interest
invoke(
&amount_to_ui_amount(token_program.key, mint_info.key, test_amount)?,
&[mint_info.clone(), token_program.clone()],
)?;
let (_, return_data) = get_return_data().unwrap();
let ui_amount = String::from_utf8(return_data).unwrap();
msg!("ui amount: {}", ui_amount);
let float_ui_amount = ui_amount.parse::<f64>().unwrap();
if float_ui_amount != 50.0 {
return Err(ProgramError::InvalidInstructionData);
}
Ok(())
}

#[tokio::test]
async fn amount_conversions() {
let authority = Keypair::new();
let mut program_test = ProgramTest::default();
program_test.prefer_bpf(false);
program_test.add_program(
"spl_token_2022",
spl_token_2022::id(),
processor!(Processor::process),
);
let program_id = Pubkey::new_unique();
program_test.add_program(
"ui_amount_to_amount",
program_id,
processor!(process_instruction),
);

let context = program_test.start_with_context().await;
let payer = keypair_clone(&context.payer);
let last_blockhash = context.last_blockhash;
let context = Arc::new(Mutex::new(context));
let mut context = TestContext {
context,
token_context: None,
};
let initial_scale = 5.0;
context
.init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig {
authority: Some(authority.pubkey()),
scale: initial_scale,
}])
.await
.unwrap();
let TokenContext { token, .. } = context.token_context.take().unwrap();

let transaction = Transaction::new_signed_with_payer(
&[Instruction {
program_id,
accounts: vec![
AccountMeta::new_readonly(*token.get_address(), false),
AccountMeta::new_readonly(spl_token_2022::id(), false),
],
data: vec![],
}],
Some(&payer.pubkey()),
&[&payer],
last_blockhash,
);
context
.context
.lock()
.await
.banks_client
.process_transaction(transaction)
.await
.unwrap();
}
6 changes: 6 additions & 0 deletions token/program-2022/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ pub enum TokenError {
/// Withdraw / Deposit not allowed for confidential-mint-burn
#[error("Withdraw / Deposit not allowed for confidential-mint-burn")]
IllegalMintBurnConversion,
/// Invalid scale for scaled ui amount
#[error("Invalid scale for scaled ui amount")]
InvalidScale,
}
impl From<TokenError> for ProgramError {
fn from(e: TokenError) -> Self {
Expand Down Expand Up @@ -453,6 +456,9 @@ impl PrintProgramError for TokenError {
TokenError::IllegalMintBurnConversion => {
msg!("Conversions from normal to confidential token balance and vice versa are illegal if the confidential-mint-burn extension is enabled")
}
TokenError::InvalidScale => {
msg!("Invalid scale for scaled ui amount")
}
}
}
}
Expand Down
Loading

0 comments on commit 772f34f

Please sign in to comment.