Skip to content

Conversation

@istvanmajsai70-pixel
Copy link

use anchor_lang::prelude::*; use anchor_spl::token::{self, Mint, TokenAccount, Token, MintTo, Transfer, CloseAccount};

declare_id!("ReplaceWithYourProgramId1111111111111111111111");

// Simple Anchor program that: // - creates an SPL mint with a fixed total supply (100_000_000_000 tokens, decimals = 6) // - holds a token reserve owned by the program // - allows users to buy tokens by sending SOL and selling tokens back for SOL // - uses a fixed price (tokens per 1 SOL) and a small fee (in basis points)

// NOTE: // - This is an educational example. Do NOT use in production without audit. // - We use decimals = 6 so total_supply fits into u64 when scaled. // - The program uses CPI to the SPL Token program.

#[program] pub mod anchor_spl_token_exchange { use super::*;

pub fn initialize(
ctx: Context,
price_tokens_per_sol: u64,
fee_bps: u16,
) -> Result<()> {
// price_tokens_per_sol: how many token base-units (i.e. including decimals) equals 1 SOL
// fee_bps: fee in basis points (10000 bps = 100%)

let state = &mut ctx.accounts.state;
state.mint = ctx.accounts.mint.key();
state.reserve = ctx.accounts.reserve.key();
state.authority = ctx.accounts.authority.key();
state.price_tokens_per_sol = price_tokens_per_sol;
state.fee_bps = fee_bps;

// Mint the total supply to the reserve token account (program-owned reserve)
let cpi_accounts = MintTo {
    mint: ctx.accounts.mint.to_account_info(),
    to: ctx.accounts.reserve.to_account_info(),
    authority: ctx.accounts.mint_authority.to_account_info(),
};

// mint_authority is a PDA we derived; sign with seeds
let mint_authority_seeds: &[&[u8]] = &[
    b"mint_authority",
    ctx.program_id.as_ref(),
];

let signer = &[&mint_authority_seeds[..]];

let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);

// 100_000_000_000 tokens with decimals = 6 -> scaled supply = 100_000_000_000 * 10^6
let total_supply: u64 = 100_000_000_000u64.checked_mul(10u64.pow(ctx.accounts.mint.decimals as u32)).unwrap();

token::mint_to(cpi_ctx, total_supply)?;

Ok(())

}

// Buy tokens: user attaches SOL (lamports) and receives tokens at fixed price pub fn buy(ctx: Context, min_tokens_out: u64) -> Result<()> {
let state = &ctx.accounts.state;

// amount of lamports sent in this instruction must be transfered by the client
let lamports_sent = ctx.accounts.payer.to_account_info().lamports()
    .checked_sub(ctx.accounts.payer_starting_balance)
    .unwrap_or(0);

require!(lamports_sent > 0, ExchangeError::NoLamportsSent);

// tokens per 1 SOL (1 SOL = 1_000_000_000 lamports)
let tokens_per_lamport = state.price_tokens_per_sol.checked_div(1_000_000_000u64).ok_or(ExchangeError::InvalidPrice)?;
let tokens_out = lamports_sent.checked_mul(tokens_per_lamport).ok_or(ExchangeError::MathOverflow)?;

// apply fee
let fee = tokens_out.checked_mul(state.fee_bps as u64).ok_or(ExchangeError::MathOverflow)?
    .checked_div(10000).ok_or(ExchangeError::MathOverflow)?;
let tokens_to_user = tokens_out.checked_sub(fee).ok_or(ExchangeError::MathOverflow)?;

require!(tokens_to_user >= min_tokens_out, ExchangeError::SlippageExceeded);

// Transfer tokens from reserve -> user_token_account (program signed)
let cpi_accounts = Transfer {
    from: ctx.accounts.reserve.to_account_info(),
    to: ctx.accounts.user_token.to_account_info(),
    authority: ctx.accounts.reserve_authority.to_account_info(),
};

let seeds = &[b"reserve_authority", ctx.program_id.as_ref()];
let signer = &[&seeds[..]];
let cpi_ctx = CpiContext::new_with_signer(ctx.accounts.token_program.to_account_info(), cpi_accounts, signer);
token::transfer(cpi_ctx, tokens_to_user)?;

Ok(())

}

// Sell tokens: user transfers tokens to reserve, program sends SOL back at fixed price pub fn sell(ctx: Context, tokens_in: u64, min_lamports_out: u64) -> Result<()> {
let state = &mut ctx.accounts.state;

require!(tokens_in > 0, ExchangeError::InvalidAmount);

