Skip to content

Commit 3abb566

Browse files
committed
solana: add dummy transfer hook
1 parent 10c2d3e commit 3abb566

File tree

5 files changed

+180
-1
lines changed

5 files changed

+180
-1
lines changed

solana/Anchor.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ seeds = false
77
skip-lint = false
88

99
[programs.localnet]
10+
dummy-transfer-hook = "BgabMDLaxsyB7eGMBt9L22MSk9KMrL4zY2iNe14kyFP5"
1011
example_native_token_transfers = "nttiK1SepaQt6sZ4WGW5whvc9tEnGXGxuKeptcQPCcS"
11-
wormhole_governance = "wgvEiKVzX9yyEoh41jZAdC6JqGUTS4CFXbFGBV5TKdZ"
1212
ntt_quoter = "9jFBLvMZZERVmeY4tbq5MejbXRE18paGEuoB6xVJZgGe"
13+
wormhole_governance = "wgvEiKVzX9yyEoh41jZAdC6JqGUTS4CFXbFGBV5TKdZ"
1314

1415
[registry]
1516
url = "https://api.apr.dev"
@@ -31,7 +32,10 @@ address = "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"
3132
program = "programs/example-native-token-transfers/tests/fixtures/mainnet_core_bridge.so"
3233

3334
[test.validator]
35+
bind_address = "0.0.0.0"
3436
url = "https://api.mainnet-beta.solana.com"
37+
ledger = ".anchor/test-ledger"
38+
rpc_port = 8899
3539
ticks_per_slot = 16
3640

3741
[[test.validator.account]]

