Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions sdk/templates/multi-party-agreement/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Multi-Party Agreement Tutorial

Learn Canton's unique party-based authorization model by building a collaborative agreement contract.

## What You'll Learn

- How Canton's **party model** differs from address-based blockchains
- Dynamic signatory lists and authorization
- Observer pattern for selective visibility
- Multi-party workflows without complex coordination

## The Problem

On public blockchains like Ethereum, authorization is simple: if you have the private key for an address, you can sign transactions. But what if you need multiple parties to coordinate on a decision?

Canton solves this with a **party-based authorization model** where contracts explicitly declare who must authorize actions.

## The Contract

```daml
template MultiPartyAgreement
with
proposer : Party
signatories : [Party]
requiredParties : [Party]
terms : Text
where
signatory signatories
observer requiredParties
```

### Key Concepts

**Signatories** (`signatory signatories`)
- Parties who have already signed the agreement
- ALL signatories must authorize any changes to this contract
- This is a **list that grows** as parties join

**Observers** (`observer requiredParties`)
- Parties who can see the contract but haven't signed yet
- They need visibility to exercise the `AddParty` choice
- Without observer status, Bob couldn't even see the agreement to join it

**Why both?**
- `signatories` = who has committed
- `requiredParties` = who is invited but hasn't committed yet

## The Workflow

### Step 1: Alice Proposes

```daml
agreementCid <- submit alice do
createCmd MultiPartyAgreement with
proposer = alice
signatories = [alice] -- Only Alice has signed
requiredParties = [bob, carol] -- Bob and Carol can see it
terms = "We agree to collaborate on this project"
```

**What happens:**
- Alice creates the agreement
- She's the only signatory (she authorized creation)
- Bob and Carol are observers (they can see it but haven't signed)

### Step 2: Bob Joins

```daml
agreementCid <- submit bob do
exerciseCmd agreementCid AddParty with newParty = bob
```

**What happens:**
- Bob exercises the `AddParty` choice
- The choice controller is `newParty` (Bob), so he must authorize
- A new contract is created with `signatories = [bob, alice]`
- The old contract is archived (consumed by the choice)

### Step 3: Carol Joins

```daml
submit carol do
exerciseCmd agreementCid AddParty with newParty = carol
```

**Final state:**
- `signatories = [carol, bob, alice]`
- All three parties have now authorized the agreement

## The Choice

```daml
choice AddParty : ContractId MultiPartyAgreement
with
newParty : Party
controller newParty
do
assertMsg "Party not in required list" (newParty `elem` requiredParties)
assertMsg "Party already signed" (newParty `notElem` signatories)
create this with signatories = newParty :: signatories
```
8 changes: 8 additions & 0 deletions sdk/templates/multi-party-agreement/daml.yaml.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
sdk-version: __VERSION__
name: __PROJECT_NAME__
source: daml/Agreement.daml
version: 1.3.0
dependencies:
- daml-prim
- daml-stdlib
- daml-script
52 changes: 52 additions & 0 deletions sdk/templates/multi-party-agreement/daml/Agreement.daml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module Agreement where

import Daml.Script

template MultiPartyAgreement
with
proposer : Party
signatories : [Party]
requiredParties : [Party]
terms : Text
where
signatory signatories
observer requiredParties

choice AddParty : ContractId MultiPartyAgreement
with
newParty : Party
controller newParty
do
assertMsg "Party not in required list" (newParty `elem` requiredParties)
assertMsg "Party already signed" (newParty `notElem` signatories)
create this with signatories = newParty :: signatories

choice Close : ()
controller proposer
do
pure ()

-- Tests
test_agreement : Script ()
test_agreement = script do
alice <- allocateParty "Alice"
bob <- allocateParty "Bob"
carol <- allocateParty "Carol"

-- Alice proposes agreement
agreementCid <- submit alice do
createCmd MultiPartyAgreement with
proposer = alice
signatories = [alice]
requiredParties = [bob, carol]
terms = "We agree to collaborate on this project"

-- Bob joins
agreementCid <- submit bob do
exerciseCmd agreementCid AddParty with newParty = bob

-- Carol joins
submit carol do
exerciseCmd agreementCid AddParty with newParty = carol

pure ()
104 changes: 104 additions & 0 deletions sdk/templates/simple-token-utility/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Simple Token Tutorial

Learn how to build fungible tokens on Canton using patterns from the CIP-56 Canton Token Standard.

## What You'll Learn

- UTXO-style asset management
- Proposal/acceptance pattern for transfers (CIP-56)
- Split and merge operations
- Token holder privacy model
- Observer pattern for issuer oversight

## The Token Holding

```daml
template TokenHolding
with
issuer : Party
owner : Party
amount : Decimal
instrument : Text
where
signatory owner
observer issuer
```

### Key Design Decisions

**Why owner is the only signatory?**
- Owner controls their assets
- Can propose transfers without issuer approval
- Standard pattern for bearer tokens

**Why issuer is an observer?**
- Issuer sees all holdings (for compliance, total supply)
- Issuer doesn't control transfers (can't freeze without owner consent)
- Balance between privacy and transparency
agement |

## The Transfer Flow (CIP-56 Pattern)

### Why Proposal + Acceptance?

**Problem:** Alice can't just create a holding for Bob
```daml
-- This FAILS - Bob must authorize becoming owner (signatory)
create TokenHolding with owner = bob, ...
```

**Solution:** Two-step workflow

### Step 1: Alice Proposes Transfer

```daml
choice ProposeTransfer : ContractId TransferProposal
with
newOwner : Party
transferAmount : Decimal
controller owner
do
-- Create remainder for Alice
if transferAmount < amount
then create this with amount = amount - transferAmount
else pure ()

-- Create proposal for Bob
create TransferProposal with
sender = owner
receiver = newOwner
transferAmount
...
```

**What happens:**
- Alice's 100 token holding is **consumed** (archived)
- A 70 token holding is **created** for Alice (remainder)
- A **transfer proposal** is created for Bob to accept

### Step 2: Bob Accepts

```daml
template TransferProposal
where
signatory sender
observer receiver -- Bob can see the proposal

choice Accept : ContractId TokenHolding
controller receiver
do
create TokenHolding with
owner = receiver
amount = transferAmount
...
```

**What happens:**
- Bob exercises `Accept` (he authorizes)
- A 30 token holding is **created** for Bob
- The proposal is **consumed**

**Final state:**
- Alice: 70 token holding
- Bob: 30 token holding
- Both authorized their own holdings
8 changes: 8 additions & 0 deletions sdk/templates/simple-token-utility/daml.yaml.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
sdk-version: __VERSION__
name: __PROJECT_NAME__
source: daml/Token.daml
version: 1.3.0
dependencies:
- daml-prim
- daml-stdlib
- daml-script
Loading