Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5103f43
update existing apis
andrew-fleming Jun 1, 2026
c7b4f78
add shielded access
andrew-fleming Jun 1, 2026
a1036be
add latest version file
andrew-fleming Jun 1, 2026
1347959
add tmp addme
andrew-fleming Jun 1, 2026
7f59d34
add shielded access control page
andrew-fleming Jun 4, 2026
8be9dec
fix import paths
andrew-fleming Jun 4, 2026
9d02986
refactor AccessControl module
andrew-fleming Jun 4, 2026
72bef5a
update import paths
andrew-fleming Jun 4, 2026
44481a8
update circuit sigs in snippets
andrew-fleming Jun 4, 2026
1c6ecb9
update circuit sigs, replace submodules install with npm
andrew-fleming Jun 4, 2026
d95defd
refactor multitoken module docs
andrew-fleming Jun 4, 2026
11ec76a
update circuit sigs in nft module doc
andrew-fleming Jun 4, 2026
24ac0b9
refactor ownable documentation
andrew-fleming Jun 4, 2026
ef1e13f
update circuit sigs in snippet, fix import path
andrew-fleming Jun 4, 2026
9ce2336
update import path
andrew-fleming Jun 4, 2026
806bf7d
bump version constants
andrew-fleming Jun 4, 2026
e1bd792
fix file names
andrew-fleming Jun 4, 2026
53c765d
update links with improved file nm casing
andrew-fleming Jun 4, 2026
f9f3c43
update llms txt links
andrew-fleming Jun 4, 2026
ec727e5
update links
andrew-fleming Jun 4, 2026
f39f493
change zswap key to bytes<32>
andrew-fleming Jun 4, 2026
ecac001
fix callout
andrew-fleming Jun 4, 2026
d7fc3d1
fix fmt
andrew-fleming Jun 4, 2026
f7a0a56
refactor multi token api
andrew-fleming Jun 4, 2026
b6cc200
refactor nft api
andrew-fleming Jun 4, 2026
03f7d5f
remove unused links
andrew-fleming Jun 4, 2026
97914ae
fix links
andrew-fleming Jun 4, 2026
ca27d0c
add missing internal circuits, fix kind on pure and internal circuits
andrew-fleming Jun 4, 2026
8fc7489
Apply suggestions from code review
andrew-fleming Jun 4, 2026
ddf19ab
fix import path
andrew-fleming Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ title: AccessControl
[Role-Based Access Control (RBAC)]: https://en.wikipedia.org/wiki/Role-based_access_control
[principle of least privilege]: https://en.wikipedia.org/wiki/Principle_of_least_privilege

[FungibleToken]: ./fungibleToken.mdx
[FungibleToken]: ./fungible-token.mdx
[Ownable]: ./ownable.mdx
[Initializable]: ./security#initializable
[AccessControl]: api/accessControl
[AccessControl]: api/access-control
[ShieldedAccessControl]: ./shielded-access-control.mdx

[assertOnlyRole]: api/accessControl#AccessControl-assertOnlyRole
[grantRole]: api/accessControl#AccessControl-grantRole
[_grantRole]: api/accessControl#AccessControl-_grantRole
[_unsafeGrantRole]: api/accessControl#AccessControl-_unsafeGrantRole
[revokeRole]: api/accessControl#AccessControl-revokeRole
[_setRoleAdmin]: api/accessControl#AccessControl-_setRoleAdmin
[assertOnlyRole]: api/access-control#AccessControl-assertOnlyRole
[grantRole]: api/access-control#AccessControl-grantRole
[_grantRole]: api/access-control#AccessControl-_grantRole
[_unsafeGrantRole]: api/access-control#AccessControl-_unsafeGrantRole
[revokeRole]: api/access-control#AccessControl-revokeRole
[_setRoleAdmin]: api/access-control#AccessControl-_setRoleAdmin
[computeAccountId]: api/access-control#AccessControl-computeAccountId

This module provides a role-based access control mechanism,
where roles can be used to represent a set of permissions providing the flexibility to create different levels of account authorization.

Roles can be enforced using the [assertOnlyRole] circuit.
Separately, you will be able to define rules for how accounts can be granted a role, have it revoked, and more.

<Callout type='info'>
For a privacy-preserving variant that hides role holder identities on-chain, see [ShieldedAccessControl].
</Callout>

## Role-Based Access Control

While the simplicity of _ownership_ can be useful for simple systems or quick prototyping,
Expand All @@ -40,22 +46,34 @@ Separately, you will be able to define rules for how accounts can be granted a r
Most software uses access control systems that are role-based:
some users are regular users, some may be supervisors or managers, and a few will often have administrative privileges.

### Identity Model

AccessControl uses a witness-derived identity scheme. Each caller proves knowledge of a secret key
by injecting it via the `wit_AccessControlSK` witness. The module computes an account identifier
as `persistentHash(secretKey)`, which is a commitment that hides the key while providing a stable,
pseudonymous on-chain identity.

The same secret key produces the same identity across all contracts (analogous to Solidity's `msg.sender`).
Users who desire cross-contract unlinkability can use different secret keys per contract at the wallet layer.

To derive an account identifier off-chain (e.g. before passing it to a grant operation), use [computeAccountId].

### Using `AccessControl`

The Compact contracts library provides `AccessControl` for implementing role-based access control.
Its usage is straightforward: for each role that you want to define,
you will create a new role identifier that is used to grant, revoke, and check if an account has that role.

Heres a simple example of using `AccessControl` with [FungibleToken] to define a 'minter' role,
Here's a simple example of using `AccessControl` with [FungibleToken] to define a 'minter' role,
which allows accounts that have this role to create new tokens:

```ts
pragma language_version >= {{compact_language_version}};

import CompactStandardLibrary;
import "./compact-contracts/node_modules/@openzeppelin-compact/contracts/src/access/AccessControl"
import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"
prefix AccessControl_;
import "./compact-contracts/node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken"
import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"
prefix FungibleToken_;

export sealed ledger MINTER_ROLE: Bytes<32>;
Expand All @@ -67,14 +85,14 @@ constructor(
name: Opaque<"string">,
symbol: Opaque<"string">,
decimals: Uint<8>,
minter: Either<ZswapCoinPublicKey, ContractAddress>
minter: Either<Bytes<32>, ContractAddress>
) {
FungibleToken_initialize(name, symbol, decimals);
MINTER_ROLE = persistentHash<Bytes<32>>(pad(32, "MINTER_ROLE"));
AccessControl__grantRole(MINTER_ROLE, minter);
}

export circuit mint(recipient: Either<ZswapCoinPublicKey, ContractAddress>, value: Uint<128>): [] {
export circuit mint(recipient: Either<Bytes<32>, ContractAddress>, value: Uint<128>): [] {
AccessControl_assertOnlyRole(MINTER_ROLE);
FungibleToken__mint(recipient, value);
}
Expand All @@ -85,7 +103,7 @@ Make sure you fully understand how [AccessControl] works before using it on your
or copy-pasting the examples from this guide.
</Callout>

While clear and explicit, this isnt anything we wouldnt have been able to achieve with [Ownable].
While clear and explicit, this isn't anything we wouldn't have been able to achieve with [Ownable].
Indeed, where `AccessControl` shines is in scenarios where granular permissions are required,
which can be implemented by defining _multiple_ roles.

Expand All @@ -96,9 +114,9 @@ Let's augment our FungibleToken example by also defining a 'burner' role, which
pragma language_version >= {{compact_language_version}};

import CompactStandardLibrary;
import "./compact-contracts/node_modules/@openzeppelin-compact/contracts/src/access/AccessControl"
import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"
prefix AccessControl_;
import "./compact-contracts/node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken"
import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"
prefix FungibleToken_;

export sealed ledger MINTER_ROLE: Bytes<32>;
Expand All @@ -111,8 +129,8 @@ constructor(
name: Opaque<"string">,
symbol: Opaque<"string">,
decimals: Uint<8>,
minter: Either<ZswapCoinPublicKey, ContractAddress>,
burner: Either<ZswapCoinPublicKey, ContractAddress>
minter: Either<Bytes<32>, ContractAddress>,
burner: Either<Bytes<32>, ContractAddress>
) {
FungibleToken_initialize(name, symbol, decimals);
MINTER_ROLE = persistentHash<Bytes<32>>(pad(32, "MINTER_ROLE"));
Expand All @@ -121,12 +139,12 @@ constructor(
AccessControl__grantRole(BURNER_ROLE, burner);
}

export circuit mint(recipient: Either<ZswapCoinPublicKey, ContractAddress>, value: Uint<128>): [] {
export circuit mint(recipient: Either<Bytes<32>, ContractAddress>, value: Uint<128>): [] {
AccessControl_assertOnlyRole(MINTER_ROLE);
FungibleToken__mint(recipient, value);
}

export circuit burn(recipient: Either<ZswapCoinPublicKey, ContractAddress>, value: Uint<128>): [] {
export circuit burn(recipient: Either<Bytes<32>, ContractAddress>, value: Uint<128>): [] {
AccessControl_assertOnlyRole(BURNER_ROLE);
FungibleToken__burn(recipient, value);
}
Expand All @@ -147,13 +165,13 @@ But what if we later want to grant the 'minter' role to additional accounts?

By default, **accounts with a role cannot grant it or revoke it from other accounts**:
all having a role does is making the `hasRole` check pass.
To grant and revoke roles dynamically, you will need help from the _roles admin_.
To grant and revoke roles dynamically, you will need help from the _role's admin_.

Every role has an associated admin role,
which grants permission to call the [grantRole] and [revokeRole] circuits.
A role can be granted or revoked by using these if the calling account has the corresponding admin role.
Multiple roles may have the same admin role to make management easier.
A roles admin can even be the same role itself,
A role's admin can even be the same role itself,
which would cause accounts with that role to be able to also grant and revoke it.

This mechanism can be used to create complex permissioning structures resembling organizational charts,
Expand All @@ -166,15 +184,15 @@ unless [_setRoleAdmin] is used to select a new admin role.
Since it is the admin for all roles by default,
and in fact it is also its own admin, this role carries significant risk.

Lets take a look at the FungibleToken example, this time taking advantage of the default admin role:
Let's take a look at the FungibleToken example, this time taking advantage of the default admin role:

```ts
pragma language_version >= {{compact_language_version}};

import CompactStandardLibrary;
import "./compact-contracts/node_modules/@openzeppelin-compact/contracts/src/access/AccessControl"
import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"
prefix AccessControl_;
import "./compact-contracts/node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken"
import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"
prefix FungibleToken_;

export sealed ledger MINTER_ROLE: Bytes<32>;
Expand All @@ -187,31 +205,36 @@ constructor(
name: Opaque<"string">,
symbol: Opaque<"string">,
decimals: Uint<8>,
defaultAdmin: Either<Bytes<32>, ContractAddress>
) {
FungibleToken_initialize(name, symbol, decimals);
MINTER_ROLE = persistentHash<Bytes<32>>(pad(32, "MINTER_ROLE"));
BURNER_ROLE = persistentHash<Bytes<32>>(pad(32, "BURNER_ROLE"));
// Grant the contract deployer the default admin role: it will be able
// to grant and revoke any roles
AccessControl__grantRole(AccessControl_DEFAULT_ADMIN_ROLE, left<ZswapCoinPublicKey,ContractAddress>(ownPublicKey()));
// Grant the default admin role to the provided account identifier.
// This account will be able to grant and revoke any roles.
// The account identifier should be derived off-chain using computeAccountId(secretKey).
AccessControl__grantRole(AccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin);
}

export circuit mint(recipient: Either<ZswapCoinPublicKey, ContractAddress>, value: Uint<128>): [] {
export circuit mint(recipient: Either<Bytes<32>, ContractAddress>, value: Uint<128>): [] {
AccessControl_assertOnlyRole(MINTER_ROLE);
FungibleToken__mint(recipient, value);
}

export circuit burn(recipient: Either<ZswapCoinPublicKey, ContractAddress>, value: Uint<128>): [] {
export circuit burn(recipient: Either<Bytes<32>, ContractAddress>, value: Uint<128>): [] {
AccessControl_assertOnlyRole(BURNER_ROLE);
FungibleToken__burn(recipient, value);
}

```

Note that, unlike the previous examples, no accounts are granted the 'minter' or 'burner' roles.
However, because those roles' admin role is the default admin role, and _that_ role was granted to `ownPublicKey()`,
However, because those roles' admin role is the default admin role, and _that_ role was granted to `defaultAdmin`,
that same account can call [grantRole] to give minting or burning permission, and [revokeRole] to remove it.

The `defaultAdmin` account identifier should be derived off-chain using `AccessControl_computeAccountId(secretKey)`,
where `secretKey` is a 32-byte cryptographically secure random value held by the deployer.

Dynamic role allocation is often a desirable property,
for example in systems where trust in a participant may vary over time.
It can also be used to support use cases such as KYC,
Expand All @@ -220,7 +243,7 @@ or may be prohibitively expensive to include in a single transaction.

### Experimental features

This module offers an experimental circuit that allow access control permissions to be granted to contract addresses [_unsafeGrantRole].
This module offers an experimental circuit that allows access control permissions to be granted to contract addresses [_unsafeGrantRole].

Note that the circuit name is very explicit ("unsafe") with this experimental circuit.
Until contract-to-contract calls are supported,
Expand Down
Loading