solana/Cargo.lock

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "dummy-transfer-hook"
3+
version = "0.1.0"
4+
description = "Created with Anchor"
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "lib"]
9+
name = "dummy_transfer_hook"
10+
11+
[features]
12+
no-entrypoint = []
13+
no-idl = []
14+
no-log-ix-name = []
15+
cpi = ["no-entrypoint"]
16+
default = []
17+
18+
[dependencies]
19+
anchor-lang.workspace = true
20+
anchor-spl.workspace = true
21+
solana-program.workspace = true
22+
spl-tlv-account-resolution = "0.6.3"
23+
spl-transfer-hook-interface = "0.6.3"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.bpfel-unknown-unknown.dependencies.std]
2+
features = []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::{
3+
associated_token::AssociatedToken,
4+
token_interface::{Mint, TokenAccount, TokenInterface},
5+
};
6+
use spl_tlv_account_resolution::state::ExtraAccountMetaList;
7+
8+
declare_id!("BgabMDLaxsyB7eGMBt9L22MSk9KMrL4zY2iNe14kyFP5");
9+
10+
/// Index of the sender token account in the accounts passed to the transfer hook
11+
pub const SENDER_TOKEN_ACCOUNT_INDEX: u8 = 0;
12+
/// Index of the mint account in the accounts passed to the transfer hook
13+
pub const MINT_ACCOUNT_INDEX: u8 = 1;
14+
/// Index of the destination token account in the accounts passed to the transfer hook
15+
pub const DESTINATION_TOKEN_ACCOUNT_INDEX: u8 = 2;
16+
/// Index of the authority account in the accounts passed to the transfer hook
17+
pub const AUTHORITY_ACCOUNT_INDEX: u8 = 3;
18+
19+
/// Number of extra accounts in the ExtraAccountMetaList account
20+
pub const EXTRA_ACCOUNTS_LEN: u8 = 1;
21+
22+
#[program]
23+
pub mod dummy_transfer_hook {
24+
use spl_tlv_account_resolution::{
25+
account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,
26+
};
27+
use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction};
28+
29+
use super::*;
30+
31+
pub fn initialize_extra_account_meta_list(
32+
ctx: Context<InitializeExtraAccountMetaList>,
33+
) -> Result<()> {
34+
let account_metas = vec![ExtraAccountMeta::new_with_seeds(
35+
&[
36+
Seed::Literal {
37+
bytes: "dummy_account".as_bytes().to_vec(),
38+
},
39+
// owner field of the sender token account
40+
Seed::AccountData {
41+
account_index: SENDER_TOKEN_ACCOUNT_INDEX,
42+
data_index: 32,
43+
length: 32,
44+
},
45+
],
46+
false, // is_signer
47+
false, // is_writable
48+
)?];
49+
50+
assert_eq!(EXTRA_ACCOUNTS_LEN as usize, account_metas.len());
51+
52+
// initialize ExtraAccountMetaList account with extra accounts
53+
ExtraAccountMetaList::init::<ExecuteInstruction>(
54+
&mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,
55+
&account_metas,
56+
)?;
57+
58+
Ok(())
59+
}
60+
61+
pub fn transfer_hook(_ctx: Context<TransferHook>, _amount: u64) -> Result<()> {
62+
// NOTE: for now, the account constraints implement all the restrictions.
63+
Ok(())
64+
}
65+
66+
// NOTE: the CPI call makes that the token2022 program makes (naturally) does not
67+
// follow the anchor calling convention, so we need to implement a fallback
68+
// instruction to handle the custom instruction
69+
pub fn fallback<'info>(
70+
program_id: &Pubkey,
71+
accounts: &'info [AccountInfo<'info>],
72+
data: &[u8],
73+
) -> Result<()> {
74+
let instruction = TransferHookInstruction::unpack(data)?;
75+
76+
// match instruction discriminator to transfer hook interface execute instruction
77+
// token2022 program CPIs this instruction on token transfer
78+
match instruction {
79+
TransferHookInstruction::Execute { amount } => {
80+
let amount_bytes = amount.to_le_bytes();
81+
82+
// invoke custom transfer hook instruction on our program
83+
__private::__global::transfer_hook(program_id, accounts, &amount_bytes)
84+
}
85+
_ => Err(ProgramError::InvalidInstructionData.into()),
86+
}
87+
}
88+
}
89+
90+
#[derive(Accounts)]
91+
pub struct InitializeExtraAccountMetaList<'info> {
92+
#[account(mut)]
93+
payer: Signer<'info>,
94+
95+
/// CHECK: ExtraAccountMetaList Account, must use these seeds
96+
#[account(
97+
init,
98+
payer = payer,
99+
space = ExtraAccountMetaList::size_of(EXTRA_ACCOUNTS_LEN as usize)?,
100+
seeds = [b"extra-account-metas", mint.key().as_ref()],
101+
bump
102+
)]
103+
pub extra_account_meta_list: AccountInfo<'info>,
104+
pub mint: InterfaceAccount<'info, Mint>,
105+
pub token_program: Interface<'info, TokenInterface>,
106+
pub associated_token_program: Program<'info, AssociatedToken>,
107+
pub system_program: Program<'info, System>,
108+
}
109+
110+
#[derive(Accounts)]
111+
/// NOTE: this is just a dummy transfer hook to test that the accounts are
112+
/// passed in correctly. Do NOT use this as a starting point in a real
113+
/// application, as it's not secure.
114+
pub struct TransferHook<'info> {
115+
#[account(
116+
token::mint = mint,
117+
)]
118+
pub source_token: InterfaceAccount<'info, TokenAccount>,
119+
pub mint: InterfaceAccount<'info, Mint>,
120+
#[account(
121+
token::mint = mint,
122+
)]
123+
pub destination_token: InterfaceAccount<'info, TokenAccount>,
124+
/// CHECK: source token account authority, can be SystemAccount or PDA owned by another program
125+
pub authority: UncheckedAccount<'info>,
126+
/// CHECK: ExtraAccountMetaList Account,
127+
#[account(
128+
seeds = [b"extra-account-metas", mint.key().as_ref()],
129+
bump
130+
)]
131+
pub extra_account_meta_list: UncheckedAccount<'info>,
132+
#[account(
133+
seeds = [b"dummy_account", source_token.owner.as_ref()],
134+
bump
135+
)]
136+
/// CHECK: dummy account. It just tests that the off-chain code correctly
137+
/// computes and the on-chain code correctly passes on the PDA.
138+
pub dummy_account: AccountInfo<'info>,
139+
}

0 commit comments

Comments
 (0)