Dynamic fields in Sui use keys to store and retrieve values. When user-controlled or predictable keys are used, attackers can cause collisions that overwrite existing data, inject malicious values, or break protocol invariants.
Risk Level
High — Can lead to data corruption, asset theft, or protocol takeover.
CWE-653 (Improper Isolation), CWE-706 (Use of Incorrectly-Resolved Name)
The Problem
How Dynamic Field Keys Work
// Keys can be any type with `copy + drop + store`
df::add(&mutuid,key,value);// Same key retrieves the same slot
letval=df::borrow(&uid,key);// Different key types create different namespaces
df::add(&mutuid,string_key,value1);df::add(&mutuid,u64_key,value2);// Different namespace
Collision Scenarios
User-controlled string keys — Attacker chooses key to collide with system data
Predictable numeric keys — Sequential IDs can be predicted and front-run
Type confusion — Same key value in different types might be expected to differ
Namespace pollution — Attacker fills namespace with garbage data
Vulnerable Example
modulevulnerable::storage{usesui::object::UID;usesui::dynamic_fieldasdf;usesui::tx_context::{Self,TxContext};publicstructStoragehaskey{id: UID,}publicstructUserDatahasstore{balance: u64,is_admin: bool,}/// VULNERABLE: User-controlled key allows collision attacks
publicentryfunstore_user_data(storage: &mutStorage,username: vector<u8>,// User-controlled key!
balance: u64,ctx: &mutTxContext){// Attacker can choose username = "admin" and overwrite admin data
df::add(&mutstorage.id,username,UserData{balance,is_admin: false,});}/// VULNERABLE: System uses same key namespace
publicentryfunset_admin(storage: &mutStorage,admin_name: vector<u8>,){df::add(&mutstorage.id,admin_name,UserData{balance: 0,is_admin: true,});}/// VULNERABLE: No existence check before add
publicentryfunupdate_balance(storage: &mutStorage,username: vector<u8>,amount: u64,){// Will abort if key doesn't exist
// But attacker might have already added their own entry
letdata: &mutUserData=df::borrow_mut(&mutstorage.id,username);data.balance=data.balance+amount;}}modulevulnerable::vault{usesui::dynamic_fieldasdf;publicstructVaulthaskey{id: UID,next_slot_id: u64,// Sequential, predictable
}/// VULNERABLE: Predictable slot IDs can be front-run
publicentryfuncreate_deposit_slot(vault: &mutVault,ctx: &mutTxContext): u64{letslot_id=vault.next_slot_id;vault.next_slot_id=slot_id+1;// Attacker predicts next slot_id and front-runs
df::add(&mutvault.id,slot_id,DepositSlot{owner: tx_context::sender(ctx),amount: 0,});slot_id}}
Attack: Key Collision
// Attacker observes admin_name = "superadmin" was used
moduleattack::collision{usevulnerable::storage;publicentryfunbecome_admin(storage: &mutstorage::Storage,ctx: &mutTxContext){// Use same key as admin to inject data
// If store_user_data doesn't check existence, this might work
storage::store_user_data(storage,b"superadmin",// Collide with admin key
999999,ctx);}}
Secure Example
modulesecure::storage{usesui::object::{Self,UID,ID};usesui::dynamic_fieldasdf;usesui::tx_context::{Self,TxContext};usestd::type_name::{Self,TypeName};constE_KEY_EXISTS: u64=0;constE_KEY_NOT_FOUND: u64=1;constE_NOT_OWNER: u64=2;publicstructStoragehaskey{id: UID,}/// SECURE: Type-safe key for user data
publicstructUserDataKeyhascopy,drop,store{user_address: address,}/// SECURE: Separate type for admin keys
publicstructAdminKeyhascopy,drop,store{admin_address: address,}publicstructUserDatahasstore{balance: u64,}publicstructAdminDatahasstore{permissions: u64,}/// SECURE: Key is derived from sender address (unique)
publicentryfunstore_user_data(storage: &mutStorage,balance: u64,ctx: &mutTxContext){letsender=tx_context::sender(ctx);letkey=UserDataKey{user_address: sender};// Check if already exists
assert!(!df::exists_(&storage.id,key),E_KEY_EXISTS);df::add(&mutstorage.id,key,UserData{balance});}/// SECURE: Admin uses different key type
publicentryfunset_admin(storage: &mutStorage,admin_cap: &AdminCap,admin_address: address,permissions: u64,){letkey=AdminKey{admin_address};// Remove existing if present
if(df::exists_(&storage.id,key)){let_: AdminData=df::remove(&mutstorage.id,key);};df::add(&mutstorage.id,key,AdminData{permissions});}/// SECURE: Verify ownership before update
publicentryfunupdate_balance(storage: &mutStorage,amount: u64,ctx: &mutTxContext){letsender=tx_context::sender(ctx);letkey=UserDataKey{user_address: sender};assert!(df::exists_(&storage.id,key),E_KEY_NOT_FOUND);letdata: &mutUserData=df::borrow_mut(&mutstorage.id,key);data.balance=data.balance+amount;}}modulesecure::vault{usesui::object::{Self,UID,ID};usesui::dynamic_fieldasdf;usesui::tx_context::{Self,TxContext};usesui::hash;publicstructVaulthaskey{id: UID,}/// SECURE: Unpredictable slot key using object ID
publicstructSlotKeyhascopy,drop,store{slot_id: ID,}publicstructDepositSlothasstore{owner: address,amount: u64,}/// SECURE: Slot ID is unpredictable object ID
publicentryfuncreate_deposit_slot(vault: &mutVault,ctx: &mutTxContext): ID{// Create a temporary object just for its unique ID
lettemp_uid=object::new(ctx);letslot_id=object::uid_to_inner(&temp_uid);letkey=SlotKey{slot_id};df::add(&mutvault.id,key,DepositSlot{owner: tx_context::sender(ctx),amount: 0,});object::delete(temp_uid);slot_id}}
Key Design Patterns
Pattern 1: Type-Safe Key Namespaces
/// Each data type has its own key type
publicstructUserBalanceKeyhascopy,drop,store{user: address}publicstructUserProfileKeyhascopy,drop,store{user: address}publicstructConfigKeyhascopy,drop,store{name: vector<u8>}/// Compiler ensures different key types = different namespaces
df::add(&mutuid,UserBalanceKey{user},balance);df::add(&mutuid,UserProfileKey{user},profile);// These cannot collide even with same `user` value
Pattern 2: Sender-Derived Keys
/// Only sender can create their own key
publicfunuser_key(ctx: &TxContext): UserKey{UserKey{address: tx_context::sender(ctx)}}publicentryfunstore(container: &mutContainer,data: Data,ctx: &mutTxContext){letkey=user_key(ctx);// Key derived from sender
df::add(&mutcontainer.id,key,data);}
Pattern 3: Composite Keys
/// Combine multiple factors for unique keys
publicstructCompositeKeyhascopy,drop,store{owner: address,category: u8,index: u64,}/// Uniqueness across multiple dimensions
publicfunmake_key(owner: address,category: u8,index: u64): CompositeKey{CompositeKey{owner,category,index}}
// BAD
publicfunstore(uid: &mutUID,user_key: vector<u8>,value: Data){df::add(uid,user_key,value);}// GOOD
publicfunstore(uid: &mutUID,value: Data,ctx: &TxContext){letkey=TypeSafeKey{owner: tx_context::sender(ctx)};df::add(uid,key,value);}
2. Use Object IDs for Unpredictable Keys
letuid=object::new(ctx);letunique_key=object::uid_to_inner(&uid);object::delete(uid);// unique_key is cryptographically random
3. Separate Key Types for Different Data
// Different structs = different namespaces
publicstructUserKeyhascopy,drop,store{...}publicstructAdminKeyhascopy,drop,store{...}publicstructConfigKeyhascopy,drop,store{...}
4. Always Check Existence
// Before add
assert!(!df::exists_(uid,key),E_EXISTS);// Before borrow/remove
assert!(df::exists_(uid,key),E_NOT_FOUND);
Testing Checklist
Test that different users cannot access each other’s data