Weak or improperly protected initialization functions can allow attackers to reinitialize protocol state, overwrite critical settings, or take control of the protocol. The init function in Sui Move has special protections, but custom initialization patterns often lack similar safeguards.
Risk Level
Critical — Can lead to complete protocol takeover.
modulevulnerable::protocol{usesui::object::{Self,UID};usesui::tx_context::{Self,TxContext};usesui::transfer;publicstructProtocolStatehaskey{id: UID,admin: address,fee_bps: u64,treasury: address,initialized: bool,}/// VULNERABLE: Public init can be called by anyone
publicentryfuninitialize(fee_bps: u64,treasury: address,ctx: &mutTxContext){letstate=ProtocolState{id: object::new(ctx),admin: tx_context::sender(ctx),// Caller becomes admin!
fee_bps,treasury,initialized: true,};transfer::share_object(state);}/// VULNERABLE: Boolean check can be bypassed
publicentryfunreinitialize(state: &mutProtocolState,new_admin: address,ctx: &mutTxContext){// Attacker sets initialized = false first
// Then calls reinitialize
assert!(!state.initialized,E_ALREADY_INITIALIZED);state.admin=new_admin;state.initialized=true;}/// VULNERABLE: Allows resetting initialized flag
publicentryfunreset(state: &mutProtocolState){// Anyone can reset, then reinitialize
state.initialized=false;}}modulevulnerable::token{usesui::coin::{Self,TreasuryCap};/// VULNERABLE: TreasuryCap created in callable function
publicfuncreate_currency<T: drop>(witness: T,ctx: &mutTxContext): TreasuryCap<T>{// If witness type has `drop`, attacker can call this
let(treasury_cap,metadata)=coin::create_currency(witness,9,b"VULN",b"Vulnerable Token",b"",option::none(),ctx);transfer::public_freeze_object(metadata);treasury_cap// Attacker gets minting rights!
}}
Attack Scenario
// Attacker sees protocol deployed without calling init
moduleattack::takeover{usevulnerable::protocol;publicentryfunexploit(ctx: &mutTxContext){// Attacker becomes admin
protocol::initialize(9999,// Max fees
@attacker,// Treasury to attacker
ctx);}}
Secure Example
modulesecure::protocol{usesui::object::{Self,UID};usesui::tx_context::{Self,TxContext};usesui::transfer;usesui::package;/// One-Time Witness — can only be used in init
publicstructPROTOCOLhasdrop{}publicstructProtocolStatehaskey{id: UID,admin: address,fee_bps: u64,treasury: address,}/// SECURE: Admin capability created at init
publicstructAdminCaphaskey{id: UID,protocol_id: ID,}/// SECURE: Called exactly once at package publish
funinit(witness: PROTOCOL,ctx: &mutTxContext){// Create publisher for package
letpublisher=package::claim(witness,ctx);letstate=ProtocolState{id: object::new(ctx),admin: tx_context::sender(ctx),fee_bps: 100,// Default fee
treasury: tx_context::sender(ctx),};letstate_id=object::id(&state);letadmin_cap=AdminCap{id: object::new(ctx),protocol_id: state_id,};transfer::share_object(state);transfer::transfer(admin_cap,tx_context::sender(ctx));transfer::public_transfer(publisher,tx_context::sender(ctx));}/// SECURE: Admin updates require capability
publicentryfunupdate_config(cap: &AdminCap,state: &mutProtocolState,new_fee_bps: u64,new_treasury: address,){assert!(cap.protocol_id==object::id(state),E_WRONG_PROTOCOL);assert!(new_fee_bps<=1000,E_FEE_TOO_HIGH);// Max 10%
state.fee_bps=new_fee_bps;state.treasury=new_treasury;}/// SECURE: No reinitialize function exists
// Initialization happens exactly once in init()
}modulesecure::token{usesui::coin::{Self,TreasuryCap,CoinMetadata};usesui::tx_context::TxContext;usesui::transfer;/// SECURE: One-Time Witness
publicstructMY_TOKENhasdrop{}/// SECURE: Called once at publish
funinit(witness: MY_TOKEN,ctx: &mutTxContext){let(treasury_cap,metadata)=coin::create_currency(witness,9,b"MYT",b"My Token",b"A secure token",option::none(),ctx);// Freeze metadata
transfer::public_freeze_object(metadata);// Transfer cap to deployer only
transfer::transfer(treasury_cap,tx_context::sender(ctx));}// No public function to create additional currencies
}
Initialization Patterns
Pattern 1: One-Time Witness (OTW)
/// The module name, in CAPS, with `drop` ability only
publicstructMY_MODULEhasdrop{}/// init receives the OTW and can only be called once
funinit(witness: MY_MODULE,ctx: &mutTxContext){// Guaranteed to run exactly once at publish
// witness cannot be created elsewhere
}
funinit(ctx: &mutTxContext){letadmin_cap=AdminCap{id: object::new(ctx)};// Cap only exists because init was called
// Cannot be recreated
transfer::transfer(admin_cap,tx_context::sender(ctx));}
Pattern 4: Post-Deploy Configuration (Safe)
/// State created in init, configured later
publicstructPendingSetuphaskey{id: UID,deployer: address,setup_deadline: u64,}funinit(ctx: &mutTxContext){transfer::share_object(PendingSetup{id: object::new(ctx),deployer: tx_context::sender(ctx),setup_deadline: 0,// Set during first setup
});}/// SECURE: Only deployer, only once, with deadline
publicentryfuncomplete_setup(pending: PendingSetup,config: SetupConfig,clock: &Clock,ctx: &TxContext){letPendingSetup{id,deployer,setup_deadline}=pending;assert!(tx_context::sender(ctx)==deployer,E_NOT_DEPLOYER);if(setup_deadline>0){assert!(clock::timestamp_ms(clock)<setup_deadline,E_SETUP_EXPIRED);};object::delete(id);// Create actual protocol state
letstate=ProtocolState{/* ... */};transfer::share_object(state);}
Recommended Mitigations
1. Always Use the init Function
/// This is the ONLY safe way to initialize
funinit(ctx: &mutTxContext){// Called exactly once at publish
}
2. Use One-Time Witness for Important Setup
publicstructMY_PROTOCOLhasdrop{}funinit(witness: MY_PROTOCOL,ctx: &mutTxContext){// witness guarantees single execution
}
3. Never Provide Public Initialize Functions
// BAD: Anyone can call
publicfuninitialize(...){}// BAD: Entry doesn't help
publicentryfuninitialize(...){}// GOOD: Use init only
funinit(ctx: &mutTxContext){}
4. Create Capabilities at Init Time
funinit(ctx: &mutTxContext){// Admin cap created here cannot be recreated
letcap=AdminCap{id: object::new(ctx)};transfer::transfer(cap,tx_context::sender(ctx));}
Testing Checklist
Verify init() is the only initialization function
Confirm no public initialize/setup functions exist
Test that OTW cannot be created outside init
Verify state cannot be reinitialized after init
Confirm capabilities created in init cannot be recreated