# Token Flow Tracing

> Trigger Pattern SEP-41 token transfers, TokenClient::new, transfer/transfer_from/burn, XLM native balance - Inject Into Lifecycle, External-Env agents

- Skill: `plamentsv/token-flow-tracing-5` (Agent Skill)
- Install (CLI): `npx skillmds@latest add plamentsv/token-flow-tracing-5`
- Raw SKILL.md: https://api.skillmd.com/api/skills/plamentsv/token-flow-tracing-5/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: plamentsv (https://skillmd.com/u/plamentsv)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/plamentsv/token-flow-tracing-5

---


# TOKEN_FLOW_TRACING Skill (Soroban)

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

## 1. Token Entry Points

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)`
- Unsolicited SEP-41 transfers — anyone can call `token.transfer(sender, contract_address, amount)` directly; the contract has no hook to reject it
- XLM native balance — anyone can send XLM to the contract's Stellar account; the contract reads this via `e.current_contract_address().balance()` or a token client wrapping the XLM SAC
- Return tokens from cross-contract calls — tokens returned as output of `invoke_contract` (e.g., swap output, unstake return)
- Allowance-based pulls — contract holds an approved allowance; tokens pulled via `transfer_from` into the contract

**Soroban-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.

## 2. Token State Tracking

For each entry point:
- What state variable tracks the balance? (e.g., `e.storage().instance().get(&DataKey::TotalDeposited)`)
- Is `token_client.balance(contract_address)` read directly for calculations? → **Donation attack vector**
- Are tracked balances vs actual `token_client.balance()` compared anywhere?
- Can tracked balance get out of sync with actual on-chain balance?

**Red flags**:
- Exchange rate calculations using `token_client.balance(e.current_contract_address())` directly
- No reconciliation mechanism to handle unsolicited-transfer discrepancies
- Internal accounting updated BEFORE the cross-contract token transfer executes
- Balance read at function start, transfer happens mid-function, balance read again — reentrancy window (Soroban is reentrant via `invoke_contract`)

## 3. Token Exit Points

Where can tokens leave?

- `withdraw()` / `unstake()` functions
- Fee distributions to treasury address
- Reward claim functions
- Emergency withdrawal / rescue functions
- Cross-contract `invoke_contract` calls that transfer tokens as part of the call
- Liquidation transfers

For each exit:
- Does the tracked balance decrease BEFORE or AFTER `token_client.transfer()` executes?
- Can the contract be underfunded at execution time? (funds lent to external contracts, reserved for pending withdrawals)
- Does the function re-read the live balance after transferring, creating a post-transfer snapshot inconsistency?

### 3b. Self-Transfer Accounting

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.

## 4. Token Type Separation (Multi-Token Protocols)

For protocols handling multiple token types:
- Are different SEP-41 tokens handled by different code paths?
- Can one token's function be triggered with another token's contract address?
- Is the token address validated against a stored allowlist or configured token?
- Does the protocol distinguish between:
  - XLM native (via `e.current_contract_address().balance()` or XLM SAC address) vs SEP-41 tokens
  - SAC-wrapped assets (Stellar classic assets bridged to Soroban) vs pure Soroban tokens
  - Base token vs LP/receipt token (underlying vs yield-bearing)
  - Tokens with different decimal precisions (XLM is 7 decimals; many SEP-41 tokens use 7 or 18)

**Check**: If function A handles TokenX and function B handles TokenY, can TokenX's address be passed to function B?

## 5. Unsolicited Transfer Analysis

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)`):
- Does this break accounting? (tracked balance != `token_client.balance(contract_addr)`)
- Does this inflate exchange rates? (more assets per share)
- Does this enable first-depositor attack amplification?
- Are there reconciliation functions to sync tracked state?
- Can an attacker front-run deposits with unsolicited transfers?

If the protocol claims NO:
- Why not? (Is there a TransferHook equivalent? There is none in standard SEP-41.)
- Is the protection reliable? Can it be bypassed?

## 5b. Unsolicited Transfer Matrix (All Token Types)

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.

## 6. Token Flow Checklist

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

## 7. Cross-Token Interactions

For protocols with multiple tokens:
- Can operations on TokenA affect TokenB's accounting?
- Are there exchange rate dependencies between tokens (XLM vs SEP-41, base vs LP)?
- Can withdrawing TokenA affect availability of TokenB?
- Can XLM balance affect SEP-41 token operations (base reserve requirements)?

## 8. Cross-Contract Call Return Verification

For every `invoke_contract` call that returns tokens or modifies state:

### 8a. Contract Address Verification
- What contract does the call target? Is the contract address validated against a stored trusted address?
- What return value / state change is expected? Is it verified post-call?

**Common mismatches**:
- Wrong token address: attacker passes a fake SEP-41 contract that mints freely
- Decimal mismatch: token with 7 decimals vs token with 18 decimals — amounts differ by 10^11
- Return value ignored: `invoke_contract` succeeded but returned unexpected amount
- Reentrancy: callee calls back into this contract before this contract's state is updated

**Check**: Every `TokenClient::new(&e, &token_address)` — is `token_address` validated against the configured/expected token, or accepted from user-supplied input?

### 8b. Return Value / Post-Call State Validation
- Does the protocol validate contract state after `invoke_contract` completes?
- Can zero/max/unexpected return values cause issues?
- Is there a mismatch between expected and actual post-call state?

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

## 9. Allowance Expiry Analysis (Soroban-Specific)

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:

### 9a. Allowance Storage Type
- Is the allowance stored in Temporary storage? (expires automatically if TTL is not extended)
- What is the approved `expiration_ledger`? Is it far enough in the future?
- Who sets the expiration? Can it be set to 0 (immediate expiry)?

### 9b. Allowance Expiry Attack Scenarios

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

### 9c. Token Side Effects (SAC-specific)
- Is this token a SAC (Stellar Asset Contract)? If YES, the Stellar issuer may have freeze/clawback rights.
- Can a SAC clawback from the contract mid-operation? (balance disappears; tracked state diverges)
- Does the protocol handle SAC freeze/clawback gracefully, or does it panic?

### 9d. Side Effect Token Type Analysis

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

## Example Application

```rust
// 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);
```

## Finding Template

```markdown
**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]
```

---

## Step Execution Checklist (MANDATORY)

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

### Cross-Reference Markers

- **After Section 5**: IF LP/receipt tokens identified → MUST complete Sections 8-9. IF cross-contract calls return tokens → verify return state in Section 8.
- **After Section 8**: IF token address is user-supplied → mark CONTESTED until validated. IF reentrancy path exists → escalate to depth-state-trace.
- **After Section 9**: IF allowance TTL is shorter than expected operation window → FINDING (at minimum Medium). IF SAC with clawback → document clawback handling or flag missing guard.

### Mandatory Forced Output

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.

