@layerzerolabs/common-utils-stellar-contracts 0.2.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +28 -0
- package/LICENSE +23 -0
- package/clippy.toml +7 -0
- package/package.json +43 -0
- package/rust-toolchain.toml +4 -0
- package/rustfmt.toml +15 -0
- package/src/auth.rs +48 -0
- package/src/buffer_reader.rs +194 -0
- package/src/buffer_writer.rs +138 -0
- package/src/bytes_ext.rs +18 -0
- package/src/errors.rs +82 -0
- package/src/lib.rs +20 -0
- package/src/multisig.rs +247 -0
- package/src/option_ext.rs +38 -0
- package/src/ownable.rs +227 -0
- package/src/rbac.rs +438 -0
- package/src/testing_utils.rs +180 -0
- package/src/tests/auth.rs +179 -0
- package/src/tests/buffer_reader.rs +969 -0
- package/src/tests/buffer_writer.rs +689 -0
- package/src/tests/bytes_ext.rs +160 -0
- package/src/tests/mod.rs +13 -0
- package/src/tests/multisig.rs +760 -0
- package/src/tests/option_ext.rs +18 -0
- package/src/tests/ownable.rs +822 -0
- package/src/tests/rbac.rs +559 -0
- package/src/tests/test_helper.rs +67 -0
- package/src/tests/testing_utils.rs +522 -0
- package/src/tests/ttl_configurable.rs +586 -0
- package/src/tests/ttl_extendable.rs +64 -0
- package/src/tests/upgradeable.rs +529 -0
- package/src/ttl_configurable.rs +169 -0
- package/src/ttl_extendable.rs +25 -0
- package/src/upgradeable.rs +103 -0
package/src/multisig.rs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
self as utils, // Alias for #[storage] macro's generated `utils::ttl_configurable` path
|
|
3
|
+
auth::Auth,
|
|
4
|
+
errors::MultiSigError,
|
|
5
|
+
};
|
|
6
|
+
use common_macros::{contract_trait, storage};
|
|
7
|
+
use soroban_sdk::{assert_with_error, contractevent, Bytes, BytesN, Env, Vec};
|
|
8
|
+
|
|
9
|
+
// ===========================================================================
|
|
10
|
+
// MultiSig events
|
|
11
|
+
// ===========================================================================
|
|
12
|
+
|
|
13
|
+
/// Event emitted when a signer is added or removed.
|
|
14
|
+
#[contractevent]
|
|
15
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
16
|
+
pub struct SignerSet {
|
|
17
|
+
#[topic]
|
|
18
|
+
pub signer: BytesN<20>,
|
|
19
|
+
pub active: bool,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Event emitted when the signature threshold is changed.
|
|
23
|
+
#[contractevent]
|
|
24
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
25
|
+
pub struct ThresholdSet {
|
|
26
|
+
pub threshold: u32,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ===========================================================================
|
|
30
|
+
// MultiSig storage
|
|
31
|
+
// ===========================================================================
|
|
32
|
+
|
|
33
|
+
/// Storage keys for MultiSig.
|
|
34
|
+
#[storage]
|
|
35
|
+
pub enum MultiSigStorage {
|
|
36
|
+
/// List of authorized signers as Ethereum-style addresses (20 bytes).
|
|
37
|
+
///
|
|
38
|
+
/// Practically, multisig has 2-5 signers, so storing them in a single `Vec` is fine.
|
|
39
|
+
/// This makes `get_signers` trivial (just read the Vec), while `set_signer` is slightly
|
|
40
|
+
/// more complex (read-modify-write). Since reads are frequent and writes are rare,
|
|
41
|
+
/// this trade-off is acceptable.
|
|
42
|
+
#[persistent(Vec<BytesN<20>>)]
|
|
43
|
+
#[default(Vec::new(env))]
|
|
44
|
+
Signers,
|
|
45
|
+
|
|
46
|
+
/// Minimum number of valid signatures required to authorize operations (quorum).
|
|
47
|
+
#[instance(u32)]
|
|
48
|
+
#[default(0)]
|
|
49
|
+
Threshold,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ===========================================================================
|
|
53
|
+
// MultiSig trait with default implementation
|
|
54
|
+
// ===========================================================================
|
|
55
|
+
|
|
56
|
+
/// Trait for contracts with secp256k1 multisig signature verification.
|
|
57
|
+
///
|
|
58
|
+
/// Extends `Auth` to provide self-owning authorization. Contracts implementing
|
|
59
|
+
/// `MultiSig` should implement `Auth::authorizer()` to return `env.current_contract_address()`,
|
|
60
|
+
/// allowing the multisig quorum to serve as the authorizer for owner-protected operations.
|
|
61
|
+
#[contract_trait]
|
|
62
|
+
pub trait MultiSig: Auth {
|
|
63
|
+
// ===========================================================================
|
|
64
|
+
// Mutation functions, only callable by the contract itself
|
|
65
|
+
// ===========================================================================
|
|
66
|
+
|
|
67
|
+
/// Adds or removes a signer from the multisig. Requires owner authorization.
|
|
68
|
+
fn set_signer(env: &soroban_sdk::Env, signer: &soroban_sdk::BytesN<20>, active: bool) {
|
|
69
|
+
enforce_multisig_auth::<Self>(env);
|
|
70
|
+
match active {
|
|
71
|
+
true => add_signer(env, signer),
|
|
72
|
+
false => remove_signer(env, signer),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Sets the signature threshold (quorum). Requires owner authorization.
|
|
77
|
+
fn set_threshold(env: &soroban_sdk::Env, threshold: u32) {
|
|
78
|
+
enforce_multisig_auth::<Self>(env);
|
|
79
|
+
set_threshold(env, threshold);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ===========================================================================
|
|
83
|
+
// View functions
|
|
84
|
+
// ===========================================================================
|
|
85
|
+
|
|
86
|
+
/// Returns all registered signers.
|
|
87
|
+
fn get_signers(env: &soroban_sdk::Env) -> soroban_sdk::Vec<soroban_sdk::BytesN<20>> {
|
|
88
|
+
MultiSigStorage::signers(env)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Returns the total number of registered signers.
|
|
92
|
+
fn total_signers(env: &soroban_sdk::Env) -> u32 {
|
|
93
|
+
MultiSigStorage::signers(env).len()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Checks if an address is a registered signer.
|
|
97
|
+
fn is_signer(env: &soroban_sdk::Env, signer: &soroban_sdk::BytesN<20>) -> bool {
|
|
98
|
+
MultiSigStorage::signers(env).iter().any(|s| &s == signer)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Returns the current signature threshold (quorum).
|
|
102
|
+
fn threshold(env: &soroban_sdk::Env) -> u32 {
|
|
103
|
+
MultiSigStorage::threshold(env)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ===========================================================================
|
|
107
|
+
// Verification functions
|
|
108
|
+
// ===========================================================================
|
|
109
|
+
|
|
110
|
+
/// Verifies signatures against the configured threshold.
|
|
111
|
+
fn verify_signatures(
|
|
112
|
+
env: &soroban_sdk::Env,
|
|
113
|
+
digest: &soroban_sdk::BytesN<32>,
|
|
114
|
+
signatures: &soroban_sdk::Vec<soroban_sdk::BytesN<65>>,
|
|
115
|
+
) {
|
|
116
|
+
Self::verify_n_signatures(env, digest, signatures, MultiSigStorage::threshold(env));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Verifies signatures against a custom threshold.
|
|
120
|
+
fn verify_n_signatures(
|
|
121
|
+
env: &soroban_sdk::Env,
|
|
122
|
+
digest: &soroban_sdk::BytesN<32>,
|
|
123
|
+
signatures: &soroban_sdk::Vec<soroban_sdk::BytesN<65>>,
|
|
124
|
+
threshold: u32,
|
|
125
|
+
) {
|
|
126
|
+
assert_with_error!(env, threshold > 0, MultiSigError::ZeroThreshold);
|
|
127
|
+
assert_with_error!(env, signatures.len() >= threshold, MultiSigError::SignatureError);
|
|
128
|
+
|
|
129
|
+
let signers = MultiSigStorage::signers(env);
|
|
130
|
+
let mut last_signer: Option<BytesN<20>> = None;
|
|
131
|
+
for signature in signatures.iter() {
|
|
132
|
+
let signer = recover_signer(env, digest, &signature);
|
|
133
|
+
|
|
134
|
+
assert_with_error!(
|
|
135
|
+
env,
|
|
136
|
+
last_signer.as_ref().is_none_or(|last| &signer > last),
|
|
137
|
+
MultiSigError::UnsortedSigners
|
|
138
|
+
);
|
|
139
|
+
assert_with_error!(env, signers.iter().any(|s| s == signer), MultiSigError::SignerNotFound);
|
|
140
|
+
|
|
141
|
+
last_signer = Some(signer);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
// Public helper functions
|
|
148
|
+
// ===========================================================================
|
|
149
|
+
|
|
150
|
+
/// Initializes multisig with signers and threshold. Called from contract constructors.
|
|
151
|
+
pub fn init_multisig(env: &Env, signers: &Vec<BytesN<20>>, threshold: u32) {
|
|
152
|
+
assert_with_error!(env, !MultiSigStorage::has_signers(env), MultiSigError::AlreadyInitialized);
|
|
153
|
+
|
|
154
|
+
signers.iter().for_each(|signer| add_signer(env, &signer));
|
|
155
|
+
set_threshold(env, threshold);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Recovers Ethereum-style signer address from secp256k1 signature (65 bytes: r + s + v).
|
|
159
|
+
pub fn recover_signer(env: &Env, digest: &BytesN<32>, signature: &BytesN<65>) -> BytesN<20> {
|
|
160
|
+
let sig_bytes: Bytes = signature.into();
|
|
161
|
+
|
|
162
|
+
// Extract recovery ID (v) - normalize from Ethereum's 27-30 range if needed
|
|
163
|
+
let v = sig_bytes.get(64).unwrap();
|
|
164
|
+
let recovery_id = if (27..=30).contains(&v) { v - 27 } else { v };
|
|
165
|
+
|
|
166
|
+
// Extract r,s components (first 64 bytes)
|
|
167
|
+
let sig_rs: BytesN<64> = sig_bytes.slice(0..64).try_into().unwrap();
|
|
168
|
+
|
|
169
|
+
// Recover uncompressed public key (65 bytes with 0x04 prefix)
|
|
170
|
+
let public_key = env.crypto_hazmat().secp256k1_recover(digest, &sig_rs, recovery_id as u32);
|
|
171
|
+
|
|
172
|
+
// Derive Ethereum address: keccak256(pubkey[1..65])[12..32]
|
|
173
|
+
let pubkey_body: Bytes = Bytes::from(public_key).slice(1..65);
|
|
174
|
+
let hash: Bytes = env.crypto().keccak256(&pubkey_body).into();
|
|
175
|
+
hash.slice(12..32).try_into().unwrap()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Enforces multisig authorization by requiring the contract's own address to authorize.
|
|
179
|
+
/// Panics with `InvalidAuthorizer` if the authorizer is not the contract's own address.
|
|
180
|
+
pub fn enforce_multisig_auth<T: MultiSig>(env: &Env) {
|
|
181
|
+
// Ensure the authorizer is the contract's own address
|
|
182
|
+
assert_with_error!(
|
|
183
|
+
env,
|
|
184
|
+
Some(env.current_contract_address()) == T::authorizer(env),
|
|
185
|
+
MultiSigError::InvalidAuthorizer
|
|
186
|
+
);
|
|
187
|
+
env.current_contract_address().require_auth();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ===========================================================================
|
|
191
|
+
// Private helper functions
|
|
192
|
+
// ===========================================================================
|
|
193
|
+
|
|
194
|
+
/// Adds a new signer to the multisig.
|
|
195
|
+
fn add_signer(env: &Env, signer: &BytesN<20>) {
|
|
196
|
+
// Not allowed to add zero address as signer
|
|
197
|
+
assert_with_error!(env, signer != &BytesN::from_array(env, &[0u8; 20]), MultiSigError::InvalidSigner);
|
|
198
|
+
// Not allowed to add same signer twice
|
|
199
|
+
let mut signers = MultiSigStorage::signers(env);
|
|
200
|
+
assert_with_error!(env, !signers.iter().any(|s| &s == signer), MultiSigError::SignerAlreadyExists);
|
|
201
|
+
|
|
202
|
+
// Add signer to list
|
|
203
|
+
signers.push_back(signer.clone());
|
|
204
|
+
MultiSigStorage::set_signers(env, &signers);
|
|
205
|
+
|
|
206
|
+
SignerSet { signer: signer.clone(), active: true }.publish(env);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Removes a signer from the multisig.
|
|
210
|
+
fn remove_signer(env: &Env, signer: &BytesN<20>) {
|
|
211
|
+
let mut signers = MultiSigStorage::signers(env);
|
|
212
|
+
let index = signers.first_index_of(signer);
|
|
213
|
+
// Not allowed to remove non-existent signer
|
|
214
|
+
assert_with_error!(env, index.is_some(), MultiSigError::SignerNotFound);
|
|
215
|
+
|
|
216
|
+
// Remove signer from list
|
|
217
|
+
signers.remove(index.unwrap());
|
|
218
|
+
|
|
219
|
+
// Not allowed to remove signer if it would violate the threshold
|
|
220
|
+
assert_with_error!(
|
|
221
|
+
env,
|
|
222
|
+
signers.len() >= MultiSigStorage::threshold(env),
|
|
223
|
+
MultiSigError::TotalSignersLessThanThreshold
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Update signers list
|
|
227
|
+
MultiSigStorage::set_signers(env, &signers);
|
|
228
|
+
|
|
229
|
+
SignerSet { signer: signer.clone(), active: false }.publish(env);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Sets the signature threshold (quorum).
|
|
233
|
+
fn set_threshold(env: &Env, threshold: u32) {
|
|
234
|
+
// Not allowed to set threshold to zero
|
|
235
|
+
assert_with_error!(env, threshold > 0, MultiSigError::ZeroThreshold);
|
|
236
|
+
// Not allowed to set threshold to greater than the number of signers
|
|
237
|
+
assert_with_error!(
|
|
238
|
+
env,
|
|
239
|
+
MultiSigStorage::signers(env).len() >= threshold,
|
|
240
|
+
MultiSigError::TotalSignersLessThanThreshold
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Update threshold
|
|
244
|
+
MultiSigStorage::set_threshold(env, &threshold);
|
|
245
|
+
|
|
246
|
+
ThresholdSet { threshold }.publish(env);
|
|
247
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
use soroban_sdk::{panic_with_error, Env, Error};
|
|
2
|
+
|
|
3
|
+
/// Extension trait for `Option<T>` that provides Soroban-specific unwrapping utilities.
|
|
4
|
+
///
|
|
5
|
+
/// This trait extends the standard `Option` type with methods that integrate with
|
|
6
|
+
/// Soroban's error handling system, allowing for more descriptive panics when
|
|
7
|
+
/// unwrapping fails.
|
|
8
|
+
pub trait OptionExt<T> {
|
|
9
|
+
/// Unwraps the `Option`, returning the contained value if `Some`,
|
|
10
|
+
/// or panics with the provided error if `None`.
|
|
11
|
+
///
|
|
12
|
+
/// # Arguments
|
|
13
|
+
/// * `env` - The Soroban environment, required for error propagation.
|
|
14
|
+
/// * `error` - The error to emit if the `Option` is `None`. Must be convertible into a `soroban_sdk::Error`.
|
|
15
|
+
///
|
|
16
|
+
/// # Returns
|
|
17
|
+
/// The contained value if `Some`.
|
|
18
|
+
///
|
|
19
|
+
/// # Panics
|
|
20
|
+
/// Panics with the specified error if the `Option` is `None`.
|
|
21
|
+
fn unwrap_or_panic<E>(self, env: &Env, error: E) -> T
|
|
22
|
+
where
|
|
23
|
+
E: Into<Error>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl<T> OptionExt<T> for Option<T> {
|
|
27
|
+
fn unwrap_or_panic<E>(self, env: &Env, error: E) -> T
|
|
28
|
+
where
|
|
29
|
+
E: Into<Error>,
|
|
30
|
+
{
|
|
31
|
+
match self {
|
|
32
|
+
// Return the inner value if present
|
|
33
|
+
Some(val) => val,
|
|
34
|
+
// Panic with the provided error if None, using Soroban's error macro
|
|
35
|
+
None => panic_with_error!(env, error),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/ownable.rs
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
use crate::{self as utils, auth::Auth, errors::OwnableError, option_ext::OptionExt};
|
|
2
|
+
use common_macros::{contract_trait, storage};
|
|
3
|
+
use soroban_sdk::{assert_with_error, contractevent, Address, Env};
|
|
4
|
+
|
|
5
|
+
// ===========================================================================
|
|
6
|
+
// Ownable events
|
|
7
|
+
// ===========================================================================
|
|
8
|
+
|
|
9
|
+
/// Event emitted when ownership is transferred (both single-step and two-step completion).
|
|
10
|
+
#[contractevent]
|
|
11
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
12
|
+
pub struct OwnershipTransferred {
|
|
13
|
+
pub old_owner: Address,
|
|
14
|
+
pub new_owner: Address,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Event emitted when a 2-step ownership transfer is proposed.
|
|
18
|
+
#[contractevent]
|
|
19
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
20
|
+
pub struct OwnershipTransferring {
|
|
21
|
+
pub old_owner: Address,
|
|
22
|
+
pub new_owner: Address,
|
|
23
|
+
pub ttl: u32,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Event emitted when a 2-step ownership transfer is cancelled.
|
|
27
|
+
#[contractevent]
|
|
28
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
29
|
+
pub struct OwnershipTransferCancelled {
|
|
30
|
+
pub owner: Address,
|
|
31
|
+
pub cancelled_pending_owner: Address,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Event emitted when ownership is renounced.
|
|
35
|
+
#[contractevent]
|
|
36
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
37
|
+
pub struct OwnershipRenounced {
|
|
38
|
+
pub old_owner: Address,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ===========================================================================
|
|
42
|
+
// Ownable storage for default implementation
|
|
43
|
+
// ===========================================================================
|
|
44
|
+
|
|
45
|
+
/// Storage keys for Ownable.
|
|
46
|
+
#[storage]
|
|
47
|
+
pub enum OwnableStorage {
|
|
48
|
+
#[instance(Address)]
|
|
49
|
+
Owner,
|
|
50
|
+
/// Pending owner for 2-step transfer. Stored in temporary storage with TTL -
|
|
51
|
+
/// automatically expires if not accepted in time.
|
|
52
|
+
#[temporary(Address)]
|
|
53
|
+
PendingOwner,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ===========================================================================
|
|
57
|
+
// Ownable trait with default implementation
|
|
58
|
+
// ===========================================================================
|
|
59
|
+
|
|
60
|
+
/// Trait for contracts with single-owner access control.
|
|
61
|
+
///
|
|
62
|
+
/// Extends `Auth` to provide owner-based authorization. The `Auth::authorizer()`
|
|
63
|
+
/// implementation should return the owner address for Ownable contracts.
|
|
64
|
+
///
|
|
65
|
+
/// Supports both single-step and two-step ownership transfer:
|
|
66
|
+
/// - Single-step: `transfer_ownership` - Immediate transfer (use with caution)
|
|
67
|
+
/// - Two-step: `begin_ownership_transfer` + `accept_ownership` - Safer, requires new owner to accept
|
|
68
|
+
#[contract_trait]
|
|
69
|
+
pub trait Ownable: Auth {
|
|
70
|
+
// ===========================================================================
|
|
71
|
+
// View functions
|
|
72
|
+
// ===========================================================================
|
|
73
|
+
|
|
74
|
+
/// Returns the current owner address, or None if no owner is set.
|
|
75
|
+
fn owner(env: &soroban_sdk::Env) -> Option<soroban_sdk::Address> {
|
|
76
|
+
OwnableStorage::owner(env)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Returns the pending owner address for 2-step transfer, or None if no transfer is pending.
|
|
80
|
+
fn pending_owner(env: &soroban_sdk::Env) -> Option<soroban_sdk::Address> {
|
|
81
|
+
OwnableStorage::pending_owner(env)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ===========================================================================
|
|
85
|
+
// Single-step transfer (immediate)
|
|
86
|
+
// ===========================================================================
|
|
87
|
+
|
|
88
|
+
/// Transfers ownership immediately to a new address.
|
|
89
|
+
///
|
|
90
|
+
/// Use with caution - if you transfer to a wrong address, ownership is lost forever.
|
|
91
|
+
/// Consider using `begin_ownership_transfer` instead.
|
|
92
|
+
///
|
|
93
|
+
/// # Panics
|
|
94
|
+
/// - `OwnerNotSet` if no owner is currently set
|
|
95
|
+
/// - `TransferInProgress` if a 2-step transfer is in progress
|
|
96
|
+
fn transfer_ownership(env: &soroban_sdk::Env, new_owner: &soroban_sdk::Address) {
|
|
97
|
+
let old_owner = enforce_owner_auth::<Self>(env);
|
|
98
|
+
assert_no_pending_transfer::<Self>(env);
|
|
99
|
+
|
|
100
|
+
OwnableStorage::set_owner(env, new_owner);
|
|
101
|
+
OwnershipTransferred { old_owner, new_owner: new_owner.clone() }.publish(env);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ===========================================================================
|
|
105
|
+
// Two-step transfer (safer)
|
|
106
|
+
// ===========================================================================
|
|
107
|
+
|
|
108
|
+
/// Begins an ownership transfer to a new address.
|
|
109
|
+
///
|
|
110
|
+
/// The new owner must call `accept_ownership()` within `ttl` ledgers
|
|
111
|
+
/// to complete the transfer. The pending transfer will automatically expire after.
|
|
112
|
+
///
|
|
113
|
+
/// # Arguments
|
|
114
|
+
/// - `new_owner` - The proposed new owner
|
|
115
|
+
/// - `ttl` - Number of ledgers the new owner has to accept.
|
|
116
|
+
/// Use `0` to cancel a pending transfer (new_owner must match pending).
|
|
117
|
+
///
|
|
118
|
+
/// # Panics
|
|
119
|
+
/// - `OwnerNotSet` if no owner is currently set
|
|
120
|
+
/// - `NoPendingTransfer` when cancelling and no pending transfer exists
|
|
121
|
+
/// - `InvalidTtl` if ttl exceeds max TTL
|
|
122
|
+
/// - `InvalidPendingOwner` when cancelling with wrong new_owner address
|
|
123
|
+
fn begin_ownership_transfer(env: &soroban_sdk::Env, new_owner: &soroban_sdk::Address, ttl: u32) {
|
|
124
|
+
let old_owner = enforce_owner_auth::<Self>(env);
|
|
125
|
+
|
|
126
|
+
// Cancel case: ttl == 0
|
|
127
|
+
if ttl == 0 {
|
|
128
|
+
let pending = Self::pending_owner(env).unwrap_or_panic(env, OwnableError::NoPendingTransfer);
|
|
129
|
+
|
|
130
|
+
// Verify new_owner matches pending (prevents accidental cancellation)
|
|
131
|
+
assert_with_error!(env, pending == *new_owner, OwnableError::InvalidPendingOwner);
|
|
132
|
+
|
|
133
|
+
OwnableStorage::remove_pending_owner(env);
|
|
134
|
+
OwnershipTransferCancelled { owner: old_owner, cancelled_pending_owner: pending }.publish(env);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Initiate case: validate ttl
|
|
139
|
+
assert_with_error!(env, ttl <= env.storage().max_ttl(), OwnableError::InvalidTtl);
|
|
140
|
+
|
|
141
|
+
// Store pending owner with TTL
|
|
142
|
+
OwnableStorage::set_pending_owner(env, new_owner);
|
|
143
|
+
OwnableStorage::extend_pending_owner_ttl(env, ttl, ttl);
|
|
144
|
+
|
|
145
|
+
OwnershipTransferring { old_owner, new_owner: new_owner.clone(), ttl }.publish(env);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Accepts a pending 2-step ownership transfer.
|
|
149
|
+
///
|
|
150
|
+
/// Must be called by the pending owner before the TTL expires.
|
|
151
|
+
///
|
|
152
|
+
/// # Panics
|
|
153
|
+
/// - `NoPendingTransfer` if there is no pending transfer (or it expired)
|
|
154
|
+
fn accept_ownership(env: &soroban_sdk::Env) {
|
|
155
|
+
let new_owner = Self::pending_owner(env).unwrap_or_panic(env, OwnableError::NoPendingTransfer);
|
|
156
|
+
|
|
157
|
+
// Require authorization from the pending owner
|
|
158
|
+
new_owner.require_auth();
|
|
159
|
+
|
|
160
|
+
// Safe to unwrap: owner must exist if pending_owner exists because:
|
|
161
|
+
// 1. pending_owner can only be set via begin_ownership_transfer, which requires owner auth
|
|
162
|
+
// 2. renounce_ownership is blocked while a 2-step transfer is in progress
|
|
163
|
+
let old_owner = OwnableStorage::owner(env).unwrap();
|
|
164
|
+
|
|
165
|
+
// Transfer ownership
|
|
166
|
+
OwnableStorage::remove_pending_owner(env);
|
|
167
|
+
OwnableStorage::set_owner(env, &new_owner);
|
|
168
|
+
|
|
169
|
+
OwnershipTransferred { old_owner, new_owner }.publish(env);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ===========================================================================
|
|
173
|
+
// Renounce
|
|
174
|
+
// ===========================================================================
|
|
175
|
+
|
|
176
|
+
/// Permanently renounces ownership.
|
|
177
|
+
///
|
|
178
|
+
/// # Panics
|
|
179
|
+
/// - `OwnerNotSet` if no owner is currently set
|
|
180
|
+
/// - `TransferInProgress` if a 2-step transfer is in progress (cancel it first)
|
|
181
|
+
fn renounce_ownership(env: &soroban_sdk::Env) {
|
|
182
|
+
let old_owner = enforce_owner_auth::<Self>(env);
|
|
183
|
+
assert_no_pending_transfer::<Self>(env);
|
|
184
|
+
|
|
185
|
+
OwnableStorage::remove_owner(env);
|
|
186
|
+
OwnershipRenounced { old_owner }.publish(env);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Trait for initializing the owner of the contract.
|
|
191
|
+
pub trait OwnableInitializer {
|
|
192
|
+
/// Initializes the owner of the contract.
|
|
193
|
+
///
|
|
194
|
+
/// # Critical: constructor-only, never expose as a public entrypoint
|
|
195
|
+
///
|
|
196
|
+
/// `init_owner` must **ONLY** be called from the contract constructor. Do not expose it
|
|
197
|
+
/// as a public function under the assumption that it will "simply fail" after initialization.
|
|
198
|
+
///
|
|
199
|
+
/// After `renounce_ownership`, the owner is removed and `has_owner` returns false. If
|
|
200
|
+
/// `init_owner` were exposed publicly, anyone could call it post-renounce and become the
|
|
201
|
+
/// new owner, effectively undoing the renunciation. Always keep this logic internal to
|
|
202
|
+
/// the constructor.
|
|
203
|
+
fn init_owner(env: &Env, owner: &Address) {
|
|
204
|
+
assert_with_error!(env, !OwnableStorage::has_owner(env), OwnableError::OwnerAlreadySet);
|
|
205
|
+
OwnableStorage::set_owner(env, owner);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ===========================================================================
|
|
210
|
+
// Ownable helper functions
|
|
211
|
+
// ===========================================================================
|
|
212
|
+
|
|
213
|
+
/// Enforces owner authorization and returns the owner address.
|
|
214
|
+
/// Panics if no owner is set or authorization fails.
|
|
215
|
+
pub fn enforce_owner_auth<T: Ownable>(env: &Env) -> Address {
|
|
216
|
+
let owner = T::owner(env).unwrap_or_panic(env, OwnableError::OwnerNotSet);
|
|
217
|
+
// Ensure the owner is the same as the authorizer
|
|
218
|
+
assert_with_error!(env, Some(&owner) == T::authorizer(env).as_ref(), OwnableError::InvalidAuthorizer);
|
|
219
|
+
owner.require_auth();
|
|
220
|
+
owner
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Asserts that no 2-step ownership transfer is in progress.
|
|
224
|
+
/// Panics with `TransferInProgress` if a pending transfer exists.
|
|
225
|
+
fn assert_no_pending_transfer<T: Ownable>(env: &Env) {
|
|
226
|
+
assert_with_error!(env, T::pending_owner(env).is_none(), OwnableError::TransferInProgress);
|
|
227
|
+
}
|