@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.
@@ -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
+ }