From bbbbf88d9e17d8c9829a3cf068cd3e689aef2da0 Mon Sep 17 00:00:00 2001 From: Pepe Blasco Date: Fri, 25 Sep 2026 15:01:26 +0200 Subject: [PATCH 1/3] docs(canton): sync with canton-contracts and canton-specs, tighten prose Add a Token (CIP-0112) library page for openzeppelin-tokenCIP112-v1 and remove the settlement page. Correct the library, setup, and reference implementation pages against the current sources. Rewrite the prose to be shorter and more direct. --- content/canton/get-started.mdx | 92 ++++++++------- content/canton/index.mdx | 29 +++-- content/canton/library/access-control.mdx | 105 ++++++++++------- content/canton/library/index.mdx | 48 +++++--- content/canton/library/ownable.mdx | 65 ++++++----- content/canton/library/pausable.mdx | 72 +++++++----- content/canton/library/token.mdx | 113 +++++++++++++++++++ content/canton/reference-implementations.mdx | 41 +++---- content/canton/settlement.mdx | 36 ------ src/navigation/canton.json | 10 +- 10 files changed, 377 insertions(+), 234 deletions(-) create mode 100644 content/canton/library/token.mdx delete mode 100644 content/canton/settlement.mdx diff --git a/content/canton/get-started.mdx b/content/canton/get-started.mdx index a10d9a6f..439f6cd1 100644 --- a/content/canton/get-started.mdx +++ b/content/canton/get-started.mdx @@ -2,41 +2,31 @@ title: Get Started --- -This guide covers the prerequisites and the basic project wiring for building on Canton with OpenZeppelin's Daml packages. +This page shows how to set up the Daml toolchain, build the OpenZeppelin packages, and use them in your own Daml project. - - These packages target the Canton 3.4.x baseline and are under active - development. Pin exact versions from the package manifests in + + The packages are experimental, unaudited, and have no release yet. They will + be redesigned before their first release. Pin a source commit of [`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts) - rather than copying version numbers from this page. + and take version numbers from its package manifests, not from this page. ## Prerequisites -- **JDK 21+**: the Daml toolchain runs on Java. Install any JDK 21 or newer - e.g. OpenJDK or Eclipse Adoptium both work - 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: +- **JDK 21 or newer.** The Daml toolchain runs on Java. Set `JAVA_HOME` to the JDK. +- **DPM (Daml Package Manager).** DPM installs the Daml SDK and runs builds, tests, and the sandbox: ```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 +The packages target the Canton 3.4 baseline (SDK `3.4.11`, Daml-LF target `2.1`). Run `dpm install` in a project directory to install the SDK version that its `daml.yaml` or `multi-package.yaml` declares. -OpenZeppelin's Canton library is distributed as Daml packages: each primitive is its own DAR, so you add only the ones you need. +If you are new to Daml, read the [Daml documentation](https://docs.digitalasset.com/) and the [Canton guide to building and packaging](https://docs.canton.network/appdev/modules/m3-building-packaging) first. - - Experimental and work in progress. All library packages are version 0.x, - unaudited, and not yet a stable public API: interfaces may change before a - 1.0 release, and they are not intended for production use. They live under - [`experiments/`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments) - in `OpenZeppelin/canton-contracts` and move to `packages/` when they are - released. - +## Build the Packages -**1. Get the DARs.** The source of truth for production DARs is the [`dars/released/`](https://github.com/OpenZeppelin/canton-contracts/tree/main/dars) directory in `OpenZeppelin/canton-contracts`: it holds immutable DARs copied from tagged GitHub Releases, indexed in `dars/manifest.yaml` with the package IDs and SHA-256 digests operators need for verification and vetting. No production releases have been published yet, so for now build the library packages from source: +No DAR has a release yet, so build the packages from source: ```bash git clone https://github.com/OpenZeppelin/canton-contracts.git @@ -45,9 +35,28 @@ dpm install dpm build --all ``` -Library packages live under `experiments/` until their first release, grouped by category (`experiments/access/` for authorization and ownership, `experiments/security/` for operational security, `experiments/token/` for the Token Standard V2 (CIP-112) token and settlement package); released packages will live under `packages/`. Each package's DAR lands in its own `.daml/dist/` directory (for example `experiments/security/pausable-v1/.daml/dist/openzeppelin-pausable-v1-0.1.0.dar`). +To build one package, set `DAML_PACKAGE` to its directory: + +```bash +DAML_PACKAGE=experiments/security/pausable-v1 dpm build +``` + +The packages live under `experiments/`, grouped by category: -**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): +| Directory | Contents | +| --- | --- | +| `experiments/access/` | Access Control and Ownable | +| `experiments/security/` | Pausable | +| `experiments/token/` | Token (CIP-0112) | +| `experiments/test/` | One test package for each component | + +Each build writes its DAR to the `.daml/dist/` directory of the package, for example `experiments/security/pausable-v1/.daml/dist/openzeppelin-pausable-v1-0.1.0.dar`. + +Released DARs will go in [`dars/released/`](https://github.com/OpenZeppelin/canton-contracts/tree/main/dars), with package IDs and SHA-256 digests in `dars/manifest.yaml`. Participant operators use these values to verify and vet the packages. + +## Add the Packages to Your Project + +In the `daml.yaml` of your project, add each DAR under `data-dependencies`. The `dependencies` list is for SDK libraries only. ```yaml sdk-version: 3.4.11 @@ -65,43 +74,42 @@ 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. +Change the paths to match your clone. List only the DARs that you use. -**3. Import and build.** Import the modules you declared and build your project: +The [Token](/canton/library/token) package also needs the Token Standard V2 DARs from `dars/vendor/` in `data-dependencies`. The token page lists them. + +Import the modules and build: ```daml import OpenZeppelin.PausableV1 ``` ```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) +Each package page gives its module name, templates, and choices. -## Test Against a Local Canton Ledger +## Test Against a Local 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: +`dpm test` runs Daml Script tests on an in-memory ledger. To check time behavior, party visibility, and DAR upload on a real ledger, run the same scripts against a local Canton sandbox over the Ledger API: ```bash -# start a local sandbox with your DAR uploaded -dpm sandbox --dar .daml/dist/.dar +# start a sandbox with your DAR +dpm sandbox --static-time --ledger-api-port 6865 --dar .daml/dist/.dar -# run a script against it over the Ledger API +# in a second terminal, run a script against it dpm script --dar .daml/dist/.dar \ --script-name My.Module:myScript \ - --ledger-host localhost --ledger-port 6865 + --ledger-host localhost --ledger-port 6865 --static-time ``` -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. +Use `--static-time` on both commands if your scripts call `setTime`. A wall-clock ledger rejects `setTime`, and a static-time clock only moves forward. + +A sandbox keeps its state between script runs. A second allocation of the same party hint fails, so give each script its own party hints. -## Explore Further +## Next Steps -- 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. +- Read the [Library overview](/canton/library) for the package design rules. +- Read the [Reference Implementations](/canton/reference-implementations) for complete application designs. +- See the [Canton Improvement Proposals (CIPs)](https://github.com/canton-foundation/cips) for the standards that the packages implement. diff --git a/content/canton/index.mdx b/content/canton/index.mdx index 9e04b583..8620dc2a 100644 --- a/content/canton/index.mdx +++ b/content/canton/index.mdx @@ -2,34 +2,31 @@ 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://docs.digitalasset.com/). 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. +OpenZeppelin builds reusable, security-focused [Daml](https://docs.digitalasset.com/) packages for applications on the [Canton Network](https://www.canton.network/). This section documents the library packages and the Reference Implementations that use them. - - 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. + + Everything in this section is experimental. No package has a release yet, and + no package has an audit. The packages will be redesigned before their first + release, and the redesign will change module names, choice signatures, and + package IDs. Do not use them in production. ## 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). +The packages live in [`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts). Each package is an independent DAR. See the [Library overview](/canton/library) for the design rules. -- **[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, built on the Token Standard V2 (CIP-112) interfaces. Implemented as the [`openzeppelin-tokenCIP112-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments/token/tokenCIP112-v1) package in `OpenZeppelin/canton-contracts`, with executable research and live-ledger evidence in [`OpenZeppelin/canton-specs`](https://github.com/OpenZeppelin/canton-specs). +- **[Access Control](/canton/library/access-control)**: role-based authorization. An admin issues role grants, and gated choices check the grant that the caller presents. +- **[Ownable](/canton/library/ownable)**: single-owner authorization with a two-step ownership transfer. +- **[Pausable](/canton/library/pausable)**: an emergency stop that an authorized party can switch on and off. +- **[Token (CIP-0112)](/canton/library/token)**: a token that implements the Token Standard V2 interfaces, with batch settlement, compliance hooks, and CIP-86 allowances. ## 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. +The [Reference Implementations](/canton/reference-implementations) are target architectures for complete applications: a DEX, a lending protocol, a cross-chain stablecoin bridge, and a confidential auction. ## 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. +Go to [Get Started](/canton/get-started) to install the toolchain and add OpenZeppelin packages to a Daml project. --- diff --git a/content/canton/library/access-control.mdx b/content/canton/library/access-control.mdx index 2354d714..12134002 100644 --- a/content/canton/library/access-control.mdx +++ b/content/canton/library/access-control.mdx @@ -2,14 +2,13 @@ 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. +Access Control is role-based authorization for Daml. It is the Daml analogue of OpenZeppelin's `AccessControl` and `AccessControlDefaultAdminRules`. An `admin` party (the `DEFAULT_ADMIN_ROLE` holder in Solidity) grants named roles to parties, and gated choices check that the caller holds the required 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. +The package has no dependency on other OpenZeppelin packages. - Experimental and work in progress. This package is version 0.x, unaudited, - and not yet a stable public API: interfaces may change before a 1.0 release, - and it is not intended for production use. Source: + Experimental. Version 0.x, unaudited, and subject to a redesign before + release. Source: [`experiments/access/access-control-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments/access/access-control-v1) in `OpenZeppelin/canton-contracts`. @@ -20,61 +19,83 @@ import OpenZeppelin.AccessControlV1 ## The Daml Model -Two design choices differ from Solidity, and both are deliberate. +**Roles are `Text`.** Solidity uses `bytes32` role IDs such as `keccak256("MINTER_ROLE")`. A Daml template field cannot be a type variable, so a generic role primitive stores `role : Text`. For compile-time checks, define your own role type and convert it with a function such as `roleId : MyRole -> Text`. -**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. +**A grant is a contract.** Daml-LF 2.1 has no contract keys, so there is no global role table. A `RoleGrant` is a contract that `admin` signs and that names one `account`. A gated choice fetches the grant that the caller presents and checks three things: `admin` issued it, it names the caller, and it carries the required role. A party cannot forge a grant, because only `admin` can sign one. A party that presents a grant for another account fails the check. ## Templates ### `RoleGrant` -A role granted by `admin` to `account`. Possession is authorization. +A role that `admin` grants to `account`. | 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"`. | +| `admin` | `Party` | The authority that issued the grant. | +| `account` | `Party` | The party that holds the role. | +| `role` | `Text` | The role ID, 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. +- **`RoleGrant_Renounce`** (controller `account`, consuming, returns `()`): the holder gives up the role (the `renounceRole` analogue). -### `RoleAdmin` +`admin` can also archive a grant directly, because it is the signatory. -The role-administration authority that mints and revokes grants. Two paths coexist: +### `RoleAdmin` -- 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. +The contract that issues and revokes grants. It has one field, `admin : Party`. Signatory `admin`. All choices are nonconsuming, so the contract stays active. -| Choice | Controller | Result | Description | +| Choice | Controller | Arguments | Result | | --- | --- | --- | --- | -| `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. | +| `RoleAdmin_GrantRole` | `admin` | `account`, `role` | `ContractId RoleGrant` | +| `RoleAdmin_RevokeRole` | `admin` | `grantCid` | `()` | +| `RoleAdmin_GrantRoleAs` | `caller` | `caller`, `adminRole`, `adminGrantCid`, `account`, `role` | `ContractId RoleGrant` | +| `RoleAdmin_RevokeRoleAs` | `caller` | `caller`, `adminRole`, `adminGrantCid`, `grantCid` | `()` | +| `RoleAdmin_BeginDefaultAdminTransfer` | `admin` | `newAdmin`, `role`, `effectiveTime` | `ContractId DefaultAdminTransferOffer` | + +There are two ways to manage roles: + +- **Directly.** `admin` grants or revokes any role. +- **Through a delegate** (the `getRoleAdmin` analogue). The `caller` presents its own grant for `adminRole`, then grants or revokes `role`. The choice runs with the authority of the `RoleAdmin` contract, so `admin` still signs the new grant. The delegate needs `RoleAdmin` disclosed to it, usually by an off-ledger service of the application. + + + The library has no role hierarchy. The caller supplies `adminRole`, and the + choice checks only that the caller holds a grant for it. A party that can see + `RoleAdmin` and holds any grant from the same `admin` can name that role as + `adminRole`, then grant or revoke any role. The application must check that + `adminRole` is the correct admin role for `role` before it accepts a delegated + grant or revoke, and it must bind grants to the resource that they protect. + -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. +`DEFAULT_ADMIN_ROLE` has no special handling. Restrictions on who can delegate the root role are application policy. ### `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. +A pending, timelocked transfer of a role to `newAdmin` (the `AccessControlDefaultAdminRules` analogue). Use the `DEFAULT_ADMIN_ROLE` ID as `role` to hand over the default admin role. + +| Field | Type | Description | +| --- | --- | --- | +| `admin` | `Party` | The current admin. | +| `newAdmin` | `Party` | The party that receives the role. | +| `role` | `Text` | The role to transfer. | +| `effectiveTime` | `Time` | The earliest ledger time for acceptance. | -- **`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). +Signatory `admin`, observer `newAdmin`. + +- **`DefaultAdminTransferOffer_Accept`** (controller `newAdmin`, returns `ContractId RoleGrant`): accept when ledger time is at or after `effectiveTime`. Creates a `RoleGrant` of `role` for `newAdmin`. +- **`DefaultAdminTransferOffer_Cancel`** (controller `admin`, returns `()`): cancel the transfer at any time before acceptance (the `cancelDefaultAdminTransfer` analogue). + +Acceptance does not revoke the role from the current admin. To make the transfer exclusive, revoke the old grant after acceptance. ## 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. +- **`requireRole : Party -> Text -> Party -> RoleGrant -> Update ()`**: the `onlyRole` modifier analogue. Call it at the start of a gated choice with the caller, the required role, the expected admin, and the fetched grant. +- **`hasRole : Party -> Text -> Party -> RoleGrant -> Bool`**: the same check as a pure predicate. +- **`requireTimelockElapsed : Time -> Update ()`**: fails if ledger time is before the given time. `DefaultAdminTransferOffer` uses it, and you can use it in your own timelocks. -## Gating an Operation +## Gating a Choice -A consumer's privileged choice fetches the grant the caller presents and validates it before doing work: +A privileged choice fetches the grant that the caller presents and checks it first. In this example, the enclosing template has an `admin : Party` field: ```daml nonconsuming choice Mint : ContractId Token @@ -89,17 +110,19 @@ nonconsuming choice Mint : ContractId Token create Token with owner = caller; amount ``` +The fetch succeeds because `caller` is an observer of its own grant. + ## Errors -| Message | When | +| Message | Cause | | --- | --- | -| `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`. | +| `AccessControl: grant admin is not the expected authority` | A different admin issued the presented grant, or the grant to revoke. | +| `AccessControl: grant does not name the caller (impersonation)` | The grant names another party. | +| `AccessControl: grant does not carry the required role` | The grant is for another role. | +| `AccessControl: default-admin handoff nominee is the current admin` | `newAdmin` is the current admin. | +| `AccessControl: default-admin transfer timelock has not elapsed` | Acceptance before `effectiveTime`. | ## Related -- [Ownable](/canton/library/ownable), for the single-owner case. -- [Pausable](/canton/library/pausable), for an emergency stop. +- [Ownable](/canton/library/ownable) for a single owner. +- [Pausable](/canton/library/pausable) for an emergency stop. diff --git a/content/canton/library/index.mdx b/content/canton/library/index.mdx index 042b79ad..fad641e2 100644 --- a/content/canton/library/index.mdx +++ b/content/canton/library/index.mdx @@ -2,38 +2,52 @@ 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. +The OpenZeppelin library for Canton brings the patterns of the OpenZeppelin Solidity contracts to Daml: role-based access control, ownership, an emergency stop, and a standard token. Each package follows the Canton authorization and privacy model. Where Daml differs from the EVM, the package page explains the difference. - Experimental and work in progress. All library packages are version 0.x, - unaudited, and not yet a stable public API: interfaces may change before a - 1.0 release, and they are not intended for production use. Source: + Experimental. All packages are version 0.x and unaudited. They will be + redesigned and rewritten before they move from [`experiments/`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments) - in `OpenZeppelin/canton-contracts`; packages move to `packages/` when they - are released. + to `packages/` in `OpenZeppelin/canton-contracts`. The rewrite will change + module names, template and choice signatures, and package IDs. There is no + upgrade path from the current packages. ## 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: +| Package | Module | Solidity analogue | +| --- | --- | --- | +| [Access Control](/canton/library/access-control) | `OpenZeppelin.AccessControlV1` | `AccessControl`, `AccessControlDefaultAdminRules` | +| [Ownable](/canton/library/ownable) | `OpenZeppelin.OwnableV1` | `Ownable2Step` | +| [Pausable](/canton/library/pausable) | `OpenZeppelin.PausableV1` | `Pausable` | +| [Token (CIP-0112)](/canton/library/token) | `OpenZeppelin.TokenCIP112V1.*` | `ERC20` (partial) | -- **[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 Rules -## Design Philosophy +**One package, one DAR.** Daml has no inheritance. The unit of reuse is the DAR. Each component is a separate package with its own DAR, and no OpenZeppelin package depends on another. Applications build, upload, and vet only the DARs they use. -**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 `openzeppelin-pausable-v1`. +**Versioned names.** A package is named `openzeppelin--v1`, and its module is `OpenZeppelin.V1`. A compatible upgrade (a smart contract upgrade, or SCU) keeps the package name and increments the package version. A breaking change creates a new `-v2` package with a `V2` module. -**Versioned packages and modules.** Each component is released as a versioned package (`openzeppelin--v1`) with a matching module suffix (`OpenZeppelin.V1`). Compatible upgrades keep the package name; a breaking change ships as a sibling `-v2` package instead of mutating `-v1`. +**Interfaces in a separate package.** A component that defines Daml interfaces puts them in a frozen `-api-v1` package, with the templates in a separate implementation package. The current components define no interfaces of their own, so each ships as one package. -**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. +**Composition in the application.** Implementation packages do not depend on each other. Applications combine them directly or through interfaces. -**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. +**No Daml Script in production DARs.** The shipped packages do not depend on `daml-script`. Each component has its own test package under `experiments/test/`. Test packages are never released or uploaded. + +**Public and internal modules.** The documented modules are the public API. Implementation modules use an `.Internal` suffix. This is a naming convention; the ledger does not enforce it. + +## Application Responsibilities + +The packages are keyless: there are no contract keys and no global lookups. The application that uses a package must: + +- Select the canonical contract for each protected resource, for example the one `Ownership` or `PauseState` contract that applies. +- Bind authority and state to that resource. +- Disclose the contracts that other parties must read. +- Review its complete dependency graph. ## Using the Library -Add the package(s) you need as data-dependencies of your Daml project and import the module: +Build the packages, add the DARs as `data-dependencies`, and import the modules: ```daml import OpenZeppelin.AccessControlV1 @@ -41,4 +55,4 @@ import OpenZeppelin.OwnableV1 import OpenZeppelin.PausableV1 ``` -See [Get Started](/canton/get-started) for toolchain setup, and each package page for its templates, choices, and usage patterns. +See [Get Started](/canton/get-started) for the full setup. diff --git a/content/canton/library/ownable.mdx b/content/canton/library/ownable.mdx index 1bfc08d4..67ad2b40 100644 --- a/content/canton/library/ownable.mdx +++ b/content/canton/library/ownable.mdx @@ -2,14 +2,13 @@ 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. +Ownable is single-owner authorization for Daml. It is the Daml analogue of OpenZeppelin's `Ownable2Step`: one `owner` party at a time, a two-step ownership transfer, 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. +The package has no dependency on other OpenZeppelin packages. - Experimental and work in progress. This package is version 0.x, unaudited, - and not yet a stable public API: interfaces may change before a 1.0 release, - and it is not intended for production use. Source: + Experimental. Version 0.x, unaudited, and subject to a redesign before + release. Source: [`experiments/access/ownable-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments/access/ownable-v1) in `OpenZeppelin/canton-contracts`. @@ -18,63 +17,71 @@ The package is independent: it carries no role type and has no dependency on [Ac import OpenZeppelin.OwnableV1 ``` -## Why Transfer Is Always Two-Step in Daml +## Why Transfer Has Two Steps -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. +In Solidity, `transferOwnership(newOwner)` is one call. Daml cannot do this. The owner is a signatory of the `Ownership` contract, and Daml requires the authority of every signatory to create a contract. The current owner alone cannot make another party a signatory. -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. +Transfer is therefore an offer and an acceptance. The owner creates an `OwnershipOffer`, and the new owner accepts it. As a result, no party becomes owner without its agreement. -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. +While an offer is pending, no `Ownership` contract exists. The offer archives the `Ownership` contract. Acceptance creates a new one for the new owner. A decline or a withdrawal creates a new one for the current owner. Applications that require an active `Ownership` contract must handle this period. + +`Ownership` is not bound to a specific resource. The application must define which `Ownership` contract controls each protected resource. ## Templates ### `Ownership` -Sole ownership of a resource by `owner`. +Ownership of a resource by one party. | Field | Type | Description | | --- | --- | --- | -| `owner` | `Party` | The current sole owner. | +| `owner` | `Party` | The current owner. | -Signatory `owner`. Every choice is owner-controlled. +Signatory `owner`, no observers. Other parties read the contract through explicit disclosure. `owner` controls both choices, and both are consuming. -- **`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. +- **`Ownership_OfferOwnership`** (takes `newOwner : Party`, returns `ContractId OwnershipOffer`): start a transfer to `newOwner`. Fails if `newOwner` is the current owner. +- **`Ownership_RenounceOwnership`** (returns `()`): give up ownership permanently (the `renounceOwnership` analogue). No `Ownership` contract remains. ### `OwnershipOffer` -A pending ownership transfer awaiting `newOwner`'s decision. +A pending transfer. | Field | Type | Description | | --- | --- | --- | -| `owner` | `Party` | The current owner who made the offer. | -| `newOwner` | `Party` | The prospective owner who must accept. | +| `owner` | `Party` | The owner that made the offer. | +| `newOwner` | `Party` | The party that can accept. | -Signatory `owner`, observer `newOwner`. +Signatory `owner`, observer `newOwner`. All choices are consuming and take no arguments. | 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. | +| `OwnershipOffer_Accept` | `newOwner` | `ContractId Ownership` | Creates `Ownership` for `newOwner`, with `newOwner` as signatory. | +| `OwnershipOffer_Decline` | `newOwner` | `ContractId Ownership` | Returns ownership to `owner`. | +| `OwnershipOffer_Withdraw` | `owner` | `ContractId Ownership` | Returns ownership to `owner`. | + +A signatory can archive `Ownership` or `OwnershipOffer` directly. This leaves no `Ownership` contract, so the resource has no owner. ## Transferring Ownership +Each step is a separate submission by a different party. In Daml Script: + ```daml --- current owner offers -offerCid <- exercise ownershipCid Ownership_OfferOwnership with newOwner = bob +-- alice, the current owner, makes the offer +offerCid <- submit alice do + exerciseCmd ownershipCid Ownership_OfferOwnership with newOwner = bob --- prospective owner accepts, becoming the new owner -newOwnershipCid <- exercise offerCid OwnershipOffer_Accept +-- bob accepts and becomes the owner +newOwnershipCid <- submit bob do + exerciseCmd offerCid OwnershipOffer_Accept ``` ## Errors -| Message | When | +| Message | Cause | | --- | --- | -| `Ownable: new owner is the current owner` | An offer named the current owner. | +| `Ownable: new owner is the current owner` | The offer names 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. +- [Access Control](/canton/library/access-control) when more than one party or action needs separate authorization. +- [Pausable](/canton/library/pausable) for an emergency stop. diff --git a/content/canton/library/pausable.mdx b/content/canton/library/pausable.mdx index 34d365c2..4c52921d 100644 --- a/content/canton/library/pausable.mdx +++ b/content/canton/library/pausable.mdx @@ -2,14 +2,13 @@ title: Pausable --- -Pausable is a standalone, token-agnostic emergency-stop switch for Daml. It is the analogue of OpenZeppelin's `Pausable.sol`: a single boolean the authority can flip, plus a guard (`whenNotPaused`) that gated operations call to refuse work while paused. +Pausable is an emergency stop for Daml. It is the Daml analogue of OpenZeppelin's `Pausable`: a boolean flag that an authorized party sets, and a guard (`whenNotPaused`) that gated choices call to refuse work while the flag is set. -The package is independent: it carries no role type and depends on no other OpenZeppelin package, so a project that wants only a pause switch imports only this one package. +The package has no dependency on other OpenZeppelin packages. - Experimental and work in progress. This package is version 0.x, unaudited, - and not yet a stable public API: interfaces may change before a 1.0 release, - and it is not intended for production use. Source: + Experimental. Version 0.x, unaudited, and subject to a redesign before + release. Source: [`experiments/security/pausable-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments/security/pausable-v1) in `OpenZeppelin/canton-contracts`. @@ -18,55 +17,70 @@ The package is independent: it carries no role type and depends on no other Open import OpenZeppelin.PausableV1 ``` -## What "Paused" Means on a UTXO Ledger +## Pausing on a UTXO Ledger -Solidity stores `_paused` in contract storage and reads it through a modifier. Daml-LF 2.1 has no contract keys, so there is no global "is the resource paused?" lookup. Instead the live `PauseState` is a contract that gated operations read by `ContractId` (disclosed to them by the pauser) and check via `whenNotPaused`. +Solidity stores `_paused` in contract storage. Daml-LF 2.1 has no contract keys, so there is no global lookup for the flag. The flag is a `PauseState` contract instead. A gated choice fetches it by contract ID and calls `whenNotPaused`. -Pause is therefore origination control: new operations that consult the flag refuse to start while paused, while already-committed work is unaffected. That is the only pause semantic that is robustly enforceable on a UTXO ledger. Flipping the flag archives the old `PauseState` and creates the next one, which is how state transitions work throughout Daml. +A pause stops new operations that check the flag. It has no effect on transactions that are already committed. To change the flag, `PauseState_Set` archives the current contract and creates a new one with a new contract ID. Applications must always use the ID of the current `PauseState`. ## Templates ### `PauseState` -The current pause state of a resource, administered by `pauser`. +The pause state of a resource. | Field | Type | Description | | --- | --- | --- | -| `pauser` | `Party` | The authority that may pause and unpause. | -| `paused` | `Bool` | Whether the resource is currently paused. | +| `pauser` | `Party` | The party that can pause and unpause. | +| `paused` | `Bool` | `True` if the resource is paused. | -Signatory `pauser` (the contract is pauser-signed and so unforgeable). Observers are granted read access by explicit disclosure, keeping the privacy surface minimal. +Signatory `pauser`, no observers. Other parties read the contract through explicit disclosure. -- **`PauseState_Set`** (returns `ContractId PauseState`): flip the flag, archiving the current state and creating its successor. Rejects a redundant change (setting the flag to its current value) so on-ledger history records only real transitions. -- **`PauseState_Get`** (nonconsuming, returns `Bool`): read the current flag without archiving the contract. +- **`PauseState_Set`** (controller `pauser`, consuming, returns `ContractId PauseState`): takes `newPaused : Bool`, archives the current contract, and creates a new one with `paused = newPaused`. Fails if `newPaused` equals the current value. +- **`PauseState_Get`** (controller `pauser`, nonconsuming, returns `Bool`): returns the flag. Other parties do not call this choice; they fetch the contract and read `paused`. + +The template has no archive choice. `pauser` can still archive the contract directly, because it is the signatory. After that, guarded choices that reference the archived ID fail. ## Helper Functions -- **`whenNotPaused : PauseState -> Update ()`**: the `whenNotPaused` modifier analogue. A gated operation fetches the disclosed `PauseState` and calls this before doing origination work; it fails the transaction when paused. -- **`isPaused : PauseState -> Bool`**: the pure predicate form, for callers that already hold the flag. +- **`whenNotPaused : PauseState -> Update ()`**: the `whenNotPaused` modifier analogue. Fails the transaction if the fetched state is paused. +- **`isPaused : PauseState -> Bool`**: the same check as a pure predicate. -## Guarding an Operation +## Guarding a Choice ```daml -nonconsuming choice Transfer : ContractId Token +template Token with - pauseStateCid : ContractId PauseState - to : Party + issuer : Party + owner : Party amount : Int - controller owner - do - ps <- fetch pauseStateCid - whenNotPaused ps - create Token with owner = to; amount + where + signatory issuer, owner + + choice Transfer : ContractId Token + with + pauseStateCid : ContractId PauseState + to : Party + controller owner, to + do + ps <- fetch pauseStateCid + assertMsg "wrong pauser" (ps.pauser == issuer) + whenNotPaused ps + create this with owner = to ``` +Two details make this example correct: + +- **Authority for the fetch.** A `fetch` needs the authority of a stakeholder of the fetched contract. Here `issuer` is the pauser and a signatory of `Token`, so the choice has that authority. Disclosure makes a contract visible, but it does not give this authority. +- **The correct `PauseState`.** The package cannot check that a presented `PauseState` is the current state for your resource. A caller can present any `PauseState`, for example an unpaused one from another pauser. Check `ps.pauser`, and define how your application selects and discloses the canonical state. + ## Errors -| Message | When | +| Message | Cause | | --- | --- | -| `Pausable: paused` | A guarded operation ran while the resource was paused. | -| `Pausable: redundant pause change` | `PauseState_Set` was called with the flag's current value. | +| `Pausable: paused` | A guarded choice ran while the resource was paused. | +| `Pausable: redundant pause change` | `PauseState_Set` received the current value. | ## Related -- [Access Control](/canton/library/access-control) or [Ownable](/canton/library/ownable), to decide who is allowed to pause and resume. +- [Access Control](/canton/library/access-control) or [Ownable](/canton/library/ownable) to control who can pause. diff --git a/content/canton/library/token.mdx b/content/canton/library/token.mdx new file mode 100644 index 00000000..dfb7910f --- /dev/null +++ b/content/canton/library/token.mdx @@ -0,0 +1,113 @@ +--- +title: Token (CIP-0112) +--- + +The token package implements the Token Standard V2 interfaces from [CIP-0112](https://github.com/canton-foundation/cips). It provides holdings, transfer instructions, allocations with batch settlement, allocation requests, registry rules, event logging, compliance hooks, and [CIP-0086](https://github.com/canton-foundation/cips/blob/main/cip-0086/cip-0086.md) allowances. Its closest Solidity analogue is a partial `ERC20`. + +The package has no dependency on other OpenZeppelin packages. It depends on the Token Standard V2 interface DARs. + + + Experimental. Version 0.x, unaudited, and subject to a redesign before + release. The upstream Token Standard V2 interfaces are at devnet stage. The + interface DARs in `dars/vendor/` are local builds from a pinned + [splice](https://github.com/hyperledger-labs/splice) commit, and every + package ID changes when upstream publishes a release. Source: + [`experiments/token/tokenCIP112-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments/token/tokenCIP112-v1) + in `OpenZeppelin/canton-contracts`. + + +## Modules + +The package has no root module. Import the submodules that you need, for example: + +```daml +import OpenZeppelin.TokenCIP112V1.Holding +``` + +Each module implements the matching `Splice.Api.Token.*V2` interfaces. + +| Module | Templates | Purpose | +| --- | --- | --- | +| `.Holding` | `TokenHolding` | An asset holding, maintained jointly by the instrument admin and the account parties, with an optional lock. | +| `.Transfer` | `TokenTransferInstruction` | The transfer-instruction lifecycle: accept, reject, withdraw, and expire. | +| `.Allocation` | `TokenAllocation`, `BatchSettlementAuthorization` | Allocations backed by locked holdings, and all-or-nothing batch settlement. A batch must exactly cover its allocations. | +| `.AllocationRequest` | `TokenAllocationRequest` | The request from an application that a wallet turns into an allocation. | +| `.Allowance` | `TokenAllowance` | A CIP-0086 spending budget with ERC-20 `approve` and `transferFrom` behavior. | +| `.Registry` | `TokenRules` | The registry rules: transfer, allocation, and settlement factories, plus mint, burn, and allowance choices. | +| `.Base` | `TokenEventLog` | The event log for holding changes. | +| `.D1` | `TrustedAttesterRegistry`, `ComplianceAttestation`, `SeizureOrder` | Compliance attestation (D1) and lawful-process seizure authority (D2). | + +The package defines no Daml interfaces or exceptions of its own, so it ships as one implementation package. + +## Authority + +- The instrument admin and the account parties jointly maintain holdings. `TokenRules_Mint` and `TokenRules_Burn` need both. +- Wallets act through the Token Standard V2 interface choices. The choices check identity, funding, and expiry before they move value. +- The application selects and discloses the canonical `TokenRules` contract for its instrument. +- If `allowedExecutors` on `TokenRules` is `None`, any party can be named as a settlement executor. Set it when that matters for your deployment. + +## Compliance Hooks + +- **D1 attestation.** If `requiredAttesterRegistryCid` is set on `TokenRules`, every batch settlement must present a valid `ComplianceAttestation` from an attester in that `TrustedAttesterRegistry`. +- **D2 seizure.** The admin can mark an allocation for seizure. A sweep of the locked holdings also needs a `SeizureOrder` from a party other than the admin. + +## Allowances + +The owner approves a `TokenAllowance` for a spender with `TokenRules_ApproveAllowance`. The spender draws on it with `TokenRules_TransferFrom`, and the registry applies the current configuration. + +- A transfer to the spender's own account completes in one step. A transfer to another receiver creates a pending `TokenTransferInstruction` that the receiver accepts. +- Each spend writes the spender party into the transfer metadata under the key `openzeppelin.com/spender`. +- Each spend needs explicit disclosure of the owner's funding holdings. A partial spend returns change at a new holding contract ID, so a disclosure cannot be used twice. +- A spend archives the allowance and creates the remaining budget at a new contract ID. Wallets must track the new ID. + +The owner's account parties can revoke an allowance in three ways. The admin cannot revoke it. + +- `TokenAllowance_Revoke`. +- `TokenAllowance_SetRemaining` with zero. A positive value creates the allowance again at a new contract ID. +- `TokenRules_ApproveAllowance` with amount zero and the current contract ID, as with ERC-20 `approve(spender, 0)`. + +## V1 Compatibility + +The V2 interfaces are separate from V1. They share only `splice-api-token-metadata-v1`, so V1 tooling cannot see this token. To support V1, add the V1 interface instances to the same templates. The V1 DARs are in `dars/vendor/`, and the vendored `splice-token-standard-utils` DAR has helpers for this. + +## Build and Consume + +From the root of `canton-contracts`: + +```bash +DAML_PACKAGE=experiments/token/tokenCIP112-v1 dpm build +``` + +In your project, add the token DAR and the vendored interface DARs as `data-dependencies`: + +```yaml +data-dependencies: + - ../canton-contracts/experiments/token/tokenCIP112-v1/.daml/dist/openzeppelin-tokenCIP112-v1-0.1.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-metadata-v1-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-holding-v1-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-holding-v2-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-transfer-events-v2-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-allocation-v1-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-allocation-v2-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-transfer-instruction-v1-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-transfer-instruction-v2-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-allocation-instruction-v1-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-allocation-instruction-v2-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-allocation-request-v1-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-api-token-allocation-request-v2-1.0.0.dar + - ../canton-contracts/dars/vendor/splice-token-standard-utils-2.0.0.dar +build-options: + - --target=2.1 +``` + +The vendored DARs are built with SDK 3.5.1 and used from the 3.4.11 baseline. This works because both target Daml-LF 2.1, so `--target=2.1` is required. `dars/manifest.yaml` records the source commit and digest of each vendored DAR. + +## Testing + +- **Unit tests.** `dpm test` runs the Daml Script suites on an in-memory ledger. CI requires full template and choice coverage. +- **Sandbox gate.** `scripts/check-sandbox.sh` runs token creation, transfer, allowance, query, and burn scripts against a static-time Canton sandbox over the Ledger API. It needs `dpm`, Java 21 or newer, `lsof`, and a free Ledger API port (`6865` by default, or set `OZ_LEDGER_PORT`). CI does not run this gate. + +## Related + +- [Reference Implementations](/canton/reference-implementations) that settle through Token Standard V2. +- [Access Control](/canton/library/access-control), [Ownable](/canton/library/ownable), and [Pausable](/canton/library/pausable) for application-level authorization. diff --git a/content/canton/reference-implementations.mdx b/content/canton/reference-implementations.mdx index 841d1167..f5b61763 100644 --- a/content/canton/reference-implementations.mdx +++ b/content/canton/reference-implementations.mdx @@ -2,45 +2,48 @@ title: Reference Implementations --- -Reference Implementations (RIs) are complete application blueprints that show how OpenZeppelin's Canton library and settlement primitives compose into real applications. Each RI pairs a full architecture design document with working Daml, so teams can adopt an end-to-end pattern instead of assembling isolated modules. +Reference Implementations (RIs) are target architectures for complete Canton applications. Each RI describes the parties, contracts, authority, privacy, and settlement flows of one application, and names the OpenZeppelin packages that it uses. - The four RIs below are in the research and design phase: each has a complete, - reviewed design document, maintained as a reference architecture in - [`OpenZeppelin/canton-specs`](https://github.com/OpenZeppelin/canton-specs/tree/main/docs/reference-architectures), - and their application logic is being built out. These sections will expand - with code walkthroughs and integration guides as each implementation lands. + The four RIs are design documents in + [`OpenZeppelin/canton-specs`](https://github.com/OpenZeppelin/canton-specs/tree/main/docs/reference-architectures). + The designs are still in review, and no RI has published application code + yet. This page will add code walkthroughs when implementations are available. ## Privacy-Preserving DEX -An exchange design adapted to Canton's privacy model. Its organizing primitive is the atomic delivery-versus-payment (DvP) swap: a trade is two legs (asset against payment) settled all-or-nothing through the settlement engine, so no party is ever left half-filled. Canton's projection model keeps each trader's positions and flows visible only to the parties involved, which changes how order flow, pricing, and liquidity have to be designed compared to a public-mempool chain. +An operator-run, non-custodial spot exchange built as a constant-product AMM (`x·y = k`), with one liquidity pool for each instrument pair. Each trade is an atomic delivery-versus-payment swap: two committed CIP-0112 allocations settle together or not at all, within limits that the trader signs (exact input, minimum output). The ledger enforces the swap math. A multi-hosted venue party holds the authority to execute swaps. Compliance screening and KYC run off-ledger in the venue backend. -**Full design document:** [Privacy-Preserving DEX](https://github.com/OpenZeppelin/canton-specs/blob/main/docs/reference-architectures/dex.md) +The DEX settles through the Token Standard V2 interfaces, so it works with any conforming token registry. It uses [Access Control](/canton/library/access-control) for optional governance. -## Lending Protocol +**Design document:** [Privacy-Preserving DEX](https://github.com/OpenZeppelin/canton-specs/blob/main/docs/reference-architectures/dex.md) -A fixed-rate, open-term, overcollateralized, permissioned lending protocol designed for institutional participants. It covers the vault, collateral, borrow, repay, and liquidation flows, with authorization built on the library's role and ownership primitives and value movement running through the settlement engine. +## Institutional Lending Protocol -**Full design document:** [Institutional Lending Protocol](https://github.com/OpenZeppelin/canton-specs/blob/main/docs/reference-architectures/lending.md) +A vault-based lending protocol: fixed-rate, open-term, overcollateralized, and permissioned. Each position is an isolated collateralized debt position that accrues simple interest. Borrowers draw debt tokens from a treasury that a privileged funder supplies. Borrowers and liquidators need a KYC claim from a trusted issuer, and each operation can also require a compliance attestation. An unhealthy vault gets a margin-call grace period, then a partial liquidation in proportion to the payment. + +Transfers happen directly inside the vault choices. The protocol uses [Access Control](/canton/library/access-control), [Ownable](/canton/library/ownable), and [Pausable](/canton/library/pausable) for authorization, and the [Token](/canton/library/token) package for holdings and attestations. + +**Design document:** [Institutional Lending Protocol](https://github.com/OpenZeppelin/canton-specs/blob/main/docs/reference-architectures/lending.md) ## Cross-Chain Stablecoin Payments -An architectural blueprint for private, atomic settlement on Canton of stablecoin payments that originate on external blockchains. It resolves the tension between cross-chain liquidity and enterprise privacy requirements: institutional participants settle externally-originated payments on Canton without exposing their flows to the originating chain. +The Canton side of a two-way stablecoin bridge. Inbound, an attested lock on an external chain mints a wrapped instrument on Canton through a messaging gateway. Outbound, a burn on Canton causes a redemption gateway to release the backing asset on the external chain. Each inbound credit settles as a private, all-or-nothing CIP-0112 settlement batch, with a single-use compliance attestation and a KYC credential check. -**Full design document:** [Cross-Chain Stablecoin Payment Orchestration](https://github.com/OpenZeppelin/canton-specs/blob/main/docs/reference-architectures/cross-chain-stablecoin.md) +The hop between chains is not atomic, and privacy applies only on Canton: the lock on the external chain is public. The design uses the [Token](/canton/library/token) package for settlement, and [Access Control](/canton/library/access-control), [Ownable](/canton/library/ownable), and [Pausable](/canton/library/pausable) for roles, ownership handover, and pause state. -## Confidential Auction Launchpad +**Design document:** [Cross-Chain Stablecoin Payment Orchestration](https://github.com/OpenZeppelin/canton-specs/blob/main/docs/reference-architectures/cross-chain-stablecoin.md) -A launchpad for institutional, regulated, confidential token distribution. It uses Canton's sub-transaction privacy to establish a sealed-bid environment by protocol design rather than cryptographic obfuscation: bids, allocations, and settlement details are visible only to explicitly authorized parties via native ledger projection, with no commit-reveal scheme required. +## Confidential Auction -**Full design document:** [Confidential Auction Launchpad](https://github.com/OpenZeppelin/canton-specs/blob/main/docs/reference-architectures/confidential-auction.md) +A sealed-bid, uniform-price auction that distributes a fixed quantity of a fungible token in one round. The issuer's supply and each bidder's maximum payment are locked in advance as committed Token Standard V2 allocations. Bidders do not see each other's bids. The issuer and the auctioneer see all bids, and bidders trust the auctioneer to submit the complete set. One atomic transaction checks the auctioneer's off-ledger result against the published clearing rule, then settles the payment and token delivery of every winner. -## How They Relate to the Library +The design builds on the [Token](/canton/library/token) package. Pause is an optional deployment choice. -Each RI reuses the same foundation: [Access Control](/canton/library/access-control), [Ownable](/canton/library/ownable), and [Pausable](/canton/library/pausable) for authorization and safety, and [Settlement](/canton/settlement) for atomic value movement. As the RIs mature, common patterns they surface are candidates for promotion into the general library. +**Design document:** [Confidential Auction](https://github.com/OpenZeppelin/canton-specs/blob/main/docs/reference-architectures/confidential-auction.md) ## Related -- [Settlement (CIP-112)](/canton/settlement) +- [Library](/canton/library) - [Get Started](/canton/get-started) diff --git a/content/canton/settlement.mdx b/content/canton/settlement.mdx deleted file mode 100644 index 0bd4f6b1..00000000 --- a/content/canton/settlement.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Settlement (CIP-112) ---- - -The settlement primitive is an interface-shaped engine for atomic, value-moving delivery-versus-payment (DvP) on Canton, built on the Token Standard V2 (CIP-112) interfaces. The current implementation is the [`openzeppelin-tokenCIP112-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments/token/tokenCIP112-v1) package in `OpenZeppelin/canton-contracts`. - - - Experimental. The implementation builds against the upstream Token Standard - V2 interfaces, which are devnet-stage: the vendored interface DARs are local - builds from a pinned splice commit, and every package ID changes when - upstream cuts a release. It is not audited and not intended for production - use. Interfaces and behavior may change. - - -## What It Does - -Settlement coordinates the atomic exchange of assets across multiple parties. A settlement batch groups several legs (each leg moves value from one party to another) and either commits all of them together or none of them, so no party is left partially settled. - -At a high level the primitive covers: - -- **Atomic multi-leg settlement**: settle a batch of legs as a single all-or-nothing operation. -- **Value-moving settlement**: on settlement, receiver legs are credited, rather than only recording a receipt. -- **Holdings and transfer instructions**: asset holdings with optional locks, and the standard transfer-instruction flow. -- **Allocation lifecycle**: a request, instruction, and allocation flow, with cancel and withdraw paths. -- **Compliance hooks**: fail-closed reference hooks for node-level attestation, and a controlled seizure path for lawful process, gated behind explicit authority. - -## Status - -The [`openzeppelin-tokenCIP112-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments/token/tokenCIP112-v1) package in [`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts) implements this surface against the real Token Standard V2 interfaces, vendored as pinned DARs with recorded provenance. It is exercised three ways: in-memory Daml Script suites with a full coverage gate, a sandbox gate that runs token creation, transfer, and querying against a live ledger over the Ledger API, and interoperability exemplars (an ERC-20-style facade, a wallet-driven settlement lifecycle, and app-provider activity attribution) that run against a local Canton ledger. - -The research behind the primitive lives in [`OpenZeppelin/canton-specs`](https://github.com/OpenZeppelin/canton-specs), the research and incubation workspace: the [settlement experiments](https://github.com/OpenZeppelin/canton-specs/tree/main/experiments/settlement) hold the architecture, the threat model, and a regulated-settlement exemplar, while the [interoperability experiments](https://github.com/OpenZeppelin/canton-specs/tree/main/experiments/interoperability) hold the live-ledger evidence. - -## Related - -- [Reference Implementations](/canton/reference-implementations), which compose settlement into complete applications. -- [Library](/canton/library), the access-control, ownable, and pausable primitives that applications compose with settlement. diff --git a/src/navigation/canton.json b/src/navigation/canton.json index 4aa0f987..7b8a678f 100644 --- a/src/navigation/canton.json +++ b/src/navigation/canton.json @@ -32,14 +32,14 @@ "type": "page", "name": "Pausable", "url": "/canton/library/pausable" + }, + { + "type": "page", + "name": "Token (CIP-0112)", + "url": "/canton/library/token" } ] }, - { - "type": "page", - "name": "Settlement (CIP-112)", - "url": "/canton/settlement" - }, { "type": "page", "name": "Reference Implementations", From 4a966725e824bfa496c0e537ad343c34ba0573c9 Mon Sep 17 00:00:00 2001 From: Pepe Blasco Date: Fri, 25 Sep 2026 15:05:11 +0200 Subject: [PATCH 2/3] docs(canton): link the Pausable and Access Control replacement PRs --- content/canton/library/access-control.mdx | 11 +++++++++++ content/canton/library/pausable.mdx | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/content/canton/library/access-control.mdx b/content/canton/library/access-control.mdx index 12134002..ae432dcd 100644 --- a/content/canton/library/access-control.mdx +++ b/content/canton/library/access-control.mdx @@ -13,6 +13,17 @@ The package has no dependency on other OpenZeppelin packages. in `OpenZeppelin/canton-contracts`. + + This page documents the current experimental package. A replacement is in + review in [PR #48](https://github.com/OpenZeppelin/canton-contracts/pull/48) + and will be merged soon. The new `ScopedAuthorizationGrantV1` component + issues revocable, time-bounded grants. Before a protected choice runs, the + application checks the caller, the issuing authority, and the resource scope + of the grant. A treasury example in the PR builds role-based access control + on these grants. This page + will change when the new version is merged. + + ```daml import OpenZeppelin.AccessControlV1 ``` diff --git a/content/canton/library/pausable.mdx b/content/canton/library/pausable.mdx index 4c52921d..fe8313d9 100644 --- a/content/canton/library/pausable.mdx +++ b/content/canton/library/pausable.mdx @@ -13,6 +13,16 @@ The package has no dependency on other OpenZeppelin packages. in `OpenZeppelin/canton-contracts`. + + This page documents the current experimental package. A new version is in + review in [PR #46](https://github.com/OpenZeppelin/canton-contracts/pull/46) + and will be merged soon. It splits Pausable into an interface package, + `openzeppelin-api-pausable-v1`, and a guard package, `openzeppelin-pausable-v1`. + Your template holds the flag and implements the `Pausable` interface, and + gated choices call `whenNotPaused this`. This page will change when the new + version is merged. + + ```daml import OpenZeppelin.PausableV1 ``` From 012b9aef3d45f2786b557015d7038dee980bc17c Mon Sep 17 00:00:00 2001 From: Pepe Blasco Date: Fri, 25 Sep 2026 16:29:54 +0200 Subject: [PATCH 3/3] docs(canton): document the merged Pausable packages --- content/canton/get-started.mdx | 30 +++-- content/canton/index.mdx | 11 +- content/canton/library/index.mdx | 36 +++--- content/canton/library/pausable.mdx | 185 +++++++++++++++++++--------- 4 files changed, 169 insertions(+), 93 deletions(-) diff --git a/content/canton/get-started.mdx b/content/canton/get-started.mdx index 439f6cd1..2a44c3ec 100644 --- a/content/canton/get-started.mdx +++ b/content/canton/get-started.mdx @@ -5,8 +5,7 @@ title: Get Started This page shows how to set up the Daml toolchain, build the OpenZeppelin packages, and use them in your own Daml project. - The packages are experimental, unaudited, and have no release yet. They will - be redesigned before their first release. Pin a source commit of + The packages are unaudited and have no release yet. Pin a source commit of [`OpenZeppelin/canton-contracts`](https://github.com/OpenZeppelin/canton-contracts) and take version numbers from its package manifests, not from this page. @@ -35,22 +34,25 @@ dpm install dpm build --all ``` -To build one package, set `DAML_PACKAGE` to its directory: +To build one package, set `DAML_PACKAGE` to its directory. Build a package's dependencies first: ```bash -DAML_PACKAGE=experiments/security/pausable-v1 dpm build +DAML_PACKAGE=packages/security/api-pausable-v1 dpm build +DAML_PACKAGE=packages/security/pausable-v1 dpm build ``` -The packages live under `experiments/`, grouped by category: +The packages are grouped by status and category: | Directory | Contents | | --- | --- | -| `experiments/access/` | Access Control and Ownable | -| `experiments/security/` | Pausable | -| `experiments/token/` | Token (CIP-0112) | -| `experiments/test/` | One test package for each component | +| `packages/security/` | Pausable (pre-release) | +| `test/` | Test packages for `packages/` | +| `experiments/access/` | Access Control and Ownable (experimental) | +| `experiments/token/` | Token (CIP-0112) (experimental) | +| `experiments/test/` | Test packages for `experiments/` | +| `examples/` | Runnable projects that use the packages | -Each build writes its DAR to the `.daml/dist/` directory of the package, for example `experiments/security/pausable-v1/.daml/dist/openzeppelin-pausable-v1-0.1.0.dar`. +Each build writes its DAR to the `.daml/dist/` directory of the package, for example `packages/security/pausable-v1/.daml/dist/openzeppelin-pausable-v1-0.1.0.dar`. Released DARs will go in [`dars/released/`](https://github.com/OpenZeppelin/canton-contracts/tree/main/dars), with package IDs and SHA-256 digests in `dars/manifest.yaml`. Participant operators use these values to verify and vet the packages. @@ -69,19 +71,21 @@ dependencies: data-dependencies: - ../canton-contracts/experiments/access/access-control-v1/.daml/dist/openzeppelin-access-control-v1-0.1.0.dar - ../canton-contracts/experiments/access/ownable-v1/.daml/dist/openzeppelin-ownable-v1-0.1.0.dar - - ../canton-contracts/experiments/security/pausable-v1/.daml/dist/openzeppelin-pausable-v1-0.1.0.dar + - ../canton-contracts/packages/security/api-pausable-v1/.daml/dist/openzeppelin-api-pausable-v1-0.1.0.dar + - ../canton-contracts/packages/security/pausable-v1/.daml/dist/openzeppelin-pausable-v1-0.1.0.dar build-options: - --target=2.1 ``` -Change the paths to match your clone. List only the DARs that you use. +Change the paths to match your clone. List only the DARs that you use, plus their dependencies: `openzeppelin-pausable-v1` needs `openzeppelin-api-pausable-v1`. The [Token](/canton/library/token) package also needs the Token Standard V2 DARs from `dars/vendor/` in `data-dependencies`. The token page lists them. Import the modules and build: ```daml -import OpenZeppelin.PausableV1 +import OpenZeppelin.Api.PausableV1 (Pausable, PausableView (..)) +import qualified OpenZeppelin.PausableV1 as Pausable ``` ```bash diff --git a/content/canton/index.mdx b/content/canton/index.mdx index 8620dc2a..9c911dab 100644 --- a/content/canton/index.mdx +++ b/content/canton/index.mdx @@ -5,10 +5,11 @@ title: OpenZeppelin for Canton OpenZeppelin builds reusable, security-focused [Daml](https://docs.digitalasset.com/) packages for applications on the [Canton Network](https://www.canton.network/). This section documents the library packages and the Reference Implementations that use them. - Everything in this section is experimental. No package has a release yet, and - no package has an audit. The packages will be redesigned before their first - release, and the redesign will change module names, choice signatures, and - package IDs. Do not use them in production. + No package has a release or an audit yet. Do not use the packages in + production. Pausable is a pre-release package. Access Control, Ownable, and + Token are experiments: they will be redesigned before their first release, + and the redesign will change module names, choice signatures, and package + IDs. ## Library @@ -17,7 +18,7 @@ The packages live in [`OpenZeppelin/canton-contracts`](https://github.com/OpenZe - **[Access Control](/canton/library/access-control)**: role-based authorization. An admin issues role grants, and gated choices check the grant that the caller presents. - **[Ownable](/canton/library/ownable)**: single-owner authorization with a two-step ownership transfer. -- **[Pausable](/canton/library/pausable)**: an emergency stop that an authorized party can switch on and off. +- **[Pausable](/canton/library/pausable)**: an emergency stop. Your template holds the flag, and gated choices refuse to run while it is set. - **[Token (CIP-0112)](/canton/library/token)**: a token that implements the Token Standard V2 interfaces, with batch settlement, compliance hooks, and CIP-86 allowances. ## Reference Implementations diff --git a/content/canton/library/index.mdx b/content/canton/library/index.mdx index fad641e2..a0712a1e 100644 --- a/content/canton/library/index.mdx +++ b/content/canton/library/index.mdx @@ -5,34 +5,35 @@ title: Library The OpenZeppelin library for Canton brings the patterns of the OpenZeppelin Solidity contracts to Daml: role-based access control, ownership, an emergency stop, and a standard token. Each package follows the Canton authorization and privacy model. Where Daml differs from the EVM, the package page explains the difference. - Experimental. All packages are version 0.x and unaudited. They will be - redesigned and rewritten before they move from + No package has a release or an audit yet. Packages under + [`packages/`](https://github.com/OpenZeppelin/canton-contracts/tree/main/packages) + are pre-release. Packages under [`experiments/`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments) - to `packages/` in `OpenZeppelin/canton-contracts`. The rewrite will change - module names, template and choice signatures, and package IDs. There is no - upgrade path from the current packages. + will be redesigned and rewritten before they move to `packages/`. The rewrite + will change module names, template and choice signatures, and package IDs, + and there is no upgrade path from the experiments. ## Packages -| Package | Module | Solidity analogue | -| --- | --- | --- | -| [Access Control](/canton/library/access-control) | `OpenZeppelin.AccessControlV1` | `AccessControl`, `AccessControlDefaultAdminRules` | -| [Ownable](/canton/library/ownable) | `OpenZeppelin.OwnableV1` | `Ownable2Step` | -| [Pausable](/canton/library/pausable) | `OpenZeppelin.PausableV1` | `Pausable` | -| [Token (CIP-0112)](/canton/library/token) | `OpenZeppelin.TokenCIP112V1.*` | `ERC20` (partial) | +| Component | Module | Solidity analogue | Status | +| --- | --- | --- | --- | +| [Pausable](/canton/library/pausable) | `OpenZeppelin.Api.PausableV1`, `OpenZeppelin.PausableV1` | `Pausable` | Pre-release | +| [Access Control](/canton/library/access-control) | `OpenZeppelin.AccessControlV1` | `AccessControl`, `AccessControlDefaultAdminRules` | Experimental | +| [Ownable](/canton/library/ownable) | `OpenZeppelin.OwnableV1` | `Ownable2Step` | Experimental | +| [Token (CIP-0112)](/canton/library/token) | `OpenZeppelin.TokenCIP112V1.*` | `ERC20` (partial) | Experimental | ## Design Rules -**One package, one DAR.** Daml has no inheritance. The unit of reuse is the DAR. Each component is a separate package with its own DAR, and no OpenZeppelin package depends on another. Applications build, upload, and vet only the DARs they use. +**One package, one DAR.** Daml has no inheritance. The unit of reuse is the DAR. Each component is a separate package, or a pair of packages, with no dependency on other components. Applications build, upload, and vet only the DARs they use. **Versioned names.** A package is named `openzeppelin--v1`, and its module is `OpenZeppelin.V1`. A compatible upgrade (a smart contract upgrade, or SCU) keeps the package name and increments the package version. A breaking change creates a new `-v2` package with a `V2` module. -**Interfaces in a separate package.** A component that defines Daml interfaces puts them in a frozen `-api-v1` package, with the templates in a separate implementation package. The current components define no interfaces of their own, so each ships as one package. +**Interfaces in a separate package.** Daml cannot upgrade an interface. A component that defines Daml interfaces puts them in a frozen `openzeppelin-api--v1` package with modules under `OpenZeppelin.Api.V1`. The implementation package depends on it and can take SCU fixes. API packages depend only on other API packages. Pausable follows this model. A component with templates and no interfaces ships one package. **Composition in the application.** Implementation packages do not depend on each other. Applications combine them directly or through interfaces. -**No Daml Script in production DARs.** The shipped packages do not depend on `daml-script`. Each component has its own test package under `experiments/test/`. Test packages are never released or uploaded. +**No Daml Script in production DARs.** The shipped packages do not depend on `daml-script`. Each component has its own test package under `test/` or `experiments/test/`. Test packages are never released or uploaded. **Public and internal modules.** The documented modules are the public API. Implementation modules use an `.Internal` suffix. This is a naming convention; the ledger does not enforce it. @@ -40,7 +41,7 @@ The OpenZeppelin library for Canton brings the patterns of the OpenZeppelin Soli The packages are keyless: there are no contract keys and no global lookups. The application that uses a package must: -- Select the canonical contract for each protected resource, for example the one `Ownership` or `PauseState` contract that applies. +- Select the canonical contract for each protected resource, for example the one `Ownership` contract that applies. - Bind authority and state to that resource. - Disclose the contracts that other parties must read. - Review its complete dependency graph. @@ -50,9 +51,10 @@ The packages are keyless: there are no contract keys and no global lookups. The Build the packages, add the DARs as `data-dependencies`, and import the modules: ```daml +import OpenZeppelin.Api.PausableV1 (Pausable, PausableView (..)) +import qualified OpenZeppelin.PausableV1 as Pausable import OpenZeppelin.AccessControlV1 import OpenZeppelin.OwnableV1 -import OpenZeppelin.PausableV1 ``` -See [Get Started](/canton/get-started) for the full setup. +See [Get Started](/canton/get-started) for the full setup, and [`examples/`](https://github.com/OpenZeppelin/canton-contracts/tree/main/examples) for runnable projects. diff --git a/content/canton/library/pausable.mdx b/content/canton/library/pausable.mdx index fe8313d9..5e5a2139 100644 --- a/content/canton/library/pausable.mdx +++ b/content/canton/library/pausable.mdx @@ -2,94 +2,163 @@ title: Pausable --- -Pausable is an emergency stop for Daml. It is the Daml analogue of OpenZeppelin's `Pausable`: a boolean flag that an authorized party sets, and a guard (`whenNotPaused`) that gated choices call to refuse work while the flag is set. - -The package has no dependency on other OpenZeppelin packages. +Pausable is an emergency stop for Daml. It is the Daml analogue of OpenZeppelin's `Pausable`. Your template holds a `paused` flag and implements the `Pausable` interface. Gated choices call `whenNotPaused this` and refuse to run while the flag is set. - Experimental. Version 0.x, unaudited, and subject to a redesign before - release. Source: - [`experiments/security/pausable-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/experiments/security/pausable-v1) + Pre-release and unaudited. The packages have no release yet. Source: + [`packages/security/api-pausable-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/packages/security/api-pausable-v1) + and + [`packages/security/pausable-v1`](https://github.com/OpenZeppelin/canton-contracts/tree/main/packages/security/pausable-v1) in `OpenZeppelin/canton-contracts`. - - This page documents the current experimental package. A new version is in - review in [PR #46](https://github.com/OpenZeppelin/canton-contracts/pull/46) - and will be merged soon. It splits Pausable into an interface package, - `openzeppelin-api-pausable-v1`, and a guard package, `openzeppelin-pausable-v1`. - Your template holds the flag and implements the `Pausable` interface, and - gated choices call `whenNotPaused this`. This page will change when the new - version is merged. - +## Packages + +| Package | Module | Contents | +| --- | --- | --- | +| `openzeppelin-api-pausable-v1` | `OpenZeppelin.Api.PausableV1` | The `Pausable` interface and `PausableView`. Frozen. | +| `openzeppelin-pausable-v1` | `OpenZeppelin.PausableV1` | The guards and the failure statuses. Depends on the API package. | + +The interface is in its own package because Daml cannot upgrade an interface. A fix to a guard is a new version of `openzeppelin-pausable-v1`, and the interface package does not change. + +```daml +import OpenZeppelin.Api.PausableV1 (Pausable, PausableView (..)) +import qualified OpenZeppelin.PausableV1 as Pausable +``` + +## The Flag Is on Your Template + +Solidity stores `_paused` in the contract that it protects. Pausable does the same in Daml: the flag is a field of your template. A guard reads the flag from the contract that the choice runs on, so the caller cannot substitute a different switch. + +The package has no template of its own. The interface defines no choices, so a party that holds only a `ContractId Pausable` cannot change the flag. Your own choices change it. + +## Usage + +**1. Add the flag and the interface instance.** ```daml -import OpenZeppelin.PausableV1 +template Vault + with + admin : Party + owner : Party + balance : Decimal + paused : Bool + where + signatory admin, owner + + interface instance Pausable for Vault where + view = PausableView with paused ``` -## Pausing on a UTXO Ledger +**2. Guard the choices that a pause must stop.** Call `whenNotPaused this` before the choice changes state: -Solidity stores `_paused` in contract storage. Daml-LF 2.1 has no contract keys, so there is no global lookup for the flag. The flag is a `PauseState` contract instead. A gated choice fetches it by contract ID and calls `whenNotPaused`. +```daml + choice Vault_Withdraw : ContractId Vault + with amount : Decimal + controller owner + do + Pausable.whenNotPaused this + create this with balance = balance - amount +``` -A pause stops new operations that check the flag. It has no effect on transactions that are already committed. To change the flag, `PauseState_Set` archives the current contract and creates a new one with a new contract ID. Applications must always use the ID of the current `PauseState`. +**3. Write the pause and unpause choices.** The controller and the body of these choices define who can pause. Each choice guards, then creates the next contract with `paused` changed: -## Templates +```daml + choice Vault_Pause : ContractId Vault + controller admin + do + Pausable.whenNotPaused this + create this with paused = True -### `PauseState` + choice Vault_Unpause : ContractId Vault + controller admin + do + Pausable.whenPaused this + create this with paused = False +``` -The pause state of a resource. +Make these choices consuming, or call `archive self`, so that one contract stays active. Any authority model fits: a single party, a role grant, an M-of-N approval, or a timelock. For a role, take the caller and its credential as choice arguments and check them before the flip. -| Field | Type | Description | +## Functions + +All functions take any template that implements `Pausable`. + +- **`whenNotPaused : HasToInterface t Pausable => t -> Update ()`**: fails with `eEnforcedPause` if the contract is paused. Use it in gated choices and in the pause choice. +- **`whenPaused : HasToInterface t Pausable => t -> Update ()`**: fails with `eExpectedPause` if the contract is not paused. Use it in the unpause choice and in choices that run only during an incident, such as an emergency drain. +- **`isPaused : HasToInterface t Pausable => t -> Bool`**: returns the flag, for a choice that branches instead of failing. + +## Failure Statuses + +Each guard fails with a `FailureStatus`. A Ledger API or JSON Ledger API client receives a `DAML_FAILURE` error with the `errorId` below. Match on the `errorId`, not on the message. + +| Status | `errorId` | Message | | --- | --- | --- | -| `pauser` | `Party` | The party that can pause and unpause. | -| `paused` | `Bool` | `True` if the resource is paused. | +| `eEnforcedPause` | `openzeppelin.com/pausable-enforced-pause` | `Pausable: the contract is paused` | +| `eExpectedPause` | `openzeppelin.com/pausable-expected-pause` | `Pausable: the contract is not paused` | + +In Daml Script, compare the whole status: -Signatory `pauser`, no observers. Other parties read the contract through explicit disclosure. +```daml +Left (FailureStatusError status) <- + trySubmit owner do exerciseCmd vault Vault_Withdraw with amount = 1.0 +status === Pausable.eEnforcedPause +``` -- **`PauseState_Set`** (controller `pauser`, consuming, returns `ContractId PauseState`): takes `newPaused : Bool`, archives the current contract, and creates a new one with `paused = newPaused`. Fails if `newPaused` equals the current value. -- **`PauseState_Get`** (controller `pauser`, nonconsuming, returns `Bool`): returns the flag. Other parties do not call this choice; they fetch the contract and read `paused`. +## Reading the Flag Off-Ledger -The template has no archive choice. `pauser` can still archive the contract directly, because it is the signatory. After that, guarded choices that reference the archived ID fail. +A wallet, a registry endpoint, or an auditor reads `PausableView` through the interface, without knowing the template. In Daml Script: -## Helper Functions +```daml +Some v <- queryInterfaceContractId reader (toInterfaceContractId @Pausable cid) +v.paused === True +``` -- **`whenNotPaused : PauseState -> Update ()`**: the `whenNotPaused` modifier analogue. Fails the transaction if the fetched state is paused. -- **`isPaused : PauseState -> Bool`**: the same check as a pure predicate. +## Adding Pausable to an Existing Template -## Guarding a Choice +A template with active contracts adds Pausable in its next Smart Contract Upgrade (SCU) version. SCU accepts a new field only as an `Optional` at the end of the record, so the flag is `paused : Optional Bool`, and the view reads `None` as unpaused: ```daml -template Token - with - issuer : Party - owner : Party - amount : Int - where - signatory issuer, owner +import DA.Optional (fromOptional) - choice Transfer : ContractId Token - with - pauseStateCid : ContractId PauseState - to : Party - controller owner, to - do - ps <- fetch pauseStateCid - assertMsg "wrong pauser" (ps.pauser == issuer) - whenNotPaused ps - create this with owner = to + interface instance Pausable for UpgradedVault where + view = PausableView with paused = fromOptional False paused ``` -Two details make this example correct: +The pause and unpause choices store `Some True` and `Some False`. By default, `damlc` rejects a new interface instance on an existing template with `template-has-new-interface-instance`. Review the old-version behavior below, then build with `-Wno-template-has-new-interface-instance`. + +The choices of the old version have no guard, so a submission that selects the old version skips the pause. After a flip stores `Some True` or `Some False`, the old version cannot read the contract, and such a submission fails with an upgrade error. If clients of the old version must resume after an unpause, store `None` on unpause. Unvet the old version when all clients use the new one. + +The [`examples/pausable/`](https://github.com/OpenZeppelin/canton-contracts/tree/main/examples/pausable) directory has runnable projects for a vault, a registry, and this retrofit. + +## Security Considerations + +- **Per-contract switch.** Each contract has its own flag. To pause several templates together, give each a flag, or route every protected operation through one contract that holds the flag. +- **One guard per choice.** A choice is gated only if it calls the guard. Call `whenNotPaused` before the state change in every choice that a pause must stop. The guard checks only the flag, so keep each choice's controller as its access control. +- **Signatories bypass the flip choice.** The template's `Archive` has no guard. Its signatories can archive a paused contract and create it again with any flag value, without your pause authority checks. Make every signatory part of your pause authority, or trust it with the flag. +- **Pass `this`.** Do not guard with a `Pausable` fetched from a contract ID that the caller supplies. The caller can present an unpaused contract, and the gated choice runs. +- **Origination control.** A pause stops new exercises of gated choices. It does not affect committed transactions. +- **CIP-0112 fields.** `PausableView` carries only `paused`. A registry that serves CIP-0112 `reason` and `until` keeps them as its own template fields; see `examples/pausable/registry`. -- **Authority for the fetch.** A `fetch` needs the authority of a stakeholder of the fetched contract. Here `issuer` is the pauser and a signatory of `Token`, so the choice has that authority. Disclosure makes a contract visible, but it does not give this authority. -- **The correct `PauseState`.** The package cannot check that a presented `PauseState` is the current state for your resource. A caller can present any `PauseState`, for example an unpaused one from another pauser. Check `ps.pauser`, and define how your application selects and discloses the canonical state. +## Upgrades -## Errors +- A fix to `openzeppelin-pausable-v1` reaches your contracts in your next SCU version, built against the new DAR. A submission that selects your old version still runs the old guard, so unvet it. Each `errorId` stays the same across versions. +- Your template stays upgradeable, because the interface instance is on your template. +- To adopt a future `openzeppelin-api-pausable-v2`, add a second interface instance under SCU and keep both, or create a new template version outside SCU and migrate the contracts offline. The migration must copy the flag. -| Message | Cause | -| --- | --- | -| `Pausable: paused` | A guarded choice ran while the resource was paused. | -| `Pausable: redundant pause change` | `PauseState_Set` received the current value. | +## Build and Consume + +From the root of `canton-contracts`: + +```bash +DAML_PACKAGE=packages/security/api-pausable-v1 dpm build +DAML_PACKAGE=packages/security/pausable-v1 dpm build +``` + +```yaml +data-dependencies: + - ../canton-contracts/packages/security/api-pausable-v1/.daml/dist/openzeppelin-api-pausable-v1-0.1.0.dar + - ../canton-contracts/packages/security/pausable-v1/.daml/dist/openzeppelin-pausable-v1-0.1.0.dar +``` ## Related