Shared objects in Sui can be mutated by any transaction, leading to contention when many actors try to modify the same object simultaneously. This can cause performance degradation or complete denial of service (DoS) for protocols that rely heavily on shared state.
Risk Level
High — Can make protocols unusable during high-demand periods.
Sui processes transactions that touch the same shared object sequentially to maintain consistency. When many transactions contend for the same shared object:
Throughput drops — Transactions queue up waiting for access
Latency increases — Users experience long confirmation times
Transactions fail — Timeouts and gas exhaustion become common
DoS Attack Vectors
Spam transactions — Attacker floods the network with transactions touching critical shared objects
Lock contention — Deliberate operations that hold shared objects for extended processing
State bloat — Growing shared object data to increase mutation costs
Vulnerable Example
modulevulnerable::exchange{usesui::object::{Self,UID};usesui::tx_context::TxContext;usesui::transfer;usesui::table::{Self,Table};/// VULNERABLE: Single shared object for entire exchange
/// All trades must go through this one object
publicstructExchangehaskey{id: UID,/// Every order stored in one table
orders: Table<u64,Order>,order_counter: u64,/// Global fee accumulator
total_fees: u64,/// All trading pairs in one vector
pairs: vector<TradingPair>,}publicstructOrderhasstore{maker: address,amount: u64,price: u64,}publicstructTradingPairhasstore{base: TypeName,quote: TypeName,volume: u64,}/// VULNERABLE: Every trade touches the same shared object
publicentryfunplace_order(exchange: &mutExchange,amount: u64,price: u64,ctx: &mutTxContext){letorder_id=exchange.order_counter;exchange.order_counter=order_id+1;table::add(&mutexchange.orders,order_id,Order{maker: tx_context::sender(ctx),amount,price,});}/// VULNERABLE: Matching iterates through potentially large order book
publicentryfunmatch_orders(exchange: &mutExchange,buy_order_id: u64,sell_order_id: u64,){// Long-running operation on shared object
// Blocks all other trades
letbuy=table::borrow(&exchange.orders,buy_order_id);letsell=table::borrow(&exchange.orders,sell_order_id);// ... complex matching logic
}}
Attack Scenario
// Attacker spams the exchange with orders
moduleattack::dos_exchange{publicentryfunspam(exchange: &mutExchange,ctx: &mutTxContext){leti=0;while(i<100){// Each call contends for the same Exchange object
vulnerable::exchange::place_order(exchange,1,1,ctx);i=i+1;}}}
Secure Example
modulesecure::exchange{usesui::object::{Self,UID,ID};usesui::tx_context::{Self,TxContext};usesui::transfer;usesui::table::{Self,Table};usesui::dynamic_fieldasdf;/// SECURE: Minimal shared state — just configuration and routing
publicstructExchangeConfighaskey{id: UID,fee_bps: u64,paused: bool,}/// SECURE: Separate shared object per trading pair
publicstructOrderBookhaskey{id: UID,pair_id: ID,/// Sharded order storage
buy_orders: Table<u64,Order>,sell_orders: Table<u64,Order>,next_order_id: u64,}/// SECURE: Orders are user-owned objects, not stored in shared state
publicstructUserOrderhaskey{id: UID,order_book_id: ID,maker: address,amount: u64,price: u64,is_buy: bool,}/// Create separate order book for each trading pair
publicentryfuncreate_order_book(config: &ExchangeConfig,pair_id: ID,ctx: &mutTxContext){transfer::share_object(OrderBook{id: object::new(ctx),pair_id,buy_orders: table::new(ctx),sell_orders: table::new(ctx),next_order_id: 0,});}/// SECURE: User creates their own order object
/// Reduces contention on order book
publicentryfuncreate_order(book: &mutOrderBook,amount: u64,price: u64,is_buy: bool,ctx: &mutTxContext){letorder_id=book.next_order_id;book.next_order_id=order_id+1;// Order is owned by user, not stored in shared object
transfer::transfer(UserOrder{id: object::new(ctx),order_book_id: object::id(book),maker: tx_context::sender(ctx),amount,price,is_buy,},tx_context::sender(ctx));}/// SECURE: Matching happens with user-owned orders
/// Only briefly touches order book for settlement
publicentryfunmatch_orders(book: &mutOrderBook,buy_order: UserOrder,sell_order: UserOrder,ctx: &mutTxContext){// Verify orders belong to this book
assert!(buy_order.order_book_id==object::id(book),E_WRONG_BOOK);assert!(sell_order.order_book_id==object::id(book),E_WRONG_BOOK);assert!(buy_order.is_buy&&!sell_order.is_buy,E_INVALID_MATCH);assert!(buy_order.price>=sell_order.price,E_PRICE_MISMATCH);// Quick settlement — minimal time holding shared object
// ... transfer assets
// Clean up orders
letUserOrder{id: id1,..}=buy_order;letUserOrder{id: id2,..}=sell_order;object::delete(id1);object::delete(id2);}}
Sharding Strategies
Strategy 1: Per-Entity Shared Objects
/// Instead of one global registry
publicstructGlobalRegistryhaskey{...}/// Create per-user or per-entity objects
publicstructUserAccounthaskey{id: UID,owner: address,balances: Table<TypeName,u64>,}
Strategy 2: Time-Based Sharding
/// Shard by time period (hour, day, epoch)
publicstructHourlyBuckethaskey{id: UID,hour: u64,// Unix hour
entries: vector<Entry>,}publicfunget_current_bucket_id(clock: &Clock): u64{clock::timestamp_ms(clock)/3600000// Hours since epoch
}
Strategy 3: Hash-Based Sharding
/// Shard by hash of key
constNUM_SHARDS: u64=256;publicstructShardhaskey{id: UID,shard_index: u64,data: Table<vector<u8>,Value>,}publicfunget_shard_index(key: &vector<u8>): u64{lethash=std::hash::sha3_256(*key);letfirst_byte=*vector::borrow(&hash,0);(first_byteasu64)%NUM_SHARDS}
Strategy 4: Owned Object Patterns
/// Move state to user-owned objects where possible
publicstructUserPositionhaskey{id: UID,// User's individual state — no contention
balance: u64,orders: vector<Order>,}/// Shared object only for global coordination
publicstructGlobalStatehaskey{id: UID,total_supply: u64,// Updated infrequently
}
Recommended Mitigations
1. Minimize Shared Object Scope
// BAD: Everything in one shared object
publicstructProtocolhaskey{users: Table<address,User>,orders: Table<u64,Order>,config: Config,stats: Stats,}// GOOD: Separate concerns
publicstructConfighaskey{...}// Rarely modified
publicstructOrderBookhaskey{...}// Per-pair
// Users and orders as owned objects
2. Use Read-Only Access When Possible
/// Immutable reference reduces contention
publicfunget_price(book: &OrderBook): u64{// Read-only access can be parallelized
book.last_price}
3. Batch Operations
/// Allow batching to reduce transaction count
publicentryfunbatch_place_orders(book: &mutOrderBook,amounts: vector<u64>,prices: vector<u64>,ctx: &mutTxContext){// One transaction for multiple orders
}
4. Rate Limiting
publicstructRateLimiterhaskey{id: UID,window_start: u64,count: u64,max_per_window: u64,}publicfuncheck_rate_limit(limiter: &mutRateLimiter,clock: &Clock){letnow=clock::timestamp_ms(clock);if(now-limiter.window_start>60000){// New window
limiter.window_start=now;limiter.count=0;};assert!(limiter.count<limiter.max_per_window,E_RATE_LIMITED);limiter.count=limiter.count+1;}
Testing Checklist
Identify all shared objects and their access patterns