Skip to content

Add Anchor example programs#26

Merged
tilo-14 merged 20 commits intomainfrom
pr/anchor-examples
Feb 27, 2026
Merged

Add Anchor example programs#26
tilo-14 merged 20 commits intomainfrom
pr/anchor-examples

Conversation

@tilo-14
Copy link
Member

@tilo-14 tilo-14 commented Feb 16, 2026

Summary

  • Add four Light Token Anchor example programs: escrow, fundraiser, token-swap, light-token-minter
  • Each program tests 5 token configurations (SPL, Token2022, Light, LightSpl, LightT22)
  • Shared test utilities crate for mint/ATA creation, balance verification, SPL interface PDAs
  • Fix light-token-minter: remove deprecated associated_token::bump (now derived on-chain)
  • Update CI workflow to test the four example programs

Programs

Program Description
escrow Peer-to-peer token swap with rent-free Light Token vaults
fundraiser Crowdfunding with contribute, claim, and refund flows
token-swap Constant-product AMM with rent-free pool vaults
light-token-minter Light Token mint creation and minting

Test plan

  • cargo test-sbf -p escrow
  • cargo test-sbf -p fundraiser
  • cargo test-sbf -p light-token-minter
  • cargo test-sbf -p swap_example

Open with Devin

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Peer-to-peer escrow, crowdfunding, AMM swap, and mint helper programs
demonstrating rent-free Light Token vaults with full test coverage
across SPL, Token-2022, and Light token standards.
tilo-14 and others added 5 commits February 18, 2026 22:16
Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com>
Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com>
Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com>
…for_tokens.rs

Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com>
Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 27 additional findings in Devin Review.

Open in Devin Review

Comment on lines +66 to +70
let mut liquidity = I64F64::from_num(amount_a)
.checked_mul(I64F64::from_num(amount_b))
.ok_or(SwapError::Overflow)?
.sqrt()
.to_num::<u64>();
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 AMM deposit_liquidity uses sqrt for subsequent deposits instead of proportional minting, allowing value extraction from existing LPs

For non-initial deposits, deposit_liquidity computes LP tokens as sqrt(amount_a * amount_b) (line 66-70). The correct formula for subsequent deposits is min(amount_a * total_supply / reserve_a, amount_b * total_supply / reserve_b), which ensures the new depositor's share is proportional to their contribution.

Root Cause and Impact

After swaps with fees, the pool invariant R_A * R_B increases beyond the initial a1 * b1. Since sqrt(R_A * R_B) > sqrt(a1 * b1), a proportional deposit of k% of each reserve yields k * sqrt(R_A * R_B) LP tokens — more than the proportional k * total_supply.

Concrete example:

  • Initial deposit: 1000 A + 1000 B → lp_supply = 900 (sqrt(1M) - 100)
  • After swaps with fees, pool is 1100 A, 920 B (product = 1,012,000 > 1,000,000)
  • Second depositor adds 10% of each: 110 A, 92 B
  • Correct LP: min(110*1000/1100, 92*1000/920) = 100
  • Actual LP: sqrt(110*92) ≈ 100.6

The 0.6 extra LP tokens dilute existing LP holders' share. The discrepancy scales with the accumulated swap fees. This is inherited from the original Solana Program Examples SPL AMM that this code ports, but it is a real correctness issue in the newly added program.

Prompt for agents
In programs/anchor/token-swap/src/instructions/deposit_liquidity.rs, replace the sqrt-based LP minting at lines 66-77 with a proportional formula for non-initial deposits. For initial deposits (pool_creation == true), keep sqrt(amount_a * amount_b) - MINIMUM_LIQUIDITY. For subsequent deposits, compute: liquidity = min(amount_a * (lp_supply + MINIMUM_LIQUIDITY) / pool_a_balance, amount_b * (lp_supply + MINIMUM_LIQUIDITY) / pool_b_balance) using the existing I64F64 fixed-point math to avoid overflow. The lp_supply value should be read from ctx.accounts.pool.lp_supply, and MINIMUM_LIQUIDITY is already imported from constants.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

tilo-14 added 5 commits February 18, 2026 23:55
Now that the SDK returns ProgramError directly, the manual conversion
at every call site is unnecessary.
…-compressed-account

