Skip to content

BW-185: production DeployAll — yield-strategy gate and a UUPS NFTMetadataGenerator - #103

Merged
SocksNFlops merged 5 commits into
mainfrom
bw-185-production-deployall
Sep 23, 2026
Merged

SocksNFlops merged 5 commits into
mainfrom
bw-185-production-deployall

Conversation

@0xGumpy

@0xGumpy 0xGumpy commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Changes

Found by the BW-185 deploy rehearsal on a 4663 fork: DeployAll cannot complete a production run, because deployYieldStrategies() and deployNFTMetadataGenerator() only knew how to deploy test mocks and reverted when IS_TEST and IS_TESTNET were both false. The 999 deploy got past this by hand-commenting the calls, which is why 999's MortgageNFT points at a zero generator and tokenURI reverts there.

  • Yield strategies stay gated on the mock modes inside DeployAll.run(). Production SubConsols launch without a yield strategy; the strategy pointer is admin-settable on SubConsol, so one can be attached later by governance.
  • NFTMetadataGenerator is now a real contract, src/NFTMetadataGenerator.sol: UUPS + AccessControlUpgradeable, ERC-7201 namespaced storage, DEFAULT_ADMIN_ROLE gates upgrades. generateMetadata is pure and renders the position as a data:application/json;base64,… URI with name, description, and 21 attributes (collateral, term, balances, status). MortgageNFT.nftMetadataGenerator is immutable, so production deploys the ERC-1967 proxy and hands the NFT the proxy address; the renderer can be upgraded later without a LoanManager migration.
  • DeployNFTMetadataGenerator deploys implementation + proxy + initialize(deployer) in production, grants DEFAULT_ADMIN_ROLE to every configured admin, and renounces the deployer's via renounceUnlessAdmin. Test and testnet keep MockNFTMetadataGenerator. DeployAll calls it unconditionally and audits the proxy's roles in production.
  • BaseScript.setDeployMode(isTest, isTestnet) lets tests pick a deploy mode without touching process-global env vars; DeployAll.getPath() writes the test addresses file whenever a suffix is set, so a production-mode test run never overwrites a chain file.

Testing

  • forge test: 510 passed, 0 failed, 2 skipped.
  • test/NFTMetadataGenerator.t.sol: 11 tests — initializer locked on the implementation, admin-only upgrade (AccessControlUnauthorizedAccount for non-admins), storage survives upgrade through MockNFTMetadataGeneratorUpgraded, metadata prefix/name/description/attribute rendering, tokenURI end-to-end through MortgageNFT.
  • test/script/DeployAllProduction.t.sol: production-mode DeployAll completes, deploys the proxy (not the mock), the MortgageNFT points at it, admins hold DEFAULT_ADMIN_ROLE and the deployer does not.
  • forge fmt --check clean on touched files; lintspec src clean.
  • Fork rehearsal of DeployAll on Robinhood mainnet (anvil fork at block 64296698) completes with the yield gate: 260 transactions, role audit clean, positional arrays verified, all 12 Chainlink oracles reading live prices. Rehearsal feat(general-manager): adding return token-id to request-creation #2 with the generator proxy is queued after this merges.

Reviewers:

@SocksNFlops