// compute lamports out = tokens_in / tokens_per_lamport
let tokens_per_lamport = state.price_tokens_per_sol.checked_div(1_000_000_000u64).ok_or(ExchangeError::InvalidPrice)?;
require!(tokens_per_lamport > 0, ExchangeError::InvalidPrice);

let lamports_out = tokens_in.checked_div(tokens_per_lamport).ok_or(ExchangeError::MathOverflow)?;

// apply fee on tokens (fee taken to reserve, so user receives lamports based on tokens after fee)
let fee = tokens_in.checked_mul(state.fee_bps as u64).ok_or(ExchangeError::MathOverflow)?
    .checked_div(10000).ok_or(ExchangeError::MathOverflow)?;
let tokens_after_fee = tokens_in.checked_sub(fee).ok_or(ExchangeError::MathOverflow)?;

let lamports_to_user = tokens_after_fee.checked_div(tokens_per_lamport).ok_or(ExchangeError::MathOverflow)?;

require!(lamports_to_user >= min_lamports_out, ExchangeError::SlippageExceeded);

// Transfer tokens from user -> reserve (signed by user)
let cpi_accounts = Transfer {
    from: ctx.accounts.user_token.to_account_info(),
    to: ctx.accounts.reserve.to_account_info(),
    authority: ctx.accounts.user_authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
token::transfer(cpi_ctx, tokens_in)?;

// Send lamports from program's vault (the state account) to user
**ctx.accounts.state.to_account_info().try_borrow_mut_lamports()? -= lamports_to_user;
**ctx.accounts.user_authority.to_account_info().try_borrow_mut_lamports()? += lamports_to_user;

Ok(())

}

}

// ------------------- Accounts & State -------------------

#[account] pub struct State { pub mint: Pubkey, pub reserve: Pubkey, pub authority: Pubkey, pub price_tokens_per_sol: u64, pub fee_bps: u16, }

#[derive(Accounts)] #[instruction(price_tokens_per_sol: u64, fee_bps: u16)] pub struct Initialize<'info> { #[account(init, payer = authority, space = 8 + 32*3 + 8 + 2)] pub state: Account<'info, State>,

#[account(
init,
payer = authority,
mint::decimals = 6,
mint::authority = mint_authority,
)]
pub mint: Account<'info, Mint>,

/// CHECK: PDA that will be mint authority
#[account(seeds = [b"mint_authority", program_id.as_ref()], bump)] pub mint_authority: UncheckedAccount<'info>,

#[account(
init,
payer = authority,
token::mint = mint,
token::authority = reserve_authority,
)]
pub reserve: Account<'info, TokenAccount>,

/// CHECK: PDA that will be reserve authority for transferring tokens #[account(seeds = [b"reserve_authority", program_id.as_ref()], bump)] pub reserve_authority: UncheckedAccount<'info>,

#[account(mut)]
pub authority: Signer<'info>,

pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,

}

#[derive(Accounts)] pub struct Buy<'info> { #[account(mut)] pub state: Account<'info, State>,

/// CHECK: reserve authority PDA
#[account(seeds = [b"reserve_authority", program_id.as_ref()], bump)] pub reserve_authority: UncheckedAccount<'info>,

#[account(mut, token::mint = state.mint, token::authority = reserve_authority)] pub reserve: Account<'info, TokenAccount>,

#[account(mut)]
pub payer: Signer<'info>,

#[account(mut, token::mint = state.mint, token::authority = payer)] pub user_token: Account<'info, TokenAccount>,

pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,

// helper to compute lamports sent by client
#[account(mut)]
pub payer_starting_balance: AccountInfo<'info>,

}

#[derive(Accounts)] pub struct Sell<'info> { #[account(mut)] pub state: Account<'info, State>,

#[account(mut, token::mint = state.mint)]
pub reserve: Account<'info, TokenAccount>,

#[account(mut)]
pub user_authority: Signer<'info>,

#[account(mut, token::mint = state.mint, token::authority = user_authority)] pub user_token: Account<'info, TokenAccount>,

pub token_program: Program<'info, Token>,

}

// ------------------- Errors -------------------

#[error_code] pub enum ExchangeError { #[msg("No lamports were sent to buy tokens")] NoLamportsSent, #[msg("Invalid price configuration")] InvalidPrice, #[msg("Math overflow")] MathOverflow, #[msg("Slippage exceeded")] SlippageExceeded, #[msg("Invalid amount")] InvalidAmount, }

use anchor_lang::prelude::*; use anchor_spl::token::{self, Mint, TokenAccount, Token, MintTo, Transfer, CloseAccount};

