Skip to content

Commit 83f00c2

Browse files
committed
feat: add access control guide
1 parent 729a680 commit 83f00c2

9 files changed

Lines changed: 875 additions & 336 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
title: Access Control
3+
---
4+
5+
<Callout type="warn">
6+
The example code snippets used in this guide are experimental and have not been audited. They simply help exemplify usage of the OpenZeppelin Sui Package.
7+
</Callout>
8+
9+
The `access_control` module provides role-based authorization for Sui Move packages. It is the right choice when authority is not a single transferable object, or when several privileged actors need different permissions over shared protocol state.
10+
11+
## Use cases
12+
13+
Use `access_control` when your protocol needs:
14+
15+
- A root role controlled by a package One-Time Witness (OTW).
16+
- Operational roles such as admin, treasurer, operator, guardian, pauser, or keeper.
17+
- Role-admin relationships, where one role grants and revokes another.
18+
- Typed authorization proofs with `Auth<R>` for new functions.
19+
- Signature-preserving checks with `assert_role` for existing public functions.
20+
- Delayed transfer of the root role to governance or a multisig.
21+
22+
## Import
23+
24+
```move
25+
use openzeppelin_access::access_control::{Self, AccessControl, Auth};
26+
```
27+
28+
## Basic pattern
29+
30+
The central object is `AccessControl<RootRole>`. Your package defines the root role as its OTW, defines role marker types in the same module, and creates one registry during `init`.
31+
32+
```move
33+
module my_sui_app::roles;
34+
35+
use openzeppelin_access::access_control::{Self, AccessControl};
36+
37+
public struct ROLES has drop {}
38+
39+
public struct OperatorRole {}
40+
41+
const DEFAULT_ADMIN_DELAY_MS: u64 = 24 * 60 * 60 * 1_000;
42+
43+
fun init(otw: ROLES, ctx: &mut TxContext) {
44+
let mut access = access_control::new(otw, DEFAULT_ADMIN_DELAY_MS, ctx);
45+
access.grant_role<_, OperatorRole>(ctx.sender(), ctx);
46+
47+
transfer::public_share_object(access);
48+
}
49+
50+
public fun assert_operator(access: &AccessControl<ROLES>, ctx: &mut TxContext) {
51+
access.assert_role<_, OperatorRole>(ctx.sender());
52+
}
53+
```
54+
55+
<Callout type="warn">
56+
Do not renounce the last root role holder unless the protocol is intentionally being locked. The root role controls delayed root transfer and recovery of role administration.
57+
</Callout>
58+
59+
## Authorization styles
60+
61+
Use `Auth<R>` for new functions when you want authorization to be explicit in the function type and reusable across multiple calls in the same PTB.
62+
63+
Use `assert_role` when you are retrofitting an existing public function and changing the signature would be disruptive or invalid for package-upgrade compatibility.
64+
65+
## Learn more
66+
67+
For a full walkthrough of roles, `Auth<R>`, publishing, upgrades, and PTB usage, see the [Access Control guide](/contracts-sui/1.x/guides/access-control).
68+
69+
For function-level signatures, events, and errors, see the [Access API reference](/contracts-sui/1.x/api/access).

content/contracts-sui/1.x/access.mdx

