Skip to content
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, which is 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
Loading