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
35#[cfg(test)]
36mod test;
37
38use soroban_sdk::{contracterror, contractevent, contracttrait, Address, Env};
39
40pub use crate::ownable::storage::{
41 accept_ownership, enforce_owner_auth, get_owner, renounce_ownership, set_owner,
42 transfer_ownership, OwnableStorageKey,
43};
44
45/// A trait for managing contract ownership using a 2-step transfer pattern.
46///
47/// Provides functions to query ownership, initiate a transfer, or renounce
48/// ownership.
49#[contracttrait]
50pub trait Ownable {
51 /// Returns `Some(Address)` if ownership is set, or `None` if ownership has
52 /// been renounced.
53 ///
54 /// # Arguments
55 ///
56 /// * `e` - Access to the Soroban environment.
57 fn get_owner(e: &Env) -> Option<Address> {
58 get_owner(e)
59 }
60
61 /// Initiates a 2-step ownership transfer to a new address.
62 ///
63 /// Requires authorization from the current owner. The new owner must later
64 /// call `accept_ownership()` to complete the transfer.
65 ///
66 /// # Arguments
67 ///
68 /// * `e` - Access to the Soroban environment.
69 /// * `new_owner` - The proposed new owner.
70 /// * `live_until_ledger` - Ledger number until which the new owner can
71 /// accept. A value of `0` cancels any pending transfer.
72 ///
73 /// # Errors
74 ///
75 /// * [`OwnableError::OwnerNotSet`] - If the owner is not set.
76 /// * [`crate::role_transfer::RoleTransferError::NoPendingTransfer`] - If
77 /// trying to cancel a transfer that doesn't exist.
78 /// * [`crate::role_transfer::RoleTransferError::InvalidLiveUntilLedger`] -
79 /// If the specified ledger is in the past.
80 /// * [`crate::role_transfer::RoleTransferError::InvalidPendingAccount`] -
81 /// If the specified pending account is not the same as the provided `new`
82 /// address.
83 ///
84 /// # Notes
85 ///
86 /// * Authorization for the current owner is required.
87 fn transfer_ownership(e: &Env, new_owner: Address, live_until_ledger: u32) {
88 transfer_ownership(e, &new_owner, live_until_ledger);
89 }
90
91 /// Accepts a pending ownership transfer.
92 ///
93 /// # Arguments
94 ///
95 /// * `e` - Access to the Soroban environment.
96 ///
97 /// # Errors
98 ///
99 /// * [`crate::role_transfer::RoleTransferError::NoPendingTransfer`] - If
100 /// there is no pending transfer to accept.
101 ///
102 /// # Events
103 ///
104 /// * topics - `["ownership_transfer_completed"]`
105 /// * data - `[new_owner: Address]`
106 fn accept_ownership(e: &Env) {
107 accept_ownership(e);
108 }
109
110 /// Renounces ownership of the contract.
111 ///
112 /// Permanently removes the owner, disabling all functions gated by
113 /// `#[only_owner]`.
114 ///
115 /// # Arguments
116 ///
117 /// * `e` - Access to the Soroban environment.
118 ///
119 /// # Errors
120 ///
121 /// * [`OwnableError::TransferInProgress`] - If there is a pending ownership
122 /// transfer.
123 /// * [`OwnableError::OwnerNotSet`] - If the owner is not set.
124 ///
125 /// # Notes
126 ///
127 /// * Authorization for the current owner is required.
128 fn renounce_ownership(e: &Env) {
129 renounce_ownership(e);
130 }
131}
132
133// ################## ERRORS ##################
134
135#[contracterror]
136#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
137#[repr(u32)]
138pub enum OwnableError {
139 OwnerNotSet = 2100,
140 TransferInProgress = 2101,
141 OwnerAlreadySet = 2102,
142}
143
144// ################## EVENTS ##################
145
146/// Event emitted when an ownership transfer is initiated.
147#[contractevent]
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct OwnershipTransfer {
150 pub old_owner: Address,
151 pub new_owner: Address,
152 pub live_until_ledger: u32,
153}
154
155/// Emits an event when an ownership transfer is initiated.
156///
157/// # Arguments
158///
159/// * `e` - Access to the Soroban environment.
160/// * `old_owner` - The address of the current owner.
161/// * `new_owner` - The address of the proposed new owner.
162/// * `live_until_ledger` - The ledger number until which the new owner can
163/// accept the transfer.
164pub fn emit_ownership_transfer(
165 e: &Env,
166 old_owner: &Address,
167 new_owner: &Address,
168 live_until_ledger: u32,
169) {
170 OwnershipTransfer {
171 old_owner: old_owner.clone(),
172 new_owner: new_owner.clone(),
173 live_until_ledger,
174 }
175 .publish(e);
176}
177
178/// Event emitted when an ownership transfer is completed.
179#[contractevent]
180#[derive(Clone, Debug, Eq, PartialEq)]
181pub struct OwnershipTransferCompleted {
182 pub new_owner: Address,
183}
184
185/// Emits an event when an ownership transfer is completed.
186///
187/// # Arguments
188///
189/// * `e` - Access to the Soroban environment.
190/// * `new_owner` - The address of the new owner.
191pub fn emit_ownership_transfer_completed(e: &Env, new_owner: &Address) {
192 OwnershipTransferCompleted { new_owner: new_owner.clone() }.publish(e);
193}
194
195/// Event emitted when ownership is renounced.
196#[contractevent]
197#[derive(Clone, Debug, Eq, PartialEq)]
198pub struct OwnershipRenounced {
199 pub old_owner: Address,
200}
201
202/// Emits an event when ownership is renounced.
203///
204/// # Arguments
205///
206/// * `e` - Access to the Soroban environment.
207/// * `old_owner` - The address of the owner who renounced ownership.
208pub fn emit_ownership_renounced(e: &Env, old_owner: &Address) {
209 OwnershipRenounced { old_owner: old_owner.clone() }.publish(e);
210}