Pausable

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.

Pre-release and unaudited. The packages have no release yet. Source: packages/security/api-pausable-v1 and packages/security/pausable-v1 in OpenZeppelin/canton-contracts.

Packages

PackageModuleContents
openzeppelin-api-pausable-v1OpenZeppelin.Api.PausableV1The Pausable interface and PausableView. Frozen.
openzeppelin-pausable-v1OpenZeppelin.PausableV1The 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.

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.

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

2. Guard the choices that a pause must stop. Call whenNotPaused this before the choice changes state:

    choice Vault_Withdraw : ContractId Vault
      with amount : Decimal
      controller owner
      do
        Pausable.whenNotPaused this
        create this with balance = balance - amount

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:

    choice Vault_Pause : ContractId Vault
      controller admin
      do
        Pausable.whenNotPaused this
        create this with paused = True

    choice Vault_Unpause : ContractId Vault
      controller admin
      do
        Pausable.whenPaused this
        create this with paused = False

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.

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.

StatuserrorIdMessage
eEnforcedPauseopenzeppelin.com/pausable-enforced-pausePausable: the contract is paused
eExpectedPauseopenzeppelin.com/pausable-expected-pausePausable: the contract is not paused

In Daml Script, compare the whole status:

Left (FailureStatusError status) <-
  trySubmit owner do exerciseCmd vault Vault_Withdraw with amount = 1.0
status === Pausable.eEnforcedPause

Reading the Flag Off-Ledger

A wallet, a registry endpoint, or an auditor reads PausableView through the interface, without knowing the template. In Daml Script:

Some v <- queryInterfaceContractId reader (toInterfaceContractId @Pausable cid)
v.paused === True

Adding Pausable to an Existing Template

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:

import DA.Optional (fromOptional)

    interface instance Pausable for UpgradedVault where
      view = PausableView with paused = fromOptional False paused

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/ 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.

Upgrades

  • 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.

Build and Consume

From the root of canton-contracts:

DAML_PACKAGE=packages/security/api-pausable-v1 dpm build
DAML_PACKAGE=packages/security/pausable-v1 dpm build
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