Skip to content

Add Solana Anchor programs for LANTERN token ecosystem#1

Open
myleshorton wants to merge 2 commits intomainfrom
adam/solana-programs
Open

Add Solana Anchor programs for LANTERN token ecosystem#1
myleshorton wants to merge 2 commits intomainfrom
adam/solana-programs

Conversation

@myleshorton
Copy link
Contributor

Summary

  • lantern-token program: Token-2022 mint with PermanentDelegate and MetadataPointer extensions (1B supply, 9 decimals). Mint authority is the rewards vault PDA, enabling controlled minting during epoch distribution.
  • peer-registry program: Maps Ed25519 peer identity keys (from Go PeerIdentity) to Solana wallets. Supports trust tiers (0-4), admin-gated registration, and wallet owner-signed updates.
  • rewards-vault program: Epoch-based Merkle reward distribution adapted from Jito's merkle-distributor. Features include on-chain emission cap enforcement with halving schedule, configurable claim windows, and admin reclaim of unclaimed tokens after deadline.
  • merkle-tree library: Shared Rust library with TypeScript mirror for tests. SHA-256 based with domain-separated leaf/internal prefixes and sorted leaves for deterministic trees.

Programs

Program ID Description
lantern-token 8AxEi4RdPqkhKgVkQWvxrnHi84z73QkpEqdzYY8ryrUU Token-2022 mint + config
peer-registry ucAuaEqohRYht9XSWcQa3KrhMXBEmTdKzZ4i7zBMtvG Peer identity → wallet
rewards-vault H9SQ3f2v4LjtrwudT9irf46gR8ga1QMnkHXZbB8VQjgf Merkle epoch rewards

Test plan

  • anchor build — all 3 programs compile
  • anchor test — 13 integration tests pass (mint init, config init, peer registration, wallet updates, trust tiers, vault init, epoch submission, 3 peer claims, double-claim rejection, invalid proof rejection, emission cap enforcement)
  • cargo test -p lantern-merkle-tree — 4 unit tests pass

🤖 Generated with Claude Code

Three on-chain programs implementing the LANTERN token reward system:

- lantern-token: Token-2022 mint with PermanentDelegate and MetadataPointer
  extensions (1B supply, 9 decimals)
- peer-registry: Maps Ed25519 peer identity keys to Solana wallets with
  trust tiers and admin controls
- rewards-vault: Epoch-based Merkle reward distribution with emission caps,
  halving schedule, and claim deadlines (adapted from Jito merkle-distributor)

Includes shared merkle-tree library (Rust + TypeScript mirror) and 13
integration tests covering the full lifecycle: mint creation, peer
registration, epoch submission, peer claims, and error cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a Solana/Anchor workspace implementing the LANTERN token ecosystem: a Token-2022 mint program, a peer identity→wallet registry, an epoch-based Merkle rewards vault, plus a Merkle tree library and Anchor/TypeScript test harness.

Changes:

  • Added three new Anchor programs: lantern-token, peer-registry, and rewards-vault (epoch Merkle distribution + reclaim).
  • Added a shared Rust Merkle tree library and matching TypeScript utilities/tests.
  • Added Solana workspace/tooling files (Anchor.toml, Cargo workspace, TS config, npm dependencies) and integration tests.

Reviewed changes

