Add Solana Anchor programs for LANTERN token ecosystem#1
Add Solana Anchor programs for LANTERN token ecosystem#1myleshorton wants to merge 2 commits intomainfrom
Conversation
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>
There was a problem hiding this comment.
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, andrewards-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.
| 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(); | ||
|
|
There was a problem hiding this comment.
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.
| 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 | |
| ); |
| 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; |
There was a problem hiding this comment.
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.
| 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; |
| #[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>, | ||
| } |
There was a problem hiding this comment.
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.
| 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>, |
There was a problem hiding this comment.
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.
| let claim_deadline = clock.unix_timestamp | ||
| + (config.claim_window_epochs as i64) | ||
| * (config.epoch_duration_seconds as i64); | ||
|
|
There was a problem hiding this comment.
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.
| 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)?; |
| #[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>, | ||
|
|
There was a problem hiding this comment.
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).
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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, | |
| }; |
- 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>
Summary
PeerIdentity) to Solana wallets. Supports trust tiers (0-4), admin-gated registration, and wallet owner-signed updates.Programs
8AxEi4RdPqkhKgVkQWvxrnHi84z73QkpEqdzYY8ryrUUucAuaEqohRYht9XSWcQa3KrhMXBEmTdKzZ4i7zBMtvGH9SQ3f2v4LjtrwudT9irf46gR8ga1QMnkHXZbB8VQjgfTest plan
anchor build— all 3 programs compileanchor 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