Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer hook support #15

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Transfer hook support #15

wants to merge 4 commits into from

Conversation

grod220
Copy link
Contributor

@grod220 grod220 commented Feb 27, 2025

If the mint has support for the TransferHooks extension, the program will now execute them upon a wrap or unwrap action.

Makes use of helpers exposed by token-2022 repo. However, there is a dependency on solana-program/token-2022#233 for multisig support.

New test-transfer-hook program created to test out new functionality.

thiserror = "2.0.11"

# Only needed until solana-sysvar is bumped to 2.1
solana-program = "2.1.13"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bye 👋

Comment on lines +12 to +13
await $`RUST_LOG=error cargo test-sbf --manifest-path ${testProgramManifestPath} ${args}`;
await $`RUST_LOG=error cargo test-sbf --manifest-path ${mainProgramManifestPath} ${args}`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important to add the test program here as the main program tests depend on the .so file

@grod220 grod220 marked this pull request as ready for review February 27, 2025 20:24
Comment on lines +18 to +28
solana-account-info = "2.2.1"
solana-decode-error = "2.2.1"
solana-cpi = "2.2.1"
solana-instruction = "2.2.1"
solana-msg = "2.2.1"
solana-program-entrypoint = "2.2.1"
solana-program-error = "2.2.1"
solana-program-option = "2.2.1"
solana-program-pack = "2.2.1"
solana-pubkey = "2.2.1"
solana-rent = "2.2.1"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming these all needed to be bumped to use the newer token-2022 lib? In the future, this is typically better in a standalone PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, makes sense. However, this PR depends on solana-program/token-2022#233 (which depends on the other deps being updated).

spl-tlv-account-resolution = "0.9.0"
solana-account = "2.2.1"
solana-system-program = "2.2.0"
test-transfer-hook = { path = "tests/helpers/test-transfer-hook", features = ["no-entrypoint"] }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One idea for this program, so it's not so weird to use in the workspace, is you can actually define it as a builtin program, rather than an on-chain BPF program.

You can use declare_process_instruction! from the program-runtime and add_builtin on Mollusk's program_cache field to add it to the test environment.

That way, you can just define the program's simple processor in a test module and you don't need to compile a new crate. On the flip side, I noticed we do this in the token-2022 tests already, so not a big deal either way. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I was following the pattern that Mollusk had for its CPI tests. I worry if we don't do it this way, it doesn't quite emulate a program-to-program test. Perhaps I am off though?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be the same, but I suppose there's no harm in testing with true BPF-BPF cross-program invocations, as you're doing here. Maybe we can leave as you have it, then.

One small nit would be to consider committing the program ELF for the test program, like we do in Token-2022, if you think it makes life easier for CI/etc.
https://github.com/solana-program/token-2022/tree/main/clients/rust-legacy/tests/fixtures

@@ -172,6 +172,7 @@ pub fn wrap(
unwrapped_escrow_address: &Pubkey,
transfer_authority_address: &Pubkey,
multisig_signer_pubkeys: &[&Pubkey],
transfer_hook_metas: Vec<AccountMeta>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This can be a slice.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if you want, you can leave these helpers alone as the "base" helpers, and you can roll a set of additional helpers that look just like these ones in Token-2022. You could even use an offchain module to keep with the pattern in the interface and the Token-2022 program.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This can be a slice.

Given the accounts require owned AccountMeta, if this were a slice it would need to clone:

for meta in transfer_hook_metas.iter() {
    accounts.push(meta.clone());
}

So this would be a net neutral change?

if you want, you can leave these helpers alone as the "base" helpers

Ah, yes I think it's helpful to have the offchain/onchain helpers segmented off. However, all of these just form instructions, so I think all three would be pulled to the offchain file.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the accounts require owned AccountMeta, if this were a slice it would need to clone:

Ah, gotcha. If AccountMeta can't be dereferenced, then yeah makes sense.

solana_program_error::{ProgramError, ProgramResult},
solana_program_pack::Pack,
solana_pubkey::Pubkey,
solana_rent::Rent,
solana_system_interface::instruction::{allocate, assign},
spl_token::solana_program::sysvar::Sysvar,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used for:

let rent = Rent::get()?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where?

Comment on lines 280 to 289
let mut multisig_signers = Vec::new();
let mut transfer_hook_accounts = Vec::new();
for account in additional_accounts {
if account.is_signer {
multisig_signers.push(account);
} else {
transfer_hook_accounts.push(account.clone());
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A transfer hook account extra can also be a signer. What happens if you just pass all the accounts to burn and then use the onchain transfer helper for transfer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this works for burn (will update this), however it's tricker for transfer. If I use the helper invoke_transfer_checked() and signed multisig accounts were passed to the unwrap instruction (relevant to the burn), it will attempt to incorrectly add those to the escrow transfer (it should be PDA authorized transfer) https://github.com/solana-program/token-2022/blob/4829c03a0b34d86db3b663262883a434268874fa/program/src/instruction.rs#L1689-L1692 and throw MissingRequiredSignature.

Sounds like we have a few options:

Thoughts?

Copy link

@buffalojoec buffalojoec Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, it will add the multisigs first before adding the "extra metas", and the multisigs are only required for burning here.

Maybe here, since we know we don't want to add multisig signers to the transfer instruction (only the burn), we can maybe circumvent invoke_transfer_checked and just build things out with spl_transfer_hook_interface::onchain::add_extra_accounts_for_execute_cpi directly.

What do you think?

Copy link
Contributor Author

@grod220 grod220 Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, was a bit confused. There are two problems. Both require multisig account detection.

  1. invoke_transfer_checked() does not support signed extra transfer hook accounts.
    • Going to open a subsequent PR to this PR to address that. There is a current bug where it adds a redundant AccountMeta to the instruction likely messing up account ordering and dropping the signed account meta from the authority.
  2. The Unwrap call to invoke_transfer_checked() includes possible multisig accounts which aren't relevant to the PDA escrow transfer (creates a transfer authority non-signature account meta which causes a MissingRequiredSignature error in token-2022 program).
    • The challenge with using add_extra_accounts_for_execute_cpi() directly is that I would still need to call it conditionally (dependent on mint hook), assemble the instruction & remove the multisig accounts from the accounts list. If I can just do the removal step, I can re-use invoke_transfer_checked() all the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants