# Audit report

> 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.
>
> miyamints 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.
>
> Answer each numbered question, including the ones where nothing is wrong:
> 1. 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.
> 2. 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.
> 3. 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.
> 4. 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.
> 5. 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.
> 6. 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.
> 7. 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.
>
> Not 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.
>
> For 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.

| | |
|---|---|
| Repository | https://github.com/fa11up/miyamints-contracts.git |
| Commit | `b5bdcc64a7c62487d82047bb9805fda730edd95b` |
| Job | `c89f023c-dd16-4136-8f86-9ac12f7712fe` |
| Judged | 2026-11-09 18:08 UTC |
| Findings | 4 low · 5 info |

Four agents audited the code as it is at `b5bdcc6`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Low: LIMITED_DYNAMIC: getCurrentPrice rounds the curve down while mint charges it rounded up, so a mint at the quoted next-mint price is refused

`src/factories/erc1155/DynamicPricingModule.sol:44`

```
        uint256 currentPrice = basePrice.mulWad(result);
```

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.

**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'.

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import { Test } from "forge-std/Test.sol";
import { DynamicPricingModule } from "src/factories/erc1155/DynamicPricingModule.sol";

contract DynamicPriceViewVsChargeTest is Test {
    DynamicPricingModule module;

    function setUp() public {
        module = new DynamicPricingModule();
    }

    /// The quoted next-mint price (calculatePrice, rounds down) must equal what mint charges for one piece
    /// (calculateBatchCost(.., 1), rounds up). On this commit they differ by one unit.
    function test_viewEqualsChargeForOne() public view {
        uint256 shown = module.calculatePrice(1e6, 100, 7);
        uint256 charged = module.calculateBatchCost(1e6, 100, 7, 1);
        assertEq(shown, charged, "getCurrentPrice != cost of minting one");
    }

    function test_viewEqualsCharge_first200() public view {
        for (uint256 m; m < 200; ++m) {
            assertEq(module.calculatePrice(1e6, 100, m), module.calculateBatchCost(1e6, 100, m, 1));
        }
    }
}
```

### 2. Low: 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

`src/registry/MiyaRegistry.sol:15`

```
 *           only stops new sales and contributions in it; balances already held stay withdrawable.
```

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.

**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).

### 3. Low: ERC-20 auction house: ETH sent to it is locked forever; sweepUntracked knows only the one currency and the owner cannot add ETH

`src/factories/erc721/ERC721AuctionInstance.sol:506`

```
        if (!_pay.isKnown[token]) revert PaymentLib.TokenNotAccepted(token);
```

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.

**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.

### 4. Low: Deploy script: OWNER unset on mainnet leaves the registry, vault factory, factories and module owned by the deployer EOA, and _check passes

`script/DeployMiyamints.s.sol:224`

```
        if (c.owner == address(0)) c.owner = deployer;
```

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.

**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.

### 5. Info: PaymentLib NatSpec says the artist's share is pull-only, but ETH auctions push the deposit and the remainder to owner() at settlement

`src/factories/erc721/ERC721AuctionInstance.sol:397`

```
            SmartTransferLib.smartTransferETH(owner(), s.remainder, weth);
```

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.

**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.

### 6. Info: 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

`src/registry/MiyaRegistry.sol:16`

```
 *         It cannot touch deployed collections, their fee terms, their vaults, or any pool. Collections snapshot
```

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.

**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.

### 7. Info: 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

`src/registry/MiyaRegistry.sol:24`

```
    ///         benefactor), ETH included, so 15 ERC-20s + ETH always fit and no allowed token can be refused for that.
```

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.

**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.

### 8. Info: InitCodeStore comment says CHUNK is one byte under the runtime cap; it is 24,000, 576 bytes under EIP-170's 24,576

`src/factories/shared/InitCodeStore.sol:12`

```
    /// @dev Each data contract holds at most this much (one byte under the runtime cap, for SSTORE2's STOP prefix).
```

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.

**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.

### 9. Info: ERC1155Factory checks the pricing module's registry approval only in setDynamicPricingModule; createInstance still installs a module the registry has since revoked

`src/factories/erc1155/ERC1155Factory.sol:114`

```
            dynamicPricingModule: dynamicPricingModule,
```

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).

**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.

---

Judge's submission `705181a3af8cbf8b1d6c202bced76099f42e23140f8fb36784021931329e3163`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