light-token was a path dep while light-client and light-program-test
were registry deps, causing two copies of light-compressed-account@0.11.0
(path vs registry). This made CompressedProof types incompatible across
the two dependency trees. Changed all 12 Light Protocol workspace deps
to path deps pointing to the local monorepo.
Replace UncheckedAccount + Pubkey::default() sentinel with
Option<AccountInfo> + .is_some() for optional SPL interface PDAs
in fundraiser (contribute, checker, refund) and deposit_liquidity.
Remove redundant .map_err() in transfer-interface and create-and-transfer.
devin-ai-integration[bot]

This comment was marked as resolved.

Replace TransferFromSpl with TransferInterface in shared-test-utils for
consistency with program-side TransferInterfaceCpi. Update escrow,
fundraiser, and token-swap programs and tests.
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

- Add mint param to TransferInterfaceCpi::new() (all programs)
- Make fee_payer mandatory, remove Option wrapper (rust-client, pinocchio)
- Add fee_payer to Approve/Revoke structs (rust-client)
- Add mint field to TransferInterface structs (rust-client, shared-test-utils)
- Make escrow spl_interface_pda accounts optional (Option<AccountInfo>)
  with conditional with_spl_interface for Light-to-Light transfers
- Use checked arithmetic in fundraiser contribute (matches refund)
- Replace local path deps with git deps pinned to fix/authority-owner-mutability
tilo-14 added 6 commits February 27, 2026 17:04
The git branch deps pointed to a repo with an SSH submodule
(photon.git) that CI runners can't authenticate. Switch all light-*
crates to crates.io 0.23.0 releases across anchor programs, pinocchio
swap, rust-client, and streaming-tokens toolkit.

Also removes the now-consolidated transfer_checked action/instruction
from rust-client (merged into transfer_interface).
Adds a separate test-pinocchio job since it has a different working
directory and doesn't need the light-token-minter build step.
Replace duplicated Solana/Rust/Light/Photon setup steps with the
shared .github/actions/setup composite action. Bump Solana CLI from
2.1.21 to 2.3.11 to match the Rust workflow. Also normalizes
indentation from 4-space to 2-space to match rust-tests.yml.
- Remove transfer-checked row from README (files deleted)
- Update CLAUDE.md: local path deps → crates.io 0.23.0
- Add pinocchio/swap build command to CLAUDE.md
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 38 additional findings in Devin Review.

Open in Devin Review

Comment on lines 194 to 196
/// CHECK: Can be SPL, T22, or Light
#[account(mut)]
pub mint_liquidity: UncheckedAccount<'info>,
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Missing LP mint validation allows cross-pool LP token attack in DepositLiquidity and WithdrawLiquidity

The mint_liquidity account in DepositLiquidity and WithdrawLiquidity is an UncheckedAccount with no seeds, has_one, or address constraint validating it corresponds to the pool being operated on. Since all LP mints share the same global pool_authority PDA (derived from just [b"authority"]) as their mint authority, the mint_to and burn CPIs will succeed with any LP mint created by the program.

Exploit Scenario

The Pool struct at token-swap/src/state.rs:18-24 does not store the LP mint address, and DepositLiquidity at token-swap/src/instructions/deposit_liquidity.rs:194-196 declares mint_liquidity with no validation:

/// CHECK: Can be SPL, T22, or Light
#[account(mut)]
pub mint_liquidity: UncheckedAccount<'info>,

All LP mints have pool_authority (a global PDA from [AUTHORITY_SEED]) as their mint authority. An attacker can:

  1. Create pool X (legitimate, with depositors)
  2. Create pool Y (attacker-controlled, minimal liquidity)
  3. Call deposit_liquidity on pool Y but pass pool X's LP mint as mint_liquidity → receives pool X LP tokens
  4. Call withdraw_liquidity on pool X with those LP tokens → extracts pool X's funds

The mint_to CPI in deposit_liquidity succeeds because pool X's LP mint authority matches pool_authority. The burn CPI in withdraw_liquidity succeeds because the attacker owns valid pool X LP tokens. Pool X's lp_supply is decremented, locking out legitimate LPs.

Impact: An attacker can drain accumulated fees (and potentially principal) from any pool by cross-minting LP tokens via a sacrificial pool.

Prompt for agents
Fix the missing LP mint validation in both DepositLiquidity (programs/anchor/token-swap/src/instructions/deposit_liquidity.rs) and WithdrawLiquidity (programs/anchor/token-swap/src/instructions/withdraw_liquidity.rs).

