Q2 2026 Security & Compliance Report67 incidents, $764M in losses, 88% from operational failures.
Get the report →

Security Review Scope for Daml: Coverage, Methods, and EVM Comparison

8 min read

You start a Daml review the way you start an EVM one: Foundry and ethereum.org already named the methods. This article follows that pass into daml.yaml, through four places the imported catalog writes the wrong line, and ends with five questions that pin the scope.

You open daml.yaml with a Foundry pass in hand

The first habit is to keep that Foundry pass as the method list. Open daml.yaml before you import it.

You clone the repo, or you just dpm init, with a Foundry pass already in hand. Foundry documents a fork pinned to a block, vm.prank for the caller, and vm.warp for time. ethereum.org documents unit tests plus property-based analysis, fuzzing, formal verification, and an independent review. Those two pages make no Daml claim.

Then you open daml.yaml. On key-fetch-proof, that file sets sdk-version: 3.5.2 and --target=2.3. The compiled unit is a DAR, a Daml Archive. Daml Script starts on a fresh, empty ledger. dpm test tells you which templates were created and which choices were exercised.

The Daml SDK 2.10.6 pages and the current Canton docs (September 2026) do not publish a third-party review-scope checklist. They do not name a Daml-targeted fuzzer, static analyzer, formal verifier, or live-state fork. They also do not say whether those tools exist elsewhere. Import the EVM catalog and you hunt tools Canton never named, while the party sets it did name never get a line.

The review unit is the template

A common misconception is that Daml's review unit maps directly to an EVM contract and its functions. The Daml template instead defines contract data, signatories, observers, and choices, which determine the permissions and operations being reviewed.

A template is "A Daml definition specifying contract data, signatories, observers, and choices." Those four names are the review unit.

Official snippet, Contact / UpdateTelephone:

template Contact
  with
    owner : Party
    party : Party
    address : Text
    telephone : Text
  where
    signatory owner
    observer party

    choice UpdateTelephone
      : ContractId Contact
      with
        newTelephone : Text
      controller owner
      do
        create this with
          telephone = newTelephone

owner and party are parties, the ledger identities that choice_test allocates as Alice and Bob. signatory owner is the authority required to create or archive this contract. observer party can see it. Controllers are a party set. On this template the set is {owner}, and that is the only party the page allows to exercise UpdateTelephone. The choice consumes by default, so the current Contact is archived and a successor is created with newTelephone. Archive is implicit on every template, and the signatories are its controllers.

From key-fetch-proof/daml/Contact.daml on SDK 3.5.2, Alice created Contact with telephone "012 345 6789", Bob submitted UpdateTelephone under submitMustFail, then Alice exercised it to "098 7654 321". dpm test reported daml/Contact.daml:choice_test: ok, 1 active contracts, 3 transactions.

 submitMustFail party do
    exerciseCmd contactCid UpdateTelephone with
      newTelephone = "098 7654 321"

   newContactCid <- submit owner do
    exerciseCmd contactCid UpdateTelephone with
      newTelephone = "098 7654 321"

Daml Script is "the main tool to test Daml contracts." In a script, you submit commands and queries from multiple parties on "a fresh, initially empty, ledger". submitMustFail asserts that a command should be rejected and "never has an impact on the ledger. " Time control is passTime / setTime. Bob's rejected exercise is that assertion. dpm test ran the script.

Daml SDK 2.10.6 treats a ledger as valid only when it is consistent, conformant, and authorized. Authorization means "The parties who may request a particular change are restricted." The composition rule is a set of parties. "Every consequence of an exercise action act on a contract c is authorized by all signatories of c and all actors of act." A create needs the signatories of c. An exercise or fetch needs its actors. A NoSuchKey assertion needs the key's maintainers. Canton states that the protocol enforces those declarations and that "no amount of API manipulation can bypass them."

Treat a compiling signatory or controller line as the business obligation and you skip the match. The protocol enforces the declared set. The docs never name who performs that match. Point at signatory owner and controller owner on Contact. That is what the protocol enforces.

dpm test reports templates created and choices exercised

A second misconception is that the percentages printed by dpm test describe path or branch coverage. The documented metric counts templates created and choices exercised, not the completeness of execution paths or the strength of assertions.

The same dpm test that printed choice_test: ok also prints coverage. dpm test reports templates created and choices exercised, "in proportion of the total number of templates and choices." The example on that page defines 7 external templates and creates 5 of them (71.4%). It defines 27 external template choices and exercises 7 of them (25.9%). Five divided by seven is 0.714. Seven divided by twenty-seven is 0.259.

On key-fetch-proof the same runner reported 3 defined / 3 (100.0%) created internal templates and 5 defined / 2 (40.0%) exercised internal template choices. Three divided by three is 1.0. Two divided by five is 0.4. Forty percent means two of five defined choices were exercised in any test in that package.

The testing page never defines path coverage, branch coverage, or assertion strength. The Daml Script page says the same report helps identify "untested contract paths." If your kickoff slide files those percentages as paths, you invented the metric.

The testing page says "Scripts run in-process without a ledger, making them fast and deterministic." The testing-strategies page says these tests run "against an in-memory ledger (the Sandbox)." Neither page names a winner.

SDK 3.5.2 allows three active contracts on one key

A third misconception is that a Daml contract key guarantees one active contract per key across all Canton runtimes. The documented behavior depends on the SDK and Daml-LF target, and the current keys page describes a different model from the older ledger-integrity documentation.

If you specify a contract key, you must specify maintainers, and those maintainers must be signatories. The current keys page says Canton 3.x allows several active contracts of the same template to share a key, and that uniqueness is "guaranteed outside of the Daml engine." fetchByKey returns the first contract under a documented lookup order.

