Trigger Pattern Always (used by all verifier agents) - Inject Into security-verifier agents (Phase 5)
Scanned 9/6/2026
Install to Claude Code
npx -y skills add PlamenTSV/plamen --skill verification-protocol --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Verification Protocol?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/plamentsv-verification-protocol-plamen)More formats (shields.io, HTML) on the badges page.
---
name: "verification-protocol"
description: "Trigger Pattern Always (used by all verifier agents) - Inject Into security-verifier agents (Phase 5)"
---
# VERIFICATION_PROTOCOL Skill (DAML)
> **Trigger Pattern**: Always (used by all verifier agents)
> **Inject Into**: security-verifier agents (Phase 5)
> **Purpose**: Prove hypotheses TRUE or FALSE using `daml test` over `Script ()` functions on the in-memory ledger, with `submit`/`submitMustFail`/`query@T` as the harm oracles.
---
## Evidence Source Tracking (MANDATORY)
> **CRITICAL**: For EVERY piece of evidence, tag its source. Evidence from mocks, documentation, or unverified external packages CANNOT support a REFUTED verdict. There is no public Canton read RPC, so production-onchain tags do not exist for DAML — `[CODE]` (in-scope source) plus an executed `daml test` is the strongest evidence available.
| Tag | Meaning | Valid for REFUTED? |
|-----|---------|-------------------|
| `[CODE]` | Audited codebase (in-scope `.daml` source) + executed `daml test` | YES |
| `[POC-PASS]` | A `Script ()` compiled, `daml test` ran, harm assertion PASSED | YES (CONFIRM) |
| `[POC-FAIL]` | A `Script ()` compiled, `daml test` ran, harm assertion FAILED | YES (REFUTE) |
| `[CODE-TRACE]` | Manual trace with concrete party/contract values, no execution | NO (caps at CONTESTED) |
| `[MOCK]` | Helper template / test-only state that does not exist in the audited templates | **NO** |
| `[DOC]` | Documentation / spec only | **NO** |
### Evidence Audit Table (REQUIRED in every verification output)
```markdown
### Evidence Audit
| Claim | Evidence Source | Tag | Valid for REFUTED? |
|-------|-----------------|-----|-------------------|
```
### Mock Rejection Rule
If ANY evidence supporting REFUTED has tag `[MOCK]` or `[DOC]`: CANNOT return REFUTED, MUST return CONTESTED, triggers a re-PoC against the actual in-scope templates.
---
## Pre-Verification Understanding
Before writing ANY Script code, answer these three questions:
1. **What is the EXACT bug?** — Not "Authorization is missing" but "Choice `Iou.Transfer` lists only `owner` as `controller` and never re-asserts the `issuer`/co-signatory, so any single owner can move the asset without the joint authorization the template otherwise requires (Iou.daml:line N, template Iou, choice Transfer)".
2. **What OBSERVABLE difference proves it?** — Concrete before/after ACS state: which party holds which contract, what `query@T party` returns, what amount each child carries.
3. **What is the EXACT assertion?** — e.g., `submit attacker (exerciseCmd cid Transfer with newOwner = attacker)` SUCCEEDS and then `query @Iou victim` returns `[]` while `query @Iou attacker` is non-empty; or `submitMustFail attacker (...)` proves the guard holds (REFUTES).
**If you cannot answer all three → ASK FOR CLARIFICATION.**
---
## Pre-PoC Feasibility Gates (MANDATORY)
### Gate F1: Reachability
- [ ] Entry point identified (a `choice` exercisable by the attacker party, or a `create` the attacker can submit)
- [ ] Required authorizers traced: the choice's `controller` parties (AND-joined) and, for `create`, the new contract's `signatory` parties
- [ ] All required authorizers are parties the attacker profile actually controls or can obtain
If the choice's controllers / the contract's signatories include a party the attacker cannot supply → UNREACHABLE → FALSE_POSITIVE.
If reachable only by a fully-trusted operator party acting maliciously → document trust assumption, adjust likelihood.
### Gate F2: Value / Boundary Bounds
- [ ] Parameter domains identified (amount type `Int`/`Decimal`, list lengths, deadline range, cap)
- [ ] Expression evaluated at worst-case feasible inputs (0, 1, negative, max, empty list)
- [ ] Result crosses the bug threshold (invalid contract creatable, conservation violated, reachable abort, outsider query non-empty)
If values outside feasible domains → INFEASIBLE → FALSE_POSITIVE.
**Both gates PASS → proceed to PoC. Either gate FAILS → document and stop.**
---
## DAML Anti-Hallucination Rules (MANDATORY)
Verify EVERY API call against these known-correct patterns. Do NOT assume EVM/Solana/Rust shapes apply.
```haskell
-- Party allocation
attacker <- allocateParty "Attacker" -- NOT: newParty, Address::generate
[a, b] <- mapA allocateParty ["A", "B"]
-- Create / Exercise / ExerciseByKey
cid <- submit issuer (createCmd Iou with issuer; owner; amount)
cid' <- submit owner (exerciseCmd cid Transfer with newOwner = bob)
res <- submit p (exerciseByKeyCmd @Account (issuer, label) Some_Choice with ...)
-- Negative test (the cmd is EXPECTED to FAIL)
submitMustFail attacker (exerciseCmd cid Transfer with newOwner = attacker)
-- ^ a PASSING submitMustFail means the action was BLOCKED → proves SAFETY → REFUTES the bug.
-- Party-scoped visibility oracle (the privacy harm oracle)
cids <- query @Iou outsider -- what `outsider` can actually SEE; NEVER a god-view
mb <- queryContractId owner cid -- Some payload if `owner` is a stakeholder, None if not visible
-- Time control (deadline PoCs) — requires Daml.Script with time control
setTime (time (date 2024 Jan 1) 0 0 0)
passTime (days 8)
now <- getTime
-- Multi-command submit
submit p $ do
c1 <- createCmd T1 with ...
exerciseCmd c1 SomeChoice with ...
```
**Authorization model facts (the spine of every DAML auth/asset PoC):**
1. **Controller vs signatory**: the required authorizers of an `Exercise`/`Fetch` are the choice's listed `controller` parties (AND-joined); the required authorizers of a `Create`/`Archive` are the new/old contract's `signatory` parties. `submit p cmd` supplies authority for party `p` only.
2. **Consuming kind**: a `consuming` (default) choice ARCHIVES the contract on first exercise — a 2nd `exerciseCmd` on the same `ContractId` MUST fail with CONTRACT_NOT_FOUND. That failure REFUTES a double-spend claim; a SUCCEEDING second exercise on a wrongly-`nonconsuming` value-mover CONFIRMS double-spend.
3. **`ensure` violation** throws `PreconditionFailed`: a `create` whose `ensure` is `False` never enters the ACS.
4. **Keys**: maintainers MUST be signatories. `lookupByKey` returning `None` means "the submitter cannot SEE a contract with that key" (visibility), NOT "no such contract exists" — a false-None PoC must allocate a party that genuinely cannot see the keyed contract, then show the None branch mints/duplicates state.
5. **`submitMustFail` semantics**: `submitMustFail p cmd` PASSES when `cmd` FAILS. So the CONFIRM direction for an auth bug is `submit attacker (...)` SUCCEEDING plus a harm assertion; a passing `submitMustFail` proves SAFETY (REFUTES). State this explicitly whenever a `submitMustFail` is the deciding test.
6. **`query@T party` is the privacy harm oracle** — it returns only what `party` is a stakeholder of. Never assert against a god-view; an outsider seeing a contract = `not (null (query @T outsider))`.
7. **Divulgence vs observer**: `fetch` inside a shared transaction divulges a contract incidentally (transient, only to that transaction's witnesses), whereas `observer` is persistent visibility. A privacy finding must distinguish which mechanism is the leak.
---
## DAML PoC Script Templates
Every PoC is a top-level `Script ()`. `daml test` runs every in-scope `Script ()`; isolation is file-scoped (one PoC file per finding: `daml-test/PoC_{id}.daml`).
### Template 1: Missing / Single-Where-Joint Authorization
```haskell
-- AUTHORIZATION: buggy direction — attacker succeeds + harm assertion
test_single_where_joint_auth : Script ()
test_single_where_joint_auth = do
issuer <- allocateParty "Issuer"
owner <- allocateParty "Owner"
attacker <- allocateParty "Attacker"
cid <- submit issuer do createCmd Asset with issuer; owner; amount = 100.0
-- EXPLOIT: the choice lists only `owner` as controller; no co-auth from issuer
submit owner do exerciseCmd cid Transfer with newOwner = attacker
-- HARM: ownership moved without the joint authorization the template implies
victimCids <- query @Asset owner
attackerCids <- query @Asset attacker
assert (null victimCids)
assert (not (null attackerCids))
-- REFUTATION direction: if co-auth IS required, this proves SAFETY
test_auth_is_enforced : Script ()
test_auth_is_enforced = do
issuer <- allocateParty "Issuer"; owner <- allocateParty "Owner"; attacker <- allocateParty "Attacker"
cid <- submit issuer do createCmd Asset with issuer; owner; amount = 100.0
submitMustFail owner do exerciseCmd cid Transfer with newOwner = attacker
-- PASSING submitMustFail ⇒ guard holds ⇒ REFUTES the bug.
```
### Template 2: Double-Exercise via Wrong Consume-Mode
```haskell
test_double_spend_nonconsuming : Script ()
test_double_spend_nonconsuming = do
issuer <- allocateParty "Issuer"; owner <- allocateParty "Owner"
cid <- submit issuer do createCmd Iou with issuer; owner; amount = 100.0
-- If Spend is wrongly `nonconsuming`, the SAME cid can be exercised twice
submit owner do exerciseCmd cid Spend with to = issuer
submit owner do exerciseCmd cid Spend with to = issuer -- succeeds ⇒ double-spend CONFIRMED
-- (If Spend is correctly `consuming`, the 2nd exercise fails CONTRACT_NOT_FOUND ⇒ REFUTED)
```
### Template 3: Value Conservation Across Split / Merge
```haskell
test_value_conservation_split : Script ()
test_value_conservation_split = do
issuer <- allocateParty "Issuer"; owner <- allocateParty "Owner"
cid <- submit issuer do createCmd Iou with issuer; owner; amount = 100.0
(c1, c2) <- submit owner do exerciseCmd cid Split with splitAmount = 30.0
Some a1 <- queryContractId owner c1
Some a2 <- queryContractId owner c2
-- HARM: children must sum to the parent — a mint/burn breaks conservation
assert (a1.amount + a2.amount == 100.0)
```
### Template 4: lookupByKey False-None (visibility ≠ absence)
```haskell
test_lookup_false_none_duplicate : Script ()
test_lookup_false_none_duplicate = do
issuer <- allocateParty "Issuer"; outsider <- allocateParty "Outsider"
-- A keyed contract exists but `outsider` is NOT a stakeholder, so cannot see it
_ <- submit issuer do createCmd Registered with issuer; label = "x"
-- A choice controlled by `outsider` that does `lookupByKey` will get None (not visible)
-- and (buggily) treats None as "absent" → mints a duplicate keyed contract
submit outsider do createAndExerciseCmd (Registrar with p = outsider) (Register with label = "x")
-- HARM: two contracts now share the same key (uniqueness invariant broken)
```
### Template 5: CID-Capability — Lookalike / Fail-Open / Stale-Brick
```haskell
test_caller_cid_not_bound : Script ()
test_caller_cid_not_bound = do
issuer <- allocateParty "Issuer"; attacker <- allocateParty "Attacker"
good <- submit issuer do createCmd Config with issuer; allowed = True
evil <- submit attacker do createCmd Config with issuer = attacker; allowed = True
-- EXPLOIT: choice accepts a caller-supplied `cfg : ContractId Config` and does not
-- bind it to the trusted issuer → attacker passes their own lookalike config
op <- submit issuer do createCmd Op with issuer
submit attacker do exerciseCmd op Run with cfg = evil -- succeeds with forged capability ⇒ CONFIRMED
```
### Template 6: Privacy / Disclosure (party-scoped query oracle)
```haskell
test_observer_over_broad : Script ()
test_observer_over_broad = do
issuer <- allocateParty "Issuer"; owner <- allocateParty "Owner"
outsider <- allocateParty "Outsider"
_ <- submit issuer do createCmd Position with issuer; owner; secret = 42
-- HARM: an outsider who should NOT be a stakeholder can nonetheless see the contract
cids <- query @Position outsider
assert (not (null cids)) -- non-empty ⇒ over-broad observer / disclosure CONFIRMED
```
### Template 7: Boundary-Value Scripts (no fuzzer / SAST fallback, Aptos-style)
```haskell
-- ensure-gap, arithmetic abort, or deadline boundary via a shared helper
runBoundary : Decimal -> Script ()
runBoundary amt = do
issuer <- allocateParty "Issuer"
_ <- submit issuer do createCmd Asset with issuer; owner = issuer; amount = amt
pure ()
boundaryMin : Script ()
boundaryMin = runBoundary 0.0 -- ensure-gap: does a zero-amount contract create?
boundaryNeg : Script ()
boundaryNeg = runBoundary (-1.0) -- ensure-gap: does a negative-amount contract create?
boundaryMax : Script ()
boundaryMax = runBoundary 9999999999999999.0 -- arithmetic abort downstream ⇒ liveness brick
```
---
## HARM vs MECHANISM (HARD GATE)
Asserting only that a choice CAN be exercised, a contract CAN be created, or a path is reachable is a **mechanism test** → `[CODE-TRACE]` at best, NOT `[POC-PASS]`. A `[POC-PASS]` requires a HARM assertion:
| Class | Required harm assertion |
|-------|------------------------|
| Authorization | ownership/contract moved to attacker without the implied co-auth (`query` shows transfer) |
| Choice/Asset | same cid exercised twice (double-spend), or `child1.amount + child2.amount /= parent.amount` (conservation), or locked contract still moved |
| CID-capability | forged/lookalike `ContractId` accepted and harmful state produced; or whitelist-absent fail-open succeeds; or stale CID ⇒ CONTRACT_NOT_FOUND brick |
| State/key | duplicate keyed contract created via false-None; or exerciseByKey succeeds where maintainer authority should block |
| Privacy | `not (null (query @T outsider))` for a party that should NOT be a stakeholder |
| Boundary | invalid contract creates (ensure-gap), OR the choice aborts (ArithmeticError, liveness brick), OR a past-deadline action SUCCEEDS |
If the harm assertion fails (correct amounts, action blocked, outsider sees nothing) → `[POC-FAIL]`.
---
## Dual-Perspective Verification (MANDATORY)
**Phase 1 — ATTACKER**: Complete submit sequence? Which party does each `submit` use? What contract/value moves? Compose multiple `exerciseCmd` in one `submit`? Which `controller`/`signatory`/`ensure` is missing or bypassable?
**Phase 2 — DEFENDER**: What `controller`/`signatory` authorization blocks this? What `ensure` precondition rejects the input? What `key`+`maintainer` uniqueness prevents the duplicate? Does `consuming` archival prevent the replay? Does `observer` scoping prevent the disclosure? Why safe by design?
**Phase 3 — VERDICT**: Which argument won? If DEFENDER relies on a defense present in one template but ABSENT in a sibling for the same action → defense parity gap → do NOT REFUTE; minimum Medium.
---
## Realistic Parameter Validation
Substitute ACTUAL template constants. Apply Rule 10: worst realistic operational state, not current snapshot.
```
State: 'With real fields [amount type=Decimal, cap=X, deadline=Y] at worst-state
[max holders, min amount, empty observer list], bug triggers when [condition]'
OR: 'With real fields, bug does NOT trigger because [reason: ensure rejects it / controller blocks it / key is unique]'
```
**DAML-specific facts**: `Int`/`Decimal` THROW on overflow (no silent wrap → liveness/brick, never silent-value) | `ensure False` ⇒ `PreconditionFailed` | a consuming choice archives once | maintainers ⊆ signatories | `query@T party` is party-scoped.
---
## Anti-Downgrade Guard (MANDATORY for VS/BLIND findings)
Apply Rule 13's 5-question test BEFORE downgrading severity or marking FALSE_POSITIVE:
1. **Who is harmed** by this design gap?
2. **Can affected parties avoid** the harm?
3. **Is the gap documented** in protocol docs?
4. **Could the protocol achieve the same goal** without this gap?
5. **Does the choice fulfill its stated purpose completely?**
**HARD RULE**: Template A has co-authorization (or an `ensure`, or an `observer` scoping) for a choice but sibling Template B lacks it for the same action → defense parity gap, NOT "by design". Minimum severity: Medium.
**Severity-modifier caveat (DAML)**: the "on-chain-only exploit → −1 tier" modifier is EVM-framed. Do NOT auto-downgrade a liveness-brick (Int/Decimal abort) or a privacy-disclosure finding with it — an outsider `query@T` leak crosses the disclosure boundary and a reachable abort is a liveness consequence.
---
## New Observations (MANDATORY)
If during verification you discover a NEW bug not covered by any hypothesis:
- [VER-NEW-1]: {title} — {Module.daml: template/choice} — {brief description}
---
## Error Trace Output (MANDATORY for CONTESTED/FALSE_POSITIVE)
- **Failure Type**: PRECONDITION_FAILED / AUTHORIZATION_ERROR / CONTRACT_NOT_FOUND / NO_SUCH_KEY / ASSERTION_FAILED / INSUFFICIENT_EVIDENCE / COMPILE_ERROR
- **Location**: {Module}.daml: {template / choice} : {approximate line}
- **Error Message**: {DAML Script error, e.g., `PreconditionFailed`, `failed to find contract`, `requires authorizers ... but only ... were given`}
- **State at Failure**: {which parties hold which contracts in the ACS}
- **Investigation Question**: {What would resolve this}
---
## RAG Queries Before PoC (MANDATORY for HIGH/CRITICAL)
1. `get_attack_vectors(bug_class="{category}")`
2. `get_similar_findings(pattern="{vulnerability description}")`
3. `validate_hypothesis(hypothesis="{finding summary}")`
4. `search_solodit_live(keywords="{daml canton vulnerability pattern}", impact=["HIGH","CRITICAL"], tags=["DAML","Canton","Daml"], quality_score=3, max_results=15)`
Document in output: Attack Vectors Consulted, Similar Exploits Found, Historical Precedent.
### RAG Confidence Override
| RAG Confidence | Local Verdict | Final Verdict |
|----------------|---------------|---------------|
| >= 7/8 matches | FALSE_POSITIVE | **CONTESTED** (override) |
| >= 6/8 matches | FALSE_POSITIVE | **CONTESTED** (override) |
| < 6/8 matches | FALSE_POSITIVE | FALSE_POSITIVE (allowed) |
---
## Chain Hypothesis PoC Requirements
Chain hypotheses receive PRIORITY verification. Test the COMPLETE sequence in one Script: (1) execute the enabler `submit`, (2) assert the postcondition is created via `query@T`/`queryContractId` (e.g. the false-None state, the forged config in the ACS), (3) execute the previously-blocked exploit `submit`, (4) assert the combined impact exceeds either finding alone. Use standard `allocateParty` + `submit` setup.
---
## Interpreting Results
### Test PASSES (harm assertion holds) → Bug CONFIRMED
### Test FAILS → Check Why
| Failure | Meaning | Action |
|---------|---------|--------|
| `requires authorizers ... but only ... given` | Controller/signatory auth IS present | Re-examine hypothesis (likely REFUTED) |
| `PreconditionFailed` | `ensure` rejected the input | Check if the ensure IS the defense (REFUTE) or if a weaker path bypasses it |
| `failed to find contract` (CONTRACT_NOT_FOUND) | Consuming choice already archived it | A double-spend claim is REFUTED; for stale-CID a brick is CONFIRMED |
| `couldn't find key` (NO_SUCH_KEY) | Key absent or not visible | Distinguish true-absent vs false-None (visibility) |
| `submitMustFail` PASSED | The action was BLOCKED | This proves SAFETY → REFUTES the auth/bypass bug |
| Wrong amount | Conservation held / setup wrong | Verify Decimal scale and party setup |
---
## Iteration Protocol
**Attempt 1**: Direct implementation from hypothesis.
**Attempt 2**: Adjust setup (which party submits, contract field values, command ordering) — keep the SAME `exerciseCmd`/choice + SAME harm assertion + SAME location.
**Attempt 3**: Re-examine assumptions (controller vs signatory, consuming vs nonconsuming, key visibility).
**Attempt 4**: Verify choice/template signatures in source — do NOT assume EVM/Rust shapes.
**Attempt 5**: Re-read anti-hallucination rules — confirm correct DAML-Script API.
**After 5 attempts**: FALSE_POSITIVE with documented reasoning.
> **Retry guard**: changing WHICH choice is exercised or WHICH `query@T` is asserted between attempts caps the result at `[CODE-TRACE]` (it is a different test, not a setup fix).
---
## Insufficient Evidence (HALT CONDITIONS)
Before marking REFUTED, check ALL boxes:
- [ ] Required-authorizer set verified by reading the actual `controller`/`signatory` clauses (not assumed)
- [ ] Consume-mode of the choice verified (`consuming`/`nonconsuming`/`pre`/`postconsuming`) by reading source
- [ ] For key findings: `maintainer` set verified ⊆ `signatory` set; false-None tested with a genuinely non-stakeholder party
- [ ] Missing precondition documented (STATE / ACCESS / TIMING / EXTERNAL / BALANCE)
- [ ] Searched other findings for matching postconditions (chain integration)
- [ ] For privacy: harm tested with a party-scoped `query@T outsider`, not a god-view
- [ ] `ensure` clause read directly from source (not assumed present)
- [ ] External package (`data-dependencies`) behavior marked UNVERIFIED, not relied on for REFUTE
### Evidence That Does NOT Count
- "Helper template shows X" — helper templates are `[MOCK]`, not the audited templates
- "Choice is `nonconsuming`" — does not by itself bypass controller authorization
- "Attacker loses by transferring" — may profit via a position held in another template
- "Requires the operator party" — operator may be a compromised key or malicious signatory
- "Key cannot collide" — verify the actual `key`/`maintainer` and visibility scope
- "`submitMustFail` passed" — that proves SAFETY (REFUTES), it is NOT evidence the bug exists
---
## Output Format
### CONFIRMED
```markdown
## Verdict: CONFIRMED
### Bug Mechanism Verified
{2-3 sentences on what daml test proves}
### Test Code
{Full Script () using allocateParty/createCmd/exerciseCmd + harm assertion}
### Test Output
{Assertions and ACS values from `daml test --files daml-test/PoC_{id}.daml`}
### Key Evidence
| Metric | Value |
|--------|-------|
| Before / After / Expected / Difference | {which party holds which contract, amounts} |
### Evidence Audit
| Claim | Evidence Source | Tag | Valid for REFUTED? |
### Severity: {LEVEL}
{1-2 sentence justification; note liveness/privacy are NOT auto-downgraded}
### Suggested Fix
{diff block + Fix scope + Verified: YES/NO}
```
### FALSE_POSITIVE
```markdown
## Verdict: FALSE_POSITIVE
### Attempts Made
**Attempt 1-N:** Approach, Result (Failure Type / error message), Learning
### Evidence Audit
| Claim | Evidence Source | Tag | Valid for REFUTED? |
### Why It Is Not a Bug
{2-3 sentences — e.g., a passing submitMustFail proved the controller co-auth holds}
### Error Trace
{Failure Type, Location, Error Message, State at Failure, Investigation Question}
```
### CONTESTED
```markdown
## Verdict: CONTESTED
### Evidence Status
| Checkpoint | Status | Details |
|------------|--------|---------|
| Controller/signatory set verified from source | YES/NO | |
| Consume-mode verified from source | YES/NO | |
| Key maintainer ⊆ signatory verified | YES/NO | |
| Privacy harm tested via party-scoped query@T | YES/NO | |
### Evidence Audit
| Claim | Evidence Source | Tag | Valid for REFUTED? |
### Why This Cannot Be REFUTED
{What evidence is missing — e.g., external data-dependency behavior unverified}
### Escalation Required
- [ ] Read actual controller/signatory clauses for {template.choice}
- [ ] Confirm key+maintainer against source
- [ ] Test additional choice paths: {list}
### Error Trace
{as above}
```
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!