@layerzerolabs/protocol-stellar-v2 0.2.18 → 0.2.19

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.
Files changed (138) hide show
  1. package/.turbo/turbo-build.log +275 -248
  2. package/.turbo/turbo-lint.log +52 -58
  3. package/.turbo/turbo-test.log +1224 -1358
  4. package/Cargo.lock +8 -5
  5. package/Cargo.toml +1 -1
  6. package/contracts/ERROR_SPEC.md +1 -1
  7. package/contracts/message-libs/uln-302/src/send_uln.rs +1 -1
  8. package/contracts/oapps/oapp/src/oapp_receiver.rs +1 -1
  9. package/contracts/oapps/oft/Cargo.toml +10 -7
  10. package/contracts/oapps/{oft-std → oft}/integration-tests/extensions/test_oft_fee.rs +3 -4
  11. package/contracts/oapps/{oft-std → oft}/integration-tests/extensions/test_pausable.rs +2 -3
  12. package/contracts/oapps/{oft-std → oft}/integration-tests/extensions/test_rate_limiter.rs +1 -1
  13. package/contracts/oapps/oft/integration-tests/mod.rs +1 -1
  14. package/contracts/oapps/oft/integration-tests/setup.rs +28 -127
  15. package/contracts/oapps/oft/integration-tests/utils.rs +254 -21
  16. package/contracts/oapps/oft/src/extensions/oft_fee.rs +5 -6
  17. package/contracts/oapps/oft/src/lib.rs +10 -14
  18. package/contracts/oapps/oft/src/oft.rs +151 -189
  19. package/contracts/oapps/oft/src/oft_types/lock_unlock.rs +9 -11
  20. package/contracts/oapps/oft/src/oft_types/mint_burn.rs +32 -12
  21. package/contracts/oapps/oft/src/oft_types/mod.rs +13 -0
  22. package/contracts/oapps/{oft-std → oft-core}/Cargo.toml +6 -4
  23. package/contracts/oapps/{oft-std → oft-core}/integration-tests/mod.rs +1 -1
  24. package/contracts/oapps/{oft-std → oft-core}/integration-tests/setup.rs +126 -29
  25. package/contracts/oapps/{oft → oft-core}/integration-tests/test_with_sml.rs +3 -3
  26. package/contracts/oapps/oft-core/integration-tests/utils.rs +201 -0
  27. package/contracts/oapps/oft-core/src/lib.rs +18 -0
  28. package/contracts/oapps/oft-core/src/oft_core.rs +439 -0
  29. package/contracts/oapps/{oft → oft-core}/src/tests/mod.rs +0 -2
  30. package/contracts/oapps/{oft → oft-core}/src/tests/test_lz_receive.rs +7 -7
  31. package/contracts/oapps/{oft → oft-core}/src/tests/test_oft_msg_codec.rs +4 -4
  32. package/contracts/oapps/{oft → oft-core}/src/tests/test_resolve_address.rs +3 -3
  33. package/contracts/oapps/{oft → oft-core}/src/tests/test_utils.rs +44 -25
  34. package/contracts/oapps/{oft → oft-core}/src/utils.rs +1 -1
  35. package/contracts/utils/src/errors.rs +5 -1
  36. package/contracts/utils/src/ownable.rs +125 -3
  37. package/contracts/utils/src/tests/option_ext.rs +1 -1
  38. package/contracts/utils/src/tests/ownable.rs +445 -7
  39. package/contracts/utils/src/tests/ttl_configurable.rs +2 -2
  40. package/package.json +4 -5
  41. package/sdk/.turbo/turbo-test.log +216 -206
  42. package/sdk/dist/generated/bml.d.ts +30 -0
  43. package/sdk/dist/generated/bml.js +28 -5
  44. package/sdk/dist/generated/counter.d.ts +122 -2
  45. package/sdk/dist/generated/counter.js +36 -7
  46. package/sdk/dist/generated/dvn.d.ts +30 -0
  47. package/sdk/dist/generated/dvn.js +28 -5
  48. package/sdk/dist/generated/dvn_fee_lib.d.ts +122 -2
  49. package/sdk/dist/generated/dvn_fee_lib.js +36 -7
  50. package/sdk/dist/generated/endpoint.d.ts +122 -2
  51. package/sdk/dist/generated/endpoint.js +36 -7
  52. package/sdk/dist/generated/executor.d.ts +122 -2
  53. package/sdk/dist/generated/executor.js +36 -7
  54. package/sdk/dist/generated/executor_fee_lib.d.ts +122 -2
  55. package/sdk/dist/generated/executor_fee_lib.js +36 -7
  56. package/sdk/dist/generated/executor_helper.d.ts +30 -0
  57. package/sdk/dist/generated/executor_helper.js +28 -5
  58. package/sdk/dist/generated/oft.d.ts +1842 -0
  59. package/sdk/dist/generated/oft.js +345 -0
  60. package/sdk/dist/generated/price_feed.d.ts +122 -2
  61. package/sdk/dist/generated/price_feed.js +36 -7
  62. package/sdk/dist/generated/sml.d.ts +122 -2
  63. package/sdk/dist/generated/sml.js +36 -7
  64. package/sdk/dist/generated/treasury.d.ts +122 -2
  65. package/sdk/dist/generated/treasury.js +36 -7
  66. package/sdk/dist/generated/uln302.d.ts +122 -2
  67. package/sdk/dist/generated/uln302.js +36 -7
  68. package/sdk/dist/generated/upgrader.d.ts +15 -0
  69. package/sdk/dist/generated/upgrader.js +18 -0
  70. package/sdk/dist/index.d.ts +1 -2
  71. package/sdk/dist/index.js +1 -3
  72. package/sdk/package.json +3 -2
  73. package/sdk/src/index.ts +1 -4
  74. package/sdk/test/oft-sml.test.ts +16 -16
  75. package/sdk/turbo.json +8 -0
  76. package/tools/ts-bindings-gen/Cargo.toml +2 -0
  77. package/tools/ts-bindings-gen/src/main.rs +51 -4
  78. package/turbo.json +0 -2
  79. package/contracts/oapps/oft/src/interfaces/mint_burn_token.rs +0 -23
  80. package/contracts/oapps/oft/src/interfaces/mod.rs +0 -3
  81. package/contracts/oapps/oft/src/oft_impl.rs +0 -201
  82. package/contracts/oapps/oft/src/tests/extensions/mod.rs +0 -11
  83. package/contracts/oapps/oft/src/tests/extensions/setup.rs +0 -917
  84. package/contracts/oapps/oft/src/tests/extensions/test_oft_fee.rs +0 -751
  85. package/contracts/oapps/oft/src/tests/extensions/test_pausable.rs +0 -434
  86. package/contracts/oapps/oft/src/tests/extensions/test_rate_limiter.rs +0 -1080
  87. package/contracts/oapps/oft-std/integration-tests/utils.rs +0 -427
  88. package/contracts/oapps/oft-std/src/lib.rs +0 -16
  89. package/contracts/oapps/oft-std/src/oft.rs +0 -174
  90. package/sdk/dist/generated/oft_std.d.ts +0 -1722
  91. package/sdk/dist/generated/oft_std.js +0 -316
  92. package/sdk/dist/wasm/blocked-message-lib.d.ts +0 -1
  93. package/sdk/dist/wasm/blocked-message-lib.js +0 -2
  94. package/sdk/dist/wasm/counter.d.ts +0 -1
  95. package/sdk/dist/wasm/counter.js +0 -2
  96. package/sdk/dist/wasm/dvn-fee-lib.d.ts +0 -1
  97. package/sdk/dist/wasm/dvn-fee-lib.js +0 -2
  98. package/sdk/dist/wasm/dvn.d.ts +0 -1
  99. package/sdk/dist/wasm/dvn.js +0 -2
  100. package/sdk/dist/wasm/endpoint-v2.d.ts +0 -1
  101. package/sdk/dist/wasm/endpoint-v2.js +0 -2
  102. package/sdk/dist/wasm/executor-fee-lib.d.ts +0 -1
  103. package/sdk/dist/wasm/executor-fee-lib.js +0 -2
  104. package/sdk/dist/wasm/executor-helper.d.ts +0 -1
  105. package/sdk/dist/wasm/executor-helper.js +0 -2
  106. package/sdk/dist/wasm/executor.d.ts +0 -1
  107. package/sdk/dist/wasm/executor.js +0 -2
  108. package/sdk/dist/wasm/layerzero-views.d.ts +0 -1
  109. package/sdk/dist/wasm/layerzero-views.js +0 -2
  110. package/sdk/dist/wasm/oft-std.d.ts +0 -1
  111. package/sdk/dist/wasm/oft-std.js +0 -2
  112. package/sdk/dist/wasm/price-feed.d.ts +0 -1
  113. package/sdk/dist/wasm/price-feed.js +0 -2
  114. package/sdk/dist/wasm/simple-message-lib.d.ts +0 -1
  115. package/sdk/dist/wasm/simple-message-lib.js +0 -2
  116. package/sdk/dist/wasm/treasury.d.ts +0 -1
  117. package/sdk/dist/wasm/treasury.js +0 -2
  118. package/sdk/dist/wasm/uln302.d.ts +0 -1
  119. package/sdk/dist/wasm/uln302.js +0 -2
  120. package/sdk/dist/wasm/upgrader.d.ts +0 -1
  121. package/sdk/dist/wasm/upgrader.js +0 -2
  122. package/sdk/dist/wasm.d.ts +0 -15
  123. package/sdk/dist/wasm.js +0 -15
  124. /package/contracts/oapps/{oft-std → oft}/integration-tests/extensions/mod.rs +0 -0
  125. /package/contracts/oapps/{oft → oft-core}/src/codec/mod.rs +0 -0
  126. /package/contracts/oapps/{oft → oft-core}/src/codec/oft_compose_msg_codec.rs +0 -0
  127. /package/contracts/oapps/{oft → oft-core}/src/codec/oft_msg_codec.rs +0 -0
  128. /package/contracts/oapps/{oft → oft-core}/src/errors.rs +0 -0
  129. /package/contracts/oapps/{oft → oft-core}/src/events.rs +0 -0
  130. /package/contracts/oapps/{oft → oft-core}/src/storage.rs +0 -0
  131. /package/contracts/oapps/{oft → oft-core}/src/tests/test_decimals.rs +0 -0
  132. /package/contracts/oapps/{oft → oft-core}/src/tests/test_oft_compose_msg_codec.rs +0 -0
  133. /package/contracts/oapps/{oft → oft-core}/src/tests/test_oft_version.rs +0 -0
  134. /package/contracts/oapps/{oft → oft-core}/src/tests/test_quote_oft.rs +0 -0
  135. /package/contracts/oapps/{oft → oft-core}/src/tests/test_quote_send.rs +0 -0
  136. /package/contracts/oapps/{oft → oft-core}/src/tests/test_send.rs +0 -0
  137. /package/contracts/oapps/{oft → oft-core}/src/tests/test_token.rs +0 -0
  138. /package/contracts/oapps/{oft → oft-core}/src/types.rs +0 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  use crate::{
6
6
  codec::oft_msg_codec::OFTMessage,
7
- oft::OFTClient,
7
+ oft_core::OFTClient,
8
8
  types::{OFTReceipt, SendParam},
9
9
  };
