Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/oft-solana/Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ skip-lint = false

[programs.localnet]
oft = "G2BYTnfGCMQAErMZkTBCFSapKevzf6QCjizjXi8hFEtJ"
transfer_hook = "Hook111111111111111111111111111111111111111"

[registry]
url = "https://api.apr.dev"
Expand Down
10 changes: 10 additions & 0 deletions examples/oft-solana/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions examples/oft-solana/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,37 @@ Before deploying, ensure the following:
- (recommended) you have profiled the gas usage of `lzReceive` on your destination chains
<!-- TODO: mention https://docs.layerzero.network/v2/developers/evm/technical-reference/integration-checklist#set-security-and-executor-configurations after it has been updated to reference the CLI -->

## Transfer Hook Example

This example also includes a **Token-2022 Transfer Hook** program that demonstrates how to add custom transfer validation logic to your OFT.

### What is a Transfer Hook?

Token-2022's [Transfer Hook extension](https://spl.solana.com/token-2022/extensions#transfer-hook) allows you to execute custom validation logic on every token transfer. Use cases include:

- **Compliance**: Enforce allowlist/blocklist for regulated tokens
- **Transfer restrictions**: Time-locks, vesting schedules, daily limits
- **Royalties**: Ensure royalty payments on NFT transfers

### Getting Started with Transfer Hook

The Transfer Hook program is located in [`programs/transfer-hook/`](./programs/transfer-hook/). See the [Transfer Hook README](./programs/transfer-hook/README.md) for:

- Architecture overview and how it integrates with Token-2022
- Step-by-step usage instructions
- Example code for extending the hook with your own logic
- Integration patterns with OFT

### Building and Testing the Transfer Hook

```bash
# Build
anchor build -p transfer-hook

# Test
pnpm run test:anchor
```

## Appendix

### Running tests
Expand Down
29 changes: 29 additions & 0 deletions examples/oft-solana/programs/transfer-hook/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "transfer-hook"
version = "0.1.0"
description = "Token-2022 Transfer Hook example for compliance validation"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "transfer_hook"

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]

[dependencies]
anchor-lang = { version = "0.31.1", features = ["init-if-needed"] }
anchor-spl = "0.31.1"
spl-transfer-hook-interface = "0.9"
spl-tlv-account-resolution = "0.9"

[lints.rust]
# Suppress warnings from Anchor's internal cfg checks
unexpected_cfgs = { level = "allow", check-cfg = [
"cfg(feature, values(\"custom-heap\", \"custom-panic\", \"anchor-debug\"))"
] }
243 changes: 243 additions & 0 deletions examples/oft-solana/programs/transfer-hook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# Token-2022 Transfer Hook Example

This program demonstrates how to implement a **Transfer Hook** for Token-2022 tokens on Solana. Transfer Hooks allow you to execute custom validation logic on every token transfer.

## What is a Transfer Hook?

Token-2022 (SPL Token Extensions) introduced the [Transfer Hook extension](https://spl.solana.com/token-2022/extensions#transfer-hook), which allows token creators to specify a program that gets invoked on every transfer. This enables powerful use cases:

| Use Case | Description |
|----------|-------------|
| **Compliance** | Enforce allowlist/blocklist for regulated tokens (securities, stablecoins) |
| **Royalties** | Ensure royalty payments are included in NFT transfers |
| **Transfer Restrictions** | Time-locks, vesting schedules, daily limits |
| **Analytics** | Track transfer volumes, collect fees, log events |
| **Cross-chain** | Custom logic for bridged/wrapped tokens |

## Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│ User: transfer_checked() │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Token-2022 Program │
│ 1. Validate balances, decimals, signatures │
│ 2. Check for Transfer Hook extension │
│ 3. CPI to Transfer Hook program ───────────────────────────┐ │
└─────────────────────────────────────────────────────────────│───┘
┌──────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Transfer Hook Program (this) │
│ 1. Receive transfer context (source, dest, amount, authority) │
│ 2. Execute custom validation logic │
│ 3. Return Ok(()) to allow, or Err to reject │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────┐
│ Transfer Complete │
│ (or Reverted) │
└─────────────────────┘
```

## Key Components

### 1. ExtraAccountMetaList PDA

Before transfers can work, you must initialize an `ExtraAccountMetaList` PDA. This account declares which additional accounts your hook needs beyond the standard transfer accounts.

```
Seeds: ["extra-account-metas", mint.key()]
```

### 2. The `fallback` Instruction

Token-2022 uses the SPL instruction format, not Anchor's. The `fallback` handler bridges this gap:

```rust
pub fn fallback<'info>(
program_id: &Pubkey,
accounts: &'info [AccountInfo<'info>],
data: &[u8],
) -> Result<()> {
let instruction = TransferHookInstruction::unpack(data)?;
match instruction {
TransferHookInstruction::Execute { amount } => {
// Route to our transfer_hook handler
}
_ => Err(ProgramError::InvalidInstructionData.into()),
}
}
```

### 3. The `transfer_hook` Instruction

This is where your custom logic lives:

```rust
pub fn transfer_hook(ctx: Context<TransferHookExecute>, amount: u64) -> Result<()> {
// Your validation logic here
// Return Ok(()) to allow, Err to reject
require!(amount > 100, HookError::AmountTooSmall);
Ok(())
}
```

## Usage

### 1. Build the Program

```bash
anchor build -p transfer-hook
```

### 2. Create a Mint with Transfer Hook Extension

```typescript
import {
createInitializeMintInstruction,
createInitializeTransferHookInstruction,
getMintLen,
ExtensionType,
TOKEN_2022_PROGRAM_ID,
} from "@solana/spl-token";