The WithKey / Helper shape is the keys page's own example. From key-fetch-proof/daml/Keys.daml on SDK 3.5.2, --target=2.3:

template WithKey
  with
    p : Party
    payload : Text
  where
    signatory p
    key p : Party
    maintainer key

template Helper
 with
   p : Party
 where
   signatory p
   choice PerformFetchByKey : (ContractId WithKey, WithKey)
     controller p
     do fetchByKey @WithKey p
multipleContractsPerKey = script do
  alice <- allocateParty "alice"
  cid1 <- alice `submit` createCmd (WithKey alice "first")
  cid2 <- alice `submit` createCmd (WithKey alice "second")
  cid3 <- alice `submit` createCmd (WithKey alice "third")
  (kcid, contract) <- alice `submit`
    createAndExerciseCmd (Helper alice) PerformFetchByKey
  assertMsg
    ("fetchByKey returned payload=" <> contract.payload
      <> " cid=" <> show kcid
      <> " created=" <> show [cid1, cid2, cid3])
    (kcid == cid3 && contract.payload == "third")

dpm test reported daml/Keys.daml:multipleContractsPerKey: ok, 3 active contracts, 4 transactions. Three WithKey contracts stayed active on one key. The page's own script asserts only that the fetched pair is one of the three created; this 3.5.2 script asserted the stronger kcid == cid3. Those three creates were separate submits; the fetch was a later submit. The keys page's participant-known step returns contracts "in any order" and says the current recency implementation "is not guaranteed and should not be relied on."

The same WithKey template on SDK 3.4.11 did not compile: damlc reported "Failure to process Daml program, this feature is not currently supported. Contract keys." --target=2.3 was "option --target: Unknown Daml-LF version: 2.3."

The current Canton glossary is on the same site and says keys "must be unique within their template scope." SDK 2.10.6 says key consistency means "there is at most one active contract for each key."

A note that writes "keys are unique" without a page and a runtime is already false against that keys page, against the glossary, or against this 3.5.2 run. The keys page requires Daml-LF 2.3 or later and says changing that target "changes the package ID."

Canton's Ethereum table is Canton's

A fourth misconception is that EVM testing concepts can be presented as native equivalents of Daml and Canton methods. The comparison depends on which documentation establishes each method and what the runtime enforces.

Canton maps msg.sender to Controller and require() to declarations "Enforced by protocol." ethereum.org and Foundry never wrote that sentence. On Contact, that controller set is {owner}. There is no ambient caller threaded through a call stack. Bob's submitMustFail on UpdateTelephone is the Script form of a rejected exercise.

Pin

Foundry / ethereum.org

Named Canton / Daml pages

Test state

Fork pinned to a block

Script on "a fresh, initially empty, ledger"

Who acts

vm.prank for one call as msg.sender

submit from a party; Contact's UpdateTelephone controller set is {owner}

Time

vm.warp

passTime / setTime

Use the Daml column when the artifact is a Daml or Canton application and every line has to survive a reopen of the cited page. Keep an EVM method name only when the artifact is an EVM contract, or when you mark the method as imported. That split expires if Digital Asset or Canton publishes a review-scope template that names a Daml analyzer, or if the keys page and the glossary stop contradicting each other.

daml.yaml can answer these questions

Open the daml.yaml you will build or review. Each question is yes or no.

  1. Does daml.yaml name an SDK version? Fail signal: no sdk-version, or the review omits it. On 3.4.11 this WithKey template did not compile. On 3.5.2 it did. Write the version next to the keys claim.
  2. Does it set an explicit Daml-LF compilation target for keys? Fail signal: no --target in build-options, or the review omits it. The keys page requires Daml-LF 2.3 or later. 3.4.11 rejected --target=2.3 as unknown. Write the --target value next to the keys claim.
  3. Does the review name the page that licenses uniqueness for that runtime? Fail signal: it writes "keys are unique" with no URL and no runtime. The glossary and SDK 2.10.6 state uniqueness. The keys page, and this 3.5.2 run, permit several active contracts on one key. Do not file this script's kcid == cid3 assertion as a recency rule.
  4. Does the review define coverage as templates created and choices exercised? Fail signal: a dpm test percentage is filed as path coverage, branch coverage, or assertion strength. On key-fetch-proof the runner printed 5 defined / 2 (40.0%) exercised. Rewrite the metric as created and exercised over the printed denominators.
  5. Does the review name only methods that appear on the Canton and Daml pages cited here, or mark any other method as imported? Fail signal: a Foundry fork, vm.prank, vm.warp, fuzzing, or formal verification appears as a native Daml method. Mark it imported or delete it.

A no means that rule is unpinned. Write the page URL and the runtime next to it.

Pinning a page does not make the keys page and the glossary agree.

Open daml.yaml and the DAR. Pin SDK, LF target, the uniqueness page, the coverage metric, and the named method set. If uniqueness cannot be pinned to one page and one runtime, or if the note files dpm test percentages as path coverage, request a scoped Daml review.

Conclusion

This scope was built from test runs and documentation pinned to specific SDK and Daml-LF versions by Ivan Bondar, Principal Smart Contract Auditor. 

You can read further on the design-level patterns this scope assumes in Daml design patterns and security analysis.

Building Daml app?

Hacken offers five free AI Auditor assessments for Daml applications, helping teams identify potential security issues and determine where further manual review may be needed..

Grab free AI review
Banner Image

Subscribe to our newsletter

Be the first to receive our latest company updates, Web3 security insights, and exclusive content curated for the blockchain enthusiasts.

Speaker Img