--- name: "ptb-composability" description: "Trigger Pattern PTB flag (always for Sui -- Programmable Transaction Blocks are the Sui transaction model) - Inject Into Breadth agents, depth-external, depth-state-trace" --- # Skill: PTB_COMPOSABILITY (Sui) > **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. --- ## 1. Entry Point Inventory 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. ### 1b. Single-Call Assumption Audit 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**: - **Post-call state check**: Function reads state, performs action, then a SEPARATE function checks the result. Attacker inserts commands between action and check. - **Balance snapshot**: Function reads a balance at start, assumes balance changed only due to its work. Another PTB command could have changed the balance. - **One-operation-per-transaction**: Protocol assumes a user can only deposit OR withdraw in a single transaction. PTB allows deposit + borrow + withdraw atomically. - **Temporal separation**: Protocol assumes time must pass between action A and action B (cooldown). If both functions are callable, PTB executes them in the same transaction with zero time delta. - **Reentrancy-like patterns**: Function A updates state partially, function B reads partial state. PTBs naturally enable "call A then call B" -- state IS updated between commands. **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?" --- ## 2. Multi-Step Composition Analysis 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 / Object / HotPotato} | {any function accepting this type} | YES/NO | **Return value routing checks**: - **Coin routing**: If function A returns `Coin`, can it be routed to function C (external) instead of intended function B? - **Coin splitting**: PTB has native `SplitCoins` command. Returned `Coin` can be split. Does protocol assume full amount passed? - **Coin merging**: PTB has native `MergeCoins` command. Can attacker merge extra coins to inflate amounts? - **Mutable reference routing**: `&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? - **Recipient mismatch**: If function returns a value-bearing object, the final `TransferObjects` command determines the recipient. ### 2b. Value Interception Pattern Model this specific attack for each function returning value: ``` 1. Attacker calls protocol_function_A() -> returns Coin (intended for pool) 2. Attacker routes Coin 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. --- ## 3. Flash Loan via PTB Without explicit flash loan protocols, PTBs enable flash-loan-like patterns: ``` 1. [Command 1] Split large Coin 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, FlashReceipt) // Creates hot potato 2. [attacker does arbitrary operations with Coin] 3. repay(pool, coin, receipt) // Consumes hot potato ``` **Checks for hot potato flash loans**: - Does `repay()` verify returned amount >= borrowed amount + fee? - Can attacker call OTHER protocol functions between borrow and repay that benefit from temporarily inflated balance? - Is `FlashReceipt` type-parameterized to prevent cross-pool receipt reuse? - Verify hot potato struct has NO abilities (no `key`, `store`, `drop`, `copy`) --- ## 4. Shared Object Mutation Ordering Within a PTB touching shared objects: ### 4a. Oracle Manipulation Within PTB ``` 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)? ### 4b. Balance Manipulation Within PTB ``` 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 ``` ### 4c. State Toggling Within PTB ``` 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) ### 4d. Shared Object Reorder Sensitivity | 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? --- ## 5. Hot Potato Enforcement 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**: - [ ] Verify struct has NO abilities. If it has `drop` -> NOT a hot potato. - [ ] Is the consuming function the ONLY function accepting this type? - [ ] Can multiple hot potatoes be created in a single PTB? Does consumption order matter? - [ ] Does consuming function validate the hot potato matches the creating context? - [ ] Can attacker cause consumption precondition to fail AFTER hot potato created? (entire PTB aborts -- griefing vector) - [ ] Can a wrapper with `store` ability store the hot potato? (bypass via external module -- check for public generic wrappers) --- ## 6. Object Wrapping/Unwrapping in PTB 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. --- ## 7. Gas Budget Manipulation 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. --- ## Output Schema ```markdown ## 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 Execution Checklist (MANDATORY) | 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 | | | ### Cross-Reference Markers **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).