Access

Ownable

Source Code

Overview

The Ownable module provides a simple access control mechanism where a contract has a single account (owner) that can be granted exclusive access to specific functions. This pattern is useful for contracts that need a straightforward authorization system with a single privileged account.

Key Concepts

Ownership Management

The system designates a single owner with exclusive access to functions marked with the #[only_owner] macro. The initial owner must be ideally set during contract initialization for the module to function properly.

Like the Access Control module, ownership transfers are implemented as a two-step process to prevent accidental or malicious takeovers:

  1. The current owner initiates the transfer by specifying the new owner and an expiration time (live_until_ledger).
  2. The designated new owner must explicitly accept the transfer to complete it.

Until the transfer is accepted, the original owner retains full control and can override or cancel the transfer by initiating a new one or using a live_until_ledger of 0.

Ownership Renunciation

The Ownable module allows the owner to permanently renounce ownership of the contract. This is a one-way operation that cannot be undone. After ownership is renounced, all functions marked with #[only_owner] become permanently inaccessible.

This feature is useful for contracts that need to become fully decentralized after an initial setup phase, or to make a contract immutable and non-upgradeable by removing administrative control entirely.

Example: Making a Contract Immutable

A common use case is renouncing ownership after initial setup to ensure the contract becomes immutable and cannot be upgraded:

use soroban_sdk::{contract, contractimpl, Address, BytesN, Env};
use stellar_access::ownable::{self as ownable, Ownable};
use stellar_contract_utils::upgradeable;
use stellar_macros::only_owner;

#[contract]
pub struct MyContract;

#[contractimpl]
impl MyContract {
    pub fn __constructor(e: &Env, initial_owner: Address) {
        ownable::set_owner(e, &initial_owner);
    }

    // No `#[only_owner]` here: `renounce_ownership` already requires the owner's
    // authorization, and requiring it twice in the same call would panic.
    pub fn finalize_and_lock(e: &Env) {
        // Perform any final configuration here
        // ...

        // Renounce ownership to make the contract immutable
        // After this call, no owner-restricted functions can ever be called again
        ownable::renounce_ownership(e);

        // The contract is now permanently locked and non-upgradeable
    }

    #[only_owner]
    pub fn emergency_upgrade(e: &Env, new_wasm_hash: BytesN<32>) {
        // This function becomes permanently inaccessible after renounce_ownership
        upgradeable::upgrade(e, &new_wasm_hash);
    }
}

#[contractimpl(contracttrait)]
impl Ownable for MyContract {}

Important Notes:

  • Once renounce_ownership() is called, there is no way to restore ownership
  • All functions marked with #[only_owner] become permanently inaccessible
  • This effectively makes the contract immutable and non-upgradeable
  • You cannot renounce ownership while a transfer is in progress

Procedural Macro

The module includes a procedural macro to simplify owner authorization checks:

#[only_owner]

Ensures the caller is the owner before executing the function:

#[only_owner]
pub fn restricted_function(e: &Env, other_param: u32) {
    // Function body - only accessible to owner
}

This expands to a call to stellar_access::ownable::enforce_owner_auth(e) at the start of the function body, which retrieves the owner from storage and requires its authorization. It panics with OwnableError::OwnerNotSet (2100) if no owner is set, e.g. after ownership has been renounced.

Don't add #[only_owner] to a function that calls ownable::transfer_ownership() or ownable::renounce_ownership(): these already enforce the owner's authorization, and requiring it twice in the same call panics.

Usage Example

Here’s a simple example of using the Ownable module:

use soroban_sdk::{contract, contractimpl, Address, Env};
use stellar_access::ownable::{self as ownable, Ownable};
use stellar_macros::only_owner;

#[contract]
pub struct MyContract;

#[contractimpl]
impl MyContract {
    pub fn __constructor(e: &Env, initial_owner: Address) {
        // Set the contract owner
        ownable::set_owner(e, &initial_owner);
    }

    #[only_owner]
    pub fn update_config(e: &Env, new_value: u32) {
        // Only the owner can call this function
        // Implementation...
    }

    // This function is accessible to anyone
    pub fn get_config(e: &Env) -> u32 {
        // Implementation...
        42
    }
}

// Exposes `get_owner`, `transfer_ownership`, `accept_ownership` and `renounce_ownership`
#[contractimpl(contracttrait)]
impl Ownable for MyContract {}

For a complete contract with tests, see the ownable example.

Benefits and Trade-offs

Benefits

  • Simple and straightforward ownership model
  • Secure two-step ownership transfer process
  • Option to permanently renounce ownership
  • Easy integration with procedural macro
  • Event emission for important actions

Trade-offs

  • Limited to a single privileged account (compared to role-based systems)
  • Once ownership is renounced, privileged functions become permanently inaccessible

See Also