Lines changed: 28 additions & 329 deletions
Large diffs are not rendered by default.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
title: Delayed Transfer
3+
---
4+
5+
<Callout type="warn">
6+
The example code snippets used in this guide are experimental and have not been audited. They simply help exemplify usage of the OpenZeppelin Sui Package.
7+
</Callout>
8+
9+
The `delayed_transfer` module provides an ownership-transfer wrapper that enforces a configurable minimum delay before a privileged object can transfer or unwrap.
10+
11+
## When to use it
12+
13+
Use `delayed_transfer` when:
14+
15+
- Your protocol requires on-chain lead time before authority changes.
16+
- Users, DAOs, or monitoring systems need a window to detect and respond.
17+
- The delay should be a reliable, inspectable commitment visible to anyone.
18+
19+
## Import
20+
21+
```move
22+
use openzeppelin_access::delayed_transfer;
23+
```
24+
25+
## Step 1: Wrap with a delay
26+
27+
```move
28+
module my_sui_app::treasury;
29+
30+
use openzeppelin_access::delayed_transfer;
31+
32+
public struct TreasuryCap has key, store { id: UID }
33+
34+
const MIN_DELAY_MS: u64 = 86_400_000; // 24 hours
35+
36+
/// Creates the wrapper and transfers it to ctx.sender() internally.
37+
public fun wrap_treasury_cap(cap: TreasuryCap, ctx: &mut TxContext) {
38+
delayed_transfer::wrap(cap, MIN_DELAY_MS, ctx.sender(), ctx);
39+
}
40+
```
41+
42+
`wrap` creates a `DelayedTransferWrapper<TreasuryCap>`, stores the capability inside it as a dynamic object field, and transfers the wrapper to the specified `recipient`. Unlike `two_step_transfer::wrap`, which returns the wrapper, `delayed_transfer::wrap` handles the transfer internally and has no return value.
43+
44+
## Step 2: Schedule a transfer
45+
46+
```move
47+
/// Called by the current wrapper owner.
48+
wrapper.schedule_transfer(new_owner_address, &clock, ctx);
49+
/// Emits TransferScheduled with execute_after_ms = clock.timestamp_ms() + min_delay_ms
50+
```
51+
52+
The `Clock` object is Sui's shared on-chain clock. The deadline is computed as `clock.timestamp_ms() + min_delay_ms` and stored in the wrapper. Only one action can be pending at a time; scheduling a second without canceling the first aborts with `ETransferAlreadyScheduled`.
53+
54+
During the delay window, the `TransferScheduled` event is visible on-chain. Monitoring systems, governance dashboards, or individual users watching the chain can detect the pending transfer and take action before it executes.
55+
56+
<Callout type="warn">
57+
The `recipient` in `schedule_transfer` must be a wallet address, not an object ID. If the wrapper is transferred to an object via transfer-to-object (TTO), both the wrapper and the capability inside it become permanently locked. The `delayed_transfer` module does not implement a `Receiving`-based retrieval mechanism, so there is no way to borrow, unwrap, or further transfer a wrapper that has been sent to an object.
58+
</Callout>
59+
60+
## Step 3: Wait, then execute
61+
62+
```move
63+
/// Callable after the delay window has passed.
64+
wrapper.execute_transfer(&clock, ctx);
65+
/// Emits OwnershipTransferred. Consumes the wrapper and delivers it to the recipient.
66+
```
67+
68+
`execute_transfer` consumes the wrapper by value. After this call, the wrapper has been transferred to the scheduled recipient and no longer exists in the caller's scope. Calling it before `execute_after_ms` aborts with `EDelayNotElapsed`.
69+
70+
## Scheduling an unwrap
71+
72+
The same delay enforcement applies to recovering the raw capability:
73+
74+
```move
75+
/// Schedule the unwrap.
76+
wrapper.schedule_unwrap(&clock, ctx);
77+
/// Emits UnwrapScheduled.
78+
79+
/// After the delay has elapsed, execute the unwrap.
80+
let treasury_cap = wrapper.unwrap(&clock, ctx);
81+
/// Emits UnwrapExecuted.
82+
```
83+
84+
## Canceling
85+
86+
The owner can cancel a pending action at any time before execution:
87+
88+
```move
89+
wrapper.cancel_schedule();
90+
```
91+
92+
This clears the pending slot immediately, allowing a new action to be scheduled.
93+
94+
## Borrowing without unwrapping
95+
96+
The module provides three ways to use the wrapped capability without changing ownership:
97+
98+
```move
99+
let cap_ref = wrapper.borrow();
100+
let cap_mut = wrapper.borrow_mut();
101+
let (cap, borrow_token) = wrapper.borrow_val();
102+
wrapper.return_val(cap, borrow_token);
103+
```
104+
105+
`borrow_val` uses a hot-potato guard, so the value must be returned to the same wrapper before the transaction ends.
106+
107+
## API Reference
108+
109+
For function-level signatures and error codes, see the [Access API reference](/contracts-sui/1.x/api/access#delayed_transfer).

0 commit comments

Comments
 (0)