Copilot reviewed 31 out of 33 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
solana/tsconfig.json TypeScript project settings for Anchor tests.
solana/tests/rewards-vault.ts Integration tests for rewards vault init, epoch submission, claims, and emission cap.
solana/tests/peer-registry.ts Integration tests for peer registration, wallet updates, and trust tiers.
solana/tests/lantern-token.ts Integration tests for Token-2022 mint/config initialization.
solana/tests/helpers/setup.ts PDA helpers + airdrop helper for tests.
solana/tests/helpers/merkle.ts TS Merkle tree + leaf/proof generation for tests.
solana/programs/rewards-vault/src/merkle_verify.rs On-chain Merkle leaf computation + proof verification.
solana/programs/rewards-vault/src/lib.rs Rewards vault program entrypoints, accounts, and error codes.
solana/programs/rewards-vault/src/instructions/submit_epoch.rs Epoch submission + emission cap enforcement + mint-to vault.
solana/programs/rewards-vault/src/instructions/reclaim_unclaimed.rs Admin reclaim of unclaimed rewards after deadline.
solana/programs/rewards-vault/src/instructions/mod.rs Rewards vault instruction module exports.
solana/programs/rewards-vault/src/instructions/initialize_vault.rs Vault config initialization + vault ATA creation.
solana/programs/rewards-vault/src/instructions/claim_rewards.rs Proof verification + transfer to claimant + claim status tracking.
solana/programs/rewards-vault/Cargo.toml Rewards vault crate manifest + dependencies.
solana/programs/peer-registry/src/lib.rs Peer registry program entrypoints, accounts, and error codes.
solana/programs/peer-registry/src/instructions/update_wallet.rs Wallet update instruction gated by current wallet signature.
solana/programs/peer-registry/src/instructions/update_trust_tier.rs Admin-only trust tier update with bounds checking.
solana/programs/peer-registry/src/instructions/register_peer.rs Peer record initialization mapping identity pubkey→wallet.
solana/programs/peer-registry/src/instructions/mod.rs Peer registry instruction module exports.
solana/programs/peer-registry/Cargo.toml Peer registry crate manifest.
solana/programs/lantern-token/src/lib.rs Token program entrypoints, TokenConfig account, constants/errors.
solana/programs/lantern-token/src/instructions/mod.rs Token program instruction module exports.
solana/programs/lantern-token/src/instructions/initialize_mint.rs Token-2022 mint creation with extensions + authority setup.
solana/programs/lantern-token/src/instructions/initialize_config.rs Token config initialization.
solana/programs/lantern-token/Cargo.toml Token program crate manifest.
solana/package.json npm deps/scripts for Anchor TS tests.
solana/package-lock.json Locked npm dependency tree for solana workspace.
solana/libraries/merkle-tree/src/lib.rs Shared Rust Merkle tree implementation + unit tests.
solana/libraries/merkle-tree/Cargo.toml Merkle tree library manifest.
solana/Cargo.toml Workspace members + shared Anchor dependencies + release profile.
solana/Anchor.toml Anchor toolchain/program IDs/provider + test script.
solana/.gitignore Ignores Anchor/target/node_modules outputs.
Files not reviewed (1)
  • solana/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 35 to 39