deployYieldStrategies() and deployNFTMetadataGenerator() deploy test mocks
and revert when IS_TEST and IS_TESTNET are both false, so DeployAll could
not complete a mainnet run. Gate both calls on the mock modes instead:
production SubConsols launch without a yield strategy and the LoanManager
with a zero metadata generator, matching the live 999 stack.
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
File % Lines % Statements % Branches % Funcs
script/BaseScript.s.sol 83.33% (25/30) 89.66% (26/29) 75.00% (6/8) 66.67% (4/6)
script/CollateralSetup.s.sol 63.83% (30/47) 87.27% (48/55) 83.33% (5/6) 0.00% (0/9)
script/DeployAll.s.sol 97.98% (97/99) 99.01% (100/101) 70.00% (7/10) 85.71% (6/7)
script/DeployConsol.s.sol 59.38% (19/32) 77.14% (27/35) N/A (0/0) 0.00% (0/6)
script/DeployForfeitedAssetsPool.s.sol 50.00% (10/20) 76.19% (16/21) N/A (0/0) 0.00% (0/5)
script/DeployGeneralManager.s.sol 69.81% (37/53) 90.32% (56/62) 50.00% (2/4) 0.00% (0/10)
script/DeployInterestOracle.s.sol 25.00% (3/12) 44.44% (4/9) N/A (0/0) 0.00% (0/4)
script/DeployLoanManager.s.sol 40.00% (6/15) 61.54% (8/13) N/A (0/0) 0.00% (0/4)
script/DeployMockStockTokens.s.sol 0.00% (0/16) 0.00% (0/21) 0.00% (0/2) 0.00% (0/2)
script/DeployNFTMetadataGenerator.s.sol 47.06% (8/17) 70.59% (12/17) N/A (0/0) 0.00% (0/4)
script/DeployOrderPool.s.sol 59.09% (13/22) 78.26% (18/23) 100.00% (1/1) 0.00% (0/4)
script/DeployOriginationScheduler.s.sol 66.67% (22/33) 84.85% (28/33) N/A (0/0) 0.00% (0/6)
script/DeployPriceOracles.s.sol 66.00% (33/50) 83.93% (47/56) 62.50% (5/8) 20.00% (2/10)
script/DeployQueues.s.sol 65.38% (34/52) 86.44% (51/59) N/A (0/0) 0.00% (0/10)
script/DeploySimpleOracle.s.sol 90.38% (47/52) 96.61% (57/59) 68.75% (11/16) 57.14% (4/7)
script/DeploySubConsols.s.sol 62.16% (23/37) 85.42% (41/48) 50.00% (1/2) 0.00% (0/7)
script/DeployUSDX.s.sol 57.14% (12/21) 78.26% (18/23) N/A (0/0) 0.00% (0/4)
script/LocalhostSetupPart1.s.sol 100.00% (24/24) 100.00% (27/27) N/A (0/0) 100.00% (2/2)
script/LocalhostSetupPart2.s.sol 100.00% (42/42) 100.00% (49/49) N/A (0/0) 100.00% (3/3)
script/LocalhostSetupPart3.s.sol 100.00% (18/18) 100.00% (21/21) N/A (0/0) 100.00% (2/2)
script/RemoveUsdh.s.sol 0.00% (0/13) 0.00% (0/15) 0.00% (0/10) 0.00% (0/1)
src/ChainlinkPriceOracle.sol 100.00% (23/23) 100.00% (24/24) 100.00% (4/4) 100.00% (3/3)
src/Consol.sol 87.50% (28/32) 87.88% (29/33) 0.00% (0/4) 100.00% (6/6)
src/ConversionQueue.sol 94.52% (69/73) 95.92% (94/98) 69.23% (9/13) 100.00% (7/7)
src/ForfeitedAssetsPool.sol 90.70% (39/43) 95.35% (41/43) 85.71% (6/7) 90.00% (9/10)
src/ForfeitedAssetsQueue.sol 94.74% (18/19) 95.45% (21/22) 66.67% (2/3) 100.00% (2/2)
src/GeneralManager.sol 94.50% (309/327) 94.75% (307/324) 73.33% (33/45) 95.71% (67/70)
src/HyperCorePriceOracle.sol 100.00% (36/36) 100.00% (44/44) 100.00% (8/8) 100.00% (4/4)
src/LenderQueue.sol 89.36% (42/47) 93.88% (46/49) 75.00% (6/8) 90.00% (9/10)
src/LoanManager.sol 98.36% (120/122) 98.18% (108/110) 86.67% (13/15) 100.00% (27/27)
src/MortgageNFT.sol 100.00% (22/22) 100.00% (17/17) 100.00% (2/2) 100.00% (7/7)
src/MortgageQueue.sol 98.44% (63/64) 97.01% (65/67) 100.00% (21/21) 100.00% (8/8)
src/MultiTokenVault.sol 92.42% (61/66) 92.86% (65/70) 54.55% (6/11) 100.00% (15/15)
src/NFTMetadataGenerator.sol 94.44% (34/36) 95.45% (21/22) 100.00% (2/2) 93.33% (14/15)
src/OrderPool.sol 96.77% (60/62) 96.77% (60/62) 81.82% (9/11) 100.00% (11/11)
src/OriginationPool.sol 100.00% (59/59) 98.39% (61/62) 100.00% (12/12) 100.00% (10/10)
src/OriginationPoolScheduler.sol 94.57% (122/129) 94.96% (132/139) 81.82% (9/11) 90.32% (28/31)
src/PythInterestRateOracle.sol 77.27% (17/22) 78.26% (18/23) 50.00% (4/8) 100.00% (2/2)
src/PythPriceOracle.sol 80.95% (17/21) 80.00% (16/20) 25.00% (1/4) 100.00% (3/3)
src/QueueProcesssor.sol 100.00% (4/4) 100.00% (2/2) N/A (0/0) 100.00% (2/2)
src/RebasingERC20.sol 95.65% (22/23) 100.00% (22/22) 100.00% (5/5) 83.33% (5/6)
src/SimpleOracle.sol 100.00% (39/39) 100.00% (37/37) 100.00% (6/6) 100.00% (8/8)
src/SimpleOraclePriceOracle.sol 100.00% (21/21) 100.00% (22/22) 100.00% (3/3) 100.00% (3/3)
src/StaticInterestRateOracle.sol 100.00% (7/7) 100.00% (8/8) 100.00% (1/1) 100.00% (2/2)
src/SubConsol.sol 100.00% (41/41) 100.00% (37/37) 100.00% (6/6) 100.00% (9/9)
src/USDX.sol 97.87% (46/47) 100.00% (64/64) 100.00% (5/5) 88.89% (8/9)
src/UsdxQueue.sol 94.74% (18/19) 95.45% (21/22) 66.67% (2/3) 100.00% (2/2)
src/libraries/MortgageMath.sol 96.89% (156/161) 97.74% (173/177) 91.18% (31/34) 96.30% (26/27)
src/libraries/SharesMath.sol 93.33% (14/15) 90.91% (20/22) 83.33% (5/6) 100.00% (3/3)
src/types/OPoolConfigId.sol 100.00% (2/2) 100.00% (2/2) N/A (0/0) 100.00% (1/1)
test/BaseTest.t.sol 72.13% (132/183) 69.40% (127/183) 60.00% (3/5) 93.33% (14/15)
test/SimpleOraclePriceOracle.t.sol 100.00% (7/7) 100.00% (4/4) N/A (0/0) 100.00% (3/3)
test/integration/10_complexHistory.t.sol 99.60% (500/502) 100.00% (529/529) 100.00% (2/2) 33.33% (1/3)
test/integration/11_conversionRefinance.t.sol 98.37% (181/184) 99.47% (186/187) 100.00% (1/1) 33.33% (1/3)
test/integration/12_compoundingHalfConvert.t.sol 98.60% (141/143) 100.00% (149/149) N/A (0/0) 33.33% (1/3)
test/integration/13_convertedWithPenalties.t.sol 97.78% (132/135) 99.28% (138/139) 100.00% (1/1) 33.33% (1/3)
test/integration/14_compoundingNoPlanConversion.t.sol 98.71% (153/155) 100.00% (159/159) N/A (0/0) 33.33% (1/3)
test/integration/15_halfConverted.t.sol 98.47% (129/131) 100.00% (135/135) N/A (0/0) 33.33% (1/3)
test/integration/16_redeemAndDequeue.t.sol 98.13% (105/107) 100.00% (110/110) N/A (0/0) 33.33% (1/3)
test/integration/17_forecloseAndDequeue.t.sol 98.15% (106/108) 100.00% (111/111) N/A (0/0) 33.33% (1/3)
test/integration/18_expandLowerConvert.t.sol 99.06% (210/212) 100.00% (220/220) N/A (0/0) 50.00% (2/4)
test/integration/19_enqueueAfterCreation.t.sol 97.89% (93/95) 100.00% (96/96) N/A (0/0) 33.33% (1/3)
test/integration/1_foreclosure.t.sol 98.36% (120/122) 100.00% (123/123) N/A (0/0) 33.33% (1/3)
test/integration/20_completeConversion.t.sol 98.48% (130/132) 100.00% (136/136) N/A (0/0) 33.33% (1/3)
test/integration/21_noRedeemDequeue.t.sol 97.92% (94/96) 100.00% (99/99) N/A (0/0) 33.33% (1/3)
test/integration/22_ogPoolDonation.t.sol 95.00% (38/40) 100.00% (37/37) N/A (0/0) 33.33% (1/3)
test/integration/23_requestWithGasFees.t.sol 96.00% (48/50) 100.00% (49/49) N/A (0/0) 33.33% (1/3)
test/integration/24_conversionSmallAmount.t.sol 98.26% (113/115) 100.00% (124/124) N/A (0/0) 33.33% (1/3)
test/integration/25_usdxQueueCancel.t.sol 95.12% (39/41) 100.00% (42/42) N/A (0/0) 33.33% (1/3)
test/integration/26_forfeitedAssetsQueueCancel.t.sol 97.65% (83/85) 100.00% (89/89) N/A (0/0) 33.33% (1/3)
test/integration/27_donationBeforeOrigination.sol 97.87% (92/94) 100.00% (95/95) N/A (0/0) 33.33% (1/3)
test/integration/28_partialConvertAndForeclosure.sol 98.35% (119/121) 100.00% (121/121) N/A (0/0) 33.33% (1/3)
test/integration/2_payAndRefinance.t.sol 98.43% (125/127) 100.00% (135/135) N/A (0/0) 33.33% (1/3)
test/integration/3_payAndPenaltyImposed.t.sol 98.47% (129/131) 100.00% (136/136) N/A (0/0) 33.33% (1/3)
test/integration/4_orderExpires.t.sol 97.44% (76/78) 100.00% (77/77) N/A (0/0) 33.33% (1/3)
test/integration/5_usdxMaximumCap.t.sol 97.92% (94/96) 100.00% (98/98) N/A (0/0) 33.33% (1/3)
test/integration/6_slowForeclosure.t.sol 97.62% (82/84) 100.00% (85/85) N/A (0/0) 33.33% (1/3)
test/integration/7_usdxDonation.t.sol 90.00% (18/20) 100.00% (17/17) N/A (0/0) 33.33% (1/3)
test/integration/8_consolDonation.t.sol 93.75% (30/32) 100.00% (31/31) N/A (0/0) 33.33% (1/3)
test/integration/9_usdxWithdraw.t.sol 96.55% (84/87) 98.90% (90/91) 100.00% (1/1) 33.33% (1/3)
test/mocks/MockAggregatorV3.sol 70.00% (14/20) 76.92% (10/13) N/A (0/0) 57.14% (4/7)
test/mocks/MockERC20.sol 75.00% (6/8) 75.00% (3/4) N/A (0/0) 75.00% (3/4)
test/mocks/MockGatedERC20.sol 0.00% (0/14) 0.00% (0/13) 0.00% (0/2) 0.00% (0/4)
test/mocks/MockHyperCorePrecompile.sol 100.00% (10/10) 100.00% (9/9) 100.00% (1/1) 100.00% (3/3)
test/mocks/MockLenderQueue.sol 88.24% (15/17) 90.00% (18/20) 33.33% (1/3) 100.00% (2/2)
test/mocks/MockMortgageQueue.sol 100.00% (7/7) 100.00% (4/4) N/A (0/0) 100.00% (4/4)
test/mocks/MockNFTMetadataGenerator.sol 100.00% (4/4) 100.00% (2/2) N/A (0/0) 100.00% (2/2)
test/mocks/MockNFTMetadataGeneratorUpgraded.sol 100.00% (2/2) 100.00% (1/1) N/A (0/0) 100.00% (1/1)
test/mocks/MockOriginationPoolDeployCallback.sol 100.00% (7/7) 100.00% (5/5) N/A (0/0) 100.00% (3/3)
test/mocks/MockOriginationPoolSchedulerUpgraded.sol 100.00% (2/2) 100.00% (1/1) N/A (0/0) 100.00% (1/1)
test/mocks/MockRouter.sol 50.00% (34/68) 54.29% (38/70) 23.08% (3/13) 54.55% (6/11)
test/mocks/MockWrapper.sol 48.39% (15/31) 53.85% (14/26) 14.29% (1/7) 37.50% (3/8)
test/mocks/MockYieldStrategy.sol 80.00% (8/10) 71.43% (5/7) 0.00% (0/2) 100.00% (3/3)
test/script/DeployAllRoleInvariants.t.sol 94.12% (16/17) 90.48% (19/21) 100.00% (1/1) 100.00% (3/3)
test/script/DeploySimpleOracle.t.sol 100.00% (6/6) 100.00% (3/3) N/A (0/0) 100.00% (3/3)
Total 92.33% (5561/6023) 94.94% (5911/6226) 72.53% (264/364) 69.90% (411/588)

