agents/skills/evm/token-flow-tracing/SKILL.md
Performs comprehensive token flow analysis by tracing all token entry and exit paths, verifying accounting consistency, detecting unsolicited transfer vectors, and identifying risks such as donation attacks, balance desynchronization, token type confusion, and side-effect-driven state changes.
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:
transfer\|transferFrom\|safeTransfer\|mint\|burn\|balanceOf.*thisInject Into: Lifecycle, External-Env agents
For every token the protocol handles:
Where can tokens enter?
deposit() / stake() functions - standard entry pointstransfer() to contract address (bypasses deposit logic)onERC721Received, onERC1155Received, onERC1155BatchReceivedreceive() / fallback() for native ETHFor each callback handler the protocol IMPLEMENTS (not calls): verify access control (can anyone trigger it?), verify what state it modifies and whether that state is iterated elsewhere.
For each entry point:
balanceOf(address(this)) used directly? → Donation attack vectorRed flags:
balanceOf(address(this)) directlyWhere can tokens leave?
withdraw() / unstake() functionsFor each exit: does the tracked balance decrease BEFORE or AFTER the actual transfer? For each transfer call: can the source address be underfunded at execution time? (funds deployed externally, locked, or lent out → transfer reverts)
For each transfer function: can the sender and recipient be the same address? If YES: does a self-transfer update accounting state (fees credited, rewards claimed, snapshots updated, share ratios changed) without net token movement? Flag as FINDING.
For protocols handling multiple token types:
Check: If function A handles TokenX and function B handles TokenY, can TokenX reach function B's logic? Also: within a single function, if some code paths branch on token type (e.g., input handling), do ALL code paths branch consistently (e.g., refund, fee, return)?
A single token variable that may be either an ERC-20 address OR a native/sentinel
pseudo-address (address(0), 0xEeee...EEeE, a wrapped-native address) is a
recurring source of reverts and silent no-ops. For EACH approve(),
transferFrom(), safeTransfer(), .call{value:} on a token that CAN be native/
sentinel, trace BOTH branches:
IERC20(token).approve(...),
transferFrom) → reverts (no ERC-20 code at that address) → breaks the native
swap/transfer path entirely. Is the native case branched BEFORE the typed call?.call/safeTransfer to the sentinel → may silently succeed/no-op
(no code to revert), so value is lost without an error. Is success actually verified?approve(fromToken, ...) before a swap, where fromToken may be
native/sentinel → the unconditional approve reverts the whole operation.Check: list every token op on a possibly-native/sentinel token; for each, state whether the native branch is handled separately. A typed ERC-20 op on a native/sentinel address that is NOT branched is a confirmed finding (path broken / value lost), independent of severity.
Cross-ref (cross-chain bridge/gateway callbacks): this §4a covers the native/sentinel-vs-ERC20 token op. A separate asset-FORM mismatch (the bridge delivers/expects an asset that may be in a different form (native vs wrapped) — e.g. ETH/WETH — verify the conversion is present) is checked in INTEGRATION_HAZARD_RESEARCH §0c-bis. Apply both for any cross-chain bridge/gateway callback handler: a token op can be type-correct yet still revert/trap funds because the delivered form was never reconciled.
SIBLING ASYMMETRY (promote, do not bury): if one function guards the
native/sentinel (or any edge) case — e.g. if (token != _ETH_ADDRESS_) approve(...)
— and a paired/sibling function performing the same operation does NOT, that
asymmetry is itself a confirmed finding (the unguarded sibling reverts / mis-handles
the edge). You MUST write it as its own ## Finding using the Finding Template below
— recording it only as a ✓ or a note in the Step Execution Checklist is a recall
loss: the checklist row attests you ran the check, but the report is built from
## Finding sections, so a bug that lives only in a checklist cell is silently
dropped at inventory promotion. One sibling guards and the other does not ⇒ emit a
finding, every time.
Can tokens be sent to the contract without calling deposit()?
If YES:
If NO:
For EVERY external token type the protocol holds, queries, or receives as side effects - not just the protocol's primary token:
| Token Type | Can Transfer To Protocol? | Changes Protocol Accounting? | Blocks Operations? | Triggers Side Effects? | |------------|--------------------------|-----------------------------|--------------------|----------------------| | {token_a} | YES/NO | YES/NO | YES/NO | YES/NO |
RULE: If ANY token type is transferable to the protocol AND affects state → analyze each consequence:
For each token identified:
| Token | Entry Points | Exit Points | Tracking Var | balanceOf(this) Used? | Unsolicited Possible? | |-------|--------------|-------------|--------------|----------------------|----------------------| | [Name] | deposit, receive | withdraw, claim | totalDeposited | YES/NO | YES/NO |
For protocols with multiple tokens:
For every external call that returns tokens or values:
Common mismatches:
Check: interface.function() returns (TokenA) - verify TokenA is what's actually returned, not TokenB
For every transfer() / transferFrom() call to external contracts:
| Token | On Transfer Side Effect | Impact on Protocol | |-------|------------------------|-------------------| | [Token] | Claims pending rewards | Unexpected balance increase | | [Token] | Updates delegation state | Accounting mismatch | | [Token] | Triggers rebase | Exchange rate affected |
Example: Transferring staking receipt tokens (e.g., stETH, aTokens) may trigger rebases or reward claims as a side effect
For each documented side effect that produces or claims tokens:
| External Call / Event | Side Effect | Token Type Produced | Protocol Handles This Type? | Mismatch? | |-----------------------|-------------|--------------------|-----------------------------|-----------| | {call_or_event} | {side_effect} | {token_type_or_UNKNOWN} | YES/NO | YES/NO |
RULE: If side effect token type != protocol's expected token type → FINDING (stranded tokens of wrong type) RULE: If side effect token type is UNKNOWN → CONTESTED (assume adversarial per Rule 4) RULE: Check BOTH direct calls AND unsolicited transfers for side effect token types
// RED FLAG: Direct balance usage
uint256 rate = token.balanceOf(address(this)) / totalShares;
// BETTER: Tracked balance
uint256 rate = totalPooledTokens / totalShares;
// But check: is totalPooledTokens updated correctly on ALL entry paths?
When this skill identifies an issue:
**ID**: [LC-N] or [EX-N]
**Severity**: [based on fund impact]
**Step Execution**: ✓1,2,3,4,5,6,7,8,9 | ✗(reasons) | ?(uncertain)
**Location**: Contract.sol: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: You MUST 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 | ✓/✗/? | |
| 2. Token State Tracking | YES | ✓/✗/? | |
| 3. Token Exit Points | YES | ✓/✗/? | |
| 4. Token Type Separation | IF multi-token | ✓/✗(N/A)/? | |
| 4a. Native/ETH-sentinel vs ERC-20 path | IF any native/sentinel token | ✓/✗(N/A)/? | typed ERC-20 op on native/sentinel reverts; low-level no-ops; sibling-asymmetry (one guards, sibling does not) ⇒ MUST also be a ## Finding, not only this row |
| 5. Unsolicited Transfer Analysis | YES | ✓/✗/? | |
| 5b. Unsolicited Transfer Matrix (All Types) | YES | ✓/✗/? | MANDATORY - never skip |
| 6. Token Flow Checklist | YES | ✓/✗/? | |
| 7. Cross-Token Interactions | IF multi-token | ✓/✗(N/A)/? | |
| 8. External Call Return Type | YES | ✓/✗/? | MANDATORY - never skip |
| 9. Transfer Side Effects | YES | ✓/✗/? | MANDATORY - never skip |
| 9d. Side Effect Token Type | YES | ✓/✗/? | MANDATORY - never skip |
After Section 5 (Unsolicited Transfer Analysis):
After Section 8 (External Call Return Type):
STAKING_RECEIPT_TOKENS.md Section 8 for on-transfer side effectsAfter Section 9 (Transfer Side Effects):
For Sections 8 and 9, you MUST produce output even if uncertain:
Section 8 Output (always required):
### 8. External Call Return Type Verification
| External Call | Expected Return | Verified Production Return | Match? |
|--------------|-----------------|---------------------------|--------|
| [call] | [expected] | [verified/UNVERIFIED] | ✓/✗/? |
**If UNVERIFIED**: Finding verdict cannot be REFUTED. Use CONTESTED.
Section 9 Output (always required):
### 9. Transfer Side Effects Analysis
| Token | On Transfer Side Effect | Verified? | Assumed Impact |
|-------|------------------------|-----------|----------------|
| [token] | [effect or UNKNOWN] | YES/NO | [impact trace] |
**Adversarial Default Applied**: [list assumptions made]
data-ai
Trigger Pattern Always (run during recon TASK 0, not breadth) - Inject Into Recon agent only (meta_buffer.md enrichment)
data-ai
Trigger Pattern Always (run during recon TASK 0, not breadth) - Inject Into Recon agent only (meta_buffer.md enrichment)
data-ai
Trigger Pattern Always (run during recon TASK 0, not breadth) - Inject Into Recon agent only (meta_buffer.md enrichment)
data-ai
Trigger STABLESWAP_FORK flag (fork-ancestry detects Curve/StableSwap parent via get_d/get_y/ramp_a/StableSwap patterns) - Agent Type general-purpose (standalone niche agent, 1 budget slot)