pub fn handler(ctx: Context<InitializeMint>, _rewards_vault_program: Pubkey) -> Result<()> {
let mint_key = ctx.accounts.mint.key();
let vault_authority_key = ctx.accounts.vault_authority.key();
let admin_key = ctx.accounts.admin.key();

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialize_mint accepts a rewards_vault_program argument but the handler ignores it, and vault_authority is completely unchecked. As a result, the mint authority/permanent delegate can be set to any arbitrary account supplied by the initializer, rather than being bound to the rewards-vault vault_config PDA as described. Consider deriving the expected vault_config PDA from rewards_vault_program (e.g. seeds [b"vault_config"]) and requiring vault_authority.key() == expected, and then using/storing rewards_vault_program so clients can verify the intended authority on-chain.

Suggested change
pub fn handler(ctx: Context<InitializeMint>, _rewards_vault_program: Pubkey) -> Result<()> {
let mint_key = ctx.accounts.mint.key();
let vault_authority_key = ctx.accounts.vault_authority.key();
let admin_key = ctx.accounts.admin.key();
pub fn handler(ctx: Context<InitializeMint>, rewards_vault_program: Pubkey) -> Result<()> {
let mint_key = ctx.accounts.mint.key();
let vault_authority_key = ctx.accounts.vault_authority.key();
let admin_key = ctx.accounts.admin.key();
// Derive the expected vault config PDA from the rewards-vault program and
// ensure the provided vault_authority matches it.
let (expected_vault_authority, _vault_bump) =
Pubkey::find_program_address(&[b"vault_config"], &rewards_vault_program);
require_keys_eq!(
vault_authority_key,
expected_vault_authority,
ErrorCode::AccountDidNotSerialize
);

Copilot uses AI. Check for mistakes.
Comment on lines 29 to 34
pub fn handler(ctx: Context<InitializeConfig>) -> Result<()> {
let config = &mut ctx.accounts.token_config;
config.bump = ctx.bumps.token_config;
config.admin = ctx.accounts.admin.key();
config.mint = ctx.accounts.mint.key();
config.rewards_vault_program = crate::ID;
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TokenConfig.rewards_vault_program is being set to crate::ID (the lantern-token program id), which doesn't match the field name/intent and makes the config misleading for clients. This should likely be set to the rewards-vault program id used for mint authority (e.g. passed as an argument or read from an already-initialized config), otherwise downstream tooling can't reliably discover/validate the expected rewards-vault program.

Suggested change
pub fn handler(ctx: Context<InitializeConfig>) -> Result<()> {
let config = &mut ctx.accounts.token_config;
config.bump = ctx.bumps.token_config;
config.admin = ctx.accounts.admin.key();
config.mint = ctx.accounts.mint.key();
config.rewards_vault_program = crate::ID;
pub fn handler(ctx: Context<InitializeConfig>, rewards_vault_program: Pubkey) -> Result<()> {
let config = &mut ctx.accounts.token_config;
config.bump = ctx.bumps.token_config;
config.admin = ctx.accounts.admin.key();
config.mint = ctx.accounts.mint.key();
config.rewards_vault_program = rewards_vault_program;

Copilot uses AI. Check for mistakes.
Comment on lines 8 to 21
#[account(
init,
payer = admin,
space = 8 + PeerRecord::INIT_SPACE,
seeds = [b"peer", peer_pubkey.as_ref()],
bump,
)]
pub peer_record: Account<'info, PeerRecord>,

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

pub system_program: Program<'info, System>,
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

register_peer is not actually admin-gated: any signer can initialize a peer_record for any peer_pubkey and thereby permanently set the initial wallet/admin for that identity (PDA squatting). This contradicts the PR description and can let an attacker pre-register peers they don't control. Consider introducing a singleton config with an authoritative admin key and constraining admin here to that config (or otherwise validating the caller), so registration can’t be front-run.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +30
pub struct InitializeVault<'info> {
#[account(
init,
payer = admin,
space = 8 + VaultConfig::INIT_SPACE,
seeds = [b"vault_config"],
bump,
)]
pub vault_config: Account<'info, VaultConfig>,

/// CHECK: Token-2022 mint, validated by ATA creation.
pub mint: UncheckedAccount<'info>,

/// CHECK: ATA for the vault, created via CPI.
#[account(mut)]
pub token_vault: UncheckedAccount<'info>,

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

pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialize_vault allows the first caller to initialize the singleton vault_config PDA and become config.admin without any authorization check. If this program is deployed uninitialized, a third party can front-run initialization and permanently take admin control of emissions/reclaim. Consider constraining the initializer (e.g., to a preconfigured admin pubkey, or an on-chain config/program-data authority) so ownership can’t be claimed by an arbitrary signer.

Copilot uses AI. Check for mistakes.
Comment on lines 100 to 103
let claim_deadline = clock.unix_timestamp
+ (config.claim_window_epochs as i64)
* (config.epoch_duration_seconds as i64);

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claim_deadline is computed via clock.unix_timestamp + (claim_window_epochs as i64) * (epoch_duration_seconds as i64) with lossy u64 -> i64 casts and unchecked multiplication/addition. Large values can wrap to negative deadlines or overflow and abort (overflow-checks are enabled in release). Use checked arithmetic and validate the inputs fit into i64 (or compute in i128 and bounds-check) before storing deadlines.

Suggested change
let claim_deadline = clock.unix_timestamp
+ (config.claim_window_epochs as i64)
* (config.epoch_duration_seconds as i64);
let claim_window_epochs_i64 = i64::try_from(config.claim_window_epochs)
.map_err(|_| VaultError::ArithmeticOverflow)?;
let epoch_duration_i64 = i64::try_from(config.epoch_duration_seconds)
.map_err(|_| VaultError::ArithmeticOverflow)?;
let claim_deadline_delta = claim_window_epochs_i64
.checked_mul(epoch_duration_i64)
.ok_or(VaultError::ArithmeticOverflow)?;
let claim_deadline = clock
.unix_timestamp
.checked_add(claim_deadline_delta)
.ok_or(VaultError::ArithmeticOverflow)?;

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +34
#[account(
init,
payer = claimant,
space = 8 + ClaimStatus::INIT_SPACE,
seeds = [b"claim", claimant.key().as_ref(), epoch_distributor.key().as_ref()],
bump,
)]
pub claim_status: Account<'info, ClaimStatus>,

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VaultError::AlreadyClaimed is declared but the program never returns it: claim_status uses init, so a double-claim fails with a generic system "account already in use" error instead of a stable program error. Either remove the unused error, or switch to a pattern that can intentionally return AlreadyClaimed (e.g., init_if_needed + explicit require! on an initialized flag / claimed_at).

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 20
use solana_sha256_hasher::hashv;

