Skip to content

Commit fb6ea60

Browse files
committed
The canvas ran: one drain, two refusals, and two things sync.mjs got wrong
First workflow on the KeeperHub canvas (7v0qwhcp5gcex58gjugyg), the day the Pro trial unblocked code/run-code and HTTP Request. Execution hq4av5dbacb1i5z9bxfs0 drained VOL_1 on Sepolia: 0x188addfb861b79c7d300966680fe3edc02140e58515425bfef358b4329b95a5c, verified, 102503 gas, from a wallet that owns nothing on that Safe. Before it, the same graph refused VOL_2 (not-next-nonce, two proposals at nonce 37) and VOL_3 (refund-requested, gasPrice=1) — same assemble.mjs, same decisions drain.mjs makes. Two generator bugs, both only findable by running: - gate-addr used matchesRegex. The docs list it; the runtime condition validator bans `new RegExp`, has no `test` in ALLOWED_METHODS, and rejects any `[` after a word char even inside a quoted string. Replaced with startsWith + length; the strict ADDRESS_RE check lives in assemble(). - The Code node substitutes templates as JSON values, so wrapping the nonce and threshold templates in quotes produced ""37"" and a syntax error. Unquoted now. Both graphs regenerated from the fixed generator.
1 parent 80fc5b1 commit fb6ea60

3 files changed

Lines changed: 21 additions & 9 deletions

File tree

‎scripts/sync.mjs‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,16 @@ return assemble({
125125
safeAddress: __safeAddress,
126126
roster: ROSTER,
127127
queue: { transactions: __transactions, count: __transactions.length },
128+
// Unquoted on purpose: the Code node substitutes every template as a JSON
129+
// value, so a string result arrives already quoted. Wrapping it again produced
130+
// ""37"" and a syntax error on the first canvas run (2026-09-11, execution
131+
// 6x4fj09qq46up7rcej3hk). toCount() accepts string or number either way.
128132
onchainNonce: ${chainId === '8453'
129-
? '"{{@nonce-1:Safe Nonce.nonce}}"'
130-
: '"{{@nonce-1:Read Nonce.result}}"'},
133+
? '{{@nonce-1:Safe Nonce.nonce}}'
134+
: '{{@nonce-1:Read Nonce.result}}'},
131135
onchainThreshold: ${chainId === '8453'
132-
? '"{{@threshold-1:Safe Threshold.threshold}}"'
133-
: '"{{@threshold-1:Read Threshold.result}}"'},
136+
? '{{@threshold-1:Safe Threshold.threshold}}'
137+
: '{{@threshold-1:Read Threshold.result}}'},
134138
onchainOwners: ${chainId === '8453'
135139
? '{{@owners-1:Safe Owners.owners}}'
136140
: '{{@owners-1:Read Owners.result}}'},
@@ -188,9 +192,17 @@ const nodes = [
188192
// Marketplace listing) can send anything; nothing downstream should have to
189193
// assume otherwise. Rejecting non-addresses here means the injection vector in
190194
// the Code node is closed at the boundary as well as at the splice.
195+
//
196+
// Shape check only, not a regex. The docs list `matchesRegex`, but the runtime
197+
// validator (lib/workflow/nodes/condition/validator.ts) bans `new RegExp`, has
198+
// no `test` in ALLOWED_METHODS, and rejects any `[` preceded by a word char —
199+
// so `0x[0-9a-fA-F]{40}` fails even quoted (first canvas run, 2026-09-11,
200+
// execution 4y5c1zst9hoegqkybyhvj). `startsWith` and `.length` are allowed.
201+
// The strict ADDRESS_RE check lives in assemble(), which refuses
202+
// `malformed-payload` for anything that slips past this.
191203
node('gate-addr', 'Valid Address', 'action', {
192204
actionType: 'Condition',
193-
condition: '{{@trigger-1:Webhook.safeAddress}} matchesRegex ^0x[0-9a-fA-F]{40}$',
205+
condition: '{{@trigger-1:Webhook.safeAddress}}.startsWith("0x") && {{@trigger-1:Webhook.safeAddress}}.length === 42',
194206
}, 250, 0),
195207

196208
node('queue-1', 'Safe Queue', 'action', {

0 commit comments

Comments
 (0)