feat: light token amm program examples #12
feat: light token amm program examples #12tilo-14 wants to merge 11 commits intojorrit/feat-escrow-anchor-examplefrom
Conversation
46f7553 to
9753fa9
Compare
- 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
9753fa9 to
b4f661f
Compare
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.
There was a problem hiding this comment.
🔴 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 --forceThe 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| - name: Setup Node.js | ||
| if: inputs.node-version != '' | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "22" | ||
| node-version: ${{ inputs.node-version }} |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| - 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" |
There was a problem hiding this comment.
🟡 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: trueBut 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.
| - 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" | |
Was this helpful? React with 👍 or 👎 to provide feedback.
| 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 | ||
| ); |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Programs included
Test plan
anchor buildin programs/anchor