Daml Smart Contract Vulnerabilities: 8 Authorization and Privacy Risks
Daml prevents many low level smart contract bugs, but it does not decide whether a business workflow gives the right parties the right authority.
Daml prevents many low-level smart contract bugs, but it does not decide whether a business workflow gives the right parties the right authority. This guide explains eight Daml security risks, how to recognize them during review, and how to test the intended fix.
TL;DR
- Most serious Daml vulnerabilities are authorization, privacy, and business-logic errors rather than EVM-style memory or callback bugs.
- Review every template by asking who can create it, archive it, observe it, and exercise each choice.
- A signatory lends authority to the consequences of choices on its contract. A narrowly named choice can therefore grant broader power than its controller appears to have.
nonconsumingchoices are repeatable. Do not use them for redemption, payment, issuance, or another one-time effect.ensure,assert, and negative Daml Script tests are required for economic invariants; the type checker cannot infer business rules.- Contract-key behavior is version-dependent. Do not treat a negative lookup as proof that no matching contract exists elsewhere.
This article is for Daml developers and reviewers searching for a Daml smart contract security checklist or examples of Daml authorization vulnerabilities. Readers who first need the language mechanics should start with Writing Correct Daml Contracts on Canton. Teams moving from Solidity should also read the EVM-to-Canton development guide.
Table of Contents
- Why Daml Has a Different Threat Model
- 1. Weak Signatories and Unilateral Archival
- 2. Flexible Controller Escalation
- 3. Excess Authority in Choice Consequences
- 4. Repeatable Effects in Nonconsuming Choices
- 5. Missing Economic and State Invariants
- 6. Privacy Leaks Through Observers and Transaction Consequences
- 7. Unsafe Contract-Key Assumptions
- 8. Business-Logic Regressions During Upgrades
- Daml and Solidity: Different Review Priorities
- Daml Security Review Checklist
- FAQ
Why Daml Has a Different Threat Model
Daml is developed by Digital Asset for multi-party applications. A Daml ledger stores immutable contract instances. State changes occur by creating contracts and exercising choices, often consuming an old contract and creating a replacement.
The language and ledger model enforce authorization rules. Creation requires the authority of a contract's signatories, exercising a choice requires its controllers, and the consequences of an exercise receive authority from both the choice actors and the signatories of the contract being exercised. The official parties and authority tutorial demonstrates these rules with the same IOU pattern used below.
That enforcement is important, but it only proves that a transaction follows the authorization model written in the templates. It cannot prove that the model matches the parties' real agreement. If an administrator is made a signatory, an operator is made a controller, or a counterparty is added as an observer, the runtime assumes those declarations are intentional.
Daml security review map showing authority, state, and privacy questions
The practical review unit is not an isolated function. It is the full transaction tree produced by a choice:
- Which parties authorize the root command?
- Which signatories add authority inside the choice body?
- Which contracts are created, fetched, exercised, or archived below it?
- Which parties learn the parent action and its consequences?
- Can the same effect occur twice or under an unexpected package version?
The following examples are deliberately small. In a real application, the dangerous action may be several nested exercises away from the choice that a user sees.
1. Weak Signatories and Unilateral Archival
The risk
Signatories must authorize contract creation and are controllers of the implicit Archive choice. If an issuer is the only signatory on an IOU, the issuer can archive it without the owner's consent. Making the owner an observer gives the owner visibility, not veto power.
01template SimpleIou02 with03 issuer : Party04 owner : Party05 amount : Decimal06 where07 signatory issuer08 observer owner09 ensure amount > 0.0After the owner provides goods or another off-ledger consideration, the issuer can submit:
01submit issuer do02 archiveCmd iouIdThis is not a runtime bypass. The model explicitly permits it. The misconception is assuming that an observer's interest in a contract gives that party control over its lifetime.
The fix
If both parties must approve destruction or replacement of the obligation, both need to be signatories:
01template Iou02 with03 issuer : Party04 owner : Party05 amount : Decimal06 where07 signatory issuer, owner08 ensure amount > 0.0Co-signing also means the owner must authorize creation. Use a propose-accept workflow when the issuer must first publish an offer that the owner can inspect and accept. Do not add co-signatories mechanically: every signatory contributes authority inside choices, which creates the next class of risk.
Review question: Which party would suffer if this contract disappeared, and does that party either sign the contract or explicitly trust every signatory?
2. Flexible Controller Escalation
The risk
A flexible controller is calculated from choice arguments at exercise time. This is useful when the controller is not known at contract creation, but an unchecked party argument can let any party with visibility nominate itself.
01template RoleRequest02 with03 admin : Party04 nominee : Party05 reviewers : [Party]06 where07 signatory admin08 observer nominee, reviewers09 10 choice ClaimRole : ContractId OperatorRole11 with12 claimant : Party13 controller claimant14 do15 create OperatorRole with16 admin17 operator = claimantIf a reviewer can see RoleRequest, that reviewer can pass itself as claimant. The participant still checks that the reviewer is authorized to act as its own party; the bug does not permit party impersonation. The problem is that the template accepts any visible party as the controller and then records that party as the operator.
The fix
Use the party stored in the contract when it is known:
01 choice ClaimRole : ContractId OperatorRole02 controller nominee03 do04 create OperatorRole with05 admin06 operator = nomineeIf the workflow genuinely needs a flexible controller, bind it to contract state with assertMsg and test every unintended visible party with submitMustFail. The Daml choice reference also notes that controllers need contract visibility; visibility is a prerequisite for exercising a choice, not a substitute for an authorization rule.
Review question: Is any controller derived from a choice argument, and what prevents a visible but unintended party from supplying itself?
3. Excess Authority in Choice Consequences
The risk
The consequences of a choice are authorized by its actors plus the signatories of the contract on which it is exercised. This enables multi-party workflows, but it can also turn a limited operator role into a broad delegation.
01template OperatorRole02 with03 admin : Party04 operator : Party05 where06 signatory admin07 observer operator08 09 nonconsuming choice IssueCredit : ContractId Credit10 with11 recipient : Party12 amount : Decimal13 controller operator14 do15 create Credit with16 issuer = admin17 owner = recipient18 amountExercising IssueCredit makes both operator and admin available as authorizers inside the choice body. If Credit only requires admin as a signatory, the create is authorized even though the administrator did not submit that individual issuance. The administrator consented earlier by creating OperatorRole.
Calling this "impersonation" would be inaccurate. It is delegated authority encoded by the template. It becomes a vulnerability when the delegation is wider than the administrator understood, for example because there is no cap, asset restriction, recipient allowlist, expiry, or revocation mechanism.
The fix
Replace generic execution roles with choices that encode the smallest useful permission:
- Store a maximum amount and expiry on the role contract.
- Restrict the asset type and permitted recipients.
- Make high-impact actions consuming so authorization is one-time.
- Require a separate proposal and acceptance for each issuance when the administrator must approve every action.
- Provide a consuming revocation path controlled by the delegator.
Then inspect every create, exercise, and archive in the choice body. For each action, write down which parties authorize it and where that authority came from. Authority is local to the current choice context; it is not inherited transitively through unrelated earlier exercises.
Review question: What can this choice create or exercise using its contract signatories' authority, not just its controller's authority?
4. Repeatable Effects in Nonconsuming Choices
The risk
Choices are consuming by default. An explicitly nonconsuming choice leaves its contract active, so it can be exercised repeatedly. That is correct for queries and repeatable notifications, but unsafe for a one-time redemption or issuance.
01template Voucher02 with03 issuer : Party04 holder : Party05 value : Decimal06 where07 signatory issuer08 observer holder09 ensure value > 0.010 11 nonconsuming choice Redeem : ContractId Payment12 controller holder13 do14 create Payment with15 payer = issuer16 payee = holder17 amount = valueThe holder can exercise Redeem again because the voucher remains active. Command deduplication does not repair this model: five intentionally distinct commands are still five valid exercises.
The fix
Remove nonconsuming so the default consuming behavior archives the voucher on the first successful redemption:
01 choice Redeem : ContractId Payment02 controller holder03 do04 create Payment with05 payer = issuer06 payee = holder07 amount = valueAlso test the second exercise with submitMustFail. Conversely, check that read-only choices are explicitly nonconsuming; accidentally consuming a registry during a lookup is a denial-of-service or availability bug rather than a double issuance.
Review question: If this choice succeeds twice on the same contract ID, does value, authority, or an external side effect occur twice?
5. Missing Economic and State Invariants
The risk
Daml's types distinguish parties, contract IDs, text, integers, and fixed-point numbers, but they do not know that a token amount must be positive or that a withdrawal must not exceed a balance. Those rules need to appear as template preconditions and choice assertions.
01template Token02 with03 issuer : Party04 owner : Party05 amount : Decimal06 where07 signatory issuer08 observer owner09 10 choice Split : (ContractId Token, ContractId Token)11 with12 splitAmount : Decimal13 controller owner14 do15 left <- create this with amount = splitAmount16 right <- create this with amount = amount - splitAmount17 pure (left, right)With no guards, a negative splitAmount creates one negative token and another token larger than the original. This is a semantic failure, not arithmetic overflow. Daml Int and Numeric arithmetic raises an error for overflow, and Int division by zero also fails, as documented in the Daml data type reference. Checked arithmetic does not make an economically invalid result valid.
The fix
Enforce invariants at both boundaries:
01template Token02 with03 issuer : Party04 owner : Party05 amount : Decimal06 where07 signatory issuer08 observer owner09 ensure amount > 0.010 11 choice Split : (ContractId Token, ContractId Token)12 with13 splitAmount : Decimal14 controller owner15 do16 assertMsg "split must be positive" (splitAmount > 0.0)17 assertMsg "split must be below total" (splitAmount < amount)18 left <- create this with amount = splitAmount19 right <- create this with amount = amount - splitAmount20 pure (left, right)ensure protects every creation path, including paths added later. Choice assertions provide a precise failure close to the invalid input. Test zero, negative, equal-to-total, just-below-total, maximum supported values, and any rounding boundary used in fees or allocation.
Review question: Which relationships between fields must always hold, and are they enforced whenever a contract is created or replaced?
6. Privacy Leaks Through Observers and Transaction Consequences
The risk
Daml privacy is scoped by stakeholders and transaction views; it is not a field-level redaction system. An observer sees the contract payload. In addition, a party informed about an action sees the transitive consequences needed to validate that action. The formal Daml ledger privacy model defines informees separately for creates, consuming and nonconsuming exercises, and fetches.
Two recurring mistakes follow:
- A template adds a regulator, operator, or downstream counterparty as an observer even though that party needs only a reference or status.
- A choice fetches or exercises a sensitive contract below an action visible to a broader set of parties, revealing more of the transaction tree than the designer expected.
For example, adding a settlement operator as an observer to a trade contract exposes the entire payload, including notional, price, and both counterparties. A UI that hides those fields does not change ledger visibility.
The fix
Split contracts by visibility scope. Keep sensitive trade terms on a contract whose stakeholders need the full payload, and create a separate settlement instruction containing only the fields the operator needs. Review the generated transaction tree with each party's view rather than reading template declarations in isolation.
Explicit contract disclosure can support deliberate off-ledger sharing with a non-stakeholder. It attaches authenticated contract data to a command so the receiving party can use that contract during interpretation. It does not add authority, redact the payload, or make an unintended recipient safe. Use the official explicit disclosure guide for the platform version you deploy.
Review question: What exact data does each party learn from the contract payload and every consequence below the action it observes?
7. Unsafe Contract-Key Assumptions
The risk
Contract-key support and semantics have changed across Canton releases. Current Canton documentation describes keys as under development for newer 3.x releases, while older Daml SDK documentation describes unique keys with maintainer-based lookup rules. Check the documentation and feature flags for the exact participant and synchronizer version being deployed; do not copy a Daml 2.x key pattern into a Canton 3.x application without confirming support.
Where stakeholder-scoped lookup semantics apply, fetchByKey and lookupByKey resolve contracts on a best-effort basis from the submitting participant's view. A None result does not prove that no matching contract exists outside that view. Likewise, a divulged contract may be fetchable by contract ID but unavailable through a key lookup.
The earlier pattern below is therefore unsafe when used as a global uniqueness or authorization test:
01existing <- lookupByKey @Username (registrar, name)02case existing of03 None -> create Username with registrar; owner; name04 Some _ -> abort "name already exists"Two misconceptions should be separated:
- A maintainer that cannot be derived from the key is a model validity error, not a subtle production exploit that silently disables uniqueness.
- A valid key declaration still does not justify treating a local negative lookup as universal knowledge across independent privacy domains or synchronizers.
The fix
First confirm whether keys are supported and what uniqueness scope the deployed Canton version provides. The current Canton ledger model overview marks the feature status and should be checked alongside release-specific documentation.
For older environments with unique keys, include the maintainer in the key, make the maintainer a signatory, ensure the submitting party has the required visibility, and serialize creation through an authoritative maintainer-controlled workflow. For environments without the required key guarantees, use an explicit registry contract or another authoritative allocation service and document its trust and concurrency assumptions.
Review question: Is code treating a participant-local negative lookup as a global fact, or relying on key behavior that the deployed Canton version does not provide?
8. Business-Logic Regressions During Upgrades
The risk
Daml smart contract upgrade checks protect structural compatibility, but they cannot decide whether a changed choice still implements the parties' agreement. An upgrade can preserve types while removing a limit, changing a fee formula, or widening the consequences of an operator-controlled choice.
Consider a version 1 choice that checks a credit limit:
01choice Draw : ContractId Credit02 with03 amount : Decimal04 controller borrower05 do06 assertMsg "credit limit exceeded" (amount <= remainingLimit)07 create Credit with remainingLimit = remainingLimit - amountA version 2 choice with the same name and argument types can omit the assertion or calculate remainingLimit differently. The change may be structurally upgrade-compatible while violating the original economic property. Depending on package selection, existing contract data can be interpreted under newer code.
The fix
Express important business properties as tests that run against every supported package version and against contracts created with earlier versions. The official smart contract upgrade guide explains that contract metadata is recomputed and the ensure clause is re-evaluated during use. That validation is necessary but does not replace invariant tests.
Before rollout:
- Diff choice bodies and authorization expressions, not only serialized types.
- Run old contract fixtures against the new package.
- Test package-selection behavior across all participating organizations.
- Verify that signatories, observers, and preconditions compute consistently for existing data.
- Add rollback or migration procedures for changes that cannot be made compatibility-preserving.
Review question: Which security properties could change even though the package passes upgrade compatibility checks?
Daml and Solidity: Different Review Priorities
| Review area | Solidity/EVM emphasis | Daml/Canton emphasis |
|---|---|---|
| Calls and composition | External calls, callbacks, reentrancy guards | Nested choices, local authorization, transaction consequences |
| State change | Mutable contract storage | Consuming old contracts and creating replacements |
| Access control | Modifiers, role mappings, proxy admin | Signatories, observers, controllers, choice observers |
| Arithmetic | Precision, rounding, checked/unchecked blocks | Precision, rounding, failed arithmetic, domain invariants |
| Visibility | Public state unless cryptography or another privacy layer is used | Stakeholder and sub-transaction visibility; unintended disclosure |
| Upgrades | Proxy storage layout and implementation control | Package compatibility, selection, old contract data, changed choice behavior |
| Uniqueness | Storage mappings and explicit registries | Version-specific contract-key or registry semantics |
The comparison does not mean one platform requires less review. It changes where review time should go. A Daml reviewer should spend less time searching for classic external callback reentrancy and more time tracing authority and visibility across the complete transaction tree.
Daml Security Review Checklist
Intent and roles
- [ ] Is the real-world role of every party field documented?
- [ ] Is there a roles table covering create, archive, observe, and exercise rights?
- [ ] Does every party harmed by unilateral archival sign the contract or explicitly trust its signatories?
Choices and authority
- [ ] Is every flexible controller bound to an intended party?
- [ ] Have all nested
create,exercise,fetch, andarchiveactions been mapped to their authorizers? - [ ] Are delegated roles bounded by operation, amount, asset, recipient, and time?
- [ ] Is there a revocation path for standing authority?
- [ ] Are one-time effects consuming?
State and arithmetic
- [ ] Does each template use
ensurefor invariants that must hold on every creation? - [ ] Are choice inputs checked before state replacement?
- [ ] Do tests cover zero, negative, equality, limits, precision, and rounding?
- [ ] Does every consuming state transition create the intended replacement?
Privacy
- [ ] Is every observer required to see the full payload?
- [ ] Have party-specific transaction views been inspected?
- [ ] Are sensitive terms separated from operational summaries?
- [ ] Is explicit disclosure limited to deliberate recipients and authenticated contract data?
Keys and upgrades
- [ ] Does the deployed Canton version support the key semantics the model assumes?
- [ ] Is a negative key lookup ever treated as global proof of absence?
- [ ] Do upgrade tests run old contracts under new package code?
- [ ] Have changes to choice bodies, observers, signatories, and
ensureexpressions been reviewed as security changes?
Tests
- [ ] Does every sensitive choice have success tests for intended parties and
submitMustFailtests for unintended parties? - [ ] Is repeat exercise tested for consuming choices?
- [ ] Are privacy expectations tested or inspected per party?
- [ ] Does each regression test name the security property it protects?
Summary
Daml enforces the authorization model that developers write. The central security question is therefore not whether the runtime will ignore signatory or controller, but whether those declarations and their transaction consequences match the intended agreement.
A useful review traces four things for every choice: authority, state, visibility, and repeatability. Add version and upgrade behavior when the application uses contract keys or multiple package versions. Those checks catch the Daml-specific failures that a generic EVM checklist is likely to miss.