@wowok/agent-mcp 2.3.18 → 2.4.3
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/dist/customer/industry-risks.js +31 -31
- package/dist/customer/order-monitor.js +57 -57
- package/dist/customer/order-strategy.js +89 -89
- package/dist/customer/post-purchase.js +97 -97
- package/dist/customer/reminder-system.js +26 -26
- package/dist/customer/risk-assessment.js +46 -46
- package/dist/customer/types.d.ts +4 -4
- package/dist/customer/types.js +4 -4
- package/dist/customer/user-preferences.js +50 -50
- package/dist/experience/intent-distill.js +6 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +11 -1624
- package/dist/knowledge/acquisition-flywheel.js +64 -64
- package/dist/knowledge/arbitration-trust.js +10 -10
- package/dist/knowledge/demand-matching.js +8 -8
- package/dist/knowledge/dynamic-pricing.js +18 -18
- package/dist/knowledge/flywheel-loop.js +11 -11
- package/dist/knowledge/glossary.js +22 -22
- package/dist/knowledge/guard-ledger.d.ts +1 -1
- package/dist/knowledge/guard-ledger.js +145 -104
- package/dist/knowledge/guard-lint.d.ts +77 -0
- package/dist/knowledge/guard-lint.js +670 -0
- package/dist/knowledge/guard-migration.d.ts +48 -0
- package/dist/knowledge/guard-migration.js +228 -0
- package/dist/knowledge/guard-puzzle.d.ts +109 -14
- package/dist/knowledge/guard-puzzle.js +627 -101
- package/dist/knowledge/guard-risk.d.ts +10 -1
- package/dist/knowledge/guard-risk.js +1634 -292
- package/dist/knowledge/guard-templates.d.ts +38 -0
- package/dist/knowledge/guard-templates.js +605 -0
- package/dist/knowledge/guard-translation.d.ts +14 -2
- package/dist/knowledge/guard-translation.js +825 -194
- package/dist/knowledge/index.d.ts +8 -4
- package/dist/knowledge/index.js +9 -5
- package/dist/knowledge/industry-evolution.js +29 -29
- package/dist/knowledge/industry-generalizer.js +52 -52
- package/dist/knowledge/industry-registry.js +20 -33
- package/dist/knowledge/intent-metrics.js +67 -67
- package/dist/knowledge/process-model.js +80 -80
- package/dist/knowledge/reputation-rules.js +9 -9
- package/dist/knowledge/reward-templates.js +51 -51
- package/dist/knowledge/safety-rules.js +3 -3
- package/dist/knowledge/tool-constraints.js +20 -4
- package/dist/knowledge/trust-metrics.js +26 -26
- package/dist/project/graph.d.ts +1 -0
- package/dist/project/graph.js +27 -1
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.js +39 -2
- package/dist/project/namespace.d.ts +11 -1
- package/dist/project/namespace.js +27 -2
- package/dist/project/query.d.ts +2 -0
- package/dist/project/query.js +56 -15
- package/dist/rules.d.ts +12 -0
- package/dist/rules.js +9 -0
- package/dist/schema/call/allocation.d.ts +10 -10
- package/dist/schema/call/base.js +19 -19
- package/dist/schema/call/bridge.d.ts +32 -32
- package/dist/schema/call/demand.d.ts +84 -84
- package/dist/schema/call/guard.d.ts +153 -0
- package/dist/schema/call/guard.js +50 -0
- package/dist/schema/call/machine.d.ts +38 -38
- package/dist/schema/call/permission.d.ts +78 -78
- package/dist/schema/call/repository.d.ts +22 -22
- package/dist/schema/call/semantic.js +229 -25
- package/dist/schema/call/service.d.ts +7 -7
- package/dist/schema/local/wip.d.ts +39 -19
- package/dist/schema/local/wip.js +5 -5
- package/dist/schema/messenger/index.d.ts +26 -26
- package/dist/schema/messenger/index.js +2 -2
- package/dist/schema/operations.d.ts +270 -192
- package/dist/schema/query/index.d.ts +184 -184
- package/dist/schema/query/index.js +15 -9
- package/dist/schema/trust/index.d.ts +4 -4
- package/dist/schema/utils/guard-parser.js +4 -4
- package/dist/schema/utils/guard-query-utils.d.ts +7 -0
- package/dist/schema/utils/guard-query-utils.js +14 -1
- package/dist/schema/utils/node-parser.js +14 -14
- package/dist/schemas/bridge_operation.output.json +15 -15
- package/dist/schemas/index.json +1 -1
- package/dist/schemas/messenger_operation.output.json +18 -16
- package/dist/schemas/onchain_events.output.json +15 -15
- package/dist/schemas/onchain_operations.output.json +16 -16
- package/dist/schemas/onchain_operations.schema.json +11 -20
- package/dist/schemas/onchain_operations_allocation.schema.json +7 -10
- package/dist/schemas/onchain_operations_machine.schema.json +2 -5
- package/dist/schemas/onchain_operations_service.schema.json +2 -5
- package/dist/schemas/onchain_table_data.output.json +20 -26
- package/dist/schemas/wip_file.output.json +24 -1
- package/dist/tools/handlers/bridge.d.ts +1 -0
- package/dist/tools/handlers/bridge.js +1 -0
- package/dist/tools/handlers/config.d.ts +2 -0
- package/dist/tools/handlers/config.js +71 -0
- package/dist/tools/handlers/file-export.d.ts +3 -0
- package/dist/tools/handlers/file-export.js +90 -0
- package/dist/tools/handlers/local.d.ts +30 -0
- package/dist/tools/handlers/local.js +27 -0
- package/dist/tools/handlers/messenger.d.ts +16 -0
- package/dist/tools/handlers/messenger.js +187 -0
- package/dist/tools/handlers/onchain.d.ts +2 -0
- package/dist/tools/handlers/onchain.js +246 -0
- package/dist/tools/handlers/project.d.ts +2 -0
- package/dist/tools/handlers/project.js +53 -0
- package/dist/tools/handlers/query.d.ts +5 -0
- package/dist/tools/handlers/query.js +256 -0
- package/dist/tools/handlers/schema-query.d.ts +2 -0
- package/dist/tools/handlers/schema-query.js +92 -0
- package/dist/tools/handlers/trust.d.ts +2 -0
- package/dist/tools/handlers/trust.js +194 -0
- package/dist/tools/handlers/wip.d.ts +2 -0
- package/dist/tools/handlers/wip.js +44 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +479 -0
- package/dist/tools/rules-hook.d.ts +2 -0
- package/dist/tools/rules-hook.js +22 -0
- package/dist/tools/shared.d.ts +29 -0
- package/dist/tools/shared.js +130 -0
- package/dist/tools/types.d.ts +35 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/wrap.d.ts +6 -0
- package/dist/tools/wrap.js +55 -0
- package/package.json +19 -5
|
@@ -1,367 +1,1693 @@
|
|
|
1
|
+
import { GUARDQUERY } from "@wowok/wowok";
|
|
2
|
+
function rootStr(ctx) {
|
|
3
|
+
return JSON.stringify(ctx.root).toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
function hasSignerCheck(ctx) {
|
|
6
|
+
return JSON.stringify(ctx.root).includes('"Signer"');
|
|
7
|
+
}
|
|
8
|
+
function hasClockCheck(ctx) {
|
|
9
|
+
return JSON.stringify(ctx.root).includes('"Clock"');
|
|
10
|
+
}
|
|
11
|
+
function hasGuardSelfRef(ctx) {
|
|
12
|
+
return JSON.stringify(ctx.root).includes('"Guard"');
|
|
13
|
+
}
|
|
14
|
+
function hasWitness(root) {
|
|
15
|
+
const str = JSON.stringify(root);
|
|
16
|
+
return str.includes('"convert_witness"');
|
|
17
|
+
}
|
|
18
|
+
function queriesRepository(ctx) {
|
|
19
|
+
return rootStr(ctx).includes('repository');
|
|
20
|
+
}
|
|
21
|
+
function queriesProgress(ctx) {
|
|
22
|
+
return rootStr(ctx).includes('progress');
|
|
23
|
+
}
|
|
24
|
+
function extractWitnessCodes(root) {
|
|
25
|
+
const codes = [];
|
|
26
|
+
const str = JSON.stringify(root);
|
|
27
|
+
const matches = str.matchAll(/"convert_witness"\s*:\s*(\d+)/g);
|
|
28
|
+
for (const m of matches) {
|
|
29
|
+
const code = parseInt(m[1], 10);
|
|
30
|
+
if (code >= 100 && code <= 108)
|
|
31
|
+
codes.push(code);
|
|
32
|
+
}
|
|
33
|
+
return codes;
|
|
34
|
+
}
|
|
35
|
+
const WITNESS_CHAIN = {
|
|
36
|
+
100: ["Order", "Progress"],
|
|
37
|
+
101: ["Order", "Machine"],
|
|
38
|
+
102: ["Order", "Service"],
|
|
39
|
+
103: ["Progress", "Machine"],
|
|
40
|
+
104: ["Arb", "Order"],
|
|
41
|
+
105: ["Arb", "Arbitration"],
|
|
42
|
+
106: ["Arb", "Order", "Progress"],
|
|
43
|
+
107: ["Arb", "Order", "Service"],
|
|
44
|
+
108: ["Arb", "Order", "Machine"],
|
|
45
|
+
};
|
|
46
|
+
function extractWitnessIdentifierPairs(root) {
|
|
47
|
+
const pairs = [];
|
|
48
|
+
const visited = new WeakSet();
|
|
49
|
+
function walk(node) {
|
|
50
|
+
if (node === null || typeof node !== "object")
|
|
51
|
+
return;
|
|
52
|
+
if (visited.has(node))
|
|
53
|
+
return;
|
|
54
|
+
visited.add(node);
|
|
55
|
+
if (typeof node.convert_witness === "number" &&
|
|
56
|
+
typeof node.identifier === "number") {
|
|
57
|
+
pairs.push({ code: node.convert_witness, identifier: node.identifier });
|
|
58
|
+
}
|
|
59
|
+
for (const key of Object.keys(node)) {
|
|
60
|
+
const child = node[key];
|
|
61
|
+
if (Array.isArray(child)) {
|
|
62
|
+
for (const item of child)
|
|
63
|
+
walk(item);
|
|
64
|
+
}
|
|
65
|
+
else if (child !== null && typeof child === "object") {
|
|
66
|
+
walk(child);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(root);
|
|
71
|
+
return pairs;
|
|
72
|
+
}
|
|
73
|
+
function getChildNodes(node) {
|
|
74
|
+
if (!node || typeof node !== "object")
|
|
75
|
+
return [];
|
|
76
|
+
const children = [];
|
|
77
|
+
if (Array.isArray(node.nodes))
|
|
78
|
+
children.push(...node.nodes);
|
|
79
|
+
if (node.left)
|
|
80
|
+
children.push(node.left);
|
|
81
|
+
if (node.right)
|
|
82
|
+
children.push(node.right);
|
|
83
|
+
if (node.vec)
|
|
84
|
+
children.push(node.vec);
|
|
85
|
+
if (node.target)
|
|
86
|
+
children.push(node.target);
|
|
87
|
+
if (node.object && typeof node.object === "object")
|
|
88
|
+
children.push(node.object);
|
|
89
|
+
if (Array.isArray(node.parameters))
|
|
90
|
+
children.push(...node.parameters);
|
|
91
|
+
return children.filter((c) => c && typeof c === "object");
|
|
92
|
+
}
|
|
93
|
+
function findSignerLogicEqualNodes(root) {
|
|
94
|
+
const results = [];
|
|
95
|
+
function walk(node, parentType) {
|
|
96
|
+
if (!node || typeof node !== "object")
|
|
97
|
+
return;
|
|
98
|
+
if (node.type === "logic_equal") {
|
|
99
|
+
const operands = Array.isArray(node.nodes)
|
|
100
|
+
? node.nodes
|
|
101
|
+
: node.left && node.right
|
|
102
|
+
? [node.left, node.right]
|
|
103
|
+
: [];
|
|
104
|
+
if (operands.length === 2) {
|
|
105
|
+
const signerIdx = operands.findIndex((op) => op?.type === "context" && op?.context === "Signer");
|
|
106
|
+
if (signerIdx >= 0) {
|
|
107
|
+
results.push({
|
|
108
|
+
node,
|
|
109
|
+
parentType,
|
|
110
|
+
otherOperand: operands[1 - signerIdx],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const child of getChildNodes(node)) {
|
|
116
|
+
walk(child, node.type);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
walk(root, null);
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
function detectLevel1StrictBinding(root, table) {
|
|
123
|
+
const signerChecks = findSignerLogicEqualNodes(root);
|
|
124
|
+
for (const check of signerChecks) {
|
|
125
|
+
const other = check.otherOperand;
|
|
126
|
+
if (other?.type === "identifier") {
|
|
127
|
+
const entry = table.find((e) => e.identifier === other.identifier);
|
|
128
|
+
if (entry &&
|
|
129
|
+
!entry.b_submission &&
|
|
130
|
+
entry.value_type === "Address" &&
|
|
131
|
+
typeof entry.value === "string" &&
|
|
132
|
+
entry.value.length > 0) {
|
|
133
|
+
return {
|
|
134
|
+
detected: true,
|
|
135
|
+
entry: {
|
|
136
|
+
identifier: entry.identifier,
|
|
137
|
+
value: entry.value,
|
|
138
|
+
name: entry.name,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { detected: false };
|
|
145
|
+
}
|
|
146
|
+
function isSignerCheckInLogicOr(root) {
|
|
147
|
+
const signerChecks = findSignerLogicEqualNodes(root);
|
|
148
|
+
return signerChecks.some((check) => check.parentType === "logic_or");
|
|
149
|
+
}
|
|
150
|
+
const QUERY_INVARIANT_MAP = (() => {
|
|
151
|
+
const map = new Map();
|
|
152
|
+
for (const q of GUARDQUERY) {
|
|
153
|
+
if (q.invariant !== undefined) {
|
|
154
|
+
map.set(q.id, q.invariant);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return map;
|
|
158
|
+
})();
|
|
159
|
+
function extractQueryIds(root) {
|
|
160
|
+
const ids = [];
|
|
161
|
+
const str = JSON.stringify(root);
|
|
162
|
+
const numMatches = str.matchAll(/"query"\s*:\s*(\d+)/g);
|
|
163
|
+
for (const m of numMatches) {
|
|
164
|
+
const id = parseInt(m[1], 10);
|
|
165
|
+
if (!ids.includes(id))
|
|
166
|
+
ids.push(id);
|
|
167
|
+
}
|
|
168
|
+
const nameMatches = str.matchAll(/"query"\s*:\s*"([^"]+)"/g);
|
|
169
|
+
for (const m of nameMatches) {
|
|
170
|
+
const name = m[1];
|
|
171
|
+
const entry = GUARDQUERY.find((q) => q.name === name);
|
|
172
|
+
if (entry && !ids.includes(entry.id))
|
|
173
|
+
ids.push(entry.id);
|
|
174
|
+
}
|
|
175
|
+
return ids;
|
|
176
|
+
}
|
|
177
|
+
function getQueryInvariant(queryId) {
|
|
178
|
+
return QUERY_INVARIANT_MAP.get(queryId);
|
|
179
|
+
}
|
|
180
|
+
function rootHasNonZeroQuery(root) {
|
|
181
|
+
const ids = extractQueryIds(root);
|
|
182
|
+
return ids.some((id) => QUERY_INVARIANT_MAP.has(id));
|
|
183
|
+
}
|
|
184
|
+
function rootAllQueriesNonZero(root) {
|
|
185
|
+
const ids = extractQueryIds(root);
|
|
186
|
+
if (ids.length === 0)
|
|
187
|
+
return false;
|
|
188
|
+
return ids.every((id) => QUERY_INVARIANT_MAP.has(id));
|
|
189
|
+
}
|
|
190
|
+
const REENTRANCY_SPECS = [
|
|
191
|
+
{
|
|
192
|
+
sceneId: "reward_guard",
|
|
193
|
+
level: "critical",
|
|
194
|
+
existsPrimitives: [1613],
|
|
195
|
+
countPrimitives: [1612],
|
|
196
|
+
addressPrimitives: [1626],
|
|
197
|
+
rationale: "Guard pass distributes reward funds to the Claimant AND creates a reward_record, but the Guard conditions (identity, time, node state) do NOT naturally invalidate after a claim. " +
|
|
198
|
+
"Without an explicit anti-reentrancy primitive, the same Claimant can repeatedly pass the Guard and DRAIN THE ENTIRE POOL. " +
|
|
199
|
+
"This is the most severe reentrancy scene — direct fund loss.",
|
|
200
|
+
mitigation: "Add ONE of the following to the root (wrapped in logic_and with existing conditions):\n" +
|
|
201
|
+
" - logic_not(query('reward.record has', object=reward, parameters=[claimant_address])) — RECOMMENDED (per-user Bool check)\n" +
|
|
202
|
+
" - logic_equal([query('reward.record count', object=reward), identifier(N)]) where table[N].value=0 — total-claim count is 0\n" +
|
|
203
|
+
" - logic_not(query('reward.record.find_first', object=reward, where={...})) — no matching record found\n" +
|
|
204
|
+
"Reference template: MyShop_Advanced Guard 7 condition 4 (logic_not(query_reward_record_exists(where.storeFromId=order_id)))",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
sceneId: "arbitration_voting_guard",
|
|
208
|
+
level: "critical",
|
|
209
|
+
existsPrimitives: [1404, 1406],
|
|
210
|
+
countPrimitives: [1405],
|
|
211
|
+
addressPrimitives: [],
|
|
212
|
+
rationale: "Guard pass counts the vote with the extracted weight. Without anti-reentrancy, the same voter can vote MULTIPLE times, " +
|
|
213
|
+
"multiplying their weight and skewing the arbitration result — which directly determines fund ownership via compensation. " +
|
|
214
|
+
"The voter's eligibility (identity, reputation) does NOT naturally invalidate after voting. " +
|
|
215
|
+
"Note: arb.voted_count (1403) is total voter count (NOT per-voter) and does NOT serve as an anti-reentrancy primitive for an individual voter.",
|
|
216
|
+
mitigation: "Add ONE of the following to the root (wrapped in logic_and):\n" +
|
|
217
|
+
" - logic_not(query('arb.voted has', object=arb, parameters=[voter_address])) — RECOMMENDED (per-voter Bool)\n" +
|
|
218
|
+
" - logic_not(query('arb.voted.agree has', object=arb, parameters=[voter_address, proposition_index])) — per-voter-per-proposition\n" +
|
|
219
|
+
" - logic_equal([query('arb.voted.agree count', object=arb, parameters=[voter_address]), identifier(N)]) where table[N].value=0",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
sceneId: "arbitration_usage_guard",
|
|
223
|
+
level: "high",
|
|
224
|
+
existsPrimitives: [1565],
|
|
225
|
+
countPrimitives: [1564],
|
|
226
|
+
addressPrimitives: [],
|
|
227
|
+
rationale: "Guard pass creates a new Arb case. Without anti-reentrancy, a customer can create multiple Arb cases for the SAME Order " +
|
|
228
|
+
"(up to MAX_DISPUTE_COUNT=10 per order.move L96), harassing the counterparty and inflating arbitration system load. " +
|
|
229
|
+
"Not direct fund loss, but severe system abuse. order::dispute (order.move L93-99) deduplicates by Arb address but does NOT prevent multiple distinct Arbs.",
|
|
230
|
+
mitigation: "Add ONE of the following to the root (wrapped in logic_and):\n" +
|
|
231
|
+
" - logic_not(query('order.dispute has', object=order, parameters=[existing_arb_address])) — per-Arb check\n" +
|
|
232
|
+
" - logic_equal([query('order.dispute count', object=order), identifier(N)]) where table[N].value=0 — STRICT: block any prior dispute\n" +
|
|
233
|
+
" - logic_as_u256_less_or_equal([query('order.dispute count', object=order), identifier(N)]) where table[N].value=K — LENIENT: allow up to K disputes",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
sceneId: "service_order_allocators_guard",
|
|
237
|
+
level: "info",
|
|
238
|
+
existsPrimitives: [1568],
|
|
239
|
+
countPrimitives: [],
|
|
240
|
+
addressPrimitives: [1569],
|
|
241
|
+
rationale: "Protocol-level protection: order::on_allocation (order.move L116-119) asserts ONE Allocation per Order (E_ALLOCATION_ALREADY_SPECIFIED). " +
|
|
242
|
+
"After alloc() executes, typical Rate-mode allocators (sum=10000) and Surplus-mode allocators drain balance to 0, making reentrancy unprofitable. " +
|
|
243
|
+
"EDGE CASE: Amount-mode-only allocators (no Surplus, total < full balance) leave remainder; a second alloc() could extract more — mitigated by preferring Rate/Surplus modes. " +
|
|
244
|
+
"External deposit via receive() is possible but irrational (attacker funds the Allocation they attack).",
|
|
245
|
+
mitigation: "Default: no action needed (protocol-level guard). IF using Amount-mode-only allocators without Surplus: " +
|
|
246
|
+
"consider adding logic_not(query('order.allocation some', object=order)) OR switch to Rate/Surplus mode to drain balance completely.",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
sceneId: "service_buy_guard",
|
|
250
|
+
level: "info",
|
|
251
|
+
existsPrimitives: [],
|
|
252
|
+
countPrimitives: [],
|
|
253
|
+
addressPrimitives: [],
|
|
254
|
+
rationale: "Each purchase creates a fresh Order; no existing Order state is modified. Default: no reentrancy risk. " +
|
|
255
|
+
"IF business intent is 'limit one purchase per user' (limited-edition goods, whitelist single-buy), an anti-reentrancy check is REQUIRED " +
|
|
256
|
+
"but no direct GUARDQUERY primitive exists for per-user purchase history.",
|
|
257
|
+
mitigation: "IF limit-purchase intent: record purchases in a Repository and add logic_not(query('repository.data has', ...)) to root; " +
|
|
258
|
+
"OR revoke EntityRegistrar membership after first buy; OR use an off-chain allowlist with on-chain Merkle proof.",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
sceneId: "repository_write_guard",
|
|
262
|
+
level: "info",
|
|
263
|
+
existsPrimitives: [1166],
|
|
264
|
+
countPrimitives: [],
|
|
265
|
+
addressPrimitives: [],
|
|
266
|
+
rationale: "Default writes are idempotent (same ID overwrites, different ID appends); no fund flow. " +
|
|
267
|
+
"IF business intent is 'one write per ID' (voting, immutable credential, single-attestation), an anti-reentrancy check is needed.",
|
|
268
|
+
mitigation: "IF one-write-per-ID intent: add logic_not(query('repository.data has', object=repository, parameters=[policy_name, user_or_object_id])) to root.",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
sceneId: "gen_passport_guard",
|
|
272
|
+
level: "info",
|
|
273
|
+
existsPrimitives: [],
|
|
274
|
+
countPrimitives: [],
|
|
275
|
+
addressPrimitives: [],
|
|
276
|
+
rationale: "Each gen_passport call generates an independent Passport (one Passport per operation). " +
|
|
277
|
+
"Multiple Passports for the same user are usually valid (each serves a different subsequent operation). " +
|
|
278
|
+
"IF business intent is 'one active Passport per user', an anti-reentrancy check is needed but no direct GUARDQUERY primitive exists.",
|
|
279
|
+
mitigation: "IF one-passport-per-user intent: use off-chain tracking or a Repository to record issued Passport IDs.",
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
const QUERY_TYPE_ALIASES = new Map([
|
|
283
|
+
["query_reward_record_exists", 1613],
|
|
284
|
+
["query_reward_record_count", 1612],
|
|
285
|
+
["query_reward_record_find", 1626],
|
|
286
|
+
["query_progress_history_find", 1274],
|
|
287
|
+
["query_progress_history_session_find", 1275],
|
|
288
|
+
["query_progress_history_session_forward_find", 1276],
|
|
289
|
+
["query_progress_history_session_count", 1273],
|
|
290
|
+
["query_progress_history_session_forward_count", 1262],
|
|
291
|
+
]);
|
|
292
|
+
function detectReentrancyProtection(root, table, spec) {
|
|
293
|
+
const allPrimitives = new Set([
|
|
294
|
+
...spec.existsPrimitives,
|
|
295
|
+
...spec.countPrimitives,
|
|
296
|
+
...spec.addressPrimitives,
|
|
297
|
+
]);
|
|
298
|
+
if (allPrimitives.size === 0) {
|
|
299
|
+
return {
|
|
300
|
+
state: "absent",
|
|
301
|
+
evidence: "No anti-reentrancy primitive available in GUARDQUERY for this scene",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const idToName = new Map();
|
|
305
|
+
const nameToId = new Map();
|
|
306
|
+
for (const id of allPrimitives) {
|
|
307
|
+
const entry = GUARDQUERY.find((q) => q.id === id);
|
|
308
|
+
if (entry) {
|
|
309
|
+
idToName.set(id, entry.name);
|
|
310
|
+
nameToId.set(entry.name, id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const primitiveNames = Array.from(nameToId.keys());
|
|
314
|
+
function resolvePrimitiveId(node) {
|
|
315
|
+
if (!node || typeof node !== "object")
|
|
316
|
+
return -1;
|
|
317
|
+
if (typeof node.query === "number" && allPrimitives.has(node.query)) {
|
|
318
|
+
return node.query;
|
|
319
|
+
}
|
|
320
|
+
if (typeof node.query === "string" && nameToId.has(node.query)) {
|
|
321
|
+
return nameToId.get(node.query);
|
|
322
|
+
}
|
|
323
|
+
if (typeof node.type === "string" && QUERY_TYPE_ALIASES.has(node.type)) {
|
|
324
|
+
const aliasId = QUERY_TYPE_ALIASES.get(node.type);
|
|
325
|
+
if (allPrimitives.has(aliasId)) {
|
|
326
|
+
return aliasId;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (typeof node.type === "string" && nameToId.has(node.type)) {
|
|
330
|
+
return nameToId.get(node.type);
|
|
331
|
+
}
|
|
332
|
+
return -1;
|
|
333
|
+
}
|
|
334
|
+
function isZeroValue(node) {
|
|
335
|
+
if (!node || typeof node !== "object")
|
|
336
|
+
return false;
|
|
337
|
+
if (typeof node.type === "string" &&
|
|
338
|
+
["number", "u8", "u16", "u32", "u64", "u128", "u256"].includes(node.type)) {
|
|
339
|
+
return node.value === 0 || node.value === "0" || node.value === 0n;
|
|
340
|
+
}
|
|
341
|
+
if (node.type === "identifier" && typeof node.identifier === "number") {
|
|
342
|
+
const entry = table.find((e) => e.identifier === node.identifier);
|
|
343
|
+
if (entry && !entry.b_submission) {
|
|
344
|
+
return entry.value === 0 || entry.value === "0" || entry.value === 0n;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
function getSiblings(parent, child) {
|
|
350
|
+
if (!parent || typeof parent !== "object")
|
|
351
|
+
return [];
|
|
352
|
+
const siblings = [];
|
|
353
|
+
if (Array.isArray(parent.nodes)) {
|
|
354
|
+
for (const n of parent.nodes) {
|
|
355
|
+
if (n !== child)
|
|
356
|
+
siblings.push(n);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (parent.left && parent.left !== child)
|
|
360
|
+
siblings.push(parent.left);
|
|
361
|
+
if (parent.right && parent.right !== child)
|
|
362
|
+
siblings.push(parent.right);
|
|
363
|
+
return siblings.filter((s) => s && typeof s === "object");
|
|
364
|
+
}
|
|
365
|
+
let foundPrimitive = false;
|
|
366
|
+
let foundProtected = false;
|
|
367
|
+
let pseudoEvidence = "";
|
|
368
|
+
let foundPrimitiveName = "";
|
|
369
|
+
function walk(node, parent) {
|
|
370
|
+
if (!node || typeof node !== "object")
|
|
371
|
+
return;
|
|
372
|
+
const primId = resolvePrimitiveId(node);
|
|
373
|
+
if (primId >= 0) {
|
|
374
|
+
foundPrimitive = true;
|
|
375
|
+
const primName = idToName.get(primId) ?? `query_${primId}`;
|
|
376
|
+
foundPrimitiveName = primName;
|
|
377
|
+
const parentType = parent?.type ?? null;
|
|
378
|
+
let properlyWrapped = false;
|
|
379
|
+
const isExistsStyle = spec.existsPrimitives.includes(primId);
|
|
380
|
+
const isAddressStyle = spec.addressPrimitives.includes(primId);
|
|
381
|
+
const isCountStyle = spec.countPrimitives.includes(primId);
|
|
382
|
+
if ((isExistsStyle || isAddressStyle) && parentType === "logic_not") {
|
|
383
|
+
properlyWrapped = true;
|
|
384
|
+
}
|
|
385
|
+
if (isCountStyle && parentType === "logic_equal") {
|
|
386
|
+
const siblings = getSiblings(parent, node);
|
|
387
|
+
if (siblings.some((sib) => isZeroValue(sib))) {
|
|
388
|
+
properlyWrapped = true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (properlyWrapped) {
|
|
392
|
+
foundProtected = true;
|
|
393
|
+
}
|
|
394
|
+
else if (!pseudoEvidence) {
|
|
395
|
+
const expected = isCountStyle
|
|
396
|
+
? "logic_equal with a zero operand"
|
|
397
|
+
: "logic_not";
|
|
398
|
+
pseudoEvidence = `Primitive "${primName}" found but its immediate parent is ${parentType ?? "root"} (expected ${expected}). The primitive is present but does NOT actually prevent reentrancy — this is a false sense of security.`;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const children = [];
|
|
402
|
+
if (Array.isArray(node.nodes))
|
|
403
|
+
children.push(...node.nodes);
|
|
404
|
+
if (node.left)
|
|
405
|
+
children.push(node.left);
|
|
406
|
+
if (node.right)
|
|
407
|
+
children.push(node.right);
|
|
408
|
+
if (node.node)
|
|
409
|
+
children.push(node.node);
|
|
410
|
+
if (node.vec)
|
|
411
|
+
children.push(node.vec);
|
|
412
|
+
if (node.target)
|
|
413
|
+
children.push(node.target);
|
|
414
|
+
if (node.object && typeof node.object === "object")
|
|
415
|
+
children.push(node.object);
|
|
416
|
+
if (Array.isArray(node.parameters))
|
|
417
|
+
children.push(...node.parameters);
|
|
418
|
+
for (const child of children) {
|
|
419
|
+
if (child && typeof child === "object") {
|
|
420
|
+
walk(child, node);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
walk(root, null);
|
|
425
|
+
if (foundProtected) {
|
|
426
|
+
return {
|
|
427
|
+
state: "protected",
|
|
428
|
+
evidence: `Anti-reentrancy primitive "${foundPrimitiveName}" is properly wrapped`,
|
|
429
|
+
foundPrimitiveName,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (foundPrimitive) {
|
|
433
|
+
return {
|
|
434
|
+
state: "pseudo",
|
|
435
|
+
evidence: pseudoEvidence,
|
|
436
|
+
foundPrimitiveName,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
state: "absent",
|
|
441
|
+
evidence: `No anti-reentrancy primitive found in root. Expected one of: ${primitiveNames.join(", ")}`,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
1
444
|
const RISK_RULES = [
|
|
2
445
|
{
|
|
3
|
-
id: "R-
|
|
4
|
-
|
|
446
|
+
id: "R-C1-01",
|
|
447
|
+
data_source_class: "type1_onchain_constant",
|
|
448
|
+
trigger: "Guard table contains Address constants with b_submission=false; values may change after Guard rebuild",
|
|
5
449
|
check: (ctx) => {
|
|
6
|
-
const
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
450
|
+
const constAddrEntries = ctx.table.filter((e) => !e.b_submission && e.value_type === "Address" && e.value);
|
|
451
|
+
if (constAddrEntries.length === 0)
|
|
452
|
+
return null;
|
|
453
|
+
return {
|
|
454
|
+
id: "R-C1-01",
|
|
455
|
+
data_source_class: "type1_onchain_constant",
|
|
456
|
+
level: "low",
|
|
457
|
+
category: "data_source_trust",
|
|
458
|
+
title: "Constant object addresses may be inconsistent after Guard rebuild",
|
|
459
|
+
description: `Guard table contains ${constAddrEntries.length} Address constants (b_submission=false).` +
|
|
460
|
+
"Guard is immutable, but if a future Guard rebuild is needed (e.g. to fix logic), the new Guard's table constant addresses may differ from the old version." +
|
|
461
|
+
"Objects referencing the old Guard (Machine/Service, etc.) will not auto-update, leading to constant address inconsistency.",
|
|
462
|
+
affected_stakeholders: ["provider"],
|
|
463
|
+
scenario: "Guard iteration: after rebuild, constant addresses change but references still point to the old Guard",
|
|
464
|
+
mitigation: "1) After Guard rebuild, update all references (Machine forward guard, Service buy_guard, etc.);" +
|
|
465
|
+
"2) Use guard2file to export a backup and record constant-address-to-semantics mappings;" +
|
|
466
|
+
"3) Document constant-address changes in the migration notes",
|
|
467
|
+
evidence: `Constant Address entries: ${constAddrEntries.map((e) => `#${e.identifier}=${e.value}`).join(", ")}`,
|
|
468
|
+
};
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: "R-C1-02",
|
|
473
|
+
data_source_class: "type1_onchain_constant",
|
|
474
|
+
trigger: "Guard table contains EntityRegistrar(0xaab) or EntityLinker(0xaaa) system address constants",
|
|
475
|
+
check: (ctx) => {
|
|
476
|
+
const sysAddrEntries = ctx.table.filter((e) => !e.b_submission &&
|
|
477
|
+
e.value_type === "Address" &&
|
|
478
|
+
typeof e.value === "string" &&
|
|
479
|
+
(e.value.toLowerCase() === "0xaab" || e.value.toLowerCase() === "0xaaa"));
|
|
480
|
+
if (sysAddrEntries.length === 0)
|
|
481
|
+
return null;
|
|
482
|
+
const wrong = sysAddrEntries.find((e) => (e.value === "0xaab" && e.name && !e.name.toLowerCase().includes("registrar")) ||
|
|
483
|
+
(e.value === "0xaaa" && e.name && !e.name.toLowerCase().includes("linker")));
|
|
484
|
+
return {
|
|
485
|
+
id: "R-C1-02",
|
|
486
|
+
data_source_class: "type1_onchain_constant",
|
|
487
|
+
level: wrong ? "medium" : "info",
|
|
488
|
+
category: "data_source_trust",
|
|
489
|
+
title: wrong
|
|
490
|
+
? "EntityRegistrar/EntityLinker system address name annotation suspected to be wrong"
|
|
491
|
+
: "Guard uses EntityRegistrar/EntityLinker system address constants",
|
|
492
|
+
description: "EntityRegistrar (0xaab) and EntityLinker (0xaaa) are fixed WoWok protocol system addresses." +
|
|
493
|
+
(wrong
|
|
494
|
+
? "Detected name annotation does not match the address (e.g. 0xaab not annotated as registrar), which may cause semantic misunderstanding."
|
|
495
|
+
: "Address is correct, but ensure the name annotation is clear to avoid later maintenance confusion."),
|
|
496
|
+
affected_stakeholders: ["provider"],
|
|
497
|
+
scenario: "System address misuse: incorrect 0xaab/0xaaa name annotation causes query target confusion",
|
|
498
|
+
mitigation: "1) Confirm 0xaab corresponds to EntityRegistrar (entity registrar);" +
|
|
499
|
+
"2) Confirm 0xaaa corresponds to EntityLinker (entity linker);" +
|
|
500
|
+
"3) The table entry name should clearly indicate the system address purpose",
|
|
501
|
+
evidence: `System address entries: ${sysAddrEntries.map((e) => `#${e.identifier}=${e.value}(${e.name})`).join(", ")}`,
|
|
502
|
+
};
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
id: "R-C1-03",
|
|
507
|
+
data_source_class: "type1_onchain_constant",
|
|
508
|
+
trigger: "Guard queries Repository data without verifying writer permissions",
|
|
509
|
+
check: (ctx) => {
|
|
510
|
+
if (!queriesRepository(ctx))
|
|
511
|
+
return null;
|
|
512
|
+
if (hasSignerCheck(ctx))
|
|
513
|
+
return null;
|
|
514
|
+
return {
|
|
515
|
+
id: "R-C1-03",
|
|
516
|
+
data_source_class: "type1_onchain_constant",
|
|
517
|
+
level: "high",
|
|
518
|
+
category: "data_source_trust",
|
|
519
|
+
title: "Repository data source does not verify writer permissions",
|
|
520
|
+
description: "Guard queries Repository data but does not verify who has permission to write to that Repository." +
|
|
521
|
+
"A malicious writer can manipulate Repository data to deceive the Guard. As a Type1 constant object, " +
|
|
522
|
+
"the Repository's address is immutable, but its internal data can be modified by authorized writers.",
|
|
523
|
+
affected_stakeholders: ["customer", "provider"],
|
|
524
|
+
scenario: "Data source manipulation: attacker writes false data so Guard passes",
|
|
525
|
+
mitigation: "1) Restrict writer permissions in the Repository write_guard;" +
|
|
526
|
+
"2) Add context(Signer) in the current Guard to verify writer identity;" +
|
|
527
|
+
"3) Use multi-point data source cross-validation",
|
|
528
|
+
};
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: "R-C2-01",
|
|
533
|
+
data_source_class: "type2_witness_derived",
|
|
534
|
+
trigger: "Guard uses multi-hop witness (106/107/108: Arb→Progress/Machine/Service) to derive target object",
|
|
535
|
+
check: (ctx) => {
|
|
536
|
+
const codes = extractWitnessCodes(ctx.root);
|
|
537
|
+
const multiHop = codes.filter((c) => [106, 107, 108].includes(c));
|
|
538
|
+
if (multiHop.length === 0)
|
|
539
|
+
return null;
|
|
540
|
+
const witnessNames = {
|
|
541
|
+
106: "Arb→Progress (two hops: arb.order → order.progress)",
|
|
542
|
+
107: "Arb→Machine (two hops: arb.order → order.machine)",
|
|
543
|
+
108: "Arb→Service (two hops: arb.order → order.service)",
|
|
544
|
+
};
|
|
545
|
+
return {
|
|
546
|
+
id: "R-C2-01",
|
|
547
|
+
data_source_class: "type2_witness_derived",
|
|
548
|
+
level: "medium",
|
|
549
|
+
category: "data_source_trust",
|
|
550
|
+
title: "Multi-hop witness chain derivation may break",
|
|
551
|
+
description: `Guard uses multi-hop witness [${multiHop.join(", ")}] to derive target object.` +
|
|
552
|
+
"Multi-hop witness (e.g. Arb→Progress requires reading arb.order then order.progress) requires intermediate objects to exist and fields to be set." +
|
|
553
|
+
"If an intermediate object does not exist (e.g. Arb's order field is empty) or the relationship chain breaks, witness derivation fails." +
|
|
554
|
+
"In the WoWok protocol this usually returns empty or 0, potentially causing unexpected Guard behavior.\n" +
|
|
555
|
+
multiHop.map((c) => ` - ${c}: ${witnessNames[c]}`).join("\n"),
|
|
556
|
+
affected_stakeholders: ["customer", "provider", "arbitrator"],
|
|
557
|
+
scenario: "Witness break: intermediate object missing causes derivation failure and unexpected Guard behavior",
|
|
558
|
+
mitigation: "1) Confirm the Arb object's order field is properly set (Arb is bound to Order at creation);" +
|
|
559
|
+
"2) Add null checks for derived results in Guard logic;" +
|
|
560
|
+
"3) Prefer single-hop witness (100-105) to reduce derivation chain length;" +
|
|
561
|
+
"4) Test the edge case where Arb is not associated with an Order",
|
|
562
|
+
evidence: `Multi-hop witness codes: ${multiHop.join(", ")}`,
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
id: "R-C2-02",
|
|
568
|
+
data_source_class: "type2_witness_derived",
|
|
569
|
+
trigger: "Guard uses witness but source object's object_type may not match the witness source type, or multi-hop chain has missing intermediate objects",
|
|
570
|
+
check: (ctx) => {
|
|
571
|
+
const pairs = extractWitnessIdentifierPairs(ctx.root);
|
|
572
|
+
const validPairs = pairs.filter((p) => p.code >= 100 && p.code <= 108);
|
|
573
|
+
if (validPairs.length === 0)
|
|
574
|
+
return null;
|
|
575
|
+
const hardMismatches = [];
|
|
576
|
+
const missingAnnotations = [];
|
|
577
|
+
for (const { code, identifier } of validPairs) {
|
|
578
|
+
const chain = WITNESS_CHAIN[code];
|
|
579
|
+
if (!chain)
|
|
580
|
+
continue;
|
|
581
|
+
const expectedSource = chain[0];
|
|
582
|
+
const entry = ctx.table.find((e) => e.identifier === identifier);
|
|
583
|
+
if (!entry)
|
|
584
|
+
continue;
|
|
585
|
+
const chainStr = `${code} (${chain.join("→")})`;
|
|
586
|
+
if (entry.object_type) {
|
|
587
|
+
if (entry.object_type !== expectedSource) {
|
|
588
|
+
hardMismatches.push(`witness ${chainStr} references #${identifier} ` +
|
|
589
|
+
`but object_type=${entry.object_type} (expected ${expectedSource})`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
missingAnnotations.push(`witness ${chainStr} references #${identifier} ` +
|
|
594
|
+
`but object_type is not set (expected ${expectedSource})`);
|
|
595
|
+
}
|
|
596
|
+
if (chain.length === 3) {
|
|
597
|
+
const intermediateType = chain[1];
|
|
598
|
+
const hasIntermediate = ctx.table.some((e) => e.object_type === intermediateType);
|
|
599
|
+
if (!hasIntermediate) {
|
|
600
|
+
missingAnnotations.push(`witness ${chainStr} multi-hop chain expects intermediate ` +
|
|
601
|
+
`${intermediateType} in table, but no entry with object_type=${intermediateType} found`);
|
|
602
|
+
}
|
|
23
603
|
}
|
|
24
604
|
}
|
|
25
|
-
|
|
605
|
+
const totalIssues = hardMismatches.length + missingAnnotations.length;
|
|
606
|
+
if (totalIssues === 0)
|
|
607
|
+
return null;
|
|
608
|
+
const level = hardMismatches.length > 0 ? "medium" : "low";
|
|
609
|
+
const evidenceParts = [
|
|
610
|
+
...hardMismatches,
|
|
611
|
+
...missingAnnotations,
|
|
612
|
+
];
|
|
613
|
+
return {
|
|
614
|
+
id: "R-C2-02",
|
|
615
|
+
data_source_class: "type2_witness_derived",
|
|
616
|
+
level,
|
|
617
|
+
category: "data_source_trust",
|
|
618
|
+
title: hardMismatches.length > 0
|
|
619
|
+
? "witness source object_type explicitly contradicts the witness derivation chain"
|
|
620
|
+
: "witness source object_type annotation missing or multi-hop chain incomplete",
|
|
621
|
+
description: "Guard uses witness derivation, but the source object's object_type in table is either " +
|
|
622
|
+
"inconsistent with the witness expected source type, or the annotation is missing " +
|
|
623
|
+
"(preventing validation), or the multi-hop chain lacks an intermediate object.\n" +
|
|
624
|
+
evidenceParts.map((e) => ` - ${e}`).join("\n") +
|
|
625
|
+
"\nNote: object_type is an auxiliary annotation field on table entries; it is not strictly " +
|
|
626
|
+
"validated at on-chain creation. However, wrong or missing annotations cause semantic " +
|
|
627
|
+
"misunderstanding and maintenance difficulty, and may indicate a deeper structural mismatch.",
|
|
628
|
+
affected_stakeholders: ["provider"],
|
|
629
|
+
scenario: hardMismatches.length > 0
|
|
630
|
+
? "Type annotation mismatch: witness source type annotation is wrong, causing semantic confusion"
|
|
631
|
+
: "Type annotation missing: witness source type cannot be validated, raising maintenance risk",
|
|
632
|
+
mitigation: "1) Set or fix the table entry's object_type field to match the witness source type;\n" +
|
|
633
|
+
"2) witness 100-102 source type is Order; 103 is Progress; 104-108 is Arb;\n" +
|
|
634
|
+
"3) For multi-hop witnesses (106-108), ensure the intermediate Order object is also in the table;\n" +
|
|
635
|
+
"4) Use wowok_buildin_info(info='guard instructions') to query the full witness definition",
|
|
636
|
+
evidence: evidenceParts.join("; "),
|
|
637
|
+
};
|
|
26
638
|
},
|
|
27
639
|
},
|
|
28
640
|
{
|
|
29
|
-
id: "R-
|
|
30
|
-
|
|
641
|
+
id: "R-C2-03",
|
|
642
|
+
data_source_class: "type2_witness_derived",
|
|
643
|
+
trigger: "Guard queries Progress status without verifying Machine publish state",
|
|
31
644
|
check: (ctx) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
645
|
+
if (!queriesProgress(ctx))
|
|
646
|
+
return null;
|
|
647
|
+
if (!hasWitness(ctx.root))
|
|
648
|
+
return null;
|
|
649
|
+
if (ctx.scene?.host_object !== "Machine")
|
|
650
|
+
return null;
|
|
651
|
+
return {
|
|
652
|
+
id: "R-C2-03",
|
|
653
|
+
data_source_class: "type2_witness_derived",
|
|
654
|
+
level: "medium",
|
|
655
|
+
category: "data_source_trust",
|
|
656
|
+
title: "Progress data depends on Machine immutability",
|
|
657
|
+
description: "Guard queries Progress status via witness, and the Guard is bound to a Machine." +
|
|
658
|
+
"If the Machine is not published (bPublished=false), the Provider can modify the Machine node structure, " +
|
|
659
|
+
"causing node names validated by the Guard to become invalid or be renamed. Progress's current node name, node time, etc. " +
|
|
660
|
+
"all depend on the Machine's node definitions.",
|
|
661
|
+
affected_stakeholders: ["customer"],
|
|
662
|
+
scenario: "Provider modifies Machine node names to invalidate Guard verification",
|
|
663
|
+
mitigation: "1) Ensure the Machine is published (bPublished=true) before binding the Guard;" +
|
|
664
|
+
"2) Query the Machine.bPublished field in the Guard to verify;" +
|
|
665
|
+
"3) Use node IDs instead of node names for comparison",
|
|
666
|
+
};
|
|
49
667
|
},
|
|
50
668
|
},
|
|
51
669
|
{
|
|
52
|
-
id: "R-
|
|
53
|
-
|
|
670
|
+
id: "R-C2-04",
|
|
671
|
+
data_source_class: "type2_witness_derived",
|
|
672
|
+
trigger: "Guard uses witness query and the query parameter requires type conversion (e.g. Address → U64 timestamp key)",
|
|
673
|
+
check: (ctx) => {
|
|
674
|
+
if (!hasWitness(ctx.root))
|
|
675
|
+
return null;
|
|
676
|
+
const str = rootStr(ctx);
|
|
677
|
+
const hasTypeConversion = str.includes("convert_number_address") ||
|
|
678
|
+
str.includes("convert_address_number") ||
|
|
679
|
+
str.includes("convert_u256") ||
|
|
680
|
+
str.includes("convert_string");
|
|
681
|
+
const queriesRepo = str.includes("repository") && str.includes('"query"');
|
|
682
|
+
const queriesProgressHistory = str.includes("progress_history") && str.includes('"query"');
|
|
683
|
+
if (!queriesRepo && !queriesProgressHistory)
|
|
684
|
+
return null;
|
|
685
|
+
return {
|
|
686
|
+
id: "R-C2-04",
|
|
687
|
+
data_source_class: "type2_witness_derived",
|
|
688
|
+
level: "medium",
|
|
689
|
+
category: "data_source_trust",
|
|
690
|
+
title: "witness query parameter requires type translation (missing will cause type mismatch or wrong comparison)",
|
|
691
|
+
description: "Guard queries object fields via witness, but the query parameter type may not match the type declared in table.\n" +
|
|
692
|
+
"Typical scenarios:\n" +
|
|
693
|
+
" - Repository.data query: the key may be of Address type (vector<u8> address representation in Move), " +
|
|
694
|
+
"but the actual semantics is a U64 timestamp, requiring convert_number_address translation\n" +
|
|
695
|
+
" - Progress.history query: Forward ID may need to be converted from String to Address\n" +
|
|
696
|
+
" - Order field query: some ID fields may be stored as Address but need U256 for comparison\n" +
|
|
697
|
+
(hasTypeConversion
|
|
698
|
+
? "\nThe current Guard already uses type conversion nodes, but ensure the conversion direction is correct."
|
|
699
|
+
: "\nWarning: no type conversion node detected in the current Guard; parameter type mismatch risk may exist."),
|
|
700
|
+
affected_stakeholders: ["provider", "customer"],
|
|
701
|
+
scenario: "Query parameter type does not match the parameters type defined by GUARDQUERY, causing creation failure or runtime comparison error",
|
|
702
|
+
mitigation: "1) Query GUARDQUERY to confirm the parameters type and return type of each query instruction;\n" +
|
|
703
|
+
"2) If table declares Address but the query needs U64, use convert_number_address to translate;\n" +
|
|
704
|
+
"3) Use wowok_buildin_info info='guard instructions' to verify parameter types;\n" +
|
|
705
|
+
"4) Verify type compatibility via schema_query before creation",
|
|
706
|
+
evidence: hasTypeConversion ? "Type conversion node in use" : "No type conversion node detected",
|
|
707
|
+
};
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
id: "R-C3-01",
|
|
712
|
+
data_source_class: "type3_submitted_object",
|
|
713
|
+
trigger: "Guard depends on submitted data but does not verify signer identity (no Signer check=high; with Signer check=info downgrade)",
|
|
54
714
|
check: (ctx) => {
|
|
55
715
|
const hasSubmission = ctx.table.some((e) => e.b_submission);
|
|
56
716
|
if (!hasSubmission)
|
|
57
717
|
return null;
|
|
58
718
|
if (ctx.scene?.binding_field === "voting_guard")
|
|
59
719
|
return null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
720
|
+
if (ctx.scene?.id === "service_order_allocators_guard" &&
|
|
721
|
+
ctx.sharing_recipients &&
|
|
722
|
+
ctx.sharing_recipients.length > 0 &&
|
|
723
|
+
!ctx.sharing_recipients.includes("Signer")) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
if (!hasSignerCheck(ctx)) {
|
|
727
|
+
if (ctx.scene?.id === "machine_forward_guard") {
|
|
728
|
+
return {
|
|
729
|
+
id: "R-C3-01",
|
|
730
|
+
data_source_class: "type3_submitted_object",
|
|
731
|
+
level: "medium",
|
|
732
|
+
category: "submission_forgery",
|
|
733
|
+
title: "Submitted data not bound to signer identity (Forward scenario implicitly verified by permissionIndex)",
|
|
734
|
+
description: "Guard accepts runtime-submitted data (b_submission=true), and root does not explicitly verify Signer." +
|
|
735
|
+
"This Guard is used for Machine Forward; Forward's permissionIndex/namedOperator already implicitly verifies operator identity, " +
|
|
736
|
+
"so the Signer binding risk is reduced. But permissionIndex only verifies 'operator has permission', " +
|
|
737
|
+
"not 'submitted data belongs to operator' — for example, a merchant holding permissionIndex can still submit someone else's credentials.",
|
|
738
|
+
affected_stakeholders: ["customer", "provider"],
|
|
739
|
+
scenario: "In-permission forgery: an authorized operator submits someone else's data (e.g. someone else's Merkle Root)",
|
|
740
|
+
mitigation: "1) If the submitted data should belong to the operator (e.g. the operator's address), it is still recommended to add context(Signer) binding;" +
|
|
741
|
+
"2) If the submitted data is a public credential (e.g. Merkle Root string), the current risk is acceptable;" +
|
|
742
|
+
"3) Use retained_submission to retain submitted data for later audit",
|
|
743
|
+
};
|
|
744
|
+
}
|
|
63
745
|
return {
|
|
64
|
-
id: "R-
|
|
746
|
+
id: "R-C3-01",
|
|
747
|
+
data_source_class: "type3_submitted_object",
|
|
65
748
|
level: "high",
|
|
66
749
|
category: "submission_forgery",
|
|
67
|
-
title: "
|
|
68
|
-
description: "Guard
|
|
69
|
-
"
|
|
750
|
+
title: "Submitted data not bound to signer identity",
|
|
751
|
+
description: "Guard accepts runtime-submitted data (b_submission=true), but does not verify whether the submitter is the current transaction signer." +
|
|
752
|
+
"An attacker can submit someone else's data (e.g. someone else's address, someone else's credentials) to pass the Guard." +
|
|
753
|
+
"Type3 submitted objects are isomorphic with Type1 at the native layer (TYPE_QUERY+TYPE_CONSTANT), " +
|
|
754
|
+
"the only difference being that the value is injected by submission, so identity must be bound via logical constraints.",
|
|
70
755
|
affected_stakeholders: ["customer", "provider"],
|
|
71
|
-
scenario: "
|
|
72
|
-
mitigation: "1)
|
|
73
|
-
"2)
|
|
74
|
-
"3)
|
|
756
|
+
scenario: "Identity forgery: attacker submits someone else's address to impersonate an authorized user",
|
|
757
|
+
mitigation: "1) Add context(Signer) binding with the submitted data for verification;" +
|
|
758
|
+
"2) Use vec_contains_address to verify the signer is in the whitelist;" +
|
|
759
|
+
"3) Use Passport credentials instead of bare address submission",
|
|
75
760
|
};
|
|
76
761
|
}
|
|
77
762
|
return {
|
|
78
|
-
id: "R-
|
|
763
|
+
id: "R-C3-01",
|
|
764
|
+
data_source_class: "type3_submitted_object",
|
|
79
765
|
level: "info",
|
|
80
766
|
category: "submission_forgery",
|
|
81
|
-
title: "
|
|
82
|
-
description: "Guard
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
767
|
+
title: "Submitted data is indirectly bound via context(Signer) (confirm the binding direction is correct)",
|
|
768
|
+
description: "Guard accepts runtime-submitted data, and the root subtree contains a context(Signer) check, " +
|
|
769
|
+
"forming an indirect binding between submitted data and signer, reducing forgery risk." +
|
|
770
|
+
"But manual confirmation is needed: the constant compared with context(Signer) is the correct role (e.g. Provider address rather than Customer address), " +
|
|
771
|
+
"and the submitted data should indeed be provided by that signer. See R-C4-01 Signer direction risk.",
|
|
86
772
|
affected_stakeholders: ["customer", "provider"],
|
|
87
|
-
scenario: "
|
|
88
|
-
mitigation: "1)
|
|
89
|
-
"2)
|
|
90
|
-
"3)
|
|
773
|
+
scenario: "Indirect binding to confirm: whether the Signer check direction matches the submitted data ownership",
|
|
774
|
+
mitigation: "1) Confirm context(Signer) is compared with the correct table constant (Provider address rather than Customer address);" +
|
|
775
|
+
"2) Confirm the source role of the submitted data matches the Signer check;" +
|
|
776
|
+
"3) For stricter binding, use query to verify the submitter's on-chain identity",
|
|
91
777
|
};
|
|
92
778
|
},
|
|
93
779
|
},
|
|
94
780
|
{
|
|
95
|
-
id: "R-
|
|
96
|
-
|
|
781
|
+
id: "R-C3-02",
|
|
782
|
+
data_source_class: "type3_submitted_object",
|
|
783
|
+
trigger: "voting_guard weight comes from submitted data without verifying source",
|
|
97
784
|
check: (ctx) => {
|
|
98
|
-
if (ctx.scene?.binding_field
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
785
|
+
if (ctx.scene?.binding_field !== "voting_guard")
|
|
786
|
+
return null;
|
|
787
|
+
const numericEntries = ctx.table.filter((e) => e.b_submission &&
|
|
788
|
+
["U8", "U16", "U32", "U64", "U128", "U256"].includes(e.value_type));
|
|
789
|
+
if (numericEntries.length === 0)
|
|
790
|
+
return null;
|
|
791
|
+
return {
|
|
792
|
+
id: "R-C3-02",
|
|
793
|
+
data_source_class: "type3_submitted_object",
|
|
794
|
+
level: "critical",
|
|
795
|
+
category: "submission_forgery",
|
|
796
|
+
title: "Voting weight comes from user submission and may be forged",
|
|
797
|
+
description: "voting_guard weight data comes from table entries with b_submission=true (Type3 submitted values)." +
|
|
798
|
+
"If the weight is not verified through EntityRegistrar or other trusted sources, " +
|
|
799
|
+
"a voter can submit arbitrarily high weights to manipulate the voting result." +
|
|
800
|
+
"Type3 submitted values are only validated by type byte (value[0]) at the native layer; value range and source are not checked.",
|
|
801
|
+
affected_stakeholders: ["arbitrator"],
|
|
802
|
+
scenario: "Weight forgery: voters submit fake high scores to manipulate the arbitration result",
|
|
803
|
+
mitigation: "1) Weights should be queried from EntityRegistrar rather than submitted by users;" +
|
|
804
|
+
"2) Use query(entity.records) to obtain on-chain registered reputation scores;" +
|
|
805
|
+
"3) Verify in the Guard that the submitted weight matches the on-chain record",
|
|
806
|
+
evidence: `Submitted numeric entries: ${numericEntries.map((e) => `#${e.identifier}(${e.value_type})`).join(", ")}`,
|
|
807
|
+
};
|
|
118
808
|
},
|
|
119
809
|
},
|
|
120
810
|
{
|
|
121
|
-
id: "R-
|
|
122
|
-
|
|
811
|
+
id: "R-C3-03",
|
|
812
|
+
data_source_class: "type3_submitted_object",
|
|
813
|
+
trigger: "Guard contains Address entries with b_submission=true; the runtime submitted object type may not match expectations",
|
|
123
814
|
check: (ctx) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
815
|
+
const submittedAddr = ctx.table.filter((e) => e.b_submission && e.value_type === "Address");
|
|
816
|
+
if (submittedAddr.length === 0)
|
|
817
|
+
return null;
|
|
818
|
+
const noType = submittedAddr.filter((e) => !e.object_type);
|
|
819
|
+
const hasQuery = rootStr(ctx).includes('"query"');
|
|
820
|
+
if (!hasQuery)
|
|
821
|
+
return null;
|
|
822
|
+
return {
|
|
823
|
+
id: "R-C3-03",
|
|
824
|
+
data_source_class: "type3_submitted_object",
|
|
825
|
+
level: noType.length > 0 ? "medium" : "low",
|
|
826
|
+
category: "submission_forgery",
|
|
827
|
+
title: noType.length > 0
|
|
828
|
+
? "Submitted object missing object_type annotation; runtime type validation relies on native inference"
|
|
829
|
+
: "Submitted object type validation relies on runtime native inference",
|
|
830
|
+
description: "Guard contains " + submittedAddr.length + " Address entries with b_submission=true (Type3 submitted objects)." +
|
|
831
|
+
"The native layer (passport.rs#parse_table_items) only checks the value[0] type byte for submission; " +
|
|
832
|
+
"object_type is derived at runtime by wobject_type and does not strictly validate the actual submitted object type.\n" +
|
|
833
|
+
(noType.length > 0
|
|
834
|
+
? `Warning: ${noType.length} entries are missing object_type annotation, semantics are unclear.`
|
|
835
|
+
: "object_type is annotated, but still confirm the annotation matches what the query expects."),
|
|
836
|
+
affected_stakeholders: ["customer", "provider"],
|
|
837
|
+
scenario: "Type mismatch: submitting a wrong-typed object causes query to return unexpected values",
|
|
838
|
+
mitigation: "1) Annotate object_type for all Address entries with b_submission=true;" +
|
|
839
|
+
"2) object_type should match the object type expected by the query instruction;" +
|
|
840
|
+
"3) Indirectly verify the object type via query results in Guard logic (e.g. query order.amount to verify it is an Order)",
|
|
841
|
+
evidence: noType.length > 0
|
|
842
|
+
? `Missing object_type: ${noType.map((e) => `#${e.identifier}`).join(", ")}`
|
|
843
|
+
: `Annotated: ${submittedAddr.map((e) => `#${e.identifier}=${e.object_type}`).join(", ")}`,
|
|
844
|
+
};
|
|
141
845
|
},
|
|
142
846
|
},
|
|
143
847
|
{
|
|
144
|
-
id: "R-
|
|
145
|
-
|
|
848
|
+
id: "R-C3-04",
|
|
849
|
+
data_source_class: "type3_submitted_object",
|
|
850
|
+
trigger: "Repository write_guard does not verify id_from_submission type",
|
|
146
851
|
check: (ctx) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
852
|
+
if (ctx.scene?.binding_field !== "write_guard")
|
|
853
|
+
return null;
|
|
854
|
+
return {
|
|
855
|
+
id: "R-C3-04",
|
|
856
|
+
data_source_class: "type3_submitted_object",
|
|
857
|
+
level: "high",
|
|
858
|
+
category: "binding_risk",
|
|
859
|
+
title: "Repository write_guard must verify id_from_submission type",
|
|
860
|
+
description: "The table entry referenced by Repository write_guard's id_from_submission must be of Address type (Type3 submitted object)." +
|
|
861
|
+
"The entry referenced by data_from_submission must match the Repository value_type." +
|
|
862
|
+
"Type mismatch will cause write failure or data corruption. The write_guard submission contains both id and data parts; " +
|
|
863
|
+
"the native layer distinguishes them by op_code, but value types must match the table declaration.",
|
|
864
|
+
affected_stakeholders: ["provider"],
|
|
865
|
+
scenario: "Type mismatch: write failure or data corruption",
|
|
866
|
+
mitigation: "1) Confirm the entry referenced by id_from_submission has value_type=Address;" +
|
|
867
|
+
"2) Confirm the entry referenced by data_from_submission has value_type matching the Repository;" +
|
|
868
|
+
"3) Cover write scenarios in gen_passport tests",
|
|
869
|
+
};
|
|
165
870
|
},
|
|
166
871
|
},
|
|
167
872
|
{
|
|
168
|
-
id: "R-
|
|
169
|
-
|
|
873
|
+
id: "R-C3-05",
|
|
874
|
+
data_source_class: "type3_submitted_object",
|
|
875
|
+
trigger: "Guard accepts a runtime-submitted Order/Progress/Arb object (Type3) but does NOT verify the submitted object belongs to the current project (Machine or Service). " +
|
|
876
|
+
"The WoWok runtime does NOT enforce any binding between the submitted order_id and the actual Progress being forwarded — verify_guard receives only the caller-provided submission bytes, never the Progress object under advancement (passport.move:27,251-253; progress.move:458-483). " +
|
|
877
|
+
"A caller can therefore submit an unrelated order_id from a DIFFERENT project while forwarding a different Progress, causing witness=100 (Order->Progress) to evaluate against foreign data.",
|
|
170
878
|
check: (ctx) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
879
|
+
const submittedAddrs = ctx.table.filter((e) => e.b_submission && e.value_type === "Address");
|
|
880
|
+
if (submittedAddrs.length === 0)
|
|
881
|
+
return null;
|
|
882
|
+
const witnessCodes = extractWitnessCodes(ctx.root);
|
|
883
|
+
const hasOrderWitness = witnessCodes.some((c) => [100, 101, 102].includes(c));
|
|
884
|
+
const hasProgressWitness = witnessCodes.some((c) => [103].includes(c));
|
|
885
|
+
const hasArbWitness = witnessCodes.some((c) => [104, 105, 106, 107, 108].includes(c));
|
|
886
|
+
const submittedOrders = submittedAddrs.filter((e) => e.object_type === "Order" || (hasOrderWitness && !e.object_type));
|
|
887
|
+
const submittedProgress = submittedAddrs.filter((e) => e.object_type === "Progress" || (hasProgressWitness && !e.object_type));
|
|
888
|
+
const submittedArbs = submittedAddrs.filter((e) => e.object_type === "Arb" || (hasArbWitness && !e.object_type));
|
|
889
|
+
const hasProjectRelevantSubmission = submittedOrders.length > 0 ||
|
|
890
|
+
submittedProgress.length > 0 ||
|
|
891
|
+
submittedArbs.length > 0;
|
|
892
|
+
if (!hasProjectRelevantSubmission)
|
|
893
|
+
return null;
|
|
894
|
+
const affectedSceneIds = [
|
|
895
|
+
"machine_forward_guard",
|
|
896
|
+
"service_order_allocators_guard",
|
|
897
|
+
"arbitration_usage_guard",
|
|
898
|
+
"arbitration_voting_guard",
|
|
899
|
+
"reward_guard",
|
|
900
|
+
"progress_submission_guard",
|
|
901
|
+
];
|
|
902
|
+
const sceneId = ctx.scene?.id;
|
|
903
|
+
const isAffectedScene = !sceneId || affectedSceneIds.includes(sceneId);
|
|
904
|
+
if (!isAffectedScene)
|
|
905
|
+
return null;
|
|
906
|
+
const PROJECT_BINDING_QUERY_IDS = new Set([1563, 1560, 1250, 1402, 1401]);
|
|
907
|
+
const rootQueryIds = extractQueryIds(ctx.root);
|
|
908
|
+
const hasProjectBinding = rootQueryIds.some((id) => PROJECT_BINDING_QUERY_IDS.has(id));
|
|
909
|
+
if (hasProjectBinding)
|
|
910
|
+
return null;
|
|
911
|
+
const criticalScenes = new Set([
|
|
912
|
+
"machine_forward_guard",
|
|
913
|
+
"service_order_allocators_guard",
|
|
914
|
+
]);
|
|
915
|
+
const level = sceneId && criticalScenes.has(sceneId)
|
|
916
|
+
? "critical"
|
|
917
|
+
: "high";
|
|
918
|
+
const unboundEntries = [];
|
|
919
|
+
for (const e of submittedOrders) {
|
|
920
|
+
unboundEntries.push(`#${e.identifier}${e.object_type ? "(Order)" : "(inferred Order via witness 100-102)"}`);
|
|
189
921
|
}
|
|
190
|
-
|
|
922
|
+
for (const e of submittedProgress) {
|
|
923
|
+
unboundEntries.push(`#${e.identifier}${e.object_type ? "(Progress)" : "(inferred Progress via witness 103)"}`);
|
|
924
|
+
}
|
|
925
|
+
for (const e of submittedArbs) {
|
|
926
|
+
unboundEntries.push(`#${e.identifier}${e.object_type ? "(Arb)" : "(inferred Arb via witness 104-108)"}`);
|
|
927
|
+
}
|
|
928
|
+
const sceneLabel = sceneId ?? "unknown scene (conservative flag)";
|
|
929
|
+
return {
|
|
930
|
+
id: "R-C3-05",
|
|
931
|
+
data_source_class: "type3_submitted_object",
|
|
932
|
+
level,
|
|
933
|
+
category: "submission_forgery",
|
|
934
|
+
title: "Submitted Order/Progress/Arb is NOT verified to belong to the current project (cross-project bypass — CRITICAL trust-boundary gap)",
|
|
935
|
+
description: `Guard accepts runtime-submitted Order/Progress/Arb object(s) [${unboundEntries.join(", ")}] but the root does NOT verify the submitted object belongs to the current project (no query of order.service/order.machine/progress.machine/arb.service compared to a project constant).\n\n` +
|
|
936
|
+
"TRUST-BOUNDARY GAP (verified via Move source):\n" +
|
|
937
|
+
" - The Passport submission is built by the CALLER, not auto-injected by the runtime (passport.move:120,149,157)\n" +
|
|
938
|
+
" - verify_guard receives ONLY the caller-provided submission bytes — it never sees the Progress/Order actually being forwarded (passport.move:27,251-253)\n" +
|
|
939
|
+
" - next_with_passport performs NO linkage check between the passport submission and the Progress under advancement (progress.move:458-483)\n" +
|
|
940
|
+
" - Even order_next_with_passport's E_ORDER_NOT_MATCH only checks the separate `order:&Order` parameter, NOT the order_id embedded in the passport submission (progress.move:452)\n\n" +
|
|
941
|
+
`EXPLOIT (scene=${sceneLabel}):\n` +
|
|
942
|
+
" 1. Attacker holds Order Y from project B (where progress.current_time is old enough to pass the time-lock)\n" +
|
|
943
|
+
" 2. Attacker calls forward on Order X's Progress (project A, freshly entered the node)\n" +
|
|
944
|
+
" 3. Attacker submits Order Y's ID in the passport (lying about which order is being forwarded)\n" +
|
|
945
|
+
" 4. Guard's witness=100 reads Order Y's Progress -> time-lock passes (Order Y is old)\n" +
|
|
946
|
+
" 5. forward actually executes on Order X's Progress -> TIME-LOCK BYPASSED\n\n" +
|
|
947
|
+
"This is distinct from R-C3-01 (Signer binding — WHO can submit) and R-C3-03 (type matching — WHAT type is submitted). " +
|
|
948
|
+
"R-C3-05 addresses WHETHER the submitted object belongs to the current project — a third orthogonal axis of submission trust. " +
|
|
949
|
+
"Even with R-C3-01 satisfied (authorized Signer) and R-C3-03 satisfied (correct type), the Signer can still submit an unrelated project's object.",
|
|
950
|
+
affected_stakeholders: ["customer", "provider", "arbitrator"],
|
|
951
|
+
scenario: "Cross-project bypass: authorized signer submits an unrelated project's order/progress/arb to bypass time-lock, status, or allocation conditions",
|
|
952
|
+
mitigation: "Add a project-binding check to the root (wrap existing root in logic_and):\n" +
|
|
953
|
+
" - For submitted Order: add logic_equal[query(1563: order.service), identifier[N](project_service_address)] — RECOMMENDED (service is the canonical project anchor)\n" +
|
|
954
|
+
" - For submitted Order: add logic_equal[query(1560: order.machine), identifier[N](project_machine_address)] — alternative (machine anchor)\n" +
|
|
955
|
+
" - For submitted Progress: add logic_equal[query(1250: progress.machine), identifier[N](project_machine_address)]\n" +
|
|
956
|
+
" - For submitted Arb: add logic_equal[query(1402: arb.service), identifier[N](project_service_address)]\n" +
|
|
957
|
+
" - Use witness=102 (TypeOrderService) on the submitted Order to derive the Service, then compare to the project service constant\n\n" +
|
|
958
|
+
"Intentional cross-project exception: if the Guard is DESIGNED to accept cross-project submissions (rare), document this explicitly in the description and add a binding_hint annotation 'cross_project_intentional:true' to suppress this rule.",
|
|
959
|
+
evidence: `Unbound submitted project-relevant entries: ${unboundEntries.join(", ")}. ` +
|
|
960
|
+
`Scene: ${sceneLabel}. ` +
|
|
961
|
+
`Missing project-binding queries: none of [1563(order.service), 1560(order.machine), 1250(progress.machine), 1402(arb.service)] present in root.`,
|
|
962
|
+
};
|
|
191
963
|
},
|
|
192
964
|
},
|
|
193
965
|
{
|
|
194
|
-
id: "R-
|
|
195
|
-
|
|
966
|
+
id: "R-C3-06",
|
|
967
|
+
data_source_class: "type3_submitted_object",
|
|
968
|
+
trigger: "Guard is bound to Service order_allocators (service_order_allocators_guard scene). " +
|
|
969
|
+
"In this scene the Guard decides IF allocation happens, while sharing.who decides WHERE funds go. " +
|
|
970
|
+
"If sharing.who=Signer, the transaction caller receives the funds — so a Guard that does NOT bind " +
|
|
971
|
+
"context(Signer) lets ANYONE who passes the Guard steal 100% of the order funds. " +
|
|
972
|
+
"This is a Guard+sharing coupling risk: neither piece alone reveals the theft — only their combination.",
|
|
196
973
|
check: (ctx) => {
|
|
197
|
-
if (ctx.
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
974
|
+
if (ctx.scene?.id !== "service_order_allocators_guard")
|
|
975
|
+
return null;
|
|
976
|
+
if (hasSignerCheck(ctx))
|
|
977
|
+
return null;
|
|
978
|
+
const recipients = ctx.sharing_recipients;
|
|
979
|
+
const sharingProvided = recipients !== undefined && recipients.length > 0;
|
|
980
|
+
const hasSignerSharing = sharingProvided && recipients.includes("Signer");
|
|
981
|
+
if (sharingProvided && !hasSignerSharing)
|
|
982
|
+
return null;
|
|
983
|
+
const level = hasSignerSharing ? "critical" : "medium";
|
|
984
|
+
const recipientsLabel = sharingProvided
|
|
985
|
+
? `[${recipients.join(", ")}]`
|
|
986
|
+
: "(not provided — conservative flag)";
|
|
987
|
+
const titleSuffix = hasSignerSharing
|
|
988
|
+
? "sharing.who=Signer confirmed — anyone can steal 100% of funds"
|
|
989
|
+
: "if sharing.who=Signer, anyone can steal 100% of funds";
|
|
990
|
+
return {
|
|
991
|
+
id: "R-C3-06",
|
|
992
|
+
data_source_class: "type3_submitted_object",
|
|
993
|
+
level,
|
|
994
|
+
category: "binding_risk",
|
|
995
|
+
title: `allocators Guard without Signer binding — ${titleSuffix}`,
|
|
996
|
+
description: `Guard is bound to Service order_allocators but does NOT verify context(Signer). ` +
|
|
997
|
+
`In the allocators scene, sharing.who determines the fund recipient:\n` +
|
|
998
|
+
` - sharing.who=Signer → funds flow to the CALLER (transaction signer)\n` +
|
|
999
|
+
` - sharing.who=Entity → funds flow to a FIXED address (Treasury/personal)\n` +
|
|
1000
|
+
` - sharing.who=GuardIdentifier → funds flow to a Guard-table-derived address\n\n` +
|
|
1001
|
+
`When sharing.who=Signer and the Guard does not bind Signer, ANY caller who passes the Guard ` +
|
|
1002
|
+
`receives the order funds — a direct theft. Even an authorized caller is unnecessary: the attacker ` +
|
|
1003
|
+
`only needs to pass the Guard's condition (e.g. submit any completed order_id), then the funds go to ` +
|
|
1004
|
+
`their own address.\n\n` +
|
|
1005
|
+
`This is distinct from R-C3-01 (generic Signer binding) and R-C3-05 (cross-project bypass). ` +
|
|
1006
|
+
`R-C3-06 addresses the allocators-specific Guard+sharing coupling: the SAFE design uses ` +
|
|
1007
|
+
`sharing.who=Entity so the recipient is fixed, making Guard Signer binding unnecessary.\n\n` +
|
|
1008
|
+
`sharing_recipients hint: ${recipientsLabel}`,
|
|
1009
|
+
affected_stakeholders: ["provider", "customer"],
|
|
1010
|
+
scenario: "Fund theft: attacker passes the allocators Guard (e.g. submits a completed order_id) and " +
|
|
1011
|
+
"sharing.who=Signer routes 100% of the order funds to the attacker's address",
|
|
1012
|
+
mitigation: "TWO mitigation options:\n" +
|
|
1013
|
+
" (A) Traditional: add context(Signer) binding to the Guard root " +
|
|
1014
|
+
"(logic_equal[context(Signer), identifier[N](authorized_address)]) so only the authorized " +
|
|
1015
|
+
"recipient can trigger allocation.\n" +
|
|
1016
|
+
" (B) PREFERRED: use sharing.who=Entity (Treasury object or fixed personal address) in the " +
|
|
1017
|
+
"Allocator's sharing config. Funds then always flow to the designated recipient regardless of " +
|
|
1018
|
+
"who triggers allocation — the Guard does NOT need to bind Signer. This is more robust because " +
|
|
1019
|
+
"it does not depend on Guard correctness.\n\n" +
|
|
1020
|
+
"Design checklist when using sharing.who=Entity:\n" +
|
|
1021
|
+
" - If Entity points to a Treasury object, verify the Treasury's permission is consistent with " +
|
|
1022
|
+
"the Service's permission (different permissions = different permission organizations = minor risk).\n" +
|
|
1023
|
+
" - allocators require unique guard addresses: create separate Guard objects (identical root/table, " +
|
|
1024
|
+
"different descriptions) for each allocator.\n" +
|
|
1025
|
+
" - First-match-wins: only the first Allocator whose Guard passes executes; multiple allocators " +
|
|
1026
|
+
"represent alternative strategies, not a simultaneous split.\n" +
|
|
1027
|
+
" - Still add order.service project binding (query 1563) to prevent cross-project bypass (R-C3-05) " +
|
|
1028
|
+
"triggering premature allocation.",
|
|
1029
|
+
evidence: `Scene: service_order_allocators_guard. ` +
|
|
1030
|
+
`Guard binds Signer: no. ` +
|
|
1031
|
+
`sharing_recipients: ${recipientsLabel}. ` +
|
|
1032
|
+
`Level rationale: ${hasSignerSharing ? "Signer sharing confirmed → critical" : "sharing hint absent → conservative medium"}.`,
|
|
1033
|
+
};
|
|
212
1034
|
},
|
|
213
1035
|
},
|
|
214
1036
|
{
|
|
215
|
-
id: "R-
|
|
216
|
-
|
|
1037
|
+
id: "R-C4-01",
|
|
1038
|
+
data_source_class: "type4_system_context",
|
|
1039
|
+
trigger: "Guard contains context(Signer) check; need to confirm comparison direction (== vs !=) and compared object role",
|
|
217
1040
|
check: (ctx) => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
1041
|
+
if (!hasSignerCheck(ctx))
|
|
1042
|
+
return null;
|
|
1043
|
+
const rootStrRaw = JSON.stringify(ctx.root);
|
|
1044
|
+
const hasEqual = rootStrRaw.includes('"logic_equal"');
|
|
1045
|
+
const hasNotEqual = rootStrRaw.includes('"logic_not_equal"');
|
|
1046
|
+
return {
|
|
1047
|
+
id: "R-C4-01",
|
|
1048
|
+
data_source_class: "type4_system_context",
|
|
1049
|
+
level: "medium",
|
|
1050
|
+
category: "system_context_risk",
|
|
1051
|
+
title: "Signer check direction needs manual confirmation (comparison operator and role match)",
|
|
1052
|
+
description: "Guard contains a context(Signer) check. Signer is a Type4 system context, " +
|
|
1053
|
+
"from TransactionContext.sender() (transaction initiator address).\n" +
|
|
1054
|
+
`Detected comparison: ${hasEqual ? "logic_equal(==)" : ""} ${hasNotEqual ? "logic_not_equal(!=)" : ""}\n` +
|
|
1055
|
+
"Manual confirmation required:\n" +
|
|
1056
|
+
" 1) Whether the comparison direction is correct: use == for identity verification (Signer == authorized address), use != for exclusion (Signer != blacklist address)\n" +
|
|
1057
|
+
" 2) Whether the compared object role is correct: buy_guard should compare the Customer address, Forward.guard may compare the Provider address\n" +
|
|
1058
|
+
" 3) Wrong direction causes reverse effect: == blacklist address would only allow blacklisted users",
|
|
1059
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1060
|
+
scenario: "Wrong direction: Signer == blacklist address would only allow blacklisted users",
|
|
1061
|
+
mitigation: "1) Confirm logic_equal is used for identity verification (Signer == authorized address);" +
|
|
1062
|
+
"2) Confirm logic_not_equal is used for exclusion (Signer != blacklist address);" +
|
|
1063
|
+
"3) Confirm the compared table constant is the address of the correct role;" +
|
|
1064
|
+
"4) Cover both expected-pass and expected-fail scenarios in gen_passport tests",
|
|
1065
|
+
evidence: `Comparison: ${hasEqual ? "==" : ""} ${hasNotEqual ? "!=" : ""}`,
|
|
1066
|
+
};
|
|
236
1067
|
},
|
|
237
1068
|
},
|
|
238
1069
|
{
|
|
239
|
-
id: "R-
|
|
240
|
-
|
|
1070
|
+
id: "R-C4-02",
|
|
1071
|
+
data_source_class: "type4_system_context",
|
|
1072
|
+
trigger: "Guard uses context(Clock) for precise boundary time-lock comparison",
|
|
241
1073
|
check: (ctx) => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
1074
|
+
if (!hasClockCheck(ctx))
|
|
1075
|
+
return null;
|
|
1076
|
+
const str = rootStr(ctx);
|
|
1077
|
+
const hasGreaterOrEqual = str.includes("logic_as_u256_greater_or_equal");
|
|
1078
|
+
const hasGreater = str.includes("logic_as_u256_greater");
|
|
1079
|
+
if (!hasGreaterOrEqual && !hasGreater)
|
|
1080
|
+
return null;
|
|
1081
|
+
return {
|
|
1082
|
+
id: "R-C4-02",
|
|
1083
|
+
data_source_class: "type4_system_context",
|
|
1084
|
+
level: "low",
|
|
1085
|
+
category: "system_context_risk",
|
|
1086
|
+
title: "Clock time-lock should have tolerance to avoid boundary invalidation from validator block-time variance",
|
|
1087
|
+
description: "Guard uses context(Clock) for time-lock comparison. Clock comes from the timestamp_ms field (U64 millisecond timestamp) of system shared object 0x6.\n" +
|
|
1088
|
+
"The Wow on-chain timestamp is produced by validator consensus and has a slight deviation from real time (usually < 1 second), " +
|
|
1089
|
+
"and block-time variance may cause timestamp jumps. Using precise boundaries (>= or ==) may cause:\n" +
|
|
1090
|
+
" - The time-lock expiration transaction is packaged exactly at the moment but fails due to slight timestamp deviation\n" +
|
|
1091
|
+
" - Or conversely, slight deviation causes the time-lock to pass early\n" +
|
|
1092
|
+
"It is recommended to use > instead of >=, or add a small tolerance constant.",
|
|
1093
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1094
|
+
scenario: "Timestamp variance: validator block-time deviation causes boundary transactions to fail or pass early",
|
|
1095
|
+
mitigation: "1) Use logic_as_u256_greater (>) instead of greater_or_equal (>=) for expiration judgment;" +
|
|
1096
|
+
"2) Or add a small tolerance constant (e.g. 1000ms) in calc_number_add;" +
|
|
1097
|
+
"3) Time-locks should not rely on second-level precision; reserve minute-level tolerance;" +
|
|
1098
|
+
"4) Design timeout Forward nodes in the Machine as a fallback",
|
|
1099
|
+
evidence: `Comparison: ${hasGreaterOrEqual ? ">=" : ""} ${hasGreater ? ">" : ""}`,
|
|
1100
|
+
};
|
|
260
1101
|
},
|
|
261
1102
|
},
|
|
262
1103
|
{
|
|
263
|
-
id: "R-
|
|
264
|
-
|
|
1104
|
+
id: "R-C4-03",
|
|
1105
|
+
data_source_class: "type4_system_context",
|
|
1106
|
+
trigger: "Guard uses context(Guard) self-reference",
|
|
265
1107
|
check: (ctx) => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
return null;
|
|
1108
|
+
if (!hasGuardSelfRef(ctx))
|
|
1109
|
+
return null;
|
|
1110
|
+
return {
|
|
1111
|
+
id: "R-C4-03",
|
|
1112
|
+
data_source_class: "type4_system_context",
|
|
1113
|
+
level: "info",
|
|
1114
|
+
category: "system_context_risk",
|
|
1115
|
+
title: "Guard uses context(Guard) self-reference (rare scenario, needs special explanation)",
|
|
1116
|
+
description: "Guard contains a context(Guard) node. Guard is a Type4 system context, " +
|
|
1117
|
+
"returning the ObjectID of the currently verified Guard object itself.\n" +
|
|
1118
|
+
"This is a rare scenario, typically used for:\n" +
|
|
1119
|
+
" 1) Guard referencing its own address as a replacement for a table constant (avoiding hardcoding)\n" +
|
|
1120
|
+
" 2) Identifying the current Guard in a rely combination (rare)\n" +
|
|
1121
|
+
"Need to confirm whether the use of context(Guard) is necessary and whether the comparison object is correct.",
|
|
1122
|
+
affected_stakeholders: ["provider"],
|
|
1123
|
+
scenario: "Self-reference misuse: context(Guard) comparing wrong object causes logic invalidation",
|
|
1124
|
+
mitigation: "1) Confirm the context(Guard) comparison object is the Guard ObjectID rather than an address constant;" +
|
|
1125
|
+
"2) Evaluate whether self-reference is truly needed; usually a table constant can replace it;" +
|
|
1126
|
+
"3) Verify in gen_passport tests that self-reference behavior meets expectations",
|
|
1127
|
+
};
|
|
289
1128
|
},
|
|
290
1129
|
},
|
|
291
1130
|
{
|
|
292
|
-
id: "R-
|
|
293
|
-
|
|
1131
|
+
id: "R-C4-04",
|
|
1132
|
+
data_source_class: "type4_system_context",
|
|
1133
|
+
trigger: "Guard uses logic_equal[context(Signer), identifier[N](fixed_address)] — Level 1 strict single-identity binding. " +
|
|
1134
|
+
"Only ONE address can pass the Guard, and because Guards are immutable, changing the authorized address requires " +
|
|
1135
|
+
"rebuilding the Guard and migrating all references. This creates a convenience trade-off: maximum security but " +
|
|
1136
|
+
"maximum inflexibility (key loss or personnel change permanently blocks the operation).",
|
|
294
1137
|
check: (ctx) => {
|
|
295
|
-
if (
|
|
296
|
-
return
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
1138
|
+
if (!hasSignerCheck(ctx))
|
|
1139
|
+
return null;
|
|
1140
|
+
if (isSignerCheckInLogicOr(ctx.root))
|
|
1141
|
+
return null;
|
|
1142
|
+
const detection = detectLevel1StrictBinding(ctx.root, ctx.table);
|
|
1143
|
+
if (!detection.detected || !detection.entry)
|
|
1144
|
+
return null;
|
|
1145
|
+
const entry = detection.entry;
|
|
1146
|
+
return {
|
|
1147
|
+
id: "R-C4-04",
|
|
1148
|
+
data_source_class: "type4_system_context",
|
|
1149
|
+
level: "info",
|
|
1150
|
+
category: "system_context_risk",
|
|
1151
|
+
title: "Level 1 strict single-identity Signer binding — convenience trade-off (consider Level 2/3 alternatives)",
|
|
1152
|
+
description: `Guard uses logic_equal[context(Signer), identifier[${entry.identifier}](fixed address)] — only the address "${entry.value}" can pass.\n` +
|
|
1153
|
+
"This is Level 1 strict single-identity binding: maximally secure but maximally inconvenient.\n" +
|
|
1154
|
+
"Guards are immutable on-chain. If the authorized address becomes unavailable (key loss, personnel change, " +
|
|
1155
|
+
"role rotation), the Guard permanently blocks the operation — the only recovery is to build a new Guard " +
|
|
1156
|
+
"and migrate ALL references (Machine forward guards, Service buy_guard, allocators, etc.).\n\n" +
|
|
1157
|
+
"Three verifier constraint levels trade off security vs convenience:\n" +
|
|
1158
|
+
" - Level 1 (current): strict single-identity — one address, no flexibility (AVOID unless justified)\n" +
|
|
1159
|
+
" - Level 2: identity-set — logic_or of multiple valid identities (e.g. order.owner OR order.agent; " +
|
|
1160
|
+
"permission.owner OR permission.admin). Recommended for role-based access.\n" +
|
|
1161
|
+
" - Level 3: scene-combined — verify whether Signer binding is even needed. Many scenes (allocators with " +
|
|
1162
|
+
"sharing.who=Entity, machine forward with permissionIndex) already ensure safety without Signer binding.\n\n" +
|
|
1163
|
+
"Use Level 1 only when: (a) the role is permanently tied to one address, AND (b) the designer explicitly " +
|
|
1164
|
+
"accepts the immutability lock-in risk. Otherwise prefer Level 2 or Level 3.",
|
|
1165
|
+
affected_stakeholders: ["provider"],
|
|
1166
|
+
scenario: "Immutability lock-in: authorized address becomes unavailable, Guard permanently blocks the operation, " +
|
|
1167
|
+
"requiring full Guard rebuild and reference migration",
|
|
1168
|
+
mitigation: "Consider these alternatives before settling on Level 1 strict binding:\n" +
|
|
1169
|
+
" (A) Level 2 identity-set (RECOMMENDED for role-based access): use logic_or to allow multiple valid identities.\n" +
|
|
1170
|
+
" - Order holder: logic_or[logic_equal[query(1562: order.owner), context(Signer)], query(1567: order.agent has, parameters: [context(Signer)])]\n" +
|
|
1171
|
+
" - Service provider: logic_or[logic_equal[query(1002: permission.owner), context(Signer)], query(1004: permission.admin has, parameters: [context(Signer)])]\n" +
|
|
1172
|
+
" - Dynamic permission (survives rotation): submit permission address, verify query(1488: service.permission) == submitted, then check 1002/1004\n" +
|
|
1173
|
+
" (B) Level 3 scene-combined: evaluate whether Signer binding is needed at all.\n" +
|
|
1174
|
+
" - If using allocators with sharing.who=Entity (Treasury): funds flow to a fixed recipient regardless of caller — no Signer binding needed (R-C3-06 safe)\n" +
|
|
1175
|
+
" - If using machine forward: permissionIndex already verifies operator identity — Signer binding may be redundant\n" +
|
|
1176
|
+
" (C) If Level 1 is truly required: document the justification in the Guard description and ensure a key-recovery / address-rotation plan exists.",
|
|
1177
|
+
evidence: `Level 1 strict binding detected: logic_equal[context(Signer), identifier[${entry.identifier}]] ` +
|
|
1178
|
+
`where identifier[${entry.identifier}] is a fixed Address constant (b_submission=false, value="${entry.value}"${entry.name ? `, name="${entry.name}"` : ""}). ` +
|
|
1179
|
+
`Not wrapped in logic_or (not Level 2 identity-set).`,
|
|
1180
|
+
};
|
|
312
1181
|
},
|
|
313
1182
|
},
|
|
314
1183
|
{
|
|
315
|
-
id: "R-
|
|
316
|
-
|
|
1184
|
+
id: "R-X1-01",
|
|
1185
|
+
data_source_class: "cross_type",
|
|
1186
|
+
trigger: "Time-lock uses logic_as_u256_greater_or_equal with a baseline query that has NO non-zero invariant (i.e. the query MAY legitimately return 0 when it succeeds), causing Clock >= 0 + timeout to always be true",
|
|
317
1187
|
check: (ctx) => {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1188
|
+
const str = rootStr(ctx);
|
|
1189
|
+
const hasGreaterOrEqual = str.includes("logic_as_u256_greater_or_equal");
|
|
1190
|
+
const hasClock = str.includes('"clock"');
|
|
1191
|
+
const hasCalcAdd = str.includes("calc_number_add");
|
|
1192
|
+
const hasQuery = str.includes('"query"');
|
|
1193
|
+
if (!(hasGreaterOrEqual && hasClock && hasCalcAdd && hasQuery))
|
|
1194
|
+
return null;
|
|
1195
|
+
if (rootAllQueriesNonZero(ctx.root))
|
|
1196
|
+
return null;
|
|
1197
|
+
const queryIds = extractQueryIds(ctx.root);
|
|
1198
|
+
const riskyQueries = queryIds.filter((id) => !QUERY_INVARIANT_MAP.has(id));
|
|
1199
|
+
const evidence = riskyQueries.length > 0
|
|
1200
|
+
? `Queries without non-zero invariant: ${riskyQueries
|
|
1201
|
+
.map((id) => {
|
|
1202
|
+
const q = GUARDQUERY.find((x) => x.id === id);
|
|
1203
|
+
return q ? `${q.id} (${q.name})` : `${id}`;
|
|
1204
|
+
})
|
|
1205
|
+
.join(", ")}`
|
|
1206
|
+
: "Baseline query may return 0";
|
|
1207
|
+
return {
|
|
1208
|
+
id: "R-X1-01",
|
|
1209
|
+
data_source_class: "cross_type",
|
|
1210
|
+
level: "medium",
|
|
1211
|
+
category: "logic_gap",
|
|
1212
|
+
title: "Time-lock baseline query may return 0 causing >= comparison to always be true",
|
|
1213
|
+
description: "Guard uses logic_as_u256_greater_or_equal for time-lock comparison, and the compared baseline value comes from a query (Type1/2/3 data sources). " +
|
|
1214
|
+
"If the query SUCCEEDS but its stored value is 0 (e.g. a count that is 0 in the empty initial state, a user-submitted value of 0, or a Some(0) from an Option field), " +
|
|
1215
|
+
"then Clock >= 0 + timeout is always true, the time-lock is effectively void, and the disadvantaged party can bypass the time constraint immediately.\n\n" +
|
|
1216
|
+
"IMPORTANT — 'VM error = Guard failure' baseline: " +
|
|
1217
|
+
"if a query targets an absent object, unset Option, or missing collection entry, the WoWok native runtime aborts with a VM error (e.g. W_FIELD_NOT_FOUND) " +
|
|
1218
|
+
"and the Guard fails as a whole (does NOT silently return 0). Therefore the null-bypass risk ONLY applies to queries whose stored value can legitimately be 0 " +
|
|
1219
|
+
"while the query itself succeeds. Such queries are exactly those WITHOUT an `invariant` annotation in the SDK GUARDQUERY catalog.",
|
|
1220
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1221
|
+
scenario: "Time-lock invalidation: baseline query returns 0 (not VM error), making the >= condition always pass",
|
|
1222
|
+
mitigation: "1) Use a query whose return is guaranteed non-zero (look for `invariant` in guard-ins.ts — e.g. progress.current_time id=1272 with invariant='clock_derived'); " +
|
|
1223
|
+
"2) Add a precondition check query > 0 before the time-lock (for queries without an invariant, e.g. user-submitted baselines); " +
|
|
1224
|
+
"3) Avoid using count/index/find/user-submitted values as time-lock baselines — these can all legitimately return 0; " +
|
|
1225
|
+
"4) Distinguish 'VM error' (safe — Guard fails) from 'query returns 0' (unsafe — Guard bypassed). Only the latter is a null-bypass risk.",
|
|
1226
|
+
evidence,
|
|
1227
|
+
};
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
id: "R-X1-02",
|
|
1232
|
+
data_source_class: "cross_type",
|
|
1233
|
+
trigger: "Guard uses logic_as_u256_greater with a query operand that has NO non-zero invariant (i.e. the query MAY legitimately return 0 when it succeeds), and the comparison does not distinguish 'query succeeded with 0' from 'query failed'",
|
|
1234
|
+
check: (ctx) => {
|
|
1235
|
+
const str = rootStr(ctx);
|
|
1236
|
+
if (!(str.includes("logic_as_u256_greater") && str.includes('"query"')))
|
|
1237
|
+
return null;
|
|
1238
|
+
if (rootAllQueriesNonZero(ctx.root))
|
|
1239
|
+
return null;
|
|
1240
|
+
const queryIds = extractQueryIds(ctx.root);
|
|
1241
|
+
const riskyQueries = queryIds.filter((id) => !QUERY_INVARIANT_MAP.has(id));
|
|
1242
|
+
const evidence = riskyQueries.length > 0
|
|
1243
|
+
? `Queries without non-zero invariant: ${riskyQueries
|
|
1244
|
+
.map((id) => {
|
|
1245
|
+
const q = GUARDQUERY.find((x) => x.id === id);
|
|
1246
|
+
return q ? `${q.id} (${q.name})` : `${id}`;
|
|
1247
|
+
})
|
|
1248
|
+
.join(", ")}`
|
|
1249
|
+
: "Query may return 0";
|
|
1250
|
+
return {
|
|
1251
|
+
id: "R-X1-02",
|
|
1252
|
+
data_source_class: "cross_type",
|
|
1253
|
+
level: "medium",
|
|
1254
|
+
category: "logic_gap",
|
|
1255
|
+
title: "Numeric comparison with a query that may legitimately return 0",
|
|
1256
|
+
description: "Guard uses logic_as_u256_greater for numeric comparison, and one of the operands comes from a query (Type1/2/3 data sources) that has NO non-zero invariant annotation. " +
|
|
1257
|
+
"If the query SUCCEEDS but its stored value is 0 (e.g. a count that is 0 in the empty initial state, a user-submitted value of 0, or a Some(0) from an Option field), " +
|
|
1258
|
+
"the comparison may produce an unexpected result (e.g. `query > 0` fails, or `query > some_threshold` is silently bypassed).\n\n" +
|
|
1259
|
+
"IMPORTANT — 'VM error = Guard failure' baseline: " +
|
|
1260
|
+
"if a query targets absent data (e.g. object does not exist, Option is None, collection entry is missing), the WoWok native runtime aborts with a VM error " +
|
|
1261
|
+
"(not 0). The Guard fails as a whole (does NOT silently return 0). Therefore this rule only flags queries whose stored value can be 0 while the query succeeds.",
|
|
1262
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1263
|
+
scenario: "Null misjudgment: query succeeds with value 0, causing > 0 condition to unexpectedly fail (or > threshold to be bypassed)",
|
|
1264
|
+
mitigation: "1) Prefer queries with an `invariant` annotation (e.g. clock_derived / positive_lower_bound); " +
|
|
1265
|
+
"2) Add a precondition check (query > 0) before the main comparison when using count/index/find/user-submitted baselines; " +
|
|
1266
|
+
"3) Distinguish 'VM error' (safe — Guard fails) from 'query returns 0' (potentially unsafe). Only the latter is a null-bypass risk.",
|
|
1267
|
+
evidence,
|
|
1268
|
+
};
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
id: "R-X1-03",
|
|
1273
|
+
data_source_class: "cross_type",
|
|
1274
|
+
trigger: "Guard uses logic_string_contains and may be bypassed by substring matching",
|
|
1275
|
+
check: (ctx) => {
|
|
1276
|
+
const str = rootStr(ctx);
|
|
1277
|
+
if (!(str.includes("logic_string_contains") && !str.includes("logic_string_nocase")))
|
|
1278
|
+
return null;
|
|
1279
|
+
return {
|
|
1280
|
+
id: "R-X1-03",
|
|
1281
|
+
data_source_class: "cross_type",
|
|
1282
|
+
level: "low",
|
|
1283
|
+
category: "logic_gap",
|
|
1284
|
+
title: "logic_string_contains substring match may be bypassed",
|
|
1285
|
+
description: "Guard uses logic_string_contains for string matching." +
|
|
1286
|
+
"Substring matching may be bypassed: e.g. when checking 'Completed', 'NotCompleted' also contains 'Completed'." +
|
|
1287
|
+
"In addition, case sensitivity may cause unexpected failures. String data may come from Type1 (query object fields), " +
|
|
1288
|
+
"Type3 (submitted values), or the context of a rely-dependent Guard.",
|
|
1289
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1290
|
+
scenario: "Substring mismatch: 'NotCompleted' contains 'Completed', causing false pass",
|
|
1291
|
+
mitigation: "1) Prefer logic_equal for exact matching;" +
|
|
1292
|
+
"2) If substring matching is needed, add boundary conditions (e.g. prefix + suffix);" +
|
|
1293
|
+
"3) Confirm whether case-insensitivity is needed (use the nocase variant)",
|
|
1294
|
+
};
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
id: "R-X1-04",
|
|
1299
|
+
data_source_class: "cross_type",
|
|
1300
|
+
trigger: "Guard root is an identifier directly returning Bool",
|
|
1301
|
+
check: (ctx) => {
|
|
1302
|
+
if (ctx.root?.type !== "identifier")
|
|
1303
|
+
return null;
|
|
1304
|
+
const entry = ctx.table.find((e) => e.identifier === ctx.root.identifier);
|
|
1305
|
+
const isSubmission = entry?.b_submission ?? false;
|
|
1306
|
+
return {
|
|
1307
|
+
id: "R-X1-04",
|
|
1308
|
+
data_source_class: "cross_type",
|
|
1309
|
+
level: isSubmission ? "medium" : "low",
|
|
1310
|
+
category: "logic_gap",
|
|
1311
|
+
title: "Root directly returns identifier value, no operation logic",
|
|
1312
|
+
description: "Guard root is an identifier node, directly returning the value (Bool) from table." +
|
|
1313
|
+
(isSubmission
|
|
1314
|
+
? "This entry has b_submission=true (Type3 submitted value); the caller can directly submit true to bypass all verification."
|
|
1315
|
+
: "This entry has b_submission=false (Type1 constant); the value is fixed but the Guard has no actual verification logic."),
|
|
1316
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1317
|
+
scenario: isSubmission
|
|
1318
|
+
? "Bypass verification: caller submits true to pass the Guard directly"
|
|
1319
|
+
: "No verification logic: Guard always returns a fixed value, providing no actual protection",
|
|
1320
|
+
mitigation: isSubmission
|
|
1321
|
+
? "1) Ensure the entry referenced by identifier has b_submission=false (constant);" +
|
|
1322
|
+
"2) Or add an additional logic_and condition for cross-validation"
|
|
1323
|
+
: "1) Add actual verification logic (query + logic comparison);" +
|
|
1324
|
+
"2) Evaluate whether this Guard needs to exist",
|
|
1325
|
+
};
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
id: "R-X1-05",
|
|
1330
|
+
data_source_class: "cross_type",
|
|
1331
|
+
trigger: "Allocation Guard may cause some participants to be skipped",
|
|
1332
|
+
check: (ctx) => {
|
|
1333
|
+
if (ctx.scene?.binding_field !== "order_allocators")
|
|
1334
|
+
return null;
|
|
1335
|
+
return {
|
|
1336
|
+
id: "R-X1-05",
|
|
1337
|
+
data_source_class: "cross_type",
|
|
1338
|
+
level: "medium",
|
|
1339
|
+
category: "game_theory",
|
|
1340
|
+
title: "order_allocators first-match-wins may cause unfair allocation",
|
|
1341
|
+
description: "order_allocators uses a first-match-wins strategy: the first Allocator whose Guard passes executes the allocation." +
|
|
1342
|
+
"If the order is poorly designed, some participants may never get allocated." +
|
|
1343
|
+
"For example: if the first Allocator's Guard condition is too broad, subsequent more precise Allocators will never execute." +
|
|
1344
|
+
"Allocation Guards typically combine Type1 (query order fields), Type3 (submit Order address), and Type4 (Signer) data.",
|
|
1345
|
+
affected_stakeholders: ["provider"],
|
|
1346
|
+
scenario: "Allocation skip: the Provider's allocation Allocator is skipped by preceding broad conditions",
|
|
1347
|
+
mitigation: "1) Ensure the order_allocators order goes from strict to loose;" +
|
|
1348
|
+
"2) Each Allocator's Guard conditions should be mutually exclusive;" +
|
|
1349
|
+
"3) Consider using logic_or dependencies rather than sequential ordering",
|
|
1350
|
+
};
|
|
335
1351
|
},
|
|
336
1352
|
},
|
|
337
1353
|
{
|
|
338
|
-
id: "R-
|
|
339
|
-
|
|
1354
|
+
id: "R-X1-06",
|
|
1355
|
+
data_source_class: "cross_type",
|
|
1356
|
+
trigger: "Workflow Guard time-lock may lock the disadvantaged party",
|
|
340
1357
|
check: (ctx) => {
|
|
341
|
-
|
|
342
|
-
|
|
1358
|
+
const str = rootStr(ctx);
|
|
1359
|
+
if (!(str.includes("clock") && str.includes("calc_number_add")))
|
|
1360
|
+
return null;
|
|
1361
|
+
return {
|
|
1362
|
+
id: "R-X1-06",
|
|
1363
|
+
data_source_class: "cross_type",
|
|
1364
|
+
level: "medium",
|
|
1365
|
+
category: "game_theory",
|
|
1366
|
+
title: "Time-lock may lock the disadvantaged party",
|
|
1367
|
+
description: "Guard uses a time-lock (context(Clock) + calc_number_add). The time-lock itself is neutral, " +
|
|
1368
|
+
"but the locking direction needs to be checked: if the Customer is locked to pay first but cannot accept in time, the Provider gains advantage; " +
|
|
1369
|
+
"if the Provider is locked to deliver first but cannot receive payment in time, the Customer gains advantage." +
|
|
1370
|
+
"The time-lock combines Type4 (Clock) and Type1/2 (query time fields) data sources.",
|
|
1371
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1372
|
+
scenario: "Time asymmetry: one party is locked by time while the other has freedom of action",
|
|
1373
|
+
mitigation: "1) Ensure the time-lock is symmetric (both parties have time constraints);" +
|
|
1374
|
+
"2) Provide an exit mechanism after the time-lock expires (e.g. auto-refund/auto-complete);" +
|
|
1375
|
+
"3) Design corresponding timeout Forward nodes in the Machine",
|
|
1376
|
+
};
|
|
1377
|
+
},
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
id: "R-X1-07",
|
|
1381
|
+
data_source_class: "cross_type",
|
|
1382
|
+
trigger: "Arbitration usage_guard may block legitimate disputes",
|
|
1383
|
+
check: (ctx) => {
|
|
1384
|
+
if (ctx.scene?.binding_field !== "usage_guard")
|
|
1385
|
+
return null;
|
|
1386
|
+
const str = rootStr(ctx);
|
|
1387
|
+
const andCount = (str.match(/"logic_and"/g) || []).length;
|
|
1388
|
+
if (andCount < 3)
|
|
1389
|
+
return null;
|
|
1390
|
+
return {
|
|
1391
|
+
id: "R-X1-07",
|
|
1392
|
+
data_source_class: "cross_type",
|
|
1393
|
+
level: "high",
|
|
1394
|
+
category: "game_theory",
|
|
1395
|
+
title: "usage_guard has too many conditions and may block legitimate disputes",
|
|
1396
|
+
description: `Detected ${andCount} logic_and combinations. Too many AND conditions make the dispute threshold too high, ` +
|
|
1397
|
+
"the Customer may not be able to initiate a legitimate dispute, and the Provider gains an unfair advantage." +
|
|
1398
|
+
"usage_guard typically combines Type4 (Signer identity), Type3 (submit Passport credentials), " +
|
|
1399
|
+
"and Type1 (query EntityRegistrar verification) data sources.",
|
|
1400
|
+
affected_stakeholders: ["customer"],
|
|
1401
|
+
scenario: "Dispute threshold too high: Customer cannot initiate a legitimate dispute",
|
|
1402
|
+
mitigation: "1) Re-evaluate the necessity of each AND condition;" +
|
|
1403
|
+
"2) Consider using logic_or to lower the threshold;" +
|
|
1404
|
+
"3) Move non-essential conditions to voting_guard rather than usage_guard",
|
|
1405
|
+
};
|
|
1406
|
+
},
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
id: "R-X1-08",
|
|
1410
|
+
data_source_class: "cross_type",
|
|
1411
|
+
trigger: "Guard is bound to a published Machine/Service (immutable)",
|
|
1412
|
+
check: (ctx) => {
|
|
1413
|
+
if (!ctx.scene || ctx.scene.mutable_after_publish)
|
|
1414
|
+
return null;
|
|
1415
|
+
return {
|
|
1416
|
+
id: "R-X1-08",
|
|
1417
|
+
data_source_class: "cross_type",
|
|
1418
|
+
level: "info",
|
|
1419
|
+
category: "binding_risk",
|
|
1420
|
+
title: `Guard is bound to immutable ${ctx.scene.host_object} (cannot be modified after publish)`,
|
|
1421
|
+
description: `After ${ctx.scene.host_object} is published, ${ctx.scene.binding_field} cannot be modified.` +
|
|
1422
|
+
"If the Guard design is flawed, a new Machine/Service must be created and all references migrated." +
|
|
1423
|
+
"This is a design constraint rather than a risk, but the Guard design must be correct before publishing.",
|
|
1424
|
+
affected_stakeholders: ["provider"],
|
|
1425
|
+
scenario: "Irreversible binding: after publish, a flawed Guard design cannot be modified",
|
|
1426
|
+
mitigation: "1) Use gen_passport for thorough testing before publishing;" +
|
|
1427
|
+
"2) Test both expected-pass and expected-fail scenarios;" +
|
|
1428
|
+
"3) Export a Guard backup (guard2file) before publishing",
|
|
1429
|
+
};
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
id: "R-X1-09",
|
|
1434
|
+
data_source_class: "cross_type",
|
|
1435
|
+
trigger: "Guard uses rely but the dependent Guard may fail",
|
|
1436
|
+
check: (ctx) => {
|
|
1437
|
+
if (!ctx.rely || ctx.rely.guards.length === 0)
|
|
1438
|
+
return null;
|
|
1439
|
+
const logic = ctx.rely.logic_or ? "OR" : "AND";
|
|
1440
|
+
return {
|
|
1441
|
+
id: "R-X1-09",
|
|
1442
|
+
data_source_class: "cross_type",
|
|
1443
|
+
level: ctx.rely.logic_or ? "low" : "medium",
|
|
1444
|
+
category: "dependency_risk",
|
|
1445
|
+
title: `rely ${logic} depends on ${ctx.rely.guards.length} external Guards`,
|
|
1446
|
+
description: `Guard uses rely ${logic} to depend on ${ctx.rely.guards.length} external Guards.` +
|
|
1447
|
+
(ctx.rely.logic_or
|
|
1448
|
+
? "OR logic: any one passing is enough. If a dependent Guard is loosely designed, it may become a bypass entry point."
|
|
1449
|
+
: "AND logic: all dependencies must pass. If a dependent Guard fails or is deleted, the current Guard will always fail.") +
|
|
1450
|
+
" rely-dependent Guards must have rep=true (repository.data queries do not depend on runtime submission).",
|
|
1451
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1452
|
+
scenario: ctx.rely.logic_or
|
|
1453
|
+
? "Dependency bypass: a loose dependent Guard becomes an attack entry point"
|
|
1454
|
+
: "Dependency failure: a dependent Guard is deleted, causing the current Guard to always fail",
|
|
1455
|
+
mitigation: ctx.rely.logic_or
|
|
1456
|
+
? "1) Review the strictness of each dependent Guard; 2) Ensure all dependent Guards are at least as strict as the current Guard"
|
|
1457
|
+
: "1) Confirm all dependent Guards have rep=true and cannot be deleted; 2) Consider whether AND dependency is truly needed",
|
|
1458
|
+
};
|
|
1459
|
+
},
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
id: "R-X1-10",
|
|
1463
|
+
data_source_class: "cross_type",
|
|
1464
|
+
trigger: "Guard uses repository.data query (query 1167) and the Repository has quote_guard set",
|
|
1465
|
+
check: (ctx) => {
|
|
1466
|
+
const str = rootStr(ctx);
|
|
1467
|
+
if (!(str.includes("repository") && str.includes('"query"')))
|
|
1468
|
+
return null;
|
|
1469
|
+
return {
|
|
1470
|
+
id: "R-X1-10",
|
|
1471
|
+
data_source_class: "cross_type",
|
|
1472
|
+
level: "medium",
|
|
1473
|
+
category: "impack_risk",
|
|
1474
|
+
title: "repository.data query's quote_guard is invalid in the verify phase (impack_list is empty)",
|
|
1475
|
+
description: "Guard queries repository.data (query 1167). According to passport.move#L289 and passport.rs implementation, " +
|
|
1476
|
+
"the impack_list is always empty in the verify phase (only filled after result_for_permission).\n" +
|
|
1477
|
+
"Therefore in check_repository_quote_guard (inside query 1167), " +
|
|
1478
|
+
"if the Repository policy's quote_guard is Some(addr), " +
|
|
1479
|
+
"since addr is not in the empty list, it will always report IMPACK_GUARD_NOT_FOUND.\n" +
|
|
1480
|
+
"Only when quote_guard==None can query 1167 pass.\n" +
|
|
1481
|
+
"This means: querying Repository data with a quote_guard in the Guard will always fail.",
|
|
1482
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1483
|
+
scenario: "quote_guard invalidation: impack_list is empty in the verify phase, causing the 1167 query to always fail",
|
|
1484
|
+
mitigation: "1) Confirm the queried Repository's quote_guard is None;" +
|
|
1485
|
+
"2) If quote authentication is needed, modify the verify loop in the Move layer to maintain impack_list;" +
|
|
1486
|
+
"3) Document this limitation clearly: Repositories with quote_guard cannot be queried in the Guard;" +
|
|
1487
|
+
"4) Consider using other data sources instead of Repository.data query",
|
|
1488
|
+
};
|
|
1489
|
+
},
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
id: "R-X1-11",
|
|
1493
|
+
data_source_class: "cross_type",
|
|
1494
|
+
trigger: "Guard uses rely to depend on other Guards; need to confirm the rep flag of dependent Guards is correct",
|
|
1495
|
+
check: (ctx) => {
|
|
1496
|
+
if (!ctx.rely || ctx.rely.guards.length === 0)
|
|
1497
|
+
return null;
|
|
1498
|
+
const str = rootStr(ctx);
|
|
1499
|
+
const queriesRepoData = str.includes("repository") && str.includes('"query"');
|
|
1500
|
+
const hasRepoSubmission = ctx.table.some((e) => e.b_submission && e.value_type === "Address" && e.object_type === "Repository");
|
|
1501
|
+
return {
|
|
1502
|
+
id: "R-X1-11",
|
|
1503
|
+
data_source_class: "cross_type",
|
|
1504
|
+
level: queriesRepoData && hasRepoSubmission ? "medium" : "low",
|
|
1505
|
+
category: "rep_risk",
|
|
1506
|
+
title: "rep semantics needs confirmation (when repository.data query depends on submission, rep=false and cannot be depended on)",
|
|
1507
|
+
description: "Guard uses rely to depend on external Guards. In the WoWok protocol, rely-dependent Guards must have rep=true.\n" +
|
|
1508
|
+
"Precise rep semantics (guard.rs#L483-L492): repository.data query (query 1167) does not depend on runtime submission.\n" +
|
|
1509
|
+
" - rep=true: repository.data query is fully constant-driven (object address b_submission=false and query parameters are not submission)\n" +
|
|
1510
|
+
" - rep=false: repository address comes from runtime submission, or query parameters come from submission\n" +
|
|
1511
|
+
"Guards with rep=false cannot appear in other Guards' rely lists.\n" +
|
|
1512
|
+
(queriesRepoData && hasRepoSubmission
|
|
1513
|
+
? "\nWarning: the current Guard itself queries repository.data and the Repository address comes from submission, " +
|
|
1514
|
+
"so its own rep=false and cannot be depended on by other Guards."
|
|
1515
|
+
: ""),
|
|
1516
|
+
affected_stakeholders: ["provider"],
|
|
1517
|
+
scenario: "rep misjudgment: depending on a rep=false Guard causes creation failure, or its own rep=false is wrongly depended on",
|
|
1518
|
+
mitigation: "1) Confirm all rely-dependent Guards have rep=true (do not depend on submission to query repository.data);" +
|
|
1519
|
+
"2) If the current Guard queries repository.data and the address comes from submission, its own rep=false; annotate that it cannot be depended on;" +
|
|
1520
|
+
"3) rep is not 'no Repository dependency', but 'repository.data query does not depend on submission';" +
|
|
1521
|
+
"4) Use wowok_buildin_info to query the full definition of the rep flag",
|
|
1522
|
+
evidence: queriesRepoData && hasRepoSubmission
|
|
1523
|
+
? "Self rep=false (repository.data query depends on submission)"
|
|
1524
|
+
: "Self rep status to be confirmed",
|
|
1525
|
+
};
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
id: "R-X1-12",
|
|
1530
|
+
data_source_class: "cross_type",
|
|
1531
|
+
trigger: "Guard table contains b_submission=true entries with vague name descriptions or missing binding_constraint",
|
|
1532
|
+
check: (ctx) => {
|
|
1533
|
+
const submissionEntries = ctx.table.filter((e) => e.b_submission);
|
|
1534
|
+
if (submissionEntries.length === 0)
|
|
1535
|
+
return null;
|
|
1536
|
+
const vagueEntries = submissionEntries.filter((e) => {
|
|
1537
|
+
const name = e.name ?? "";
|
|
1538
|
+
return name.length < 10 || /^[a-z_]+$/i.test(name) || name === "";
|
|
1539
|
+
});
|
|
1540
|
+
if (vagueEntries.length === 0)
|
|
1541
|
+
return null;
|
|
1542
|
+
return {
|
|
1543
|
+
id: "R-X1-12",
|
|
1544
|
+
data_source_class: "cross_type",
|
|
1545
|
+
level: "medium",
|
|
1546
|
+
category: "submission_forgery",
|
|
1547
|
+
title: "Submitted data entry description is vague (retained_submission risk: caller may submit wrong data)",
|
|
1548
|
+
description: `Guard table has ${vagueEntries.length} b_submission=true entries with vague name descriptions.\n` +
|
|
1549
|
+
"The name of a submission entry is the only data contract between the Guard and the caller.\n" +
|
|
1550
|
+
"If the name description is unclear (e.g. only 'addr' / 'order_id'), the caller may:\n" +
|
|
1551
|
+
" - Submit a wrong-typed object address (e.g. submit a Service address instead of an Order address)\n" +
|
|
1552
|
+
" - Submit a wrong value (e.g. inconsistent amount units)\n" +
|
|
1553
|
+
" - Confuse submission slots of different Guards in multi-Guard scenarios\n" +
|
|
1554
|
+
"Vague submission entries are 'retained' in the table but provide no effective data constraints, " +
|
|
1555
|
+
"equivalent to the Guard accepting arbitrary data without validating its semantic correctness.",
|
|
1556
|
+
affected_stakeholders: ["customer", "provider"],
|
|
1557
|
+
scenario: "Caller submits wrong data due to vague name description; the Guard passes verification but the business logic is wrong",
|
|
1558
|
+
mitigation: "1) Each submission entry's name should be a complete natural-language description (e.g. 'The order ID that identifies the target Order for verification');\n" +
|
|
1559
|
+
"2) Avoid Technical identifiers as name (e.g. 'order_id' should become 'Order ID, used to query progress status');\n" +
|
|
1560
|
+
"3) Add failure_conditions in binding_hint to clarify failure scenarios;\n" +
|
|
1561
|
+
"4) Consider adding type validation logic in root (e.g. query the submitted object's objectType and match with expectation)",
|
|
1562
|
+
evidence: `Vague entries: ${vagueEntries.map((e) => `identifier=${e.identifier}, name="${e.name}"`).join("; ")}`,
|
|
1563
|
+
};
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1566
|
+
{
|
|
1567
|
+
id: "R-X1-13",
|
|
1568
|
+
data_source_class: "cross_type",
|
|
1569
|
+
trigger: "Guard references its host object (Host Object) itself — circular reference pattern",
|
|
1570
|
+
check: (ctx) => {
|
|
1571
|
+
if (!ctx.scene)
|
|
1572
|
+
return null;
|
|
1573
|
+
const hostType = ctx.scene.host_object;
|
|
1574
|
+
const circularEntry = ctx.table.find((e) => !e.b_submission && e.value_type === "Address" &&
|
|
1575
|
+
e.object_type === hostType);
|
|
1576
|
+
if (!circularEntry)
|
|
1577
|
+
return null;
|
|
1578
|
+
return {
|
|
1579
|
+
id: "R-X1-13",
|
|
1580
|
+
data_source_class: "cross_type",
|
|
1581
|
+
level: "medium",
|
|
1582
|
+
category: "binding_risk",
|
|
1583
|
+
title: "Circular reference pattern (Guard references the host object itself; note creation order)",
|
|
1584
|
+
description: `In the Guard's table, identifier=${circularEntry.identifier} has object_type equal to the host object type "${hostType}".\n` +
|
|
1585
|
+
"Circular reference pattern: the Guard is bound to a Host Object (e.g. Service.buy_guard) and also queries the Host Object's own fields.\n" +
|
|
1586
|
+
"Creation order constraints:\n" +
|
|
1587
|
+
" 1. The Host Object must be created first (unpublished state)\n" +
|
|
1588
|
+
" 2. When the Guard is created, the table uses the Host Object's name (not address); the SDK resolves it at binding time\n" +
|
|
1589
|
+
" 3. After the Guard is bound to the Host Object, the Host Object is published\n" +
|
|
1590
|
+
"Risks:\n" +
|
|
1591
|
+
" - If the Host Object is already published (immutable), the Guard can no longer be bound\n" +
|
|
1592
|
+
" - If the Host Object's name is modified after the Guard is created, the Guard's table reference is invalidated\n" +
|
|
1593
|
+
" - Circular reference makes the Guard not independently testable (the Host Object must exist to verify)",
|
|
1594
|
+
affected_stakeholders: ["provider"],
|
|
1595
|
+
scenario: "Guard queries fields of the Service/Machine/Reward it is bound to; wrong creation order causes binding failure",
|
|
1596
|
+
mitigation: "1) Ensure the Host Object is unpublished (bPublished=false) when creating and binding the Guard;\n" +
|
|
1597
|
+
"2) Use the Host Object's name rather than address in table (the SDK auto-resolves);\n" +
|
|
1598
|
+
"3) Perform Guard verification tests after the Host Object is published;\n" +
|
|
1599
|
+
"4) Use guard2file to export a Guard backup for restoring references during rebuild",
|
|
1600
|
+
evidence: `Circular reference: identifier=${circularEntry.identifier}, object_type="${hostType}" → ${ctx.scene.host_object}`,
|
|
1601
|
+
};
|
|
1602
|
+
},
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
id: "R-X1-14",
|
|
1606
|
+
data_source_class: "cross_type",
|
|
1607
|
+
trigger: "Scene-specific reentrancy: Guard pass triggers a state-changing side effect (fund distribution, vote count, Arb creation) that does NOT naturally invalidate the Guard condition. " +
|
|
1608
|
+
"Without an anti-reentrancy primitive, the same Signer can repeatedly pass the Guard. " +
|
|
1609
|
+
"Additionally detects pseudo-protection: primitive present but not properly wrapped in logic_not (for Bool queries) or logic_equal(0) (for count queries).",
|
|
1610
|
+
check: (ctx) => {
|
|
1611
|
+
const spec = REENTRANCY_SPECS.find((s) => s.sceneId === ctx.scene?.id);
|
|
1612
|
+
if (!spec)
|
|
1613
|
+
return null;
|
|
1614
|
+
const protection = detectReentrancyProtection(ctx.root, ctx.table, spec);
|
|
1615
|
+
if (protection.state === "protected")
|
|
1616
|
+
return null;
|
|
1617
|
+
if (protection.state === "pseudo") {
|
|
343
1618
|
return {
|
|
344
|
-
id: "R-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1619
|
+
id: "R-X1-14",
|
|
1620
|
+
data_source_class: "cross_type",
|
|
1621
|
+
level: "medium",
|
|
1622
|
+
category: "logic_gap",
|
|
1623
|
+
title: `Reentrancy pseudo-protection: anti-reentrancy primitive present but not properly wrapped (${spec.sceneId})`,
|
|
1624
|
+
description: `Scene: ${spec.sceneId}\n` +
|
|
1625
|
+
`The Guard root contains an anti-reentrancy primitive (${protection.foundPrimitiveName}), but it is NOT properly wrapped.\n` +
|
|
1626
|
+
"Proper wrapping rules:\n" +
|
|
1627
|
+
" - Bool-returning primitives (exists-style): must be wrapped in logic_not\n" +
|
|
1628
|
+
" - U64-returning primitives (count-style): must be compared to 0 via logic_equal\n" +
|
|
1629
|
+
" - Address-returning primitives: must be wrapped in logic_not (None check)\n\n" +
|
|
1630
|
+
"WHY THIS IS DANGEROUS:\n" +
|
|
1631
|
+
" The developer likely intended to prevent reentrancy but the wrapping is incorrect, " +
|
|
1632
|
+
"so the primitive does NOT actually block repeated Guard passes. This is arguably MORE " +
|
|
1633
|
+
"dangerous than absence — code reviewers see the primitive and assume protection is in place.\n\n" +
|
|
1634
|
+
`Scene rationale: ${spec.rationale}`,
|
|
1635
|
+
affected_stakeholders: deriveStakeholders(spec.sceneId),
|
|
1636
|
+
scenario: `Developer added anti-reentrancy primitive but used wrong wrapping; reentrancy exploit still possible in ${spec.sceneId}`,
|
|
1637
|
+
mitigation: "Fix the wrapping:\n" +
|
|
1638
|
+
" - For exists-style (Bool) primitives: wrap in logic_not — { type: 'logic_not', node: { type: 'query', query: <primitive>, ... } }\n" +
|
|
1639
|
+
" - For count-style (U64) primitives: compare to 0 — { type: 'logic_equal', nodes: [ { type: 'query', query: <primitive>, ... }, identifier(N) ] } where table[N].value = 0\n" +
|
|
1640
|
+
" - For address-style primitives: wrap in logic_not (None check)\n" +
|
|
1641
|
+
`Then wrap the protection in logic_and with the existing root conditions.\n\n` +
|
|
1642
|
+
`Full mitigation for ${spec.sceneId}:\n${spec.mitigation}`,
|
|
1643
|
+
evidence: protection.evidence,
|
|
359
1644
|
};
|
|
360
1645
|
}
|
|
361
|
-
|
|
1646
|
+
const level = spec.level;
|
|
1647
|
+
const stakeholders = deriveStakeholders(spec.sceneId);
|
|
1648
|
+
return {
|
|
1649
|
+
id: "R-X1-14",
|
|
1650
|
+
data_source_class: "cross_type",
|
|
1651
|
+
level,
|
|
1652
|
+
category: "logic_gap",
|
|
1653
|
+
title: level === "info"
|
|
1654
|
+
? `Reentrancy reminder: ${spec.sceneId} has no anti-reentrancy primitive (LOW risk)`
|
|
1655
|
+
: `Reentrancy risk: ${spec.sceneId} has NO anti-reentrancy protection (${level.toUpperCase()})`,
|
|
1656
|
+
description: `Scene: ${spec.sceneId}\n` +
|
|
1657
|
+
`Reentrancy risk level: ${level.toUpperCase()}\n\n` +
|
|
1658
|
+
`RATIONALE:\n${spec.rationale}\n\n` +
|
|
1659
|
+
(protection.evidence
|
|
1660
|
+
? `DETECTION:\n${protection.evidence}\n\n`
|
|
1661
|
+
: "") +
|
|
1662
|
+
(level === "critical"
|
|
1663
|
+
? "IMPACT: Without protection, the same Signer can repeatedly trigger the side effect (fund distribution / vote counting), causing direct fund loss or result manipulation. THIS IS A CRITICAL VULNERABILITY — the Guard MUST be rebuilt with an anti-reentrancy primitive before deployment.\n\n"
|
|
1664
|
+
: level === "high"
|
|
1665
|
+
? "IMPACT: Without protection, the system can be abused (multiple dispute cases, arbitration harassment). Strongly recommended to add protection.\n\n"
|
|
1666
|
+
: "IMPACT: Low by default; only relevant if business intent requires single-action enforcement. Review the rationale above.\n\n"),
|
|
1667
|
+
affected_stakeholders: stakeholders,
|
|
1668
|
+
scenario: level === "info"
|
|
1669
|
+
? `${spec.sceneId} lacks anti-reentrancy primitive; may or may not be needed depending on business intent`
|
|
1670
|
+
: `${spec.sceneId} lacks anti-reentrancy primitive; same Signer can repeatedly trigger the side effect`,
|
|
1671
|
+
mitigation: spec.mitigation,
|
|
1672
|
+
evidence: protection.evidence,
|
|
1673
|
+
};
|
|
362
1674
|
},
|
|
363
1675
|
},
|
|
364
1676
|
];
|
|
1677
|
+
function deriveStakeholders(sceneId) {
|
|
1678
|
+
switch (sceneId) {
|
|
1679
|
+
case "reward_guard":
|
|
1680
|
+
return ["claimant", "provider"];
|
|
1681
|
+
case "arbitration_voting_guard":
|
|
1682
|
+
return ["customer", "provider", "arbitrator"];
|
|
1683
|
+
case "arbitration_usage_guard":
|
|
1684
|
+
return ["customer", "provider"];
|
|
1685
|
+
case "service_order_allocators_guard":
|
|
1686
|
+
return ["customer", "provider"];
|
|
1687
|
+
default:
|
|
1688
|
+
return ["provider"];
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
365
1691
|
export function assessGuardRisks(params) {
|
|
366
1692
|
const ctx = {
|
|
367
1693
|
table: params.table,
|
|
@@ -369,6 +1695,7 @@ export function assessGuardRisks(params) {
|
|
|
369
1695
|
rely: params.rely,
|
|
370
1696
|
scene: params.scene,
|
|
371
1697
|
operation_type: params.operation_type,
|
|
1698
|
+
sharing_recipients: params.sharing_recipients,
|
|
372
1699
|
};
|
|
373
1700
|
const risks = [];
|
|
374
1701
|
for (const rule of RISK_RULES) {
|
|
@@ -381,6 +1708,11 @@ export function assessGuardRisks(params) {
|
|
|
381
1708
|
}
|
|
382
1709
|
}
|
|
383
1710
|
const overall_risk = getOverallRiskLevel(risks);
|
|
1711
|
+
const risks_by_data_source = {};
|
|
1712
|
+
for (const risk of risks) {
|
|
1713
|
+
risks_by_data_source[risk.data_source_class] =
|
|
1714
|
+
(risks_by_data_source[risk.data_source_class] ?? 0) + 1;
|
|
1715
|
+
}
|
|
384
1716
|
const stakeholder_advice = generateStakeholderAdvice(risks, params.scene);
|
|
385
1717
|
const needs_human_review = risks.some((r) => r.level === "critical") ||
|
|
386
1718
|
risks.filter((r) => r.level === "high").length >= 2;
|
|
@@ -390,6 +1722,7 @@ export function assessGuardRisks(params) {
|
|
|
390
1722
|
scene: params.scene,
|
|
391
1723
|
risks,
|
|
392
1724
|
overall_risk,
|
|
1725
|
+
risks_by_data_source,
|
|
393
1726
|
stakeholder_advice,
|
|
394
1727
|
needs_human_review,
|
|
395
1728
|
};
|
|
@@ -419,22 +1752,22 @@ function generateStakeholderAdvice(risks, scene) {
|
|
|
419
1752
|
case "buy_guard":
|
|
420
1753
|
if (!advice.customer)
|
|
421
1754
|
advice.customer = [];
|
|
422
|
-
advice.customer.push("
|
|
1755
|
+
advice.customer.push("Before purchase, verify the buy_guard logic to ensure your identity/credentials can pass validation");
|
|
423
1756
|
break;
|
|
424
1757
|
case "order_allocators":
|
|
425
1758
|
if (!advice.provider)
|
|
426
1759
|
advice.provider = [];
|
|
427
|
-
advice.provider.push("
|
|
1760
|
+
advice.provider.push("Verify the order_allocators ordering to ensure your Allocator will not be skipped by preceding conditions");
|
|
428
1761
|
break;
|
|
429
1762
|
case "voting_guard":
|
|
430
1763
|
if (!advice.arbitrator)
|
|
431
1764
|
advice.arbitrator = [];
|
|
432
|
-
advice.arbitrator.push("
|
|
1765
|
+
advice.arbitrator.push("Confirm the voting weight source is trustworthy; prefer querying EntityRegistrar rather than user-submitted data");
|
|
433
1766
|
break;
|
|
434
1767
|
case "Forward.guard":
|
|
435
1768
|
if (!advice.customer)
|
|
436
1769
|
advice.customer = [];
|
|
437
|
-
advice.customer.push("
|
|
1770
|
+
advice.customer.push("Verify the Forward.guard conditions to ensure you can satisfy the advancement criteria and avoid locked funds");
|
|
438
1771
|
break;
|
|
439
1772
|
}
|
|
440
1773
|
}
|
|
@@ -442,12 +1775,12 @@ function generateStakeholderAdvice(risks, scene) {
|
|
|
442
1775
|
}
|
|
443
1776
|
function generateGuardSummary(params) {
|
|
444
1777
|
const parts = [];
|
|
445
|
-
parts.push(`table
|
|
1778
|
+
parts.push(`table entries: ${params.table.length}`);
|
|
446
1779
|
const submissions = params.table.filter((e) => e.b_submission).length;
|
|
447
|
-
parts.push(
|
|
448
|
-
parts.push(`root
|
|
1780
|
+
parts.push(`submitted data: ${submissions} entries`);
|
|
1781
|
+
parts.push(`root type: ${params.root?.type ?? "unknown"}`);
|
|
449
1782
|
if (params.rely) {
|
|
450
|
-
parts.push(`rely: ${params.rely.guards.length}
|
|
1783
|
+
parts.push(`rely: ${params.rely.guards.length} dependencies (${params.rely.logic_or ? "OR" : "AND"})`);
|
|
451
1784
|
}
|
|
452
1785
|
return parts.join(", ");
|
|
453
1786
|
}
|
|
@@ -472,3 +1805,12 @@ export function getRiskSummary(assessment) {
|
|
|
472
1805
|
.map((r) => r.title),
|
|
473
1806
|
};
|
|
474
1807
|
}
|
|
1808
|
+
export function getRiskRulesByDataSourceClass(dataSourceClass) {
|
|
1809
|
+
return RISK_RULES.filter((r) => r.data_source_class === dataSourceClass);
|
|
1810
|
+
}
|
|
1811
|
+
export function getAllRiskRules() {
|
|
1812
|
+
return RISK_RULES;
|
|
1813
|
+
}
|
|
1814
|
+
export function getRiskRulesCount() {
|
|
1815
|
+
return RISK_RULES.length;
|
|
1816
|
+
}
|