{"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":"c89f023c-dd16-4136-8f86-9ac12f7712fe","kind":"audit","nodes":[{"acceptedSubmissionHash":"156d712d8c0e9c219fc7fee4d8b49535c5e70c664faedf5c12049c891720d606","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":"561b14903db54203657ffeea2e3fff7f036175488600174c92bb3e65188067d0","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":"705181a3af8cbf8b1d6c202bced76099f42e23140f8fb36784021931329e3163","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":"66ed811b1304dc9eff1789bc8d596779a7db39d21c19d4867c4bc5eef4f62d26","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":"911c0e0d75edaee7b926962586e37961a47bbab64618d4b424d05f8a8c0a270e","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":"Audit src/libraries/PaymentLib.sol, src/factories/ (ERC721 drops, ERC1155 editions with DynamicPricingModule, ERC721 auctions, InitCodeStore), src/registry/MiyaRegistry.sol, script/DeployMiyamints.s.sol and script/config/1.json, at the pinned commit, for a mainnet launch. Read whatever else in src/ these depend on, but report on this scope.\n\nmiyamints is an NFT launchpad on Ethereum mainnet (fork of ms2fun-contracts). Collections (ERC721 drops, ERC721 auctions, ERC1155 editions) sell in any token on the registry allowlist (ETH, IMD, INFER, USDC, imdUSD, cbBTC, WBTC) at a price per token; the artist collects each token exactly as paid; a 1% protocol cut and a collection-chosen alignment share (at least 10%) are taken in the token paid. The alignment share goes to an alignment vault, which books it as pending per benefactor and token; a keeper converts pending tokens to IMD (and ETH) with aggregator calldata it supplies (0x AllowanceHolder, zRouter fallback; both allowlisted by the factory owner, a Safe), the vault balances IMD and INFER and adds permanent full-range liquidity to the hooked INFER/IMD Uniswap v4 pool, and benefactors claim the position's INFER and IMD fees (or take them as ETH, or reinvest). Read docs/IMD-INFER-VAULT.md (design and trust model) and AUDIT-NOTES.md (internal findings, fixes and accepted risks) first. The keeper is trusted for timing and for its floors; the question is what it, or a bug in it, can do beyond selling a batch at its own minimum outputs. BUILD: solc 0.8.28 with via_ir; `forge build --skip test --skip script` builds src in a few minutes; a full build with test/ needs much more memory. `SKIP_FORK_TESTS=true forge test --offline` runs the unit, invariant and integration suites; fork tests need a mainnet RPC and are optional.\n\nAnswer each numbered question, including the ones where nothing is wrong:\n1. PER-TOKEN BOOKS (unsplit, ownerOwed, parked protocol and vault cuts, escrow, refunds; distribute, withdraw, withdrawAll/withdrawAllStep, flushPendingProtocolCut, flushPendingVaultCut with a top-up, sweepUntracked). For every token: balance held >= everything owed; nothing counted twice; sweepUntracked counts only the surplus; withdrawAll skips a failing token without breaking the others.\n2. PAYMENT INTAKE (mint, mintTo, createBid, queuePiece; maxCost; msg.value rules for ETH and ERC-20; the balance-delta check; dynamic pricing rounding up with a floor of basePrice x amount, per-token base prices on one curve). Underpay, overpay without a refund, mint in a token the sale does not price or below price at any decimals, or slip a fee-on-transfer token through.\n3. AUCTIONS (one currency per house; ETH push refunds with forceSafeTransferETH; ERC-20 pull refunds refundOf and claimRefund(to); settle, reclaim, anti-snipe; escrow). Grief settlement, lose or double a refund, or take escrow.\n4. VAULT AND TREASURY CALLS (exact approval and reset, try/catch parking, the gas a caller leaves; claimVaultFees forwarding the vault's infer() and imd() by balance change). Force a cut to be parked or lost, or make claimVaultFees forward proceeds (INFER or IMD sales) as fees or fees as proceeds.\n5. FACTORIES AND REGISTRY (InitCodeStore SSTORE2 init code written once; CREATE3 salts bound to factory and creator; checks of vault, alignment share and payment tokens; setPaymentToken with its 15-token cap, setFactory, setVaultFactory, registerVault, setConversionKeeper, fee terms). Deploy a collection that skips a check, take an address someone else computed, change terms for collections already launched, or reach a role without being owner.\n6. DEPLOY (script and config/1.json). Every address, decimals, minimum, swap target and pool key (INFER/IMD fee 12500, spacing 60, PoolInitializationGuard hook; IMD/ETH 10000/200) matches mainnet; after the script every owner is the Safe and nothing is left with the deployer; _check and _checkPayments catch what they claim to; no step can be front-run or leave a usable but wrong system after a partial run.\n7. END TO END. One sale in each of ETH, USDC (6), WBTC (8), imdUSD and INFER from mint through distribute, the vault contribution, a conversion and a claim: value in equals value out at every step (artist, treasury, vault, pending, shares). And any third party able, for less than the harm, to stop sales, distributions, conversions, claims or withdrawals for others.\n\nNot findings: anything AUDIT-NOTES.md lists as accepted or not fixed by decision (say so if you find it worse than written), including an issuer blocklist (USDC) on the vault making that token's splits revert; src/pools, src/markets and src/gating unless a question names them; test/ and its mocks; gas optimisations unless they change behaviour.\n\nFor every finding: severity by impact; file, line and the lines copied; the call sequence from an external caller; a concrete failing input or state with expected against actual; whether it is reachable with script/config/1.json; the smallest fix; and, where it can be written, a Foundry test that fails on this commit, written out in the finding (you write no files; the judge reproduces it). Reuse the harnesses in test/vaults/VaultTestBase.sol, test/helpers/TestableUniAlignmentVault.sol and test/factories/payments/PaymentMocks.sol. Answer every numbered question, including those where nothing is wrong. Give a coverage row (holds, finding or unreached, with a reason) for every external function of the contracts in scope. Also report every comment or NatSpec that claims a property the code does not have.","parentJobId":null,"planHash":"557229f6f43e3e1ad39be248945f462e528aee0da05200298b229d52f634ee47","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"c89f023c-dd16-4136-8f86-9ac12f7712fe","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":"52604","feedbackHash":"cc61d1bfd8c6630a4badc0da40a476476469f97e39cbb4cc3fea521259f0687f","nodeKey":"audit_economics","submissionHash":"156d712d8c0e9c219fc7fee4d8b49535c5e70c664faedf5c12049c891720d606","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51510","feedbackHash":"c2a6594901d0bedfb1161ab2087f56aa2442849a01eaa04564ad31eac5dabd24","nodeKey":"audit_flow","submissionHash":"561b14903db54203657ffeea2e3fff7f036175488600174c92bb3e65188067d0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51082","feedbackHash":"a34bc6d6888b3b561d40c65f39a8d282fa94a67ff3680861e86191943da00387","nodeKey":"audit_judge","submissionHash":"705181a3af8cbf8b1d6c202bced76099f42e23140f8fb36784021931329e3163","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52128","feedbackHash":"91ef76a004129089b3c7161290ef100d79afc21ec9d2df1ea646df80176e7110","nodeKey":"audit_math","submissionHash":"66ed811b1304dc9eff1789bc8d596779a7db39d21c19d4867c4bc5eef4f62d26","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52139","feedbackHash":"2caca0cc3c950f9702c4649f625d70295487d13991fc9768faf5cb26a11c75a6","nodeKey":"audit_permissions","submissionHash":"911c0e0d75edaee7b926962586e37961a47bbab64618d4b424d05f8a8c0a270e","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"89b5c6c0514461682f3204880fcb14a7f240dd8bfee33b0f13f46ef2cdfebac4","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9e17b97b8ac7c97a","findings":[{"citation":"resolved","description":"getCurrentPrice (the 'price for the next mint' view) returns DynamicPricingModule.calculatePrice = basePrice.mulWad(rpow(r, minted)), which rounds down. mint() charges calculateMintCost -> calculateBatchCost, which rounds up twice: fullMulDivUp(fullMulDivUp(base, rStart, r-1e18), rAmount-1e18, 1e18), with a floor of base*amount. For amount = 1 the charge can be up to 2 units above the view. ERC-20 mints require totalCost <= maxCost, so a buyer or frontend that sets maxCost = getCurrentPrice reverts ExceedsMaxCost. No funds are at risk and the buyer never pays below the curve. The view is simply not the price, as its NatSpec claims.","line":401,"path":"src/factories/erc1155/ERC1155Instance.sol","reproduction":"Edition is LIMITED_DYNAMIC, priceIncreaseRate = 1 (bp), base price 3 units of an accepted ERC-20, minted = 1. getCurrentPrice: mulWad(3, 1.0001e18) = floor(3.0003) = 3. calculateMintCost(id, 1, token): inner = fullMulDivUp(3, 1.0001e18, 1e14) = 30003; outer = fullMulDivUp(30003, 1e14, 1e18) = ceil(3.0003) = 4; least = 3, so the cost is 4. mint(id, 1, 0, \"\", token, maxCost = 3): expected success at the advertised price 3; actual revert ExceedsMaxCost (cost 4 > 3). Reachable with script/config/1.json (the DynamicPricingModule is the 1155 factory default). Fix: make getCurrentPrice return calculateBatchCost(base, rate, minted, 1), the same function mint charges, or document it as the floor-rounded curve point and point frontends at calculateMintCost.","severity":"low","snippet":"            return dynamicPricingModule.calculatePrice(basePrice, edition.priceIncreaseRate, edition.minted);","title":"ERC1155Instance.getCurrentPrice under-reports what a one-piece LIMITED_DYNAMIC mint charges (it rounds down, the charge rounds up twice), so an ERC-20 mint using it as maxCost reverts"},{"citation":"resolved","description":"Collections do snapshot their treasury and bps (they are immutables in the instances), so that half holds. Vaults do not. UniAlignmentVault.protocolTreasury() and withdrawProtocolFees read registry.treasury() live (UniAlignmentVault.sol:862, 1015). So a later setTreasury redirects the 1% LP-yield cut of every deployed vault, including cuts already accrued. setConversionKeeper picks the only address that can sell every vault's pending batches at its own floors (AUDIT-NOTES M2 keeper trust). Both are intended owner powers (a Safe). The comment understates them, so a reader relying on it misjudges the trust model.","line":16,"path":"src/registry/MiyaRegistry.sol","reproduction":"Deploy with config/1.json. A collection contributes and the keeper converts, so the vault accrues _protocolFees. The Safe calls registry.setTreasury(X). Then vault.protocolTreasury() == X and withdrawProtocolFees(IMD) pays the accrued cut to X. Expected per the comment: no effect on deployed vaults. Actual: their protocol fee recipient changes. Fix: correct the NatSpec to list the live vault dependencies (treasury for the vault protocol cut, conversion keeper, payment-token allowlist for contributions).","severity":"info","snippet":" *         It cannot touch deployed collections, their fee terms, their vaults, or any pool. Collections snapshot","title":"MiyaRegistry NatSpec says the owner cannot touch deployed vaults, but setTreasury and setConversionKeeper act live on every deployed vault"},{"citation":"resolved","description":"The comment continues 'only stops new sales and contributions in it; balances already held stay withdrawable'. ownerOwed does stay withdrawable. But a collection's unsplit proceeds and parked vaultOwed in the dropped token can only leave through UniAlignmentVault.receiveContribution, which reverts TokenNotAllowed for a token off the allowlist (UniAlignmentVault.sol:295). distribute() re-parks the cut, and flushPendingVaultCut reverts. Nothing else (sweepUntracked counts only the surplus; withdraw pays ownerOwed only) can move vaultOwed, so the alignment share sits in the collection until the Safe re-lists the token. AUDIT-NOTES describes the park on refusal. This entry reports only that the registry comment overstates withdrawability.","line":14,"path":"src/registry/MiyaRegistry.sol","reproduction":"A drop accepts USDC and sells 100 USDC; distribute has not been called. The Safe calls setPaymentToken(USDC, false). distribute(USDC): protocol 1 USDC is paid, the vault call reverts TokenNotAllowed, and pendingVaultCut(USDC) = 10 USDC (alignment 10%). flushPendingVaultCut(USDC, 0) reverts. Expected per the comment: all held balances withdrawable. Actual: 10 USDC is not movable by anyone until setPaymentToken(USDC, true). Fix: reword the NatSpec (vault cuts in a dropped token wait for re-listing), or let the vault accept contributions in a delisted token that still has pending.","severity":"info","snippet":" *         - allow and drop payment tokens (ERC-20s collections may price in; ETH is always allowed). Dropping a token","title":"MiyaRegistry NatSpec: dropping a payment token does not leave all balances withdrawable; parked and future vault cuts in it are stranded until it is re-listed"},{"citation":"resolved","description":"PaymentLib.sol:24-25 documents ownerOwed as 'the artist's share, paid only by the owner's withdraw (pull: a blocklisted artist never blocks a sale, a split or anyone's refund)'. In an ETH auction, settleAuction instead pushes the deposit (forceSafeTransferETH) and the remainder (smartTransferETH, forwarding all gas, with a WETH fallback) to owner(), and ownerOwed[ETH] is never credited. The WETH fallback means the owner can't make settlement revert if the settler sends enough gas: the 63/64 rule leaves the settler at least gas/64 for the fallback. So this is a documentation mismatch, not a lock. withdraw(ETH) on an ETH auction always reverts NothingToWithdraw unless sweepUntracked credited something.","line":397,"path":"src/factories/erc721/ERC721AuctionInstance.sol","reproduction":"ETH auction: queuePiece(uri, 1 ether), bid 2 ether, warp past the end, settleAuction. Expected per PaymentLib NatSpec: ownerBalance(address(0)) == 2 ether - cuts + 1 ether, paid on withdraw. Actual: ownerBalance(address(0)) == 0 and the owner received 1 ether + 1.78 ether directly (alignment 10%, protocol 1%). Fix: correct the NatSpec (ETH auctions push to the owner) or credit ownerOwed[ETH] as the ERC-20 branch does.","severity":"info","snippet":"            SmartTransferLib.smartTransferETH(owner(), s.remainder, weth);","title":"PaymentLib NatSpec says the artist's share is pull-only, but ETH auctions push the remainder and the deposit to owner() at settlement"}],"hash":"156d712d8c0e9c219fc7fee4d8b49535c5e70c664faedf5c12049c891720d606","nodeId":"4391cd5b-89a1-4271-8ec3-41563f7e7ea0","outcome":"completed","summary":"I found no critical, high or medium defects in the scope. `.imd-findings.json` holds four findings: one low and three info. Each has a concrete failing state, and each snippet matches its file and line. None is critical or high, so none carries a Foundry proof. I didn't run `forge build` or `forge test`, given the time and the cost of a via_ir build. The conclusions below come from reading the code and doing exact arithmetic by hand. I checked mainnet addresses with `cast` against a public RPC.\n\n## Findings\n\n1. **Low: the 1155 price view under-reports the dynamic mint price** (`ERC1155Instance.sol:401`). `getCurrentPrice` rounds down (`mulWad`), but `mint` charges `calculateBatchCost`, which rounds up twice. Example: base price 3, rate 1 bp, minted = 1. The view says 3; the mint charges 4. An ERC-20 mint with `maxCost = getCurrentPrice` therefore reverts `ExceedsMaxCost`. No money is lost and nobody mints below the curve. Fix: have the view return the batch cost for one piece.\n2. **Info: a registry comment understates owner power** (`MiyaRegistry.sol:16`). It says the owner \"cannot touch … their vaults\". In fact `setTreasury` redirects every deployed vault's protocol cut, because the vault reads `registry.treasury()` live, including cuts already accrued. `setConversionKeeper` also picks who sells every vault's batches.\n3. **Info: delisting a token strands vault cuts in it** (`MiyaRegistry.sol:14`). The comment says \"balances already held stay withdrawable\". That's true for the artist's balance. A parked or unsplit vault cut in a delisted token can't move at all until the token is re-listed, because the vault refuses it (`TokenNotAllowed`). Example: 100 USDC sold, USDC delisted, `distribute` leaves 10 USDC parked, and `flushPendingVaultCut` reverts.\n4. **Info: ETH auctions don't follow the documented pull model** (`ERC721AuctionInstance.sol:397`). PaymentLib says the artist's share is paid only by the owner's `withdraw`. ETH auctions push the deposit and remainder at settlement instead. That can't lock settlement: the WETH fallback works as long as the settler sends enough gas.\n\n## Answers to the seven questions\n\n1. **Per-token books: hold.**\n   - Each amount owed sits in exactly one bucket. `liabilities()` sums `unsplit`, `ownerOwed`, `protocolOwed`, `vaultOwed` and `escrow`. `split` zeroes `unsplit` before booking.\n   - `payProtocol` and `sendOrPark` zero a parked cut, retry it, and re-park the total on failure. `flushVault` zeroes the parked cut before the external call, and a refusal reverts the whole call, returning the top-up.\n   - `sweepUntracked` adds only the balance above `liabilities`.\n   - `withdrawAll` calls `this.withdrawAllStep` inside try/catch, so a failing token rolls back only its own step and emits `WithdrawSkipped`. The step itself is callable only by the contract.\n   - Auction escrow stays consistent: an ETH outbid moves `escrow` by +new −old, an ERC-20 outbid by +new with the old bid kept as a refund owed, and `claimRefund` and `settle` subtract exactly what they pay.\n   - The vault-fee tokens (INFER, IMD and ETH) are forwarded in the same call they arrive in, so they never inflate the surplus.\n2. **Payment intake: holds, apart from finding 1.**\n   - ETH: the Drop requires `msg.value == cost`. The 1155 accepts `msg.value >= cost` and refunds the excess. An auction requires `msg.value == amount`.\n   - ERC-20: `msg.value` must be 0, `maxCost` is required, and `collect` checks the exact balance change, so a fee-on-transfer token reverts `ShortTransfer`.\n   - A token the sale doesn't price reverts `TokenNotPriced`; a token not accepted reverts in `checkUsable`.\n   - Fixed prices are `price * qty`, exact at any decimals. The dynamic curve rounds up with a floor of `base × amount`, and all tokens share one position on it (`edition.minted`).\n3. **Auctions: hold.**\n   - The winner's mint gets a fixed 500k gas and settlement requires about 916k gas left. A winner contract that reverts or burns gas is escrowed, not","treeHash":null,"usage":{"cachedInputTokens":1791866,"cost":{"source":"runtime","usdMicros":2023344},"inputTokens":41,"model":"claude-opus-5-5","outputTokens":32843,"runtime":"claude","turns":24,"wallClockMs":349247}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8d428b115b0ebd64","findings":[{"citation":"resolved","description":"DynamicPricingModule.calculatePrice (behind ERC1155Instance.getCurrentPrice, documented as 'Current price for the next mint') computes basePrice*r^minted with mulWad, which rounds DOWN. calculateBatchCost (behind calculateMintCost, what mint() actually charges) rounds UP twice (fullMulDivUp). For the same edition state the single-piece quote is therefore up to 1 unit below the charge, and at tiny base prices the gap is large relative to the price (fuzz: basePrice 2, rate 3 bps, minted 8 -> quote 2, charge 3). A wallet or UI that quotes getCurrentPrice and sends exactly that as msg.value (or as maxCost for an ERC-20) has its mint reverted with InsufficientPayment / ExceedsMaxCost. No funds at risk; it is a correctness/NatSpec mismatch between the two pure functions that the instance exposes as the same number. Reachable with script/config/1.json (DynamicPricingModule is deployed and set as the 1155 factory default). Smallest fix: make calculatePrice round the same way as the batch cost, e.g. return FixedPointMathLib.fullMulDivUp(basePrice, result, 1e18) (or define getCurrentPrice as calculateMintCost(editionId, 1, token)).","line":44,"path":"src/factories/erc1155/DynamicPricingModule.sol","reproduction":"Module alone: new DynamicPricingModule(); calculatePrice(1e6, 33, 3) == 1009932 but calculateBatchCost(1e6, 33, 3, 1) == 1009933. Expected: equal. Instance: ERC1155 edition LIMITED_DYNAMIC with ETH base price 1e15+1, rate 33 bps; alice mints 3; getCurrentPrice(1, ETH) = 1009932705937001, calculateMintCost(1,1,ETH) = 1009932705937002; bob calls mint{value: getCurrentPrice}(1, 1, 0, \"\", ETH, 0) -> reverts InsufficientPayment(). Expected: the mint at the quoted current price succeeds. Verified with test/scratch/Review.t.sol (DynamicQuoteMismatch.test_quoteBelowCharge, ReviewInstances.test_A_mintAtQuotedCurrentPriceReverts) on this commit.","severity":"low","snippet":"        uint256 currentPrice = basePrice.mulWad(result);","title":"LIMITED_DYNAMIC: getCurrentPrice rounds down while the mint charge rounds up, so paying the published next-mint price is refused"},{"citation":"resolved","description":"After MiyaRegistry.setPaymentToken(token, false), UniAlignmentVault.receiveContribution reverts TokenNotAllowed for that token. In a collection that still holds unsplit proceeds in it, distribute(token) pays the 1% to the treasury and the artist share to ownerOwed, but the alignment share (>= 10%) is parked in vaultOwed (PaymentLib.sendOrPark). From then on nothing can move it: flushPendingVaultCut reverts (vault refuses), the owner's withdraw/withdrawAll only pay ownerOwed, sweepUntracked excludes it (it is a liability), and no other path reads vaultOwed. The same happens to a cut already parked before the delisting. The registry NatSpec quoted above and the drop/1155 setAcceptedTokens NatSpec ('Balances already held in a removed token stay withdrawable') claim otherwise. Impact: funds locked in the collection until the owner relists the token (which the owner may not want to do), proportional to alignmentBps of every un-distributed sale plus any parked residue, across all collections selling in that token. Reachable with script/config/1.json (any of the six listed ERC-20s can be dropped by the Safe). Smallest fix: let a parked cut in a token the registry no longer allows be released, e.g. flushPendingVaultCut falls back to crediting ownerOwed (or to paying the treasury) when registry.isPaymentToken(token) is false; or document the lock and change the NatSpec.","line":15,"path":"src/registry/MiyaRegistry.sol","reproduction":"Drop accepting USDC (phase price 100e6, alignmentBps 1900 with a 1% protocol fee); alice mints 1 (100 USDC held, unsplit). Then registry.setPaymentToken(USDC, false) and the vault refuses (mock: setRejecting(true), as the real vault does for a delisted token). distribute(USDC): pendingVaultCut(USDC) == 19e6. flushPendingVaultCut(USDC, 0) reverts. artist withdrawAll() pays 80e6 to the artist; drop still holds 19e6 and pendingVaultCut stays 19e6 with no function able to reduce it. Expected per NatSpec: the balance stays withdrawable (by someone). Verified with test/scratch/Review.t.sol ReviewInstances.test_B_delistedTokenStrandsParkedVaultCut on this commit.","severity":"low","snippet":" *           only stops new sales and contributions in it; balances already held stay withdrawable.","title":"Dropping a payment token from the registry strands that token's parked vault cut in every collection (NatSpec says balances held stay withdrawable)"},{"citation":"resolved","description":"ERC721AuctionInstance has a payable receive() (line 620, 'Allow receiving ETH (for vault refunds etc.)') but, unlike the drop and 1155 instances, no setAcceptedTokens: _pay.known holds only the creation currency. For an ERC-20 auction house every ETH path is closed: sweepUntracked(address(0)) reverts TokenNotAccepted, withdraw(address(0)) / withdrawAll() pay only ownerOwed (0 for ETH), flushPending*Cut(address(0)) revert NothingParked, and claimVaultFees forwards only the balance delta of that one call. Any ETH that reaches the house outside a vault claim (marketplace royalty payouts to the collection address, a buyer sending ETH by mistake) is irrecoverable by anyone. The drop/1155 owner can recover the same situation by adding ETH to acceptedTokens; the auction owner cannot. Reachable with script/config/1.json (any ERC-20 currency auction). Smallest fix: in sweepUntracked accept ETH as well as the currency (ETH is always allowed by the registry) and credit it through the split like any untracked balance, or make receive() revert when currency != ETH and the transient vault-claim flag is not set.","line":506,"path":"src/factories/erc721/ERC721AuctionInstance.sol","reproduction":"createInstance with currency = USDC. alice sends 1 ether to the instance (plain call, succeeds). Then: sweepUntracked(address(0)) reverts TokenNotAccepted(0x0); artist withdraw(address(0)) reverts NothingToWithdraw; flushPendingProtocolCut(address(0)) reverts NothingParked; address(instance).balance stays 1 ether with no external function able to reduce it. Expected: the ETH is sweepable into the split (as on the drop/1155 once ETH is accepted) or refused at receive(). Verified with test/scratch/Auction.t.sol AuctionStrayEth.test_ethSentToErc20AuctionIsLockedForever on this commit (every expectRevert holds; the final 'balance == 0' assertion is what fails).","severity":"low","snippet":"        if (!_pay.isKnown[token]) revert PaymentLib.TokenNotAccepted(token);","title":"ERC-20 auction house: ETH sent to it (royalty routers, mistaken transfers) is locked forever; sweepUntracked only knows the one currency"},{"citation":"resolved","description":"NatSpec-only. The EIP-170 runtime cap is 24,576 bytes; with SSTORE2's 1-byte STOP prefix the largest data contract is 24,575 bytes, not 24,000. The code is safe (smaller chunks are fine, two chunks cover 48,000 bytes and the InitCodeTooLarge check matches), but the comment claims a property the constant does not have. Also IDynamicPricingModule.sol:13/23 still document basePrice as 'Starting price in wei' although prices are now per payment token in that token's units (USDC 6 decimals, BTC 8).","line":12,"path":"src/factories/shared/InitCodeStore.sol","reproduction":"Read-only: CHUNK = 24_000 vs 24_576 - 1 = 24_575. Fix the comment (or set CHUNK = 24_575).","severity":"info","snippet":"    /// @dev Each data contract holds at most this much (one byte under the runtime cap, for SSTORE2's STOP prefix).","title":"InitCodeStore comment misstates the chunk size ('one byte under the runtime cap'); CHUNK is 24,000, 576 bytes under"}],"hash":"561b14903db54203657ffeea2e3fff7f036175488600174c92bb3e65188067d0","nodeId":"59a30915-aeb6-4264-a988-b01a24c6a65d","outcome":"completed","summary":"Review complete. Four findings are in `.imd-findings.json` (three low, one info), each verified with a failing Foundry test under `test/scratch/` on this commit. No critical or high defect was found in the assigned scope.\n\n## Findings\n\n- **Low. Dynamic quote below the charge.** `DynamicPricingModule.calculatePrice` rounds down, `calculateBatchCost` rounds up. For a USDC-style base price of 1e6, rate 33 bps, 3 minted, the next-mint quote is 1009932 and the single-mint charge is 1009933. A buyer sending exactly `getCurrentPrice` as msg.value gets `InsufficientPayment`. At tiny base prices the gap is large (fuzz: base 2, rate 3, minted 8 gives quote 2, charge 3). Fix: round `calculatePrice` up the same way.\n- **Low. Delisting a payment token strands parked vault cuts.** After `setPaymentToken(token,false)` the vault refuses the token, so `distribute` parks the alignment share and nothing can move it afterwards. `flushPendingVaultCut` reverts, `withdrawAll` pays only the artist share, `sweepUntracked` excludes it. The registry NatSpec and both `setAcceptedTokens` NatSpecs say \"balances already held stay withdrawable\". Repro: 100 USDC drop sale, delist, distribute, 19 USDC locked.\n- **Low. ETH sent to an ERC-20 auction house is locked forever.** The auction has a payable `receive()` but its `known` list holds only its one currency and it has no `setAcceptedTokens`. `sweepUntracked(0)`, `withdraw(0)` and both flushes revert. Drops and 1155s can recover the same case by adding ETH to accepted tokens; auctions cannot. Royalty routers paying the collection address will hit this.\n- **Info. NatSpec.** `InitCodeStore` says the chunk is \"one byte under the runtime cap\"; it is 24,000, not 24,575. `IDynamicPricingModule` still documents prices \"in wei\" though they are per-token units.\n\n## Answers to the numbered questions\n\n**1. Per-token books. Holds.** `liabilities = unsplit + ownerOwed + protocolOwed + vaultOwed + escrow`, and every path keeps balance >= liabilities: `collect` checks the exact delta before booking, `split` moves `unsplit` into the three legs in one step, parked cuts stay in the contract, `payOwner` zeroes before paying, `flushVault` zeroes `vaultOwed` and reverts whole if the vault refuses. `sweepUntracked` is `balance - liabilities`, so nothing is counted twice; the auction splits it directly and the drop/1155 add it to `unsplit`. `withdrawAll` wraps each token in `try this.withdrawAllStep` with a self-sender check, so one failing token is skipped and emits `WithdrawSkipped`. The only lock is the delisting case above.\n\n**2. Payment intake. Holds.** Drop ETH requires `msg.value == cost`; 1155 ETH refunds excess with WETH fallback; auctions require `msg.value == amount`. All ERC-20 paths require `msg.value == 0`, `cost <= maxCost`, and pull exactly `cost` with a balance-delta check, so the fee-on-transfer mock is refused. An unpriced token reverts `TokenNotPriced`; a token removed from `accepted` or the registry reverts in `checkUsable`. The dynamic batch cost rounds up twice and floors at `basePrice * amount`; the only gap is the 1-unit quote mismatch above. Very large rates or supplies make `rpow` overflow and revert, bricking that edition, but only the owner can configure that.\n\n**3. Auctions. Holds.** One currency fixed at creation. ETH refunds use `forceSafeTransferETH` with a 100k stipend and selfdestruct fallback, so a reverting bidder cannot block a higher bid. ERC-20 refunds are pull via `refundOf` and `claimRefund(to)`, and escrow is reduced only on claim. Settlement sets `settled` first, caps the winner mint at 500k gas with a floor check so a settler cannot force escrow, and falls back to `escrowedWinner`. `escrow -= minBid + highBid` matches exactly what was booked. Anti-snipe is uncapped by design, as accepted. No path takes escrow: `sweepUntracked` subtracts it, `withdraw` pays only `ownerOwed`.\n\n**4. Vault and treasury calls. Holds.** ERC-20 cuts use `safeApproveWithRetry(exact)`, the vault pulls exactly tha","treeHash":null,"usage":{"cachedInputTokens":2026382,"cost":{"source":"runtime","usdMicros":6380504},"inputTokens":450,"model":"claude-fable-5-1","outputTokens":46996,"runtime":"claude","turns":31,"wallClockMs":722919}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9c6767b941fcfedc","findings":[{"citation":"resolved","description":"ERC1155Instance.getCurrentPrice (NatSpec: 'Current price for the next mint') returns DynamicPricingModule.calculatePrice, which rounds basePrice*r^minted DOWN (mulWad). ERC1155Instance.mint charges calculateMintCost -> calculateBatchCost(basePrice, rate, minted, 1), which since the 'Dynamic pricing rounds up' fix uses fullMulDivUp twice and so rounds the same quantity UP. Whenever basePrice*r^minted is not a multiple of 1e18 the two differ by one unit (and the inner ceil over r-1e18 can add more for large rates). The view therefore under-reports the price a mint will charge, and ERC1155Instance.mint reverts ExceedsMaxCost (line 482: `if (totalCost > maxCost) revert ExceedsMaxCost();`) for any ERC-20 buyer who passes the quoted price as maxCost, and InsufficientPayment for an ETH buyer who sends exactly the quote. Reachable with script/config/1.json (DynamicPricingModule is deployed and set as the 1155 factory default). Impact is a 1-unit quote error and failed mints for integrators that use getCurrentPrice; no funds at risk. Smallest fix: make calculatePrice use the same rounding as the batch formula (e.g. return calculateBatchCost(basePrice, rate, minted, 1), or mulWadUp), or document that calculateMintCost is the price to quote.","line":44,"path":"src/factories/erc1155/DynamicPricingModule.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\nimport { Test } from \"forge-std/Test.sol\";\nimport { DynamicPricingModule } from \"src/factories/erc1155/DynamicPricingModule.sol\";\n\ncontract DynamicPriceViewVsChargeTest is Test {\n    DynamicPricingModule module;\n\n    function setUp() public {\n        module = new DynamicPricingModule();\n    }\n\n    /// The quoted next-mint price (calculatePrice, rounds down) must equal what mint charges for one piece\n    /// (calculateBatchCost(.., 1), rounds up). On this commit they differ by one unit.\n    function test_viewEqualsChargeForOne() public view {\n        uint256 shown = module.calculatePrice(1e6, 100, 7);\n        uint256 charged = module.calculateBatchCost(1e6, 100, 7, 1);\n        assertEq(shown, charged, \"getCurrentPrice != cost of minting one\");\n    }\n\n    function test_viewEqualsCharge_first200() public view {\n        for (uint256 m; m < 200; ++m) {\n            assertEq(module.calculatePrice(1e6, 100, m), module.calculateBatchCost(1e6, 100, m, 1));\n        }\n    }\n}","reproduction":"DynamicPricingModule.calculatePrice(1e6, 100, 7) = 1_072_135 but calculateBatchCost(1e6, 100, 7, 1) = 1_072_136 (expected equal). Across minted = 0..199 of a 1%-curve USDC edition (basePrice 1e6) the two disagree 196 times. End to end: create an ERC1155Instance accepting USDC, addEdition(LIMITED_DYNAMIC, price 1e6 USDC, rate 100, supply 1000); alice mints 7; quoted = getCurrentPrice(1, USDC) = 1_072_135; alice calls mint(1, 1, 0, '', USDC, quoted) -> reverts ExceedsMaxCost(); expected: the mint succeeds at the quoted price (or the view returns 1_072_136).","severity":"low","snippet":"        uint256 currentPrice = basePrice.mulWad(result);","title":"Dynamic edition: getCurrentPrice rounds down while mint charges calculateBatchCost rounded up, so the quoted next-mint price is 1 unit under the charge and a maxCost taken from the view is refused"},{"citation":"resolved","description":"The comment claims each SSTORE2 chunk is sized one byte under the 24,576-byte runtime cap to leave room for SSTORE2's STOP prefix. The constant is 24,000, so a data contract is 24,001 bytes and 575 bytes of capacity per chunk are unused; the two-chunk ceiling InitCodeTooLarge enforces is 48,000 bytes of instance creation code, not 49,150. No behavioural defect: every instance's creation code fits today (the factories deploy in the test suites). Reported because the task asks for comments that claim a property the code does not have. Smallest fix: either set CHUNK = 24_575 or reword the comment to state the actual margin.","line":12,"path":"src/factories/shared/InitCodeStore.sol","reproduction":"Read the constant: CHUNK = 24_000. 24_576 - 1 = 24_575 != 24_000. InitCodeStore.write(code) with code.length = 48_001 reverts InitCodeTooLarge although two SSTORE2 contracts of 24_575 bytes each could hold up to 49_150 bytes; expected (per the comment) the cap is 2 x 24_575, actual 2 x 24_000.","severity":"info","snippet":"    /// @dev Each data contract holds at most this much (one byte under the runtime cap, for SSTORE2's STOP prefix).\n    uint256 private constant CHUNK = 24_000;","title":"InitCodeStore NatSpec: CHUNK is described as 'one byte under the runtime cap' but is 24,000, 576 bytes under EIP-170's 24,576"}],"hash":"66ed811b1304dc9eff1789bc8d596779a7db39d21c19d4867c4bc5eef4f62d26","nodeId":"0db9e2cf-eecd-4e85-8f59-f82a1c575830","outcome":"completed","summary":"Review complete. The findings file holds two substantiated items (one low, one info); the proof test for the low finding fails on this commit exactly as written. Everything else in the assigned scope held under the Math Precision, Boundary and Numerical Gap passes.\n\n## Findings\n\n**Low. Dynamic-edition quote is 1 unit under the charge.** `DynamicPricingModule.calculatePrice` (used by `ERC1155Instance.getCurrentPrice`, NatSpec \"Current price for the next mint\") rounds down with `mulWad`, while `mint` charges `calculateBatchCost(..., 1)`, which rounds up twice. For a 1 USDC base, 1% curve, after 7 mints the view says 1,072,135 and the mint charges 1,072,136. Across the first 200 mints they disagree 196 times. A buyer who passes the quote as `maxCost` is refused with `ExceedsMaxCost`. Reachable with the mainnet config. Fix: make `calculatePrice` return `calculateBatchCost(base, rate, minted, 1)` or round up. Proof included; it fails on this commit.\n\n**Info. InitCodeStore comment is wrong.** `CHUNK = 24_000` is described as \"one byte under the runtime cap\"; it is 576 bytes under. No behavioural effect.\n\n## Answers to the numbered questions\n\n1. **Per-token books: holds.** For every path (mint, distribute, withdraw, withdrawAllStep, flushPendingProtocolCut, flushPendingVaultCut with top-up, sweepUntracked, claimVaultFees) balance held stays ≥ unsplit + ownerOwed + protocolOwed + vaultOwed + escrow. Parked amounts are zeroed before the retry and restored only on failure. `sweepUntracked` counts only balance minus liabilities. `withdrawAll` self-calls `withdrawAllStep` in try/catch, so a reverting token is rolled back and skipped. The gas-starvation path I suspected does not exist: by the 63/64 rule a caller cannot make the vault's call fail while leaving enough gas for the fallback writes. Verified against the real vault harness, least succeeding gas 323,974 still delivers the cut.\n2. **Payment intake: holds except the rounding finding.** ERC-20 paths require `msg.value == 0`, pull exactly the cost and check the balance delta, so fee-on-transfer tokens revert `ShortTransfer`. Drop ETH requires exact value; 1155 ETH refunds excess; auction ETH requires exact value. Unpriced tokens revert `TokenNotPriced`; delisted tokens revert `TokenNotAllowed`. Batch cost is floored at base × amount. Drop phases may price 0 (free) by design.\n3. **Auctions: holds.** One currency fixed at construction and checked against the allowlist at every `queuePiece`. ETH refunds use `forceSafeTransferETH`; ERC-20 refunds are pull via `refundOf` and `claimRefund(to)` with escrow decremented once. Settlement caps winner-mint gas and requires a minimum gas so a settler cannot force escrow. Escrow equals deposits + live bids + refunds owed on every path.\n4. **Vault and treasury calls: holds.** Exact approve, reset to 0 on failure; the vault pulls exactly the amount or reverts. The treasury's ETH receive costs about 21k gas on mainnet, inside the 100k stipend (checked over RPC). `claimVaultFees` forwards by balance delta during the vault call only; the vault pays the benefactor itself unless the benefactor set a delegate, which collections never do, so fees cannot land as untracked \"proceeds\" and sale balances cannot be forwarded as fees.\n5. **Factories and registry: holds.** SSTORE2 init code written once in the constructor and read back; CREATE3 salts prefixed with the factory address and hashed with the creator, matching `computeInstanceAddress`; the drop factory uses plain CREATE. Vault, alignment share and payment tokens are checked by factory and constructor. Payment-token cap of 15 enforced on add. Launched collections snapshot treasury and fee terms as immutables. All setters are owner-only; `registerVault` and `registerInstance` are restricted to registered factories.\n6. **Deploy: holds.** Verified over a public RPC today: every address in `1.json` has code; INFER, IMD, imdUSD (18 decimals) and cbBTC (8) report the expected symbols; the INFER/IMD pool (fee 1","treeHash":null,"usage":{"cachedInputTokens":2393077,"cost":{"source":"runtime","usdMicros":6490638},"inputTokens":546,"model":"claude-fable-5-1","outputTokens":44045,"runtime":"claude","turns":40,"wallClockMs":720106}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f47f953f8e35921e","findings":[{"citation":"resolved","description":"Merged from four specialist reports (permissions, flow, economics, math). ERC1155Instance.getCurrentPrice (NatSpec at ERC1155Instance.sol:390: '@return price Current price for the next mint') returns DynamicPricingModule.calculatePrice = basePrice.mulWad(r^minted), which rounds DOWN. ERC1155Instance.mint charges calculateMintCost -> DynamicPricingModule.calculateBatchCost(base, rate, minted, 1), which since the 'Dynamic pricing rounds up' fix uses fullMulDivUp twice (lines 63-65). Whenever basePrice*r^minted is not an exact WAD multiple the two differ by one unit (the inner ceil over r-1e18 can add a second unit at larger rates). An ERC-20 buyer who passes the quote as maxCost reverts ExceedsMaxCost (ERC1155Instance.sol:482); an ETH buyer who sends exactly the quote reverts InsufficientPayment (line 479). No funds at risk and the buyer never pays below the curve; it is a wrong view that integrators are told to use. Reachable with script/config/1.json: DynamicPricingModule is deployed and set as the 1155 factory default. Related NatSpec: IDynamicPricingModule.sol:13 and :23 still describe basePrice as 'Starting price in wei' although prices are per payment token in that token's units. Smallest fix: make calculatePrice round the same way as the batch formula (return FixedPointMathLib.mulWadUp(basePrice, result), or have getCurrentPrice return calculateMintCost(editionId, 1, token)) and fix the 'wei' wording.","line":44,"path":"src/factories/erc1155/DynamicPricingModule.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\nimport { Test } from \"forge-std/Test.sol\";\nimport { DynamicPricingModule } from \"src/factories/erc1155/DynamicPricingModule.sol\";\n\ncontract DynamicPriceViewVsChargeTest is Test {\n    DynamicPricingModule module;\n\n    function setUp() public {\n        module = new DynamicPricingModule();\n    }\n\n    /// The quoted next-mint price (calculatePrice, rounds down) must equal what mint charges for one piece\n    /// (calculateBatchCost(.., 1), rounds up). On this commit they differ by one unit.\n    function test_viewEqualsChargeForOne() public view {\n        uint256 shown = module.calculatePrice(1e6, 100, 7);\n        uint256 charged = module.calculateBatchCost(1e6, 100, 7, 1);\n        assertEq(shown, charged, \"getCurrentPrice != cost of minting one\");\n    }\n\n    function test_viewEqualsCharge_first200() public view {\n        for (uint256 m; m < 200; ++m) {\n            assertEq(module.calculatePrice(1e6, 100, m), module.calculateBatchCost(1e6, 100, m, 1));\n        }\n    }\n}","reproduction":"Module alone: new DynamicPricingModule(); calculatePrice(1e6, 100, 7) == 1_072_135 but calculateBatchCost(1e6, 100, 7, 1) == 1_072_136; calculatePrice(3, 1, 1) == 3 but calculateBatchCost(3, 1, 1, 1) == 4. Expected: equal. Across minted = 0..199 of a 1 percent USDC curve (base 1e6) the first disagreement is at minted = 1. Instance level (test/scratch/Judge.t.sol test_F1_mintAtQuotedPriceReverts_usdc, run on this commit): ERC1155Factory with the module as default; edition LIMITED_DYNAMIC, prices USDC 1e6 and ETH 1e15+1, rate 100 bps, supply 1000; alice mints 7 in USDC; getCurrentPrice(1, USDC) = 1_072_135, calculateMintCost(1, 1, USDC) = 1_072_136; alice calls mint(1, 1, 0, '', USDC, maxCost = 1_072_135) -> reverts ExceedsMaxCost. bob calls mint{value: getCurrentPrice(1, ETH)}(1, 1, 0, '', ETH, 0) -> reverts InsufficientPayment. Expected: both mints succeed at the quoted price. The attached proof (the math specialist's, re-run here) fails on this commit with '1072135 != 1072136'.","severity":"low","snippet":"        uint256 currentPrice = basePrice.mulWad(result);","title":"LIMITED_DYNAMIC: getCurrentPrice rounds the curve down while mint charges it rounded up, so a mint at the quoted next-mint price is refused"},{"citation":"resolved","description":"Merged from the flow (low) and economics (info) reports. After MiyaRegistry.setPaymentToken(token, false), UniAlignmentVault.receiveContribution reverts TokenNotAllowed(token) (UniAlignmentVault.sol:295). In a drop, 1155 or ERC-20 auction that still holds unsplit proceeds in that token, distribute(token) / settleAuction pays the 1 percent to the treasury and credits the artist's share to ownerOwed, but the alignment share (at least 10 percent) is parked in vaultOwed by PaymentLib.sendOrPark (PaymentLib.sol:181-189). From then on nothing can move it: flushPendingVaultCut reverts because the vault still refuses, withdraw / withdrawAll pay only ownerOwed, sweepUntracked excludes it (it is a liability in PaymentLib.liabilities), and no other function reads vaultOwed. A cut already parked before the delisting is stranded the same way. The registry NatSpec (line 14-15), PaymentLib.checkUsable's comment (PaymentLib.sol:113-114) and the drop/1155 setAcceptedTokens NatSpec ('Balances already held in a removed token stay withdrawable') all claim otherwise. AUDIT-NOTES documents parking on refusal and the USDC-blocklist case, not a permanent lock after a delist, so this is worse than written. Impact: alignmentBps of every undistributed sale plus any parked residue, in every collection selling in that token, is locked until the Safe re-lists the token. Reachable with script/config/1.json: any of the six listed ERC-20s can be dropped by the Safe. Smallest fix: let flushPendingVaultCut release a parked cut in a token the registry no longer allows (credit ownerOwed, or pay the treasury), or let the vault accept contributions in a delisted token, or correct the three NatSpec claims.","line":15,"path":"src/registry/MiyaRegistry.sol","reproduction":"test/scratch/Judge.t.sol test_F2_delistedTokenStrandsVaultCut (passes on this commit, i.e. the lock is observed): ERC721DropFactory with the PaymentMocks registry and MockVaultV2; drop accepts USDC, phase price 100e6, alignmentBps 1900, protocol 100. alice mints 1 (unsplit USDC = 100e6). registry.setPaymentToken(USDC, false) and vault.setRejecting(true) (the mock mirrors the real vault's TokenNotAllowed). distribute(USDC): treasury receives 1e6, ownerBalance(USDC) = 80e6, pendingVaultCut(USDC) = 19e6. flushPendingVaultCut(USDC, 0) reverts. artist withdrawAll(): artist receives 80e6; the drop still holds 19e6 and pendingVaultCut(USDC) stays 19e6. sweepUntracked(USDC) reverts NothingToWithdraw and withdraw(USDC) reverts NothingToWithdraw. Expected per the NatSpec: every balance held stays withdrawable by someone. Actual: 19e6 USDC is movable by no function until setPaymentToken(USDC, true).","severity":"low","snippet":" *           only stops new sales and contributions in it; balances already held stay withdrawable.","title":"Dropping a payment token from the registry strands that token's alignment share in every collection (parked vaultOwed has no exit), contrary to the registry and setAcceptedTokens NatSpec"},{"citation":"resolved","description":"From the flow specialist, reproduced. ERC721AuctionInstance has a payable receive() (line 620, 'Allow receiving ETH (for vault refunds etc.)') but, unlike the drop and 1155 instances, no setAcceptedTokens: _pay.known holds only the creation currency (constructor lines 186-188). For an ERC-20 auction house every ETH path is closed: sweepUntracked(address(0)) reverts TokenNotAccepted, withdraw(address(0)) and withdrawAll() pay only ownerOwed (0 for ETH), flushPendingProtocolCut / flushPendingVaultCut(address(0)) revert NothingParked, and claimVaultFees forwards only the balance delta of that one call. Any ETH that reaches the house outside a vault claim (marketplace royalty payouts to the collection address, a mistaken transfer) is irrecoverable by anyone. The drop and 1155 owner can recover the same situation by adding ETH to acceptedTokens; the auction owner cannot. Reachable with script/config/1.json (any ERC-20 currency auction). Smallest fix: in sweepUntracked also accept ETH (always allowed by the registry) and split it like any untracked balance, or make receive() revert when currency != ETH and no vault claim is in progress.","line":506,"path":"src/factories/erc721/ERC721AuctionInstance.sol","reproduction":"test/scratch/Judge.t.sol test_F3_strayEthOnErc20AuctionLocked (passes on this commit, i.e. the lock is observed): ERC721AuctionFactory.createInstance with currency = USDC. alice sends 1 ether by plain call: succeeds, address(instance).balance == 1 ether. sweepUntracked(address(0)) reverts TokenNotAccepted(0x0); artist withdraw(address(0)) reverts NothingToWithdraw; artist withdrawAll() reverts NothingToWithdraw; flushPendingProtocolCut(address(0)) and flushPendingVaultCut(address(0), 0) revert NothingParked; claimVaultFees(TOKENS, 0) with nothing to claim reverts NoFeesToClaim; the balance stays 1 ether. Contrast in the same test: a drop that received 1 ether has its owner add ETH with setAcceptedTokens([USDC, ETH]) and sweepUntracked(ETH) books 1 ether as unsplit proceeds. Expected: the ETH is sweepable into the split or refused at receive(). Actual: locked.","severity":"low","snippet":"        if (!_pay.isKnown[token]) revert PaymentLib.TokenNotAccepted(token);","title":"ERC-20 auction house: ETH sent to it is locked forever; sweepUntracked knows only the one currency and the owner cannot add ETH"},{"citation":"resolved","description":"From the permissions specialist, reproduced offline. OWNER is env-only (loadConfig line 150: vm.envOr('OWNER', address(0))); script/config/1.json has no owner key. When OWNER is unset, _deployAll sets c.owner = deployer. The mainnet guard at line 228 (`if (c.owner != deployer) _requireCode(c.owner, \"OWNER\");`) skips exactly this case, and the handoff at line 319 (`if (c.owner != deployer)`) is skipped. _check compares each owner against the same mutated c.owner (Config memory is passed by reference), so registry.owner, vaultFactory.owner, 1155.owner, auction.owner and dynamicPricing.owner all pass as the deployer EOA; only a console warning in _finish notes it. The brief requires every owner to be the Safe after the script; nothing enforces that on chain 1 or 4663. A partial run has the same effect: forge broadcasts one transaction per call, so if any call between the registry deployment (step 1) and the handoff (step 11) fails, a registry, vault factory and default vault that are registered and usable stay owned by the deployer key, and a re-run with the same SALT_NAMESPACE reverts at the registry's CREATE3 address ('already deployed at this salt'). Reachable with script/config/1.json: yes, whenever OWNER is not exported. Smallest fix: on chainid 1 or 4663 revert in loadConfig unless OWNER is set, differs from the deployer and has code; and in _check assert r.owner() != d.deployer on those chains.","line":224,"path":"script/DeployMiyamints.s.sol","reproduction":"test/scratch/Judge.t.sol JudgeDeploy.test_F5_ownerUnsetLeavesDeployerAsOwner (passes on this commit, i.e. the outcome is observed): chain 31337 with script/config/31337.json and the offline mocks of test/deploy/DeployMiyamintsOffline.t.sol, KEEPER set, OWNER unset; new DeployMiyamints().runAs(deployer, address(0)). The script completes, _check passes, and MiyaRegistry.owner(), UniAlignmentVaultFactory.owner(), ERC1155Factory.owner(), ERC721AuctionFactory.owner() and DynamicPricingModule.owner() all equal the deployer. On chain 1 the same path runs because line 228 only checks OWNER when it differs from the deployer (read at lines 224-230 and 319-326). Expected: the script refuses on chain 1 or 4663 when OWNER is unset, or _check fails. Actual: it completes with every owner role on the deployer key.","severity":"low","snippet":"        if (c.owner == address(0)) c.owner = deployer;","title":"Deploy script: OWNER unset on mainnet leaves the registry, vault factory, factories and module owned by the deployer EOA, and _check passes"},{"citation":"resolved","description":"From the economics specialist, reproduced. PaymentLib.sol:24-25 documents ownerOwed as 'the artist's share, paid only by the owner's withdraw (pull: a blocklisted artist never blocks a sale, a split or anyone's refund)'. In an ETH auction settleAuction instead pushes the deposit with forceSafeTransferETH (line 391) and the remainder with smartTransferETH (WETH fallback) to owner(), and ownerOwed[ETH] is never credited. Because of the WETH fallback and the gas reserve check, the owner cannot make settlement revert, so this is a documentation mismatch, not a lock: withdraw(ETH) on an ETH auction always reverts NothingToWithdraw unless sweepUntracked credited something. Fix: correct the PaymentLib NatSpec (ETH auctions push to the owner) or credit ownerOwed[ETH] as the ERC-20 branch does.","line":397,"path":"src/factories/erc721/ERC721AuctionInstance.sol","reproduction":"test/scratch/Judge.t.sol test_F4_ethAuctionPushesOwnerShare (passes on this commit, i.e. the push is observed): ETH auction, alignment 1900, protocol 100; artist queuePiece{value: 1 ether}; alice bids 2 ether; warp past the end; settleAuction(1). ownerBalance(address(0)) == 0 and the artist's ETH balance rose by 1 ether + 1.6 ether in the settlement transaction; escrowed(ETH) == 0. Expected per the PaymentLib NatSpec: ownerBalance(ETH) == 2.6 ether, paid on withdraw.","severity":"info","snippet":"            SmartTransferLib.smartTransferETH(owner(), s.remainder, weth);","title":"PaymentLib NatSpec says the artist's share is pull-only, but ETH auctions push the deposit and the remainder to owner() at settlement"},{"citation":"resolved","description":"Merged from the permissions and economics specialists, confirmed by reading the vault. Line 10 says the treasury is set 'for FUTURE launches' and line 16 that the owner 'cannot touch ... their vaults'. UniAlignmentVault.protocolTreasury() returns registry.treasury() (UniAlignmentVault.sol:1014-1016) and withdrawProtocolFees pays registry.treasury() at call time (line 862), so setTreasury redirects every existing vault's accrued and future 1 percent LP-yield cut. Every vault's conversion gate is msg.sender == registry.conversionKeeper() (line 208), so setConversionKeeper hands control of all pending vault balances to the new keeper (which AUDIT-NOTES says can sell a batch at its own floors). setPaymentToken(token, false) also stops contributions to live vaults (see the stranded-cut finding). Collections' own split treasuries and bps are immutables, as claimed. These are intended Safe powers (AUDIT-NOTES M3 made the vault treasury follow the registry), so the defect is the comment, which states a narrower power than the code grants. Fix: reword lines 10 and 16 to say that the treasury (vault protocol cut), the conversion keeper and the payment-token allowlist apply live to every deployed vault.","line":16,"path":"src/registry/MiyaRegistry.sol","reproduction":"Read-only: UniAlignmentVault.sol:1014-1016 `function protocolTreasury() external view returns (address) { return registry.treasury(); }`, line 862 `address treasury = registry.treasury();` inside withdrawProtocolFees, line 208 `if (msg.sender != registry.conversionKeeper()) revert NotKeeper();`. Sequence: deploy with config/1.json, a collection contributes, the keeper converts so the vault accrues _protocolFees; the Safe calls registry.setTreasury(X); vault.protocolTreasury() == X and withdrawProtocolFees(IMD) pays X. Expected per the comment: no effect on deployed vaults. Actual: their protocol-fee recipient and their keeper change.","severity":"info","snippet":" *         It cannot touch deployed collections, their fee terms, their vaults, or any pool. Collections snapshot","title":"MiyaRegistry NatSpec says the owner cannot touch deployed vaults and sets the treasury for FUTURE launches; vaults read the treasury and the conversion keeper live"},{"citation":"resolved","description":"From the permissions specialist, confirmed by reading. The cap counts only currently allowed tokens (setPaymentToken lines 178-190). A token removed with setPaymentToken(t, false) stays in the vault's _openTokens and in each benefactor's token list until the keeper converts its pending balance, and a replacement can be allowed at once, so up to 17 distinct tokens can be pending and _book reverts TooManyTokens (UniAlignmentVault.sol:309 and 314, MAX_BATCH_TOKENS = 16). Collections catch the revert and park the cut (PaymentLib.sendOrPark), so nothing is lost; the cut waits until the keeper converts the dropped token. AUDIT-NOTES already states this exception ('Only a token dropped from the allowlist while still pending can push a vault past 16'), so only the code comment is wrong. Not reachable with 1.json as deployed (6 tokens) without later owner changes. Fix: reword the comment, or have setPaymentToken refuse a new token while a dropped one is still pending in a vault.","line":24,"path":"src/registry/MiyaRegistry.sol","reproduction":"Read-only, by the AUDIT-NOTES test test_tooManyTokens and the code: registry with 15 allowed ERC-20s T1..T15; a collection's vault cuts leave ETH and T1..T15 pending in one vault (16 open tokens); owner calls setPaymentToken(T1, false) then setPaymentToken(T16, true) (succeeds: length is 14 < 15); a sale in T16 is distributed; receiveContribution reaches _book with pendingByToken[T16] == 0 and _openTokens.length == 16 -> TooManyTokens; the collection parks the cut (pendingVaultCut(T16) > 0). Expected per the comment: the vault accepts every allowed token. Actual: refused until T1 converts.","severity":"info","snippet":"    ///         benefactor), ETH included, so 15 ERC-20s + ETH always fit and no allowed token can be refused for that.","title":"MAX_PAYMENT_TOKENS comment claims an allowed token can never be refused by the vault's 16-token cap; false after a token is dropped while still pending"},{"citation":"resolved","description":"Merged from the flow and math specialists. With SSTORE2's 1-byte STOP prefix the largest data contract is 24,575 bytes, so 'one byte under the cap' would be CHUNK = 24_575; the constant on line 13 is 24_000. The code is safe (smaller chunks are fine, two chunks cover 48,000 bytes and the InitCodeTooLarge check matches the constant; all three factories deploy in the test suites), but the comment claims a property the constant does not have, and the two-chunk ceiling is 48,000 bytes of instance creation code rather than 49,150. Fix: set CHUNK = 24_575 or reword the comment to state the actual margin.","line":12,"path":"src/factories/shared/InitCodeStore.sol","reproduction":"Read-only: line 13 `uint256 private constant CHUNK = 24_000;` versus 24_576 - 1 = 24_575. InitCodeStore.write(code) with code.length == 48_001 reverts InitCodeTooLarge although two SSTORE2 contracts of 24,575 bytes could hold up to 49,150 bytes.","severity":"info","snippet":"    /// @dev Each data contract holds at most this much (one byte under the runtime cap, for SSTORE2's STOP prefix).","title":"InitCodeStore comment says CHUNK is one byte under the runtime cap; it is 24,000, 576 bytes under EIP-170's 24,576"},{"citation":"resolved","description":"From the permissions specialist, confirmed by reading. setDynamicPricingModule (lines 129-134) checks registry.isApprovedComponent once. createInstance re-checks the gating module (lines 77-79) but not the pricing module, so after registry.setComponent(module, false) new 1155 collections still snapshot the revoked module (an immutable-like reference in the instance; LIMITED_DYNAMIC editions price through it). Both roles belong to the same Safe, so this is a consistency gap, not a bypass, and it contradicts DynamicPricingModule's NatSpec ('Deploy once, approve in MiyaRegistry (setComponent), set as factory default'). Fix: in createInstance pass address(0) (or revert UnapprovedComponent) when dynamicPricingModule != address(0) and !registry.isApprovedComponent(dynamicPricingModule).","line":114,"path":"src/factories/erc1155/ERC1155Factory.sol","reproduction":"Read-only: createInstance (lines 73-101) contains no isApprovedComponent check for dynamicPricingModule; _buildInitCode copies the storage value unconditionally (line 114). Sequence: owner registry.setComponent(dpm, false); creator ERC1155Factory.createInstance(salt, params). Expected: revert UnapprovedComponent or the module left out. Actual: the instance is created with dynamicPricingModule() == dpm.","severity":"info","snippet":"            dynamicPricingModule: dynamicPricingModule,","title":"ERC1155Factory checks the pricing module's registry approval only in setDynamicPricingModule; createInstance still installs a module the registry has since revoked"}],"hash":"705181a3af8cbf8b1d6c202bced76099f42e23140f8fb36784021931329e3163","nodeId":"94580d80-4af9-494c-9df5-caa2a8f8f55a","outcome":"completed","summary":"The findings file is written and validated. Final recap follows.\n\n**Outcome.** I reproduced every specialist lead against the pinned commit, merged the duplicates, and wrote 9 findings to `.imd-findings.json`: 4 low, 5 info, none high or critical. Reproductions ran in `test/scratch/Judge.t.sol` plus the math specialist's proof file, which fails on this commit for its stated reason and is attached to the first finding.\n\n**Findings kept (merged):**\n- **Low, dynamic pricing quote vs charge.** The next-mint view rounds down while the mint charges rounded up, so a buyer paying the quoted price is refused. Four specialists reported this; merged into one.\n- **Low, delisted token strands the vault cut.** After the Safe drops a payment token, the alignment share of every undistributed sale in that token is parked with no exit path, contradicting three NatSpec claims. Flow and economics reports merged.\n- **Low, ETH sent to an ERC-20 auction house is locked.** No function can sweep or withdraw it, unlike the drop and 1155 instances.\n- **Low, deploy with OWNER unset.** On mainnet the script completes with every owner role on the deployer key and its own checks pass. Reproduced offline on chain 31337; the chain-1 guard skip is shown by reading lines 224 to 230 and 319.\n- **Info, five NatSpec mismatches.** ETH auctions push the artist share rather than pull. The registry owner does touch deployed vaults through the live treasury and keeper. The 16-token cap comment is false after a delist while pending. The InitCodeStore chunk comment overstates the margin. The 1155 factory does not re-check a revoked pricing module at creation.\n\n**Numbered questions.** Books (1), intake (2), auctions (3), vault calls (4), factories and registry (5), and end to end (7) hold apart from the findings above. The only griefing I could construct is gas-limiting a permissionless distribute so the vault call runs out of gas and the cut parks; the next split or a flush carries it, so no lasting harm. Config 1.json (6) checked against mainnet by RPC: all six token symbols and decimals, and code at the hook, zRouter, AllowanceHolder and treasury, match. The hook address carries only the beforeInitialize flag. Pool liveness is checked by the script itself at deploy time; I did not read the pools.\n\n**Coverage.** Every external function of PaymentLib's three consumers, the three factories, InitCodeStore, MiyaRegistry and the script was read. Functions with a finding are listed above. All others hold by reading and by the existing payment suites. Unreached: fork-only paths (real vault conversion and claims through zRouter), since no mainnet fork was run.\n\n**Not findings.** The USDC blocklist on the vault and everything AUDIT-NOTES marks accepted, except that the delist case is worse than written there, which the second finding says.","treeHash":null,"usage":{"cachedInputTokens":1730380,"cost":{"source":"runtime","usdMicros":5148859},"inputTokens":418,"model":"claude-fable-5-1","outputTokens":29579,"runtime":"claude","turns":28,"wallClockMs":486085}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"079b3bf4557d8811","findings":[{"citation":"resolved","description":"OWNER is env-only (loadConfig line 150: vm.envOr(\"OWNER\", address(0))). script/config/1.json has no owner key. When OWNER is unset, _deployAll sets c.owner = deployer (line 224). The mainnet guard at line 228 (`if (c.owner != deployer) _requireCode(c.owner, \"OWNER\");`) skips exactly this case, and the handoff at line 319 (`if (c.owner != deployer)`) is skipped. _check compares each owner against the same mutated c.owner (Config memory is passed by reference), so registry.owner, vaultFactory.owner, 1155.owner, auction.owner and dynamicPricing.owner all pass as the deployer EOA. The brief requires every owner to be the Safe after the script; nothing enforces that on chain 1 or 4663. A partial run has the same effect: forge broadcasts one transaction per call, so if any call between registry deployment (step 1) and the handoff (step 11) fails, a registry, vault factory and default vault that are registered and usable stay owned by the deployer key. A re-run with the same SALT_NAMESPACE reverts at the registry's CREATE3 address.","line":224,"path":"script/DeployMiyamints.s.sol","reproduction":"Chain 1 (fork), 1.json, env OWNER unset, KEEPER unset: run DeployMiyamints.run() (or runAs(deployer) / runAs(deployer, address(0))). Expected: the script refuses on chain 1 because OWNER is unset (or is not a contract), or the owners are the Safe. Actual: it completes. MiyaRegistry.owner() == deployer EOA, and so do UniAlignmentVaultFactory.owner(), ERC1155Factory.owner(), ERC721AuctionFactory.owner() and DynamicPricingModule.owner(). _check passes. The deployer key alone can call setTreasury, setConversionKeeper, setPaymentToken and setSwapTarget. Reachable with script/config/1.json: yes. Smallest fix: in loadConfig or _deployAll, on chainid 1 or 4663 revert unless OWNER is set, is not the deployer and has code; in _check, also on those chains, assert r.owner() != d.deployer.","severity":"low","snippet":"        if (c.owner == address(0)) c.owner = deployer;","title":"Deploy script: OWNER unset on mainnet leaves every owner role with the deployer EOA, and _check still passes"},{"citation":"resolved","description":"ERC1155Instance.getCurrentPrice (documented at ERC1155Instance.sol:390 as `@return price Current price for the next mint`) calls calculatePrice, which floors basePrice*r^minted with mulWad. mint() charges calculateBatchCost, which rounds up twice with fullMulDivUp. For a one-piece mint the charge can exceed getCurrentPrice by 1 unit, and a multi-piece batch by up to 2 units over the exact geometric sum. A front end or integrator that passes maxCost = getCurrentPrice(e,t) * amount gets ExceedsMaxCost. The buyer is never undercharged, so the 'never pays less than the curve' property holds.","line":44,"path":"src/factories/erc1155/DynamicPricingModule.sol","reproduction":"LIMITED_DYNAMIC edition, base price 3 (any token), priceIncreaseRate 5000, 1 piece minted. getCurrentPrice = mulWad(3, 1.5e18) = 4. calculateMintCost(e,1,t) = fullMulDivUp(fullMulDivUp(3,1.5e18,0.5e18)=9, 0.5e18, 1e18) = 5. Then mint(e,1,0,\"\",t, maxCost=4) reverts ExceedsMaxCost. Expected: the view equals the one-piece charge (5). Reachable with 1.json: yes (DynamicPricingModule is the 1155 default; the gap also appears with realistic 6-decimal USDC prices whose curve value is not exact). Fix: in calculatePrice use FixedPointMathLib.mulWadUp, or document the view as a lower bound and point integrators at calculateMintCost.","severity":"info","snippet":"        uint256 currentPrice = basePrice.mulWad(result);","title":"ERC1155 getCurrentPrice rounds down but mint charges the curve rounded up: NatSpec 'Current price for the next mint' is not what a single mint costs"},{"citation":"resolved","description":"Line 10 says the owner sets 'the treasury ... for FUTURE launches' and line 16 says it 'cannot touch ... their vaults'. UniAlignmentVault.protocolTreasury() returns registry.treasury() (UniAlignmentVault.sol:1014-1015), and withdrawProtocolFees pays registry.treasury() at call time (line 862). setTreasury therefore redirects every existing vault's accrued and future protocol LP-yield cut. Every vault's conversion gate is msg.sender == registry.conversionKeeper() (line 208), so setConversionKeeper hands control of all pending vault balances to a new keeper. AUDIT-NOTES documents that a keeper can take a batch it converts. setPaymentToken(token,false) also stops sales in live collections (documented elsewhere in the NatSpec). Collections' own split treasuries are snapshotted, as claimed. These are owner (Safe) trust assumptions, but the comment states a narrower power than the code grants.","line":16,"path":"src/registry/MiyaRegistry.sol","reproduction":"Deploy, create a collection, contribute, let protocol fees accrue in the vault. Owner calls registry.setTreasury(X). Expected per the NatSpec: the deployed vault still pays the old treasury. Actual: vault.protocolTreasury() == X and withdrawProtocolFees pays X. Owner calls setConversionKeeper(K2): K2 can now call convert on every existing vault. Fix: correct the NatSpec (lines 10 and 16) to say that the treasury and the keeper apply live to all vaults.","severity":"info","snippet":" *         It cannot touch deployed collections, their fee terms, their vaults, or any pool. Collections snapshot","title":"MiyaRegistry NatSpec says the treasury is set for FUTURE launches and the owner cannot touch vaults; vaults read treasury and keeper live"},{"citation":"resolved","description":"The cap only counts currently allowed tokens. A token removed with setPaymentToken(t,false) stays in the vault's _openTokens and the benefactor's token list until the keeper converts its pending balance. A replacement token can be allowed at once, so up to 17 distinct tokens (16 ERC-20s + ETH) can be pending, and _book reverts TooManyTokens (UniAlignmentVault.sol:309/314). Collections catch this and park the cut (PaymentLib.sendOrPark), so nothing is lost. The cut waits until the keeper converts the dropped token.","line":24,"path":"src/registry/MiyaRegistry.sol","reproduction":"Registry with 15 allowed ERC-20s T1..T15. A collection's vault cuts leave ETH and T1..T15 pending in its vault (16 open). Owner calls setPaymentToken(T1,false), then setPaymentToken(T16,true). A sale in T16 is distributed. Expected per the comment: the vault accepts it. Actual: receiveContribution reverts TooManyTokens and pendingVaultCut(T16) > 0. Not reachable with 1.json as deployed (6 tokens) without later owner changes. Fix: reword the comment, or have setPaymentToken count tokens still pending in vaults (or have the keeper convert a dropped token before a new one is allowed).","severity":"info","snippet":"    ///         benefactor), ETH included, so 15 ERC-20s + ETH always fit and no allowed token can be refused for that.","title":"MAX_PAYMENT_TOKENS comment claims an allowed token can never be refused by the vault's 16-token cap; false after a token is dropped while pending"},{"citation":"resolved","description":"setDynamicPricingModule checks registry.isApprovedComponent once (line 129ff). createInstance re-checks the gating module (line 75-77) but not the pricing module. After registry.setComponent(module,false), new 1155 collections still snapshot the revoked module as their pricing math. Both roles belong to the same Safe, so this is a consistency gap, not a bypass. It also contradicts DynamicPricingModule's NatSpec, which says the module is 'approve[d] in MiyaRegistry (setComponent)' as the gate.","line":114,"path":"src/factories/erc1155/ERC1155Factory.sol","reproduction":"Owner: registry.setComponent(dpm,false). Creator: ERC1155Factory.createInstance(salt, params). Expected: revert UnapprovedComponent, or the module left out. Actual: the instance is created with dynamicPricingModule() == dpm, and LIMITED_DYNAMIC editions price through it. Fix: in createInstance, pass address(0) (or revert) when dynamicPricingModule != 0 and !registry.isApprovedComponent(dynamicPricingModule).","severity":"info","snippet":"            dynamicPricingModule: dynamicPricingModule,","title":"ERC1155Factory checks dynamicPricingModule approval only when set; a module the registry later revokes is still put into new instances"}],"hash":"911c0e0d75edaee7b926962586e37961a47bbab64618d4b424d05f8a8c0a270e","nodeId":"15e2d404-48c4-4857-9d2e-a1c07b5b1745","outcome":"completed","summary":"I found nothing critical, high or medium in the permissions area. Five findings are in `.imd-findings.json`: one low (the deploy script) and four info items. None needed a Foundry proof, so I wrote and ran no tests. I didn't build the project and had no mainnet RPC.\n\n**Coverage limits.** I ran the Access Control, Trust Gap and Asymmetry passes myself, with no helpers. I read the vault (`UniAlignmentVault`, `UniAlignmentVaultFactory`) only where the scoped contracts call it. I couldn't check INFER, IMD, imdUSD, the hook address or the treasury on-chain. The other mainnet addresses in `1.json` match the canonical ones I know, but I didn't query the chain for them either. A clean answer below means I found nothing; it doesn't prove there is nothing.\n\n## Findings\n1. **Low: `script/DeployMiyamints.s.sol:224`.** If `OWNER` isn't set on chain 1, `c.owner` falls back to the deployer. The mainnet code check (line 228) and the ownership handoff (line 319) both skip that case, and `_check` compares against the same changed value. The run passes with the deployer wallet owning the registry, the vault factory, both CREATE3 factories and `DynamicPricingModule`. A run that stops between steps 1 and 11 leaves a working system owned by the deployer too. Fix: on chain 1 and 4663, refuse unless `OWNER` is set, is not the deployer and has code.\n2. **Info: `DynamicPricingModule.sol:44`.** `getCurrentPrice`, documented as the \"current price for the next mint\", rounds down, while `mint` charges the curve rounded up. Example: base price 3, rate 5000, one piece already minted. The view says 4, the mint costs 5, so `maxCost=4` reverts.\n3. **Info: `MiyaRegistry.sol:10/16` NatSpec.** It says the treasury is set \"for FUTURE launches\" and the owner \"cannot touch their vaults\". In fact `setTreasury` changes where every existing vault sends its protocol fees, and `setConversionKeeper` hands one keeper control over the pending balances of all vaults.\n4. **Info: `MiyaRegistry.sol:24` comment.** It says no allowed token can ever be refused by a vault's 16-token limit. That's false if a token is dropped while still pending in a vault and a new one is added: the vault reverts `TooManyTokens`, and the cut is parked, not lost.\n5. **Info: `ERC1155Factory.sol:114`.** A pricing module the registry has since revoked is still put into new 1155 collections, because approval is only checked when the module is set.\n\n## Answers to the seven questions\n1. **Per-token books: hold.**\n   - What the contract owes is `unsplit + ownerOwed + protocolOwed + vaultOwed + escrow`. Each path moves amounts between these and never counts one twice.\n   - Auction ETH bids: escrow goes up by the new bid minus the refund. ERC-20 refunds stay in escrow until `claimRefund`.\n   - `sweepUntracked` only counts the balance above what is owed.\n   - `withdrawAllStep` can only be called by the contract itself, and a token that fails rolls back its whole step without stopping the others.\n   - A top-up on `flushVault` is collected before anything is sent.\n2. **Payment intake: holds.**\n   - Drop and auction require ETH sent to equal the cost exactly; 1155 refunds any excess.\n   - ERC-20 payments require no ETH sent, cost at or under `maxCost`, and an exact balance increase, so fee-on-transfer tokens are rejected.\n   - A token the sale doesn't price reverts `TokenNotPriced`. Price times quantity is overflow-checked.\n   - The dynamic curve is never below base price times amount. Only the view rounding in finding 2 is off.\n3. **Auctions: hold.**\n   - Outbid ETH bidders are refunded with a forced transfer; ERC-20 bidders pull refunds with `claimRefund(to)`.\n   - Settlement checks gas before minting the winner, and a winner who can't receive gets the token held in escrow.\n   - `settled` is set first and everything is `nonReentrant`.\n   - `reclaimUnsold` gives the deposit to the treasury.\n   - The anti-snipe window has no cap; AUDIT-NOTES accepts this.\n4. **Vault and treasury calls: hold.**\n   - The approval is","treeHash":null,"usage":{"cachedInputTokens":1611819,"cost":{"source":"runtime","usdMicros":1413475},"inputTokens":3415,"model":"claude-opus-5-5","outputTokens":24348,"runtime":"claude","turns":22,"wallClockMs":353845}}],"verification":[]}