@prajwalghate/sourcetruth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-16
4
+
5
+ First release.
6
+
7
+ - **Daml and Solidity** source, detected automatically.
8
+ - **The map** (`--html`): who can act, split into outside and inside the protocol; each action's
9
+ archives, creates and calls in code order, with **Play**; whose authority an action carries beyond
10
+ its caller; which contract ids the caller chooses; each contract's life; blind spots in amber.
11
+ One self-contained file, deterministic, with a guided tour and a Learn view drawn from your code.
12
+ - **Solidity tracing**: every call site classified — typed variables, casts, libraries, `this.`, ETH
13
+ and low-level calls — with nothing dropped. Access checks followed through helpers, inheritance
14
+ and vendored modifiers, credited only when they run on every path. Inherited public functions from
15
+ vendored parents appear on the contracts that run them. Imports resolved through remappings,
16
+ node_modules and nested dependencies; missing ones reported.
17
+ - **Daml tracing**: controllers, signatories, consuming effects, helper functions folded into the
18
+ choices that call them, test packages excluded.
19
+ - `--surface`, `--holes`, `--graph`, `--json` (schemaVersion 1), `--min-resolution` for CI,
20
+ `--demo`, `-o`.
21
+ - **GitHub Action** (`action.yml`): builds the map, uploads it, writes a run summary, and can fail
22
+ below a resolution floor.
package/GUIDE.md ADDED
@@ -0,0 +1,255 @@
1
+ # How to use sourcetruth
2
+
3
+ The same guide, with a live demo, is at **https://prajwalghate.github.io/sourcetruth/**.
4
+
5
+ sourcetruth reads Daml or Solidity source and draws what it does: who can act, what each action
6
+ changes, and what could not be traced. You do the judging. This guide gets you from install to a
7
+ first audit, then covers every option.
8
+
9
+ - [1. Install](#1-install)
10
+ - [2. First look: the demo](#2-first-look-the-demo)
11
+ - [3. A first audit, step by step](#3-a-first-audit-step-by-step)
12
+ - [4. Reading the map](#4-reading-the-map)
13
+ - [5. Pointing it at your code](#5-pointing-it-at-your-code)
14
+ - [6. Command-line reference](#6-command-line-reference)
15
+ - [7. In CI](#7-in-ci)
16
+ - [8. What it cannot tell you](#8-what-it-cannot-tell-you)
17
+ - [9. Troubleshooting](#9-troubleshooting)
18
+
19
+ ---
20
+
21
+ ## 1. Install
22
+
23
+ Node 22 or newer. Nothing else.
24
+
25
+ ```bash
26
+ npm install -g @prajwalghate/sourcetruth
27
+ sourcetruth --version
28
+ ```
29
+
30
+ Or without installing, from a clone:
31
+
32
+ ```bash
33
+ git clone git@github.com:prajwalghate/sourcetruth.git
34
+ node sourcetruth/bin/sourcetruth.mjs --help
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 2. First look: the demo
40
+
41
+ ```bash
42
+ sourcetruth --demo -o demo.html # a small Solidity vault
43
+ sourcetruth --demo daml -o demo-daml.html # a small Daml lending protocol
44
+ open demo.html
45
+ ```
46
+
47
+ The first time the map opens, a short tour clicks through it for you. The two demos are written to
48
+ show everything the map can draw — an open function, a guard hidden in a helper, borrowed authority,
49
+ a caller-chosen contract id, and a blind spot — so they are the fastest way to learn to read it.
50
+
51
+ ---
52
+
53
+ ## 3. A first audit, step by step
54
+
55
+ This walks the Solidity demo. Every step is one click.
56
+
57
+ 1. **Look at the top bar.** `94% traced · 1 blind spot`. Nearly everything the code does could be
58
+ followed. Click **1 blind spot**: `Vault.execute` does a `delegatecall` to an address the caller
59
+ passes in. That is a line to read by hand.
60
+
61
+ 2. **Look at who can act.** On the left: `anyone` is *outside*; `onlyOwner`, `vault` and
62
+ `keepers[msg.sender]` are *inside*. Anything under `anyone` is callable with no access check.
63
+
64
+ 3. **Start where outsiders get in.** The panel on the right lists the actions an outsider can take,
65
+ riskiest first. `takeFee` is there — no check, and it moves tokens.
66
+
67
+ 4. **Open it and press Play.** The map shows `takeFee` writing the vault's storage and calling
68
+ `IERC20` — a transfer to the owner, of an amount the caller chooses.
69
+
70
+ 5. **Check a guarded one.** Click `harvest`. It is guarded by `keepers[msg.sender]`, and the panel
71
+ shows where that check lives: `_onlyKeeper() → require(keepers[msg.sender])`. The guard is a
72
+ helper call, not a modifier — sourcetruth follows those.
73
+
74
+ 6. **Follow the flow.** In `harvest`, press Play: `IStrategy.harvest`, then the fee transfer, then
75
+ `_invest` sending funds back to the strategy. Click `IStrategy` to see the strategy's side.
76
+
77
+ 7. **Read the code.** Every action has a **Code** button that opens its source, comments intact for
78
+ reading — though nothing on the map was derived from them.
79
+
80
+ The Daml demo reads the same way. There, open `Position.Liquidate`: the liquidator **names
81
+ themselves**, passes in the `priceCid` it is checked against, and the action runs with the
82
+ authority of `operator` **and** `owner` — the signers of the position, not just the caller.
83
+
84
+ ---
85
+
86
+ ## 4. Reading the map
87
+
88
+ | On screen | What it shows | What to do with it |
89
+ |---|---|---|
90
+ | **Outside / Inside** (left, or across the top on a small screen) | Every party the code lets act. *Outside* can act without the protocol's own parties. | Click one: the map lights up every contract they can reach. |
91
+ | **The map** | Contracts as cards. Solid arrows *create*, dashed arrows *call*. Dashed cards are defined outside the code you pointed at. | Drag to pan, pinch or ⌘-scroll to zoom, **Fit** to see everything. |
92
+ | **Where can someone outside get in?** | Actions an outsider can take alone, riskiest first. | Start at the top. |
93
+ | **An action** | Who fires it; what it does to its own contract; **whose authority it also carries**; which contract ids the caller picks; every archive, create and call in code order. | Press **Play** (or `P`). |
94
+ | **A contract** | Its life: made by → replaced by → ended by, and who reads it. | Look for *Nothing in this code ends it* on anything that holds value. |
95
+ | **Blind spots** (amber) | Calls whose target can't be named from source. | Read them before trusting the rest. |
96
+ | **Learn** | Each idea of the language, drawn with contracts from your code. | Read once if the language is new to you. |
97
+ | **Code** | The plain listing: every contract, action, authority, link and source body. | Look things up. It is also the whole page with JavaScript off. |
98
+
99
+ **Colours.** Green made · red archived or ended · blue called or selected · violet authority that is
100
+ not the caller's own (a signer's, or no access check at all) · amber could not trace.
101
+
102
+ **Typefaces.** Monospace is copied from the source. Sans-serif is the tool talking.
103
+
104
+ `Esc` goes back a step. Links open the map at a place, on load or while it is open: `#e-Vault-takeFee` an action, `#u-Vault` a contract, `#a-anyone` a party, `#learn` the Learn view, `#map` the start — so a runbook or a ticket can point straight at what it means.
105
+
106
+ ### How "outside" is decided
107
+
108
+ From the code, never from names.
109
+
110
+ - **Daml:** the parties who sign the contracts nothing in the code creates are *inside*, and so are
111
+ the signers of any contract only ever created with an inside party's consent. A party who can act
112
+ without any of them is *outside*.
113
+ - **Solidity:** a function with no access check is callable by *anyone*. A named check is a
114
+ privilege, and whoever holds it is *inside*.
115
+
116
+ ### How Solidity access checks are found
117
+
118
+ A check counts when it runs on every path: in a modifier that checks the caller, or as a top-level
119
+ `require` / `if (...) revert` on `msg.sender` — in the function itself or in a helper it calls,
120
+ through inheritance and into vendored libraries. A check inside an `if` does **not** count: claiming a
121
+ guard that might not run would hide an open function. `a || b` means either may call.
122
+
123
+ ---
124
+
125
+ ## 5. Pointing it at your code
126
+
127
+ Point it at a repository root, or at the folder that holds the source. The language is detected.
128
+
129
+ **Daml.** `.daml` files are read recursively. Skipped: `.daml/` build output, and test packages —
130
+ any package whose `daml.yaml` name ends in `-test` or `-tests`, and directories named `test`/`tests`.
131
+
132
+ **Solidity.** `.sol` files are read recursively. Skipped by default:
133
+
134
+ - your dependencies in `lib/` (Foundry) and `node_modules/` — still **read**, to follow inheritance
135
+ and access checks, but not drawn as your code (`--include-libs` draws them);
136
+ - tests and scripts: `test/`, `tests/`, `script/`, `scripts/`, `TestContracts/`, `mocks/`, `*.t.sol`,
137
+ `*.s.sol` (`--include-tests` brings them back).
138
+
139
+ Imports are followed the way the compiler would: relative paths, `remappings.txt` and `foundry.toml`
140
+ remappings, `node_modules`, and nested dependencies. Public functions a contract inherits from a
141
+ vendored parent — `deposit()` from an ERC4626 base, say — appear on that contract, running the
142
+ contract's own overrides.
143
+
144
+ **If dependencies are missing** — typically a git submodule that was never initialised — the map
145
+ says so at the top, and every function whose access might depend on the missing code is marked. Run
146
+ `forge install` or `git submodule update --init` and generate the map again.
147
+
148
+ ---
149
+
150
+ ## 6. Command-line reference
151
+
152
+ ```
153
+ sourcetruth <dir> [mode] [options]
154
+ ```
155
+
156
+ | Mode | Output |
157
+ |---|---|
158
+ | *(none)* | Summary and blind spots in the terminal |
159
+ | `--html` | The interactive map, one self-contained file |
160
+ | `--surface` | State-changing actions at most one party can take alone |
161
+ | `--holes` | Only the calls that could not be traced |
162
+ | `--graph` | Contract-to-contract creates and calls |
163
+ | `--json` | The full model (see below) |
164
+
165
+ | Option | |
166
+ |---|---|
167
+ | `-o, --out <file>` | Write to a file instead of stdout |
168
+ | `--title <text>` | Name the map (default: the directory name) |
169
+ | `--lang daml\|evm` | Force the language |
170
+ | `--min-resolution N` | Exit 1 if less than N% of links could be traced |
171
+ | `--include-tests` | Include test packages and test contracts |
172
+ | `--include-libs` | Draw vendored dependencies as if they were your code |
173
+ | `--dated` | Stamp today's date on the map (off by default, so maps from the same commit are identical) |
174
+ | `--demo [daml\|solidity]` | Render a bundled example |
175
+ | `-v, --version` · `-h, --help` | |
176
+
177
+ **Exit codes.** `0` success · `1` nothing found, or below `--min-resolution` · `2` bad usage.
178
+ Blind spots alone never fail a run.
179
+
180
+ **JSON.** `--json` prints `{ "schemaVersion": 1, "tool": {...}, "units": [...], "entries": [...], ... }`.
181
+ `schemaVersion` changes only when a field changes meaning or is removed.
182
+
183
+ ```bash
184
+ # every state-changing function with no access check
185
+ sourcetruth ./src --json | jq -r '.entries[] | select(.authority == [] and .effect != "none" and (.declared|not)) | "\(.unit).\(.name)"'
186
+ ```
187
+
188
+ ---
189
+
190
+ ## 7. In CI
191
+
192
+ ### GitHub Actions
193
+
194
+ ```yaml
195
+ name: sourcetruth
196
+ on: [pull_request]
197
+ jobs:
198
+ map:
199
+ runs-on: ubuntu-latest
200
+ steps:
201
+ - uses: actions/checkout@v4
202
+ with:
203
+ submodules: recursive # so inherited code and access checks can be read
204
+ - uses: prajwalghate/sourcetruth@v0
205
+ with:
206
+ path: ./contracts
207
+ min-resolution: 90
208
+ ```
209
+
210
+ Inputs: `path`, `title`, `min-resolution` (0 = off), `output` (default `sourcetruth-map.html`),
211
+ `include-tests`, `upload`. Outputs: `resolution`, `blind-spots`. Each run uploads the map as the
212
+ `sourcetruth-map` artifact and writes a summary table.
213
+
214
+ ### Anywhere else
215
+
216
+ ```bash
217
+ npx @prajwalghate/sourcetruth ./contracts --min-resolution 90
218
+ ```
219
+
220
+ Pick the floor from today's number, not from 100: if you are at 94%, set 90. When a change makes the
221
+ number drop, a construct the tool can't follow has arrived — worth knowing on the pull request.
222
+
223
+ ---
224
+
225
+ ## 8. What it cannot tell you
226
+
227
+ - **Whether anything is a bug.** It draws; you judge.
228
+ - **Runtime facts.** Which contract id a caller will actually pass, who holds a role, what a proxy
229
+ currently points at, what value a price has.
230
+ - **Branches.** It cannot tell which path runs. An action that archives a contract and creates one
231
+ is shown as possibly replacing it *and* possibly ending it.
232
+ - **Arithmetic.** Overflow, rounding, decimals — none of it.
233
+ - **Inside a blind spot.** A `delegatecall` to a caller's address, an assembly `call`, a receiver
234
+ whose type can't be worked out: shown, never followed.
235
+ - **Code that isn't on disk.** Missing imports are reported, not guessed.
236
+
237
+ ---
238
+
239
+ ## 9. Troubleshooting
240
+
241
+ **"0 contracts found."** You pointed at build output or an empty folder. Point at the repository
242
+ root or the source folder.
243
+
244
+ **A function you know is guarded shows as `anyone`.** Check the top of the map for a *not on disk*
245
+ note — the guard may live in a dependency that isn't installed. If everything resolved, the check
246
+ may be inside an `if`, which is deliberately not credited. The **Code** button shows the source.
247
+
248
+ **A contract is missing.** It may be in a skipped folder (`test/`, `mocks/`, `TestContracts/`,
249
+ `lib/`). Use `--include-tests` or `--include-libs`.
250
+
251
+ **Resolution is lower than you expected.** Run `sourcetruth <dir> --holes` and look at the lines:
252
+ they share a pattern, and that pattern is what the tool couldn't follow.
253
+
254
+ **The map looks empty.** Click **Fit** (bottom right). On very large codebases the opening view
255
+ zooms in on the riskiest action so the cards are readable.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prajwal Ghate
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # sourcetruth
2
+
3
+ [![npm](https://img.shields.io/npm/v/@prajwalghate/sourcetruth)](https://www.npmjs.com/package/@prajwalghate/sourcetruth)
4
+ [![test](https://github.com/prajwalghate/sourcetruth/actions/workflows/test.yml/badge.svg)](https://github.com/prajwalghate/sourcetruth/actions/workflows/test.yml)
5
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ **[Documentation](https://prajwalghate.github.io/sourcetruth/)** · **[Live demo](https://prajwalghate.github.io/sourcetruth/demo)** · [Guide](GUIDE.md)
8
+
9
+ See what smart-contract code **does** — who can act, and what each action changes — straight from
10
+ the source. And see, just as plainly, what it **could not trace**.
11
+
12
+ Works on **Daml** (Canton) and **Solidity**. One command, one self-contained HTML file, no server.
13
+
14
+ ```bash
15
+ sourcetruth --demo -o demo.html # a first look, on a bundled example
16
+ sourcetruth ./contracts --html -o map.html
17
+ ```
18
+
19
+ ## What you get
20
+
21
+ A map of the protocol you can click through:
22
+
23
+ - **Who can act** — every party the code lets take an action, split into those *outside* the
24
+ protocol and those running it. Worked out from the code, not from what things are called.
25
+ - **What an action does** — pick one and press **Play**: what it archives, what it creates, what it
26
+ calls, in the order the code does them, and whose authority it carries beyond the caller's own.
27
+ - **Each contract's life** — what makes it, what replaces it, what ends it.
28
+ - **Blind spots** — every call whose target can't be named from source is shown in amber. The tool
29
+ never guesses one and never drops one.
30
+
31
+ Comments are stripped before anything is read. If a comment says a function is admin-only and the
32
+ code says otherwise, the map shows the code.
33
+
34
+ ## Install
35
+
36
+ Node 22 or newer. No dependencies.
37
+
38
+ ```bash
39
+ npm install -g @prajwalghate/sourcetruth
40
+ sourcetruth --version
41
+ ```
42
+
43
+ ## Use it
44
+
45
+ ```bash
46
+ sourcetruth <dir> # summary + blind spots in the terminal
47
+ sourcetruth <dir> --html -o map.html # the interactive map
48
+ sourcetruth <dir> --surface # actions one party can take alone
49
+ sourcetruth <dir> --json # the full model, for your own tooling
50
+ sourcetruth <dir> --min-resolution 90 # for CI: fail if too little can be traced
51
+ ```
52
+
53
+ **[GUIDE.md](GUIDE.md)** walks through a first audit, reading the map, every option, CI, and what
54
+ the tool cannot tell you.
55
+
56
+ ## In CI
57
+
58
+ ```yaml
59
+ - uses: prajwalghate/sourcetruth@v0
60
+ with:
61
+ path: ./contracts
62
+ min-resolution: 90
63
+ ```
64
+
65
+ The map is uploaded as a workflow artifact and a summary is written to the run.
66
+
67
+ ## Contributing
68
+
69
+ Bug reports with a small piece of code that shows the problem are the most useful thing you can send
70
+ — unfamiliar code is how most of this tool's bugs have been found. See [CONTRIBUTING.md](CONTRIBUTING.md).
71
+
72
+ ## What it is not
73
+
74
+ Not a bug finder. It has no vulnerability patterns and makes no judgement about whether code is
75
+ wrong. It draws what the code does so a person can decide — and it stores no findings.
76
+
77
+ ## License
78
+
79
+ [MIT](LICENSE)
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ // sourcetruth — read what contract code does, from source, and say what could not be determined.
3
+ //
4
+ // Exit codes: 0 parsed, 1 nothing found or below --min-resolution, 2 bad usage. Holes are NOT a
5
+ // failure — an honest 80% is the output, not an error.
6
+ //
7
+ // WHY THIS IS A FUNCTION WITH RETURNS, and never calls process.exit():
8
+ // stdout to a pipe is ASYNCHRONOUS. `process.exit()` tears the process down before the buffer
9
+ // drains, and `--json` on a real codebase was truncating at exactly 65282 bytes — a 64 KB pipe
10
+ // buffer — emitting JSON that ended mid-string. It looked perfect in a terminal, where stdout is
11
+ // synchronous, and broke the instant anyone piped it to jq. Set `process.exitCode` and return;
12
+ // Node then exits on its own once the buffer has flushed.
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import daml from "../src/adapters/daml.mjs";
18
+ import evm from "../src/adapters/evm.mjs";
19
+ import { graph, attackSurface } from "../src/model.mjs";
20
+ import { render as renderHtml } from "../src/report.mjs";
21
+
22
+ const PKG = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
23
+ const ADAPTERS = { daml, evm, solidity: evm };
24
+
25
+ /** The JSON shape's version. Bump it when a field changes meaning or goes away — tools key off it. */
26
+ const SCHEMA_VERSION = 1;
27
+
28
+ const USAGE = `sourcetruth ${PKG.version} — see what contract code does, from source
29
+
30
+ Usage
31
+ sourcetruth <dir> [mode] [options]
32
+
33
+ Modes (pick one; the default prints a summary and the blind spots)
34
+ --html the interactive map, as one self-contained HTML file
35
+ --surface state-changing actions that at most one party can take alone
36
+ --holes only what could not be traced
37
+ --graph contract-to-contract calls and creates
38
+ --json the full model, for your own tooling
39
+
40
+ Options
41
+ -o, --out <file> write the output to a file instead of stdout
42
+ --title <text> name the map (default: the directory name)
43
+ --lang daml|evm force the language (default: detected)
44
+ --min-resolution N exit 1 if less than N% of links could be traced (for CI)
45
+ --include-tests include test packages, TestContracts/, mocks/, *.t.sol
46
+ --include-libs report vendored dependencies (lib/) as if they were this code
47
+ --dated stamp today's date on the map (off by default, so maps diff cleanly)
48
+ --demo [daml|solidity] render a bundled example instead of <dir> (default: solidity)
49
+ -v, --version print the version
50
+ -h, --help print this
51
+
52
+ Examples
53
+ sourcetruth --demo -o demo.html
54
+ sourcetruth ./contracts --html -o map.html
55
+ sourcetruth ./daml --surface
56
+ sourcetruth . --json | jq '.entries[] | select(.authority == []) | .name'
57
+ sourcetruth ./src --min-resolution 90`;
58
+
59
+ /** Pick an adapter by what the tree contains. Ties go to whichever has more files. */
60
+ function detectLanguage(root) {
61
+ try {
62
+ const nDaml = daml.findSources(root).length;
63
+ const nSol = evm.findSources(root).length;
64
+ if (nSol > nDaml) return "evm";
65
+ if (nDaml > 0) return "daml";
66
+ return nSol > 0 ? "evm" : "daml";
67
+ } catch { return "daml"; }
68
+ }
69
+
70
+ function main(input) {
71
+ let argv = [...input];
72
+ const flag = (...names) => names.some((n) => argv.includes(n));
73
+ const opt = (names, d = null) => {
74
+ for (const n of [].concat(names)) { const i = argv.indexOf(n); if (i >= 0) return argv[i + 1] ?? d; }
75
+ return d;
76
+ };
77
+ // A bare word that is not the value of an option that takes one.
78
+ const VALUED = new Set(["--min-resolution", "--lang", "--title", "-o", "--out", "--demo"]);
79
+ let target = argv.find((a, i) => !a.startsWith("-") && !VALUED.has(argv[i - 1]));
80
+
81
+ if (flag("--help", "-h")) { console.log(USAGE); return 0; }
82
+ if (flag("--version", "-v")) { console.log(PKG.version); return 0; }
83
+ // A first look before pointing it at real code: render one of the bundled examples.
84
+ if (flag("--demo")) {
85
+ const which = String(opt("--demo", "solidity"));
86
+ const dir = /^(daml)$/i.test(which) ? "daml-lending" : "solidity-vault";
87
+ target = fileURLToPath(new URL(`../examples/${dir}`, import.meta.url));
88
+ if (!flag("--html", "--json", "--surface", "--holes", "--graph")) argv = [...argv, "--html"];
89
+ if (!argv.includes("--title")) argv = [...argv, "--title", `sourcetruth demo — ${dir}`];
90
+ }
91
+ if (!target) { console.error(USAGE); return 2; }
92
+ if (!fs.existsSync(target)) { console.error(`sourcetruth: no such file or directory: ${target}`); return 2; }
93
+
94
+ const lang = opt("--lang") ?? detectLanguage(path.resolve(target));
95
+ const adapter = ADAPTERS[lang];
96
+ if (!adapter) {
97
+ console.error(`no adapter for "${lang}" (have: ${Object.keys(ADAPTERS).join(", ")})`);
98
+ return 2;
99
+ }
100
+
101
+ let m;
102
+ try { m = adapter.parse(path.resolve(target), { includeLibs: flag("--include-libs"), includeTests: flag("--include-tests") }); }
103
+ catch (e) { console.error(`parse failed: ${e.message}`); return 1; }
104
+
105
+ // Output goes to stdout, or to --out. Either way it is written once, whole.
106
+ const outFile = opt(["-o", "--out"]);
107
+ const lines = [];
108
+ const say = (s = "") => lines.push(s);
109
+ const flush = () => {
110
+ const text = `${lines.join("\n")}\n`;
111
+ if (outFile) {
112
+ fs.writeFileSync(outFile, text);
113
+ console.error(`wrote ${outFile} (${Math.max(1, Math.round(Buffer.byteLength(text) / 1024))} KB)`);
114
+ } else process.stdout.write(text);
115
+ };
116
+
117
+ if (flag("--json")) {
118
+ say(JSON.stringify({ schemaVersion: SCHEMA_VERSION, tool: { name: "sourcetruth", version: PKG.version }, ...m }, null, 2));
119
+ flush();
120
+ return 0;
121
+ }
122
+
123
+ if (flag("--html")) {
124
+ // No timestamp unless asked: a report that differs every run cannot be diffed between commits,
125
+ // and "what changed since last week" is the question this format is for.
126
+ say(renderHtml(m, {
127
+ title: opt("--title", `sourcetruth — ${path.basename(path.resolve(target))}`),
128
+ generatedAt: flag("--dated") ? new Date().toISOString().slice(0, 10) : null,
129
+ }));
130
+ flush();
131
+ return 0;
132
+ }
133
+
134
+ if (flag("--holes")) {
135
+ for (const h of m.holes) say(`${h.path} ${h.owner} ${h.kind} ${h.raw}`);
136
+ flush();
137
+ return 0;
138
+ }
139
+ if (flag("--graph")) {
140
+ for (const e of graph(m).edges) {
141
+ say(`${e.from} --${e.kind}--> ${e.to}${e.external ? " [external]" : ""} (${e.via.join(", ")})`);
142
+ }
143
+ flush();
144
+ return 0;
145
+ }
146
+ if (flag("--surface")) {
147
+ for (const e of attackSurface(m)) {
148
+ say(`${String(e.unguarded ? "ANYONE" : e.authority).padEnd(18)} ${`${e.unit}.${e.name}`.padEnd(38)} ${e.effect.padEnd(11)} moves:${e.moves} holes:${e.holes} ${e.path}:${e.line}`);
149
+ }
150
+ flush();
151
+ return 0;
152
+ }
153
+
154
+ // Default: the summary an auditor wants first.
155
+ const s = m.stats;
156
+ say(`\n ${m.language} ${m.root}`);
157
+ say(` ${s.modules} module(s) ${s.units} unit(s) ${s.entries} entr(ies) ${s.edges} edge(s)`);
158
+ if (s.resolution !== null) {
159
+ say(`\n resolution ${s.resolution}% (${s.holes} edge(s) could not be resolved from source)`);
160
+ }
161
+ say(` solo-authority entries: ${s.soloAuthority} terminal entries: ${s.terminal}`);
162
+ for (const n of m.notes) say(` note: ${n}`);
163
+
164
+ if (m.holes.length) {
165
+ say(`\n HOLES — what this tool could not determine. Not guessed, not omitted:`);
166
+ const byOwner = new Map();
167
+ for (const h of m.holes) {
168
+ if (!byOwner.has(h.owner)) byOwner.set(h.owner, []);
169
+ byOwner.get(h.owner).push(h);
170
+ }
171
+ for (const [owner, hs] of [...byOwner].slice(0, 20)) {
172
+ say(` ${owner}`);
173
+ for (const h of hs.slice(0, 4)) say(` ${h.kind.padEnd(8)} ${h.raw.slice(0, 84)}`);
174
+ if (hs.length > 4) say(` … ${hs.length - 4} more`);
175
+ }
176
+ if (byOwner.size > 20) say(` … ${byOwner.size - 20} more entr(ies) with holes`);
177
+ }
178
+ flush();
179
+
180
+ const floor = opt("--min-resolution");
181
+ if (floor !== null && s.resolution !== null && s.resolution < Number(floor)) {
182
+ console.error(` resolution ${s.resolution}% is below the required ${floor}%`);
183
+ return 1;
184
+ }
185
+ return s.units === 0 ? 1 : 0;
186
+ }
187
+
188
+ process.exitCode = main(process.argv.slice(2));
@@ -0,0 +1,7 @@
1
+ sdk-version: 3.3.0
2
+ name: example-lending
3
+ version: 0.1.0
4
+ source: src
5
+ dependencies:
6
+ - daml-prim
7
+ - daml-stdlib
@@ -0,0 +1,92 @@
1
+ -- A small lending protocol, written to show what sourcetruth draws. Not production code.
2
+ module Lending where
3
+
4
+ template Pool
5
+ with
6
+ operator : Party
7
+ rateBps : Int
8
+ where
9
+ signatory operator
10
+
11
+ nonconsuming choice OpenPosition : ContractId Position
12
+ with
13
+ owner : Party
14
+ collateral : Decimal
15
+ controller owner
16
+ do
17
+ assertMsg "collateral must be positive" (collateral > 0.0)
18
+ create Position with operator; owner; collateral; debt = 0.0
19
+
20
+ choice SetRate : ContractId Pool
21
+ with
22
+ newRate : Int
23
+ controller operator
24
+ do
25
+ create this with rateBps = newRate
26
+
27
+ template Position
28
+ with
29
+ operator : Party
30
+ owner : Party
31
+ collateral : Decimal
32
+ debt : Decimal
33
+ where
34
+ signatory operator, owner
35
+
36
+ choice Borrow : ContractId Position
37
+ with
38
+ amount : Decimal
39
+ priceCid : ContractId Price
40
+ controller owner
41
+ do
42
+ price <- fetch priceCid
43
+ assertMsg "undercollateralised" (collateral * price.value >= (debt + amount) * 1.5)
44
+ create this with debt = debt + amount
45
+
46
+ choice Repay : ContractId Position
47
+ with
48
+ amount : Decimal
49
+ controller owner
50
+ do
51
+ create this with debt = debt - amount
52
+
53
+ choice Close : ()
54
+ controller owner
55
+ do
56
+ assertMsg "debt outstanding" (debt == 0.0)
57
+ pure ()
58
+
59
+ choice Liquidate : ContractId Receipt
60
+ with
61
+ liquidator : Party
62
+ priceCid : ContractId Price
63
+ evidence : [ContractId Price]
64
+ controller liquidator
65
+ do
66
+ price <- fetch priceCid
67
+ let (primary, backup) = (priceCid, head evidence)
68
+ second <- fetch backup
69
+ assertMsg "position is healthy" (collateral * price.value < debt * 1.1 && price.value == second.value)
70
+ create Receipt with operator; liquidator; seized = collateral
71
+
72
+ template Receipt
73
+ with
74
+ operator : Party
75
+ liquidator : Party
76
+ seized : Decimal
77
+ where
78
+ signatory operator
79
+
80
+ template Price
81
+ with
82
+ oracle : Party
83
+ value : Decimal
84
+ where
85
+ signatory oracle
86
+
87
+ choice Update : ContractId Price
88
+ with
89
+ newValue : Decimal
90
+ controller oracle
91
+ do
92
+ create this with value = newValue
@@ -0,0 +1,2 @@
1
+ [profile.default]
2
+ src = "src"