Skip to content

feat: light token amm program examples #12

Open
tilo-14 wants to merge 11 commits intojorrit/feat-escrow-anchor-examplefrom
escrow-anchor-example
Open

feat: light token amm program examples #12
tilo-14 wants to merge 11 commits intojorrit/feat-escrow-anchor-examplefrom
escrow-anchor-example

Conversation

@tilo-14
Copy link
Contributor

@tilo-14 tilo-14 commented Jan 27, 2026

Summary

  • Programs: escrow, fundraiser, light-token-minter, token-swap, token-transfer
  • Each program includes tests for SPL, Token-2022, and Light tokens

Programs included

Program Description
escrow Token escrow with light token support
fundraiser Crowdfunding supporting SPL/T22/Light tokens
light-token-minter Create and mint light tokens
token-swap AMM with liquidity pools for light tokens
token-transfer Basic light token transfers
shared-test-utils Common test utilities

Test plan

  • Run anchor build in programs/anchor
  • Run tests for each program

Open with Devin

@tilo-14 tilo-14 changed the title Add Anchor program examples with Light Protocol integration Add escrow amm program examples Jan 27, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@tilo-14 tilo-14 force-pushed the escrow-anchor-example branch from 46f7553 to 9753fa9 Compare January 29, 2026 23:08
- Escrow: peer-to-peer token swap (make_offer, take_offer)
- Fundraiser: time-gated fundraising with refunds
- Token-swap: constant-product AMM with liquidity pools
- Light-token-minter: create light-mints with metadata
- All programs tested across 5 token configs (SPL, T22, Light, LightSpl, LightT22)
- CI aligned with upstream Photon indexer rev
@tilo-14 tilo-14 force-pushed the escrow-anchor-example branch from 9753fa9 to b4f661f Compare January 29, 2026 23:10
@ananas-block ananas-block changed the base branch from main to jorrit/feat-escrow-anchor-example January 30, 2026 14:26
tilo-14 added 2 commits February 10, 2026 12:54
SPL_COMPARISON.md: align all Light code blocks with actual source.
Sections 4-6 had wrong struct fields, function signatures, and
implementations. Section 8 showed outdated 5-file test structure.
Terminology normalized (associated token accounts, cross-standard).

CLAUDE.md: update test architecture section to match consolidated
test structure and terminology.
@tilo-14 tilo-14 changed the title Add escrow amm program examples feat: light token amm program examples Feb 10, 2026
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 4 new potential issues.

View 34 additional findings in Devin Review.

Open in Devin Review

Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Leftover Photon steps reference removed inputs, causing CI failures for all workflows

The old Photon indexer steps (lines 60-73) were not removed during the refactor and reference inputs.photon-version and inputs.photon-commit, which are no longer declared as inputs. These steps have no if condition, so they execute unconditionally in every workflow that uses this action.

Root Cause and Impact

Line 65 builds a cache key using undefined inputs:

key: photon-${{ runner.os }}-${{ inputs.photon-version }}-${{ inputs.photon-commit }}

Both inputs.photon-version and inputs.photon-commit resolve to empty strings, producing a degenerate cache key like photon-Linux--.

Line 68 conditionally installs Photon when the cache misses, but line 72 runs:

cargo install --git https://github.com/helius-labs/photon.git --rev  --locked --force

The empty --rev flag will cause cargo install to fail.

Additionally, there's a duplicate step ID cache-photon at lines 61 and 81. GitHub Actions does not allow duplicate step IDs in composite actions — the second definition shadows the first, making the conditional steps.cache-photon.outputs.cache-hit at line 88 reference the wrong step.

Impact: The Rust test workflow (rust-tests.yml) does not pass photon-indexer: "true", so the old unconditional steps at lines 60-73 will run and fail. The TypeScript workflow passes photon-indexer: "true" but the duplicate ID issue means the new Photon cache step (line 81) conflicts with the old one (line 61).

(Refers to lines 60-73)

