Dynamic fields and child objects in Sui allow flexible, runtime-determined storage attached to objects. Incorrect usage leads to unbounded growth, key collisions, invariant violations, and data corruption.
Risk Level
High — Can cause state corruption, gas exhaustion, or security bypasses.
CWE-710 (Improper Adherence to Coding Standards), CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes)
The Problem
Dynamic Fields vs Regular Fields
Aspect
Regular Fields
Dynamic Fields
Defined at
Compile time
Runtime
Type safety
Full
Partial (key type determines value type)
Enumeration
Yes
No (must know keys)
Growth
Fixed
Unbounded
Common Mistakes
Unbounded growth — No limit on dynamic field additions
Key collisions — User-controlled keys overwrite existing data
Type confusion — Same key used for different value types
Orphaned data — Parent deleted without removing dynamic fields
Access control bypass — Dynamic fields circumvent intended restrictions
Vulnerable Example
modulevulnerable::inventory{usesui::object::{Self,UID};usesui::tx_context::TxContext;usesui::dynamic_fieldasdf;publicstructInventoryhaskey{id: UID,owner: address,}publicstructItemhasstore{name: vector<u8>,value: u64,}/// VULNERABLE: User-controlled key allows overwriting
publicentryfunadd_item(inventory: &mutInventory,item_name: vector<u8>,// User-controlled key!
value: u64,ctx: &mutTxContext){// Attacker can overwrite existing items!
df::add(&mutinventory.id,item_name,Item{name: item_name,value});}/// VULNERABLE: No ownership check for item modification
publicentryfunupdate_item_value(inventory: &mutInventory,item_name: vector<u8>,new_value: u64,){// Anyone can modify any item if they know the key
letitem: &mutItem=df::borrow_mut(&mutinventory.id,item_name);item.value=new_value;}/// VULNERABLE: No limit on items — gas exhaustion attack
publicentryfunbulk_add(inventory: &mutInventory,count: u64,ctx: &mutTxContext){leti=0;while(i<count){letkey=i;// Sequential keys
df::add(&mutinventory.id,key,Item{name: b"spam",value: 0});i=i+1;}// Inventory now has unbounded number of items
}}modulevulnerable::storage{usesui::dynamic_fieldasdf;usesui::dynamic_object_fieldasdof;publicstructContainerhaskey{id: UID,}/// VULNERABLE: Same key used for different types
publicentryfunstore_string(container: &mutContainer,key: vector<u8>,value: vector<u8>,){df::add(&mutcontainer.id,key,value);}publicentryfunstore_number(container: &mutContainer,key: vector<u8>,value: u64,){// If key already exists with string value, this will fail
// OR worse — type confusion if not properly checked
df::add(&mutcontainer.id,key,value);}}
Attack Scenarios
Key Collision Attack:
// Legitimate user creates an item
add_item(inventory,b"valuable_sword",1000000,ctx);// Attacker overwrites it (if add doesn't check existence)
add_item(inventory,b"valuable_sword",0,ctx);// Original item's value is now corrupted
State Bloat Attack:
// Attacker adds millions of items
bulk_add(inventory,1000000,ctx);// Future operations on this inventory become expensive
Secure Example
modulesecure::inventory{usesui::object::{Self,UID,ID};usesui::tx_context::{Self,TxContext};usesui::dynamic_fieldasdf;usesui::dynamic_object_fieldasdof;constE_ITEM_EXISTS: u64=0;constE_ITEM_NOT_FOUND: u64=1;constE_NOT_OWNER: u64=2;constE_MAX_ITEMS: u64=3;constMAX_ITEMS: u64=1000;publicstructInventoryhaskey{id: UID,owner: address,item_count: u64,// Track count for limits
}/// SECURE: Item is an object, not just stored data
publicstructItemhaskey,store{id: UID,inventory_id: ID,// Link back to parent
name: vector<u8>,value: u64,creator: address,}/// Key type ensures type safety
publicstructItemKeyhascopy,drop,store{item_id: ID,}/// SECURE: Module-controlled keys, existence checks, limits
publicentryfunadd_item(inventory: &mutInventory,name: vector<u8>,value: u64,ctx: &mutTxContext){// Ownership check
assert!(tx_context::sender(ctx)==inventory.owner,E_NOT_OWNER);// Limit check
assert!(inventory.item_count<MAX_ITEMS,E_MAX_ITEMS);letitem=Item{id: object::new(ctx),inventory_id: object::id(inventory),name,value,creator: tx_context::sender(ctx),};letitem_id=object::id(&item);letkey=ItemKey{item_id};// Use object ID as key — guaranteed unique
dof::add(&mutinventory.id,key,item);inventory.item_count=inventory.item_count+1;}/// SECURE: Ownership and existence checks
publicentryfunupdate_item_value(inventory: &mutInventory,item_key: ItemKey,new_value: u64,ctx: &TxContext){assert!(tx_context::sender(ctx)==inventory.owner,E_NOT_OWNER);assert!(dof::exists_(&inventory.id,item_key),E_ITEM_NOT_FOUND);letitem: &mutItem=dof::borrow_mut(&mutinventory.id,item_key);item.value=new_value;}/// SECURE: Proper cleanup with count update
publicentryfunremove_item(inventory: &mutInventory,item_key: ItemKey,ctx: &TxContext){assert!(tx_context::sender(ctx)==inventory.owner,E_NOT_OWNER);assert!(dof::exists_(&inventory.id,item_key),E_ITEM_NOT_FOUND);letitem: Item=dof::remove(&mutinventory.id,item_key);// Verify item belongs to this inventory
assert!(item.inventory_id==object::id(inventory),E_ITEM_NOT_FOUND);inventory.item_count=inventory.item_count-1;// Properly delete the item
letItem{id,inventory_id: _,name: _,value: _,creator: _}=item;object::delete(id);}}
Safe Dynamic Field Patterns
Pattern 1: Type-Safe Keys
/// Different key types for different value types
publicstructStringKeyhascopy,drop,store{name: vector<u8>}publicstructNumberKeyhascopy,drop,store{index: u64}publicstructObjectKeyhascopy,drop,store{id: ID}/// Compiler enforces correct value types
publicfunstore_string(container: &mutContainer,key: StringKey,value: vector<u8>){df::add(&mutcontainer.id,key,value);}publicfunstore_number(container: &mutContainer,key: NumberKey,value: u64){df::add(&mutcontainer.id,key,value);}
Pattern 2: Module-Controlled Keys
/// Key is only constructable within this module
publicstructInternalKeyhascopy,drop,store{prefix: vector<u8>,index: u64,}/// Users cannot create arbitrary keys
funmake_key(index: u64): InternalKey{InternalKey{prefix: b"internal_",index}}
Pattern 3: Registry Pattern
/// Track all keys in a separate structure
publicstructRegistryhaskey{id: UID,keys: vector<ID>,// All known keys
max_entries: u64,}publicfunadd_entry(registry: &mutRegistry,key: ID,value: Entry){assert!(vector::length(®istry.keys)<registry.max_entries,E_FULL);assert!(!vector::contains(®istry.keys,&key),E_EXISTS);vector::push_back(&mutregistry.keys,key);df::add(&mutregistry.id,key,value);}
publicstructContainerhaskey{id: UID,field_count: u64,}constMAX_FIELDS: u64=10000;publicfunadd_checked(container: &mutContainer,...){assert!(container.field_count<MAX_FIELDS,E_LIMIT_REACHED);// ... add field
container.field_count=container.field_count+1;}
2. Use Object IDs as Keys
// Object IDs are globally unique — no collisions
letkey=object::id(&item);df::add(&mutparent.id,key,item);
3. Clean Up Before Deletion
publicfundelete_container(container: Container){letContainer{id,keys}=container;// Remove all dynamic fields first
while(!vector::is_empty(&keys)){letkey=vector::pop_back(&mutkeys);let_: Value=df::remove(&mutid,key);};vector::destroy_empty(keys);object::delete(id);}
Testing Checklist
Test dynamic field addition with duplicate keys
Test removal of non-existent keys
Verify count limits are enforced
Test parent deletion with existing dynamic fields
Verify access control on all dynamic field operations
Test with maximum number of allowed dynamic fields