Summary
100% of 10 reported findings have been addressed.
10Findings
8Solved
2Acknowledged
0Risk accepted or open
Introduction
Acme Tokenization engaged Opty to audit the DAML contracts behind its tokenized asset registry, together with the Go automation that submits settlement batches to Canton. The review ran from August 3rd to August 21st, 2026 and covered the two repositories listed under scope at the commits shown.
Assessment summary
Two Opty engineers spent 15 business days on the engagement, combining manual review of every template and choice with DAML Script property tests and exploitation attempts against a local Canton sandbox.
The contracts follow sound DAML patterns: authorisation is expressed through signatories and controllers, holdings and allocations are separate templates, and settlement is atomic within a single transaction. The most serious issues were authorisation mistakes in individual choices rather than design flaws. An over-broad controller on Transfer and a non-consuming Split would each have let a party move or create value it did not own.
Acme fixed all critical, high and medium findings except one medium finding that depends on domain configuration, which it has acknowledged. We re-tested every fix at the remediation commits listed with each finding.
Methodology
- Map every template’s signatories, observers and controllers, and check each choice’s authority against the intended business rules.
- Write DAML Script tests for value conservation across split, merge, transfer, lock and settlement.
- Review privacy: what each party can see through observers, divulgence and explicit disclosure.
- Run the settlement automation against a Canton sandbox and inject failures: timeouts, contract-not-found errors and restarts mid-batch.
Scope
| Repository | Commit | Files | Lines |
| acme-tokenization/daml-contracts |
3f9c2a1 |
24 |
4180 |
| acme-tokenization/settlement-automation |
b81e4d0 |
11 |
1920 |
- Manual code review
- DAML Script property tests
- Canton sandbox exploitation
Out of scope: changes made after the remediation commits listed with each finding.
Finding details
OPT-01
Critical
Solved
Observers can exercise Transfer on any holding they can see
- Score
- 9.1
- Vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
- Location
- daml/Acme/Holding.daml:48
- Fixed
- Aug 19, 2026
- Commit
- 7c1d9e4
Description
The Transfer choice on Holding declares its controller as owner :: observers. Every party added as an observer, including the registry’s read-only reporting party, is therefore authorised to move the holding to any receiver.
choice Transfer : ContractId Holding
with receiver : Party
controller owner :: observers
do create this with owner = receiver
A reporting service with observer rights on every holding could transfer the full supply to itself in a single transaction.
Recommendation
Restrict the controller to owner and move observer-initiated flows to a separate proposal choice that the owner must accept.
OPT-02
High
Solved
Non-consuming Split allows the same holding to be spent twice
- Score
- 8.1
- Vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
- Location
- daml/Acme/Holding.daml:71
- Fixed
- Aug 19, 2026
- Commit
- a42f0b8
Description
Split is declared nonconsuming, so the original holding stays active after the two new holdings are created. The owner can split repeatedly and then transfer the original, inflating their balance.
Recommendation
Make Split a consuming choice and add an invariant test that the total amount across an owner’s holdings is unchanged after a split.
OPT-03
High
Solved
Settlement does not verify the allocation executor against the instrument admin
- Score
- 7.4
- Vector
- CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H
- Location
- daml/Acme/Settlement.daml:112
- Fixed
- Aug 20, 2026
- Commit
- e90b3c6
Description
ExecuteTransfer accepts any executor named in the allocation instead of checking it against the instrument’s registry admin. A malicious venue could create allocations naming itself as executor and settle legs for instruments it does not administer.
Recommendation
Fetch the instrument configuration inside the choice and assertMsg that allocation.executor == instrument.admin.
OPT-04
Medium
Solved
Holding amounts disclosed to every settlement participant
- Score
- 5.3
- Vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/A:N/I:N
- Location
- daml/Acme/Settlement.daml:64
- Fixed
- Aug 20, 2026
- Commit
- e90b3c6
Description
Settlement batches add all counterparties as observers on each input holding. Parties on unrelated legs can see each other’s balances and the full holding history through divulgence.
Recommendation
Pass holdings to settlement as explicitly disclosed contracts scoped to each leg, and keep the observer set limited to the owner and instrument admin.
OPT-05
Medium
Solved
Fee calculation truncates to zero for small transfers
- Score
- 4.3
- Vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
- Location
- daml/Acme/Fees.daml:22
- Fixed
- Aug 18, 2026
- Commit
- 5d7e2f1
Description
computeFee multiplies the amount by the basis-point rate and truncates to 2 decimal places. Transfers below 10.00 units pay no fee, so a user can split a large transfer into many small ones and avoid fees entirely.
Recommendation
Round fees up with ceiling at the instrument’s precision, or enforce a minimum fee per transfer.
OPT-06
Medium
Acknowledged
Lock expiry compares ledger time without skew tolerance
- Score
- 4.2
- Vector
- CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L
- Location
- daml/Acme/Lock.daml:39
Description
ReleaseExpired succeeds when now >= lock.expiresAt. Because Canton allows ledger time to drift within the configured skew, a submitter can release a lock up to the skew window early and front-run the counterparty’s settlement.
Recommendation
Add a grace period at least as large as the domain’s ledger time skew before expired locks can be released.
OPT-07
Low
Solved
Holding template accepts zero and negative amounts
- Score
- 3.1
- Vector
- CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N
- Location
- daml/Acme/Holding.daml:12
- Fixed
- Aug 18, 2026
- Commit
- 5d7e2f1
Description
Holding has no ensure clause, so zero or negative holdings can be created by the issuer and later merged into user balances.
Recommendation
Add ensure amount > 0.0 to Holding and Allocation.
OPT-08
Low
Solved
Settlement automation retries with stale disclosed contracts
- Score
- 2.6
- Vector
- CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L
- Location
- settlement-automation/internal/submit.go:204
- Fixed
- Aug 21, 2026
- Commit
- c3a8d52
Description
When a command fails with CONTRACT_NOT_FOUND, the automation retries using the same cached disclosed factory contract. After a factory upgrade every settlement batch fails until the service restarts.
Recommendation
Invalidate the disclosure cache on CONTRACT_NOT_FOUND and refetch from the registry before retrying.
OPT-09
Informational
Solved
Holding interface view missing token standard metadata
- Score
- 0.0
- Location
- daml/Acme/Holding.daml:90
- Fixed
- Aug 21, 2026
- Commit
- c3a8d52
Description
The HoldingV1 interface view leaves meta empty, so wallets that follow the Canton token standard cannot display instrument names or decimals.
Recommendation
Populate meta with the instrument symbol, name and decimals.
OPT-10
Informational
Acknowledged
DAML Script setup allocates parties with production-like names
- Score
- 0.0
- Location
- daml/Test/Setup.daml:8
Description
Test scripts allocate parties named AcmeRegistry and AcmeTreasury, matching production party hints. Running the script against a shared devnet participant could create confusingly named parties.
Recommendation
Prefix test party hints with test- or generate unique hints per run.
Disclaimer
This report reflects the code at the commits listed under scope, reviewed during the engagement dates above. It is not a guarantee that the code is free of vulnerabilities. Opty recommends a follow-up review within six months or after any material change to the codebase, whichever comes first.