agents/skills/sui/token-flow-tracing/SKILL.md
Trigger Pattern BALANCE_DEPENDENT flag (required) - Inject Into Depth-token-flow, breadth 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: BALANCE_DEPENDENT flag (required) Inject Into: Depth-token-flow, breadth agents Purpose: Trace all Coin<T> and Balance<T> flows through Sui Move protocols to identify accounting desync, unsolicited deposit vectors, type confusion, and token lifecycle issues
For every token the protocol handles:
Enumerate ALL Coin<T> and Balance<T> types the protocol handles:
| Token Type | Representation | Location (module::struct field) | Owned or Shared? | Entry Functions | Exit Functions | |------------|---------------|-------------------------------|-------------------|-----------------|----------------| | {e.g., SUI} | Balance<SUI> | pool::Pool.balance | Shared object | deposit() | withdraw() | | {e.g., USDC} | Coin<USDC> | (function parameter) | Owned (user) | swap_in() | swap_out() |
Sui-specific representations:
Coin<T>: Owned object with id: UID and balance: Balance<T>. Has key + store abilities -- freely transferable to any address via transfer::public_transfer. This is the primary unsolicited transfer vector on Sui.Balance<T>: Value type with store ability only (no key). Stored inside other objects. Cannot exist as a standalone on-chain object. Must live inside a struct with key.TreasuryCap<T>: Capability to mint/burn. Must be tracked.Balance<T>: Hidden balance storage -- check dynamic_field::add/borrow/remove.Where can tokens enter the protocol?
| Entry Path | Function | Token Form | Accounting Update | Validated? | |------------|----------|-----------|-------------------|------------| | Standard deposit | {module::deposit} | Coin<T> parameter | {state variable updated} | YES/NO | | PTB coin splitting | (PTB splits user coin) | Coin<T> from split | {same as above?} | N/A | | Direct transfer | transfer::public_transfer | Coin<T> to address | NONE -- creates new owned object | NO | | Balance merge | balance::join | Balance<T> | {state variable updated?} | YES/NO | | Mint | coin::from_balance / coin::mint | TreasuryCap | {supply tracking} | YES/NO | | Side-effect receipt | {external call returns coin} | Coin<T> | {handled?} | YES/NO |
Sui-specific entry analysis:
coin::into_balance(coin) converts Coin<T> to Balance<T> -- does the protocol track this conversion?coin::split(&mut coin, amount, ctx) creates a new Coin<T> -- does the protocol validate the split amount?coin::zero<T>(ctx))Where can tokens leave the protocol?
| Exit Path | Function | Token Form | Accounting Update | Authorized? | |-----------|----------|-----------|-------------------|-------------| | Standard withdraw | {module::withdraw} | Coin<T> returned | {state variable decremented} | {access check} | | Transfer out | transfer::public_transfer | Coin<T> | {accounting updated?} | {access check} | | Balance extraction | balance::split | Balance<T> | {state variable decremented?} | {access check} | | Burn | coin::burn / balance::decrease_supply | TreasuryCap | {supply tracking} | {cap holder} | | Fee distribution | {fee function} | Coin<T> or Balance<T> | {fee accounting} | {access check} | | Emergency withdraw | {emergency function} | Coin<T> | {does it clear ALL state?} | {admin cap?} |
For each exit: does the tracked balance decrease BEFORE or AFTER the actual balance extraction? For each transfer/withdrawal: can the source be underfunded at execution time? (funds deployed externally, locked, or lent out → transfer aborts) Check for:
balance::split before state update -> can the function abort between split and update?balance::split -> can state be inconsistent if split aborts?For each transfer function: can the sender and recipient be the same address/object? 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 each token in the protocol:
| Token | Internal Tracking Variable | Actual Balance Source | Can They Desync? | Desync Vector | |-------|---------------------------|---------------------|-----------------|---------------| | {token} | {e.g., pool.total_deposited} | balance::value(&pool.balance) | YES/NO | {how} |
Red flags:
balance::value() directly instead of tracked internal variablebalance::join(&mut pool.balance, deposit_balance) without incrementing tracked counterSui-specific desync vectors:
balance::join into a shared object's Balance<T> without updating the tracking variableCan tokens be added to the protocol's balance without going through deposit logic?
Sui object model considerations:
Coin<T> via transfer::public_transfer to a shared object's address creates a NEW owned object at that address -- it does NOT add to the shared object's Balance<T>. The protocol would need to explicitly receive and merge it.Balance<T> fields without going through the module's public API.Coin<T> and calls balance::join without proper accounting, this IS a donation vector.| Donation Path | Possible? | Changes Protocol Balance? | Breaks Accounting? | Impact | |---------------|-----------|--------------------------|-------------------|--------| | transfer::public_transfer to pool address | YES (creates owned obj) | NO (not auto-merged) | NO (unless protocol sweeps) | {analysis} | | Public function accepting Coin<T> without accounting | {YES/NO} | YES | YES | {analysis} | | Dynamic field injection | {YES/NO -- needs module API} | {YES/NO} | {YES/NO} | {analysis} | | Reward/fee distribution to protocol address | {YES/NO} | {YES/NO} | {YES/NO} | {analysis} |
For EVERY external token type the protocol holds, queries, or receives as side effects -- not just the protocol's primary token:
| Token Type | Can Be Sent 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 can enter the protocol's balance AND affects state -> analyze each consequence:
Can the wrong Coin<T> type be passed to protocol functions?
| Function | Expected Type Parameter | Validated? | What if Wrong Type? | |----------|------------------------|-----------|---------------------| | {function} | Coin<USDC> | {by Move type system / runtime check} | {impact} |
Sui Move type safety: Move's type system provides strong static guarantees -- Coin<USDC> and Coin<SUI> are different types at compile time. However:
fun deposit<T>(coin: Coin<T>) accept ANY Coin<T> -- is T validated?T against an allowed list? (e.g., assert!(type_name::get<T>() == allowed_type))Coin<ATTACKER_TOKEN> to a generic function?Pool<A, B> called with Coin<B> where Coin<A> expected)Analyze coin::split() and coin::join() / balance::split() and balance::join() operations:
| Operation | Location | Amount Source | Validated? | Edge Cases | |-----------|----------|-------------|-----------|------------| | coin::split(&mut coin, amount, ctx) | {location} | {user input / computed} | {amount <= coin.value?} | {amount = 0? amount = full value?} | | coin::join(&mut coin_a, coin_b) | {location} | {coin_b.value} | {overflow check?} | {coin_b is zero?} | | balance::split(&mut bal, amount) | {location} | {computed} | {amount <= bal.value?} | {amount = 0?} | | balance::join(&mut bal_a, bal_b) | {location} | {bal_b.value} | {overflow check?} | {bal_b is zero?} |
Check:
coin::split(&mut coin, 0, ctx) creates a zero-value coin -- does the protocol handle this?What happens with zero-value tokens?
| Operation | Zero Input Behavior | Impact | |-----------|-------------------|--------| | deposit(coin::zero<T>()) | {aborts / succeeds / mints zero shares} | {accounting impact} | | withdraw(0) | {aborts / succeeds / burns zero shares} | {accounting impact} | | swap(coin::zero<T>()) | {aborts / succeeds} | {state change without value?} | | claim_rewards() when rewards = 0 | {aborts / succeeds} | {side effects?} |
Check: Can zero-value operations be used to:
For protocols with multiple token types:
| Interaction | Token A | Token B | Dependency | Impact | |-------------|---------|---------|-----------|--------| | Exchange rate | {A type} | {B type} | {A balance affects B's rate?} | {if A manipulated, B price changes?} | | Collateral/debt | {collateral type} | {debt type} | {collateral value gates borrowing} | {if collateral inflated, excess borrowing} | | LP composition | {A type} | {B type} | {ratio determines share value} | {imbalance vector} |
**ID**: [TF-N]
**Severity**: [based on fund impact]
**Step Execution**: check1,2,3,4,5,6,7,8,9 | x(reasons) | ?(uncertain)
**Rules Applied**: [R1:check, R11:check, R4:check, R10:check]
**Location**: module::function: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]
{CONTRACTS} -- Move modules to analyze
{TOKEN_TYPES} -- Coin<T>/Balance<T> types handled
{SHARED_OBJECTS} -- Shared objects holding balances
{ENTRY_FUNCTIONS} -- Token deposit/entry functions
{EXIT_FUNCTIONS} -- Token withdraw/exit functions
{GENERIC_FUNCTIONS} -- Functions with type parameter <T>
| Field | Required | Description | |-------|----------|-------------| | asset_inventory | yes | All Coin<T> and Balance<T> types | | entry_points | yes | All token entry paths | | exit_points | yes | All token exit paths | | balance_tracking | yes | Internal vs actual balance analysis | | unsolicited_analysis | yes | Donation/unsolicited deposit vectors | | type_confusion | yes | Type parameter validation | | finding | yes | CONFIRMED / REFUTED / CONTESTED | | evidence | yes | Code locations with line numbers | | step_execution | yes | Status for each step |
| Section | Required | Completed? | Notes | |---------|----------|------------|-------| | 1. Asset Inventory | YES | check/x/? | | | 2. Token Entry Points | YES | check/x/? | | | 3. Token Exit Points | YES | check/x/? | | | 4. Balance Tracking and Desync | YES | check/x/? | | | 5. Unsolicited Deposit Analysis | YES | check/x/? | | | 5b. Unsolicited Transfer Matrix (All Types) | YES | check/x/? | MANDATORY -- never skip | | 6. Token Type Confusion | YES | check/x/? | | | 7. Coin Splitting and Merging | YES | check/x/? | | | 8. Zero-Value Operations | YES | check/x/? | | | 9. Cross-Token Interactions | IF multi-token | check/x(N/A)/? | |
After Section 5 (Unsolicited Deposit Analysis):
After Section 7 (Coin Splitting and Merging):
After Section 8 (Zero-Value Operations):
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