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

Commit 2e2bf8f

Browse files
author
Joe C
authored
docs: transfer hook interface (#6176)
* docs: transfer hook interface * address review feedback * follow sidebar pattern for transfer hook pages
1 parent f9f8824 commit 2e2bf8f

File tree

6 files changed

+522
-2
lines changed

6 files changed

+522
-2
lines changed

docs/sidebars.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,20 @@ module.exports = {
6565
"account-compression/usage",
6666
]
6767
},
68+
{
69+
type: "category",
70+
label: "Transfer Hook Interface",
71+
collapsed: true,
72+
items: [
73+
{
74+
type: 'doc',
75+
label: 'Introduction',
76+
id: 'transfer-hook-interface',
77+
},
78+
"transfer-hook-interface/specification",
79+
"transfer-hook-interface/configuring-extra-accounts",
80+
"transfer-hook-interface/examples",
81+
]
82+
},
6883
],
6984
};

docs/src/token-2022/extensions.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,8 +1467,9 @@ harder.
14671467

14681468
#### Solution
14691469

1470-
To improve the situation, Token-2022 introduces the concept of the transfer-hook
1471-
interface and extension. A token creator must develop and deploy a program that
1470+
To improve the situation, Token-2022 introduces the concept of the
1471+
[transfer-hook interface](../transfer-hook-interface)
1472+
and extension. A token creator must develop and deploy a program that
14721473
implements the interface and then configure their token mint to use their program.
14731474

14741475
During transfer, Token-2022 calls into the program with the accounts specified

