stellar_access/ownable/
mod.rs

1//! # Ownable Contract Module.
2//!
3//! This module introduces a simple access control mechanism where a contract
4//! has an account (owner) that can be granted exclusive access to specific
5//! functions.
6//!
7//! The `Ownable` trait exposes methods for:
8//! - Getting the current owner
9//! - Transferring ownership
10//! - Renouncing ownership
11//!
12//! The helper `enforce_owner_auth()` is available to restrict access to only
13//! the owner. You can also use the `#[only_owner]` macro (provided elsewhere)
14//! to simplify this.
15//!
16//! ```ignore
17//! #[only_owner]
18//! fn set_config(e: &Env, new_config: u32) { ... }
19//! ```
20//!
21//! See `examples/ownable/src/contract.rs` for a working example.
22//!
23//! ## Note
24//!
25//! The ownership transfer is processed in 2 steps:
26//!
27//! 1. Initiating the ownership transfer by the current owner
28//! 2. Accepting the ownership by the designated owner
29//!
30//! Not providing a direct ownership transfer is a deliberate design decision to
31//! help avoid mistakes by transferring to a wrong address.
32
33mod storage;
34
35mod test;
36
37use soroban_sdk::{contracterror, Address, Env, Symbol};
38
39pub use crate::ownable::storage::{
40    accept_ownership, enforce_owner_auth, get_owner, renounce_ownership, set_owner,
41    transfer_ownership, OwnableStorageKey,
42};
43
44/// A trait for managing contract ownership using a 2-step transfer pattern.
45///
46/// Provides functions to query ownership, initiate a transfer, or renounce
47/// ownership.
48pub trait Ownable {
49    /// Returns `Some(Address)` if ownership is set, or `None` if ownership has
50    /// been renounced.
51    ///
52    /// # Arguments
53    ///
54    /// * `e` - Access to the Soroban environment.
55    fn get_owner(e: &Env) -> Option<Address>;
56
57    /// Initiates a 2-step ownership transfer to a new address.
58    ///
59    /// Requires authorization from the current owner. The new owner must later
60    /// call `accept_ownership()` to complete the transfer.
61    ///
62    /// # Arguments
63    ///
64    /// * `e` - Access to the Soroban environment.
65    /// * `new_owner` - The proposed new owner.
66    /// * `live_until_ledger` - Ledger number until which the new owner can
67    ///   accept. A value of `0` cancels any pending transfer.
68    ///
69    /// # Errors
70    ///
71    /// * [`OwnableError::OwnerNotSet`] - If the owner is not set.
72    /// * [`crate::role_transfer::RoleTransferError::NoPendingTransfer`] - If
73    ///   trying to cancel a transfer that doesn't exist.
74    /// * [`crate::role_transfer::RoleTransferError::InvalidLiveUntilLedger`] -
75    ///   If the specified ledger is in the past.
76    /// * [`crate::role_transfer::RoleTransferError::InvalidPendingAccount`] -
77    ///   If the specified pending account is not the same as the provided `new`
78    ///   address.
79    ///
80    /// # Notes
81    ///
82    /// * Authorization for the current owner is required.
83    fn transfer_ownership(e: &Env, new_owner: Address, live_until_ledger: u32);
84
85    /// Accepts a pending ownership transfer.
86    ///
87    /// # Arguments
88    ///
89    /// * `e` - Access to the Soroban environment.
90    ///
91    /// # Errors
92    ///
93    /// * [`crate::role_transfer::RoleTransferError::NoPendingTransfer`] - If
94    ///   there is no pending transfer to accept.
95    ///
96    /// # Events
97    ///
98    /// * topics - `["ownership_transfer_completed"]`
99    /// * data - `[new_owner: Address]`
100    fn accept_ownership(e: &Env);
101
102    /// Renounces ownership of the contract.
103    ///
104    /// Permanently removes the owner, disabling all functions gated by
105    /// `#[only_owner]`.
106    ///
107    /// # Arguments
108    ///
109    /// * `e` - Access to the Soroban environment.
110    ///
111    /// # Errors
112    ///
113    /// * [`OwnableError::TransferInProgress`] - If there is a pending ownership
114    ///   transfer.
115    /// * [`OwnableError::OwnerNotSet`] - If the owner is not set.
116    ///
117    /// # Notes
118    ///
119    /// * Authorization for the current owner is required.
120    fn renounce_ownership(e: &Env);
121}
122
123// ################## ERRORS ##################
124
125#[contracterror]
126#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
127#[repr(u32)]
128pub enum OwnableError {
129    OwnerNotSet = 1220,
130    TransferInProgress = 1221,
131    OwnerAlreadySet = 1222,
132}
133
134// ################## EVENTS ##################
135
136/// Emits an event when an ownership transfer is initiated.
137///
138/// # Arguments
139///
140/// * `e` - Access to the Soroban environment.
141/// * `old_owner` - The current owner initiating the transfer.
142/// * `new_owner` - The proposed new owner.
143/// * `live_until_ledger` - The ledger number at which the pending transfer will
144///   expire. If this value is `0`, it means the pending transfer is cancelled.
145///
146/// # Events
147///
148/// * topics - `["ownership_transfer"]`
149/// * data - `[old_owner: Address, new_owner: Address]`
150pub fn emit_ownership_transfer(
151    e: &Env,
152    old_owner: &Address,
153    new_owner: &Address,
154    live_until_ledger: u32,
155) {
156    let topics = (Symbol::new(e, "ownership_transfer"),);
157    e.events().publish(topics, (old_owner, new_owner, live_until_ledger));
158}
159
160/// Emits an event when an ownership transfer is completed.
161///
162/// # Arguments
163///
164/// * `e` - Access to the Soroban environment.
165/// * `new_owner` - The new owner who accepted the transfer.
166///
167/// # Events
168///
169/// * topics - `["ownership_transfer_completed"]`
170/// * data - `[new_owner: Address]`
171pub fn emit_ownership_transfer_completed(e: &Env, new_owner: &Address) {
172    let topics = (Symbol::new(e, "ownership_transfer_completed"),);
173    e.events().publish(topics, new_owner);
174}
175
176/// Emits an event when ownership is renounced.
177///
178/// # Arguments
179///
180/// * `e` - Access to the Soroban environment.
181/// * `old_owner` - The address of the owner who renounced ownership.
182///
183/// # Events
184///
185/// * topics - `["ownership_renounced"]`
186/// * data - `[old_owner: Address]`
187pub fn emit_ownership_renounced(e: &Env, old_owner: &Address) {
188    let topics = (Symbol::new(e, "ownership_renounced"),);
189    e.events().publish(topics, old_owner);
190}