{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"37a926e3-85be-40b6-a656-1fa263e9b485","kind":"audit","nodes":[{"acceptedSubmissionHash":"5a937a4303e61c5b8b7861ad766f88f703ca13f93336f9ea65fa22fc7e37f3d5","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"47952d3d0b1ac67d077f2b4b426b791b52b5fb17117f9c179e48c4598f3ef802","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"4739150cb5b2661875e59c99bbd59c9796aa92a156d4f0c6518e326ed3dbbefb","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","subagents":4,"tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"9e0cf0b8c543178bfca6ad521140006f8977c336f2a5f55db8324355064412d0","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"37c91cfa69269e7472c5750fc093bc9ba900566b5f859b294367a5b098052269","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"}],"objective":"Review Submission24/ only (read Submission24/AUDIT_REQUEST.md first). World source fd46d69, NOT deployed; no real Celestao payment and no real grant has ever been made. It fixes audit 521bc017 and adds an administrator GOLD grant, which is new authority: the first GOLD credit with no paid order or failed service behind it. The wallet-lock test fails on purpose until this audit is handled.\n\n1. Grant limits and authority: any path past 5,000 per grant, 20,000 or 100 grants per 24 hours, or for another wallet. Include D1 batch behaviour, races, clocks, replays, session end and cross-site requests.\n2. Ledger consistency: anything that assumes every credit has an order, or must count world_gold_grants. Is a 7-day session enough for this authority, or should each grant need a wallet signature? One grant wallet is the treasury.\n3. Pay and receive after the rework: interleavings of prepare, admit, refusal, expiry, repeat, delivery, restart and the 15-minute limit that lose a paid order or charge twice.\n4. Retention: can unadmitted be wrong so that it deletes a run a buyer holds, or keeps data no buyer holds?\n5. Budgets: can one buyer still close checkout for others, or can the new keys or the limit table's new behaviour bypass a limit?\n6. Migrations 0023 and 0024: partial application, the trigger in both orders, and the view rebuild on D1.\n\nKnown items are in AUDIT_REQUEST.md section 4. Report severity, reproduction and fix. Not in scope: chart engines, report wording, 3D/rendering, media, AIMarket, Genesis.","parentJobId":null,"planHash":"b152961994623e5eec1e2b123e9861fc9876fd192a972de698436cb72e61c3fd","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"37a926e3-85be-40b6-a656-1fa263e9b485","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51877","feedbackHash":"ce30a8bdbc1034eeff9e32daa85e00b9d2fcc7ae18436c96ea61cd432e13c59f","nodeKey":"audit_economics","submissionHash":"5a937a4303e61c5b8b7861ad766f88f703ca13f93336f9ea65fa22fc7e37f3d5","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51375","feedbackHash":"38da61da769a96c4e15319626a564994c087d8eb4b1d0f05e371322b2b382512","nodeKey":"audit_flow","submissionHash":"47952d3d0b1ac67d077f2b4b426b791b52b5fb17117f9c179e48c4598f3ef802","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52666","feedbackHash":"9b69dcdd04bee12f94a9a486f6c814b3a4510891f218b1e028f9bc6cb669e46f","nodeKey":"audit_judge","submissionHash":"4739150cb5b2661875e59c99bbd59c9796aa92a156d4f0c6518e326ed3dbbefb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51538","feedbackHash":"b12a753648269728f75a51450c8ef7ad739f8929a5c349f82dff62720bf453ef","nodeKey":"audit_math","submissionHash":"9e0cf0b8c543178bfca6ad521140006f8977c336f2a5f55db8324355064412d0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52127","feedbackHash":"73fc51a19bb53520695f391a79b49c21108a6d6c0b1ee764944e71d12bf53486","nodeKey":"audit_permissions","submissionHash":"37c91cfa69269e7472c5750fc093bc9ba900566b5f859b294367a5b098052269","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"3e396ab07299998db48a562514156e6b410e6d3525de30e9da74c7d9cca418ed","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"797dadf3f0881394","findings":[],"hash":"223401f6acb58e51520ccaced9c4bac4245b3adad4a2ac343df2c3eb31ca83cd","nodeId":"5883be02-3bb4-4059-bde6-c7d233699b48","outcome":"failed","summary":"API Error: Your account can't receive replies from Claude right now. You can find the next step in the Anthropic Verification Portal: https://portal.anthropic.com/…","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":2505}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"fd5402086dce252e","findings":[{"citation":"resolved","description":"The grant batch (goldAdminCommand, action 'grant') is the first write that creates GOLD with no paid order behind it, yet its only authority check is authCheck: a row in `sessions` with the cookie's token hash, address, wallet_type='EOA' and verification_method='ECDSA'. Nothing in the request proves the grant wallet's key signed this grant. By contrast every GOLD debit of a buyer goes through an explicit per-order wallet signature (goldChallenge/payGold in WorldGoldPanel.tsx line 127). So the asymmetry is: spending 1 GOLD needs the key, creating 5,000 GOLD needs only the cookie. Sessions last 7 days (AUDIT_REQUEST section 1), one of the two grant wallets is ECONOMY_TREASURY, the recipient may be any address including the granter, there is no second approver or notification, and granted GOLD is spendable on services and (when open) mint. A stolen or hijacked session cookie (XSS on imdember.com, a leaked HAR/devtools export, malware on the operator's machine, a shared browser profile) therefore fabricates up to 20,000 GOLD per 24 h, about 140,000 GOLD over the cookie's life, and can spend it before anyone notices; the book is immutable and the request says a grant cannot be taken back. The limits in migrations/0024 bound the damage; they do not establish who authorised it. This is the question section 5 item 2 asks; the answer is that the session is not enough for this authority. Fix (keeps the owner's design: same two wallets, same limits, same off switch): require a wallet signature per grant. The panel asks the grant wallet to sign EIP-712 typed data {recipient, gold, requestId, reasonHash, chainId, expiresMs} through the same challenge flow buyers already use for GOLD payments (economy_gold_challenges); the server recovers the signer with ecrecover, requires it to equal session.address and a GOLD_GRANT_WALLETS member, binds the signature into the batch (store it on the world_gold_grants row and include it in the audit payload), and refuses expired or reused challenges. A weaker step-up (a fresh sign-in within a few minutes for grant writes, and a shorter session for the two grant wallets) reduces but does not remove the exposure.","line":22,"path":"Submission24/source/server/world-gold.ts","reproduction":"State: WORLD_GOLD_GRANTS='enabled', migrations through 0024 applied, a live EOA/ECDSA session row for GRANT-WALLET-2 (the treasury) whose cookie value an attacker has copied (no key, no wallet extension). Input: POST https://imdember.com/api/gold/admin with headers Origin: https://imdember.com, Content-Type: application/json, Cookie: <SESSION_COOKIE>=<copied token>, body {\"expectedAddress\":\"<grant wallet 2>\",\"requestId\":\"<fresh 16+ char id>\",\"reason\":\"attacker reason\",\"command\":{\"action\":\"grant\",\"wallet\":\"0x<attacker address>\",\"gold\":\"5000\"}}. Expected for an authority of this size: refusal unless the grant wallet's key signed (recipient, amount, requestId, chain, expiry). Actual: 200 {ok:true,result:{id,...}}; world_gold_grants gains a row and economy_gold_accounts[attacker]+=5000 (tests/world-gold-grants.test.mjs fixture() shows exactly this flow succeeds with nothing but a sessions row). Repeating with new requestIds and amounts 5000,5000,5000,4999,... reaches 20,000 within the day; the attacker then pays for services or mints with it. No step asks the wallet to sign.","severity":"medium","snippet":"const authCheck=(db:D1Database,wallet:string,now:number,auth?:{tokenHash:string})=>check(db,auth?`EXISTS(SELECT 1 FROM sessions WHERE token_hash=? AND address=? AND revoked_at IS NULL AND expires_at>? AND wallet_type='EOA' AND verification_method='ECDSA')`:'1',...auth?[auth.tokenHash,wallet,now]:[]);","title":"Administrator GOLD grant is authorised by the 7-day cookie session alone; no per-grant wallet signature, unlike every GOLD spend"},{"citation":"resolved","description":"The guard runs isAddress(...,{strict:true}) only when the typed address contains both a lowercase and an uppercase hex letter. An address typed entirely in uppercase hex (0xAB12...) has no [a-f] match, so the strict check is skipped and economyAddress() lowercases it and the grant is written. viem's strict isAddress accepts only all-lowercase or the exact EIP-55 form; an all-uppercase spelling of an address whose checksum form is mixed case is not a valid checksum and viem rejects it. The comment on this line says the purpose is that 'a mistyped character is refused rather than paid to nobody', and AUDIT_REQUEST section 1 says 'a mixed-case address must pass its checksum'. An operator who pastes from a source that uppercases hex (some explorers, spreadsheets, OCR, shouting-case chat copies) loses the protection entirely: a one-character typo in such a paste is granted, irreversibly, to an address nobody holds. The companion client rule grantFormReady (src/world/goldGrantRules.ts line 5) has the same gap: it only checks the regex.","line":262,"path":"Submission24/source/server/world-gold.ts","reproduction":"Input: a grant with wallet '0xAB12AB12AB12AB12AB12AB12AB12AB12AB12AB12' (the uppercase spelling of 0x+'ab12'*10; its EIP-55 form, computed with Keccak-256, is 0xAb12AB12Ab12AB12ab12Ab12ab12aB12AB12aB12, so the uppercase spelling is not the checksum form and viem isAddress(uppercase,{strict:true}) returns false). Expected: 409 INVALID_ADDRESS_CHECKSUM and no row, the same answer the existing test gets for a flipped single character. Actual: /[a-f]/.test('AB12...') is false, the guard is skipped, 200 ok, and a world_gold_grants row for 0xab12ab12... is written. Minimal check in the test fixture: f.grant('1','0x'+'AB12'.repeat(10)) returns status 200 while f.grant('1','0xAB12AB12Ab12AB12ab12Ab12ab12aB12AB12aB12') (the checksum form with its first 'b' uppercased, one flipped character) returns INVALID_ADDRESS_CHECKSUM. Fix: run the strict check whenever the typed address is not all-lowercase (if(typed!==typed.toLowerCase()&&!isAddress(cmd.wallet,{strict:true}))fail('INVALID_ADDRESS_CHECKSUM')), mirror it in grantFormReady, and say in the panel that lowercase is accepted unchecked.","severity":"low","snippet":"  if(/[a-f]/.test(typed)&&/[A-F]/.test(typed)&&!isAddress(cmd.wallet as string,{strict:true}))fail('INVALID_ADDRESS_CHECKSUM');","title":"Checksum guard on the grant recipient is skipped for an all-uppercase address, so a mistyped uppercase paste is paid rather than refused"}],"hash":"37c91cfa69269e7472c5750fc093bc9ba900566b5f859b294367a5b098052269","nodeId":"5883be02-3bb4-4059-bde6-c7d233699b48","outcome":"completed","summary":"The review is complete and `.imd-findings.json` at the repository root holds two substantiated findings. No repository file was changed.\n\n**What I reviewed.** Submission24 at World source fd46d69: the administrator grant (world-gold.ts, world-gold-api.ts, migration 0024, goldGrants.ts, goldGrantRules.ts, panel and client), ledger consistency (economy-recovery.ts, the history view, schema checks), the reworked pay-and-receive path on both sides (celestao-checkout.ts, celestao-delivery.ts, celestao-auth.ts, vault.py, paid.py, bridge.py, sessions.py), budgets (limits.py, the new limiter keys in the Worker), and migrations 0023 and 0024 against the original 0020 table. The six areas in the request were each traced; the known items in section 4 were not re-reported.\n\n**Findings written**\n\n- **Medium. Grant authority rests on the cookie session alone.** The grant batch checks only a live EOA session row (world-gold.ts line 22). Every GOLD spend needs a per-order wallet signature, but creating 5,000 GOLD needs only the cookie. A copied session cookie for the treasury wallet can fabricate 20,000 GOLD a day for the cookie's 7-day life and spend it. Fix proposed: an EIP-712 signature per grant over recipient, amount, request id, chain and expiry, verified against the session address and bound into the batch, reusing the existing challenge flow.\n- **Low. The recipient checksum guard is skipped for all-uppercase addresses** (world-gold.ts line 262). I computed the EIP-55 form of the test address with a self-checked Keccak-256 and confirmed its uppercase spelling is not the checksum form, so viem strict rejects it while the server pays it. A one-character typo in an uppercase paste is granted irreversibly.\n\n**What held up.** The 5,000 per grant, 20,000 and 100 per day limits agree between the application guards and the 0024 trigger; the guards run inside the serialized D1 batch so parallel grants, same-id races, session revocation during the write, switch-off replays and cross-site POSTs are all closed. Recovery counts grants in the balance check. The unadmitted mark deletes only runs no buyer ever received, and the repeat, refusal, 15-minute replacement and restart interleavings I traced lose no paid order beyond the known items. The new budget keys cannot be used by a third party to close checkout for others. Migrations 0023 and 0024 apply in either release order and fail closed when partial.\n\n**Limits of this review.** The project's Node and Python suites could not be run here because their fixtures and several imported modules are outside the package, so all reproductions are traced inputs rather than executed tests. Files not in the package (economy-session.ts, economy-config.ts, auth.ts, the payment indexer, serve_production.py) were not read, and the two grant wallets are redacted.","treeHash":null,"usage":{"cachedInputTokens":2532671,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":52398,"runtime":"claude","turns":47,"wallClockMs":771752}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30bcdea6dcb9ee40","findings":[{"citation":"resolved","description":"Answer to AUDIT_REQUEST section 5 item 2 (merged from four specialist reports). The grant branch of goldAdminCommand (world-gold.ts:256-266) and the HTTP handler (world-gold-api.ts:57, 71, 79) prove only possession of the World session cookie: authCheck at line 22 requires a sessions row with the cookie's token_hash, the actor address, wallet_type='EOA', verification_method='ECDSA', unrevoked and unexpired. Nothing in the request is signed by the granting wallet's key. Every other GOLD-moving action a user takes (service payment, mint) needs an explicit wallet signature over the amount (world-gold-api.ts:29-31 and WorldGoldPanel.tsx payGold); the one action that creates GOLD with no order behind it does not. Sessions live 7 days with no step-up. A copied cookie (device or browser-profile compromise, a same-origin XSS on imdember.com, a leaked HAR) therefore issues grants to any address including the holder's own: 4 x 5,000 per rolling 24 h, about 140,000 GOLD over the cookie's life, each row immutable with no correcting entry (section 4), spendable on services and mint. GRANT-WALLET-2 is ECONOMY_TREASURY (tests/world-gold-grants.test.mjs:38), so the payment-receiving key is used interactively in a browser for this. The limits, triggers, origin/Sec-Fetch-Site checks, replay and race handling were checked and hold; this finding is about the credential behind the authority, which section 1 acknowledges and section 5 asks to be judged. It is a trust-assumption gap, not a permission bypass: an attacker still needs the cookie. Fix that keeps the owner's design (same two wallets, same limits, same switch): require in the grant command an EIP-712 signature by the session wallet over {recipient (lowercase), gold, requestId, reasonHash, chainId, domain imdember.com, deadline <= 10 min}; verify it with viem verifyTypedData/recoverTypedDataAddress against session.address before building the batch, store the signature in the world_gold_admin_actions payload (so a replay still compares equal) and refuse an expired deadline. Optionally bind a per-session total in the application guard (sum per sessions.token_hash <= 20,000) so one cookie cannot reach the weekly figure, and sign in with a dedicated grant EOA rather than the treasury address.","line":22,"path":"Submission24/source/server/world-gold.ts","reproduction":"State: WORLD_GOLD_GRANTS='enabled', migrations through 0024 applied, economy configured and not halted, a live EOA/ECDSA sessions row for a grant wallet G whose cookie value an attacker has copied (no key, no wallet extension). Input: POST https://imdember.com/api/gold/admin, headers Origin: https://imdember.com, Content-Type: application/json, Cookie: <SESSION_COOKIE>=<copied token>, body {\"expectedAddress\":G,\"requestId\":\"<fresh 16-80 char id>\",\"reason\":\"ops\",\"command\":{\"action\":\"grant\",\"wallet\":\"0x<attacker address>\",\"gold\":\"5000\"}}. Expected for issuance authority of this size: refusal unless the grant wallet's key signed (recipient, amount, request id, chain, expiry). Actual: 200 {ok:true,result:{id,...}}, a world_gold_grants row and economy_gold_accounts[attacker] += 5000 by the 0024 apply trigger. Repeating with new request ids and amounts 5000,5000,5000,4999 reaches 20,000 within the day, and again each day until the session expires. The shipped fixture tests/world-gold-grants.test.mjs fixture() lines 22-27 exercises exactly this cookie-only path (one INSERT INTO sessions, then the POST with the cookie) and every grant test passes on it with no signature anywhere in the request.","severity":"medium","snippet":"const authCheck=(db:D1Database,wallet:string,now:number,auth?:{tokenHash:string})=>check(db,auth?`EXISTS(SELECT 1 FROM sessions WHERE token_hash=? AND address=? AND revoked_at IS NULL AND expires_at>? AND wallet_type='EOA' AND verification_method='ECDSA')`:'1',...auth?[auth.tokenHash,wallet,now]:[]);","title":"Administrator GOLD grant is authorised by the 7-day session cookie alone: no per-grant wallet signature, unlike every GOLD spend, and one grant wallet is the treasury"},{"citation":"resolved","description":"Merged from three specialist reports. The state route computes grantLeft from the 24-hour SUM(delta) only (world-gold-api.ts:68-69). The count limit GOLD_GRANT_DAILY_COUNT=100 is enforced by the application guard (world-gold.ts:266) and by the 0024 trigger (line 22) but is never read into the state, and WorldGoldPanel.tsx:58 renders grantLeft as the remaining allowance and keeps the form open. After 100 small grants an operator sees a large positive number, sends a grant, and gets GRANT_DAILY_LIMIT with no way to tell from the panel which limit was hit or when it reopens; the 10-minute GRANT_DUPLICATE_RECENT rule then also blocks the natural retry. No limit is bypassed. Fix: in the same canGrant branch also select COUNT(*) FROM world_gold_grants WHERE created_ms>? over the same window, return grantLeft=String(count>=GOLD_GRANT_DAILY_COUNT?0:Math.max(0,GOLD_GRANT_DAILY-sum)) plus grantsLeft=String(Math.max(0,GOLD_GRANT_DAILY_COUNT-count)), and let grantFormReady disable the button when either is zero.","line":68,"path":"Submission24/source/server/world-gold-api.ts","reproduction":"Fixture from tests/world-gold-grants.test.mjs: insert 100 rows of delta 1 with for(let i=0;i<100;i++)direct(f.db,{id:'n'+i}) (as the test 'no more than a hundred grants in 24 hours' does at line 233). GET /api/gold/state?expected_address=<grant wallet> with the grant cookie. Expected: grantLeft '0' (or grantsLeft '0') because no grant can be made for 24 hours. Actual: canGrant true, grantLeft '19900'. Then POST /api/gold/admin {action:'grant',wallet:RECIPIENT,gold:'1'}: 409 GRANT_DAILY_LIMIT. The existing test asserts the refusal but never reads grantLeft.","severity":"low","snippet":"   const [granted,recentGrants]=canGrant?await Promise.all([deps.db.prepare('SELECT COALESCE(SUM(delta),0) n FROM world_gold_grants WHERE created_ms>?').bind(now-GOLD_GRANT_WINDOW_MS).first<{n:number}>(),deps.db.prepare('SELECT id,wallet,CAST(delta AS TEXT) gold,actor,created_ms FROM world_gold_grants ORDER BY created_ms DESC,id DESC LIMIT 10').all()]):[null,null];","title":"grantLeft reports only the 20,000 GOLD budget and ignores the 100-grant count limit, so the panel offers GOLD that every grant request will refuse"},{"citation":"resolved","description":"The guard runs viem isAddress(...,{strict:true}) only when the typed address contains both a lowercase and an uppercase hex letter. An address typed entirely in uppercase hex has no [a-f] match, so the strict check is skipped, economyAddress() lowercases it and the grant is written. viem 2.56.9 (the project's pinned version) strict isAddress accepts only the all-lowercase or the exact EIP-55 spelling: for the uppercase spelling of an address whose checksum form is mixed case it returns false, so the code deliberately skips a check the library would fail. The comment on this line says the purpose is that 'a mistyped character is refused rather than paid to nobody', and AUDIT_REQUEST section 1 says 'a mixed-case address must pass its checksum'. Hex letters are only ever uppercased by EIP-55, so an all-uppercase string is almost always a checksummed address that an explorer, spreadsheet, OCR or chat client uppercased; a one-character typo in such a paste is granted, irreversibly, to an address nobody holds. The client rule grantFormReady (src/world/goldGrantRules.ts:5) has the same gap. Fix: run the strict check whenever the typed address is not all-lowercase, i.e. if(typed!==typed.toLowerCase()&&!isAddress(cmd.wallet as string,{strict:true}))fail('INVALID_ADDRESS_CHECKSUM'); mirror it in grantFormReady, and say in the panel that lowercase is accepted unchecked.","line":262,"path":"Submission24/source/server/world-gold.ts","reproduction":"Verified with viem 2.56.9 outside the repository: getAddress('0x'+'ab12'.repeat(10)) = 0xAb12AB12Ab12AB12ab12Ab12ab12aB12AB12aB12; isAddress('0x'+'AB12'.repeat(10),{strict:true}) returns false; /[a-f]/.test('AB12'.repeat(10)) returns false so the line-262 predicate is false and the check is skipped. Input, with the fixture of tests/world-gold-grants.test.mjs: f.grant('1','0x'+'AB12'.repeat(10)). Expected: 409 INVALID_ADDRESS_CHECKSUM and no row, the answer the existing test 'a mixed-case address must pass its checksum' gets for a single flipped character. Actual: 200 ok and a world_gold_grants row for 0xab12ab12ab12ab12ab12ab12ab12ab12ab12ab12. Control: f.grant('1','0xAB12AB12Ab12AB12ab12Ab12ab12aB12AB12aB12') (checksum form with its first b uppercased) is refused with INVALID_ADDRESS_CHECKSUM, because it contains both cases.","severity":"low","snippet":"  if(/[a-f]/.test(typed)&&/[A-F]/.test(typed)&&!isAddress(cmd.wallet as string,{strict:true}))fail('INVALID_ADDRESS_CHECKSUM');","title":"Checksum guard on the grant recipient is skipped for an all-uppercase address, so a mistyped uppercase paste is paid rather than refused"},{"citation":"resolved","description":"vault.claim (vault.py:129-131) hands out a new attempt whenever state='working' and lease_ms<=now, with lease_ms=now+120000, and nothing renews it while processor.compute runs. The design in AUDIT_REQUEST section 3 ('A computation keeps its attempt while it runs. After 15 minutes it is cancelled') is implemented only through the in-memory dict `active` in paid.py:90-96. So the vault protects a running computation for 120 s; after that the protection exists only inside the one process that started it. Any deliver call that reaches a process whose `active` lacks the run (a second uvicorn worker or replica behind the proxy, or a restarted process while the old one still finishes its computation during the 60 s shutdown wait) re-claims the run, starts a second computation of the same paid order, and the first computation's vault.complete fails with LEASE_LOST so its result is thrown away. World retries delivery every minute, so with N processes up to N simultaneous annual or profile computations of one order can occupy the two shared engine slots, which section 3 says closes the engine to public requests. No GOLD is lost: the order stays paid and is delivered by whichever attempt finishes last. The package does not include serve_production.py, so the single-process assumption could not be confirmed. Fix: make the vault the authority. Either set lease_ms to now+COMPUTE_LIMIT_MS in vault.claim so the lease covers the whole allowed computation and the 15-minute replacement happens at lease expiry, or have calculate() renew the lease (UPDATE runs SET lease_ms=? WHERE id=? AND attempt=?) every 60 s while it runs; keep the `active` map for the in-process cancel.","line":131,"path":"Submission24/source/integrations/celestao/celestao_world/vault.py","reproduction":"Reproduced against the submission's vault module with two Vault instances on one database file (standing in for two processes), run from outside the repository: t=0 P1.claim -> attempt a1 (lease t+120 s), P1 computation starts (5 minutes). t=60 s P2.claim -> None (lease held). t=130 s P2.claim -> a new attempt a2 (P2 has no `active` entry; paid.py line 96 calls vault.claim whenever `running` is falsy), a second computation starts. t=300 s P1.complete(a1) -> VaultError LEASE_LOST, the first result is discarded (paid.py:70 logs delivery_pending). t=430 s P2.complete(a2) -> kept. Expected: one computation per run until it ends or the 15-minute limit replaces it. The shipped test test_a_computation_past_its_lease_is_not_restarted_and_its_result_is_kept (test_paid.py:79) passes only because it runs one process.","severity":"low","snippet":"            self.db.execute(\"UPDATE runs SET state='working',attempt=?,lease_ms=? WHERE id=?\",(attempt,now+120000,rid))","title":"Vault claim lease (120 s) is never renewed while a computation runs; only the per-process active map stops a second claim, so a second Celestao process re-computes a paid order and discards the first "},{"citation":"resolved","description":"paid.py:103-104 waits DELIVERY_WAIT_SECONDS on the shielded task and swallows the timeout so the route answers kind='pending' inside World's 10 s client timeout (fix for 521bc017 finding 7). asyncio.wait_for raises asyncio.exceptions.TimeoutError, which became an alias of the builtin TimeoutError only in Python 3.11; on 3.8 to 3.10 it is a distinct Exception subclass and `except TimeoutError` does not catch it. The exception then escapes the route, FastAPI answers 500 with no signed body, World's createCelestaoDelivery throws DELIVERY_UNAVAILABLE (celestao-delivery.ts:21) and the job is retried after its 60 s lease. The computation keeps running under the shield and is delivered on a later call, so no order is lost, but the intended pending answer never happens on those interpreters and every slow report produces a 500 and an unexplained retry. The package pins no interpreter version (no deployment/ or release image, no requires-python anywhere in the tree), and evidence/celestao_side_summary.txt does not state the version the suite ran on; on this machine (3.12) asyncio.TimeoutError is TimeoutError. The code is only correct on 3.11+. Fix: `except (TimeoutError,asyncio.TimeoutError):pass`, and pin or assert sys.version_info>=(3,11) at startup.","line":104,"path":"Submission24/source/integrations/celestao/celestao_world/paid.py","reproduction":"State: Celestao service on CPython 3.10 or older, a paid run claimed, processor.compute longer than 8 s (any annual or profile report). Input: World's signed POST /api/internal/world-gold/deliver for that run. Expected: 200 with a signed envelope kind='pending'. Actual: asyncio.exceptions.TimeoutError propagates past `except TimeoutError`, HTTP 500, no signature; World logs nothing (status is not 409) and retries after the lease. Verification on this machine: python3 -c 'import asyncio;print(asyncio.TimeoutError is TimeoutError)' prints True on 3.12 and False on 3.10 (documented in the 3.11 changelog: 'asyncio.TimeoutError is now an alias of TimeoutError'). The shipped test test_a_slow_report_answers_pending_inside_worlds_timeout_and_is_delivered_later passes only on 3.11+.","severity":"low","snippet":"            except TimeoutError:pass","title":"Delivery wait catches the builtin TimeoutError only; on CPython 3.10 and older asyncio.wait_for raises asyncio.TimeoutError, so every report longer than 8 s answers 500 instead of pending"},{"citation":"resolved","description":"Line 36 validates the bridge session with readCelestaoGrant at clock()=t0. Line 37 takes a fresh now=t1 and the INSERT ... SELECT at lines 38-42 inserts only when b.expires_at>t1 AND s.expires_at>t1 AND s.revoked_at IS NULL. If the bridge session (MIN(session expiry, created+1h), celestao-auth.ts:72) or the sign-in session expires in the gap t0<expiry<=t1, or is revoked by a concurrent logout, the statement inserts nothing without error. Line 43 then finds no row and line 44 answers 409 IDEMPOTENCY_CONFLICT, a code that means 'this run id belongs to another wallet, product or hash'. The second readCelestaoGrant at line 45, which would answer 401, is never reached. On the Celestao side bridge.py:65-66 maps only 401/403 AUTH_REQUIRED to AuthDenied; a 409 becomes BridgeUnavailable, so paid.py:41-43 deletes the fresh run and answers 503 CHECKOUT_UNAVAILABLE, which the overlay presents as a temporary outage instead of a sign-in prompt. The code assumes 'no row after the upsert' can only mean a conflicting row. Narrow window, no money moves. Fix: when row is null, re-run readCelestaoGrant first and answer 401 AUTH_REQUIRED if it is gone (or check meta.changes===0 together with no row); keep 409 for a row that exists and differs.","line":44,"path":"Submission24/source/server/celestao-checkout.ts","reproduction":"Fixture from tests/celestao-checkout.test.mjs setup(t): bridge session row with expires_at = NOW+1. Call handleCelestaoCheckout with a correctly signed prepare body and deps.now = a clock returning NOW for every call up to and including the readCelestaoGrant at line 36 (passes: NOW+1>NOW) and NOW+1 from the line-37 call onward (the INSERT's b.expires_at>NOW+1 is false, nothing inserted; the signature timestamp stays within its 30 s window). Equivalent live state: the user logs out in another tab between the two reads. Expected: 401 AUTH_REQUIRED. Actual: 409 IDEMPOTENCY_CONFLICT with no checkout row; Celestao's prepare_checkout raises BridgeUnavailable, the run is deleted by forget_unpaid, and the buyer gets HTTP 503 CHECKOUT_UNAVAILABLE.","severity":"low","snippet":"   if(!row||row.wallet!==identity.address||row.product!==product.product||row.input_hash!==b.inputHash)return bad(409,'IDEMPOTENCY_CONFLICT');","title":"A signed prepare whose bridge or sign-in session lapses between the grant read and the insert answers 409 IDEMPOTENCY_CONFLICT instead of 401, so Celestao deletes the run and shows an outage rather th"},{"citation":"resolved","description":"The recipient check (world-gold.ts:259-262 and 0024 trigger line 18) accepts any well-formed non-zero address, as AUDIT_REQUEST section 1 states. A smart-contract wallet (Safe, ERC-4337 account) can sign in to World with ERC-1271 and sees the granted balance in /api/me/gold, but every write on /api/gold/* is refused with EOA_REQUIRED at this line and the write batch's authCheck (world-gold.ts:22) requires wallet_type='EOA' AND verification_method='ECDSA', so the holder can never quote a service or mint. With the grant immutable and no correcting entry (section 4), such a grant permanently inflates economy_gold_accounts and the recovery totals (economy-recovery.ts:237) with GOLD nobody can use, and the daily budget it consumed is gone. The checksum guard protects against a mistyped character but not against this class of wrong recipient. Operator error, bounded by 5,000 per grant, no attacker gain. Fix: in the grant branch refuse a recipient that has an existing sessions row with wallet_type<>'EOA' (no RPC needed), or read eth_getCode through the Worker's existing chain access as sign-in already does, and fail('RECIPIENT_NOT_EOA'); otherwise say in the panel that the recipient must be an EOA.","line":57,"path":"Submission24/source/server/world-gold-api.ts","reproduction":"State: grants enabled, grant wallet signed in, recipient C is a Safe whose owner has signed in to World before (sessions row wallet_type='SCA'/'CONTRACT', verification_method='ERC1271', as the test 'a wallet other than an EOA session cannot grant' sets up). Input: POST /api/gold/admin with command {action:'grant',wallet:C,gold:'5000'}. Actual: 200, world_gold_grants row, balance 5000 for C. Then C's owner signs in via ERC-1271 and POSTs /api/gold/quote: 403 EOA_REQUIRED (line 57); the 5,000 GOLD is unspendable and cannot be clawed back. Expected: the grant is refused or needs an explicit override when the recipient is known not to be an EOA.","severity":"low","snippet":"  if(write&&(session.walletType!=='EOA'||session.verificationMethod!=='ECDSA'))return await authReply({error:'EOA_REQUIRED'},403);","title":"A grant to a contract-wallet address is accepted and irreversible, but the recipient can never spend it because every GOLD write requires an EOA/ECDSA session"},{"citation":"resolved","description":"canGrant at line 67 does not check deps.config, but the POST path requires it: line 78 answers 503 GOLD_NOT_CONFIGURED for every admin action other than repair/refund, and world-gold.ts:263 fails the grant branch. When the release gate fails (worker/app.ts:214 swallows the error and leaves config null, which line 216 passes as deps.config) the panel shows the grant fieldset and a positive grantLeft while each attempt answers 503. No GOLD moves and nothing is bypassed; the state and the write disagree. Fix: add &&!!deps.config to the canGrant expression.","line":67,"path":"Submission24/source/server/world-gold-api.ts","reproduction":"State: WORLD_GOLD_GRANTS='enabled', 0024 applied, grant wallet signed in, deps.config null (fixture: fixture(t,GRANT_A,{config:null})). Input: GET /api/gold/state?expected_address=<grant wallet>. Actual: canGrant true, grantLeft '20000', paymentsReady false. POST a grant: 503 GOLD_NOT_CONFIGURED. Expected: canGrant omitted, as it is when the switch is off or the migration is partial.","severity":"info","snippet":"   const canGrant=isAdmin&&deps.grantsEnabled===true&&session.walletType==='EOA'&&session.verificationMethod==='ECDSA'&&GOLD_GRANT_WALLETS.includes(wallet)&&await goldGrantsReady(deps.db);","title":"The grant form is offered when no economy config is loaded, although every grant is then refused with GOLD_NOT_CONFIGURED"},{"citation":"resolved","description":"payload is JSON.stringify([cmd,args.reason]) where cmd is the parsed body object in the caller's key order (command() at world-gold-api.ts:23-27 only checks the key set). AUDIT_REQUEST section 1 says a request id 'replays any number of times', but the comparison at line 232 is byte-wise on the serialisation, not on the normalised grant (recipient lowercased, amount, reason). The shipped panel always serialises {action,wallet,gold} in the same order and sends the address as typed, so this is not reachable from WorldGoldPanel.tsx; any other client (a script re-sending a lost answer, a future panel that lowercases the address) gets 409 for a grant that was already paid, and the operator may then send it again under a new id, where only the 10-minute GRANT_DUPLICATE_RECENT rule stands between them and a second payment. Fix: compare a canonical form (sorted keys, economyAddress(cmd.wallet), String(Number(cmd.gold)), reason as stored) or store and compare a digest of it.","line":231,"path":"Submission24/source/server/world-gold.ts","reproduction":"Fixture from tests/world-gold-grants.test.mjs with request id R: POST body {expectedAddress,requestId:R,reason:'Fixture tester grant',command:{action:'grant',wallet:RECIPIENT,gold:'1000'}} -> 200. Re-send with command:{wallet:RECIPIENT,gold:'1000',action:'grant'} (same fields, different key order) or with wallet 0xCCC...C (uppercase spelling of the same recipient): expected {id,replayed:true}; actual 409 IDEMPOTENCY_CONFLICT. world_gold_grants still has one row, so no double payment on this path.","severity":"info","snippet":" const id=await economyDigest(wallet+':'+rid),payload=JSON.stringify([cmd,args.reason]);","title":"Replay detection compares the raw JSON serialisation of [command,reason], so the same grant re-sent with another key order or address casing is refused as IDEMPOTENCY_CONFLICT rather than replayed"}],"hash":"4739150cb5b2661875e59c99bbd59c9796aa92a156d4f0c6518e326ed3dbbefb","nodeId":"6b831a0a-463d-4177-8220-82dcf1ee647b","outcome":"completed","summary":"Review complete. The findings file is written and the tree is otherwise unchanged.\n\n**Outcome:** 9 findings kept in `.imd-findings.json` (1 medium, 6 low, 2 info), merged from the 14 specialist reports. No critical or high finding, so no Foundry proof applies; the submission contains no Solidity.\n\n**What I verified and kept**\n\n- **Medium, session-only grant authority** (four duplicates merged). The grant batch checks only a sessions row, and the shipped test fixture reaches a 200 with nothing but a cookie. This is the judgement the request asked for in section 5 item 2. My answer is that a 7-day cookie is not enough for irreversible issuance, and the fix keeps the two wallets, the limits and the switch by adding an EIP-712 signature per grant.\n- **Low, grantLeft ignores the 100-grant count** (three duplicates merged). Confirmed at the state route: only SUM is read.\n- **Low, all-uppercase recipient skips the checksum guard.** Confirmed with the project's pinned viem 2.56.9 outside the repo: strict isAddress rejects the uppercase spelling, and the guard predicate skips it.\n- **Low, vault lease never renewed.** Reproduced against the submission's vault module with two instances on one database: the second re-claims at 130 s and the first completion fails with LEASE_LOST.\n- **Low, asyncio TimeoutError on Python 3.10 and older.** The code is correct on 3.11+ only, and the package pins no interpreter. Kept with that caveat stated.\n- **Low, 409 instead of 401 in prepare** when the session lapses between the two clock reads. Kept as a narrow error-class defect.\n- **Low, grant to a contract wallet is accepted and unspendable.** Confirmed at the EOA_REQUIRED write gate.\n- **Info, canGrant shown without economy config,** and **info, replay compares raw JSON key order.** Both confirmed by reading.\n\n**Not reproducible in this environment**\n\nThe Node test suite could not run because the package tree lacks several server modules it imports, and the Python test suite could not run because fastapi is not installable here. Reproductions for the TypeScript findings are stated against the shipped fixtures rather than executed. Every snippet was copied from the file and checked to match.\n\n**What I checked and found holding**\n\nGrant limits and the 0024 trigger in both orders, the batch-level session recheck, replay and same-id race handling, origin and Sec-Fetch-Site rules, the 0023 table rebuild and trigger, the recovery balance check counting grants, and the prepare, admit, refusal, delivery and 15-minute paths on the Celestao side.","treeHash":null,"usage":{"cachedInputTokens":1572534,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":30548,"runtime":"claude","turns":38,"wallClockMs":416471}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"93c37f17670e4d98","findings":[{"citation":"resolved","description":"This answers audit question 2 (is a session enough for this authority). The grant branch of goldAdminCommand (world-gold.ts:256-266) and the HTTP handler (world-gold-api.ts:57, 71, 76-79) prove possession of the World session cookie only: the sessions row must be EOA/ECDSA and unexpired. Nothing in the request is signed by the granting wallet. Sessions live SESSION_TTL_MS = 7 days with no renewal and no step-up (source/server/auth.ts:25), so a single disclosed cookie value (device compromise, a stolen browser profile, a proxy or extension on the operator's machine, or an XSS anywhere on https://imdember.com, which is the only origin check applied) lets the holder issue grants to any address, including their own, for the rest of the week: 4 x 5,000 per 24 h, about 140,000 GOLD over the cookie's life, then spend it on services or mint while mint is open. There is no second approver, no notification, no per-wallet daily cap, and the grant cannot be reversed (AUDIT_REQUEST section 4). Every other GOLD-moving action a user takes (service payment, mint) requires an explicit wallet signature over the amount; the one action that creates GOLD from nothing is the only one that does not. The second grant wallet is ECONOMY_TREASURY (tests/world-gold-grants.test.mjs:38), so the key that receives all EMBEO payments is also kept hot in a browser to sign in to a web panel. This is a trust-assumption gap, not a permission bypass: an attacker still needs the cookie. It is reported because the owner asked whether a session is sufficient for new issuance authority and because the blast radius (irreversible issuance over 7 days) is larger than for any other session-only action in the codebase.","line":22,"path":"Submission24/source/server/world-gold.ts","reproduction":"State: a grant wallet W has signed in (sessions row wallet_type='EOA', verification_method='ECDSA', expires_at = issued_at + 7 days). Input from any client that holds only W's cookie value, with no wallet interaction: POST https://imdember.com/api/gold/admin, headers origin=https://imdember.com, content-type=application/json, cookie=<SESSION_COOKIE>=<token>, body {\"expectedAddress\":W,\"requestId\":<random 16+ chars>,\"reason\":\"ops\",\"command\":{\"action\":\"grant\",\"wallet\":<attacker address>,\"gold\":\"5000\"}}. Actual: 200 {ok:true,result:{id,...}} and economy_gold_accounts[attacker] += 5000; repeated 4 times per 24 h for 7 days: 140,000 GOLD, each row immutable. Expected for issuance authority: the request is refused unless it carries a signature by W over the grant itself. The test fixture in tests/world-gold-grants.test.mjs:25-27 already exercises exactly this cookie-only path and passes. Fix (preserves the agreed limits, switch and wallets): add a required `signature` field to the grant command, an EIP-712 typed message {domain: imdember.com, chainId, actor, recipient (lowercase), gold, requestId, deadline <= now + 10 min}; verify with viem verifyTypedData against the session address before building the batch and store the signature in world_gold_admin_actions.payload so the trail is self-proving; optionally also cap grants per session (for example 20,000 per sessions.token_hash) in the application guard so one cookie cannot reach the weekly total. Separately, as a scope decision for the owner: use a dedicated grant EOA instead of the treasury address for web sign-in, so a browser compromise cannot expose the payment-receiving key.","severity":"medium","snippet":"const authCheck=(db:D1Database,wallet:string,now:number,auth?:{tokenHash:string})=>check(db,auth?`EXISTS(SELECT 1 FROM sessions WHERE token_hash=? AND address=? AND revoked_at IS NULL AND expires_at>? AND wallet_type='EOA' AND verification_method='ECDSA')`:'1',...auth?[auth.tokenHash,wallet,now]:[]);","title":"Administrator GOLD grant is authorised by a 7-day bearer session cookie alone; no per-grant wallet signature, and one grant wallet is the treasury"},{"citation":"resolved","description":"paid.py:103-104 waits DELIVERY_WAIT_SECONDS (8 s) on the shielded computation and swallows the timeout so the route can answer kind='pending' inside World's 10 s client timeout (the fix for 521bc017 finding 7). asyncio.wait_for raises asyncio.exceptions.TimeoutError. That class became an alias of the builtin TimeoutError only in Python 3.11; on 3.8 to 3.10 it is a distinct subclass of Exception, so `except TimeoutError` does not catch it. The exception then escapes the route handler: FastAPI answers 500, World's createCelestaoDelivery sees status != 200 and throws DELIVERY_UNAVAILABLE, the job stays 'working' until its 60 s lease lapses and is retried. The computation itself keeps running under the shield and is delivered on a later call, so no order is lost, but the intended pending answer never happens on those interpreters and every report longer than 8 s produces a 500 and an unexplained retry. The package does not include deployment/ or the release image, and the test suite (evidence/celestao_side_summary.txt) ran on the author's interpreter, so the production interpreter version could not be verified; the code is only correct on 3.11+.","line":104,"path":"Submission24/source/integrations/celestao/celestao_world/paid.py","reproduction":"State: Celestao service running on CPython 3.10 (or 3.9/3.8), a paid run claimed, processor.compute taking longer than 8 s (any annual or profile report). Input: World's signed POST /api/internal/world-gold/deliver for that run. Expected: 200 with a signed envelope kind='pending'. Actual: asyncio.exceptions.TimeoutError propagates from `await asyncio.wait_for(asyncio.shield(task),DELIVERY_WAIT_SECONDS)` past `except TimeoutError`, the route fails with HTTP 500 and no signed body; World throws DELIVERY_UNAVAILABLE and retries after the lease. Same shape in the test test_a_slow_report_answers_pending_inside_worlds_timeout_and_is_delivered_later, which passes only on 3.11+. Fix: `except (TimeoutError,asyncio.TimeoutError):pass`, and pin the interpreter version in the release image or assert `sys.version_info>=(3,11)` at startup.","severity":"low","snippet":"            except TimeoutError:pass","title":"Delivery 8-second wait catches builtin TimeoutError; on Python 3.10 and older asyncio.wait_for raises asyncio.TimeoutError, so every slow report returns 500 instead of the pending envelope"},{"citation":"resolved","description":"The state route computes grantLeft from the 24-hour GOLD sum only (world-gold-api.ts:68-69). The count limit GOLD_GRANT_DAILY_COUNT = 100 is enforced by the application guard (world-gold.ts:266) and the trigger (0024 line 22) but is not reflected in the state, and WorldGoldPanel.tsx:58 renders grantLeft as the remaining allowance. An operator who sees 'N GOLD left' sends a grant, receives GRANT_DAILY_LIMIT, and has no way to tell from the panel whether the sum or the count was hit or when it reopens; the 10-minute duplicate rule then also blocks the natural retry. Low because no limit is bypassed; it is a correctness gap in the only number the operator is given.","line":69,"path":"Submission24/source/server/world-gold-api.ts","reproduction":"State: 100 grants of 1 GOLD in the last 24 hours (for example via the panel, or `for(let i=0;i<100;i++)direct(f.db,{id:'n'+i})` as in tests/world-gold-grants.test.mjs:233). Input: GET /api/gold/state?expected_address=<grant wallet>. Actual: canGrant=true, grantLeft='19900'. Then POST a grant of 1 GOLD: 409 GRANT_DAILY_LIMIT. Expected: grantLeft '0' (or an additional grantsLeft:'0') so the form is disabled until the oldest row leaves the window. Fix: also select COUNT(*) over the same window and return grantLeft = count>=GOLD_GRANT_DAILY_COUNT ? '0' : <sum-based value>, plus grantsLeft, and let grantFormReady disable the button when either is zero.","severity":"low","snippet":"grantLeft:String(Math.max(0,GOLD_GRANT_DAILY-(granted?.n??0)))","title":"grantLeft reports only the GOLD budget; after the 100-grant count limit the form still advertises a positive budget and every grant is refused"},{"citation":"resolved","description":"The recipient check (world-gold.ts:259-262 and trigger 0024 line 18) accepts any well-formed non-zero address. A smart-contract wallet (Safe, ERC-4337 account) can sign in to World with ERC-1271 and will see the granted balance in /api/me/gold, but every write on /api/gold/* is refused with EOA_REQUIRED (world-gold-api.ts:57) and the write batch's authCheck requires wallet_type='EOA' AND verification_method='ECDSA' (world-gold.ts:22), so the holder can never quote a service or mint. Combined with the grant being immutable with no correcting entry (AUDIT_REQUEST section 4), such a grant permanently inflates the book (economy_gold_accounts, recovery totals) with GOLD nobody can use, and the daily budget it consumed is gone. The panel's checksum guard protects against a mistyped character but not against this class of wrong recipient. Low: operator error, bounded by the 5,000 per-grant limit, no attacker gain.","line":262,"path":"Submission24/source/server/world-gold.ts","reproduction":"State: grants enabled, grant wallet signed in. Input: POST /api/gold/admin with command {action:'grant', wallet:<address of a Safe or any contract>, gold:'5000'}. Actual: 200, world_gold_grants row written, balance 5000 for that address. Then the contract's owner signs in via ERC-1271 and POSTs /api/gold/quote: 403 EOA_REQUIRED; the 5,000 GOLD is unspendable forever and cannot be clawed back. Expected: the grant is refused or at least requires an explicit override when the recipient has code. Fix: in goldAdminCommand's grant branch, read eth_getCode for the recipient through the Worker's existing chain access (the sign-in path already classifies EOA vs contract this way) and fail('RECIPIENT_NOT_EOA') when code is present; if an RPC read is undesirable in the write path, refuse recipients that have an existing sessions row with wallet_type<>'EOA' and document the remaining gap in the panel text.","severity":"low","snippet":"  if(/[a-f]/.test(typed)&&/[A-F]/.test(typed)&&!isAddress(cmd.wallet as string,{strict:true}))fail('INVALID_ADDRESS_CHECKSUM');","title":"A grant to a contract-wallet address is accepted, irreversible, and can never be spent, because every GOLD write path requires an EOA/ECDSA session"},{"citation":"resolved","description":"canGrant does not check deps.config, but the POST path requires it (world-gold-api.ts:78 returns 503 GOLD_NOT_CONFIGURED for every admin action other than repair/refund, and world-gold.ts:263 fails the grant branch). When the release gate fails (worker/app.ts:214 swallows the error and leaves config null) the panel shows the grant fieldset and a positive grantLeft, and each attempt answers 503. No GOLD moves and nothing is bypassed; the state and the write disagree.","line":67,"path":"Submission24/source/server/world-gold-api.ts","reproduction":"State: WORLD_GOLD_GRANTS='enabled', 0024 applied, grant wallet signed in, reviewedEconomyConfig() returning null or throwing. Input: GET /api/gold/state?expected_address=<grant wallet>. Actual: canGrant=true, grantLeft='20000', paymentsReady=false. POST a grant: 503 GOLD_NOT_CONFIGURED. Expected: canGrant omitted (as it is when the switch is off or the migration is partial). Fix: add `&&!!deps.config` to the canGrant expression.","severity":"info","snippet":"   const canGrant=isAdmin&&deps.grantsEnabled===true&&session.walletType==='EOA'&&session.verificationMethod==='ECDSA'&&GOLD_GRANT_WALLETS.includes(wallet)&&await goldGrantsReady(deps.db);","title":"The grant form is offered when no economy config is loaded, although every grant is then refused with GOLD_NOT_CONFIGURED"}],"hash":"47952d3d0b1ac67d077f2b4b426b791b52b5fb17117f9c179e48c4598f3ef802","nodeId":"0706fdf3-1a81-45ce-a275-a4a12f9d14da","outcome":"completed","summary":"The review is complete and the findings file is written and verified against the tree.\n\n**Outcome.** No path past the grant limits was found. Five findings are recorded in `.imd-findings.json`: one medium, three low, one info. None is a permission bypass. The shell sandbox never initialised in this session, so no test or script was run; every conclusion comes from reading the code, the migrations, the patches and the supplied test suites.\n\n**Findings written**\n\n- **Medium, grant authority.** A grant needs only the 7-day World session cookie. No wallet signature, no second approver, no per-session cap, and one grant wallet is the treasury. A disclosed cookie can issue about 140,000 irreversible GOLD over its life. Fix: an EIP-712 signature per grant stored in the audit payload, plus a dedicated grant EOA instead of the treasury address.\n- **Low, delivery timeout.** The 8-second wait in paid.py catches the builtin TimeoutError. On Python 3.10 and older asyncio raises a different class, so slow reports answer 500 instead of pending. The release image's interpreter is not in the package, so this holds only if it is below 3.11.\n- **Low, grantLeft.** The state shows the GOLD budget only. After 100 grants the panel still shows budget left while every grant is refused.\n- **Low, contract recipient.** A grant to a contract-wallet address is accepted and can never be spent, since every GOLD write requires an EOA session. The grant cannot be taken back.\n- **Info, canGrant without config.** The form appears when the economy config failed to load, and every grant then returns 503.\n\n**Areas checked and found sound**\n\n- Limits: application guards and the database trigger agree on 5,000, 20,000 and 100 per rolling day. Replay ids include the actor, races fall back to the winner's answer, session end is checked inside the write batch, and cross-site posts fail the origin check.\n- Ledger: only the history view and the recovery planner sum journals. Both include the grants table.\n- Pay and receive: the unadmitted mark, the forget-on-refusal path, the 15-minute cancel and the lease recovery hold under the interleavings traced. The remaining loss paths are the ones already listed in section 4 of the request.\n- Budgets: signed prepares are keyed per bridge session and unsigned traffic per caller address. The limit table's eviction only ever makes a client's count more permissive.\n- Migrations: 0023 installs the sixth product in either order, and 0024 fails closed on any missing table, trigger or view change.\n\n**Not verified.** The two grant wallet constants are redacted, the wrangler limiter rates and the release image's Python version are outside the package, and the JSON file could not be machine-parsed here. It was re-read by eye and all five snippets and line numbers were confirmed with exact-match searches.","treeHash":null,"usage":{"cachedInputTokens":2305078,"inputTokens":452,"model":"claude-fable-5-1","outputTokens":55438,"runtime":"claude","turns":52,"wallClockMs":884014}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f494611affb5524c","findings":[{"citation":"resolved","description":"The grant path (world-gold.ts goldAdminCommand, action 'grant', line 256-266) is authorised by: the economy session cookie (readEconomySession / revalidateEconomySession in world-gold-api.ts), the in-batch authCheck above (session row exists, not revoked, EOA/ECDSA), the actor being one of GOLD_GRANT_WALLETS, and the WORLD_GOLD_GRANTS variable. Nothing in the request is signed by the granting wallet: there is no per-grant ECDSA/EIP-712 signature over (recipient, amount, requestId), and the 0024 trigger cannot add one. By contrast every GOLD-moving write (service payment, mint hold) requires an explicit wallet signature (world-gold-api.ts line 29-31: 'spending continues through the existing explicit GOLD-payment signature'). So the asymmetry is: with a stolen session an attacker cannot spend the victim's GOLD, but can create new GOLD into a wallet they control and spend it there with their own signature. The recipient is unrestricted (any non-zero address, including the actor), there is no second approver, no notification, the 20,000/day and 100/day budgets are shared by both wallets, and the session lasts 7 days (AUDIT_REQUEST section 1). One of the two grant wallets is ECONOMY_TREASURY, so the key that owns the treasury must be used interactively in a browser to sign in, which widens its exposure to phishing and malware for an authority that then needs no further signature. The triggers and application limits were checked and hold (5,000 per grant, 20,000 and 100 per rolling 24h, actor list, immutability, reference binding, idempotent replay, same-id race returning replayed, batch-level session recheck, Origin/Sec-Fetch-Site/Content-Type on POST); this finding is about the authority model the limits sit on, which the request asks to be judged.","line":22,"path":"Submission24/source/server/world-gold.ts","reproduction":"State: Worker variable WORLD_GOLD_GRANTS='enabled', migration 0024 applied, economy configured and not halted, a valid EOA/ECDSA economy session S for grant wallet G (cookie obtained from the operator's browser, device or a same-origin XSS; S is valid up to 7 days). Input, repeated with four distinct requestIds and four distinct attacker recipients A1..A4 (or one recipient with amounts 5000,4999,4998,4997 to pass GRANT_DUPLICATE_RECENT): POST https://imdember.com/api/gold/admin, Origin: https://imdember.com, Content-Type: application/json, Cookie: S, body {\"expectedAddress\":G,\"requestId\":\"<16-80 chars>\",\"reason\":\"ops\",\"command\":{\"action\":\"grant\",\"wallet\":\"<A_i>\",\"gold\":\"5000\"}}. Actual: four 200 {ok:true,result:{id,results:[...]}} responses; world_gold_grants gains four rows, economy_gold_accounts of A1..A4 gain 5000 each (world_gold_grant_apply trigger); repeating each 24h for the session's life yields about 140,000 GOLD that A1..A4 spend on services or mint with their own signatures; no grant can be reversed (section 4). Expected: a write that creates GOLD with no order behind it should not be executable from a session alone. Fix (keeps the owner's design: same two wallets, same limits, same switch): require in the grant command a signature by G over an EIP-712 typed message {recipient, gold, requestId, chainId, verifyingContract/origin, deadline<=10 min}, verify it with viem recoverTypedDataAddress before building the batch, store the signature hash in world_gold_grants (new column) and include it in the admin action payload so replays still compare equal; optionally require the second grant wallet's signature above a threshold (e.g. 1,000) and use an operational wallet rather than ECONOMY_TREASURY for interactive sign-in. Until then, keep WORLD_GOLD_GRANTS off, as it is.","severity":"medium","snippet":"const authCheck=(db:D1Database,wallet:string,now:number,auth?:{tokenHash:string})=>check(db,auth?`EXISTS(SELECT 1 FROM sessions WHERE token_hash=? AND address=? AND revoked_at IS NULL AND expires_at>? AND wallet_type='EOA' AND verification_method='ECDSA')`:'1',...auth?[auth.tokenHash,wallet,now]:[]);","title":"Administrator GOLD grant is the only value-creating write authorised by a cookie session alone; a stolen 7-day session of either grant wallet mints up to 20,000 GOLD per day to any address"},{"citation":"resolved","description":"vault.claim (vault.py line 129-131) hands out a new attempt whenever state='working' and lease_ms<=now, with lease_ms=now+120000, and it is never renewed while the computation runs. The design (AUDIT_REQUEST section 3: 'A computation keeps its attempt while it runs. After 15 minutes it is cancelled') is implemented only through the in-memory dict `active` in paid.py. The vault lease therefore protects a running computation for 120 s, and after that the protection exists only inside the one process that started it. Any deliver call that reaches a process whose `active` lacks the run (a second uvicorn worker, a second replica behind the proxy, or a process that restarted and was then joined by a still-running old one during the 60 s shutdown wait) re-claims the run, starts a second computation for the same paid order, and the first computation's vault.complete fails with LEASE_LOST (attempt mismatch) so its result is thrown away. World retries delivery every minute, so with N processes up to N simultaneous computations of one annual/profile run can occupy the two shared engine slots, which AUDIT_REQUEST section 3 says closes the engine to public requests. No GOLD is lost (the order stays paid and is eventually delivered by whichever attempt finishes last), but the 'keeps its attempt' guarantee and the engine-capacity argument only hold for a single process, and nothing in the package pins that (serve_production.py is not included). test_paid.py line 79 (test_a_computation_past_its_lease_is_not_restarted_and_its_result_is_kept) exercises exactly one process.","line":96,"path":"Submission24/source/integrations/celestao/celestao_world/paid.py","reproduction":"State: two Celestao processes P1 and P2 sharing one vault database (the proxy may route either), run R admitted and bound to paid order O, processor.compute for 'annual_ziwei' takes 5 minutes. t=0: World deliver reaches P1: vault.claim gives attempt a1 (lease_ms=t+120000), P1 starts the computation, answers pending after 8 s. t=60 s: World retries, reaches P2: active (P2) is empty, vault.claim sees state='working' AND lease_ms>now so returns None, pending. t=130 s: World retries, reaches P2: lease expired, vault.claim returns attempt a2 and overwrites runs.attempt, P2 starts a second computation of the same input. t=300 s: P1's compute finishes, vault.complete(a1) raises LEASE_LOST (row.attempt=a2), logged as delivery_pending and discarded. t=430 s: P2 completes with a2. Actual: two full computations, one result discarded, engine slot occupied twice; with more retries landing on alternating processes, more. Expected: one computation per run until it ends or the 15-minute limit replaces it. Fix: make the vault the authority. Either set lease_ms to now+COMPUTE_LIMIT_MS in vault.claim (so the lease, not the process map, covers the whole allowed computation and the 15-minute replacement happens exactly at lease expiry), or have calculate() renew the lease (UPDATE runs SET lease_ms=? WHERE id=? AND attempt=?) every 60 s while it runs. Keep the `active` map for the in-process cancel, and state in the deployment notes that the service runs as one process if that remains the intent.","severity":"low","snippet":"        claim=None if running else vault.claim(b['wallet'],b['runId'],b['orderId'],clock())","title":"Vault claim lease (120 s) is far shorter than the 15-minute compute limit; only the per-process `active` map stops a second claim, so a second Celestao process (or any deliver not seen by that map) re"},{"citation":"resolved","description":"GET /api/gold/state computes grantLeft = GOLD_GRANT_DAILY - SUM(delta over 24h) and the panel (WorldGoldPanel.tsx line 58) prints it as 'left in the last 24 hours'. The count limit (GOLD_GRANT_DAILY_COUNT=100, enforced by the application guard in world-gold.ts line 266 and by the 0024 trigger line 22) is not reflected. Operational only: no limit is bypassed, the grant is refused with GRANT_DAILY_LIMIT and the panel shows the translated message.","line":68,"path":"Submission24/source/server/world-gold-api.ts","reproduction":"State: 100 grants of 1 GOLD each within the last 24 hours (sum 100). GET /api/gold/state as a grant wallet with grants enabled: grantLeft='19900', canGrant=true. POST /api/gold/admin with command {action:'grant',wallet:<any>,gold:'1'}: 409 GRANT_DAILY_LIMIT. Expected: grantLeft '0' (or an additional grantsLeft count) so the form is disabled. Fix: also select COUNT(*) over the window and report min over both budgets, e.g. grantLeft=String(count>=GOLD_GRANT_DAILY_COUNT?0:Math.max(0,GOLD_GRANT_DAILY-sum)) plus grantsLeft=String(Math.max(0,GOLD_GRANT_DAILY_COUNT-count)).","severity":"info","snippet":"   const [granted,recentGrants]=canGrant?await Promise.all([deps.db.prepare('SELECT COALESCE(SUM(delta),0) n FROM world_gold_grants WHERE created_ms>?').bind(now-GOLD_GRANT_WINDOW_MS).first<{n:number}>(),deps.db.prepare('SELECT id,wallet,CAST(delta AS TEXT) gold,actor,created_ms FROM world_gold_grants ORDER BY created_ms DESC,id DESC LIMIT 10').all()]):[null,null];","title":"grantLeft shown to the operator counts only the 20,000 GOLD budget, not the 100-grant count budget, so the panel can offer GOLD that every grant request will refuse"}],"hash":"5a937a4303e61c5b8b7861ad766f88f703ca13f93336f9ea65fa22fc7e37f3d5","nodeId":"ec5ebeca-95dd-4841-b038-5d0bf2493152","outcome":"completed","summary":"The review of Submission24 is complete. The findings file `.imd-findings.json` holds three findings, each with a concrete failing input or state and a fix that keeps the owner's design.\n\n**Findings**\n\n- **Medium. Grant authority is session-only.** The grant is the one write that creates GOLD with no order behind it, yet it needs only the economy cookie session plus the in-batch session row check in `server/world-gold.ts` line 22. Every GOLD-moving write requires an explicit wallet signature. With a stolen session of either grant wallet (one is the treasury), an attacker sends four grant commands with distinct request ids or amounts and receives 20,000 GOLD per day into wallets they control, about 140,000 over the 7-day session, irreversibly. Recommended fix: an EIP-712 signature by the granting wallet over recipient, amount, request id, chain and a short deadline, verified before the batch and recorded in the grant row. Optionally a second signer above a threshold and an operational wallet instead of the treasury for interactive sign-in.\n- **Low. The vault lease does not cover the compute limit.** The claim lease is 120 seconds and is never renewed, while a computation may run 15 minutes. Only the in-process `active` dictionary in `paid.py` stops a second claim. A second Celestao process or replica re-claims after 130 seconds, computes the same paid run again, and the first result is discarded with LEASE_LOST. No GOLD is lost, but the \"keeps its attempt\" guarantee and the engine-slot argument hold for one process only, and nothing in the package pins that. Fix: set the lease to the compute limit at claim, or renew it while computing.\n- **Info. `grantLeft` ignores the 100-grant count budget.** After 100 small grants the panel still shows GOLD left while every grant is refused.\n\n**Checked and found holding** (no finding): the 5,000 per grant, 20,000 and 100 per rolling 24 hours in both the application guards and the 0024 trigger, actor list, reference binding, immutability, idempotent replay and same-id race handling, in-batch session recheck, Origin, Sec-Fetch-Site and Content-Type rules on the admin POST, the 0024 view rebuild, recovery planning including grants in the balance check, the `unadmitted` lifecycle including the forget/admit race and the 30-day prune, the per-bridge-session and per-code budgets, the limit table eviction, and the 0023 rebuild and release trigger in either order.\n\n**Limits of this review.** The sandbox could not start a shell, so nothing was executed. The reported items were verified by reading. There is no Solidity in scope, so no Foundry proof applies. Files not in the package were not read: the economy payment path, `economy-reconciliation.ts`, `world-gold-report.ts`, migration 0022 and `serve_production.py`. If any omitted file recomputes a balance from journals, as the 0021 comment describes the invariant, it would now need to count `world_gold_grants` as well. The two grant wallet literals are masked, so the lowercase match between the trigger literals and the application list could not be confirmed.","treeHash":null,"usage":{"cachedInputTokens":1889442,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":50857,"runtime":"claude","turns":51,"wallClockMs":855827}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b57ae3a96321a0e1","findings":[{"citation":"resolved","description":"The grant has two rolling 24-hour limits enforced in server/world-gold.ts:266 and in the 0024 trigger: SUM(delta) <= 20,000 and COUNT(*) < 100. The state read computes grantLeft from the sum only (SELECT COALESCE(SUM(delta),0) FROM world_gold_grants WHERE created_ms>now-24h) and never reads the count. Boundary x invariant seam: the displayed budget is consistent with one of the two guards but not with the invariant the server actually enforces (both). An operator reads 'N GOLD left in the last 24 hours' in WorldGoldPanel.tsx:58, sends a grant, and is refused with GRANT_DAILY_LIMIT. The error text mentions both limits, but the number on screen is wrong for the rest of the day. Fix: also read COUNT(*) over the same window and report grantLeft as '0' (or add grantsLeft) when the count has reached GOLD_GRANT_DAILY_COUNT, so the form closes for the right reason.","line":69,"path":"Submission24/source/server/world-gold-api.ts","reproduction":"Fixture from tests/world-gold-grants.test.mjs: insert 100 rows with direct(f.db,{id:'n'+i}) (delta 1 each, created_ms NOW). GET /api/gold/state as a grant wallet: expected grantLeft '0' (no grant can be made for 24 hours); actual grantLeft '19900' and canGrant true. POST /api/gold/admin {action:'grant',wallet:RECIPIENT,gold:'1'} then answers 409 GRANT_DAILY_LIMIT. The existing test 'no more than a hundred grants in 24 hours' asserts the refusal but never reads grantLeft.","severity":"low","snippet":"grantLeft:String(Math.max(0,GOLD_GRANT_DAILY-(granted?.n??0)))","title":"grantLeft reports only the 20,000 GOLD budget and ignores the 100-grant count limit, so the panel can show GOLD left while every grant is refused"},{"citation":"resolved","description":"Line 36 validates the bridge session with readCelestaoGrant at clock() = t0. Line 37 takes a fresh now = t1 and the INSERT ... SELECT at lines 38-42 only inserts when b.expires_at>t1 AND s.expires_at>t1 AND s.revoked_at IS NULL. If the bridge session (MIN(session expiry, created+1h) from celestao-auth.ts:72) or the sign-in session expires or is revoked in the gap t0<expiry<=t1, the statement inserts nothing without error. Line 43 then finds no row and line 44 answers 409 IDEMPOTENCY_CONFLICT, a code that means 'this run id belongs to another wallet/product/hash'. The second readCelestaoGrant at line 45, which would answer 401 AUTH_REQUIRED, is never reached because the 409 returns first. On the Celestao side bridge.py:65-66 maps only 401/403 AUTH_REQUIRED to AuthDenied; a 409 becomes BridgeUnavailable, so paid.py:41-43 deletes the fresh run and answers 503 CHECKOUT_UNAVAILABLE, which the overlay presents as a temporary outage rather than asking for a new sign-in. Boundary: the code assumes 'no row after the upsert' can only mean a conflicting row, but the SELECT-fed INSERT also yields no row when its session predicates fail. Fix: before answering 409 at line 44, when row is null re-check readCelestaoGrant (or check meta.changes===0 && no row) and answer 401 AUTH_REQUIRED; or move the line-45 re-check ahead of line 44 for the null-row case.","line":44,"path":"Submission24/source/server/celestao-checkout.ts","reproduction":"Fixture from tests/celestao-checkout.test.mjs setup(t): bridge session row with expires_at = NOW+1. Call handleCelestaoCheckout with a correctly signed prepare body and deps.now = a clock that returns NOW for every call up to and including the readCelestaoGrant at line 36 (which passes because NOW+1>NOW) and NOW+1 from the line-37 call onward (now = NOW+1, so the INSERT's b.expires_at>NOW+1 is false and nothing is inserted; the signature timestamp stays within its 30 s window). Expected: 401 AUTH_REQUIRED (identity lapsed). Actual: 409 IDEMPOTENCY_CONFLICT with no checkout row, and Celestao's prepare_checkout raises BridgeUnavailable -> HTTP 503 CHECKOUT_UNAVAILABLE instead of 401 AUTH_REQUIRED.","severity":"low","snippet":"if(!row||row.wallet!==identity.address||row.product!==product.product||row.input_hash!==b.inputHash)return bad(409,'IDEMPOTENCY_CONFLICT');","title":"A signed prepare whose bridge or sign-in session lapses between the grant read and the insert answers 409 IDEMPOTENCY_CONFLICT instead of 401, so Celestao treats a re-login case as an outage"},{"citation":"resolved","description":"payload is JSON.stringify([cmd,args.reason]) where cmd is the parsed body object in the caller's key order (command() at world-gold-api.ts:23-27 only checks the key set). The replay rule in AUDIT_REQUEST section 1 says a request id 'replays any number of times'; the comparison is byte-wise on the serialisation, not on the normalised grant (recipient lowercased, amount, reason). The shipped panel always serialises {action,wallet,gold} in the same order, so this is not reachable from WorldGoldPanel.tsx, but any other client (a script re-sending a lost answer from a log, a future panel that lowercases the address or trims the reason) gets 409 for a grant that was already paid, and the operator may then send it again under a new id, where only the 10-minute GRANT_DUPLICATE_RECENT rule stands between them and a second payment. Fix: compare a canonical form (sorted keys, economyAddress(cmd.wallet), String(Number(cmd.gold)), reason as stored) or store and compare a digest of that canonical form.","line":232,"path":"Submission24/source/server/world-gold.ts","reproduction":"Fixture from tests/world-gold-grants.test.mjs with requestId R: POST /api/gold/admin body {expectedAddress,requestId:R,reason:'Fixture tester grant',command:{action:'grant',wallet:RECIPIENT,gold:'1000'}} -> 200. Re-send the same grant with command:{wallet:RECIPIENT,gold:'1000',action:'grant'} (same fields, different key order) or with wallet uppercased 0xCCC... (checksum passes, same lowercase recipient): expected {id,replayed:true}; actual 409 IDEMPOTENCY_CONFLICT. world_gold_grants still has one row, so no double payment occurs on this path.","severity":"info","snippet":"const old=await st(db,'SELECT payload FROM world_gold_admin_actions WHERE id=?',id).first<{payload:string}>();if(old){if(old.payload!==payload)fail('IDEMPOTENCY_CONFLICT');return {id,replayed:true};}","title":"Replay detection compares the raw JSON serialisation of [command,reason], so the same grant re-sent with a different key order or trimmed reason is refused as IDEMPOTENCY_CONFLICT rather than replayed"},{"citation":"resolved","description":"Answer to AUDIT_REQUEST section 5 item 2. The grant batch checks only that a sessions row with the cookie's token_hash, the admin address, wallet_type EOA and verification_method ECDSA is unrevoked and unexpired. No per-grant wallet signature is taken, unlike GOLD spending, which already requires an explicit signature (goldChallenge/payGold in WorldGoldPanel.tsx:127). The limits bound what the session can do: 5,000 per grant, 20,000 and 100 grants per rolling 24 h shared by both wallets, so one session held for its full 7-day life can issue at most 7 x 20,000 = 140,000 GOLD in up to 700 grants, to any address including an attacker's own, with no second approver and no notification, and none of it can be reversed (world_gold_grants is immutable and has no correcting entry). The same session, or the recipient's, can then spend it on services or mint when mint is open. With WORLD_GOLD_GRANTS unset nothing is reachable, and the daily sum limit also caps a legitimate operator at the same rate, so the control is the switch plus the caps, not the credential. Recommendation (a design decision for the owner, not a defect in the stated design): require a per-grant EIP-712 signature from the granting EOA over (recipient, amount, request id, expiry <= 10 min, chain id, domain), verified server-side in goldAdminCommand before the batch; alternatively a much shorter session lifetime or a re-authentication (fresh ECDSA challenge) for the grant action only. Either keeps the two-wallet list and the caps unchanged.","line":22,"path":"Submission24/source/server/world-gold.ts","reproduction":"State: WORLD_GOLD_GRANTS=enabled, migration 0024 applied, an attacker holds a copy of a grant wallet's __Host session cookie issued within the last 7 days (no key material). From any browser, same-origin POST /api/gold/admin with that cookie and {action:'grant',wallet:<attacker>,gold:'5000'} succeeds 4 times per 24 h (20,000) and again each day until the session expires; expected under a per-grant-signature design: refused without the wallet's signature. No real grant has been made and the switch is off, so this is a documented risk, not an exploited state.","severity":"info","snippet":"const authCheck=(db:D1Database,wallet:string,now:number,auth?:{tokenHash:string})=>check(db,auth?","title":"Trust assumption: a 7-day EOA session cookie is the only credential behind a grant; the arithmetic of what one stolen session can issue"}],"hash":"9e0cf0b8c543178bfca6ad521140006f8977c336f2a5f55db8324355064412d0","nodeId":"f7e09f57-2029-46ba-82d7-7a607965adb2","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds four findings, none above low. The Bash sandbox failed to initialise for the whole session, so I could not run the test suites or a JSON parser; every snippet was instead verified against the tree with exact-string searches, and the file was re-read by eye.\n\n**Outcome.** Across the math, boundary and numerical-gap passes, the grant limits, the ledger counting, the pay/receive rework, the retention mark, the budgets and both migrations hold. No path past 5,000 per grant, 20,000 or 100 per 24 hours, or for another wallet was found. The application guard and the 0024 trigger use the same half-open window and the same clock value, so there is no gap between them. Recovery counts grants in the balance check. The `unadmitted` mark cannot delete a run a buyer holds, because the id is returned only after `admit` succeeds and every deletion path requires the mark. The two reopen clocks on Celestao and World can differ, but World's window is the one the quote checks, so no paid order is lost by that.\n\n**Findings written:**\n- **Low, grantLeft:** the state read computes GOLD left from the sum only and ignores the 100-grant count, so after 100 small grants the panel shows 19,900 left while every grant is refused.\n- **Low, prepare error code:** if the bridge or sign-in session lapses between the grant read and the insert, World answers 409 IDEMPOTENCY_CONFLICT instead of 401, and Celestao maps that to a 503 outage and forgets the run rather than asking for a re-login.\n- **Info, replay comparison:** replay detection compares the raw JSON of the command, so the same grant re-sent with different key order or a lowercased address gets 409 instead of a replay. Not reachable from the shipped panel.\n- **Info, trust assumption:** answering the session question in the brief, one stolen 7-day session can issue up to 140,000 GOLD in 700 grants with no signature and no reversal. A per-grant EIP-712 signature, mirroring the existing GOLD payment signature, would close it without changing the limits.\n\n**Not substantiated, so not reported:** the limit-table eviction gives a flooder only one extra window per 2,048 addresses, which they already hold; trigger-only refusals surfacing as 503 are unreachable because the application guards are a superset; the 0023 index and trigger behave in both orders.\n\n**Limits of this review.** The economy-config, economy-session, auth and migrations 0010 to 0013 are not in the package, so the session clock units and any older account triggers were taken from how the included code uses them. No proof test files were written because no finding reached high.","treeHash":null,"usage":{"cachedInputTokens":2324627,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":61133,"runtime":"claude","turns":67,"wallClockMs":939360}}],"verification":[]}