basic_token_staking

Conceptual Seahorse program for basic staking of BRB token .

# seahorse_example_staking.py
# Conceptual Seahorse program for basic staking of a token (could be BRB).
# Assumes the token exists (e.g., the MPL-404 BRB token).

from seahorse.prelude import *

# Assume BRB Token Mint Pubkey is known
# declare_id('BRBTokenMintAddressHERE') # Replace with actual Mint address
# brb_token_mint = Pubkey('BRBTokenMintAddressHERE')

# Declare the Token Program ID (could be Token-2022 or legacy SPL Token)
# Replace with the correct program ID used by your BRB token
declare_id('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') # Legacy SPL Token example
token_program = Program('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')

# --- Staking Vault Configuration ---
@account
class StakingVault:
    admin: Pubkey
    brb_token_mint: Pubkey      # Mint of the token being staked
    vault_token_account: Pubkey # PDA token account holding staked tokens
    total_staked: u64
    reward_rate_per_slot: u64 # Simple reward calculation example

# --- User Stake Record ---
@account
class StakeRecord:
    owner: Pubkey           # The user who staked
    amount_staked: u64
    last_claimed_slot: u64  # Slot when rewards were last claimed

# --- Instruction: Initialize Staking Vault ---
@instruction
def init_staking_vault(
    vault: Empty[StakingVault],         # Account to be created
    vault_token_account: Empty[TokenAccount], # Token account PDA to be created
    brb_token_mint: Account,       # Mint account of the token
    admin: Signer,                  # Authority initializing the vault
    reward_rate: u64
):
    # Create the vault PDA token account to hold staked tokens
    # Authority is the vault account itself (PDA)
    vault_pda_token_account = vault_token_account.init(
        payer = admin,
        seeds = ['vault-token-account', vault], # Use vault address as seed
        mint = brb_token_mint,
        authority = vault # The vault PDA is the authority
    )

    # Create the StakingVault config account
    v = vault.init(payer=admin, seeds=['staking-vault', brb_token_mint])
    v.admin = admin.key()
    v.brb_token_mint = brb_token_mint.key()
    v.vault_token_account = vault_pda_token_account.key()
    v.total_staked = 0
    v.reward_rate_per_slot = reward_rate # e.g., units of reward per slot per token staked

    print("Staking Vault Initialized")

# --- Instruction: Create Stake Record ---
# Users need to create this once before staking
@instruction
def create_stake_record(
    record: Empty[StakeRecord], # Account to be created
    vault: StakingVault,        # Reference the vault config
    staker: Signer              # The user creating the record
):
    # Create the StakeRecord account for the user
    rec = record.init(
        payer = staker,
        seeds = ['stake-record', vault, staker.key()] # Seeds ensure one record per user per vault
    )
    rec.owner = staker.key()
    rec.amount_staked = 0
    rec.last_claimed_slot = Clock.get().slot # Initialize to current slot
    print(f"Stake Record created for {staker.key()}")


# --- Instruction: Stake Tokens ---
@instruction
def stake_tokens(
    vault: Mut[StakingVault],           # Vault to update total staked
    record: Mut[StakeRecord],           # User's stake record to update
    staker: Signer,                     # User initiating the stake
    staker_token_account: TokenAccount, # User's source token account
    vault_token_account: Mut[TokenAccount], # PDA token account holding staked tokens
    amount: u64
):
    # 1. Check authority and ownership
    assert staker.key() == record.owner, "Signer mismatch"
    assert staker_token_account.owner == staker.key(), "Token account owner mismatch"
    assert vault_token_account.key() == vault.vault_token_account, "Vault token account mismatch"
    assert amount > 0, "Stake amount must be positive"

    # 2. Calculate and claim pending rewards before staking more
    # (Good practice to avoid reward calculation complexity)
    # claim_rewards(vault, record, staker, vault_token_account, staker_token_account) # Call claim logic if separate

    # 3. Transfer tokens from staker to vault PDA token account using CPI
    transfer(
        from_ = staker_token_account,
        to = vault_token_account,
        authority = staker,
        amount = amount
    ).invoke(token_program.key()) # Invoke SPL Token transfer

    # 4. Update stake record and vault total
    record.amount_staked += amount
    vault.total_staked += amount
    # Update last_claimed_slot if rewards were claimed implicitly here
    record.last_claimed_slot = Clock.get().slot

    print(f"{amount} tokens staked by {staker.key()}. Total staked: {record.amount_staked}")

