diff --git a/docs/src/token-2022/extensions.mdx b/docs/src/token-2022/extensions.mdx index 7179fec2f51..e67bd14929c 100644 --- a/docs/src/token-2022/extensions.mdx +++ b/docs/src/token-2022/extensions.mdx @@ -1347,6 +1347,10 @@ A usable example program exists at Token-2022 uses this example program in tests to ensure that it properly uses the transfer hook interface. +The example program and the interface are powered by the +[spl-tlv-account-resolution](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution/README.md) +library, which is explained a bit more below. + #### How to use Developers must implement the `Execute` instruction, and optionally the @@ -1358,8 +1362,66 @@ at that instruction descriminator. Your program may implement multiple interface so any other instruction in your program can create the account at the program-derived address! -More information in the -[spl-transfer-hook-interface README](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook-interface/README.md). +When your program stores configurations for extra required accounts in the +well-defined program-derived address, it's possible to send an instruction - +such as `Execute` (transfer) - to your program with only accounts required +for the interface instruction, and all extra required accounts are +automatically resolved! + +#### Account Resolution + +Implementers of the transfer-hook interface are encouraged to make use of the +[spl-tlv-account-resolution](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution/README.md) +library to manage the additional required accounts for their transfer hook +program. + +This library is capable of storing two types of configurations for additional +required accounts: + +- Accounts with a fixed address +- Accounts with a **dynamic program-derived address** derived from seeds that +may come from any combination of the following: + - Hard-coded values, such as string literals or integers + - A slice of the instruction data provided to the transfer-hook program + - The address of another account in the total list of accounts + +When you store configurations for a dynamic Program-Derived Address within the +additional required accounts, the PDA itself is evaluated (or resolved) at the +time of instruction invocation using the instruction itself. This +occurs in the offchain and onchain helpers mentioned below, which leverage +the SPL TLV Account Resolution library to perform this resolution +automatically. + +#### An Example + +You have created a DAO to govern a community. Your DAO's authority is a +multisig account, and you want to ensure that any transfer of your token is +approved by the DAO. You also want to make sure that someone who intends to +transfer your token has the proper permissions to do so. + +Let's assume the DAO multisig has some **fixed address**. And let's assume that +in order to have the `can_transfer` permission, a user must have this +**dynamic program-derived address** associated with their wallet via the +following seeds: `"can_transfer" + `. + +Using the transfer-hook interface, you can store these configurations in the +well-defined program-derived address for your mint and program id. + +When a user attempts to transfer your token, they might provide to Token-2022: + +```rust +[source, mint, destination, owner/delegate] +``` + +Token-2022 will then call into your program, +**resolving the extra required accounts automatically** from your stored +configurations, to result in the following accounts being provided to your +program: + +```rust +[source, mint, destination, owner/delegate, dao_authority, can_transfer_pda] +``` + #### Utilities diff --git a/libraries/tlv-account-resolution/README.md b/libraries/tlv-account-resolution/README.md index 1c425a5a6f4..a02ff6ee079 100644 --- a/libraries/tlv-account-resolution/README.md +++ b/libraries/tlv-account-resolution/README.md @@ -21,7 +21,7 @@ impl SplDiscriminate for MyInstruction { const SPL_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([1; ArrayDiscriminator::LENGTH]); } -// Actually put it in the additional required account keys and signer / writable +// Prepare the additional required account keys and signer / writable let extra_metas = [ AccountMeta::new(Pubkey::new_unique(), false).into(), AccountMeta::new(Pubkey::new_unique(), true).into(), @@ -29,7 +29,7 @@ let extra_metas = [ AccountMeta::new_readonly(Pubkey::new_unique(), false).into(), ]; -// Assume that this buffer is actually account data, already allocated to `account_size` +// Allocate a new buffer with the proper `account_size` let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap(); let mut buffer = vec![0; account_size]; @@ -57,7 +57,7 @@ ExtraAccountMetaList::add_to_cpi_instruction::( ).unwrap(); ``` -For ease of use on-chain, `ExtraAccountMetaList::init_with_account_infos` is also +For ease of use on-chain, `ExtraAccountMetaList::init` is also provided to initialize directly from a set of given accounts. ## Motivation @@ -83,7 +83,7 @@ uses static account data. For example, let's imagine there's a `Transferable` interface, along with a `transfer` instruction. Some programs that implement `transfer` may need more -accounts than just the ones defined in the interface. How does a an on-chain or +accounts than just the ones defined in the interface. How does an on-chain or off-chain client figure out the additional required accounts? The "static" approach requires programs to write the extra required accounts to @@ -99,13 +99,35 @@ instruction and give the correct account infos. This approach could also be called a "state interface". +### Types of Required Accounts + +This library is capable of storing two types of configurations for additional +required accounts: + +- Accounts with a fixed address +- Accounts with a **dynamic program-derived address** derived from seeds that +may come from any combination of the following: + - Hard-coded values, such as string literals or integers + - A slice of the instruction data provided to the transfer-hook program + - The address of another account in the total list of accounts + +When you store configurations for a dynamic Program-Derived Address within the +additional required accounts, the PDA itself is evaluated (or resolved) at the +time of instruction invocation using the instruction itself. This +occurs in the offchain and onchain helpers mentioned below, which leverage +the SPL TLV Account Resolution library to perform this resolution +automatically. + ## How it works This library uses `spl-type-length-value` to read and write required instruction accounts from account data. Interface instructions must have an 8-byte discriminator, so that the exposed -`ExtraAccountMetaList` type can use the instruction discriminator as a `ArrayDiscriminator`. +`ExtraAccountMetaList` type can use the instruction discriminator as an +`ArrayDiscriminator`, which allows that discriminator to serve as a unique TLV +discriminator for identifying entries that correspond to that particular +instruction. This can be confusing. Typically, a type implements `SplDiscriminate`, so that the type can be written into TLV data. In this case, `ExtraAccountMetaList` is @@ -136,4 +158,4 @@ into return data, making it easy for on-chain and off-chain clients to consume. See the [relevant sRFC](https://forum.solana.com/t/srfc-00010-additional-accounts-request-transfer-spec/122) -for more information about the dynamic approach. +for more information about the dynamic approach. \ No newline at end of file diff --git a/token/transfer-hook-interface/README.md b/token/transfer-hook-interface/README.md index e0d8bd444cb..a5a290a2a12 100644 --- a/token/transfer-hook-interface/README.md +++ b/token/transfer-hook-interface/README.md @@ -38,25 +38,16 @@ pub fn process_instruction( return Err(ProgramError::InvalidSeeds); } - // Get the extra account metas from the account data + // Load the extra required accounts from the validation account let data = extra_account_metas_info.try_borrow_data()?; - let state = TlvStateBorrowed::unpack(&data).unwrap(); - let extra_account_metas = - ExtraAccountMetaList::unpack_with_tlv_state::(&state)?; - - // If incorrect number of accounts is provided, error - let extra_account_infos = account_info_iter.as_slice(); - let account_metas = extra_account_metas.data(); - if extra_account_infos.len() != account_metas.len() { - return Err(ProgramError::InvalidInstructionData); - } - // Let's require that they're provided in the correct order - for (i, account_info) in extra_account_infos.iter().enumerate() { - if &account_metas[i] != account_info { - return Err(ProgramError::InvalidInstructionData); - } - } + // Check the provided accounts against the validation data + ExtraAccountMetaList::check_account_infos::( + accounts, + &TransferHookInstruction::Execute { amount }.pack(), + program_id, + &data, + )?; Ok(()) } @@ -64,8 +55,8 @@ pub fn process_instruction( ### Motivation -Token creators may need more control over transfers of their token. The most -prominent use case revolves around NFT royalties. Whenever a token is moved, +Token creators may need more control over how their token is transferred. The +most prominent use case revolves around NFT royalties. Whenever a token is moved, the creator should be entitled to royalties, but due to the design of the current token program, it's impossible to stop a transfer at the protocol level. @@ -79,28 +70,92 @@ harder. ### Solution -To give more flexibility to token creators and improve the situation for everyone, -`spl-transfer-hook-interface` introduces the concept of an interface integrated -with `spl-token-2022`. A token creator must develop and deploy a program that +To improve the situation, Token-2022 introduces the concept of the transfer-hook +interface and extension. A token creator must develop and deploy a program that implements the interface and then configure their token mint to use their program. -During transfer, token-2022 calls into the program with the accounts specified +During transfer, Token-2022 calls into the program with the accounts specified at a well-defined program-derived address for that mint and program id. This call happens after all other transfer logic, so the accounts reflect the *end* state of the transfer. -A developer must implement the `Execute` instruction, and the +### How to Use + +Developers must implement the `Execute` instruction, and optionally the `InitializeExtraAccountMetaList` instruction to write the required additional account pubkeys into the program-derived address defined by the mint and program id. -Side note: it's technically not required to implement `InitializeExtraAccountMetaList` +Note: it's technically not required to implement `InitializeExtraAccountMetaList` at that instruction descriminator. Your program may implement multiple interfaces, so any other instruction in your program can create the account at the program-derived address! -This library provides offchain and onchain helpers for resolving the additional -accounts required. See +When your program stores configurations for extra required accounts in the +well-defined program-derived address, it's possible to send an instruction - +such as `Execute` (transfer) - to your program with only accounts required +for the interface instruction, and all extra required accounts are +automatically resolved! + +### Account Resolution + +Implementers of the transfer-hook interface are encouraged to make use of the +[spl-tlv-account-resolution](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution/README.md) +library to manage the additional required accounts for their transfer hook +program. + +This library is capable of storing two types of configurations for additional +required accounts: + +- Accounts with a fixed address +- Accounts with a **dynamic program-derived address** derived from seeds that +may come from any combination of the following: + - Hard-coded values, such as string literals or integers + - A slice of the instruction data provided to the transfer-hook program + - The address of another account in the total list of accounts + +When you store configurations for a dynamic Program-Derived Address within the +additional required accounts, the PDA itself is evaluated (or resolved) at the +time of instruction invocation using the instruction itself. This +occurs in the offchain and onchain helpers mentioned below, which leverage +the SPL TLV Account Resolution library to perform this resolution +automatically. + +### An Example + +You have created a DAO to govern a community. Your DAO's authority is a +multisig account, and you want to ensure that any transfer of your token is +approved by the DAO. You also want to make sure that someone who intends to +transfer your token has the proper permissions to do so. + +Let's assume the DAO multisig has some **fixed address**. And let's assume that +in order to have the `can_transfer` permission, a user must have this +**dynamic program-derived address** associated with their wallet via the +following seeds: `"can_transfer" + `. + +Using the transfer-hook interface, you can store these configurations in the +well-defined program-derived address for your mint and program id. + +When a user attempts to transfer your token, they might provide to Token-2022: + +```rust +[source, mint, destination, owner/delegate] +``` + +Token-2022 will then call into your program, +**resolving the extra required accounts automatically** from your stored +configurations, to result in the following accounts being provided to your +program: + +```rust +[source, mint, destination, owner/delegate, dao_authority, can_transfer_pda] +``` + +### Utilities + +The `spl-transfer-hook-interface` library provides offchain and onchain helpers +for resolving the additional accounts required. See [invoke.rs](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook-interface/src/invoke.rs) for usage on-chain, and [offchain.rs](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook-interface/src/offchain.rs) -for fetching the additional required account metas. +for fetching the additional required account metas with any async off-chain client +like `BanksClient` or `RpcClient`.