Accidentally exposing objects as shared via transfer::share_object enables global mutation by anyone. Once an object is shared, it cannot be unshared — this is a permanent, irreversible change to the object’s ownership model.
Risk Level
High — Shared objects are accessible to all, potentially exposing sensitive operations.
Global access — Any transaction can reference the shared object
No revocation — Cannot convert back to address-owned
Mutation exposure — All &mut entry functions become callable by anyone
Contention — Performance issues from concurrent access
Vulnerable Example
modulevulnerable::wallet{usesui::object::{Self,UID};usesui::tx_context::{Self,TxContext};usesui::transfer;usesui::coin::{Self,Coin};usesui::sui::SUI;publicstructWallethaskey{id: UID,funds: Coin<SUI>,owner: address,}/// VULNERABLE: Wallet is shared instead of transferred!
funinit(ctx: &mutTxContext){letwallet=Wallet{id: object::new(ctx),funds: coin::zero(ctx),owner: tx_context::sender(ctx),};// WRONG: This should be transfer(), not share_object()!
transfer::share_object(wallet);}/// Because wallet is shared, ANYONE can call this!
publicentryfunwithdraw(wallet: &mutWallet,amount: u64,ctx: &mutTxContext){// This check is useless — attacker just passes their address
letrecipient=tx_context::sender(ctx);// Wait, the owner check is missing entirely!
letwithdrawn=coin::split(&mutwallet.funds,amount,ctx);transfer::public_transfer(withdrawn,recipient);}/// VULNERABLE: Even with owner check, sharing was wrong
publicentryfunwithdraw_checked(wallet: &mutWallet,amount: u64,ctx: &mutTxContext){// Owner check exists but...
assert!(tx_context::sender(ctx)==wallet.owner,E_NOT_OWNER);// If owner's key is compromised, wallet is drained
// With address-owned, owner could at least try to transfer first
letwithdrawn=coin::split(&mutwallet.funds,amount,ctx);transfer::public_transfer(withdrawn,wallet.owner);}}
Attack Scenario
// Attacker finds the shared wallet object
moduleattack::drain_wallet{usevulnerable::wallet;usesui::tx_context::TxContext;publicentryfunsteal(wallet: &mutwallet::Wallet,ctx: &mutTxContext){// Because wallet is shared, attacker can reference it directly
// If withdraw() lacks owner check, funds are gone
wallet::withdraw(wallet,1000000,ctx);}}
Secure Example
modulesecure::wallet{usesui::object::{Self,UID};usesui::tx_context::{Self,TxContext};usesui::transfer;usesui::coin::{Self,Coin};usesui::sui::SUI;/// Wallet is address-owned — only owner can use it
publicstructWallethaskey{id: UID,funds: Coin<SUI>,}/// SECURE: Transfer to user, not share
funinit(ctx: &mutTxContext){transfer::transfer(Wallet{id: object::new(ctx),funds: coin::zero(ctx),},tx_context::sender(ctx));}/// Owner must possess the wallet to call this
publicentryfunwithdraw(wallet: &mutWallet,amount: u64,recipient: address,ctx: &mutTxContext){letwithdrawn=coin::split(&mutwallet.funds,amount,ctx);transfer::public_transfer(withdrawn,recipient);}/// Only owner can transfer their wallet
publicentryfuntransfer_wallet(wallet: Wallet,new_owner: address,){transfer::transfer(wallet,new_owner);}}
When Sharing IS Appropriate
moduleappropriate_sharing::examples{usesui::object::{Self,UID};usesui::tx_context::TxContext;usesui::transfer;/// APPROPRIATE: Global configuration that needs to be readable by all
publicstructGlobalConfighaskey{id: UID,fee_bps: u64,paused: bool,}/// APPROPRIATE: Order book that multiple parties interact with
publicstructOrderBookhaskey{id: UID,bids: vector<Order>,asks: vector<Order>,}/// APPROPRIATE: Liquidity pool for AMM
publicstructLiquidityPoolhaskey{id: UID,reserve_a: Coin<A>,reserve_b: Coin<B>,}/// For shared objects, use capability-based access control
publicstructAdminCaphaskey{id: UID,config_id: ID,}publicentryfunupdate_config(cap: &AdminCap,config: &mutGlobalConfig,new_fee: u64,){assert!(cap.config_id==object::id(config),E_WRONG_CONFIG);config.fee_bps=new_fee;}}
Sharing Decision Checklist
Ask these questions before using share_object:
Must multiple unrelated parties access this object?
Yes → Consider sharing
No → Use transfer() for address-owned
Does the object contain funds or valuable assets?
Yes → Prefer address-owned or use strict capability controls
No → Sharing may be acceptable
Can operations be separated into read-only vs write?
Yes → Consider immutable config + mutable state pattern
No → Ensure all write paths have access control
Is contention expected?
Yes → Consider sharding or owned-object patterns
No → Sharing may be acceptable
Recommended Mitigations
1. Default to Address-Owned
// Unless you have a specific reason, use transfer()
funinit(ctx: &mutTxContext){transfer::transfer(MyObject{...},tx_context::sender(ctx));}
2. Separate Shared and Owned State
/// Shared: Global registry (read-heavy)
publicstructRegistryhaskey{id: UID,// Immutable or admin-only mutable state
}/// Owned: User-specific state
publicstructUserAccounthaskey{id: UID,// User's private state
}
3. Use Capabilities for Shared Object Mutations
/// Any mutation of shared objects requires a capability
publicentryfunmodify_shared(cap: &ModifyCap,shared: &mutSharedObject,...){assert!(cap.shared_id==object::id(shared),E_WRONG_CAP);// Now safe to modify
}
4. Document Sharing Decisions
/// SHARED: This object is intentionally shared because:
/// 1. Multiple parties need to interact (buyers/sellers)
/// 2. All mutations require OrderCap
/// 3. Contention is managed via order sharding
publicstructOrderBookhaskey{...}
Testing Checklist
Review all share_object calls and justify each one
Verify shared objects have proper access control on all mutations
Test that address-owned objects cannot be accessed by non-owners
Confirm no sensitive operations are exposed on shared objects without capability checks