const extensions = [ExtensionType.TransferHook];
const mintLen = getMintLen(extensions);

const transaction = new Transaction().add(
// Create the mint account
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: mint,
space: mintLen,
lamports: await connection.getMinimumBalanceForRentExemption(mintLen),
programId: TOKEN_2022_PROGRAM_ID,
}),
// Initialize the Transfer Hook extension
createInitializeTransferHookInstruction(
mint,
authority,
TRANSFER_HOOK_PROGRAM_ID, // Your hook program
TOKEN_2022_PROGRAM_ID
),
// Initialize the mint
createInitializeMintInstruction(
mint,
decimals,
mintAuthority,
freezeAuthority,
TOKEN_2022_PROGRAM_ID
)
);
```

### 3. Initialize the ExtraAccountMetaList

```typescript
await program.methods
.initializeExtraAccountMetaList()
.accounts({
payer: payer.publicKey,
mint: mint,
})
.rpc();
```

### 4. Transfers Now Go Through Your Hook

```typescript
import { createTransferCheckedWithTransferHookInstruction } from "@solana/spl-token";

const transferIx = await createTransferCheckedWithTransferHookInstruction(
connection,
source,
mint,
destination,
authority,
amount,
decimals,
[],
"confirmed",
TOKEN_2022_PROGRAM_ID
);
```

## Extending This Example

### Adding Config State

For a compliance hook, you might add a config PDA:

```rust
#[account]
pub struct TokenConfig {
pub authority: Pubkey, // Admin who can update config
pub paused: bool, // Global pause switch
pub allowlist_mode: u8, // 0=Open, 1=Blacklist, 2=Whitelist
}
```

### Adding Allowlist PDAs

```rust
#[account]
pub struct AllowlistEntry {
pub bump: u8, // Marker PDA - existence determines status
}
```

### Updating ExtraAccountMetaList

When you add custom accounts, update `Initialize::apply()`:

```rust
let extra_account_metas: Vec<ExtraAccountMeta> = vec![
// Add your TokenConfig PDA
ExtraAccountMeta::new_with_seeds(
&[Seed::Literal { bytes: b"config".to_vec() }],
false, // is_signer
false, // is_writable
)?,
];
```

## Integration with OFT

This Transfer Hook can be used with LayerZero OFT tokens. When configured:

1. **Inbound transfers** (from OFT program) can be whitelisted to skip checks
2. **Outbound/P2P transfers** go through full validation
3. **Compliance** is enforced at the token level, not the OFT level

See the main [oft-solana README](../../README.md) for OFT integration details.

## Testing

The tests use Jest and require a local validator. From the workspace root:

```bash
# Run all Anchor tests (starts local validator automatically)
pnpm run test:anchor

# Or run just the transfer-hook tests directly
npx jest test/anchor/transfer-hook.test.ts
```

Note: The tests require the program to be built first (`anchor build -p transfer-hook`).

## References

- [Solana Transfer Hook Guide](https://solana.com/developers/guides/token-extensions/transfer-hook)
- [Token-2022 Documentation](https://spl.solana.com/token-2022)
- [ExtraAccountMetaList Docs](https://docs.rs/spl-tlv-account-resolution)
- [SPL Transfer Hook Interface](https://docs.rs/spl-transfer-hook-interface)

## License

Apache-2.0
2 changes: 2 additions & 0 deletions examples/oft-solana/programs/transfer-hook/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
38 changes: 38 additions & 0 deletions examples/oft-solana/programs/transfer-hook/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! Transfer Hook error codes
//!
//! Define custom errors that can be returned from the transfer hook.
//! These will cause the Token-2022 transfer to revert.

use anchor_lang::prelude::error_code;

#[error_code]
pub enum HookError {
/// Transfer amount must meet the minimum threshold.
/// This is a placeholder error for the example; replace with your logic.
#[msg("Transfer amount must be at least 100")]
AmountTooSmall,

// =========================================================================
// Example errors for a compliance-focused Transfer Hook:
// =========================================================================

// /// Global pause is active - all transfers blocked
// #[msg("Transfers are paused")]
// Paused,

// /// Source address is on the blocklist
// #[msg("Source address is blocked")]
// SourceBlocked,

// /// Destination address is on the blocklist
// #[msg("Destination address is blocked")]
// DestinationBlocked,

// /// Delegate (spender) is on the blocklist
// #[msg("Delegate is blocked")]
// DelegateBlocked,

// /// Address not on the allowlist (for whitelist mode)
// #[msg("Address not on allowlist")]
// NotOnAllowlist,
}
Loading
Loading