Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 772f34f

Browse files
committed
token-2022: Add scaled amount extension
#### 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`.
1 parent 0f54203 commit 772f34f

File tree

10 files changed

+877
-4
lines changed

10 files changed

+877
-4
lines changed

token/client/src/token.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ use {
4343
ConfidentialTransferFeeConfig,
4444
},
4545
cpi_guard, default_account_state, group_member_pointer, group_pointer,
46-
interest_bearing_mint, memo_transfer, metadata_pointer, transfer_fee, transfer_hook,
47-
BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsOwned,
46+
interest_bearing_mint, memo_transfer, metadata_pointer, scaled_ui_amount, transfer_fee,
47+
transfer_hook, BaseStateWithExtensions, Extension, ExtensionType,
48+
StateWithExtensionsOwned,
4849
},
4950
instruction, offchain,
5051
solana_zk_sdk::{
@@ -188,6 +189,10 @@ pub enum ExtensionInitializationParams {
188189
authority: Option<Pubkey>,
189190
member_address: Option<Pubkey>,
190191
},
192+
ScaledUiAmountConfig {
193+
authority: Option<Pubkey>,
194+
scale: f64,
195+
},
191196
}
192197
impl ExtensionInitializationParams {
193198
/// Get the extension type associated with the init params
@@ -207,6 +212,7 @@ impl ExtensionInitializationParams {
207212
}
208213
Self::GroupPointer { .. } => ExtensionType::GroupPointer,
209214
Self::GroupMemberPointer { .. } => ExtensionType::GroupMemberPointer,
215+
Self::ScaledUiAmountConfig { .. } => ExtensionType::ScaledUiAmount,
210216
}
211217
}
212218
/// Generate an appropriate initialization instruction for the given mint
@@ -316,6 +322,9 @@ impl ExtensionInitializationParams {
316322
authority,
317323
member_address,
318324
),
325+
Self::ScaledUiAmountConfig { authority, scale } => {
326+
scaled_ui_amount::instruction::initialize(token_program_id, mint, authority, scale)
327+
}
319328
}
320329
}
321330
}
@@ -1805,6 +1814,29 @@ where
18051814
.await
18061815
}
18071816

