Skip to main content

sendfun_sdk/
transfer_hook.rs

1//! Resolves the Token-2022 transfer-hook accounts of an instruction from
2//! accounts that you fetch.
3//!
4//! A builder resolves each transfer of its instruction from the
5//! [`HookState`] of each mint, as `spl-tlv-account-resolution` 0.11.4 does.
6//! It appends the merged list after all accounts of the instruction.
7//!
8//! The resolver refuses a list that no resolver can resolve before the
9//! instruction runs, and a list with a seed on the transfer amount
10//! ([`Unresolvable`]). An instruction can still fail when a hook refuses the
11//! transfer, the chain holds an extra read-only, or a mint is paused.
12//! Simulate the transaction. A router that cannot simulate checks a trade
13//! with [`route`].
14//!
15//! Read a mint with [`MintInfo::load`](crate::utils::MintInfo::load) and fill
16//! its [`HookState`]: the builders and [`route::trade`] take the `MintInfo`.
17
18use std::collections::BTreeMap;
19use std::fmt;
20
21use solana_address::{Address, MAX_SEEDS};
22use solana_instruction::AccountMeta;
23use spl_discriminator::{ArrayDiscriminator, SplDiscriminate};
24use spl_tlv_account_resolution::account::ExtraAccountMeta;
25use spl_tlv_account_resolution::pubkey_data::PubkeyData;
26use spl_tlv_account_resolution::seeds::Seed;
27use spl_tlv_account_resolution::solana_program_error::ProgramError;
28use spl_tlv_account_resolution::state::ExtraAccountMetaList;
29use spl_token_2022_interface::extension::transfer_hook::TransferHook;
30use spl_type_length_value::error::TlvError;
31use spl_type_length_value::state::TlvStateBorrowed;
32
33use crate::transfer_fee::{
34	TransferFeeConfig, extension, mint_extensions, spl_token_mint,
35};
36use crate::utils::{MintInfo, TradeMints};
37
38pub mod route;
39
40/// Discriminator of the hook's `Execute` instruction. Also the TLV type of the
41/// entry that holds the account list.
42pub const EXECUTE_DISCRIMINATOR: [u8; 8] =
43	[105, 37, 101, 197, 75, 251, 102, 26];
44
45/// Seeds of the validation account: `["extra-account-metas", mint]`, under
46/// the hook program.
47pub const VALIDATION_SEED: &[u8] = b"extra-account-metas";
48
49/// `Execute` accounts: source, mint, destination, authority, validation
50/// account.
51const VALIDATION_INDEX: usize = 4;
52const EXECUTE_ACCOUNTS: usize = 5;
53
54/// The `Execute` data: the discriminator, then the transfer amount as a
55/// little-endian `u64`.
56const EXECUTE_DATA_LEN: usize = 16;
57
58/// Mint and owner: the token-account bytes that no transfer changes.
59const TOKEN_ACCOUNT_KNOWN_LEN: usize = 64;
60
61const PUBKEY_LEN: usize = 32;
62
63/// 128 and above: a PDA of the program at index `discriminator - 128`.
64const META_FIXED: u8 = 0;
65const META_HOOK_PDA: u8 = 1;
66const META_PUBKEY_DATA: u8 = 2;
67const META_EXTERNAL_PDA: u8 = 128;
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct AccountView<'a> {
71	pub owner: &'a Address,
72	pub data: &'a [u8],
73}
74
75/// What a lookup knows of an account.
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum Fetched<'a> {
78	Account(AccountView<'a>),
79	/// Fetched, and no account exists at the address.
80	Missing,
81	/// Not fetched yet.
82	NotFetched,
83}
84
85#[must_use]
86pub fn find_validation_address(mint: &Address, program: &Address) -> Address {
87	Address::find_program_address(&[VALIDATION_SEED, mint.as_ref()], program).0
88}
89
90/// A decoded mint. [`MintInfo::load`] reads one.
91#[derive(Clone, Copy, Debug, PartialEq)]
92pub(crate) struct MintState {
93	pub token_program: Address,
94	/// Fixed when the mint is created.
95	pub decimals: u8,
96	pub transfer_fee: Option<TransferFeeConfig>,
97	/// The hook program. Token-2022 calls it on every transfer. The instruction
98	/// must carry its accounts.
99	pub transfer_hook: Option<Address>,
100}
101
102impl MintState {
103	/// Fails with `ProgramError::IncorrectProgramId` if no token program owns
104	/// the mint, and with another error if its data is not an initialized
105	/// mint. An SPL Token mint is the 82-byte base mint alone, with no hook
106	/// and no fee.
107	pub(crate) fn load(mint: AccountView<'_>) -> Result<Self, ProgramError> {
108		let token_program = *mint.owner;
109		let Some(state) = mint_extensions(mint.data, &token_program)? else {
110			return Ok(Self {
111				token_program,
112				decimals: spl_token_mint(mint.data)?.decimals,
113				transfer_fee: None,
114				transfer_hook: None,
115			});
116		};
117		Ok(Self {
118			token_program,
119			decimals: state.base.decimals,
120			transfer_fee: extension::<TransferFeeConfig>(&state)?.copied(),
121			// Token-2022 calls a hook only when `program_id` is set.
122			transfer_hook: extension::<TransferHook>(&state)?
123				.and_then(|hook| Option::from(hook.program_id)),
124		})
125	}
126}
127
128/// A list that no resolver can resolve before the instruction runs, or that
129/// reads the transfer amount.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131#[non_exhaustive]
132pub enum Unresolvable {
133	MalformedList,
134	NoExecuteEntry,
135	/// An extra is at an account index above 255.
136	ListTooLong,
137	MalformedExtra,
138	MalformedSeed,
139	/// A PDA has more than 15 seeds. The bump is the 16th.
140	TooManySeeds,
141	SeedTooLong,
142	/// SPL's resolver fails on an extra: a read past the end of an account's
143	/// data, or of a missing account, or seeds no address takes.
144	ExtraUnresolved,
145	/// A seed reads the transfer amount.
146	ReadsTransferAmount,
147	/// An extra reads past byte 16 of the `Execute` data: the discriminator,
148	/// then the amount.
149	ReadsPastInstructionData,
150	/// An extra names an account at its own index or later.
151	NamesUnresolvedAccount,
152	/// An extra reads data that changes before the hook runs. This is data
153	/// past the mint and owner of a token account that the instruction moves,
154	/// or an account that it writes first.
155	ReadsSwapTimeData,
156}
157
158impl fmt::Display for Unresolvable {
159	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160		f.write_str(match self {
161			Self::MalformedList => "malformed list",
162			Self::NoExecuteEntry => "no Execute entry in the list",
163			Self::ListTooLong => "list too long",
164			Self::MalformedExtra => "malformed extra",
165			Self::MalformedSeed => "malformed seed",
166			Self::TooManySeeds => "a PDA has more seeds than an address allows",
167			Self::SeedTooLong => "seed longer than 32 bytes",
168			Self::ExtraUnresolved => "an extra does not resolve",
169			Self::ReadsTransferAmount => "a seed reads the transfer amount",
170			Self::ReadsPastInstructionData => {
171				"an extra reads past the end of the hook's instruction data"
172			}
173			Self::NamesUnresolvedAccount => {
174				"an extra names an account not yet resolved"
175			}
176			Self::ReadsSwapTimeData => {
177				"an extra reads account data the instruction changes first"
178			}
179		})
180	}
181}
182
183#[derive(Clone, Debug, PartialEq, Eq)]
184#[non_exhaustive]
185pub enum HookError {
186	/// The mint infos do not hold these accounts. Fetch them. Insert them into
187	/// a copy of each mint info. Build again until the build does not return
188	/// `NotLoaded`.
189	NotLoaded(Vec<Address>),
190	/// The validation account is missing, or the hook program does not own it.
191	/// Token-2022 still calls the hook, and the transfer fails.
192	NoList,
193	/// A mint in a [`HookState`] is not a mint that a token program owns, or
194	/// its extensions do not decode. [`HookState::missing`] gives it.
195	MalformedMint,
196	Unresolvable(Unresolvable),
197}
198
199impl fmt::Display for HookError {
200	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201		match self {
202			Self::NotLoaded(_) => {
203				write!(f, "transfer hook accounts not loaded")
204			}
205			Self::NoList => {
206				write!(f, "transfer hook has no ExtraAccountMetaList")
207			}
208			Self::MalformedMint => {
209				write!(f, "a transfer's mint does not decode as a mint")
210			}
211			Self::Unresolvable(reason) => write!(
212				f,
213				"transfer hook cannot be resolved ahead of the instruction: \
214				 {reason}"
215			),
216		}
217	}
218}
219
220impl std::error::Error for HookError {}
221
222impl From<Unresolvable> for HookError {
223	fn from(reason: Unresolvable) -> Self {
224		Self::Unresolvable(reason)
225	}
226}
227
228/// A mint's armed hook and the state of its list.
229#[derive(Clone, Debug, PartialEq, Eq)]
230pub(crate) struct Hook {
231	mint: Address,
232	program: Address,
233	validation: Address,
234	list: List,
235}
236
237#[derive(Clone, Debug, PartialEq, Eq)]
238enum List {
239	NotFetched,
240	Missing,
241	Loaded(Vec<u8>),
242}
243
244impl Hook {
245	/// The hook of `mint`, which runs `program`. Returns `None` for a mint
246	/// that runs no hook. Reads the list at its validation address from
247	/// `accounts`. A list that the hook program does not own reads as missing.
248	pub(crate) fn load<'a>(
249		mint: &Address,
250		program: Option<Address>,
251		accounts: impl Fn(&Address) -> Fetched<'a>,
252	) -> Option<Self> {
253		let program = program?;
254		let validation = find_validation_address(mint, &program);
255		let list = match accounts(&validation) {
256			Fetched::Account(account) if *account.owner == program => {
257				List::Loaded(account.data.to_vec())
258			}
259			Fetched::Account(_) | Fetched::Missing => List::Missing,
260			Fetched::NotFetched => List::NotFetched,
261		};
262		Some(Self {
263			mint: *mint,
264			program,
265			validation,
266			list,
267		})
268	}
269
270	/// The list's bytes. `None` until it is fetched, and for a missing list.
271	fn list(&self) -> Option<&[u8]> {
272		match &self.list {
273			List::Loaded(data) => Some(data),
274			List::NotFetched | List::Missing => None,
275		}
276	}
277
278	fn extras(&self) -> Result<Vec<ExtraAccountMeta>, HookError> {
279		match &self.list {
280			List::NotFetched => {
281				Err(HookError::NotLoaded(vec![self.validation]))
282			}
283			List::Missing => Err(HookError::NoList),
284			List::Loaded(data) => Ok(screen_list(data)?),
285		}
286	}
287}
288
289/// The hooks of a market's two mints, each read from its own [`MintInfo`].
290/// `None` is a hook-free mint.
291#[derive(Clone, Debug, PartialEq, Eq)]
292pub(crate) struct MarketHooks {
293	base: Option<Hook>,
294	quote: Option<Hook>,
295}
296
297impl MarketHooks {
298	/// The hooks of both mints. Reads each list at its validation address from
299	/// the mint's own [`HookState`]. A list that the hook program does not own
300	/// reads as missing.
301	pub(crate) fn load(base: &MintInfo, quote: &MintInfo) -> Self {
302		Self {
303			base: Self::hook_of(base),
304			quote: Self::hook_of(quote),
305		}
306	}
307
308	fn hook_of(info: &MintInfo) -> Option<Hook> {
309		Hook::load(&info.address, info.hook_program, |address| {
310			info.fetched(address)
311		})
312	}
313
314	/// Both hooks, base first.
315	pub(crate) fn hooks(&self) -> Vec<&Hook> {
316		[&self.base, &self.quote].into_iter().flatten().collect()
317	}
318}
319
320/// The hook accounts of `legs` on the market of `mints`. Each list comes from
321/// its own mint's [`HookState`]. A list read looks in both mints' snapshots,
322/// then in `cache`.
323///
324/// # Errors
325///
326/// [`HookError::NotLoaded`] with each list of a moved mint that its snapshot
327/// lacks. Else the errors of [`resolve_legs`].
328pub(crate) fn resolve_market<'i, 'a: 'i>(
329	mints: TradeMints<'i>,
330	legs: &HookLegs,
331	cache: impl Fn(&Address) -> Fetched<'a>,
332) -> Result<Step<Resolved>, HookError> {
333	let hooks = MarketHooks::load(mints.base, mints.quote);
334	let hooks = hooks.hooks();
335	let lists: Vec<Address> = hooks
336		.iter()
337		.filter(|hook| {
338			hook.list == List::NotFetched
339				&& legs.legs.iter().any(|leg| leg.mint == hook.mint)
340		})
341		.map(|hook| hook.validation)
342		.collect();
343	if !lists.is_empty() {
344		return Err(HookError::NotLoaded(lists));
345	}
346	resolve_legs(&hooks, legs, &|key| {
347		[mints.base, mints.quote]
348			.into_iter()
349			.map(|info| info.fetched(key))
350			.find(|fetched| !matches!(fetched, Fetched::NotFetched))
351			.unwrap_or_else(|| cache(key))
352	})
353}
354
355/// One token transfer an instruction makes.
356#[derive(Clone, Copy, Debug, PartialEq, Eq)]
357pub(crate) struct HookLeg {
358	pub source: Address,
359	pub mint: Address,
360	pub destination: Address,
361	/// Signs the transfer and owns `source`.
362	pub authority: Address,
363	pub destination_owner: Address,
364}
365
366/// The transfers of one instruction, in the order the program makes them.
367#[derive(Clone, Debug, Default, PartialEq, Eq)]
368pub(crate) struct HookLegs {
369	pub legs: Vec<HookLeg>,
370	/// Accounts the instruction writes before its transfers. A list that reads
371	/// one does not resolve.
372	pub written: Vec<Address>,
373}
374
375/// An account that exists, as a lookup fetched it.
376#[derive(Clone, Debug, PartialEq, Eq)]
377pub struct FetchedAccount {
378	pub owner: Address,
379	pub data: Vec<u8>,
380}
381
382/// A snapshot of the accounts that the hook lists of its mints read. A
383/// builder resolves the hook accounts from the snapshot of each mint.
384///
385/// Fill the snapshot with [`missing`](Self::missing): fetch the accounts it
386/// returns, [`insert`](Self::insert) each one and call it again, until it
387/// returns none. A builder reads no account outside the snapshot. An account
388/// that the snapshot does not have, such as a PDA of the trader, fails with
389/// [`HookError::NotLoaded`], which names the accounts.
390///
391/// A hook armed or changed after the fill makes the transaction fail in
392/// simulation. Fill the snapshot again.
393#[derive(Clone, Debug, Default, PartialEq, Eq)]
394pub struct HookState {
395	/// Each fetched account: `Some` where it exists, `None` where it does
396	/// not. An address it does not hold was not fetched.
397	pub accounts: BTreeMap<Address, Option<FetchedAccount>>,
398}
399
400impl HookState {
401	/// Records what a fetch of `address` returned: `None` for no account.
402	pub fn insert(
403		&mut self,
404		address: Address,
405		account: Option<FetchedAccount>,
406	) {
407		self.accounts.insert(address, account);
408	}
409
410	/// What the snapshot knows of `address`.
411	#[must_use]
412	pub fn fetched(&self, address: &Address) -> Fetched<'_> {
413		match self.accounts.get(address) {
414			Some(Some(account)) => Fetched::Account(AccountView {
415				owner: &account.owner,
416				data: &account.data,
417			}),
418			Some(None) => Fetched::Missing,
419			None => Fetched::NotFetched,
420		}
421	}
422
423	/// The accounts to fetch next for a snapshot of `mints`: the mints, then
424	/// each hook's list, then each account a list reads at a fixed address.
425	/// Empty when the snapshot holds them all.
426	///
427	/// An address is fixed when no trade sets it: a fixed key, or a PDA or
428	/// data key whose inputs are literal seeds, the first 8 bytes of the
429	/// `Execute` data (the discriminator), the mint, the validation account
430	/// and earlier fixed extras. It names no extra that reads the source, the
431	/// destination or the authority.
432	///
433	/// A missing mint runs no hook. For a missing list, or a list that no
434	/// resolver can resolve, it returns no more accounts, and a build fails.
435	///
436	/// # Errors
437	///
438	/// [`HookError::MalformedMint`] for a mint that does not decode.
439	pub fn missing(
440		&self,
441		mints: &[Address],
442	) -> Result<Vec<Address>, HookError> {
443		let mut distinct: Vec<Address> = Vec::new();
444		for mint in mints {
445			if !distinct.contains(mint) {
446				distinct.push(*mint);
447			}
448		}
449		let unread: Vec<Address> = distinct
450			.iter()
451			.filter(|mint| !self.accounts.contains_key(mint))
452			.copied()
453			.collect();
454		if !unread.is_empty() {
455			return Ok(unread);
456		}
457		let mut hooks = Vec::new();
458		let mut fetch = Vec::new();
459		for mint in &distinct {
460			let Fetched::Account(account) = self.fetched(mint) else {
461				continue;
462			};
463			let state = MintState::load(account)
464				.map_err(|_| HookError::MalformedMint)?;
465			match Hook::load(mint, state.transfer_hook, |address| {
466				self.fetched(address)
467			}) {
468				Some(hook) if hook.list == List::NotFetched => {
469					fetch.push(hook.validation);
470				}
471				Some(hook) => hooks.push(hook),
472				None => {}
473			}
474		}
475		if !fetch.is_empty() {
476			return Ok(fetch);
477		}
478		for hook in &hooks {
479			for address in self.fixed_reads(hook) {
480				if !fetch.contains(&address) {
481					fetch.push(address);
482				}
483			}
484		}
485		Ok(fetch)
486	}
487
488	/// The accounts at a fixed address whose data `hook`'s list reads, which
489	/// the snapshot lacks.
490	fn fixed_reads(&self, hook: &Hook) -> Vec<Address> {
491		let Ok(extras) = hook.extras() else {
492			return Vec::new();
493		};
494		let list = hook.list().unwrap_or_default();
495		let data = execute_data();
496		// Each `Execute` account's address when no trade sets it: the mint
497		// and the validation account, then each fixed extra.
498		let mut fixed: Vec<Option<Address>> =
499			vec![None, Some(hook.mint), None, None, Some(hook.validation)];
500		let mut fetch = Vec::new();
501		for extra in &extras {
502			let mut bytes: Vec<Option<Vec<u8>>> = vec![None; fixed.len()];
503			let mut known = true;
504			for (index, _) in data_reads(extra) {
505				// `None` where the trade sets the account.
506				let address = fixed.get(index).copied().flatten();
507				let read = if index == VALIDATION_INDEX {
508					Some(list.to_vec())
509				} else {
510					address.and_then(|address| match self.fetched(&address) {
511						Fetched::Account(account) => {
512							Some(account.data.to_vec())
513						}
514						// On chain a missing account has no data.
515						Fetched::Missing => Some(Vec::new()),
516						Fetched::NotFetched => {
517							if !fetch.contains(&address) {
518								fetch.push(address);
519							}
520							None
521						}
522					})
523				};
524				match (read, bytes.get_mut(index)) {
525					(Some(read), Some(slot)) => *slot = Some(read),
526					_ => known = false,
527				}
528			}
529			let keyed = key_reads(extra)
530				.into_iter()
531				.all(|index| fixed.get(index).copied().flatten().is_some());
532			let resolved = (known && keyed)
533				.then(|| {
534					extra
535						.resolve(&data, &hook.program, |index| {
536							fixed.get(index).and_then(Option::as_ref).map(
537								|key| {
538									(
539										key,
540										bytes
541											.get(index)
542											.and_then(|read| read.as_deref()),
543									)
544								},
545							)
546						})
547						.ok()
548				})
549				.flatten();
550			fixed.push(resolved.map(|meta| meta.pubkey));
551		}
552		fetch
553	}
554}
555
556/// One leg's `Execute`: every account the hook receives, with the role SPL's
557/// resolver gives it, and the flags the list declares for each extra.
558#[derive(Clone, Debug, PartialEq, Eq)]
559pub(crate) struct LegExecute {
560	pub(crate) program: Address,
561	/// Source, mint, destination, authority, the validation account, then
562	/// each extra.
563	pub(crate) metas: Vec<AccountMeta>,
564	/// `(signer, writable)` as the list declares each extra.
565	pub(crate) declared: Vec<(bool, bool)>,
566}
567
568impl LegExecute {
569	/// SPL resolver order: the extras, the hook program, the validation
570	/// account.
571	fn trailing(&self) -> impl Iterator<Item = AccountMeta> + '_ {
572		let validation = self
573			.metas
574			.get(VALIDATION_INDEX)
575			.map_or_else(Address::default, |meta| meta.pubkey);
576		self.metas.iter().skip(EXECUTE_ACCOUNTS).cloned().chain([
577			AccountMeta::new_readonly(self.program, false),
578			AccountMeta::new_readonly(validation, false),
579		])
580	}
581}
582
583pub(crate) struct Resolved {
584	pub(crate) accounts: Vec<AccountMeta>,
585	pub(crate) executes: Vec<LegExecute>,
586	/// Each account whose data a list read through the lookup.
587	pub(crate) reads: Vec<Address>,
588}
589
590pub(crate) enum Step<T> {
591	Done(T),
592	Fetch(Vec<Address>),
593}
594
595/// The token accounts the legs move, each with its mint and owner.
596fn moved_accounts(
597	legs: &HookLegs,
598) -> Vec<(Address, [u8; TOKEN_ACCOUNT_KNOWN_LEN])> {
599	let mut moved: Vec<(Address, [u8; TOKEN_ACCOUNT_KNOWN_LEN])> = Vec::new();
600	for leg in &legs.legs {
601		for (account, owner) in [
602			(leg.source, leg.authority),
603			(leg.destination, leg.destination_owner),
604		] {
605			if !moved.iter().any(|(key, _)| *key == account) {
606				moved.push((account, token_account_prefix(&leg.mint, &owner)));
607			}
608		}
609	}
610	moved
611}
612
613fn token_account_prefix(
614	mint: &Address,
615	owner: &Address,
616) -> [u8; TOKEN_ACCOUNT_KNOWN_LEN] {
617	let mut prefix = [0; TOKEN_ACCOUNT_KNOWN_LEN];
618	let (mint_bytes, owner_bytes) = prefix.split_at_mut(PUBKEY_LEN);
619	mint_bytes.copy_from_slice(mint.as_ref());
620	owner_bytes.copy_from_slice(owner.as_ref());
621	prefix
622}
623
624/// The hook accounts of `legs`, or the accounts that `accounts` has not
625/// fetched. `hooks` holds the hook of each hooked mint that the legs move. A
626/// leg whose mint has no hook in `hooks` is hook-free.
627///
628/// Each extra resolves as SPL's resolver resolves it, from the transfer's
629/// `Execute`: source, mint, destination, authority and the validation
630/// account, all read-only. An extra that repeats a key of the `Execute` is
631/// read-only when each earlier copy is read-only. No extra signs.
632///
633/// The hook program and the validation account follow the extras. The legs
634/// merge into one list. Each address occurs once, writable if one leg makes
635/// it writable.
636///
637/// A token account that a leg moves reads as its mint and owner, so it need
638/// not exist before the instruction.
639///
640/// # Errors
641///
642/// [`HookError::NotLoaded`] for a list that was not fetched.
643/// [`HookError::NoList`] for a missing list.
644/// [`HookError::Unresolvable`] for a list that does not resolve.
645pub(crate) fn resolve_legs<'a>(
646	hooks: &[&Hook],
647	legs: &HookLegs,
648	accounts: &impl Fn(&Address) -> Fetched<'a>,
649) -> Result<Step<Resolved>, HookError> {
650	let moved = moved_accounts(legs);
651	let mut executes = Vec::new();
652	let mut fetch: Vec<Address> = Vec::new();
653	let mut reader = Reader {
654		moved: &moved,
655		written: &legs.written,
656		accounts,
657		reads: Vec::new(),
658	};
659	for leg in &legs.legs {
660		let Some(hook) = hooks.iter().find(|hook| hook.mint == leg.mint) else {
661			continue;
662		};
663		match resolve_leg(hook, leg, &mut reader)? {
664			Step::Done(execute) => executes.push(execute),
665			// The other legs still name what they read.
666			Step::Fetch(addresses) => {
667				for address in addresses {
668					if !fetch.contains(&address) {
669						fetch.push(address);
670					}
671				}
672			}
673		}
674	}
675	if !fetch.is_empty() {
676		return Ok(Step::Fetch(fetch));
677	}
678	let mut merged: Vec<AccountMeta> = Vec::new();
679	for execute in &executes {
680		for meta in execute.trailing() {
681			match merged
682				.iter_mut()
683				.find(|account| account.pubkey == meta.pubkey)
684			{
685				Some(existing) => existing.is_writable |= meta.is_writable,
686				None => merged.push(meta),
687			}
688		}
689	}
690	Ok(Step::Done(Resolved {
691		accounts: merged,
692		executes,
693		reads: reader.reads,
694	}))
695}
696
697/// Where SPL's resolver finds the data of an `Execute` account.
698struct Reader<'r, F> {
699	moved: &'r [(Address, [u8; TOKEN_ACCOUNT_KNOWN_LEN])],
700	written: &'r [Address],
701	accounts: &'r F,
702	/// Each account read through `accounts`.
703	reads: Vec<Address>,
704}
705
706enum Data {
707	Bytes(Vec<u8>),
708	NotFetched,
709}
710
711impl<'a, F: Fn(&Address) -> Fetched<'a>> Reader<'_, F> {
712	/// The data of `key` that a read up to byte `end` sees on chain.
713	fn read(
714		&mut self,
715		key: &Address,
716		end: usize,
717	) -> Result<Data, Unresolvable> {
718		if let Some((_, prefix)) =
719			self.moved.iter().find(|(account, _)| account == key)
720		{
721			// Past the mint and owner, the transfer changes the account.
722			if end > TOKEN_ACCOUNT_KNOWN_LEN {
723				return Err(Unresolvable::ReadsSwapTimeData);
724			}
725			return Ok(Data::Bytes(prefix.to_vec()));
726		}
727		if self.written.contains(key) {
728			return Err(Unresolvable::ReadsSwapTimeData);
729		}
730		Ok(match (self.accounts)(key) {
731			Fetched::Account(account) => {
732				self.record(key);
733				Data::Bytes(account.data.to_vec())
734			}
735			Fetched::Missing => {
736				self.record(key);
737				// On chain a missing account has no data, and the read fails.
738				Data::Bytes(Vec::new())
739			}
740			Fetched::NotFetched => Data::NotFetched,
741		})
742	}
743
744	fn record(&mut self, key: &Address) {
745		if !self.reads.contains(key) {
746			self.reads.push(*key);
747		}
748	}
749}
750
751/// Rust `ExtraAccountMetaList::add_to_instruction` for one transfer.
752fn resolve_leg<'a, F: Fn(&Address) -> Fetched<'a>>(
753	hook: &Hook,
754	leg: &HookLeg,
755	reader: &mut Reader<'_, F>,
756) -> Result<Step<LegExecute>, HookError> {
757	let extras = hook.extras()?;
758	let list = hook.list().unwrap_or_default();
759	let data = execute_data();
760
761	// Token-2022 passes the five `Execute` accounts read-only.
762	let mut metas: Vec<AccountMeta> = [
763		leg.source,
764		leg.mint,
765		leg.destination,
766		leg.authority,
767		hook.validation,
768	]
769	.into_iter()
770	.map(|key| AccountMeta::new_readonly(key, false))
771	.collect();
772	let mut declared = Vec::with_capacity(extras.len());
773	for extra in &extras {
774		let mut bytes: Vec<Option<Vec<u8>>> = vec![None; metas.len()];
775		for (index, end) in data_reads(extra) {
776			let Some(slot) = bytes.get_mut(index) else {
777				// The list check allows only reads of earlier accounts.
778				return Err(Unresolvable::NamesUnresolvedAccount.into());
779			};
780			if index == VALIDATION_INDEX {
781				*slot = Some(list.to_vec());
782				continue;
783			}
784			let key =
785				metas.get(index).map(|meta| meta.pubkey).unwrap_or_default();
786			match reader.read(&key, end)? {
787				Data::Bytes(read) => *slot = Some(read),
788				Data::NotFetched => return Ok(Step::Fetch(vec![key])),
789			}
790		}
791		let resolved = extra
792			.resolve(&data, &hook.program, |index| {
793				metas.get(index).map(|meta| {
794					(
795						&meta.pubkey,
796						bytes.get(index).and_then(|read| read.as_deref()),
797					)
798				})
799			})
800			.map_err(|_| Unresolvable::ExtraUnresolved)?;
801		// `de_escalate_account_meta`: a repeat of a key of the `Execute` is
802		// read-only when each earlier copy is read-only. Never a signer.
803		let earlier = metas
804			.iter()
805			.filter(|meta| meta.pubkey == resolved.pubkey)
806			.map(|meta| meta.is_writable)
807			.reduce(|any, writable| any || writable);
808		metas.push(AccountMeta {
809			pubkey: resolved.pubkey,
810			is_signer: false,
811			is_writable: resolved.is_writable && earlier != Some(false),
812		});
813		declared
814			.push((bool::from(extra.is_signer), bool::from(extra.is_writable)));
815	}
816	Ok(Step::Done(LegExecute {
817		program: hook.program,
818		metas,
819		declared,
820	}))
821}
822
823/// The `Execute` data that a resolution reads: the discriminator, then a zero
824/// amount. No screened list reads the amount.
825const fn execute_data() -> [u8; EXECUTE_DATA_LEN] {
826	let mut data = [0; EXECUTE_DATA_LEN];
827	let (discriminator, _) = data.split_at_mut(EXECUTE_DISCRIMINATOR.len());
828	discriminator.copy_from_slice(&EXECUTE_DISCRIMINATOR);
829	data
830}
831
832struct Execute;
833
834impl SplDiscriminate for Execute {
835	const SPL_DISCRIMINATOR: ArrayDiscriminator =
836		ArrayDiscriminator::new(EXECUTE_DISCRIMINATOR);
837}
838
839fn unpack_list(data: &[u8]) -> Result<Vec<ExtraAccountMeta>, Unresolvable> {
840	let list_error = |error: ProgramError| {
841		if error == ProgramError::from(TlvError::TypeNotFound) {
842			Unresolvable::NoExecuteEntry
843		} else {
844			Unresolvable::MalformedList
845		}
846	};
847	let state = TlvStateBorrowed::unpack(data).map_err(list_error)?;
848	let extras = ExtraAccountMetaList::unpack_with_tlv_state::<Execute>(&state)
849		.map_err(list_error)?;
850	Ok(extras.iter().copied().collect())
851}
852
853/// Refuses a list that SPL's resolver fails or panics on, whatever the
854/// transfer, and a list that reads the transfer amount.
855fn screen_list(data: &[u8]) -> Result<Vec<ExtraAccountMeta>, Unresolvable> {
856	let extras = unpack_list(data)?;
857	for (index, extra) in extras.iter().enumerate() {
858		let position = index
859			.checked_add(EXECUTE_ACCOUNTS)
860			.filter(|position| u8::try_from(*position).is_ok())
861			.ok_or(Unresolvable::ListTooLong)?;
862		screen_extra(extra, position)?;
863	}
864	if reads_amount(&extras) {
865		return Err(Unresolvable::ReadsTransferAmount);
866	}
867	Ok(extras)
868}
869
870fn screen_extra(
871	extra: &ExtraAccountMeta,
872	position: usize,
873) -> Result<(), Unresolvable> {
874	match extra.discriminator {
875		META_FIXED => Ok(()),
876		META_HOOK_PDA => screen_seeds(&extra.address_config, position),
877		META_PUBKEY_DATA => match PubkeyData::unpack(&extra.address_config)
878			.map_err(|_| Unresolvable::MalformedExtra)?
879		{
880			PubkeyData::AccountData { account_index, .. } => {
881				prior(account_index, position)
882			}
883			// A pubkey is 32 bytes. The `Execute` data is 16.
884			PubkeyData::InstructionData { .. } => {
885				Err(Unresolvable::ReadsPastInstructionData)
886			}
887			PubkeyData::Uninitialized => Err(Unresolvable::MalformedExtra),
888		},
889		external => match external.checked_sub(META_EXTERNAL_PDA) {
890			Some(program) => {
891				prior(program, position)?;
892				screen_seeds(&extra.address_config, position)
893			}
894			None => Err(Unresolvable::MalformedExtra),
895		},
896	}
897}
898
899fn seeds_of(extra: &ExtraAccountMeta) -> Vec<Seed> {
900	match extra.discriminator {
901		META_FIXED | META_PUBKEY_DATA => Vec::new(),
902		_ => Seed::unpack_address_config(&extra.address_config)
903			.unwrap_or_default(),
904	}
905}
906
907fn screen_seeds(
908	config: &[u8; 32],
909	position: usize,
910) -> Result<(), Unresolvable> {
911	let seeds = Seed::unpack_address_config(config)
912		.map_err(|_| Unresolvable::MalformedSeed)?;
913	// With the bump, this is above `MAX_SEEDS`: `find_program_address` panics.
914	if seeds.len() >= MAX_SEEDS {
915		return Err(Unresolvable::TooManySeeds);
916	}
917	for seed in &seeds {
918		match *seed {
919			// A literal fits in the 32-byte config. It is never too long.
920			Seed::Uninitialized | Seed::Literal { .. } => {}
921			Seed::InstructionData { index, length } => {
922				let end = usize::from(index)
923					.checked_add(usize::from(length))
924					.ok_or(Unresolvable::MalformedSeed)?;
925				if end > EXECUTE_DATA_LEN {
926					return Err(Unresolvable::ReadsPastInstructionData);
927				}
928			}
929			Seed::AccountKey { index } => prior(index, position)?,
930			Seed::AccountData {
931				account_index,
932				length,
933				..
934			} => {
935				if usize::from(length) > PUBKEY_LEN {
936					return Err(Unresolvable::SeedTooLong);
937				}
938				prior(account_index, position)?;
939			}
940		}
941	}
942	Ok(())
943}
944
945/// `index` must name an account resolved before `position`, as on-chain.
946fn prior(index: u8, position: usize) -> Result<(), Unresolvable> {
947	if usize::from(index) < position {
948		Ok(())
949	} else {
950		Err(Unresolvable::NamesUnresolvedAccount)
951	}
952}
953
954/// `true` if a seed reads bytes 8 to 16 of `Execute`, the transfer amount.
955fn reads_amount(extras: &[ExtraAccountMeta]) -> bool {
956	extras.iter().flat_map(seeds_of).any(|seed| match seed {
957		Seed::InstructionData { index, length } => {
958			length > 0 && read_end(index, length) > EXECUTE_DISCRIMINATOR.len()
959		}
960		_ => false,
961	})
962}
963
964/// The end of a read of `length` bytes at `offset`.
965#[expect(
966	clippy::arithmetic_side_effects,
967	reason = "two u8s cannot overflow a u16"
968)]
969fn read_end(offset: u8, length: u8) -> usize {
970	usize::from(u16::from(offset) + u16::from(length))
971}
972
973/// The `Execute` account index and the end of each data read of `extra`.
974fn data_reads(extra: &ExtraAccountMeta) -> Vec<(usize, usize)> {
975	if extra.discriminator == META_PUBKEY_DATA {
976		return match PubkeyData::unpack(&extra.address_config) {
977			Ok(PubkeyData::AccountData {
978				account_index,
979				data_index,
980			}) => vec![(
981				usize::from(account_index),
982				// A pubkey read is 32 bytes.
983				read_end(data_index, 32),
984			)],
985			_ => Vec::new(),
986		};
987	}
988	seeds_of(extra)
989		.into_iter()
990		.filter_map(|seed| match seed {
991			Seed::AccountData {
992				account_index,
993				data_index,
994				length,
995			} => {
996				Some((usize::from(account_index), read_end(data_index, length)))
997			}
998			_ => None,
999		})
1000		.collect()
1001}
1002
1003/// The `Execute` account index of each key `extra` reads: the program of an
1004/// external PDA, and each account-key seed.
1005fn key_reads(extra: &ExtraAccountMeta) -> Vec<usize> {
1006	let program = extra
1007		.discriminator
1008		.checked_sub(META_EXTERNAL_PDA)
1009		.map(usize::from);
1010	program
1011		.into_iter()
1012		.chain(seeds_of(extra).into_iter().filter_map(|seed| match seed {
1013			Seed::AccountKey { index } => Some(usize::from(index)),
1014			_ => None,
1015		}))
1016		.collect()
1017}
1018
1019#[cfg(test)]
1020mod tests;