Skip to content

Comments

Add Anchor example programs#26

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

Add Anchor example programs#26
tilo-14 wants to merge 14 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.

tilo-14 added 2 commits February 19, 2026 16:11
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.

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

Open in Devin Review

Comment on lines +106 to +124
let cpi = TransferInterfaceCpi::new(
params.token_a_offered_amount,
decimals,
ctx.accounts.maker_token_account_a.to_account_info(),
ctx.accounts.vault.to_account_info(),
ctx.accounts.fee_payer.to_account_info(),
ctx.accounts.fee_payer.to_account_info(),
ctx.accounts.light_token_cpi_authority.to_account_info(),
ctx.accounts.system_program.to_account_info(),
)
.with_spl_interface(
Some(ctx.accounts.token_mint_a.to_account_info()),
Some(ctx.accounts.token_program.to_account_info()),
Some(ctx.accounts.spl_interface_pda_a.to_account_info()),
Some(params.spl_interface_bump_a),
)
?;

cpi.invoke()?;
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Escrow unconditionally configures SPL interface for all transfers, breaking Light-to-Light token config

The escrow program's make_offer and take_offer instructions always call with_spl_interface() on TransferInterfaceCpi, even for Light-to-Light transfers where no SPL interface PDA exists. For the Light token config, the test passes Pubkey::default() for the PDA and an unrelated bump, which will cause the Light Token CPI to fail at runtime.

Detailed Explanation

In escrow/src/instructions/make_offer.rs:106-122, the transfer always configures SPL interface:

let cpi = TransferInterfaceCpi::new(...)
    .with_spl_interface(
        Some(ctx.accounts.token_mint_a.to_account_info()),
        Some(ctx.accounts.token_program.to_account_info()),
        Some(ctx.accounts.spl_interface_pda_a.to_account_info()),
        Some(params.spl_interface_bump_a),
    )?;

Similarly in escrow/src/instructions/take_offer.rs:99-115 and take_offer.rs:129-145.

Contrast with the fundraiser (fundraiser/src/instructions/contribute.rs:107-114) and token-swap (token-swap/src/instructions/swap_exact_tokens_for_tokens.rs:81,94-101) programs which correctly use Option<AccountInfo> for the SPL interface PDA and conditionally call with_spl_interface:

if self.spl_interface_pda.is_some() {
    cpi = cpi.with_spl_interface(...)?
}
cpi.invoke()?;

The escrow's spl_interface_pda_a and spl_interface_pda_b are declared as UncheckedAccount (required), not Option<AccountInfo>. For the Light config test, the test passes Pubkey::default() (escrow/tests/escrow.rs:289) because spl_interface_a is None. This bogus PDA will be sent to with_spl_interface, causing the Light Token program to attempt a cross-standard transfer path when a direct Light-to-Light transfer is needed.

Impact: The test_escrow_light, test_escrow_spl_light, and test_escrow_t22_light tests will fail at runtime (though this is masked by the local path deps issue preventing any CI execution).

Prompt for agents
In programs/anchor/escrow/src/instructions/make_offer.rs (the MakeOffer struct and send_offered_tokens_to_vault function) and programs/anchor/escrow/src/instructions/take_offer.rs (the TakeOffer struct and both send_wanted_tokens_to_maker and withdraw_from_vault functions):

1. Change `spl_interface_pda_a` and `spl_interface_pda_b` from `UncheckedAccount<'info>` to `Option<AccountInfo<'info>>` in both account structs (MakeOffer and TakeOffer).

2. Wrap the `with_spl_interface` calls in a conditional, matching the pattern used in fundraiser and token-swap:
   let mut cpi = TransferInterfaceCpi::new(...);
   if ctx.accounts.spl_interface_pda_a.is_some() {
       cpi = cpi.with_spl_interface(
           Some(ctx.accounts.token_mint_a.to_account_info()),
           Some(ctx.accounts.token_program.to_account_info()),
           ctx.accounts.spl_interface_pda_a.as_ref().map(|a| a.to_account_info()),
           Some(params.spl_interface_bump_a),
       )?;
   }
   cpi.invoke()?;

3. Update the test in programs/anchor/escrow/tests/escrow.rs to pass Option values (Some(pda) for SPL/T22, None for Light) instead of unwrap_or_default().
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