19

I'm curious what the practical difference is between the Transfer and TransferChecked instructions, and when should we use one over the other.

In this question, I'm using Transfer/TransferChecked as an example, but there are other instructions that are suffixed with Checked and it would be great if there would be a generic answer.

I found a reference in the solana-program-library repo that explains it as follows:

This instruction differs from Transfer in that the token mint and decimals value is checked by the caller. This may be useful when creating transactions offline or within a hardware wallet.

This explains how the two are different in usage, but it does not explain why one should be used over the other.

the token mint and decimals value is checked by the caller

In what sense are the token mint and decimals 'checked' by the caller? What is the benefit of this approach and who 'checked' it before?

Is there a recommended one to use? Do we miss out on something if we use one over the other?

Some input here would be highly appreciated!

1
  • 1
    Please update to clarify that you are inquiring about instructions from the SPL Token program
    – trent.sol
    Commented Jul 19, 2022 at 18:47

2 Answers 2

24

I looked into this a bit myself a while ago, definitely not an expert but this is what I figured out: I think it's basically that the _checked variant requires you to pass the mint account and the number of decimals the token has, and if you pass the wrong mint account or decimals number then it will error. If it errors then the transaction is rolled back safely. This is presumably intended so that you don't accidentally eg. send someone 1,000,000 tokens when you meant to send them 10 because you thought it had 6 decimals but it actually has 1. Or send the wrong token.

The amount you send is always 10^decimals. So if you have the number of decimals wrong you can send a very different number to what you intended.

The distinction actually carries all the way through to the core Rust code that processes the instruction, so they're separate instructions in the program code. The relevant source code is this file: https://github.com/solana-labs/solana-program-library/blob/master/token/program/src/processor.rs

Here's what it does when it processes a Transfer instruction:

TokenInstruction::Transfer { amount } => {
  msg!("Instruction: Transfer");
  Self::process_transfer(program_id, accounts, amount, None)
}

And when it processes a TransferChecked one:

TokenInstruction::TransferChecked { amount, decimals } => {
  msg!("Instruction: TransferChecked");
  Self::process_transfer(program_id, accounts, amount, Some(decimals))
}

So they're both calling process_transfer, the checked one passes in a Some(decimals) as the last argument while the unchecked passes None.

Here's process_transfer:

pub fn process_transfer(
  program_id: &Pubkey,
  accounts: &[AccountInfo],
  amount: u64,
  expected_decimals: Option<u8>,
) -> ProgramResult 

That last argument is expected_decimals, an option. So that matches.

Here's some of what that function does:

let expected_mint_info = if let Some(expected_decimals) = expected_decimals {
  Some((next_account_info(account_info_iter)?, expected_decimals))
} else {
  None
};

If expected_decimals is defined (as in the checked case, but not the unchecked one), it reads the mint account from the accounts list and the expected decimals number. If not then expected_mint_info is None. No mint account or expected decimals is recorded.

Later on:

if let Some((mint_info, expected_decimals)) = expected_mint_info {
  if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) {
    return Err(TokenError::MintMismatch.into());
  }

  let mint = Mint::unpack(&mint_info.data.borrow_mut())?;
  if expected_decimals != mint.decimals {
    return Err(TokenError::MintDecimalsMismatch.into());
  }
}

This is where it's doing the checks the documentation referred to. It only does these if expected_mint_info is defined, which only happens for the checked call. Here it's checking that the passed mint account has the expected address, and that its number of decimals is the one we passed in. If they don't match the transaction errors.

If you check the source code you'll see it basically works the same for all these checked/unchecked variants. They all have an optional expected_decimals that triggers them to read a mint account and perform the same checks.

In terms of why you'd use one over the other, the only thing I can think of is that using the unchecked is a performance optimisation. It requires you to fetch and pass less data from the client, and it requires the instruction to do less work.

Also FWIW it looks like Transfer has been deprecated in program-2022, which is the upcoming new version of the token program: https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/processor.rs#L1212

#[allow(deprecated)]
TokenInstruction::Transfer { amount } => {
  msg!("Instruction: Transfer");
  Self::process_transfer(program_id, accounts, amount, None, None)
}
9

@callum-m nailed it. There's an added benefit that's less obvious in that hardware wallets can display the transfer amount and asset safely and correctly with TransferChecked whereas they cannot with Transfer. The processor for Transfer simply checks that source and destination mints match implicitly by comparing the mint assignments from the accounts' data. However it isn't possible to upload account data to a hardware wallet in a trusted way, so instead the new instruction(s) were introduced such that by signing the transaction, the owner is explicitly asserting the intended mint and decimals. This was actually the main motivation for introducing the *Checked variants throughout SPL Token.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.