Skip to content

Comments

Add Anchor example programs#26

Open
tilo-14 wants to merge 13 commits intomainfrom
pr/anchor-examples
Open

Add Anchor example programs#26
tilo-14 wants to merge 13 commits intomainfrom
pr/anchor-examples

Conversation

@tilo-14
Copy link
Contributor

@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.
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 28 additional findings in Devin Review.

Open in Devin Review

Comment on lines 65 to 77
light-sdk = { path = "/home/tilo/Workspace/light-protocol/sdk-libs/sdk", features = ["anchor", "v2", "cpi-context"] }
light-sdk-macros = { path = "/home/tilo/Workspace/light-protocol/sdk-libs/macros" }
light-sdk-types = { path = "/home/tilo/Workspace/light-protocol/sdk-libs/sdk-types", features = ["v2", "cpi-context"] }
light-account = { path = "/home/tilo/Workspace/light-protocol/sdk-libs/account", features = ["anchor", "token"] }
light-compressible = { path = "/home/tilo/Workspace/light-protocol/program-libs/compressible", features = ["anchor"] }
light-hasher = { path = "/home/tilo/Workspace/light-protocol/program-libs/hasher", features = ["solana"] }
light-macros = { path = "/home/tilo/Workspace/light-protocol/program-libs/macros" }
light-compressed-account = { path = "/home/tilo/Workspace/light-protocol/program-libs/compressed-account" }
light-token = { path = "/home/tilo/Workspace/light-protocol/sdk-libs/token-sdk", features = ["anchor"] }
light-token-types = { path = "/home/tilo/Workspace/light-protocol/sdk-libs/token-types", features = ["anchor"] }
light-token-interface = { path = "/home/tilo/Workspace/light-protocol/program-libs/token-interface" }
light-program-test = { path = "/home/tilo/Workspace/light-protocol/sdk-libs/program-test" }
light-client = { path = "/home/tilo/Workspace/light-protocol/sdk-libs/client", features = ["v2", "anchor"] }
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 All Light Protocol workspace dependencies use hardcoded local filesystem paths that break CI and other developers

All light-* workspace dependencies in Cargo.toml were changed from published crate versions to local filesystem paths pointing to /home/tilo/Workspace/light-protocol/.... This path only exists on one developer's machine.

Root Cause and Impact

The diff shows the change from versioned dependencies (e.g., light-sdk = { version = "0.22.0", ... }) to local path dependencies (e.g., light-sdk = { path = "/home/tilo/Workspace/light-protocol/sdk-libs/sdk", ... }) across lines 65-77 of programs/anchor/Cargo.toml.

While the commit message (fix(deps): unify Light crates to path deps to resolve duplicate light-compressed-account) suggests this was done intentionally to resolve a dependency conflict, the hardcoded absolute paths will cause cargo build-sbf and cargo test-sbf to fail on:

  • The CI runner (.github/workflows/rust-tests.yml runs cargo test-sbf for each program)
  • Any other developer's machine

Impact: The project is unbuildable for anyone except the original developer. All CI jobs for the four new example programs (escrow, fundraiser, light-token-minter, swap_example) will fail.

Prompt for agents
In programs/anchor/Cargo.toml, replace all local path dependencies (lines 65-77) with published crate versions. The original versions were:

light-sdk = { version = "0.22.0", features = ["anchor", "v2", "cpi-context"] }
light-sdk-macros = "0.22.0"
light-sdk-types = { version = "0.22.0", features = ["v2", "cpi-context"] }
light-account = { version = "0.22.0", features = ["anchor", "token"] }
light-compressible = { version = "0.6.0", features = ["anchor"] }
light-hasher = { version = "5.0.0", features = ["solana"] }
light-macros = "2.2.0"
light-compressed-account = "0.11.0"
light-token = { version = "0.22.1", features = ["anchor"] }
light-token-types = { version = "0.22.0", features = ["anchor"] }
light-token-interface = "0.5.0"
light-program-test = "0.22.0"
light-client = { version = "0.22.0", features = ["v2", "anchor"] }

