Skip to main content

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

Security Best Practices - Selendra