Comment thread script/DeployAll.s.sol Outdated
Comment on lines +50 to +53
// Deploy NFTMetadataGenerator (mock; production LoanManager launches with none)
if (isTest || isTestnet) {
deployNFTMetadataGenerator();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Prod should launch with an upgradeable NFTMetadataGenerator.

Comment thread script/DeployAll.s.sol
Comment on lines +36 to +39
// Deploy YieldStrategies (mocks; production SubConsols launch without one)
if (isTest || isTestnet) {
deployYieldStrategies();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as this is mutable by governance, this is fine.

@0xGumpy

0xGumpy commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

On the two review notes:

  • Yield strategies (L39): confirmed governance-mutable post-deploy — SubConsol.setYieldStrategy(address) is onlyRole(DEFAULT_ADMIN_ROLE) (src/SubConsol.sol:63), so launching without one and attaching one later is a single admin transaction per SubConsol. 999 runs that way today (no yield strategies in its address book).
  • NFTMetadataGenerator (L53): agreed — the gate is being removed. This PR will instead deploy a production NFTMetadataGenerator as a UUPS proxy (implementation + ERC1967Proxy, admin = protocol admin) before the LoanManager, so MortgageNFT is constructed with the proxy address and metadata stays upgradeable. Commits incoming on this branch; I'll re-request review once they're up.

Renders a MortgagePosition as a data:application/json;base64 URI with the
position's fields as ERC-721 attributes. UUPS + AccessControl behind ERC-7201
namespaced storage, with upgrades gated by DEFAULT_ADMIN_ROLE.
DeployAll calls deployNFTMetadataGenerator() unconditionally again: production
deploys the implementation, an ERC1967Proxy and the initializer, then hands the
admin role to the configured admins. Test and testnet keep the mock.

The deploy mode is settable so the production script test can run without
writing IS_TEST / IS_TESTNET, which forge shares across parallel suites, and the
addresses file is chosen by the test-only suffix so that suite writes its own.
@0xGumpy 0xGumpy changed the title BW-185: skip mock-only deploys on a production DeployAll run BW-185: production DeployAll — yield-strategy gate and a UUPS NFTMetadataGenerator Sep 22, 2026
@0xGumpy

0xGumpy commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 4e706f2 and 861c525: production now deploys a real NFTMetadataGenerator behind a UUPS proxy instead of skipping it. The yield-strategy gate stays (mutable by governance via SubConsol). Title and description updated; re-requesting review.

@0xGumpy
0xGumpy requested a review from SocksNFlops September 22, 2026 00:15

@SocksNFlops SocksNFlops left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix the deploy script

Comment thread script/DeployAll.s.sol
// Deploy NFTMetadataGenerator
deployNFTMetadataGenerator(); // Disabled for production
// Deploy NFTMetadataGenerator (the UUPS proxy in production, the settable mock in test/testnet)
deployNFTMetadataGenerator();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's cleaner to just use the UUPS proxy in both

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a9f863a: deployNFTMetadataGenerator() always deploys the implementation + ERC1967 proxy now; the mock branch is gone from the script (the mock stays a test-only fixture), and DeployAll audits the proxy's roles in every mode. 510 tests pass.

@SocksNFlops
SocksNFlops merged commit 5da30a1 into main Sep 23, 2026
7 checks passed
@SocksNFlops
SocksNFlops deleted the bw-185-production-deployall branch September 23, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants