Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 97 additions & 0 deletions content/canton/get-started.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
title: Get Started
---

This guide covers the prerequisites and the basic project wiring for building on Canton with OpenZeppelin's Daml packages.

<Callout type="info">
These packages target the Canton 3.4.x baseline and are under active
development. Pin exact versions from the package manifests in
[`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts)
rather than copying version numbers from this page.
</Callout>

## Prerequisites

* **JDK 21**: the Daml toolchain runs on Java. Install a recent OpenJDK 21 build and make sure `JAVA_HOME` points at it.
* **DPM (Daml Package Manager)**: Digital Asset's package manager and SDK installer. Install it, then use it to provision the Daml SDK / Canton baseline:

```bash
curl https://get.digitalasset.com/install/install.sh | sh
dpm install 3.4.11 # or the baseline pinned in your project's daml.yaml
```

* **A Daml project**: a directory with a `daml.yaml` (single package) or a `multi-package.yaml` (workspace of packages). New to Daml? Start with the [Daml documentation](https://docs.digitalasset.com/) and the [Canton Network overview](https://www.canton.network/).

## Add OpenZeppelin packages

OpenZeppelin's Canton library is distributed as Daml packages — each primitive is its own DAR, so you add only the ones you need.

**1. Get the DARs.** Clone [`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts) and build the library packages:

```bash
git clone https://github.com/OpenZeppelin/canton-contracts.git
cd canton-contracts
dpm build --all
```

Each package's DAR lands in its own `.daml/dist/` directory (for example `pausable/.daml/dist/oz-pausable-0.1.0.dar`).

**2. Declare them as data-dependencies.** In your project's `daml.yaml`, reference the built DARs under `data-dependencies` (not `dependencies`, which is for the SDK's own libraries):

```yaml
sdk-version: 3.4.11
name: my-app
source: daml
version: 0.1.0
dependencies:
- daml-prim
- daml-stdlib
data-dependencies:
- ../canton-contracts/access-control/.daml/dist/oz-access-control-0.1.0.dar
- ../canton-contracts/ownable/.daml/dist/oz-ownable-0.1.0.dar
- ../canton-contracts/pausable/.daml/dist/oz-pausable-0.1.0.dar
build-options:
- --target=2.1
```
Adjust the paths to wherever you cloned the repo, and pin the exact versions from its package manifests. If you only need one primitive, list only that DAR.
**3. Import and build.** Import the modules you declared and build your project:
```daml
import OpenZeppelin.Pausable
```

```bash
# from your project root, with DPM on PATH
dpm build
```

Refer to each component's page for the module path to import and the templates and interfaces it exposes:

* [Access Control](/canton/library/access-control)
* [Ownable](/canton/library/ownable)
* [Pausable](/canton/library/pausable)

## Test against a local Canton ledger

Daml Script tests run in-memory by default, but you can point the same scripts at a real local Canton ledger over gRPC — useful for validating time semantics, party visibility, and DAR uploads before targeting a shared network:

```bash
# start a local sandbox with your DAR uploaded
dpm sandbox --dar .daml/dist/<your-package>.dar

# run a script against it over the Ledger API
dpm script --dar .daml/dist/<your-package>.dar \
--script-name My.Module:myScript \
--ledger-host localhost --ledger-port 6865
```

If your scripts control ledger time with `setTime`, start the sandbox with `--static-time` and pass `--static-time` to `dpm script` as well — both default to wall-clock time, and ledger time only moves forward.

## Explore further