If these versions cause dependency conflicts, use compatible version ranges or a [patch] section in the workspace Cargo.toml to override specific crates, rather than absolute filesystem paths.
Open in Devin Review

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

Replace TransferFromSpl with TransferInterface in shared-test-utils for
consistency with program-side TransferInterfaceCpi. Update escrow,
fundraiser, and token-swap programs and tests.
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 30 additional findings in Devin Review.

Open in Devin Review

require!(amount <= max_contribution, FundraiserError::ContributionTooBig);

let current_time = Clock::get()?.unix_timestamp;
let elapsed_days = ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16;
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Unchecked i64 subtraction in contribute can panic instead of returning an error

The contribute function uses unchecked subtraction current_time - self.fundraiser.time_started at line 82, which can panic (abort the program) if the result overflows. The refund function at programs/anchor/fundraiser/src/instructions/refund.rs:79 uses checked_sub for the identical operation, showing the intended safe pattern.

Detailed Explanation

The workspace Cargo.toml has overflow-checks = true in the release profile (programs/anchor/Cargo.toml:31). This means arithmetic overflow on i64 subtraction causes a runtime panic (program abort) rather than wrapping.

At contribute.rs:82:

let elapsed_days = ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16;

Compare with refund.rs:79:

current_time.checked_sub(self.fundraiser.time_started)
    .ok_or(FundraiserError::CalculationOverflow)?

While current_time < time_started is unlikely under normal conditions, Solana validators have some leeway in clock values across slots. A panic produces an unhelpful "Program failed" error, whereas checked_sub returns a descriptive CalculationOverflow error.

Impact: If the subtraction ever overflows, the program aborts with a generic error instead of returning FundraiserError::CalculationOverflow.

Suggested change
let elapsed_days = ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16;
let elapsed_days = (current_time.checked_sub(self.fundraiser.time_started)
.ok_or(FundraiserError::CalculationOverflow)?
.checked_div(SECONDS_TO_DAYS)
.ok_or(FundraiserError::CalculationOverflow)?) as u16;
Open in Devin Review

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

devin-ai-integration[bot]

This comment was marked as resolved.

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 34 additional findings in Devin Review.

Open in Devin Review

ctx.accounts.authority.to_account_info(),
ctx.accounts.payer.to_account_info(),
ctx.accounts.cpi_authority.to_account_info(),
ctx.accounts.mint.clone().unwrap(),
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Unwrap of Option<AccountInfo> panics on Light-to-Light transfers when mint is None

The transfer function calls ctx.accounts.mint.clone().unwrap() at line 26, but mint is declared as Option<AccountInfo<'info>> at programs/anchor/basic-instructions/transfer-interface/src/lib.rs:71. When performing Light-to-Light transfers (no SPL interface), the test passes mint: None (see programs/anchor/basic-instructions/transfer-interface/tests/test.rs:45), which will cause a runtime panic.

Root Cause

The TransferInterfaceCpi::new() API was updated to require a mint parameter. The code was changed to pass ctx.accounts.mint.clone().unwrap(), but mint is an Option<AccountInfo> that is None for Light-to-Light transfers. The existing test at line 45 of the test file explicitly passes mint: None for this case.

The .unwrap() on None will panic with called Option::unwrap() on a None value, crashing the on-chain program.

Impact: Any Light-to-Light transfer through this program will fail with a panic. Only SPL-to-Light transfers (where mint is Some(...)) will work.

Prompt for agents
In programs/anchor/basic-instructions/transfer-interface/src/lib.rs at line 26, the code calls ctx.accounts.mint.clone().unwrap() but mint is Option<AccountInfo> and can be None for Light-to-Light transfers. The TransferInterfaceCpi::new() now requires a mint AccountInfo parameter. You need to either: (1) make mint a required (non-optional) account since TransferInterfaceCpi::new always needs it, or (2) handle the None case gracefully by returning an error instead of panicking. If mint is always required by the new API, change the account declaration at line 71 from Option<AccountInfo> to AccountInfo and update the test at tests/test.rs line 45 to always provide a mint.
Open in Devin Review

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

- 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
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