declare_id!("ReplaceWithYourProgramId1111111111111111111111");

// Simple Anchor program that: // - creates an SPL mint with a fixed total supply (100_000_000_000 tokens, decimals = 6) // - holds a token reserve owned by the program // - allows users to buy tokens by sending SOL and selling tokens back for SOL // - uses a fixed price (tokens per 1 SOL) and a small fee (in basis points)

// NOTE: // - This is an educational example. Do NOT use in production without audit. // - We use decimals = 6 so total_supply fits into u64 when scaled. // - The program uses CPI to the SPL Token program.

#[program] pub mod anchor_spl_token_exchange { use super::*;

pub fn initialize(
    ctx: Context<Initialize>,
    price_tokens_per_sol: u64,
    fee_bps: u16,
) -> Result<()> {
    // price_tokens_per_sol: how many token base-units (i.e. including decimals) equals 1 SOL
    // fee_bps: fee in basis points (10000 bps = 100%)

    let state = &mut ctx.accounts.state;
    state.mint = ctx.accounts.mint.key();
    state.reserve = ctx.accounts.reserve.key();
    state.authority = ctx.accounts.authority.key();
    state.price_tokens_per_sol = price_tokens_per_sol;
    state.fee_bps = fee_bps;

    // Mint the total supply to the reserve token account (program-owned reserve)
    let cpi_accounts = MintTo {
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.reserve.to_account_info(),
        authority: ctx.accounts.mint_authority.to_account_info(),
    };

    // mint_authority is a PDA we derived; sign with seeds
    let mint_authority_seeds: &[&[u8]] = &[
        b"mint_authority",
        ctx.program_id.as_ref(),
    ];

    let signer = &[&mint_authority_seeds[..]];

    let cpi_program = ctx.accounts.token_program.to_account_info();
    let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);

    // 100_000_000_000 tokens with decimals = 6 -> scaled supply = 100_000_000_000 * 10^6
    let total_supply: u64 = 100_000_000_000u64.checked_mul(10u64.pow(ctx.accounts.mint.decimals as u32)).unwrap();

    token::mint_to(cpi_ctx, total_supply)?;

    Ok(())
}

// Buy tokens: user attaches SOL (lamports) and receives tokens at fixed price
pub fn buy(ctx: Context<Buy>, min_tokens_out: u64) -> Result<()> {
    let state = &ctx.accounts.state;

    // amount of lamports sent in this instruction must be transfered by the client
    let lamports_sent = ctx.accounts.payer.to_account_info().lamports()
        .checked_sub(ctx.accounts.payer_starting_balance)
        .unwrap_or(0);

    require!(lamports_sent > 0, ExchangeError::NoLamportsSent);

    // tokens per 1 SOL (1 SOL = 1_000_000_000 lamports)
    let tokens_per_lamport = state.price_tokens_per_sol.checked_div(1_000_000_000u64).ok_or(ExchangeError::InvalidPrice)?;
    let tokens_out = lamports_sent.checked_mul(tokens_per_lamport).ok_or(ExchangeError::MathOverflow)?;

    // apply fee
    let fee = tokens_out.checked_mul(state.fee_bps as u64).ok_or(ExchangeError::MathOverflow)?
        .checked_div(10000).ok_or(ExchangeError::MathOverflow)?;
    let tokens_to_user = tokens_out.checked_sub(fee).ok_or(ExchangeError::MathOverflow)?;

    require!(tokens_to_user >= min_tokens_out, ExchangeError::SlippageExceeded);

    // Transfer tokens from reserve -> user_token_account (program signed)
    let cpi_accounts = Transfer {
        from: ctx.accounts.reserve.to_account_info(),
        to: ctx.accounts.user_token.to_account_info(),
        authority: ctx.accounts.reserve_authority.to_account_info(),
    };

    let seeds = &[b"reserve_authority", ctx.program_id.as_ref()];
    let signer = &[&seeds[..]];
    let cpi_ctx = CpiContext::new_with_signer(ctx.accounts.token_program.to_account_info(), cpi_accounts, signer);
    token::transfer(cpi_ctx, tokens_to_user)?;

    Ok(())
}

