agents/skills/soroban/token-flow-tracing/SKILL.md
Trigger Pattern SEP-41 token transfers, TokenClient::new, transfer/transfer_from/burn, XLM native balance - Inject Into Lifecycle, External-Env agents
npx skillsauth add plamentsv/plamen token-flow-tracingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Trigger Pattern: SEP-41
transfer/transfer_from/approve/burn,TokenClient::new, XLM native balance, SAC interactions Inject Into: Lifecycle, External-Env agents Finding prefix:[TF-N]Rules referenced: R4, R5, R10, R11
For every token the protocol handles:
Where can tokens enter?
deposit() / stake() functions — explicit entry via token_client.transfer_from(user, contract, amount) or token_client.transfer(user, contract_address, amount)token.transfer(sender, contract_address, amount) directly; the contract has no hook to reject ite.current_contract_address().balance() or a token client wrapping the XLM SACinvoke_contract (e.g., swap output, unstake return)transfer_from into the contractSoroban-specific note: Unlike Solana, there are no token account PDAs. The contract address IS the recipient. Unsolicited transfers arrive directly to e.current_contract_address() and are undetectable without an explicit balance snapshot before and after calls.
For each entry point:
e.storage().instance().get(&DataKey::TotalDeposited))token_client.balance(contract_address) read directly for calculations? → Donation attack vectortoken_client.balance() compared anywhere?Red flags:
token_client.balance(e.current_contract_address()) directlyinvoke_contract)Where can tokens leave?
withdraw() / unstake() functionsinvoke_contract calls that transfer tokens as part of the callFor each exit:
token_client.transfer() executes?For each transfer instruction: can the source and destination be the same address? If YES: does a self-transfer update accounting state (fees credited, rewards claimed, share ratios updated) without net token movement? Flag as FINDING. This targets accounting manipulation, distinct from input validation.
For protocols handling multiple token types:
e.current_contract_address().balance() or XLM SAC address) vs SEP-41 tokensCheck: If function A handles TokenX and function B handles TokenY, can TokenX's address be passed to function B?
Can tokens be sent to the protocol's address without calling deposit()?
If YES (always YES in Soroban — any SEP-41 holder can call token.transfer(self, contract_addr, amount)):
token_client.balance(contract_addr))If the protocol claims NO:
For EVERY token type the protocol holds, queries, or receives:
| Token Type | Can Transfer To Protocol? | Changes Accounting? | Blocks Operations? | Triggers Side Effects? | |------------|--------------------------|--------------------|--------------------|----------------------| | XLM (native) | YES (always) | YES/NO | YES/NO | YES/NO | | {sep41_token_a} | YES (always) | YES/NO | YES/NO | YES/NO | | SAC-{asset} | YES (always) | YES/NO | YES/NO | YES/NO |
RULE: If ANY transferable token affects state → analyze: accounting divergence, rent impact, operation blocking, side effect chains.
For each token identified:
| Token | Entry Points | Exit Points | Tracking Var | balance() Used Directly? | Unsolicited Possible? | |-------|--------------|-------------|--------------|--------------------------|----------------------| | [Name/Address] | deposit, cross-contract return | withdraw, claim | total_deposited | YES/NO | YES (always) |
For protocols with multiple tokens:
For every invoke_contract call that returns tokens or modifies state:
Common mismatches:
invoke_contract succeeded but returned unexpected amountCheck: Every TokenClient::new(&e, &token_address) — is token_address validated against the configured/expected token, or accepted from user-supplied input?
invoke_contract completes?Soroban reentrancy note: Soroban DOES allow reentrant invoke_contract calls unless the contract explicitly guards against them. If a cross-contract call can call back into this contract before the current function completes, check for reentrancy vectors.
Soroban SEP-41 allowances are stored in Temporary ledger storage with a TTL (expressed as a ledger number deadline, not an amount-only approval like EVM). This creates unique staleness vectors:
expiration_ledger? Is it far enough in the future?| Scenario | Description | Impact |
|----------|-------------|--------|
| Expired allowance | Contract holds an approved allowance; TTL expires before it is consumed; subsequent transfer_from fails | DoS: operation reverts, user funds locked pending reapproval |
| Short TTL front-run | User approves with short TTL; attacker delays their own transaction until allowance expires; then calls function that relies on the allowance | Griefing: operation fails after attacker delays it |
| Allowance amount != i128 | Approved amount stored as i64 in older code; overflow at amounts > 2^63 | Accounting mismatch: partial approval silently truncated |
| Call / Event | Side Effect | Token Type Produced | Protocol Handles This Type? | Mismatch? | |-------------|-------------|--------------------|-----------------------------|-----------| | {cross_contract_call} | {side_effect} | {token_type_or_UNKNOWN} | YES/NO | YES/NO |
RULES: Side effect type != expected → FINDING. Type UNKNOWN → CONTESTED (Rule 4). Check BOTH cross-contract calls AND unsolicited transfers.
// RED FLAG: Direct balance usage — donatable
let rate = token_client.balance(&e.current_contract_address()) / vault.total_shares;
// BETTER: Tracked balance — but verify total_deposited is updated on ALL entry paths
let rate = vault.total_deposited / vault.total_shares;
// RED FLAG: Token address from user input — not validated
let token_client = TokenClient::new(&e, &token_address); // token_address from fn args
token_client.transfer_from(&e.current_contract_address(), &from, &to, &amount);
// BETTER: Validated against configured token
let configured_token: Address = e.storage().instance().get(&DataKey::Token).unwrap();
require!(token_address == configured_token, Error::InvalidToken);
**ID**: [TF-N]
**Severity**: [based on fund impact]
**Step Execution**: S1,2,3,4,5,6,7,8,9 | X(reasons) | ?(uncertain)
**Location**: src/{file}.rs:LineN
**Title**: [Token type] can enter/exit via [path] without [expected accounting update]
**Description**: [Trace the token flow and where it diverges from expected]
**Impact**: [What breaks: exchange rates, user balances, protocol insolvency]
CRITICAL: Report completion status for ALL sections. Findings with incomplete sections will be flagged for depth review.
| Section | Required | Completed? | Notes | |---------|----------|------------|-------| | 1. Token Entry Points | YES | Y/X/? | | | 2. Token State Tracking | YES | Y/X/? | | | 3. Token Exit Points | YES | Y/X/? | | | 4. Token Type Separation | IF multi-token | Y/X(N/A)/? | | | 5. Unsolicited Transfer Analysis | YES | Y/X/? | | | 5b. Unsolicited Transfer Matrix (All Types) | YES | Y/X/? | MANDATORY — never skip | | 6. Token Flow Checklist | YES | Y/X/? | | | 7. Cross-Token Interactions | IF multi-token | Y/X(N/A)/? | | | 8. Cross-Contract Call Return Verification | YES | Y/X/? | MANDATORY — never skip | | 9. Allowance Expiry Analysis | YES | Y/X/? | MANDATORY — Soroban-specific, never skip | | 9d. Side Effect Token Type | YES | Y/X/? | MANDATORY — never skip |
Sections 8 and 9 MUST produce tabular output even if uncertain. If UNVERIFIED: verdict cannot be REFUTED, use CONTESTED. If side effects UNKNOWN: apply adversarial default and document assumptions.
development
Prepare Solidity projects for a security audit — test coverage, test quality, NatSpec docs, code hygiene, dependency health, best-practice enforcement, deployment readiness, and project documentation checks. Generates a scored Audit Readiness Report and optionally runs static analysis. Trigger on: "prepare for audit", "audit readiness", "pre-audit check", "audit prep", "NatSpec check", or any request to review a Solidity codebase before a security review.
development
Launch the Plamen deterministic Web3 security audit pipeline
development
Run the Plamen smart-contract audit wizard in Codex
testing
Launch the Plamen deterministic L1 infrastructure audit pipeline