Skip to main content

hopper_runtime/
token_admin.rs

1//! Administrative SPL Token / Token-2022 builders: mint and multisig
2//! initialization, immutable-owner accounts, the data-size and UI-amount
3//! queries, and lamport withdrawal from token accounts.
4//!
5//! Every builder here is re-exported from [`crate::token`] and follows the
6//! same contract as the transfer family: one [`TokenInstruction::emit`]
7//! encodes the bytes and metas, `invoke()` sends them to SPL Token,
8//! `invoke_on` to an explicit [`TokenProgram`], `invoke_for_owner` to the
9//! program that owns the first account, and a [`crate::token::TokenBatch`]
10//! can collect them.
11
12use crate::account::AccountView;
13use crate::address::Address;
14use crate::error::ProgramError;
15use crate::instruction::{InstructionAccount, Signer};
16use crate::token::{
17    authority_meta, encoders, require_authority_signed_direct, require_multisig_signers_direct,
18    token_program_methods, Invoke, TokenInstruction, TokenProgram, TokenSink, Trailing,
19    MAX_TOKEN_MULTISIG_SIGNERS,
20};
21use crate::ProgramResult;
22
23pub use crate::token::encoders::{MAX_EXTENSION_TYPES, MAX_UI_AMOUNT_LEN};
24
25// ---------------------------------------------------------------------
26
27/// `InitializeMint` (0): the Rent-sysvar form of
28/// [`InitializeMint2`](crate::token_mint::InitializeMint2). The mint must
29/// already be allocated (82 bytes on SPL Token) and owned by the token
30/// program. Prefer `InitializeMint2` in new code; this form exists for
31/// callers whose instruction already carries the Rent sysvar.
32pub struct InitializeMint<'a> {
33    pub mint: &'a AccountView<'a>,
34    pub rent_sysvar: &'a AccountView<'a>,
35    pub decimals: u8,
36    pub mint_authority: &'a Address,
37    pub freeze_authority: Option<&'a Address>,
38}
39
40impl InitializeMint<'_> {
41    #[inline]
42    pub fn invoke(&self) -> ProgramResult {
43        self.emit(&[], &mut Invoke::legacy(&[]))
44    }
45}
46
47impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeMint<'x> {
48    #[inline(always)]
49    fn emit(
50        &self,
51        multisig_signers: &[&'a AccountView<'a>],
52        sink: &mut impl TokenSink<'a>,
53    ) -> ProgramResult {
54        let (data, len) = encoders::encode_initialize_mint(
55            self.decimals,
56            self.mint_authority.as_array(),
57            self.freeze_authority.map(|a| a.as_array()),
58        );
59        let accounts = [
60            InstructionAccount::writable(self.mint.address()),
61            InstructionAccount::readonly(self.rent_sysvar.address()),
62        ];
63        let views = [self.mint, self.rent_sysvar];
64        sink.emit(
65            &data[..len],
66            accounts,
67            views,
68            &[Trailing::signers(multisig_signers)],
69        )
70    }
71}
72
73token_program_methods!(InitializeMint, owner = mint);
74
75// ---------------------------------------------------------------------
76
77/// Refuse a multisig configuration the token program would refuse:
78/// `1 <= m <= n <= 11` signers.
79#[inline(always)]
80fn check_multisig_shape(m: u8, signers: usize) -> ProgramResult {
81    if m == 0 || signers == 0 || signers > MAX_TOKEN_MULTISIG_SIGNERS || usize::from(m) > signers {
82        return Err(ProgramError::InvalidArgument);
83    }
84    Ok(())
85}
86
87/// `InitializeMultisig` (2): turn a 355-byte, token-program-owned account
88/// into an `m`-of-`n` multisig over `signers`. The member accounts are
89/// listed read-only and do not sign. Requires the Rent sysvar account;
90/// [`InitializeMultisig2`] does not.
91pub struct InitializeMultisig<'a> {
92    pub multisig: &'a AccountView<'a>,
93    pub rent_sysvar: &'a AccountView<'a>,
94    /// The member accounts, 1 to 11 of them.
95    pub signers: &'a [&'a AccountView<'a>],
96    /// How many members must sign, 1 to `signers.len()`.
97    pub m: u8,
98}
99
100impl InitializeMultisig<'_> {
101    #[inline]
102    pub fn invoke(&self) -> ProgramResult {
103        self.emit(&[], &mut Invoke::legacy(&[]))
104    }
105}
106
107impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeMultisig<'x> {
108    #[inline(always)]
109    fn emit(
110        &self,
111        _multisig_signers: &[&'a AccountView<'a>],
112        sink: &mut impl TokenSink<'a>,
113    ) -> ProgramResult {
114        check_multisig_shape(self.m, self.signers.len())?;
115        let data = encoders::encode_initialize_multisig(self.m);
116        let accounts = [
117            InstructionAccount::writable(self.multisig.address()),
118            InstructionAccount::readonly(self.rent_sysvar.address()),
119        ];
120        let views = [self.multisig, self.rent_sysvar];
121        sink.emit(&data, accounts, views, &[Trailing::readonly(self.signers)])
122    }
123}
124
125token_program_methods!(InitializeMultisig, owner = multisig);
126
127/// `InitializeMultisig2` (19): [`InitializeMultisig`] without the Rent
128/// sysvar account.
129pub struct InitializeMultisig2<'a> {
130    pub multisig: &'a AccountView<'a>,
131    /// The member accounts, 1 to 11 of them.
132    pub signers: &'a [&'a AccountView<'a>],
133    /// How many members must sign, 1 to `signers.len()`.
134    pub m: u8,
135}
136
137impl InitializeMultisig2<'_> {
138    #[inline]
139    pub fn invoke(&self) -> ProgramResult {
140        self.emit(&[], &mut Invoke::legacy(&[]))
141    }
142}
143
144impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeMultisig2<'x> {
145    #[inline(always)]
146    fn emit(
147        &self,
148        _multisig_signers: &[&'a AccountView<'a>],
149        sink: &mut impl TokenSink<'a>,
150    ) -> ProgramResult {
151        check_multisig_shape(self.m, self.signers.len())?;
152        let data = encoders::encode_initialize_multisig2(self.m);
153        let accounts = [InstructionAccount::writable(self.multisig.address())];
154        let views = [self.multisig];
155        sink.emit(&data, accounts, views, &[Trailing::readonly(self.signers)])
156    }
157}
158
159token_program_methods!(InitializeMultisig2, owner = multisig);
160
161// ---------------------------------------------------------------------
162
163/// `InitializeImmutableOwner` (22): mark a not-yet-initialized token
164/// account so that its owner can never be changed. Runs before
165/// `InitializeAccount3`. On Token-2022 it needs the 4-byte extension
166/// header in the allocation ([`GetAccountDataSize`] with type 7 reports
167/// the size); SPL Token accepts it as a no-op on a 165-byte account.
168pub struct InitializeImmutableOwner<'a> {
169    pub account: &'a AccountView<'a>,
170}
171
172impl InitializeImmutableOwner<'_> {
173    #[inline]
174    pub fn invoke(&self) -> ProgramResult {
175        self.emit(&[], &mut Invoke::legacy(&[]))
176    }
177}
178
179impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeImmutableOwner<'x> {
180    #[inline(always)]
181    fn emit(
182        &self,
183        multisig_signers: &[&'a AccountView<'a>],
184        sink: &mut impl TokenSink<'a>,
185    ) -> ProgramResult {
186        let data = encoders::encode_initialize_immutable_owner();
187        let accounts = [InstructionAccount::writable(self.account.address())];
188        let views = [self.account];
189        sink.emit(
190            &data,
191            accounts,
192            views,
193            &[Trailing::signers(multisig_signers)],
194        )
195    }
196}
197
198token_program_methods!(InitializeImmutableOwner, owner = account);
199
200// ---------------------------------------------------------------------
201
202/// `GetAccountDataSize` (21): ask the token program how many bytes a
203/// token account for `mint` needs, as a `u64` in the return data. On
204/// Token-2022 the mint's required account extensions are included
205/// automatically and `extension_types` adds more (for example
206/// [`crate::token_2022_ext::EXT_IMMUTABLE_OWNER`]); SPL Token ignores
207/// the list and answers 165.
208pub struct GetAccountDataSize<'a> {
209    pub mint: &'a AccountView<'a>,
210    pub extension_types: &'a [u16],
211}
212
213impl GetAccountDataSize<'_> {
214    #[inline]
215    pub fn invoke(&self) -> ProgramResult {
216        self.emit(&[], &mut Invoke::legacy(&[]))
217    }
218
219    /// Invoke on `program` and read the answer from the return data.
220    #[inline]
221    pub fn query(&self, program: TokenProgram) -> Result<u64, ProgramError> {
222        self.invoke_on(program, &[], &[])?;
223        return_data_u64(program.address())
224    }
225
226    /// [`query`](Self::query) on whichever token program owns `mint`.
227    #[inline]
228    pub fn query_for_owner(&self) -> Result<u64, ProgramError> {
229        self.query(TokenProgram::owning(self.mint)?)
230    }
231}
232
233impl<'a, 'x: 'a> TokenInstruction<'a> for GetAccountDataSize<'x> {
234    #[inline(always)]
235    fn emit(
236        &self,
237        multisig_signers: &[&'a AccountView<'a>],
238        sink: &mut impl TokenSink<'a>,
239    ) -> ProgramResult {
240        let (data, len) = encoders::encode_extension_types(21, self.extension_types)
241            .ok_or(ProgramError::InvalidArgument)?;
242        let accounts = [InstructionAccount::readonly(self.mint.address())];
243        let views = [self.mint];
244        sink.emit(
245            &data[..len],
246            accounts,
247            views,
248            &[Trailing::signers(multisig_signers)],
249        )
250    }
251}
252
253token_program_methods!(GetAccountDataSize, owner = mint);
254
255// ---------------------------------------------------------------------
256
257/// `AmountToUiAmount` (23): format a raw amount with the mint's decimals
258/// (and, on Token-2022, its interest or scaled-UI configuration) as a
259/// UTF-8 string in the return data. Read it with [`return_data_string`].
260pub struct AmountToUiAmount<'a> {
261    pub mint: &'a AccountView<'a>,
262    pub amount: u64,
263}
264
265impl AmountToUiAmount<'_> {
266    #[inline]
267    pub fn invoke(&self) -> ProgramResult {
268        self.emit(&[], &mut Invoke::legacy(&[]))
269    }
270
271    /// Invoke on `program` and copy the string the program returned into
272    /// `out`, giving its length.
273    #[inline]
274    pub fn query(
275        &self,
276        program: TokenProgram,
277        out: &mut [u8; MAX_UI_AMOUNT_LEN],
278    ) -> Result<usize, ProgramError> {
279        self.invoke_on(program, &[], &[])?;
280        return_data_string(program.address(), out)
281    }
282}
283
284impl<'a, 'x: 'a> TokenInstruction<'a> for AmountToUiAmount<'x> {
285    #[inline(always)]
286    fn emit(
287        &self,
288        multisig_signers: &[&'a AccountView<'a>],
289        sink: &mut impl TokenSink<'a>,
290    ) -> ProgramResult {
291        let data = encoders::encode_amount_to_ui_amount(self.amount);
292        let accounts = [InstructionAccount::readonly(self.mint.address())];
293        let views = [self.mint];
294        sink.emit(
295            &data,
296            accounts,
297            views,
298            &[Trailing::signers(multisig_signers)],
299        )
300    }
301}
302
303token_program_methods!(AmountToUiAmount, owner = mint);
304
305/// `UiAmountToAmount` (24): parse a decimal string against the mint and
306/// return the raw amount as a `u64` in the return data. Read it with
307/// [`return_data_u64`]. Strings longer than [`MAX_UI_AMOUNT_LEN`] bytes
308/// are refused before the CPI.
309pub struct UiAmountToAmount<'a> {
310    pub mint: &'a AccountView<'a>,
311    pub ui_amount: &'a str,
312}
313
314impl UiAmountToAmount<'_> {
315    #[inline]
316    pub fn invoke(&self) -> ProgramResult {
317        self.emit(&[], &mut Invoke::legacy(&[]))
318    }
319
320    /// Invoke on `program` and read the raw amount from the return data.
321    #[inline]
322    pub fn query(&self, program: TokenProgram) -> Result<u64, ProgramError> {
323        self.invoke_on(program, &[], &[])?;
324        return_data_u64(program.address())
325    }
326}
327
328impl<'a, 'x: 'a> TokenInstruction<'a> for UiAmountToAmount<'x> {
329    #[inline(always)]
330    fn emit(
331        &self,
332        multisig_signers: &[&'a AccountView<'a>],
333        sink: &mut impl TokenSink<'a>,
334    ) -> ProgramResult {
335        let (data, len) = encoders::encode_ui_amount_to_amount(self.ui_amount)
336            .ok_or(ProgramError::InvalidArgument)?;
337        let accounts = [InstructionAccount::readonly(self.mint.address())];
338        let views = [self.mint];
339        sink.emit(
340            &data[..len],
341            accounts,
342            views,
343            &[Trailing::signers(multisig_signers)],
344        )
345    }
346}
347
348token_program_methods!(UiAmountToAmount, owner = mint);
349
350// ---------------------------------------------------------------------
351
352/// `WithdrawExcessLamports` (38): move every lamport above the
353/// rent-exempt minimum out of a token account, mint, or multisig into
354/// `destination`. `authority` is the account's owner (or the mint's
355/// close authority, or the multisig itself), directly signed or through
356/// multisig signers.
357pub struct WithdrawExcessLamports<'a> {
358    pub source: &'a AccountView<'a>,
359    pub destination: &'a AccountView<'a>,
360    pub authority: &'a AccountView<'a>,
361}
362
363impl WithdrawExcessLamports<'_> {
364    #[inline]
365    pub fn invoke(&self) -> ProgramResult {
366        require_authority_signed_direct(self.authority)?;
367        self.emit(&[], &mut Invoke::legacy(&[]))
368    }
369
370    #[inline]
371    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
372        self.emit(&[], &mut Invoke::legacy(signers))
373    }
374
375    #[inline]
376    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
377        require_multisig_signers_direct(multisig_signers)?;
378        self.emit(multisig_signers, &mut Invoke::legacy(&[]))
379    }
380}
381
382impl<'a, 'x: 'a> TokenInstruction<'a> for WithdrawExcessLamports<'x> {
383    #[inline(always)]
384    fn emit(
385        &self,
386        multisig_signers: &[&'a AccountView<'a>],
387        sink: &mut impl TokenSink<'a>,
388    ) -> ProgramResult {
389        let data = encoders::encode_withdraw_excess_lamports();
390        let accounts = [
391            InstructionAccount::writable(self.source.address()),
392            InstructionAccount::writable(self.destination.address()),
393            authority_meta(self.authority, multisig_signers),
394        ];
395        let views = [self.source, self.destination, self.authority];
396        sink.emit(
397            &data,
398            accounts,
399            views,
400            &[Trailing::signers(multisig_signers)],
401        )
402    }
403}
404
405token_program_methods!(
406    WithdrawExcessLamports,
407    owner = source,
408    authority = authority
409);
410
411/// `UnwrapLamports` (45): move lamports out of a native (wrapped SOL)
412/// token account to `destination` without closing it, the whole balance
413/// with `amount: None` or a part of it. The instruction exists in the
414/// p-token build of SPL Token; a program that does not know discriminator
415/// 45 refuses it with `InvalidInstructionData`.
416pub struct UnwrapLamports<'a> {
417    pub source: &'a AccountView<'a>,
418    pub destination: &'a AccountView<'a>,
419    pub authority: &'a AccountView<'a>,
420    pub amount: Option<u64>,
421}
422
423impl UnwrapLamports<'_> {
424    #[inline]
425    pub fn invoke(&self) -> ProgramResult {
426        require_authority_signed_direct(self.authority)?;
427        self.emit(&[], &mut Invoke::legacy(&[]))
428    }
429
430    #[inline]
431    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
432        self.emit(&[], &mut Invoke::legacy(signers))
433    }
434
435    #[inline]
436    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
437        require_multisig_signers_direct(multisig_signers)?;
438        self.emit(multisig_signers, &mut Invoke::legacy(&[]))
439    }
440}
441
442impl<'a, 'x: 'a> TokenInstruction<'a> for UnwrapLamports<'x> {
443    #[inline(always)]
444    fn emit(
445        &self,
446        multisig_signers: &[&'a AccountView<'a>],
447        sink: &mut impl TokenSink<'a>,
448    ) -> ProgramResult {
449        let (data, len) = encoders::encode_unwrap_lamports(self.amount);
450        let accounts = [
451            InstructionAccount::writable(self.source.address()),
452            InstructionAccount::writable(self.destination.address()),
453            authority_meta(self.authority, multisig_signers),
454        ];
455        let views = [self.source, self.destination, self.authority];
456        sink.emit(
457            &data[..len],
458            accounts,
459            views,
460            &[Trailing::signers(multisig_signers)],
461        )
462    }
463}
464
465token_program_methods!(UnwrapLamports, owner = source, authority = authority);
466
467// ---------------------------------------------------------------------
468
469/// The `u64` a token program left in the return data, refusing return
470/// data set by any other program or shorter than eight bytes.
471#[inline]
472pub fn return_data_u64(program: &Address) -> Result<u64, ProgramError> {
473    let returned =
474        crate::return_data::get_return_data().ok_or(ProgramError::InvalidInstructionData)?;
475    if returned.program_id() != program {
476        return Err(ProgramError::IncorrectProgramId);
477    }
478    let bytes: [u8; 8] = returned
479        .data()
480        .get(..8)
481        .and_then(|b| b.try_into().ok())
482        .ok_or(ProgramError::InvalidInstructionData)?;
483    Ok(u64::from_le_bytes(bytes))
484}
485
486/// The string a token program left in the return data, copied into `out`;
487/// the result is its length. Refuses return data from any other program
488/// and anything longer than the buffer.
489#[inline]
490pub fn return_data_string(
491    program: &Address,
492    out: &mut [u8; MAX_UI_AMOUNT_LEN],
493) -> Result<usize, ProgramError> {
494    let returned =
495        crate::return_data::get_return_data().ok_or(ProgramError::InvalidInstructionData)?;
496    if returned.program_id() != program {
497        return Err(ProgramError::IncorrectProgramId);
498    }
499    let data = returned.data();
500    if data.len() > out.len() {
501        return Err(ProgramError::InvalidInstructionData);
502    }
503    out[..data.len()].copy_from_slice(data);
504    Ok(data.len())
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[test]
512    fn multisig_shape_is_checked_before_the_cpi() {
513        assert!(check_multisig_shape(1, 1).is_ok());
514        assert!(check_multisig_shape(11, 11).is_ok());
515        assert!(check_multisig_shape(0, 1).is_err());
516        assert!(check_multisig_shape(2, 1).is_err());
517        assert!(check_multisig_shape(1, 0).is_err());
518        assert!(check_multisig_shape(1, 12).is_err());
519    }
520
521    #[test]
522    fn admin_encoders_match_the_token_wire_format() {
523        let authority = [7u8; 32];
524        let freeze = [9u8; 32];
525        let (data, len) = encoders::encode_initialize_mint(6, &authority, Some(&freeze));
526        assert_eq!(len, 67);
527        assert_eq!(&data[..2], &[0, 6]);
528        assert_eq!(&data[2..34], &authority);
529        assert_eq!(data[34], 1);
530        assert_eq!(&data[35..67], &freeze);
531        let (data, len) = encoders::encode_initialize_mint(0, &authority, None);
532        assert_eq!(len, 35);
533        assert_eq!(data[34], 0);
534
535        assert_eq!(encoders::encode_initialize_multisig(2), [2, 2]);
536        assert_eq!(encoders::encode_initialize_multisig2(3), [19, 3]);
537        assert_eq!(encoders::encode_initialize_immutable_owner(), [22]);
538        assert_eq!(encoders::encode_withdraw_excess_lamports(), [38]);
539        assert_eq!(
540            encoders::encode_amount_to_ui_amount(258),
541            [23, 2, 1, 0, 0, 0, 0, 0, 0]
542        );
543
544        let (data, len) = encoders::encode_extension_types(21, &[7, 0x0102]).unwrap();
545        assert_eq!(len, 5);
546        assert_eq!(&data[..5], &[21, 7, 0, 2, 1]);
547        let (data, len) = encoders::encode_extension_types(29, &[]).unwrap();
548        assert_eq!((data[0], len), (29, 1));
549        assert!(encoders::encode_extension_types(21, &[0; MAX_EXTENSION_TYPES + 1]).is_none());
550
551        let (data, len) = encoders::encode_ui_amount_to_amount("1.5").unwrap();
552        assert_eq!(&data[..len], b"\x181.5");
553        let long = [b'1'; MAX_UI_AMOUNT_LEN];
554        let (_, len) =
555            encoders::encode_ui_amount_to_amount(core::str::from_utf8(&long).unwrap()).unwrap();
556        assert_eq!(len, 1 + MAX_UI_AMOUNT_LEN);
557        let too_long = [b'1'; MAX_UI_AMOUNT_LEN + 1];
558        assert!(
559            encoders::encode_ui_amount_to_amount(core::str::from_utf8(&too_long).unwrap())
560                .is_none()
561        );
562
563        let (data, len) = encoders::encode_unwrap_lamports(None);
564        assert_eq!(&data[..len], &[45, 0]);
565        let (data, len) = encoders::encode_unwrap_lamports(Some(1));
566        assert_eq!(&data[..len], &[45, 1, 1, 0, 0, 0, 0, 0, 0, 0]);
567    }
568}