agents/skills/daml/authorization-model/SKILL.md
Trigger Pattern Always required for DAML audits - Inject Into Breadth agents, depth-state-trace, depth-external
npx skillsauth add plamentsv/plamen authorization-modelInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Trigger Pattern: Always required for DAML audits Inject Into: Breadth agents, depth-state-trace, depth-external Finding prefix:
[DML-AM-N]Rules referenced: R4, R6, R12, R13
DAML's authorization model has no msg.sender. Authority flows through the transaction tree: the required authorizers of every node MUST be a subset of the parties who authorized the submission. A create requires the new contract's signatory parties; an exercise/fetch requires the choice's controller parties; an archive requires the signatories. Signatory authority propagates exactly ONE hop into a choice's consequences. The most common critical bug class is a choice whose controller is derived from a choice argument (privilege injection), or a choice that performs a privileged action with only one authorizer where joint authorization is required.
For EVERY choice in each template, record who must authorize it and what it does:
| Template.Choice | Consume-Mode | Controller Expr | Controller Source | Privileged Action? | Co-Auth Required? |
|-----------------|--------------|-----------------|-------------------|--------------------|-------------------|
| {T.C} | consuming/nonconsuming/pre/postconsuming | {controller expr} | FIXED-SIGNATORY / ARG-DERIVED / FETCHED | YES/NO | YES/NO |
Critical patterns to flag:
[ELEVATE:MISSING_COAUTH]).controller expression that reads a field of the choice argument rather than a template signatory/fixed party (privilege injection → [ELEVATE:PARAM_CONTROLLER], ALWAYS flag).DAML note: controller p makes p the required authorizer AND an automatic observer. A controller derived from arg.someParty lets the caller name themselves as the authorizer — there is no msg.sender to constrain it.
For each choice whose controller is NOT a fixed template signatory, trace where the controlling party comes from:
| Template.Choice | Controller Party Origin | Bound to Template Signatory? | Attacker Can Set It? | Finding? |
|-----------------|-------------------------|------------------------------|----------------------|----------|
| {T.C} | arg.X / this.field / fetch(...).field | YES/NO | YES/NO | [DML-AM-N] if attacker-settable |
Attack: If controller resolves from a value the caller supplies (choice argument, or a field of a contract the caller created), the caller authorizes their own privileged action. Compare against the intended authority from the AUTHORIZATION MATRIX in attack_surface.md.
ARG-DERIVED controller is ALWAYS a finding candidate — it must be proven safe (the arg is itself constrained by a prior signatory check) before it is dismissed.
A correct controller does NOT replace in-body checks. Enumerate the validation each choice performs against its controller:
| Template.Choice | Controller | In-Body Check Present? | Check Expr | Sufficient? |
|-----------------|------------|------------------------|-----------|-------------|
| {T.C} | {party} | YES/NO | assertMsg "..." (owner == ctl) / NONE | YES/NO |
Check for:
this.owner without asserting owner == controllingParty (missing owner == party).Enumerate every conditional that gates a privileged path:
| Template.Choice | Guard Expr | Guard Source | Bypassable? | Finding? |
|-----------------|-----------|--------------|-------------|----------|
| {T.C} | when (paused == False) / elem p whitelist | template field / arg / fetched | YES/NO | [DML-AM-N] if bypass |
Patterns:
paused, frozen, active) read from a contract the caller can re-create with a forged value.A fetch/fetchByKey returns a contract that the choice may branch on for an authorization decision. If the fetched contract is not bound to a trusted signatory, the caller can forge it.
| Template.Choice | Fetched Template | Auth Decision On Fetched Field? | Fetched Contract Signed By Trusted Party? | Finding? |
|-----------------|------------------|----------------------------------|-------------------------------------------|----------|
| {T.C} | {T2} | YES/NO | YES/NO | [DML-AM-N] if unbound |
Attack: A choice fetches a Config/Role/Approval contract and grants access based on a field, but the caller can create that contract themselves (they are a signatory of it). The auth decision rests on attacker-controlled state. Verify the fetched contract's signatories include the party whose authority the decision relies on ([ELEVATE:FETCH_AUTH]).
**ID**: [DML-AM-N]
**Severity**: [Critical if value/ownership, High if config/co-auth, Medium if view-privileged]
**Step Execution**: ✓1,2,3,4,5 | ✗(reasons) | ?(uncertain)
**Rules Applied**: [R4:✓/✗, R6:✓/✗, R12:✓/✗, R13:✓/✗]
**Location**: {Module}.daml:LineN (template X, choice Y)
**Title**: {Choice} controller derived from argument / missing co-authorization allows unauthorized {action}
**Description**: [Specific authority gap with the controller expr, what the tx tree requires, and why the caller can satisfy it alone]
**Impact**: [What the unauthorized party achieves: takes a contract, moves value, becomes admin, bypasses a guard]
**PoC steer**: `submit attacker (exerciseCmd cid Choice with ...)` SUCCEEDS + harm assertion; a passing `submitMustFail` instead proves SAFETY (refutes).
| Section | Required | Completed? | Notes | |---------|----------|------------|-------| | 1. Choice-Authority Inventory | YES | ✓/✗/? | Every choice in every template | | 2. Controller-Source Trace | YES | ✓/✗/? | Every ARG-DERIVED / fetched controller | | 3. In-Body Validation Enumeration | YES | ✓/✗/? | owner==party and per-party binding | | 4. Guard / Whitelist / Flag Bypass | IF guards present | ✓/✗(N/A)/? | Every privileged conditional | | 5. Fetch-Based Auth Bypass | IF auth decision on fetched contract | ✓/✗(N/A)/? | Verify fetched signatory binding |
data-ai
Trigger Pattern Always (run during recon TASK 0, not breadth) - Inject Into Recon agent only (meta_buffer.md enrichment)
data-ai
Trigger Pattern Always (run during recon TASK 0, not breadth) - Inject Into Recon agent only (meta_buffer.md enrichment)
data-ai
Trigger Pattern Always (run during recon TASK 0, not breadth) - Inject Into Recon agent only (meta_buffer.md enrichment)
data-ai
Trigger STABLESWAP_FORK flag (fork-ancestry detects Curve/StableSwap parent via get_d/get_y/ramp_a/StableSwap patterns) - Agent Type general-purpose (standalone niche agent, 1 budget slot)