A decentralized Automated Market Maker (AMM) on Solana featuring customizable bonding curves and comprehensive Token-2022 transfer hook integration. Trade any token seamlessly, whether it's standard SPL tokens or Token-2022 tokens with complex transfer logic.
- 🎯 Customizable Bonding Curves: Set your own virtual reserves for custom price curves
- 🪝 Token-2022 Transfer Hooks: Full support for tokens with transfer hooks and custom logic
- 🔄 Universal Token Support: Works with SPL tokens, Token-2022, and tokens with hooks
- 💰 Low Fees: 1% trading fee on all transactions
- 🔒 Secure: Robust error handling and proper fee management
- 📊 Price Discovery: Virtual reserves provide initial liquidity and price stability
- 🚀 High Performance: Optimized for gas efficiency and hook compatibility
- ⚡ Automatic Detection: Seamlessly handles different token types without configuration
┌─────────────────────────────────────────────────────────────────┐
│ PROGRAM STRUCTURE │
└─────────────────────────────────────────────────────────────────┘
programs/
├── hook-amm/ # Main AMM program
│ └── src/
│ ├── lib.rs # Main program entry point
│ ├── constants.rs # Program constants
│ ├── errors.rs # Custom error types
│ ├── events.rs # Event definitions
│ ├── instructions/ # Instruction handlers
│ │ ├── initialize_global_config.rs
│ │ ├── create_bonding_curve.rs
│ │ ├── buy.rs # Fixed for transfer hooks
│ │ └── sell.rs # Fixed for transfer hooks
│ ├── state/ # Account structures
│ │ ├── global_config.rs # Global configuration
│ │ └── bonding_curve.rs # Bonding curve state
│ └── utils.rs # Transfer hook utilities
└── transfer-hook/ # Example transfer hook program
└── src/
└── lib.rs # Hook implementation with counter
✅ Fixed Critical Issues:
- Fixed buy instruction panic when handling Token-2022 transfers
- Fixed sell instruction insufficient funds errors
- Corrected bonding curve reserve calculations
- Added proper transfer hook account handling
✅ New Features:
- Complete transfer hook program implementation
- Automatic token type detection (SPL vs Token-2022)
- Comprehensive test suite with transfer hooks
- Example hook with counter and validation logic
HookAMM provides seamless support for Token-2022 transfer hooks, enabling tokens with custom transfer logic to be traded on the AMM. This is the first AMM to fully support Token-2022 transfer hooks on Solana.
Transfer hooks are a Token-2022 feature that allows custom programs to be executed during token transfers. This enables:
- Custom Transfer Logic: Implement restrictions, fees, or special behaviors
- Real-time Processing: Execute code before and after transfers
- State Synchronization: Update external state during transfers
- Compliance Features: Implement KYC/AML or other regulatory requirements
┌─────────────────────────────────────────────────────────────────┐
│ TRANSFER HOOK FLOW │
└─────────────────────────────────────────────────────────────────┘
Regular Token Transfer:
Token Program → Transfer tokens → Complete
Token-2022 with Hooks:
Token-2022 Program → Pre-transfer Hook → Transfer → Post-transfer Hook → Complete
│ │
▼ ▼
Hook Program Hook Program
(Custom logic) (Cleanup/events)
The program uses a specialized perform_token_transfer function in utils.rs that automatically detects and handles transfer hooks. Key fixes in v2.0:
🔧 Fixed Transfer Logic: Corrected instruction building for Token-2022 transfers 🔧 Fixed Reserve Calculations: Proper bonding curve mathematics 🔧 Fixed Account Handling: Correct remaining accounts for hook programs
pub fn perform_token_transfer<'info>(
from: &InterfaceAccount<'info, TokenAccount>,
to: &InterfaceAccount<'info, TokenAccount>,
authority: &AccountInfo<'info>,
token_program: &Interface<'info, TokenInterface>,
mint: &InterfaceAccount<'info, Mint>,
amount: u64,
signer_seeds: &[&[&[u8]]],
remaining_accounts: &[AccountInfo<'info>], // ← Hook accounts passed here
) -> Result<()> {
// Detect if this is Token-2022 with transfer hooks
let is_token_2022 = token_program.key() == token_2022::ID;
if is_token_2022 && !remaining_accounts.is_empty() {
// Execute transfer with hook support
let mut accounts = vec![
from.to_account_info(),
mint.to_account_info(),
to.to_account_info(),
authority.to_account_info(),
];
// Add hook program accounts
for account in remaining_accounts {
accounts.push(account.clone());
}
// Build transfer instruction with hook support (FIXED in v2.0)
let mut account_metas = vec![
AccountMeta::new(from.key(), false),
AccountMeta::new_readonly(mint.key(), false),
AccountMeta::new(to.key(), false),
AccountMeta::new_readonly(authority.key(), true),
];
// Add remaining accounts for transfer hooks
for account in remaining_accounts {
account_metas.push(AccountMeta {
pubkey: account.key(),
is_signer: account.is_signer,
is_writable: account.is_writable,
});
}
let transfer_ix = Instruction {
program_id: token_program.key(),
accounts: account_metas,
data: {
let mut data = vec![12]; // TransferChecked discriminator
data.extend_from_slice(&amount.to_le_bytes());
data.extend_from_slice(&[mint.decimals]);
data
},
};
// Execute with all required accounts (FIXED invoke call in v2.0)
if signer_seeds.is_empty() {
invoke(&transfer_ix, &accounts)?;
} else {
invoke_signed(&transfer_ix, &accounts, signer_seeds)?;
}
} else {
// Standard SPL transfer without hooks
let cpi_ctx = CpiContext::new_with_signer(
token_program.to_account_info(),
TransferChecked {
from: from.to_account_info(),
mint: mint.to_account_info(),
to: to.to_account_info(),
authority: authority.to_account_info(),
},
signer_seeds
);
transfer_checked(cpi_ctx, amount, mint.decimals)?;
}
Ok(())
}When a buy/sell transaction involves a token with transfer hooks:
1. User initiates buy/sell
2. HookAMM processes SOL transfers
3. HookAMM calls perform_token_transfer()
4. Function detects Token-2022 + remaining_accounts
5. Pre-transfer hook executes:
├── Validate transfer
├── Apply custom logic
└── Can reject if needed
6. Actual token transfer occurs
7. Post-transfer hook executes:
├── Update external state
├── Emit custom events
└── Cleanup operations
8. HookAMM updates reserves
9. Transaction completes
// For regular SPL tokens (no hooks)
await program.methods
.buy(solAmount, minTokenAmount)
.accounts({
// ... standard accounts
})
.rpc();
// For Token-2022 tokens with transfer hooks (FIXED in v2.0)
await program.methods
.buy(solAmount, minTokenAmount)
.accounts({
// ... standard accounts
})
.remainingAccounts([
// CRITICAL: Hook program must be FIRST in remaining accounts
{ pubkey: transferHookProgram.programId, isSigner: false, isWritable: false },
{ pubkey: extraAccountMetaListPDA, isSigner: false, isWritable: false },
{ pubkey: counterPDA, isSigner: false, isWritable: true },
// ... any other accounts the hook needs
])
.rpc();To find the required hook accounts, query the mint:
import { getExtraAccountMetaAddress, getExtraAccountMetas } from '@solana/spl-token';
// Get hook program from mint
const mintInfo = await getMint(connection, mintAddress, 'confirmed', TOKEN_2022_PROGRAM_ID);
const transferHookProgramId = getTransferHook(mintInfo);
if (transferHookProgramId) {
// Get additional accounts needed by the hook
const extraAccountMetaAddress = getExtraAccountMetaAddress(mintAddress, transferHookProgramId);
const extraAccountMetas = await getExtraAccountMetas(
connection,
extraAccountMetaAddress,
'confirmed',
TOKEN_2022_PROGRAM_ID
);
// Convert to remaining accounts format
const remainingAccounts = extraAccountMetas.map(meta => ({
pubkey: meta.addressConfig.address,
isSigner: meta.isSigner,
isWritable: meta.isWritable,
}));
}// Hook can restrict transfers based on:
- Whitelist/blacklist validation
- Time-based locks
- Maximum transfer amounts
- Geographic restrictions// Hook can implement:
- Burn mechanisms (deflationary tokens)
- Redistribution to holders
- Treasury accumulation
- Dynamic fee rates// Hook can update:
- User statistics
- Governance voting power
- Staking balances
- External protocol state// Hook can handle:
- Item durability updates
- Experience point calculation
- Achievement tracking
- Inventory managementTransfer hooks can fail for various reasons:
try {
await hookAmm.buy(buyer, mint, buyParams, TOKEN_2022_PROGRAM_ID, remainingAccounts);
} catch (error) {
if (error.toString().includes('TransferHookFailed')) {
console.log('Transfer hook rejected the transaction');
// Handle hook-specific logic
} else if (error.toString().includes('SlippageExceeded')) {
console.log('Price moved too much during execution');
}
}HookAMM is compatible with:
- ✅ Standard SPL tokens
- ✅ Token-2022 tokens without hooks
- ✅ Token-2022 tokens with transfer hooks
- ✅ Multiple hooks per token
- ✅ Complex hook interactions
The program automatically detects the token type and handles transfers appropriately, making it seamless for users and developers.
HookAMM includes a complete example transfer hook program (programs/transfer-hook/) that demonstrates:
// Key features of the example hook:
✅ Counter tracking: Increments on every transfer
✅ Amount validation: Rejects transfers over 10,000 tokens
✅ Proper account structure: Uses PDAs for state management
✅ Event logging: Emits transfer information
✅ Fallback handling: Compatible with Token-2022 interface
// Hook execution flow:
1. Token transfer initiated
2. Hook increments counter
3. Hook validates amount < 10,000 tokens
4. If valid: transfer proceeds
5. If invalid: transaction fails with "AmountTooBig"Test Results (Fixed in v2.0):
- ✅ Create Token-2022 liquidity pool with hooks
- ✅ Buy tokens with transfer hooks enabled
- ✅ Sell tokens with transfer hooks enabled
- ✅ Counter increments on each transfer
- ✅ Amount validation works correctly
Admin → initialize_global_config()
│
▼
┌─────────────────┐
│ GlobalConfig │
│ - authority │
│ - fee_recipient│
│ - total_curves │
└─────────────────┘
Creator → create_bonding_curve(params)
│
▼
┌─────────────────────────┐
│ BondingCurve │
│ - custom virtual reserves
│ - initial supply │
│ - mint reference │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Curve Token Account │
│ (Holds tokens for AMM) │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Token Transfer │
│ Creator → Curve: tokens │
│ Mint Authority │
│ FROZEN 🔒 │
└─────────────────────────┘
User wants to buy tokens with 1 SOL
│
▼
Calculate: fee = 1 SOL × 1% = 0.01 SOL
amount_after_fee = 0.99 SOL
│
▼
┌─────────────────────────────────────┐
│ SOL TRANSFERS │
│ User → Bonding Curve: 0.99 SOL │
│ User → Fee Recipient: 0.01 SOL │
└─────────────────────────────────────┘
│
▼
Calculate tokens using bonding curve:
tokens_out = f(amount_after_fee, reserves)
│
▼
┌─────────────────────────────────────┐
│ TOKEN TRANSFER │
│ Curve → User: tokens_out │
│ (With Token-2022 hook support) │
└─────────────────────────────────────┘
│
▼
Update bonding curve reserves
User wants to sell tokens
│
▼
Transfer tokens: User → Curve
│
▼
Calculate SOL output using bonding curve:
sol_out = f(token_amount, reserves)
│
▼
Calculate: fee = sol_out × 1%
amount_after_fee = sol_out - fee
│
▼
┌─────────────────────────────────────┐
│ SOL TRANSFERS │
│ Curve → User: amount_after_fee │
│ Curve → Fee Recipient: fee │
└─────────────────────────────────────┘
│
▼
Update bonding curve reserves
HookAMM implements a sophisticated bonding curve using a constant product formula with virtual and real reserves. This creates a dynamic pricing mechanism that responds to trading activity.
pub struct BondingCurve {
pub mint: Pubkey, // Token mint address
pub creator: Pubkey, // Curve creator
pub virtual_token_reserves: u64, // 🔸 Virtual token liquidity
pub virtual_sol_reserves: u64, // 🔸 Virtual SOL liquidity
pub real_token_reserves: u64, // 🔹 Actual tokens traded
pub real_sol_reserves: u64, // 🔹 Actual SOL collected
pub token_total_supply: u64, // Total token supply
pub complete: bool, // Curve completion status
pub index: u64, // Curve index number
}┌─────────────────────────────────────────────────────────────────┐
│ RESERVE COMPOSITION │
└─────────────────────────────────────────────────────────────────┘
🔸 VIRTUAL RESERVES (Set at creation, never change):
├── Purpose: Provide initial liquidity and price stability
├── virtual_token_reserves: Starting available tokens for purchase
├── virtual_sol_reserves: Starting price reference in SOL
└── Effect: Determines initial price and curve steepness
🔹 REAL RESERVES (Dynamic, change with each trade):
├── Purpose: Track actual trading activity
├── real_token_reserves: Tokens removed from curve (purchased by users)
├── real_sol_reserves: SOL collected in curve (from user purchases)
└── Effect: Shifts the price as trading occurs
📊 EFFECTIVE RESERVES (Used in calculations):
├── Effective SOL = virtual_sol_reserves + real_sol_reserves
├── Effective Tokens = virtual_token_reserves - real_token_reserves
└── Current Price = Effective SOL ÷ Effective Tokens
The bonding curve uses a Constant Product Formula:
k = (Virtual SOL + Real SOL) × (Virtual Tokens - Real Tokens)
Where:
- k = Constant product (liquidity constant)
- Virtual reserves = Initial liquidity parameters
- Real reserves = Cumulative trading activity
// Curve creation parameters
virtual_token_reserves = 1,000,000 tokens
virtual_sol_reserves = 100 SOL
real_token_reserves = 0 (no trades yet)
real_sol_reserves = 0 (no trades yet)
// Initial calculations
effective_sol = 100 + 0 = 100 SOL
effective_tokens = 1,000,000 - 0 = 1,000,000 tokens
initial_price = 100 ÷ 1,000,000 = 0.0001 SOL per token
k = 100 × 1,000,000 = 100,000,000// User buys with 10 SOL (9.9 SOL after 1% fee)
sol_input = 9.9 SOL
// Calculate tokens received
new_sol_reserves = 100 + 9.9 = 109.9 SOL
new_token_reserves = 100,000,000 ÷ 109.9 = 909,917 tokens
tokens_purchased = 1,000,000 - 909,917 = 90,083 tokens
// Update real reserves
real_sol_reserves = 0 + 9.9 = 9.9 SOL
real_token_reserves = 0 + 90,083 = 90,083 tokens
// New effective reserves
effective_sol = 100 + 9.9 = 109.9 SOL
effective_tokens = 1,000,000 - 90,083 = 909,917 tokens
new_price = 109.9 ÷ 909,917 = 0.0001208 SOL per token
// Price increased by: (0.0001208 - 0.0001) ÷ 0.0001 = 20.8%┌─────────────────────────────────────────────────────────────────┐
│ BONDING CURVE SHAPE │
└─────────────────────────────────────────────────────────────────┘
Price │
^ │ ┌─────────────────────────────
│ │ ┌─┘
│ │ ┌─┘ ← Price increases as tokens are bought
│ │┌─┘ (SOL reserves grow, token reserves shrink)
│ └─────────────────────────────────────────► Tokens Sold
│
Initial Price (determined by virtual reserves ratio)
Mathematical relationship:
- More tokens bought = Higher price (exponential growth)
- More tokens sold = Lower price (exponential decay)
- Virtual reserves = "Price floor" and liquidity depth
{
virtual_token_reserves: 1_000_000, // 1M tokens
virtual_sol_reserves: 1000, // 1000 SOL
}
// Initial price: 1000 ÷ 1,000,000 = 0.001 SOL per token (EXPENSIVE)
// Effect: High starting price, steep price increases{
virtual_token_reserves: 1_000_000, // 1M tokens
virtual_sol_reserves: 1, // 1 SOL
}
// Initial price: 1 ÷ 1,000,000 = 0.000001 SOL per token (CHEAP)
// Effect: Low starting price, gradual price increases{
virtual_token_reserves: 10_000_000, // 10M tokens
virtual_sol_reserves: 100, // 100 SOL
}
// Effect: More liquidity depth, smaller price impact per trade{
virtual_token_reserves: 100_000, // 100K tokens
virtual_sol_reserves: 100, // 100 SOL
}
// Effect: Less liquidity depth, larger price impact per trade1. User inputs: sol_amount = 10 SOL
2. Calculate fee:
fee = 10 × 1% = 0.1 SOL
sol_after_fee = 10 - 0.1 = 9.9 SOL
3. Calculate effective reserves:
current_sol = virtual_sol_reserves + real_sol_reserves
current_tokens = virtual_token_reserves - real_token_reserves
4. Apply constant product formula:
k = current_sol × current_tokens
new_sol = current_sol + sol_after_fee
new_tokens = k ÷ new_sol
tokens_out = current_tokens - new_tokens
5. Update real reserves:
real_sol_reserves += sol_after_fee
real_token_reserves += tokens_out // Tracks cumulative tokens sold
1. User inputs: token_amount = 1000 tokens
2. Calculate effective reserves:
current_sol = virtual_sol_reserves + real_sol_reserves
current_tokens = virtual_token_reserves - real_token_reserves
3. Apply constant product formula:
k = current_sol × current_tokens
new_tokens = current_tokens + token_amount
new_sol = k ÷ new_tokens
sol_out = current_sol - new_sol
4. Calculate fee:
fee = sol_out × 1% = sol_out × 0.01
sol_after_fee = sol_out - fee
5. Update real reserves:
real_sol_reserves -= sol_out
real_token_reserves -= token_amount // Decreases cumulative tokens sold
The bonding curve creates automatic price discovery through:
- Supply Pressure: As tokens are bought, available supply decreases
- Demand Premium: Higher demand leads to exponentially higher prices
- Liquidity Depth: Virtual reserves provide baseline liquidity
- Market Efficiency: Arbitrage opportunities maintain fair pricing
Curves can be designed to "complete" when certain conditions are met:
// Example completion condition (can be customized)
let completion_threshold = virtual_token_reserves * 90 / 100; // 90% of tokens sold
if real_token_reserves >= completion_threshold {
bonding_curve.complete = true;
// Could migrate to a traditional AMM like Raydium
}- All calculations use checked math to prevent overflow
- Virtual reserves are stored once, real reserves updated per trade
- Price calculations happen off-chain for quotes, on-chain for execution
- Constant product formula provides O(1) time complexity
- Rust 1.70+
- Solana CLI 1.17+
- Anchor 0.31+
- Node.js 16+
# 1. Clone and build
git clone https://github.com/Von-Labs/hook-amm
cd hook-amm
anchor build
# 2. Run transfer hook tests
anchor test --skip-local-validator
# 3. Deploy to devnet (both programs)
anchor deploy --provider.cluster devnet# Clone the repository
git clone https://github.com/yourusername/hook-amm
cd hook-amm
# Install dependencies
yarn install
# Build the program
anchor build
# Run tests
anchor test# Configure for devnet
solana config set --url devnet
# Deploy the program
anchor deploy
# Initialize the program (one-time setup)
anchor run initializeconst params = {
initialSupply: new BN(1_000_000_000_000), // 1M tokens
virtualTokenReserves: new BN(500_000_000_000_000), // 500M
virtualSolReserves: new BN(10_000_000_000), // 10 SOL
};
await program.methods
.createBondingCurve(params)
.accounts({
bondingCurve,
curveTokenAccount,
mint,
creator: wallet.publicKey,
globalConfig,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: SYSVAR_RENT_PUBKEY,
})
.rpc();await program.methods
.buy(
new BN(1_000_000_000), // 1 SOL
new BN(950_000) // Min tokens (slippage protection)
)
.accounts({
bondingCurve,
curveTokenAccount,
userTokenAccount,
user: wallet.publicKey,
mint,
globalConfig,
feeRecipient,
tokenProgram: TOKEN_PROGRAM_ID,
// ... other accounts
})
.rpc();// For tokens with transfer hooks, pass additional accounts
await program.methods
.buy(solAmount, minTokenAmount)
.accounts({ /* ... standard accounts ... */ })
.remainingAccounts([
// CRITICAL: Transfer hook program MUST be first
{ pubkey: transferHookProgram.programId, isSigner: false, isWritable: false },
{ pubkey: extraAccountMetaListPDA, isSigner: false, isWritable: false },
{ pubkey: counterPDA, isSigner: false, isWritable: true },
// ... any additional hook accounts based on hook requirements
])
.rpc();We provide a TypeScript SDK for easier integration:
# Install the SDK
npm install @hook-amm/sdkimport { HookAmm } from '@hook-amm/sdk';
// Initialize SDK
const hookAmm = new HookAmm(connection);
// Create bonding curve
await hookAmm.createBondingCurve(creator, mint, {
initialSupply: new BN(1_000_000_000_000),
virtualTokenReserves: new BN(500_000_000_000_000),
virtualSolReserves: new BN(10_000_000_000),
});
// Buy tokens
await hookAmm.buy(buyer, mint, {
solAmount: new BN(1_000_000_000),
minTokenAmount: new BN(950_000),
});
// Get current price
const price = await hookAmm.getPrice(mint);
console.log('Price per token:', price.pricePerToken, 'SOL');- Slippage Protection: Always set
minTokenAmountfor buys andminSolAmountfor sells - Parameter Validation: Virtual reserves must be > 0 to prevent division by zero
- Fee Handling: Fees are transferred directly from users to prevent PDA transfer issues
- Overflow Protection: All calculations use checked math operations
- Trading Fee: 1% on all buys and sells
- Fee Recipient: Set during global config initialization
- Fee Distribution: Automatically transferred on each trade
# Run all tests
anchor test
# Run specific test
anchor test -- --grep "buy transaction"
# Run with logging
RUST_LOG=debug anchor testVirtual reserves determine the initial price curve:
- Higher virtual SOL = Higher initial price
- Higher virtual tokens = Lower initial price
- Ratio determines steepness of price curve
Stable Launch (High liquidity, low volatility):
{
virtualTokenReserves: new BN(1_000_000_000_000_000), // 1B tokens
virtualSolReserves: new BN(100_000_000_000), // 100 SOL
}Aggressive Launch (Low liquidity, high volatility):
{
virtualTokenReserves: new BN(100_000_000_000_000), // 100M tokens
virtualSolReserves: new BN(10_000_000_000), // 10 SOL
}- "InvalidAmount" Error: Ensure all amounts are > 0
- "SlippageExceeded" Error: Increase slippage tolerance
- "Overflow" Error: Virtual reserves might be too large
- "Token Account Not Found": Create ATA before trading
- "Account required by instruction is missing": Include transfer hook program in remainingAccounts
- "AmountTooBig" Error: Transfer amount exceeds hook validation limit (10,000 tokens)
- "Buy instruction panic": Fixed in v2.0 - reserve calculation corrected
- "Sell insufficient funds": Fixed in v2.0 - proper token balance handling
Enable debug logging:
export RUST_LOG=hook_amm=debug
anchor test- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Built with Anchor Framework
- Supports Token-2022 Program
- Inspired by pump.fun and other bonding curve implementations