Parent objects can accumulate unlimited child objects through dynamic fields or dynamic object fields. This unbounded growth causes gas exhaustion, state bloat, and can make critical operations prohibitively expensive or impossible.
Risk Level
High — Can cause denial of service and gas exhaustion.
CWE-400 (Uncontrolled Resource Consumption), CWE-770 (Allocation of Resources Without Limits)
The Problem
Gas Implications
While Sui charges gas per byte, objects with many children:
Require more time to process
Can hit transaction gas limits
Make enumeration impractical
Increase storage costs indefinitely
Attack Vectors
Spam attacks — Attacker adds millions of children
State bloat — Legitimate use causes uncontrolled growth
Frozen operations — Operations become too expensive
Vulnerable Example
modulevulnerable::collection{usesui::object::{Self,UID};usesui::dynamic_fieldasdf;usesui::dynamic_object_fieldasdof;usesui::tx_context::TxContext;publicstructCollectionhaskey{id: UID,item_count: u64,// No limit on items!
}publicstructItemhaskey,store{id: UID,data: vector<u8>,}/// VULNERABLE: No limit on child additions
publicentryfunadd_item(collection: &mutCollection,data: vector<u8>,ctx: &mutTxContext){letitem=Item{id: object::new(ctx),data,};letitem_id=collection.item_count;collection.item_count=item_id+1;// Unlimited additions possible
dof::add(&mutcollection.id,item_id,item);}/// VULNERABLE: Iteration over unbounded collection
publicfunsum_all_values(collection: &Collection): u64{letmutsum=0u64;letmuti=0u64;// If item_count is huge, this runs out of gas
while(i<collection.item_count){letitem: &Item=dof::borrow(&collection.id,i);sum=sum+vector::length(&item.data);i=i+1;};sum}/// VULNERABLE: Bulk operations on large collections
publicentryfunclear_all(collection: &mutCollection,){// Removing millions of items will fail
while(collection.item_count>0){collection.item_count=collection.item_count-1;letitem: Item=dof::remove(&mutcollection.id,collection.item_count);letItem{id,data: _}=item;object::delete(id);}}}
Attack Scenario
// Attacker floods the collection
publicentryfunspam_attack(collection: &mutvulnerable::collection::Collection,ctx: &mutTxContext){leti=0;while(i<10000){// Add 10k items per tx
vulnerable::collection::add_item(collection,b"spam data that costs gas to store",ctx);i=i+1;};// Repeat until collection is unusable
}
Secure Example
modulesecure::collection{usesui::object::{Self,UID,ID};usesui::dynamic_object_fieldasdof;usesui::tx_context::{Self,TxContext};usesui::transfer;usesui::table::{Self,Table};constE_COLLECTION_FULL: u64=0;constE_BATCH_TOO_LARGE: u64=1;constE_NOT_OWNER: u64=2;constMAX_ITEMS: u64=10000;constMAX_BATCH_SIZE: u64=100;publicstructCollectionhaskey{id: UID,owner: address,item_count: u64,max_items: u64,}/// Separate page for pagination
publicstructCollectionPagehaskey{id: UID,collection_id: ID,page_number: u64,items: Table<u64,Item>,item_count: u64,}publicstructItemhasstore{data: vector<u8>,created_at: u64,}constITEMS_PER_PAGE: u64=1000;/// SECURE: Enforces maximum items
publicentryfunadd_item(collection: &mutCollection,data: vector<u8>,ctx: &mutTxContext){// Check global limit
assert!(collection.item_count<collection.max_items,E_COLLECTION_FULL);// Optional: Charge fee for storage
// let fee = calculate_storage_fee(vector::length(&data));
// collect_fee(fee, ctx);
letitem_index=collection.item_count;collection.item_count=item_index+1;letpage_number=item_index/ITEMS_PER_PAGE;letindex_in_page=item_index%ITEMS_PER_PAGE;// Get or create page
if(!dof::exists_(&collection.id,page_number)){letpage=CollectionPage{id: object::new(ctx),collection_id: object::id(collection),page_number,items: table::new(ctx),item_count: 0,};dof::add(&mutcollection.id,page_number,page);};letpage: &mutCollectionPage=dof::borrow_mut(&mutcollection.id,page_number);table::add(&mutpage.items,index_in_page,Item{data,created_at: tx_context::epoch(ctx),});page.item_count=page.item_count+1;}/// SECURE: Paginated retrieval
publicfunget_page(collection: &Collection,page_number: u64,): &CollectionPage{dof::borrow(&collection.id,page_number)}/// SECURE: Bounded batch removal
publicentryfunremove_batch(collection: &mutCollection,page_number: u64,indices: vector<u64>,ctx: &TxContext){assert!(tx_context::sender(ctx)==collection.owner,E_NOT_OWNER);assert!(vector::length(&indices)<=MAX_BATCH_SIZE,E_BATCH_TOO_LARGE);letpage: &mutCollectionPage=dof::borrow_mut(&mutcollection.id,page_number);leti=0;while(i<vector::length(&indices)){letidx=*vector::borrow(&indices,i);if(table::contains(&page.items,idx)){let_item=table::remove(&mutpage.items,idx);page.item_count=page.item_count-1;collection.item_count=collection.item_count-1;};i=i+1;}}/// SECURE: Per-item removal (no iteration needed)
publicentryfunremove_item(collection: &mutCollection,page_number: u64,index: u64,ctx: &TxContext){assert!(tx_context::sender(ctx)==collection.owner,E_NOT_OWNER);letpage: &mutCollectionPage=dof::borrow_mut(&mutcollection.id,page_number);assert!(table::contains(&page.items,index),E_ITEM_NOT_FOUND);let_item=table::remove(&mutpage.items,index);page.item_count=page.item_count-1;collection.item_count=collection.item_count-1;}}
// Instead of iterating all items
publicfunget_items_page(collection: &Collection,offset: u64,limit: u64,): vector<&Item>{// Return only limit items starting at offset
}
3. Avoid Unbounded Loops
// BAD
while(i<collection.count){...}// GOOD
letbatch_end=min(i+BATCH_SIZE,collection.count);while(i<batch_end){...}
4. Charge for Storage
// Make spam attacks economically unfeasible
publicentryfunadd_item(payment: Coin<SUI>,// Must pay for storage
...){letfee=calculate_storage_fee();assert!(coin::value(&payment)>=fee,E_INSUFFICIENT_PAYMENT);}