docs/src/transfer-hook-interface.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
title: Transfer Hook Interface
3+
---
4+
5+
The Transfer Hook Interface is one of several interfaces introduced within the
6+
Solana Program Library that can be implemented by any Solana program.
7+
8+
During transfers, Token-2022 calls a mint's configured transfer hook program
9+
using this interface, as described in the
10+
[Transfer Hook Extension Guide](../../token-2022/extensions#transfer-hook).
11+
Additionally, a
12+
[reference implementation](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/example)
13+
can be found in the SPL GitHub repository, detailing
14+
how one might implement this interface in their own program.
15+
16+
The Transfer Hook Interface is designed to allow token creators to "hook"
17+
additional functionality into token transfers. The token program CPIs into the
18+
transfer hook program using the interface-defined instruction. The transfer
19+
hook program can then perform any custom functionality.
20+
21+
In the case of Token-2022, a token creator configures a transfer hook program
22+
using a mint extension, and this extension tells Token-2022 which program to
23+
invoke whenever a transfer is conducted.
24+
25+
With this interface, programs can compose highly customizable transfer
26+
functionality that can be compatible with many other programs - particularly
27+
tokens who implement the SPL Token interface.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
---
2+
title: Configuring Extra Accounts
3+
---
4+
5+
As mentioned previously, programs who implement the Transfer Hook interface can
6+
provide additional custom functionality to token transfers. However, this
7+
functionality may require additional accounts beyond those that exist in a
8+
transfer instruction (source, mint, destination, etc.).
9+
10+
Part of the Transfer Hook interface specification is the validation account - an
11+
account which stores configurations for additional accounts required by the
12+
transfer hook program.
13+
14+
### The Validation Account
15+
16+
The validation account is a PDA off of the transfer hook program derived from
17+
the following seeds:
18+
19+
```
20+
"extra-account-metas" + <mint-address>
21+
```
22+
23+
As you can see, one validation account maps to one mint account. This means you
24+
can customize the additional required accounts on a per-mint basis!
25+
26+
The validation account stores configurations for extra accounts using
27+
[Type-Length-Value](https://en.wikipedia.org/wiki/Type%E2%80%93length%E2%80%93value)
28+
(TLV) encoding:
29+
- **Type:** The instruction discriminator, in this case `Execute`
30+
- **Length:** The total length of the subsequent data buffer, in this case a
31+
`u32`
32+
- **Data:** The data itself, in this case containing the extra account
33+
configurations
34+
35+
When a transfer hook program seeks to deserialize extra account configurations
36+
from a validation account, it can find the 8-byte instruction discriminator for
37+
`Execute`, then read the length, then use that length to deserialize the data.
38+
39+
The data itself is a list of fixed-size configuration objects serialized into a
40+
byte slab. Because the entries are fixed-length, we can use a custom "slice"
41+
structure which divides the length by the fixed-length to determine the number
42+
of entries.
43+
44+
This custom slice structure is called a `PodSlice` and is part of the Solana
45+
Program Library's
46+
[Pod](https://github.com/solana-labs/solana-program-library/tree/master/libraries/pod)
47+
library. The Pod library provides a handful of fixed-length types that
48+
implement the `bytemuck`
49+
[`Pod`](https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html) trait, as well
50+
as the `PodSlice`.
51+
52+
Another SPL library
53+
useful for Type-Length-Value encoded data is
54+
[Type-Length-Value](https://github.com/solana-labs/solana-program-library/tree/master/libraries/type-length-value)
55+
which is used extensively to manage TLV-encoded data structures.
56+
57+
### Dynamic Account Resolution
58+
59+
When clients build a transfer instruction to the token program, they must
60+
ensure the instruction includes all required accounts, especially the extra
61+
required accounts you've specified in the validation account.
62+
63+
These additional accounts must be _resolved_, and another library used to pull off
64+
the resolution of additional accounts for transfer hooks is
65+
[TLV Account Resolution](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution).
66+
67+
Using the TLV Account Resolution library, transfer hook programs can empower
68+
**dynamic account resolution** of additional required accounts. This means that
69+
no particular client or program needs to know the specific accounts your
70+
transfer hook requires. Instead, they can be automatically resolved from the
71+
validation account's data.
72+
73+
In fact, the Transfer Hook interface offers helpers that perform this account
74+
resolution in the
75+
[onchain](https://github.com/solana-labs/solana-program-library/blob/master/token/transfer-hook/interface/src/onchain.rs)
76+
and
77+
[offchain](https://github.com/solana-labs/solana-program-library/blob/master/token/transfer-hook/interface/src/offchain.rs)
78+
modules of the Transfer Hook interface crate.
79+
80+
The account resolution is powered by the way configurations for additional
81+
accounts are stored, and how they can be used to derive actual Solana addresses
82+
and roles (signer, writeable, etc.) for accounts.
83+
84+
### The `ExtraAccountMeta` Struct
85+
86+
A member of the TLV Account Resolution library, the
87+
[`ExtraAccountMeta`](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/libraries/tlv-account-resolution/src/account.rs#L75)
88+
struct allows account configurations to be serialized into a fixed-length data
89+
format of length 35 bytes.
90+
91+
```rust
92+
pub struct ExtraAccountMeta {
93+
/// Discriminator to tell whether this represents a standard
94+
/// `AccountMeta` or a PDA
95+
pub discriminator: u8,
96+
/// This `address_config` field can either be the pubkey of the account
97+
/// or the seeds used to derive the pubkey from provided inputs
98+
pub address_config: [u8; 32],
99+
/// Whether the account should sign
100+
pub is_signer: PodBool,
101+
/// Whether the account should be writable
102+
pub is_writable: PodBool,
103+
}
104+
```
105+
106+
As the documentation on the struct conveys, an `ExtraAccountMeta` can store
107+
configurations for three types of accounts:
108+
109+
|Discriminator|Account Type|
110+
|:------------|:-----------|
111+
|`0` | An account with a static address |
112+
| `1` | A PDA off of the transfer hook program itself |
113+
| `(1 << 7) + i ` | A PDA off of another program, where `i` is that program's index in the accounts list |
114+
115+
`1 << 7` is the top bit of the `u8`, or `128`. If the program you are deriving
116+
this PDA from is at index `9` of the accounts list for `Execute`, then the
117+
discriminator for this account configuration is `128 + 9 = 137`. More on
118+
determining this index later.
119+
120+
#### Accounts With Static Addresses
121+
122+
Static-address additional accounts are straightforward to serialize with
123+
`ExtraAccountMeta`. The discriminator is simply `0` and the `address_config` is
124+
the 32-byte public key.
125+
126+
#### PDAs Off the Transfer Hook Program
127+
128+
You might be wondering: "how can I store all of my PDA seeds in only 32 bytes?".
129+
Well, you don't. Instead, you tell the account resolution functionality _where_
130+
to find the seeds you need.
131+
132+
To do this, the transfer hook program can use the
133+
[`Seed`](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/libraries/tlv-account-resolution/src/seeds.rs#L38)
134+
enum to describe their seeds and where to find them. With the exception of
135+
literals, these seed configurations comprise only a small handful of bytes.
136+
137+
The following types of seeds are supported by the `Seed` enum and can be used to
138+
create an `address_config` array of bytes.
139+
- **Literal**: The literal seed itself encoded to bytes
140+
- **Instruction Data:** A slice of the instruction data, denoted by the `index`
141+
(offset) and `length` of bytes to slice
142+
- **AccountKey:** The address of some account in the list as bytes, denoted by
143+
the `index` at which this account can be found in the accounts list
144+
- **Account Data:** A slice of an account's data, denoted by the `account_index`
145+
at which this account can be found in the accounts list, as well as the
146+
`data_index` (offset) and `length` of bytes to slice
147+
148+
Here's an example of packing a list of `Seed` entries into a 32-byte
149+
`address_config`:
150+
151+
```rust
152+
let seed1 = Seed::Literal { bytes: vec![1; 8] };
153+
let seed2 = Seed::InstructionData {
154+
index: 0,
155+
length: 4,
156+
};
157+
let seed3 = Seed::AccountKey { index: 0 };
158+
let address_config: [u8; 32] = Seed::pack_into_address_config(
159+
&[seed1, seed2, seed3]
160+
)?;
161+
```
162+
163+
#### PDAs Off Another Program
164+
165+
Storing configurations for seeds for an address that is a PDA off of another
166+
program is the same as above. However, the program whose address this account is
167+
a PDA off of must be present in the account list. Its index in the accounts list
168+
is required to build the proper discriminator, and thus resolve the proper PDA.
169+
170+
```rust
171+
let program_index = 7;
172+
let seeds = &[seed1, seed2, seed3];
173+
let is_signer = false;
174+
let is_writable = true;
175+
176+
let extra_meta = ExtraAccountMeta::new_external_pda_with_seeds(
177+
program_index,
178+
seeds,
179+
is_signer,
180+
is_writable,
181+
)?;
182+
```
183+

0 commit comments

Comments
 (0)