1817+
/// Update scale
1818+
pub async fn update_scale<S: Signers>(
1819+
&self,
1820+
authority: &Pubkey,
1821+
new_scale: f64,
1822+
signing_keypairs: &S,
1823+
) -> TokenResult<T::Output> {
1824+
let signing_pubkeys = signing_keypairs.pubkeys();
1825+
let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys);
1826+
1827+
self.process_ixs(
1828+
&[scaled_ui_amount::instruction::update_scale(
1829+
&self.program_id,
1830+
self.get_address(),
1831+
authority,
1832+
&multisig_signers,
1833+
new_scale,
1834+
)?],
1835+
signing_keypairs,
1836+
)
1837+
.await
1838+
}
1839+
18081840
/// Update transfer hook program id
18091841
pub async fn update_transfer_hook_program_id<S: Signers>(
18101842
&self,
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
#![cfg(feature = "test-sbf")]
2+
3+
mod program_test;
4+
use {
5+
program_test::{keypair_clone, TestContext, TokenContext},
6+
solana_program_test::{
7+
processor,
8+
tokio::{self, sync::Mutex},
9+
ProgramTest,
10+
},
11+
solana_sdk::{
12+
account_info::{next_account_info, AccountInfo},
13+
entrypoint::ProgramResult,
14+
instruction::{AccountMeta, Instruction, InstructionError},
15+
msg,
16+
program::{get_return_data, invoke},
17+
program_error::ProgramError,
18+
pubkey::Pubkey,
19+
signature::Signer,
20+
signer::keypair::Keypair,
21+
transaction::{Transaction, TransactionError},
22+
transport::TransportError,
23+
},
24+
spl_token_2022::{
25+
error::TokenError,
26+
extension::{scaled_ui_amount::ScaledUiAmountConfig, BaseStateWithExtensions},
27+
instruction::{amount_to_ui_amount, ui_amount_to_amount, AuthorityType},
28+
processor::Processor,
29+
},
30+
spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError},
31+
std::{convert::TryInto, sync::Arc},
32+
};
33+
34+
#[tokio::test]
35+
async fn success_initialize() {
36+
for (scale, authority) in [
37+
(f64::MIN_POSITIVE, None),
38+
(f64::MAX, Some(Pubkey::new_unique())),
39+
] {
40+
let mut context = TestContext::new().await;
41+
context
42+
.init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig {
43+
authority,
44+
scale,
45+
}])
46+
.await
47+
.unwrap();
48+
let TokenContext { token, .. } = context.token_context.unwrap();
49+
50+
let state = token.get_mint_info().await.unwrap();
51+
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
52+
assert_eq!(Option::<Pubkey>::from(extension.authority), authority,);
53+
assert_eq!(f64::from(extension.scale), scale);
54+
}
55+
}
56+
57+
#[tokio::test]
58+
async fn update_scale() {
59+
let authority = Keypair::new();
60+
let initial_scale = 5.0;
61+
let mut context = TestContext::new().await;
62+
context
63+
.init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig {
64+
authority: Some(authority.pubkey()),
65+
scale: initial_scale,
66+
}])
67+
.await
68+
.unwrap();
69+
let TokenContext { token, .. } = context.token_context.take().unwrap();
70+
71+
let state = token.get_mint_info().await.unwrap();
72+
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
73+
assert_eq!(f64::from(extension.scale), initial_scale);
74+
75+
// correct
76+
let new_scale = 10.0;
77+
token
78+
.update_scale(&authority.pubkey(), new_scale, &[&authority])
79+
.await
80+
.unwrap();
81+
let state = token.get_mint_info().await.unwrap();
82+
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
83+
assert_eq!(f64::from(extension.scale), new_scale);
84+
85+
// wrong signer
86+
let wrong_signer = Keypair::new();
87+
let err = token
88+
.update_scale(&wrong_signer.pubkey(), 1.0, &[&wrong_signer])
89+
.await
90+
.unwrap_err();
91+
assert_eq!(
92+
err,
93+
TokenClientError::Client(Box::new(TransportError::TransactionError(
94+
TransactionError::InstructionError(
95+
0,
96+
InstructionError::Custom(TokenError::OwnerMismatch as u32)
97+
)
98+
)))
99+
);
100+
}
101+
102+
#[tokio::test]
103+
async fn set_authority() {
104+
let authority = Keypair::new();
105+
let initial_scale = 500.0;
106+
let mut context = TestContext::new().await;
107+
context
108+
.init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig {
109+
authority: Some(authority.pubkey()),
110+
scale: initial_scale,
111+
}])
112+
.await
113+
.unwrap();
114+
let TokenContext { token, .. } = context.token_context.take().unwrap();
115+
116+
// success
117+
let new_authority = Keypair::new();
118+
token
119+
.set_authority(
120+
token.get_address(),
121+
&authority.pubkey(),
122+
Some(&new_authority.pubkey()),
123+
AuthorityType::ScaledUiAmount,
124+
&[&authority],
125+
)
126+
.await
127+
.unwrap();
128+
let state = token.get_mint_info().await.unwrap();
129+
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
130+
assert_eq!(
131+
extension.authority,
132+
Some(new_authority.pubkey()).try_into().unwrap(),
133+
);
134+
token
135+
.update_scale(&new_authority.pubkey(), 10.0, &[&new_authority])
136+
.await
137+
.unwrap();
138+
let err = token
139+
.update_scale(&authority.pubkey(), 100.0, &[&authority])
140+
.await
141+
.unwrap_err();
142+
assert_eq!(
143+
err,
144+
TokenClientError::Client(Box::new(TransportError::TransactionError(
145+
TransactionError::InstructionError(
146+
0,
147+
InstructionError::Custom(TokenError::OwnerMismatch as u32)
148+
)
149+
)))
150+
);
151+
152+
// set to none
153+
token
154+
.set_authority(
155+
token.get_address(),
156+
&new_authority.pubkey(),
157+
None,
158+
AuthorityType::ScaledUiAmount,
159+
&[&new_authority],
160+
)
161+
.await
162+
.unwrap();
163+
let state = token.get_mint_info().await.unwrap();
164+
let extension = state.get_extension::<ScaledUiAmountConfig>().unwrap();
165+
assert_eq!(extension.authority, None.try_into().unwrap(),);
166+
167+
// now all fail
168+
let err = token
169+
.update_scale(&new_authority.pubkey(), 50.0, &[&new_authority])
170+
.await
171+
.unwrap_err();
172+
assert_eq!(
173+
err,
174+
TokenClientError::Client(Box::new(TransportError::TransactionError(
175+
TransactionError::InstructionError(
176+
0,
177+
InstructionError::Custom(TokenError::NoAuthorityExists as u32)
178+
)
179+
)))
180+
);
181+
let err = token
182+
.update_scale(&authority.pubkey(), 5.5, &[&authority])
183+
.await
184+
.unwrap_err();
185+
assert_eq!(
186+
err,
187+
TokenClientError::Client(Box::new(TransportError::TransactionError(
188+
TransactionError::InstructionError(
189+
0,
190+
InstructionError::Custom(TokenError::NoAuthorityExists as u32)
191+
)
192+
)))
193+
);
194+
}
195+
196+
// test program to CPI into token to get ui amounts
197+
fn process_instruction(
198+
_program_id: &Pubkey,
199+
accounts: &[AccountInfo],
200+
_input: &[u8],
201+
) -> ProgramResult {
202+
let account_info_iter = &mut accounts.iter();
203+
let mint_info = next_account_info(account_info_iter)?;
204+
let token_program = next_account_info(account_info_iter)?;
205+
// 10 tokens, with 9 decimal places
206+
let test_amount = 10_000_000_000;
207+
// "10" as an amount should be smaller than test_amount due to interest
208+
invoke(
209+
&ui_amount_to_amount(token_program.key, mint_info.key, "50")?,
210+
&[mint_info.clone(), token_program.clone()],
211+
)?;
212+
let (_, return_data) = get_return_data().unwrap();
213+
let amount = u64::from_le_bytes(return_data[0..8].try_into().unwrap());
214+
msg!("amount: {}", amount);
215+
if amount != test_amount {
216+
return Err(ProgramError::InvalidInstructionData);
217+
}
218+
219+
// test_amount as a UI amount should be larger due to interest
220+
invoke(
221+
&amount_to_ui_amount(token_program.key, mint_info.key, test_amount)?,
222+
&[mint_info.clone(), token_program.clone()],
223+
)?;
224+
let (_, return_data) = get_return_data().unwrap();
225+
let ui_amount = String::from_utf8(return_data).unwrap();
226+
msg!("ui amount: {}", ui_amount);
227+
let float_ui_amount = ui_amount.parse::<f64>().unwrap();
228+
if float_ui_amount != 50.0 {
229+
return Err(ProgramError::InvalidInstructionData);
230+
}
231+
Ok(())
232+
}
233+
234+
#[tokio::test]
235+
async fn amount_conversions() {
236+
let authority = Keypair::new();
237+
let mut program_test = ProgramTest::default();
238+
program_test.prefer_bpf(false);
239+
program_test.add_program(
240+
"spl_token_2022",
241+
spl_token_2022::id(),
242+
processor!(Processor::process),
243+
);
244+
let program_id = Pubkey::new_unique();
245+
program_test.add_program(
246+
"ui_amount_to_amount",
247+
program_id,
248+
processor!(process_instruction),
249+
);
250+
251+
let context = program_test.start_with_context().await;
252+
let payer = keypair_clone(&context.payer);
253+
let last_blockhash = context.last_blockhash;
254+
let context = Arc::new(Mutex::new(context));
255+
let mut context = TestContext {
256+
context,
257+
token_context: None,
258+
};
259+
let initial_scale = 5.0;
260+
context
261+
.init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig {
262+
authority: Some(authority.pubkey()),
263+
scale: initial_scale,
264+
}])
265+
.await
266+
.unwrap();
267+
let TokenContext { token, .. } = context.token_context.take().unwrap();
268+
269+
let transaction = Transaction::new_signed_with_payer(
270+
&[Instruction {
271+
program_id,
272+
accounts: vec![
273+
AccountMeta::new_readonly(*token.get_address(), false),
274+
AccountMeta::new_readonly(spl_token_2022::id(), false),
275+
],
276+
data: vec![],
277+
}],
278+
Some(&payer.pubkey()),
279+
&[&payer],
280+
last_blockhash,
281+
);
282+
context
283+
.context
284+
.lock()
285+
.await
286+
.banks_client
287+
.process_transaction(transaction)
288+
.await
289+
.unwrap();
290+
}

token/program-2022/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ pub enum TokenError {
263263
/// Withdraw / Deposit not allowed for confidential-mint-burn
264264
#[error("Withdraw / Deposit not allowed for confidential-mint-burn")]
265265
IllegalMintBurnConversion,
266+
/// Invalid scale for scaled ui amount
267+
#[error("Invalid scale for scaled ui amount")]
268+
InvalidScale,
266269
}
267270
impl From<TokenError> for ProgramError {
268271
fn from(e: TokenError) -> Self {
@@ -453,6 +456,9 @@ impl PrintProgramError for TokenError {
453456
TokenError::IllegalMintBurnConversion => {
454457
msg!("Conversions from normal to confidential token balance and vice versa are illegal if the confidential-mint-burn extension is enabled")
455458
}
459+
TokenError::InvalidScale => {
460+
msg!("Invalid scale for scaled ui amount")
461+
}
456462
}
457463
}
458464
}

0 commit comments

Comments
 (0)