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}