pub const LEAF_PREFIX: &[u8] = &[0x00];
pub const INTERNAL_PREFIX: &[u8] = &[0x01];

pub fn compute_leaf(claimant: &[u8; 32], amount: u64) -> [u8; 32] {
hashv(&[LEAF_PREFIX, claimant, &amount.to_le_bytes()]).to_bytes()
}

pub fn verify_proof(proof: &[[u8; 32]], root: [u8; 32], leaf: [u8; 32]) -> bool {
let mut computed = leaf;
for node in proof {
if computed <= *node {
computed = hashv(&[INTERNAL_PREFIX, &computed, node]).to_bytes();
} else {
computed = hashv(&[INTERNAL_PREFIX, node, &computed]).to_bytes();
}
}
computed == root
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rewards-vault program contains a local merkle_verify implementation that is effectively duplicated by libraries/merkle-tree (same prefixes, leaf hashing, and proof verification). Since the PR description calls out a shared merkle-tree library, consider depending on lantern-merkle-tree from rewards-vault and reusing it (or remove one copy) to avoid future divergence between on-chain verification and the shared library/tests.

Suggested change
use solana_sha256_hasher::hashv;
pub const LEAF_PREFIX: &[u8] = &[0x00];
pub const INTERNAL_PREFIX: &[u8] = &[0x01];
pub fn compute_leaf(claimant: &[u8; 32], amount: u64) -> [u8; 32] {
hashv(&[LEAF_PREFIX, claimant, &amount.to_le_bytes()]).to_bytes()
}
pub fn verify_proof(proof: &[[u8; 32]], root: [u8; 32], leaf: [u8; 32]) -> bool {
let mut computed = leaf;
for node in proof {
if computed <= *node {
computed = hashv(&[INTERNAL_PREFIX, &computed, node]).to_bytes();
} else {
computed = hashv(&[INTERNAL_PREFIX, node, &computed]).to_bytes();
}
}
computed == root
}
pub use lantern_merkle_tree::{
compute_leaf,
verify_proof,
INTERNAL_PREFIX,
LEAF_PREFIX,
};

Copilot uses AI. Check for mistakes.
- Validate vault_authority in initialize_mint matches expected PDA
  derived from rewards_vault_program (prevents arbitrary mint authority)
- Fix initialize_config to accept rewards_vault_program as parameter
  instead of incorrectly storing crate::ID
- Add RegistryConfig singleton PDA to peer-registry with
  initialize_registry instruction, gate register_peer and
  update_trust_tier through registry admin check
- Use checked arithmetic for claim_deadline computation in submit_epoch
  to prevent overflow from lossy u64→i64 casts
- Remove unused AlreadyClaimed error variant (init constraint already
  prevents double claims via account-already-in-use)
- Deduplicate merkle verification by reusing lantern-merkle-tree library
  in rewards-vault instead of maintaining a separate copy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants