STEP PRIORITY: Steps 5 (Hot Potato Enforcement) and 7 (Dynamic Field Ability Propagation) are where HIGH/CRITICAL severity findings most commonly hide. Do NOT rush these steps. If constrained, skip conditional sections before skipping 5 or 7.
1. Struct Ability Inventory
Enumerate ALL structs across all modules:
Module
Struct
Abilities
Has id: UID?
Is Object?
Transferable?
Notes
{mod}
{name}
{key, store, drop, copy}
YES/NO
YES/NO
YES/NO
{context}
Sui ability semantics:
key = Object type. MUST have id: UID as the first field. Can be owned, shared, or frozen.
store = Can be transferred freely via public_transfer / public_share_object. Can be stored inside other objects via dynamic fields or wrapping.
drop = Can be implicitly discarded. Without drop, the value MUST be explicitly consumed (unpacked, transferred, or destroyed).
copy = Can be duplicated. copy + key is IMPOSSIBLE in Sui -- objects cannot be copied.
No abilities at all = Hot potato pattern. Value must be consumed within the same transaction.
Consistency check: For each struct with key:
Does it have id: UID as the FIRST field? If not -> compilation error (catch misplaced UID).
Is store intentionally included or omitted? key without store = only the defining module can transfer it (custom transfer rules).
2. Object Model Classification
Classify each object (key ability) by ownership model:
Object
Ownership
Created Via
Transfer Restricted?
Freeze Possible?
{name}
Owned / Shared / Frozen / Wrapped
{function}
YES (no store) / NO (store)
YES/NO
Security checks per ownership type:
2a. Owned Objects
Can the owner transfer to themselves via transfer::transfer to reset state?
Are there time-locks or cooldowns that reset on transfer?
Can owned objects be wrapped inside other objects to bypass module restrictions?
2b. Shared Objects
Is the object made shared via transfer::share_object at creation?
Once shared, can never be un-shared -- is this intended?
Shared objects require consensus ordering -- are there ordering-dependent operations?
Critical: Can an attacker create a competing shared object of the same type?
2c. Frozen Objects (Immutable)
Is the object frozen via transfer::freeze_object?
Once frozen, can never be mutated -- is this intended?
Are there references to the frozen object that expect mutation?
2d. Wrapped Objects
Objects stored as fields inside other objects lose their independent existence.
Can wrapping bypass transfer restrictions (object with key only, no store, wrapped inside a key + store parent)?
When unwrapped, does the object retain its original ID and state?
3. Ability Mismatch Analysis
For each struct, verify ability assignments match intended behavior:
3a. Missing drop -- Intentional?
Struct
Has drop?
Explicit Destroy Function?
Can Leak?
{name}
NO
YES: destroy_{name}() / NO
YES/NO
Rule: A struct without drop that has no explicit destroy/consume path creates a resource leak. The transaction will abort if the value is not consumed. This is sometimes intentional (hot potato) but often a bug when the struct is created in error paths.
3b. Unnecessary store -- Over-Permissive?
Struct
Has store?
Stored in Dynamic Fields?
Freely Transferable?
Should Be Restricted?
{name}
YES
YES/NO
YES
{analysis}
Check: If a struct has store but the protocol intends restricted transfers (e.g., non-transferable receipts, bound tickets), the store ability enables bypass via public_transfer. Does any security invariant depend on transfer restriction?
3c. copy Abuse Potential
Struct
Has copy?
Contains Balances/IDs?
Duplication Dangerous?
{name}
YES/NO
YES/NO
YES/NO
Rule: copy on a struct containing Balance<T>, capability tokens, or unique identifiers is almost always a bug -- it enables double-spending or capability duplication. copy + key is impossible (enforced by Sui), but copy + store on inner structs is allowed and dangerous if they hold value.
4. Capability Pattern Audit
Identify all capability/admin structs:
Capability
Abilities
Created In
Transferred To
Can Be Duplicated?
Revocable?
{name}
{abilities}
init()
{recipient}
YES (copy) / NO
YES/NO
Checks:
Is the capability created only in init() (module initializer)? If created elsewhere, can it be minted by unauthorized parties?
Does the capability have store? If yes, the holder can transfer it freely -- is this intended?
Is there a revocation mechanism? (Capability patterns in Sui are typically one-way -- once issued, not revocable without wrapping in a shared object with access control.)
One-Time Witness (OTW) vs Capability: Is this struct actually an OTW being misused as a persistent capability? OTW types should be consumed in init, not stored.
5. Hot Potato Enforcement
Identify all structs with NO abilities:
Struct
Module
Created By
Must Be Consumed By
Enforced?
{name}
{mod}
{function}
{function}
YES/NO
Hot potato security checks:
Is the hot potato created and consumed within a single PTB (Programmable Transaction Block)?
Can the consumption function be called by anyone, or only specific callers?
Does the consumption function validate the hot potato's contents match expectations?
If the hot potato is a receipt/ticket/flash-loan proof, does it store the source object ID (order_id, pool_id, position_id, loan_id) that created it?
Does every consumption function assert the stored source ID matches object::id() of the object being repaid, settled, claimed, or mutated?
Does the consumption function validate all relevant fields, not just that the receipt exists (type pair, amount, fee, epoch/deadline, pool/order/position identity)?
Can any alternate consume/repay/settle path skip the source-ID or field validation?
Can an attacker create a fake hot potato of the same type from a different module? (NO -- Move type system prevents cross-module struct creation.)
Can the hot potato be stored if someone adds store via a wrapper? (Check: is there a public wrapper that accepts arbitrary store types.)
Transaction abort impact: If the hot potato cannot be consumed (e.g., consumption function reverts), the entire PTB aborts. Can this be used for griefing? (e.g., attacker causes the consumption precondition to fail after the hot potato is created.)
Pattern validation: Trace every hot potato from creation to consumption. Document the full lifecycle:
create: module::start_action() -> HotPotato
... intervening calls that rely on HotPotato's existence ...
consume: module::finish_action(potato: HotPotato)
If any code path creates a hot potato without a guaranteed consumption path -> FINDING (transaction will always abort on that path).
If a receipt enforces consumption but is not bound to the source object it came from -> FINDING (the receipt can settle the wrong order/pool/position).
6. Transfer Restriction Analysis
For objects with key but NOT store:
Object
Module Transfer Function
Custom Rules
Bypass Possible?
{name}
{function or NONE}
{description}
YES/NO
Sui transfer rules:
key + store: Anyone can transfer via transfer::public_transfer.
key only: Only the defining module can transfer via transfer::transfer (requires module-level access).
Bypass check: Can the restricted object be wrapped inside a store-capable struct, then the wrapper transferred freely? If the wrapping struct is from a DIFFERENT module, this is a transfer restriction bypass.
Check each restricted object:
Does any public function accept this object type and wrap it?
Does any public function accept this object type and place it in a dynamic field of a freely transferable object?
If yes to either -> the transfer restriction is bypassable -> FINDING.
7. Dynamic Field Ability Propagation
For every use of dynamic_field::add or dynamic_object_field::add:
Parent Object
Field Key Type
Field Value Type
Value Has store?
Parent Has store?
{parent}
{key_type}
{value_type}
YES/NO
YES/NO
Rules:
dynamic_field::add requires the value type to have store.
dynamic_object_field::add requires the value type to have key + store.
Security check: If a value with store is added as a dynamic field, anyone who can access the parent object can potentially extract it via dynamic_field::remove. Is extraction access-controlled?
Orphan check: If the parent object is destroyed, are dynamic fields cleaned up? Orphaned dynamic fields remain in storage and can never be accessed again -> permanent storage leak.
Type confusion: Dynamic fields are keyed by type. Can an attacker add a dynamic field with a key type that collides with an expected key type? (Unlikely due to Move type system, but check for generic key types like vector<u8> or String.)
8. Module Initializer Audit
For each module with an init function:
Module
init Parameters
Objects Created
Capabilities Issued
OTW Consumed?
{mod}
{params}
{list}
{list}
YES/NO/N/A
Checks:
Is init the ONLY place critical capabilities are created?
Does init properly consume the One-Time Witness if one is passed?
Can the module be re-initialized via package upgrade? (Sui package upgrades do NOT re-run init.)
Are shared objects created in init? (They must be -- you cannot share an owned object after creation in Sui.)