Prompt for agents
In .github/actions/setup/action.yml, remove the leftover old Photon indexer steps at lines 60-73 (the 'Cache Photon indexer' step with id cache-photon that references inputs.photon-version and inputs.photon-commit, and the 'Install Photon indexer' step that references the same undefined inputs). These are remnants from the old version and conflict with the new conditional Photon steps at lines 79-92. The old steps have no 'if' condition so they run unconditionally and will fail because the inputs they reference (photon-version, photon-commit) no longer exist.
Open in Devin Review

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

Comment on lines +34 to +38
- name: Setup Node.js
if: inputs.node-version != ''
uses: actions/setup-node@v4
with:
node-version: "22"
node-version: ${{ inputs.node-version }}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Missing node-version input declaration causes Node.js setup to always fall through to default

The composite action references inputs.node-version in conditions at lines 35 and 41, but node-version is never declared in the inputs: section (lines 4-23).

Root Cause and Impact

The TypeScript workflow at .github/workflows/typescript-tests.yml:31 passes node-version: ${{ env.NODE_VERSION }} to the action. However, since node-version is not declared as an input in the action's inputs: block, inputs.node-version will always evaluate to an empty string.

This means:

  • Line 35: if: inputs.node-version != '' → always false, so the custom Node.js version step is never executed
  • Line 41: if: inputs.node-version == '' → always true, so the fallback Node.js 22 step always runs

In this specific case, the TypeScript workflow passes NODE_VERSION: "22" which happens to match the fallback, so the behavior is accidentally correct. But the intent to allow callers to specify a custom Node.js version is broken — any non-22 version passed as node-version would be silently ignored.

Prompt for agents
In .github/actions/setup/action.yml, add a `node-version` input declaration to the `inputs:` section (around line 20-23). It should be optional with no default (or default empty string), for example:

  node-version:
    description: "Node.js version (defaults to 22 if not specified)"
    required: false
    default: ""

This will allow the conditions at lines 35 and 41 that reference `inputs.node-version` to work correctly when callers pass a custom Node.js version.
Open in Devin Review

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

Comment on lines +28 to +34
- name: Setup environment
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}

- name: Cache Solana CLI tools
uses: actions/cache@v4
with:
path: |
~/.cache/solana/
~/.local/share/solana/
key: solana-cli-${{ runner.os }}-${{ env.SOLANA_CLI_VERSION }}

- name: Install Solana CLI tools
run: |
sh -c "$(curl -sSfL https://release.anza.xyz/v${{ env.SOLANA_CLI_VERSION }}/install)"
echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH

- name: Install Light CLI
run: npm install -g @lightprotocol/zk-compression-cli@alpha

- name: Install Rust (for Photon)
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: false

- name: Cache Photon indexer
id: cache-photon
uses: actions/cache@v4
with:
path: ~/.cargo/bin/photon
key: photon-${{ runner.os }}-1a785036de52896b68d06413e3b0231122d6aa4a

- name: Install Photon indexer
if: steps.cache-photon.outputs.cache-hit != 'true'
run: cargo install --git https://github.com/lightprotocol/photon.git --rev 1a785036de52896b68d06413e3b0231122d6aa4a --locked
env:
RUSTFLAGS: "-A dead-code"

- name: Generate keypair
run: solana-keygen new --no-bip39-passphrase
solana-cli-version: ${{ env.SOLANA_CLI_VERSION }}
rust-toolchain: ${{ env.RUST_TOOLCHAIN }}
photon-indexer: "true"
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Missing example input in TypeScript workflow causes required input validation failure

The TypeScript workflow at .github/workflows/typescript-tests.yml:28-34 invokes the setup action but does not pass the example input, which is declared as required: true in .github/actions/setup/action.yml:6-7.

Root Cause and Impact

The setup action declares:

inputs:
  example:
    description: "Example directory path"
    required: true

But the TypeScript workflow only passes:

with:
  node-version: ${{ env.NODE_VERSION }}
  solana-cli-version: ${{ env.SOLANA_CLI_VERSION }}
  rust-toolchain: ${{ env.RUST_TOOLCHAIN }}
  photon-indexer: "true"

