Security Best Practices
Keep WASM contracts secure
Security critical for smart contracts. Rust prevents memory bugs but logic errors still possible.
Rust Safety Advantages
- No buffer overflows - Bounds checked automatically
- No null dereferences - Option type enforced
- No data races - Borrow checker ensures safety
- Strong types - Misuse caught at compile time
Still need care: Logic bugs, integer overflow, access control errors.
Common Vulnerabilities
Integer Overflow
// Dangerous - may overflow in release
self.balance = self.balance + amount;
// Safe - explicit check
self.balance = self.balance
.checked_add(amount)
.ok_or(Error::Overflow)?;
Use checked_add(), checked_sub(), checked_mul() for financial operations. saturating_add() clamps to max.
Unauthorized Access
// Bad - No check
#[ink(message)]
pub fn mint(&mut self, to: AccountId, amount: Balance) {
self.balances.insert(to, &amount);
}
// Good - Owner check
#[ink(message)]
pub fn mint(&mut self, to: AccountId, amount: Balance) -> Result<()> {
if self.env().caller() != self.owner {
return Err(Error::Unauthorized);
}
// Mint logic
Ok(())
}
Reentrancy
// Safe pattern - Update state before external call
#[ink(message)]
pub fn withdraw(&mut self, amount: Balance) -> Result<()> {
let caller = self.env().caller();
let balance = self.balance_of(caller);
if balance < amount {
return Err(Error::InsufficientBalance);
}
// Update state first
self.balances.insert(caller, &(balance - amount));
// Then transfer
self.env().transfer(caller, amount)
.map_err(|_| Error::TransferFailed)?;
Ok(())
}
Input Validation
#[ink(message)]
pub fn transfer(&mut self, to: AccountId, amount: Balance) -> Result<()> {
if amount == 0 {
return Err(Error::InvalidAmount);
}
if to == AccountId::from([0u8; 32]) {
return Err(Error::InvalidAddress);
}
let from = self.env().caller();
let from_balance = self.balance_of(from);
if from_balance < amount {
return Err(Error::InsufficientBalance);
}
self.balances.insert(from, &(from_balance - amount));
self.balances.insert(to, &(self.balance_of(to) + amount));
Ok(())
}
Unbounded Operations
// Dangerous - Can run out of gas
for item in self.items.iter() {
self.process_item(item);
}
// Safe - Batched processing
#[ink(message)]
pub fn process_batch(&mut self, start: u32, count: u32) -> Result<()> {
let end = (start + count).min(self.items.len() as u32);
for i in start..end {
self.process_item(&self.items[i as usize]);
}
Ok(())
}
Access Control Patterns
Single Owner
fn ensure_owner(&self) -> Result<()> {
if self.env().caller() != self.owner {
return Err(Error::Unauthorized);
}
Ok(())
}
#[ink(message)]
pub fn admin_function(&mut self) -> Result<()> {
self.ensure_owner()?;
// Protected logic
Ok(())
}
Role-Based
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role { Admin, Minter, Pauser }
fn ensure_role(&self, role: Role) -> Result<()> {
if !self.roles.get((self.env().caller(), role)).unwrap_or(false) {
return Err(Error::MissingRole);
}
Ok(())
}
#[ink(message)]
pub fn mint(&mut self, to: AccountId, amount: Balance) -> Result<()> {
self.ensure_role(Role::Minter)?;
// Mint logic
Ok(())
}
Security Testing
#[ink::test]
fn test_unauthorized_mint() {
let accounts = ink::env::test::default_accounts::<Environment>();
let mut token = Token::new(1000);
ink::env::test::set_caller::<Environment>(accounts.bob);
assert_eq!(token.mint(accounts.bob, 1000), Err(Error::Unauthorized));
}
#[ink::test]
fn test_overflow() {
let mut token = Token::new(u128::MAX);
assert_eq!(token.mint(accounts.alice, 1), Err(Error::Overflow));
}
Security Checklist
Before Deployment
- All privileged functions protected
- Input validation everywhere
- Integer overflow checks
- State updates before external calls
- No unbounded loops
- Complete error handling
- Access control tested
- Attack scenarios tested
- Code reviewed by another developer
Next Steps
Contribute
Found an issue or want to contribute?
Help us improve this documentation by editing this page on GitHub.