* The **[Settlement (CIP-112)](/canton/settlement)** primitive shows how to compose the library into an atomic, value-moving settlement flow.
* The **[Reference Implementations](/canton/reference-implementations)** walk through complete application blueprints built on these pieces.
* The [Canton Improvement Proposals (CIPs)](https://github.com/canton-foundation/cips) repository holds the ecosystem standards these packages align with.
38 changes: 38 additions & 0 deletions content/canton/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: OpenZeppelin for Canton
---

OpenZeppelin is building a suite of secure, reusable building blocks for the [Canton Network](https://www.canton.network/), the privacy-enabled network of applications built on [Daml](https://www.digitalasset.com/developers). This section is the home for OpenZeppelin's Canton documentation: the general-purpose library, the settlement primitive, and the Reference Implementations that show how they fit together.

<Callout type="info">
The Canton ecosystem stack is under active development. Components are labelled
by maturity throughout these docs. Treat everything marked *experimental* as
a preview surface: interfaces may change, and it is not yet audited or intended
for production use.
</Callout>

## Library

Foundational Daml modules that other packages and applications compose on top of — see the [Library overview](/canton/library) for the design philosophy. These live in [`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts).

* **[Access Control](/canton/library/access-control)**: Role-based authorization for Daml workflows, with an admin role that grants and revokes other roles.
* **[Ownable](/canton/library/ownable)**: A single-owner authorization primitive for privileged actions, with ownership transfer.
* **[Pausable](/canton/library/pausable)**: An emergency stop mechanism that lets an authorized party halt and resume sensitive choices.

## Settlement

* **[Settlement (CIP-112)](/canton/settlement)**: An experimental, interface-shaped settlement engine for atomic multi-leg, value-moving delivery-versus-payment, aligned with the Canton Token Standard.

## Reference Implementations

* **[Reference Implementations](/canton/reference-implementations)**: End-to-end application blueprints (DEX, Lending, Cross-Chain Stablecoin, Confidential Auction) that demonstrate how the library and settlement primitives compose into real applications.

## Where to start

New to the Canton stack? Head to **[Get Started](/canton/get-started)** for prerequisites, toolchain setup, and how to add OpenZeppelin packages to a Daml project.

---

<small>
Canton is a registered trademark of Digital Asset (Switzerland) GmbH. Digital Asset is not affiliated with, and has not sponsored or endorsed, this documentation.
</small>
102 changes: 102 additions & 0 deletions content/canton/library/access-control.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: Access Control
---

Access Control is a standalone, token-agnostic role-based access control (RBAC) substrate for Daml. It is the Daml analogue of OpenZeppelin's `AccessControl.sol`: an authority (the `admin`, which plays the role of Solidity's `DEFAULT_ADMIN_ROLE` holder) grants named roles to accounts, and gated operations require that the caller holds the right role.

The package is independent: it has no dependency on [Ownable](/canton/library/ownable) or [Pausable](/canton/library/pausable), so a project that wants only RBAC imports only this one package.

<Callout type="warn">
Version 0.x, unstable, not yet public API. Interfaces may change before a 1.0
release. Source: [`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts).
</Callout>

```daml
import OpenZeppelin.AccessControl
```

## The Daml model

Two design choices differ from Solidity, and both are deliberate.

**Roles are `Text` identifiers.** Solidity identifies roles with `bytes32` constants (for example `keccak256("MINTER_ROLE")`). Daml templates are monomorphic (a template field cannot be a type variable), so a reusable role primitive stores `role : Text`. This keeps the library generic: any contract can reuse it. A consumer that wants compile-time exhaustiveness layers its own closed role type on top with a thin `roleId : MyRole -> Text` wrapper.

**A grant is a bearer credential, not a global map.** Daml-LF 2.1 has no contract keys, so there is no global role table to look up. Instead a `RoleGrant` is a contract signed by `admin` that names one `account`. Possession of a valid grant is the authorization: a gated choice fetches the grant the caller presents and checks that it is admin-signed, names the caller, and carries the required role. Because the grant is admin-signed it cannot be forged, and because it names a specific account the worst an adversary can do is present a grant they do not hold and fail their own authorization.

## Templates

### `RoleGrant`

A role granted by `admin` to `account`. Possession is authorization.

| Field | Type | Description |
| --- | --- | --- |
| `admin` | `Party` | The role authority (the `DEFAULT_ADMIN_ROLE` analogue) that issued the grant. |
| `account` | `Party` | The party granted the role. |
| `role` | `Text` | The role identifier, for example `"MINTER_ROLE"`. |

Signatory `admin`, observer `account`.

* **`RoleGrant_Renounce`**: the grantee gives up its own role, archiving the grant (the `renounceRole` analogue). Self-only by construction, since `account` is the controller.

### `RoleAdmin`

The role-administration authority that mints and revokes grants. Two paths coexist:

* The root path, controlled by `admin`, where the `DEFAULT_ADMIN_ROLE` holder manages any role directly.
* The role-admin path, controlled by an arbitrary `caller` (the `getRoleAdmin` analogue), where a delegate presents a grant for a caller-supplied `adminRole` and may then grant or revoke the target role. The new grant is still `admin`-signed because the choice runs with the contract's authority, so a delegate administers roles without holding the admin key.

| Choice | Controller | Result | Description |
| --- | --- | --- | --- |
| `RoleAdmin_GrantRole` | `admin` | `ContractId RoleGrant` | Grant `role` to `account`. |
| `RoleAdmin_RevokeRole` | `admin` | `()` | Revoke a grant this admin issued. |
| `RoleAdmin_GrantRoleAs` | `caller` | `ContractId RoleGrant` | Delegate grant: `caller` grants `role` by presenting its own grant for `adminRole`. |
| `RoleAdmin_RevokeRoleAs` | `caller` | `()` | Delegate revoke, gated the same way. |
| `RoleAdmin_BeginDefaultAdminTransfer` | `admin` | `ContractId DefaultAdminTransferOffer` | Begin a two-step, timelocked handoff of a role. |

The library hard-codes no role-to-admin graph. `adminRole` is a caller-supplied `Text`: a consumer that wants a fixed hierarchy computes which admin role gates which target role in its own code and passes the result in.

### `DefaultAdminTransferOffer`

A pending, timelocked transfer of a role to `newAdmin`, the `AccessControlDefaultAdminRules` analogue. It is an offer / accept handshake plus a ledger-time gate: `newAdmin` cannot accept before `effectiveTime`, and `admin` can cancel within the window. Pass the `DEFAULT_ADMIN_ROLE` id as `role` for a default-admin handoff.

* **`DefaultAdminTransferOffer_Accept`** (controller `newAdmin`): accept once the timelock has elapsed; grants `role` by creating a `RoleGrant` for `newAdmin`.
* **`DefaultAdminTransferOffer_Cancel`** (controller `admin`): cancel the pending handoff (the `cancelDefaultAdminTransfer` analogue).

## Helper functions

* **`requireRole : Party -> Text -> Party -> RoleGrant -> Update ()`**: the `requireRole` modifier analogue. Call it at the top of a gated choice. It asserts the grant is issued by the expected `admin`, names the `caller` (anti-impersonation), and carries the required `role`.
* **`hasRole : Party -> Text -> Party -> RoleGrant -> Bool`**: the pure predicate form, for callers that already hold the fetched grant.
* **`requireTimelockElapsed : Time -> Update ()`**: asserts the current ledger time has reached a given effective time. Shared by `DefaultAdminTransferOffer` and reusable by consumers running their own timelocked handoff.

## Gating an operation

A consumer's privileged choice fetches the grant the caller presents and validates it before doing work:

```daml
nonconsuming choice Mint : ContractId Token
with
caller : Party
grantCid : ContractId RoleGrant
amount : Int
controller caller
do
grant <- fetch grantCid
requireRole caller "MINTER_ROLE" admin grant
create Token with owner = caller; amount
```

## Errors

| Message | When |
| --- | --- |
| `AccessControl: grant admin is not the expected authority` | The presented grant was issued by a different admin. |
| `AccessControl: grant does not name the caller (impersonation)` | The grant does not name the calling party. |
| `AccessControl: grant does not carry the required role` | The grant is for a different role. |
| `AccessControl: default-admin handoff nominee is the current admin` | A begin-transfer named the current admin. |
| `AccessControl: default-admin transfer timelock has not elapsed` | Acceptance was attempted before `effectiveTime`. |

## Related

* [Ownable](/canton/library/ownable), for the single-owner case.
* [Pausable](/canton/library/pausable), for an emergency stop.
38 changes: 38 additions & 0 deletions content/canton/library/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: Library
---

The OpenZeppelin library for Canton is a set of foundational Daml modules that other packages and applications compose on top of. It brings the patterns developers know from OpenZeppelin's Solidity contracts — role-based access control, ownership, emergency stop — to Daml, adapted to Canton's authorization and privacy model rather than translated literally.

<Callout type="warn">
Version 0.x, unstable, not yet public API. Interfaces may change before a 1.0
release. Source: [`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts).
</Callout>

## Packages

Each primitive ships as its **own independent Daml package** — its own DAR, with no dependency on the others — so a consumer imports only what it needs:

* **[Access Control](/canton/library/access-control)**: role-based authorization, the `AccessControl.sol` analogue. An admin grants named roles as bearer credentials; gated choices verify the presented grant.
* **[Ownable](/canton/library/ownable)**: single-owner authorization for privileged actions, the `Ownable2Step.sol` analogue. Ownership transfer is a two-step handshake, because in Daml a new owner is a signatory and cannot be bound unilaterally.
* **[Pausable](/canton/library/pausable)**: an emergency stop, the `Pausable.sol` analogue. An authorized party halts and resumes sensitive choices; pause is origination control on a keyless ledger.

## Design philosophy

**Independence at the package boundary.** Daml has no inheritance, and its unit of reuse is the DAR. The library therefore delivers OpenZeppelin's decoupled-module promise at the package level: three packages, three DARs, zero cross-dependencies. A project that only needs pausing imports only `oz-pausable`.

**Adapted, not transliterated.** Where Daml's model differs from the EVM — monomorphic templates, no global state lookups, signatory-based authority — each primitive adopts the idiomatic Daml shape and documents the divergence on its page.

**Script-free libraries.** The shipped packages carry no `daml-script` dependency; tests and example consumers live in a separate test package. Your production DAR stays lean.

## Using the library

Add the package(s) you need as data-dependencies of your Daml project and import the module:

```daml
import OpenZeppelin.AccessControl
import OpenZeppelin.Ownable
import OpenZeppelin.Pausable
```

See [Get Started](/canton/get-started) for toolchain setup, and each package page for its templates, choices, and usage patterns.
77 changes: 77 additions & 0 deletions content/canton/library/ownable.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
title: Ownable
---

Ownable is a standalone, token-agnostic single-owner primitive for Daml. It is the analogue of OpenZeppelin's `Ownable.sol` and `Ownable2Step.sol`: exactly one `owner` party at a time, with an explicit transfer handshake and a renounce path.

The package is independent: it carries no role type and has no dependency on [Access Control](/canton/library/access-control), so a project that wants only "an owner" imports only this one package.

<Callout type="warn">
Version 0.x, unstable, not yet public API. Interfaces may change before a 1.0
release. Source: [`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts).
</Callout>

```daml
import OpenZeppelin.Ownable
```

## Why transfer is always two-step in Daml

In Solidity `transferOwnership(newOwner)` is a single call: the old owner writes the new owner into storage. That is not possible in Daml. The owner is a signatory of the `Ownership` contract, and a contract cannot be created without the authorization of every signatory, so the current owner cannot unilaterally bind a new owner.

Transfer is therefore a handshake: the owner makes an `OwnershipOffer`, and the prospective owner accepts it. This is exactly why OpenZeppelin recommends `Ownable2Step` on other ecosystems, and here it is the only correct shape, not merely the safer one. It also guarantees ownership never lands on a party that has not actively agreed to hold it.

During a pending offer, ownership is suspended: the `Ownership` contract is archived and re-created on accept (to the new owner) or on withdraw / decline (back to the current owner). Consumers that gate on "owner exists" must treat the offer window accordingly.

## Templates

### `Ownership`

Sole ownership of a resource by `owner`.

| Field | Type | Description |
| --- | --- | --- |
| `owner` | `Party` | The current sole owner. |

Signatory `owner`. Every choice is owner-controlled.

* **`Ownership_OfferOwnership`** (returns `ContractId OwnershipOffer`): begin a two-step transfer to `newOwner`. Consuming: ownership is suspended into the returned offer until accepted, withdrawn, or declined. Offering to the current owner is rejected.
* **`Ownership_RenounceOwnership`** (returns `()`): renounce ownership, leaving the resource permanently ownerless (the `renounceOwnership` analogue). Irreversible.

### `OwnershipOffer`

A pending ownership transfer awaiting `newOwner`'s decision.

| Field | Type | Description |
| --- | --- | --- |
| `owner` | `Party` | The current owner who made the offer. |
| `newOwner` | `Party` | The prospective owner who must accept. |

Signatory `owner`, observer `newOwner`.

| Choice | Controller | Result | Description |
| --- | --- | --- | --- |
| `OwnershipOffer_Accept` | `newOwner` | `ContractId Ownership` | Accept the transfer. Creates `Ownership` signed by `newOwner`, whose authorization is what makes the transfer sound. |
| `OwnershipOffer_Decline` | `newOwner` | `ContractId Ownership` | Decline; ownership returns to the offerer. |
| `OwnershipOffer_Withdraw` | `owner` | `ContractId Ownership` | Withdraw the offer; ownership returns to the offerer. |

## Transferring ownership

```daml
-- current owner offers
offerCid <- exercise ownershipCid Ownership_OfferOwnership with newOwner = bob

-- prospective owner accepts, becoming the new owner
newOwnershipCid <- exercise offerCid OwnershipOffer_Accept
```

## Errors

| Message | When |
| --- | --- |
| `Ownable: new owner is the current owner` | An offer named the current owner. |

## Related

* [Access Control](/canton/library/access-control), when more than one party or action needs independent authorization.
* [Pausable](/canton/library/pausable), for an emergency stop.
Loading