10
10
  use endpoint_v2::{LayerZeroReceiverClient, MessagingFee, MessagingParams, MessagingReceipt, Origin};
@@ -104,15 +104,21 @@ pub fn create_origin(src_eid: u32, sender: &BytesN<32>, nonce: u64) -> Origin {
104
104
  // ==================== Test OFT Contracts ====================
105
105
 
106
106
  mod test_mint_burn_oft {
107
- extern crate self as oft;
108
-
109
- use crate::initialize_oft;
110
- use crate::oft::{OFTInternal, OFT};
111
- use crate::oft_impl;
112
- use crate::types::OFTReceipt;
107
+ use crate::{
108
+ self as oft_core,
109
+ oft_core::{initialize_oft, lz_receive, OFTCore, OFTInternal},
110
+ types::OFTReceipt,
111
+ };
113
112
  use endpoint_v2::Origin;
114
113
  use oapp::oapp_receiver::LzReceiveInternal;
115
- use soroban_sdk::{contractimpl, Address, Bytes, BytesN, Env};
114
+ use soroban_sdk::{contractclient, contractimpl, Address, Bytes, BytesN, Env};
115
+
116
+ #[contractclient(name = "MintBurnTokenClient")]
117
+ #[allow(dead_code)]
118
+ trait MintBurnToken {
119
+ fn mint(env: Env, to: Address, amount: i128);
120
+ fn burn(env: Env, from: Address, amount: i128);
121
+ }
116
122
 
117
123
  #[oapp_macros::oapp]
118
124
  pub struct TestMintBurnOFT;
@@ -132,7 +138,7 @@ mod test_mint_burn_oft {
132
138
  }
133
139
 
134
140
  #[contractimpl(contracttrait)]
135
- impl OFT for TestMintBurnOFT {}
141
+ impl OFTCore for TestMintBurnOFT {}
136
142
 
137
143
  impl LzReceiveInternal for TestMintBurnOFT {
138
144
  fn __lz_receive(
@@ -144,32 +150,36 @@ mod test_mint_burn_oft {
144
150
  executor: &Address,
145
151
  value: i128,
146
152
  ) {
147
- oft_impl::lz_receive::<Self>(env, executor, origin, guid, message, extra_data, value)
153
+ lz_receive::<Self>(env, executor, origin, guid, message, extra_data, value)
148
154
  }
149
155
  }
150
156
 
151
157
  impl OFTInternal for TestMintBurnOFT {
152
158
  fn __debit(env: &Env, sender: &Address, amount_ld: i128, min_amount_ld: i128, dst_eid: u32) -> OFTReceipt {
153
- crate::oft_types::mint_burn::debit::<Self>(env, sender, amount_ld, min_amount_ld, dst_eid)
159
+ // Inline mint_burn::debit implementation
160
+ let receipt = Self::__debit_view(env, amount_ld, min_amount_ld, dst_eid);
161
+ MintBurnTokenClient::new(env, &Self::token(env)).burn(sender, &receipt.amount_received_ld);
162
+ receipt
154
163
  }
155
164
 
156
- fn __credit(env: &Env, to: &Address, amount_ld: i128, src_eid: u32) -> i128 {
157
- crate::oft_types::mint_burn::credit::<Self>(env, to, amount_ld, src_eid)
165
+ fn __credit(env: &Env, to: &Address, amount_ld: i128, _src_eid: u32) -> i128 {
166
+ // Inline mint_burn::credit implementation
167
+ MintBurnTokenClient::new(env, &Self::token(env)).mint(to, &amount_ld);
168
+ amount_ld
158
169
  }
159
170
  }
160
171
  }
161
172
  pub use test_mint_burn_oft::TestMintBurnOFT;
162
173
 
163
174
  mod test_lock_unlock_oft {
164
- extern crate self as oft;
165
-
166
- use crate::initialize_oft;
167
- use crate::oft::{OFTInternal, OFT};
168
- use crate::oft_impl;
169
- use crate::types::OFTReceipt;
175
+ use crate::{
176
+ self as oft_core,
177
+ oft_core::{initialize_oft, lz_receive, OFTCore, OFTInternal},
178
+ types::OFTReceipt,
179
+ };
170
180
  use endpoint_v2::Origin;
171
181
  use oapp::oapp_receiver::{LzReceiveInternal, OAppReceiver};
172
- use soroban_sdk::{contractimpl, Address, Bytes, BytesN, Env};
182
+ use soroban_sdk::{contractimpl, token::TokenClient, Address, Bytes, BytesN, Env};
173
183
 
174
184
  #[oapp_macros::oapp(custom = [receiver])]
175
185
  pub struct TestLockUnlockOFT;
@@ -189,7 +199,7 @@ mod test_lock_unlock_oft {
189
199
  }
190
200
 
191
201
  #[contractimpl(contracttrait)]
192
- impl OFT for TestLockUnlockOFT {}
202
+ impl OFTCore for TestLockUnlockOFT {}
193
203
 
194
204
  impl LzReceiveInternal for TestLockUnlockOFT {
195
205
  fn __lz_receive(
@@ -201,7 +211,7 @@ mod test_lock_unlock_oft {
201
211
  executor: &Address,
202
212
  value: i128,
203
213
  ) {
204
- oft_impl::lz_receive::<Self>(env, executor, origin, guid, message, extra_data, value)
214
+ lz_receive::<Self>(env, executor, origin, guid, message, extra_data, value)
205
215
  }
206
216
  }
207
217
 
@@ -211,11 +221,20 @@ mod test_lock_unlock_oft {
211
221
 
212
222
  impl OFTInternal for TestLockUnlockOFT {
213
223
  fn __debit(env: &Env, sender: &Address, amount_ld: i128, min_amount_ld: i128, dst_eid: u32) -> OFTReceipt {
214
- crate::oft_types::lock_unlock::debit::<Self>(env, sender, amount_ld, min_amount_ld, dst_eid)
224
+ // Inline lock_unlock::debit implementation
225
+ let receipt: OFTReceipt = Self::__debit_view(env, amount_ld, min_amount_ld, dst_eid);
226
+ TokenClient::new(env, &Self::token(env)).transfer(
227
+ sender,
228
+ env.current_contract_address(),
229
+ &receipt.amount_received_ld,
230
+ );
231
+ receipt
215
232
  }
216
233
 
217
- fn __credit(env: &Env, to: &Address, amount_ld: i128, src_eid: u32) -> i128 {
218
- crate::oft_types::lock_unlock::credit::<Self>(env, to, amount_ld, src_eid)
234
+ fn __credit(env: &Env, to: &Address, amount_ld: i128, _src_eid: u32) -> i128 {
235
+ // Inline lock_unlock::credit implementation
236
+ TokenClient::new(env, &Self::token(env)).transfer(&env.current_contract_address(), to, &amount_ld);
237
+ amount_ld
219
238
  }
220
239
  }
221
240
  }
@@ -38,7 +38,7 @@ pub fn remove_dust(amount_ld: i128, conversion_rate: i128) -> i128 {
38
38
  ///
39
39
  /// # Returns
40
40
  /// A 32-byte payload (contract ID hash or Ed25519 public key)
41
- pub fn address_to_bytes32(address: &Address) -> BytesN<32> {
41
+ pub fn address_payload(address: &Address) -> BytesN<32> {
42
42
  match address.to_payload().unwrap() {
43
43
  AddressPayload::ContractIdHash(payload) => payload,
44
44
  AddressPayload::AccountIdPublicKeyEd25519(payload) => payload,
@@ -27,8 +27,12 @@ pub enum TtlConfigurableError {
27
27
  /// OwnableError: 1030-1039
28
28
  #[contract_error]
29
29
  pub enum OwnableError {
30
- OwnerAlreadySet = 1030,
30
+ InvalidPendingOwner = 1030,
31
+ InvalidTtl,
32
+ NoPendingTransfer,
33
+ OwnerAlreadySet,
31
34
  OwnerNotSet,
35
+ TransferInProgress,
32
36
  }
33
37
 
34
38
  /// BytesExtError: 1040-1049
@@ -6,7 +6,7 @@ use soroban_sdk::{assert_with_error, contractevent, Address, Env};
6
6
  // Ownable events
7
7
  // ===========================================================================
8
8
 
9
- /// Event emitted when ownership is transferred.
9
+ /// Event emitted when ownership is transferred (both single-step and two-step completion).
10
10
  #[contractevent]
11
11
  #[derive(Clone, Debug, Eq, PartialEq)]
12
12
  pub struct OwnershipTransferred {
@@ -14,6 +14,15 @@ pub struct OwnershipTransferred {
14
14
  pub new_owner: Address,
15
15
  }
16
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
+
17
26
  /// Event emitted when ownership is renounced.
18
27
  #[contractevent]
19
28
  #[derive(Clone, Debug, Eq, PartialEq)]
@@ -30,6 +39,10 @@ pub struct OwnershipRenounced {
30
39
  pub enum OwnableStorage {
31
40
  #[instance(Address)]
32
41
  Owner,
42
+ /// Pending owner for 2-step transfer. Stored in temporary storage with TTL -
43
+ /// automatically expires if not accepted in time.
44
+ #[temporary(Address)]
45
+ PendingOwner,
33
46
  }
34
47
 
35
48
  // ===========================================================================
@@ -40,23 +53,126 @@ pub enum OwnableStorage {
40
53
  ///
41
54
  /// Extends `Auth` to provide owner-based authorization. The `Auth::authorizer()`
42
55
  /// implementation should return the owner address for Ownable contracts.
56
+ ///
57
+ /// Supports both single-step and two-step ownership transfer:
58
+ /// - Single-step: `transfer_ownership` - Immediate transfer (use with caution)
59
+ /// - Two-step: `propose_ownership_transfer` + `accept_ownership` - Safer, requires new owner to accept
43
60
  #[contract_trait]
44
61
  pub trait Ownable: Sized + Auth {
62
+ // ===========================================================================
63
+ // View functions
64
+ // ===========================================================================
65
+
45
66
  /// Returns the current owner address, or None if no owner is set.
46
67
  fn owner(env: &Env) -> Option<Address> {
47
68
  OwnableStorage::owner(env)
48
69
  }
49
70
 
50
- /// Transfers ownership to a new address. Requires current owner authorization.
71
+ /// Returns the pending owner address for 2-step transfer, or None if no transfer is pending.
72
+ fn pending_owner(env: &Env) -> Option<Address> {
73
+ OwnableStorage::pending_owner(env)
74
+ }
75
+
76
+ // ===========================================================================
77
+ // Single-step transfer (immediate)
78
+ // ===========================================================================
79
+
80
+ /// Transfers ownership immediately to a new address.
81
+ ///
82
+ /// Use with caution - if you transfer to a wrong address, ownership is lost forever.
83
+ /// Consider using `propose_ownership_transfer` instead.
84
+ ///
85
+ /// # Panics
86
+ /// - `OwnerNotSet` if no owner is currently set
87
+ /// - `TransferInProgress` if a 2-step transfer is in progress
51
88
  fn transfer_ownership(env: &Env, new_owner: &Address) {
52
89
  let old_owner = enforce_owner_auth::<Self>(env);
90
+ assert_no_pending_transfer::<Self>(env);
91
+
53
92
  OwnableStorage::set_owner(env, new_owner);
54
93
  OwnershipTransferred { old_owner, new_owner: new_owner.clone() }.publish(env);
55
94
  }
56
95
 
57
- /// Permanently renounces ownership. Requires current owner authorization.
96
+ // ===========================================================================
97
+ // Two-step transfer (safer)
98
+ // ===========================================================================
99
+
100
+ /// Proposes an ownership transfer to a new address.
101
+ ///
102
+ /// The new owner must call `accept_ownership()` within `ttl` ledgers
103
+ /// to complete the transfer. The pending transfer will automatically expire after.
104
+ ///
105
+ /// # Arguments
106
+ /// - `new_owner` - The proposed new owner
107
+ /// - `ttl` - Number of ledgers the new owner has to accept.
108
+ /// Use `0` to cancel a pending transfer (new_owner must match pending).
109
+ ///
110
+ /// # Panics
111
+ /// - `OwnerNotSet` if no owner is currently set
112
+ /// - `NoPendingTransfer` when cancelling and no pending transfer exists
113
+ /// - `InvalidTtl` if ttl exceeds max TTL
114
+ /// - `InvalidPendingOwner` when cancelling with wrong new_owner address
115
+ fn propose_ownership_transfer(env: &Env, new_owner: &Address, ttl: u32) {
116
+ let old_owner = enforce_owner_auth::<Self>(env);
117
+
118
+ // Cancel case: ttl == 0
119
+ if ttl == 0 {
120
+ let pending = Self::pending_owner(env).unwrap_or_panic(env, OwnableError::NoPendingTransfer);
121
+
122
+ // Verify new_owner matches pending (prevents accidental cancellation)
123
+ assert_with_error!(env, pending == *new_owner, OwnableError::InvalidPendingOwner);
124
+
125
+ OwnableStorage::remove_pending_owner(env);
126
+ return;
127
+ }
128
+
129
+ // Initiate case: validate ttl
130
+ assert_with_error!(env, ttl <= env.storage().max_ttl(), OwnableError::InvalidTtl);
131
+
132
+ // Store pending owner with TTL
133
+ OwnableStorage::set_pending_owner(env, new_owner);
134
+ OwnableStorage::extend_pending_owner_ttl(env, ttl, ttl);
135
+
136
+ OwnershipTransferring { old_owner, new_owner: new_owner.clone(), ttl }.publish(env);
137
+ }
138
+
139
+ /// Accepts a pending 2-step ownership transfer.
140
+ ///
141
+ /// Must be called by the pending owner before the TTL expires.
142
+ ///
143
+ /// # Panics
144
+ /// - `NoPendingTransfer` if there is no pending transfer (or it expired)
145
+ fn accept_ownership(env: &Env) {
146
+ let new_owner = Self::pending_owner(env).unwrap_or_panic(env, OwnableError::NoPendingTransfer);
147
+
148
+ // Require authorization from the pending owner
149
+ new_owner.require_auth();
150
+
151
+ // Safe to unwrap: owner must exist if pending_owner exists because:
152
+ // 1. pending_owner can only be set via propose_ownership_transfer, which requires owner auth
153
+ // 2. renounce_ownership is blocked while a 2-step transfer is in progress
154
+ let old_owner = OwnableStorage::owner(env).unwrap();
155
+
156
+ // Transfer ownership
157
+ OwnableStorage::remove_pending_owner(env);
158
+ OwnableStorage::set_owner(env, &new_owner);
159
+
160
+ OwnershipTransferred { old_owner, new_owner }.publish(env);
161
+ }
162
+
163
+ // ===========================================================================
164
+ // Renounce
165
+ // ===========================================================================
166
+
167
+ /// Permanently renounces ownership.
168
+ ///
169
+ /// # Panics
170
+ /// - `OwnerNotSet` if no owner is currently set
171
+ /// - `TransferInProgress` if a 2-step transfer is in progress (cancel it first)
58
172
  fn renounce_ownership(env: &Env) {
59
173
  let old_owner = enforce_owner_auth::<Self>(env);
174
+ assert_no_pending_transfer::<Self>(env);
175
+
60
176
  OwnableStorage::remove_owner(env);
61
177
  OwnershipRenounced { old_owner }.publish(env);
62
178
  }
@@ -86,3 +202,9 @@ pub fn enforce_owner_auth<T: Ownable>(env: &Env) -> Address {
86
202
  pub fn require_owner_auth<T: Ownable>(env: &Env) {
87
203
  let _ = enforce_owner_auth::<T>(env);
88
204
  }
205
+
206
+ /// Asserts that no 2-step ownership transfer is in progress.
207
+ /// Panics with `TransferInProgress` if a pending transfer exists.
208
+ fn assert_no_pending_transfer<T: Ownable>(env: &Env) {
209
+ assert_with_error!(env, T::pending_owner(env).is_none(), OwnableError::TransferInProgress);
210
+ }
@@ -10,7 +10,7 @@ fn unwrap_or_panic_some_returns_value() {
10
10
 
11
11
  #[test]
12
12
  fn unwrap_or_panic_none_panics_with_error() {
13
- const EXPECTED: &str = "Error(Contract, #1031)"; // OwnerNotSet
13
+ const EXPECTED: &str = "Error(Contract, #1034)"; // OwnerNotSet
14
14
  assert_panics_contains("none unwrap_or_panic", EXPECTED, || {
15
15
  let env = Env::default();
16
16
  let _got: u32 = None::<u32>.unwrap_or_panic(&env, OwnableError::OwnerNotSet);