Access control mistakes occur when authorization checks are missing, incorrectly implemented, or rely on wrong assumptions about TxContext::sender(). These vulnerabilities allow unauthorized users to perform privileged operations.
Risk Level
Critical — Direct path to unauthorized access and fund theft.
Missing authorization checks — Functions accessible to anyone
Checking wrong sender — Confusing gas sponsor with transaction sender
Hardcoded addresses — Addresses that cannot be updated or rotated
Race conditions — Authorization state changes between check and use
Inconsistent models — Mixing capability-based and address-based checks
Vulnerable Example
modulevulnerable::vault{usesui::object::{Self,UID};usesui::tx_context::{Self,TxContext};usesui::coin::{Self,Coin};usesui::sui::SUI;usesui::transfer;constADMIN: address=@0xDEADBEEF;publicstructVaulthaskey{id: UID,funds: Coin<SUI>,admin: address,}publicstructAdminCaphaskey,store{id: UID,}/// VULNERABLE: No access control at all!
publicentryfunwithdraw_all(vault: &mutVault,ctx: &mutTxContext){letamount=coin::value(&vault.funds);letwithdrawn=coin::split(&mutvault.funds,amount,ctx);transfer::public_transfer(withdrawn,tx_context::sender(ctx));}/// VULNERABLE: Hardcoded address cannot be updated
publicentryfunemergency_withdraw(vault: &mutVault,ctx: &mutTxContext){// What if ADMIN key is compromised? No way to rotate!
assert!(tx_context::sender(ctx)==ADMIN,E_NOT_ADMIN);// ... withdraw logic
}/// VULNERABLE: Checks sender but ignores capability
publicentryfunupdate_admin(vault: &mutVault,_cap: &AdminCap,// Cap is ignored!
new_admin: address,ctx: &mutTxContext){// This checks sender even though cap is passed
// If cap was transferred, wrong person might have access
assert!(tx_context::sender(ctx)==vault.admin,E_NOT_ADMIN);vault.admin=new_admin;}/// VULNERABLE: Time-of-check to time-of-use issue
publicentryfunconditional_withdraw(vault: &mutVault,amount: u64,ctx: &mutTxContext){letsender=tx_context::sender(ctx);// Check is performed...
assert!(is_authorized(sender),E_NOT_AUTHORIZED);// ...but in a PTB, authorization might change before this executes
letwithdrawn=coin::split(&mutvault.funds,amount,ctx);transfer::public_transfer(withdrawn,sender);}}
Secure Example
modulesecure::vault{usesui::object::{Self,UID};usesui::tx_context::{Self,TxContext};usesui::coin::{Self,Coin};usesui::sui::SUI;usesui::transfer;usesui::event;constE_NOT_ADMIN: u64=0;constE_ZERO_AMOUNT: u64=1;constE_INSUFFICIENT_FUNDS: u64=2;/// SECURE: No `store` — only this module controls the cap
publicstructAdminCaphaskey{id: UID,vault_id: ID,// Tied to specific vault
}publicstructVaulthaskey{id: UID,funds: Coin<SUI>,}publicstructWithdrawEventhascopy,drop{vault_id: ID,amount: u64,recipient: address,}funinit(ctx: &mutTxContext){letvault=Vault{id: object::new(ctx),funds: coin::zero(ctx),};letvault_id=object::id(&vault);// Create admin cap tied to this vault
letadmin_cap=AdminCap{id: object::new(ctx),vault_id,};transfer::share_object(vault);transfer::transfer(admin_cap,tx_context::sender(ctx));}/// SECURE: Capability-based access control
publicentryfunwithdraw(cap: &AdminCap,vault: &mutVault,amount: u64,recipient: address,ctx: &mutTxContext){// Verify cap is for this vault
assert!(cap.vault_id==object::id(vault),E_NOT_ADMIN);assert!(amount>0,E_ZERO_AMOUNT);assert!(coin::value(&vault.funds)>=amount,E_INSUFFICIENT_FUNDS);letwithdrawn=coin::split(&mutvault.funds,amount,ctx);event::emit(WithdrawEvent{vault_id: object::id(vault),amount,recipient,});transfer::public_transfer(withdrawn,recipient);}/// SECURE: Explicit admin transfer with cap consumption
publicentryfuntransfer_admin(cap: AdminCap,new_admin: address,ctx: &mutTxContext){// Old cap is consumed, new one is created
letAdminCap{id,vault_id}=cap;object::delete(id);transfer::transfer(AdminCap{id: object::new(ctx),vault_id,},new_admin);}/// SECURE: Multi-sig pattern for critical operations
publicstructMultiSigProposalhaskey{id: UID,action: vector<u8>,approvals: vector<address>,threshold: u64,vault_id: ID,}publicentryfunapprove_and_execute(proposal: &mutMultiSigProposal,cap: &AdminCap,ctx: &TxContext){letsender=tx_context::sender(ctx);// Add approval if not already present
if(!vector::contains(&proposal.approvals,&sender)){vector::push_back(&mutproposal.approvals,sender);};// Execute if threshold reached
if(vector::length(&proposal.approvals)>=proposal.threshold){// ... execute action
}}}
Access Control Patterns
Pattern 1: Pure Capability-Based
/// Best for most cases — clear, composable
publicentryfunadmin_action(cap: &AdminCap,...){// Whoever holds the cap can perform the action
// No sender checks needed
}
Pattern 2: Capability + Sender Verification
/// For soul-bound capabilities
publicstructSoulBoundCaphaskey{id: UID,owner: address,}publicentryfunaction(cap: &SoulBoundCap,ctx: &TxContext){assert!(tx_context::sender(ctx)==cap.owner,E_NOT_OWNER);// Both cap possession AND sender match required
}