// Sell tokens: user transfers tokens to reserve, program sends SOL back at fixed price
pub fn sell(ctx: Context<Sell>, tokens_in: u64, min_lamports_out: u64) -> Result<()> {
    let state = &mut ctx.accounts.state;

    require!(tokens_in > 0, ExchangeError::InvalidAmount);

    // compute lamports out = tokens_in / tokens_per_lamport
    let tokens_per_lamport = state.price_tokens_per_sol.checked_div(1_000_000_000u64).ok_or(ExchangeError::InvalidPrice)?;
    require!(tokens_per_lamport > 0, ExchangeError::InvalidPrice);

    let lamports_out = tokens_in.checked_div(tokens_per_lamport).ok_or(ExchangeError::MathOverflow)?;

    // apply fee on tokens (fee taken to reserve, so user receives lamports based on tokens after fee)
    let fee = tokens_in.checked_mul(state.fee_bps as u64).ok_or(ExchangeError::MathOverflow)?
        .checked_div(10000).ok_or(ExchangeError::MathOverflow)?;
    let tokens_after_fee = tokens_in.checked_sub(fee).ok_or(ExchangeError::MathOverflow)?;

    let lamports_to_user = tokens_after_fee.checked_div(tokens_per_lamport).ok_or(ExchangeError::MathOverflow)?;

    require!(lamports_to_user >= min_lamports_out, ExchangeError::SlippageExceeded);

    // Transfer tokens from user -> reserve (signed by user)
    let cpi_accounts = Transfer {
        from: ctx.accounts.user_token.to_account_info(),
        to: ctx.accounts.reserve.to_account_info(),
        authority: ctx.accounts.user_authority.to_account_info(),
    };
    let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
    token::transfer(cpi_ctx, tokens_in)?;

    // Send lamports from program's vault (the state account) to user
    **ctx.accounts.state.to_account_info().try_borrow_mut_lamports()? -= lamports_to_user;
    **ctx.accounts.user_authority.to_account_info().try_borrow_mut_lamports()? += lamports_to_user;

    Ok(())
}

}

// ------------------- Accounts & State -------------------

#[account] pub struct State { pub mint: Pubkey, pub reserve: Pubkey, pub authority: Pubkey, pub price_tokens_per_sol: u64, pub fee_bps: u16, }

#[derive(Accounts)] #[instruction(price_tokens_per_sol: u64, fee_bps: u16)] pub struct Initialize<'info> { #[account(init, payer = authority, space = 8 + 32*3 + 8 + 2)] pub state: Account<'info, State>,

#[account(
    init,
    payer = authority,
    mint::decimals = 6,
    mint::authority = mint_authority,
)]
pub mint: Account<'info, Mint>,

/// CHECK: PDA that will be mint authority
#[account(seeds = [b"mint_authority", program_id.as_ref()], bump)]
pub mint_authority: UncheckedAccount<'info>,

#[account(
    init,
    payer = authority,
    token::mint = mint,
    token::authority = reserve_authority,
)]
pub reserve: Account<'info, TokenAccount>,

/// CHECK: PDA that will be reserve authority for transferring tokens
#[account(seeds = [b"reserve_authority", program_id.as_ref()], bump)]
pub reserve_authority: UncheckedAccount<'info>,

#[account(mut)]
pub authority: Signer<'info>,

pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,

}

#[derive(Accounts)] pub struct Buy<'info> { #[account(mut)] pub state: Account<'info, State>,

/// CHECK: reserve authority PDA
#[account(seeds = [b"reserve_authority", program_id.as_ref()], bump)]
pub reserve_authority: UncheckedAccount<'info>,

#[account(mut, token::mint = state.mint, token::authority = reserve_authority)]
pub reserve: Account<'info, TokenAccount>,

#[account(mut)]
pub payer: Signer<'info>,

#[account(mut, token::mint = state.mint, token::authority = payer)]
pub user_token: Account<'info, TokenAccount>,

pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,

// helper to compute lamports sent by client
#[account(mut)]
pub payer_starting_balance: AccountInfo<'info>,

}

#[derive(Accounts)] pub struct Sell<'info> { #[account(mut)] pub state: Account<'info, State>,

#[account(mut, token::mint = state.mint)]
pub reserve: Account<'info, TokenAccount>,

#[account(mut)]
pub user_authority: Signer<'info>,

#[account(mut, token::mint = state.mint, token::authority = user_authority)]
pub user_token: Account<'info, TokenAccount>,

pub token_program: Program<'info, Token>,

}

// ------------------- Errors -------------------

#[error_code] pub enum ExchangeError { #[msg("No lamports were sent to buy tokens")] NoLamportsSent, #[msg("Invalid price configuration")] InvalidPrice, #[msg("Math overflow")] MathOverflow, #[msg("Slippage exceeded")] SlippageExceeded, #[msg("Invalid amount")] InvalidAmount, }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant