Skip to content

Commit f4198d7

Browse files
committed
cancel admin rework
1 parent 6bc70bc commit f4198d7

15 files changed

+340
-147
lines changed

examples/nft-access-control/src/test.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ fn admin_transfer_works() {
214214
e.mock_all_auths();
215215

216216
// Owner (current admin) initiates the transfer
217-
client.transfer_admin_role(&owner, &new_admin);
217+
client.transfer_admin_role(&owner, &new_admin, &1000);
218218

219219
// New admin accepts
220220
client.accept_admin_transfer(&new_admin);
@@ -234,10 +234,10 @@ fn admin_transfer_cancelled() {
234234

235235
e.mock_all_auths();
236236

237-
client.transfer_admin_role(&owner, &new_admin);
237+
client.transfer_admin_role(&owner, &new_admin, &1000);
238238

239239
// Now cancel
240-
client.cancel_admin_transfer(&owner);
240+
client.transfer_admin_role(&owner, &new_admin, &0);
241241

242242
// New admin tries to accept—should panic
243243
client.accept_admin_transfer(&new_admin);
@@ -254,7 +254,7 @@ fn non_admin_cannot_initiate_transfer() {
254254

255255
e.mock_all_auths();
256256

257-
client.transfer_admin_role(&intruder, &new_admin);
257+
client.transfer_admin_role(&intruder, &new_admin, &1000);
258258
}
259259

260260
#[test]
@@ -268,7 +268,7 @@ fn non_recipient_cannot_accept_transfer() {
268268

269269
e.mock_all_auths();
270270

271-
client.transfer_admin_role(&owner, &new_admin);
271+
client.transfer_admin_role(&owner, &new_admin, &1000);
272272

273273
// Imposter tries to accept
274274
client.accept_admin_transfer(&imposter);

examples/nft-access-control/test_snapshots/test/non_recipient_cannot_accept_transfer.1.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
},
5555
"ext": "v0"
5656
},
57-
34560
57+
1000
5858
]
5959
],
6060
[

packages/contract-utils/access-control/src/access_control.rs

+22-44
Original file line numberDiff line numberDiff line change
@@ -144,41 +144,27 @@ pub trait AccessControl {
144144
/// * `e` - Access to Soroban environment.
145145
/// * `caller` - The address of the caller, must be the admin.
146146
/// * `new_admin` - The account to transfer the admin privileges to.
147+
/// * `live_until_ledger` - The ledger number at which the pending transfer
148+
/// expires. If `live_until_ledger` is `0`, the pending transfer is cancelled.
149+
/// `live_until_ledger` argument is implicitly bounded by the maximum allowed
150+
/// TTL extension for a temporary storage entry and specifying a higher value
151+
/// will cause the code to panic.
147152
///
148153
/// # Errors
149154
///
150155
/// * `AccessControlError::Unauthorized` - If the `caller` is not the admin.
156+
/// * `AccessControlError::NoPendingAdminTransfer` - If tried to cancel the
157+
/// pending admin transfer when there is no pending admin transfer.
151158
///
152159
/// # Events
153160
///
154161
/// * topics - `["admin_transfer_started", current_admin: Address]`
155-
/// * data - `[new_admin: Address]`
162+
/// * data - `[new_admin: Address, live_until_ledger: u32]`
156163
///
157164
/// # Security Warning
158165
///
159166
/// **IMPORTANT**: You MUST implement proper authorization in your contract.
160-
fn transfer_admin_role(e: &Env, caller: Address, new_admin: Address);
161-
162-
/// Cancels a pending admin role transfer.
163-
///
164-
/// # Arguments
165-
///
166-
/// * `e` - Access to Soroban environment.
167-
/// * `caller` - The address of the caller, must be the admin.
168-
///
169-
/// # Errors
170-
///
171-
/// * `AccessControlError::Unauthorized` - If the `caller` is not the admin.
172-
///
173-
/// # Events
174-
///
175-
/// * topics - `["admin_transfer_cancelled", admin: Address]`
176-
/// * data - `[]` (empty data)
177-
///
178-
/// # Security Warning
179-
///
180-
/// **IMPORTANT**: You MUST implement proper authorization in your contract.
181-
fn cancel_admin_transfer(e: &Env, caller: Address);
167+
fn transfer_admin_role(e: &Env, caller: Address, new_admin: Address, live_until_ledger: u32);
182168

183169
/// Completes the 2-step admin transfer.
184170
///
@@ -233,6 +219,7 @@ pub enum AccessControlError {
233219
AccountNotFound = 122,
234220
NoPendingAdminTransfer = 123,
235221
OutOfBounds = 124,
222+
InvalidLiveUntilLedger = 125,
236223
}
237224

238225
// ################## EVENTS ##################
@@ -304,30 +291,21 @@ pub fn emit_role_admin_changed(
304291
/// * `e` - Access to Soroban environment.
305292
/// * `current_admin` - The current admin initiating the transfer.
306293
/// * `new_admin` - The proposed new admin.
294+
/// * `live_until_ledger` - The ledger number at which the pending transfer will
295+
/// expire. If this value is `0`, it means the pending transfer is cancelled.
307296
///
308297
/// # Events
309298
///
310-
/// * topics - `["admin_transfer_started", current_admin: Address]`
311-
/// * data - `[new_admin: Address]`
312-
pub fn emit_admin_transfer_started(e: &Env, current_admin: &Address, new_admin: &Address) {
313-
let topics = (Symbol::new(e, "admin_transfer_started"), current_admin);
314-
e.events().publish(topics, new_admin);
315-
}
316-
317-
/// Emits an event when an admin transfer is cancelled.
318-
///
319-
/// # Arguments
320-
///
321-
/// * `e` - Access to Soroban environment.
322-
/// * `admin` - The admin who cancelled the transfer.
323-
///
324-
/// # Events
325-
///
326-
/// * topics - `["admin_transfer_cancelled", admin: Address]`
327-
/// * data - `[]` (empty data)
328-
pub fn emit_admin_transfer_cancelled(e: &Env, admin: &Address) {
329-
let topics = (Symbol::new(e, "admin_transfer_cancelled"), admin);
330-
e.events().publish(topics, ());
299+
/// * topics - `["admin_transfer", current_admin: Address]`
300+
/// * data - `[new_admin: Address, live_until_ledger: u32]`
301+
pub fn emit_admin_transfer(
302+
e: &Env,
303+
current_admin: &Address,
304+
new_admin: &Address,
305+
live_until_ledger: u32,
306+
) {
307+
let topics = (Symbol::new(e, "admin_transfer"), current_admin);
308+
e.events().publish(topics, (new_admin, live_until_ledger));
331309
}
332310

333311
/// Emits an event when an admin transfer is completed.

packages/contract-utils/access-control/src/lib.rs

+19-19
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,27 @@
55
//!
66
//! # Usage
77
//!
8-
//! There is a single overarching admin, and the admin has enough privileges to call
9-
//! any function given in the [`AccessControl`] trait.
8+
//! There is a single overarching admin, and the admin has enough privileges to
9+
//! call any function given in the [`AccessControl`] trait.
1010
//!
11-
//! This `admin` must be set in the constructor of the contract. Else, none of the methods
12-
//! exposed by this module will work. You can follow the `nft-access-control` example.
11+
//! This `admin` must be set in the constructor of the contract. Else, none of
12+
//! the methods exposed by this module will work. You can follow the
13+
//! `nft-access-control` example.
1314
//!
14-
//! Each role can have an `admin role` specified for it. For example, if you create 2 roles:
15+
//! Each role can have an `admin role` specified for it. For example, if you
16+
//! create 2 roles:
1517
//! - minter
1618
//! - mint_admins
1719
//!
18-
//! You can assign the role `mint_admins` as the admin role of the `minter` role group.
19-
//! And this will allow accounts with `mint_admins` role, to grant and revoke the roles of
20-
//! `minter` roles.
20+
//! You can assign the role `mint_admins` as the admin role of the `minter` role
21+
//! group. And this will allow accounts with `mint_admins` role, to grant and
22+
//! revoke the roles of `minter` roles.
2123
//!
22-
//! One can create as many roles as they want, and create a chain of command structure if
23-
//! they want to with this approach.
24+
//! One can create as many roles as they want, and create a chain of command
25+
//! structure if they want to with this approach.
2426
//!
25-
//! If you need even more granular control over which roles can do what, you can introduce
26-
//! your own business logic, and annotate it with our macro:
27+
//! If you need even more granular control over which roles can do what, you can
28+
//! introduce your own business logic, and annotate it with our macro:
2729
//!
2830
//! ```rust
2931
//! #[has_role(caller, "minter_admin")]
@@ -39,15 +41,13 @@ mod storage;
3941

4042
pub use crate::{
4143
access_control::{
42-
emit_admin_transfer_cancelled, emit_admin_transfer_completed, emit_admin_transfer_started,
43-
emit_role_admin_changed, emit_role_granted, emit_role_revoked, AccessControl,
44-
AccessControlError,
44+
emit_admin_transfer, emit_admin_transfer_completed, emit_role_admin_changed,
45+
emit_role_granted, emit_role_revoked, AccessControl, AccessControlError,
4546
},
4647
storage::{
47-
accept_admin_transfer, add_to_role_enumeration, cancel_admin_transfer, ensure_role,
48-
get_admin, get_role_admin, get_role_member, get_role_member_count, grant_role, has_role,
49-
remove_from_role_enumeration, renounce_role, revoke_role, set_role_admin,
50-
transfer_admin_role, AccessControlStorageKey,
48+
accept_admin_transfer, add_to_role_enumeration, ensure_role, get_admin, get_role_admin,
49+
get_role_member, get_role_member_count, grant_role, has_role, remove_from_role_enumeration,
50+
renounce_role, revoke_role, set_role_admin, transfer_admin_role, AccessControlStorageKey,
5151
},
5252
};
5353

packages/contract-utils/access-control/src/storage.rs

+34-38
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
use soroban_sdk::{contracttype, panic_with_error, Address, Env, Symbol};
2-
use stellar_constants::{
3-
ADMIN_TRANSFER_THRESHOLD, ADMIN_TRANSFER_TTL, ROLE_EXTEND_AMOUNT, ROLE_TTL_THRESHOLD,
4-
};
2+
use stellar_constants::{ROLE_EXTEND_AMOUNT, ROLE_TTL_THRESHOLD};
53

64
use crate::{
7-
emit_admin_transfer_cancelled, emit_admin_transfer_completed, emit_admin_transfer_started,
8-
emit_role_admin_changed, emit_role_granted, emit_role_revoked, AccessControlError,
5+
emit_admin_transfer, emit_admin_transfer_completed, emit_role_admin_changed, emit_role_granted,
6+
emit_role_revoked, AccessControlError,
97
};
108

119
#[contracttype]
@@ -223,56 +221,54 @@ pub fn renounce_role(e: &Env, caller: &Address, role: &Symbol) {
223221
/// * `e` - Access to Soroban environment.
224222
/// * `caller` - The address of the caller, must be the admin.
225223
/// * `new_admin` - The account to transfer the admin privileges to.
224+
/// * `live_until_ledger` - The ledger number at which the pending transfer
225+
/// expires. If `live_until_ledger` is `0`, the pending transfer is cancelled.
226+
/// `live_until_ledger` argument is implicitly bounded by the maximum allowed
227+
/// TTL extension for a temporary storage entry and specifying a higher value
228+
/// will cause the code to panic.
226229
///
227230
/// # Errors
228231
///
229232
/// * `AccessControlError::Unauthorized` - If the `caller` is not the admin.
233+
/// * `AccessControlError::NoPendingAdminTransfer` - If tried to cancel the
234+
/// pending admin transfer when there is no pending admin transfer.
230235
///
231236
/// # Events
232237
///
233238
/// * topics - `["admin_transfer_started", current_admin: Address]`
234-
/// * data - `[new_admin: Address]`
235-
pub fn transfer_admin_role(e: &Env, caller: &Address, new_admin: &Address) {
239+
/// * data - `[new_admin: Address, live_until_ledger: u32]`
240+
pub fn transfer_admin_role(e: &Env, caller: &Address, new_admin: &Address, live_until_ledger: u32) {
236241
if caller != &get_admin(e) {
237242
panic_with_error!(e, AccessControlError::Unauthorized);
238243
}
239244

240-
// Store the new admin address in temporary storage
241-
e.storage().temporary().set(&AccessControlStorageKey::PendingAdmin, new_admin);
242-
e.storage().temporary().extend_ttl(
243-
&AccessControlStorageKey::PendingAdmin,
244-
ADMIN_TRANSFER_THRESHOLD,
245-
ADMIN_TRANSFER_TTL,
246-
);
247-
248-
emit_admin_transfer_started(e, caller, new_admin);
249-
}
245+
let key = AccessControlStorageKey::PendingAdmin;
250246

251-
/// Cancels a pending admin role transfer if it is not accepted yet.
252-
/// This can only be called by the current admin.
253-
///
254-
/// # Arguments
255-
///
256-
/// * `e` - Access to Soroban environment.
257-
/// * `caller` - The address of the caller, must be the admin.
258-
///
259-
/// # Errors
260-
///
261-
/// * `AccessControlError::Unauthorized` - If the `caller` is not the admin.
262-
///
263-
/// # Events
264-
///
265-
/// * topics - `["admin_transfer_cancelled", admin: Address]`
266-
/// * data - `[]` (empty data)
267-
pub fn cancel_admin_transfer(e: &Env, caller: &Address) {
268-
if caller != &get_admin(e) {
269-
panic_with_error!(e, AccessControlError::Unauthorized);
247+
if live_until_ledger == 0 {
248+
let Some(pending_new_admin) = e.storage().temporary().get::<_, Address>(&key) else {
249+
panic_with_error!(e, AccessControlError::NoPendingAdminTransfer)
250+
};
251+
e.storage().temporary().remove(&key);
252+
253+
emit_admin_transfer(e, caller, &pending_new_admin, live_until_ledger);
254+
return;
270255
}
271256

272-
e.storage().temporary().remove(&AccessControlStorageKey::PendingAdmin);
257+
let current_ledger = e.ledger().sequence();
258+
259+
if live_until_ledger < current_ledger {
260+
panic_with_error!(e, AccessControlError::InvalidLiveUntilLedger);
261+
}
262+
263+
let live_for = live_until_ledger - current_ledger;
264+
265+
// Store the new admin address in temporary storage
266+
e.storage().temporary().set(&key, new_admin);
267+
e.storage().temporary().extend_ttl(&key, live_for, live_for);
273268

274-
emit_admin_transfer_cancelled(e, caller);
269+
emit_admin_transfer(e, caller, new_admin, live_until_ledger);
275270
}
271+
// TODO: test for live_until_ledger = 0 when there is no pending admin
276272

277273
/// Completes the 2-step admin transfer.
278274
///

0 commit comments

Comments
 (0)