Agent skill

ptb-composability

Trigger Pattern PTB flag (always for Sui -- Programmable Transaction Blocks are the Sui transaction model) - Inject Into Breadth agents, depth-external, depth-state-trace

Stars 215
Forks 33

Install this agent skill to your Project

npx add-skill https://github.com/PlamenTSV/plamen/tree/main/agents/skills/sui/ptb-composability

SKILL.md

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<T> / Object / HotPotato} {any function accepting this type} YES/NO

Return value routing checks:

  • Coin routing: If function A returns Coin<T>, can it be routed to function C (external) instead of intended function B?
  • Coin splitting: PTB has native SplitCoins command. Returned Coin<T> 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<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.


3. Flash Loan via PTB

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:

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

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results