1. Add a `mint_liquidity` field to the `Pool` struct in programs/anchor/token-swap/src/state.rs. Update `Pool::LEN` accordingly.

2. In create_pool.rs and create_pool_light_lp.rs, store the LP mint address in pool.mint_liquidity during initialization.

3. In DepositLiquidity, change `mint_liquidity` from UncheckedAccount to have a constraint: `constraint = mint_liquidity.key() == pool.mint_liquidity @ SwapError::InvalidMint`.

4. In WithdrawLiquidity, add the same constraint on `mint_liquidity`.

Alternatively, if you don't want to change Pool state, you could derive the expected LP mint address from seeds and verify it matches, but storing it is simpler and more robust.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Replace terminology violations across pinocchio-swap and anchor
token-swap: ATA/ATAs → associated token account(s), T22 → Token 2022,
standalone Light → Light Token.
@tilo-14 tilo-14 merged commit 44457ad into main Feb 27, 2026
23 checks passed
@tilo-14 tilo-14 deleted the pr/anchor-examples branch February 27, 2026 19:23
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 39 additional findings in Devin Review.

Open in Devin Review

Comment on lines +47 to +63
if pool_a_balance > pool_b_balance {
(
I64F64::from_num(amount_b)
.checked_mul(ratio)
.ok_or(SwapError::Overflow)?
.to_num::<u64>(),
amount_b,
)
} else {
(
amount_a,
I64F64::from_num(amount_a)
.checked_div(ratio)
.ok_or(SwapError::Underflow)?
.to_num::<u64>(),
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Deposit liquidity ratio adjustment inflates amounts beyond depositor balance, causing unnecessary reverts

The deposit_liquidity ratio adjustment logic always inflates one side of the deposit, which can exceed the depositor's capped balance and cause the transaction to revert.

Root Cause

When an existing pool has a non-1:1 ratio, the code adjusts deposit amounts to match the pool's ratio. However, both branches inflate one of the amounts rather than deflating the other:

  • When pool_a_balance > pool_b_balance (ratio > 1): amount_a = amount_b * ratio — increases amount_a beyond its capped value
  • When pool_a_balance <= pool_b_balance (ratio <= 1): amount_b = amount_a / ratio — since ratio <= 1, dividing by it increases amount_b beyond its capped value

The amounts were already capped to the depositor's balance at lines 28-37:

let mut amount_a = if amount_a > ctx.accounts.depositor_account_a.amount {
    ctx.accounts.depositor_account_a.amount
} else { amount_a };

But then lines 47-63 overwrite one amount with a value that can exceed the depositor's token balance. The subsequent TransferInterfaceCpi at line 83/104 will then fail because the user doesn't have enough tokens.

Example: Pool has 100A:50B (ratio=2). User has 200A, 200B. After capping: amount_a=200, amount_b=200. Since pool_a > pool_b, code computes amount_a = 200 * 2 = 400. But user only has 200A → transfer reverts.

The correct approach would be to try both directions and pick the one that fits within both balances, or to always adjust downward (keep the limiting-side amount fixed and reduce the other). The fix would swap the branches: when pool_a > pool_b, keep amount_a fixed and compute amount_b = amount_a / ratio (reducing B); when pool_a <= pool_b, keep amount_b fixed and compute amount_a = amount_b * ratio (reducing A since ratio ≤ 1).

Impact: Deposits into any non-1:1 pool will fail unless the user happens to have excess tokens far beyond the deposit amount. The first deposit (pool creation) and 1:1-ratio pools are unaffected.

Suggested change
if pool_a_balance > pool_b_balance {
(
I64F64::from_num(amount_b)
.checked_mul(ratio)
.ok_or(SwapError::Overflow)?
.to_num::<u64>(),
amount_b,
)
} else {
(
amount_a,
I64F64::from_num(amount_a)
.checked_div(ratio)
.ok_or(SwapError::Underflow)?
.to_num::<u64>(),
)
}
if pool_a_balance > pool_b_balance {
(
amount_a,
I64F64::from_num(amount_a)
.checked_div(ratio)
.ok_or(SwapError::Underflow)?
.to_num::<u64>(),
)
} else {
(
I64F64::from_num(amount_b)
.checked_mul(ratio)
.ok_or(SwapError::Overflow)?
.to_num::<u64>(),
amount_b,
)
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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