agents/skills/sui/ptb-composability/SKILL.md
Trigger Pattern PTB flag (always for Sui -- Programmable Transaction Blocks are the Sui transaction model) - Inject Into Breadth agents, depth-external, depth-state-trace
npx skillsauth add plamentsv/plamen ptb-composabilityInstall 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: PTB flag (always for Sui -- Programmable Transaction Blocks are the Sui transaction model) Inject Into: Breadth agents, depth-external, depth-state-trace Finding prefix:
[PTB-N]Rules referenced: R4, R5, R8, R10, R15
Programmable Transaction Blocks (PTBs) are Sui's transaction composition primitive. A single PTB can contain up to 1024 commands, each calling a different function, with return values routed between commands. This enables atomic multi-step sequences that are fundamentally different from EVM's single-entry-point model. Every public function is a potential PTB command -- there is no "internal only" visibility equivalent to Solidity's internal.
STEP PRIORITY: Steps 1 (Single-Call Assumption Audit) and 4 (Atomic Read-Modify-Write) are where HIGH/CRITICAL severity findings most commonly hide. Do NOT rush these steps. If constrained, skip conditional sections (6, 7) before skipping 1, 3, or 4.
For EVERY public and entry function in the protocol, classify composability:
| # | Function | Module | Visibility | Returns Value? | Takes Shared Obj? | Composable in PTB? | Notes | |---|----------|--------|-----------|---------------|-------------------|-------------------|-------| | 1 | {func} | {mod} | public / entry / public(package) | YES / NO | YES / NO | YES / NO | {context} |
Sui visibility semantics:
entry functions: callable from PTB but return values CANNOT be used by subsequent commands (consumed or discarded within the command). Objects can only be transferred, not returned.public functions: fully composable -- return values pass between commands. This is the primary composability surface.public(package): callable only by other modules in the same package. NOT callable from PTB. Safe from external composition.fun (private): Not callable from outside the module. Safe.Key observation: Any function that is public (not entry, not public(package)) is fully composable. Its return values can be routed to ANY other function in the same PTB.
For EVERY public and entry function, check whether its security model implicitly assumes it is the ONLY function called in the transaction:
| # | Function | Assumes Single-Call? | What Assumption? | Breakable via PTB? | Impact | |---|----------|---------------------|-----------------|-------------------|--------| | 1 | {func} | YES/NO | {describe assumption} | YES/NO | {impact} |
Common single-call assumptions that PTBs break:
Key question for each function: "If an attacker calls this function as command N in a PTB, and can execute arbitrary commands 1..N-1 before it and N+1..1024 after it, what can go wrong?"
For each public function that returns objects:
| # | Function | Input Objects | Output Objects | Can Output Be Routed To? | Can Be Called Multiple Times? | |---|----------|-------------|---------------|-------------------------|----------------------------| | 1 | {func} | {owned/shared/immutable} | {Coin<T> / Object / HotPotato} | {any function accepting this type} | YES/NO |
Return value routing checks:
Coin<T>, can it be routed to function C (external) instead of intended function B?SplitCoins command. Returned Coin<T> can be split. Does protocol assume full amount passed?MergeCoins command. Can attacker merge extra coins to inflate amounts?&mut Object references are borrowed for a command and released after. Same object can be passed to multiple commands sequentially. Does command N assume the object wasn't modified by command N-1?TransferObjects command determines the recipient.Model this specific attack for each function returning value:
1. Attacker calls protocol_function_A() -> returns Coin<USDC> (intended for pool)
2. Attacker routes Coin<USDC> to their own address via TransferObjects
3. Protocol expects the Coin was consumed by the next step but it was intercepted
Check: Does the protocol rely on PTB command ordering to ensure returned values reach the right destination? If YES -> the user controls the ordering, not the protocol.
Without explicit flash loan protocols, PTBs enable flash-loan-like patterns:
1. [Command 1] Split large Coin<SUI> from attacker's balance
2. [Command 2] Deposit into protocol (inflates TVL/balance)
3. [Command 3] Trigger reward distribution (calculated on inflated balance)
4. [Command 4] Withdraw from protocol
5. [Command 5] Join coins back (net zero capital, captured rewards)
Can protocol state be manipulated between PTB commands?
| Function Pair | Shared State | Deposit-Action-Withdraw Possible? | Defense? | |--------------|-------------|----------------------------------|---------| | {deposit, claim} | {pool balance} | YES/NO | {cooldown? same-epoch check? minimum lock?} |
Hot potato flash loan pattern:
1. borrow(pool, amount) -> (Coin<T>, FlashReceipt) // Creates hot potato
2. [attacker does arbitrary operations with Coin<T>]
3. repay(pool, coin, receipt) // Consumes hot potato
Checks for hot potato flash loans:
repay() verify returned amount >= borrowed amount + fee?FlashReceipt type-parameterized to prevent cross-pool receipt reuse?key, store, drop, copy)Within a PTB touching shared objects:
1. [Command 1] Call DEX swap to move on-chain price
2. [Command 2] Call protocol function that reads manipulated price
3. [Command 3] Reverse DEX swap to restore price
4. Net effect: Protocol acted on manipulated price, attacker profited
Check: Does protocol read spot prices (manipulable) or TWAP/oracle prices (resistant)?
1. [Command 1] Deposit large amount (inflate balance/shares)
2. [Command 2] Trigger reward distribution (on inflated balance)
3. [Command 3] Withdraw deposited amount
4. Net effect: Captured rewards with zero time commitment
1. [Command 1] Set state to value A (e.g., via AdminCap function)
2. [Command 2] Perform action requiring state A
3. [Command 3] Reset state back to original value B
4. Net effect: Privileged action performed, state appears unchanged
Check: Possible if attacker controls an AdminCap? (Rule 6: semi-trusted role analysis)
| Shared Object | Invariant | Commands That Mutate It | Sequence-Dependent? | Exploitable? | |--------------|-----------|------------------------|-------------------|-------------| | {obj} | {invariant description} | {cmd1, cmd2, cmd3} | YES/NO | {if YES, describe exploit} |
Check for each shared object: Write down the invariant. Can a sequence of 2-3 valid individual operations (each maintaining the invariant) produce a combined state that violates the invariant?
For each zero-ability struct (hot potato):
| # | Hot Potato Type | Created By | Consumed By | Abilities | Bypass Possible? | |---|----------------|-----------|-------------|-----------|-----------------| | 1 | {Receipt} | {borrow()} | {repay()} | NONE (confirmed) | {analysis} |
Checks:
drop -> NOT a hot potato.store ability store the hot potato? (bypass via external module -- check for public generic wrappers)Can objects be wrapped, manipulated, and unwrapped within a single PTB to bypass checks?
| Wrap Operation | Unwrap Operation | What's Inside | Check Bypassed? | |---------------|-----------------|-------------|----------------| | {wrap_func} | {unwrap_func} | {object with restrictions} | {describe bypass or NONE} |
Pattern: Object has transfer restrictions (key only, no store). Attacker wraps it inside a store-capable struct, transfers the wrapper, then unwraps on the other side. If wrap and unwrap are both public functions -> transfer restriction bypassed within a single PTB.
PTB gas budget is shared across all commands.
| Attack Vector | Description | Check | |--------------|-------------|-------| | Many-small-operations | 1000+ tiny operations to circumvent aggregate limits | Do aggregate limits track across PTB commands? | | Object creation spam | PTB creates many objects (up to 1024 per PTB) | Unbounded object creation paths? | | Gas exhaustion griefing | Craft PTB that consumes max gas on revert | Gas charged on abort -- attacker pays |
Typically lower severity: Sui's gas model charges the sender, so resource attacks are self-penalizing. Focus on aggregate limit bypass.
## Finding [PTB-N]: Title
**Verdict**: CONFIRMED / PARTIAL / REFUTED / CONTESTED
**Step Execution**: check1,2,3,4,5,6,7 | skip(reason) | uncertain
**Rules Applied**: [R4:___, R5:___, R8:___, R10:___, R15:___]
**Severity**: Critical/High/Medium/Low/Info
**Location**: sources/{module}.move:LineN
**PTB Attack Type**: SINGLE_CALL_ASSUMPTION / RETURN_VALUE_ROUTING / FLASH_LOAN_VIA_PTB / ATOMIC_MANIPULATION / HOT_POTATO_BYPASS / WRAP_UNWRAP_BYPASS / AGGREGATE_LIMIT
**Attack Sequence**:
1. [Command 1]: {what attacker does}
2. [Command 2]: {what attacker does}
3. [Command N]: {what attacker does}
**Net Effect**: {what the attacker gained}
**Description**: What is wrong
**Impact**: What can happen (fund loss, unfair value capture, invariant violation)
**Evidence**: Code showing vulnerability + PTB command sequence
| Step | Required | Completed? | Notes | |------|----------|------------|-------| | 1. Entry Point Inventory | YES | | All public/entry functions classified | | 1b. Single-Call Assumption Audit | YES | | HIGH PRIORITY -- every public function checked | | 2. Multi-Step Composition Analysis | YES | | All return values checked | | 2b. Value Interception Pattern | YES | | Value-returning functions modeled | | 3. Flash Loan via PTB | YES | | Deposit-action-withdraw patterns | | 4. Shared Object Mutation Ordering | YES | | HIGH PRIORITY -- oracle, balance, state toggle | | 4d. Shared Object Reorder Sensitivity | YES | | Invariant + multi-command sequences | | 5. Hot Potato Enforcement | IF hot potatoes exist | | Zero-ability verified | | 6. Object Wrapping/Unwrapping | IF wrap/unwrap functions exist | | Transfer restriction bypass | | 7. Gas Budget Manipulation | IF aggregate limits exist | | |
After Step 1b: If single-call assumption found on fund-critical function -> immediate HIGH finding.
After Step 3: Cross-reference with SHARE_ALLOCATION_FAIRNESS for deposit-action-withdraw reward capture.
After Step 4a: Cross-reference with ORACLE_ANALYSIS if on-chain DEX prices are used.
After Step 5: Cross-reference with ABILITY_ANALYSIS Section 5 (Hot Potato Enforcement).
If any step skipped, document valid reason (N/A, no hot potatoes, no external calls, no aggregate limits).
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