# --- Instruction: Unstake Tokens ---
@instruction
def unstake_tokens(
    vault: Mut[StakingVault],           # Vault to update total staked
    record: Mut[StakeRecord],           # User's stake record to update
    staker: Signer,                     # User initiating the unstake (must match record.owner)
    staker_token_account: Mut[TokenAccount], # User's destination token account
    vault_token_account: Mut[TokenAccount], # PDA token account holding staked tokens
    vault_pda_signer: SeahorseSigner,   # PDA signer for the vault account
    amount: u64
):
    # 1. Check authority and ownership
    assert staker.key() == record.owner, "Signer mismatch"
    assert staker_token_account.owner == staker.key(), "Token account owner mismatch"
    assert vault_token_account.key() == vault.vault_token_account, "Vault token account mismatch"
    assert amount > 0, "Unstake amount must be positive"
    assert record.amount_staked >= amount, "Insufficient staked balance"

    # 2. Calculate and claim pending rewards before unstaking
    # claim_rewards(...) # Call claim logic

    # 3. Transfer tokens from vault PDA back to staker using CPI
    # Requires signing with the vault PDA's seeds
    transfer(
        from_ = vault_token_account,
        to = staker_token_account,
        authority = vault_pda_signer, # Use the PDA signer derived from seeds
        amount = amount
    ).invoke_signed(
        token_program.key(),
        signer_seeds = [['vault-token-account', vault]] # Seeds used to create the vault_token_account
    )

    # 4. Update stake record and vault total
    record.amount_staked -= amount
    vault.total_staked -= amount
    record.last_claimed_slot = Clock.get().slot # Update claim slot

    print(f"{amount} tokens unstaked by {staker.key()}. Remaining staked: {record.amount_staked}")


# --- Instruction: Claim Rewards (Simplified Example) ---
@instruction
def claim_rewards(
    vault: StakingVault,                # Vault config for reward rate
    record: Mut[StakeRecord],           # User's stake record
    staker: Signer,                     # User claiming rewards
    staker_token_account: Mut[TokenAccount], # User's destination token account
    reward_source_account: Mut[TokenAccount], # Account holding rewards to distribute
    reward_source_authority: Signer # Authority that can send rewards (e.g., admin)
    # In a real scenario, rewards might be minted or come from a treasury PDA
):
    # 1. Check authority
    assert staker.key() == record.owner, "Signer mismatch"

    # 2. Calculate elapsed slots and rewards
    current_slot = Clock.get().slot
    slots_elapsed = current_slot - record.last_claimed_slot
    # Simple reward calculation: rate * amount_staked * slots_elapsed
    # WARNING: This is a basic example. Real reward calculation needs care
    # regarding precision, large numbers, and potential exploits.
    # Consider using u128 for intermediate calculations.
    reward_amount = (vault.reward_rate_per_slot * record.amount_staked * slots_elapsed) // 1_000_000 # Adjust divisor based on rate scaling

    if reward_amount == 0:
      print("No rewards to claim yet.")
      return

    # 3. Transfer rewards from the source to the staker
    # This assumes an external account holds rewards. A better model might
    # use a rewards pool PDA funded separately.
    transfer(
        from_ = reward_source_account,
        to = staker_token_account,
        authority = reward_source_authority,
        amount = reward_amount
    ).invoke(token_program.key()) # Or invoke_signed if source is PDA

    # 4. Update the last claimed slot
    record.last_claimed_slot = current_slot

    print(f"{reward_amount} rewards claimed by {staker.key()}")

Last updated