The example input is missing. GitHub Actions will produce a warning for missing required inputs in composite actions. The cache-workspaces at line 32 uses ${{ inputs.example || '.' }} which falls back to ., so the Rust cache will target the repo root instead of a specific workspace directory. This may cause cache misses or incorrect caching for the TypeScript workflow.

Suggested change
- name: Setup environment
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache Solana CLI tools
uses: actions/cache@v4
with:
path: |
~/.cache/solana/
~/.local/share/solana/
key: solana-cli-${{ runner.os }}-${{ env.SOLANA_CLI_VERSION }}
- name: Install Solana CLI tools
run: |
sh -c "$(curl -sSfL https://release.anza.xyz/v${{ env.SOLANA_CLI_VERSION }}/install)"
echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
- name: Install Light CLI
run: npm install -g @lightprotocol/zk-compression-cli@alpha
- name: Install Rust (for Photon)
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: false
- name: Cache Photon indexer
id: cache-photon
uses: actions/cache@v4
with:
path: ~/.cargo/bin/photon
key: photon-${{ runner.os }}-1a785036de52896b68d06413e3b0231122d6aa4a
- name: Install Photon indexer
if: steps.cache-photon.outputs.cache-hit != 'true'
run: cargo install --git https://github.com/lightprotocol/photon.git --rev 1a785036de52896b68d06413e3b0231122d6aa4a --locked
env:
RUSTFLAGS: "-A dead-code"
- name: Generate keypair
run: solana-keygen new --no-bip39-passphrase
solana-cli-version: ${{ env.SOLANA_CLI_VERSION }}
rust-toolchain: ${{ env.RUST_TOOLCHAIN }}
photon-indexer: "true"
- name: Setup environment
uses: ./.github/actions/setup
with:
example: programs/anchor
node-version: ${{ env.NODE_VERSION }}
solana-cli-version: ${{ env.SOLANA_CLI_VERSION }}
rust-toolchain: ${{ env.RUST_TOOLCHAIN }}
photon-indexer: "true"
Open in Devin Review

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

Comment on lines +72 to +78
pub fn check_contributions(&self, _bumps: &CheckContributionsBumps) -> Result<()> {
let vault_balance = get_token_account_balance(&self.vault.to_account_info())
.map_err(|_| anchor_lang::prelude::ProgramError::InvalidAccountData)?;
require!(
vault_balance >= self.fundraiser.amount_to_raise,
FundraiserError::TargetNotMet
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Fundraiser check_contributions does not validate deadline, allowing premature claims

The check_contributions instruction in checker.rs allows the maker to claim funds as soon as the vault balance meets the target, without checking whether the fundraiser deadline has passed or is still active.

Root Cause and Impact

In checker.rs:72-109, the only validation is:

require!(
    vault_balance >= self.fundraiser.amount_to_raise,
    FundraiserError::TargetNotMet
);

There is no check on self.fundraiser.duration or Clock::get()?.unix_timestamp. Compare with contribute() in contribute.rs:84-89 which validates:

let elapsed_days = ((current_time - self.fundraiser.time_started) / SECONDS_TO_DAYS) as u16;
require!(
    elapsed_days < self.fundraiser.duration,
    FundraiserError::FundraiserEnded
);

And refund() in refund.rs:79-84 which validates the fundraiser has ended.

This means: if the target is met early (e.g., after 1 day of a 7-day fundraiser), the maker can immediately claim all funds. Contributors who planned to contribute later in the fundraiser period lose their opportunity, and contributors who already contributed cannot get refunds because refund() requires the fundraiser to have ended AND the target to not be met.

Whether this is intentional depends on the business logic. In many crowdfunding platforms, early claim is allowed once the target is met. However, the refund instruction's logic (vault_balance < amount_to_raise) means once the maker claims and drains the vault, any remaining contributor accounts with recorded amounts would have their contributor_account.amount but the vault would be empty, making the current_amount tracking inconsistent.

Note: The close = fee_payer on the fundraiser account means the fundraiser is closed after check_contributions, preventing double-claims. So this is more of a design consideration than a critical exploit.

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