@rev-net/core-v6 0.0.37 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +6 -7
  3. package/foundry.toml +1 -1
  4. package/package.json +23 -16
  5. package/references/operations.md +1 -1
  6. package/references/runtime.md +1 -1
  7. package/script/Deploy.s.sol +12 -9
  8. package/src/REVDeployer.sol +60 -65
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +17 -10
  11. package/src/REVOwner.sol +121 -14
  12. package/src/interfaces/IREVDeployer.sol +2 -1
  13. package/src/interfaces/IREVHiddenTokens.sol +4 -1
  14. package/src/interfaces/IREVOwner.sol +5 -0
  15. package/ADMINISTRATION.md +0 -73
  16. package/ARCHITECTURE.md +0 -116
  17. package/AUDIT_INSTRUCTIONS.md +0 -90
  18. package/RISKS.md +0 -107
  19. package/SKILLS.md +0 -46
  20. package/STYLE_GUIDE.md +0 -610
  21. package/USER_JOURNEYS.md +0 -195
  22. package/foundry.lock +0 -11
  23. package/slither-ci.config.json +0 -10
  24. package/sphinx.lock +0 -507
  25. package/test/REV.integrations.t.sol +0 -573
  26. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  27. package/test/REVDeployerRegressions.t.sol +0 -396
  28. package/test/REVInvincibility.t.sol +0 -1371
  29. package/test/REVInvincibilityHandler.sol +0 -387
  30. package/test/REVLifecycle.t.sol +0 -420
  31. package/test/REVLoans.invariants.t.sol +0 -724
  32. package/test/REVLoansAttacks.t.sol +0 -816
  33. package/test/REVLoansFeeRecovery.t.sol +0 -783
  34. package/test/REVLoansFindings.t.sol +0 -711
  35. package/test/REVLoansRegressions.t.sol +0 -364
  36. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  37. package/test/REVLoansSourced.t.sol +0 -1839
  38. package/test/REVLoansUnSourced.t.sol +0 -409
  39. package/test/TestAuditFixVerification.t.sol +0 -675
  40. package/test/TestBurnHeldTokens.t.sol +0 -394
  41. package/test/TestCEIPattern.t.sol +0 -508
  42. package/test/TestCashOutCallerValidation.t.sol +0 -452
  43. package/test/TestConversionDocumentation.t.sol +0 -365
  44. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  45. package/test/TestCrossSourceReallocation.t.sol +0 -361
  46. package/test/TestERC2771MetaTx.t.sol +0 -585
  47. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  48. package/test/TestFlashLoanSurplus.t.sol +0 -365
  49. package/test/TestHiddenTokens.t.sol +0 -474
  50. package/test/TestHookArrayOOB.t.sol +0 -278
  51. package/test/TestLiquidationBehavior.t.sol +0 -398
  52. package/test/TestLoanSourceRotation.t.sol +0 -553
  53. package/test/TestLoansCashOutDelay.t.sol +0 -493
  54. package/test/TestLongTailEconomics.t.sol +0 -677
  55. package/test/TestLowFindings.t.sol +0 -677
  56. package/test/TestMixedFixes.t.sol +0 -593
  57. package/test/TestPermit2Signatures.t.sol +0 -683
  58. package/test/TestReallocationSandwich.t.sol +0 -412
  59. package/test/TestRevnetRegressions.t.sol +0 -350
  60. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  61. package/test/TestSplitWeightE2E.t.sol +0 -605
  62. package/test/TestSplitWeightFork.t.sol +0 -855
  63. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  64. package/test/TestSwapTerminalPermission.t.sol +0 -262
  65. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  66. package/test/TestUint112Overflow.t.sol +0 -311
  67. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  68. package/test/TestZeroRepayment.t.sol +0 -354
  69. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  70. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  71. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  72. package/test/audit/NemesisVerification.t.sol +0 -97
  73. package/test/audit/OperatorDelegation.t.sol +0 -356
  74. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  75. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  76. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  77. package/test/audit/ReallocatePermission.t.sol +0 -363
  78. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  79. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  80. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  81. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  82. package/test/fork/ForkTestBase.sol +0 -727
  83. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  84. package/test/fork/TestCashOutFork.t.sol +0 -253
  85. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  86. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  87. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  88. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  89. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  90. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  91. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  92. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  93. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  94. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  95. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  96. package/test/helpers/MaliciousContracts.sol +0 -247
  97. package/test/helpers/REVEmpty721Config.sol +0 -45
  98. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  99. package/test/mock/MockBuybackDataHook.sol +0 -112
  100. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  101. package/test/mock/MockSuckerRegistry.sol +0 -17
  102. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  103. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  104. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  105. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  106. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  107. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
package/CHANGELOG.md CHANGED
@@ -52,8 +52,8 @@ This file describes the verified change from `revnet-core-v5` to the current `re
52
52
  - `IREVDeployer.deployFor(...)` now has overloads that return `(uint256, IJB721TiersHook)`.
53
53
  - `IREVDeployer.BUYBACK_HOOK()`, `LOANS()`, and `OWNER()` are explicit v6 surface area.
54
54
  - `IREVOwner` is a new interface and runtime counterpart to the deployer.
55
- - `IREVHiddenTokens` is a new interface for temporary token hiding (burn to exclude from totalSupply, re-mint on reveal).
56
- - `REVHiddenTokens` is a new standalone contract that lets holders temporarily hide tokens to increase cash-out value for remaining holders.
55
+ - `IREVHiddenTokens` is a new interface for temporary token hiding (burn to exclude from live totalSupply, re-mint on reveal).
56
+ - `REVHiddenTokens` is a new standalone contract that lets holders temporarily hide tokens from visible/governance supply while `REVOwner` and `REVLoans` keep hidden balances in economic denominators.
57
57
  - The old caller-supplied `REVBuybackHookConfig` path is no longer part of the deployer interface.
58
58
 
59
59
  ## Breaking ABI changes
package/README.md CHANGED
@@ -19,7 +19,8 @@ This package provides:
19
19
  - a deployer that launches Revnets and stores their long-lived configuration
20
20
  - a runtime hook that mediates pay, cash-out, mint-permission, and delayed-cash-out behavior
21
21
  - a loan system that burns token collateral on borrow and remints on repayment
22
- - a hidden-token system that temporarily removes tokens from visible supply
22
+ - a hidden-token system that temporarily removes tokens from visible supply while preserving economic claim
23
+ denominators
23
24
 
24
25
  It also composes with the 721 hook stack, buyback hook, router terminal, Croptop, and suckers where needed.
25
26
 
@@ -32,7 +33,7 @@ Use this repo when the product is a treasury-backed network with encoded stage t
32
33
  | `REVDeployer` | Launches and configures Revnets, stages, split operators, and optional auxiliary features. |
33
34
  | `REVOwner` | Runtime data-hook and cash-out-hook surface used by active Revnets. |
34
35
  | `REVLoans` | Loan surface that lets users borrow against Revnet tokens with burned collateral and NFT loan positions. |
35
- | `REVHiddenTokens` | Lets token holders temporarily hide tokens, excluding them from visible supply until reveal. |
36
+ | `REVHiddenTokens` | Lets token holders temporarily hide tokens from visible/governance supply until reveal, while cash-out and loan denominators still count hidden supply. |
36
37
 
37
38
  ## Mental Model
38
39
 
@@ -71,7 +72,7 @@ Most mistakes come from assuming a deploy-time parameter can be changed later or
71
72
  2. `test/REVLoans.invariants.t.sol`
72
73
  3. `test/TestLongTailEconomics.t.sol`
73
74
  4. `test/fork/TestLoanBorrowFork.t.sol`
74
- 5. `test/audit/CodexPhantomSurplusTerminal.t.sol`
75
+ 5. `test/audit/PhantomSurplusTerminal.t.sol`
75
76
 
76
77
  ## Install
77
78
 
@@ -83,16 +84,14 @@ npm install @rev-net/core-v6
83
84
 
84
85
  ```bash
85
86
  npm install
86
- forge build
87
- forge test
87
+ forge build --deny notes
88
+ forge test --deny notes
88
89
  ```
89
90
 
90
91
  Useful scripts:
91
92
 
92
93
  - `npm run deploy:mainnets`
93
94
  - `npm run deploy:testnets`
94
- - `npm run deploy:mainnets:1_1`
95
- - `npm run deploy:testnets:1_1`
96
95
 
97
96
  ## Deployment Notes
98
97
 
package/foundry.toml CHANGED
@@ -22,7 +22,7 @@ runs = 64
22
22
  depth = 50
23
23
 
24
24
  [lint]
25
- exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
25
+ exclude_lints = ["mixed-case-variable", "pascal-case-struct"]
26
26
  lint_on_build = false
27
27
 
28
28
  [rpc_endpoints]
package/package.json CHANGED
@@ -1,11 +1,20 @@
1
1
  {
2
2
  "name": "@rev-net/core-v6",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/rev-net/revnet-core-v6"
8
8
  },
9
+ "files": [
10
+ "CHANGELOG.md",
11
+ "foundry.toml",
12
+ "references/",
13
+ "remappings.txt",
14
+ "script/Deploy.s.sol",
15
+ "script/helpers/",
16
+ "src/"
17
+ ],
9
18
  "engines": {
10
19
  "node": ">=20.0.0"
11
20
  },
@@ -13,26 +22,24 @@
13
22
  "test": "forge test",
14
23
  "coverage": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
15
24
  "deploy:mainnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
16
- "deploy:mainnets:1_1": "source ./.env && npx sphinx propose ./script/Deploy1_1.s.sol --networks mainnets",
17
25
  "deploy:testnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks testnets",
18
- "deploy:testnets:1_1": "source ./.env && npx sphinx propose ./script/Deploy1_1.s.sol --networks testnets",
19
26
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
20
27
  },
21
28
  "dependencies": {
22
- "@bananapus/721-hook-v6": "^0.0.38",
23
- "@bananapus/buyback-hook-v6": "^0.0.30",
24
- "@bananapus/core-v6": "^0.0.36",
25
- "@bananapus/ownable-v6": "^0.0.20",
26
- "@bananapus/permission-ids-v6": "^0.0.19",
27
- "@bananapus/router-terminal-v6": "^0.0.30",
28
- "@bananapus/suckers-v6": "^0.0.28",
29
- "@croptop/core-v6": "^0.0.36",
30
- "@openzeppelin/contracts": "^5.6.1",
31
- "@uniswap/v4-core": "^1.0.2",
32
- "@uniswap/v4-periphery": "^1.0.3"
29
+ "@bananapus/721-hook-v6": "0.0.43",
30
+ "@bananapus/buyback-hook-v6": "0.0.37",
31
+ "@bananapus/core-v6": "0.0.39",
32
+ "@bananapus/ownable-v6": "0.0.24",
33
+ "@bananapus/permission-ids-v6": "0.0.22",
34
+ "@bananapus/router-terminal-v6": "0.0.36",
35
+ "@bananapus/suckers-v6": "0.0.33",
36
+ "@croptop/core-v6": "0.0.39",
37
+ "@openzeppelin/contracts": "5.6.1",
38
+ "@uniswap/permit2": "github:Uniswap/permit2#cc56ad0f3439c502c246fc5cfcc3db92bb8b7219"
33
39
  },
34
40
  "devDependencies": {
35
- "@bananapus/address-registry-v6": "^0.0.17",
36
- "@sphinx-labs/plugins": "^0.33.2"
41
+ "@bananapus/address-registry-v6": "0.0.25",
42
+ "@sphinx-labs/plugins": "0.33.3",
43
+ "@uniswap/v4-core": "1.0.2"
37
44
  }
38
45
  }
@@ -143,7 +143,7 @@ Use this file when you need revnet-specific risks, state reads, constants, or ex
143
143
  19. **39.16% cash-out tax crossover.** Below ~39% cash-out tax, cashing out is more capital-efficient than borrowing. Above ~39%, loans become more efficient because they preserve upside while providing liquidity. Based on CryptoEconLab academic research. Design implication: revnets intended for active token trading should consider this threshold when setting `cashOutTaxRate`.
144
144
  20. **REVDeployer always deploys a 721 hook** via `HOOK_DEPLOYER.deployHookFor` — even if `baseline721HookConfiguration` has empty tiers. This is correct by design: it lets the split operator add and sell NFTs later without migration. Non-revnet projects should follow the same pattern by using `JB721TiersHookProjectDeployer.launchProjectFor` (or `JBOmnichainDeployer.launchProjectFor`) instead of bare `launchProjectFor`.
145
145
  21. **REVOwner deployer binding is precomputed.** REVOwner records the account that created it as an internal one-time binder. That account must call `setDeployer(precomputedRevDeployerAddress)` exactly once before the canonical REVDeployer is deployed. This avoids an ambient public initializer while keeping the circular dependency manageable. If `setDeployer(...)` is never called, all DEPLOYER-gated runtime configuration breaks.
146
- 22. **Hidden tokens are economic, not cosmetic.** Hiding burns visible tokens and lowers visible supply until reveal. That changes cash-out and loan-relative economics for everyone else.
146
+ 22. **Hidden tokens are economic, not cosmetic.** Hiding burns visible tokens and lowers visible/governance supply until reveal. Cash-out and loan denominators still include hidden balances because they are recoverable claims.
147
147
 
148
148
  ### NATIVE_TOKEN Accounting on Non-ETH Chains
149
149
 
@@ -102,6 +102,6 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
102
102
 
103
103
  | Function | Permissions | What it does |
104
104
  |----------|------------|-------------|
105
- | `REVHiddenTokens.hideTokensOf(revnetId, tokenCount, holder)` | Holder only. The holder must either be allowlisted or personally hold `HIDE_TOKENS`. | Burns visible tokens, increases hidden balance, and lowers visible supply. |
105
+ | `REVHiddenTokens.hideTokensOf(revnetId, tokenCount, holder)` | Holder only. The holder must either be allowlisted or personally hold `HIDE_TOKENS`. | Burns visible tokens, increases hidden balance, and lowers visible supply. Cash-out and loan denominators still include the hidden balance while it is revealable. |
106
106
  | `REVHiddenTokens.revealTokensOf(revnetId, tokenCount, holder)` | Holder only | Re-mints previously hidden tokens back to the holder and reduces hidden balance. |
107
107
  | `REVHiddenTokens.setTokenHidingAllowedFor(revnetId, holder, isAllowed)` | Operator with `HIDE_TOKENS` | Allows or revokes a holder's ability to hide their own tokens. |
@@ -289,14 +289,16 @@ contract DeployScript is Script, Sphinx {
289
289
  if (block.chainid == 1 || block.chainid == 11_155_111) {
290
290
  suckerDeployerConfigurations = new JBSuckerDeployerConfig[](3);
291
291
  // OP
292
- suckerDeployerConfigurations[0] =
293
- JBSuckerDeployerConfig({deployer: suckers.optimismDeployer, mappings: tokenMappings});
292
+ suckerDeployerConfigurations[0] = JBSuckerDeployerConfig({
293
+ deployer: suckers.optimismDeployer, peer: bytes32(0), mappings: tokenMappings
294
+ });
294
295
 
295
296
  suckerDeployerConfigurations[1] =
296
- JBSuckerDeployerConfig({deployer: suckers.baseDeployer, mappings: tokenMappings});
297
+ JBSuckerDeployerConfig({deployer: suckers.baseDeployer, peer: bytes32(0), mappings: tokenMappings});
297
298
 
298
- suckerDeployerConfigurations[2] =
299
- JBSuckerDeployerConfig({deployer: suckers.arbitrumDeployer, mappings: tokenMappings});
299
+ suckerDeployerConfigurations[2] = JBSuckerDeployerConfig({
300
+ deployer: suckers.arbitrumDeployer, peer: bytes32(0), mappings: tokenMappings
301
+ });
300
302
  } else {
301
303
  suckerDeployerConfigurations = new JBSuckerDeployerConfig[](1);
302
304
  // L2 -> Mainnet
@@ -304,6 +306,7 @@ contract DeployScript is Script, Sphinx {
304
306
  deployer: address(suckers.optimismDeployer) != address(0)
305
307
  ? suckers.optimismDeployer
306
308
  : address(suckers.baseDeployer) != address(0) ? suckers.baseDeployer : suckers.arbitrumDeployer,
309
+ peer: bytes32(0),
307
310
  mappings: tokenMappings
308
311
  });
309
312
 
@@ -493,8 +496,8 @@ contract DeployScript is Script, Sphinx {
493
496
  directory: core.controller.DIRECTORY(),
494
497
  feeRevnetId: FEE_PROJECT_ID,
495
498
  suckerRegistry: suckers.registry,
496
- loans: address(revloans),
497
- hiddenTokens: address(revHiddenTokens)
499
+ loans: revloans,
500
+ hiddenTokens: revHiddenTokens
498
501
  });
499
502
 
500
503
  // Deploy REVDeployer with the REVLoans, buyback hook, and REVOwner addresses.
@@ -510,7 +513,7 @@ contract DeployScript is Script, Sphinx {
510
513
  hook.hook_deployer,
511
514
  croptop.publisher,
512
515
  IJBBuybackHookRegistry(address(buybackHook.registry)),
513
- address(revloans),
516
+ revloans,
514
517
  TRUSTED_FORWARDER,
515
518
  address(revOwner)
516
519
  )
@@ -527,7 +530,7 @@ contract DeployScript is Script, Sphinx {
527
530
  hookDeployer: hook.hook_deployer,
528
531
  publisher: croptop.publisher,
529
532
  buybackHook: IJBBuybackHookRegistry(address(buybackHook.registry)),
530
- loans: address(revloans),
533
+ loans: revloans,
531
534
  trustedForwarder: TRUSTED_FORWARDER,
532
535
  owner: address(revOwner)
533
536
  });
@@ -22,6 +22,7 @@ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
22
22
  import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
23
23
  import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
24
24
  import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
25
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
25
26
  import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
26
27
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
27
28
  import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
@@ -33,6 +34,7 @@ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Recei
33
34
  import {mulDiv, sqrt} from "@prb/math/src/Common.sol";
34
35
 
35
36
  import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
37
+ import {IREVLoans} from "./interfaces/IREVLoans.sol";
36
38
  import {REVOwner} from "./REVOwner.sol";
37
39
  import {REVAutoIssuance} from "./structs/REVAutoIssuance.sol";
38
40
  import {REVConfig} from "./structs/REVConfig.sol";
@@ -110,7 +112,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
110
112
  /// @notice The loan contract used by all revnets.
111
113
  /// @dev Revnets can offer loans to their participants, collateralized by their tokens.
112
114
  /// Participants can borrow up to the current cash out value of their tokens.
113
- address public immutable override LOANS;
115
+ IREVLoans public immutable override LOANS;
114
116
 
115
117
  /// @notice The runtime data hook contract that handles pay and cash out callbacks for revnets.
116
118
  /// @dev Set as `dataHook` in each revnet's ruleset metadata. Implements `IJBRulesetDataHook` and `IJBCashOutHook`.
@@ -178,7 +180,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
178
180
  IJB721TiersHookDeployer hookDeployer,
179
181
  CTPublisher publisher,
180
182
  IJBBuybackHookRegistry buybackHook,
181
- address loans,
183
+ IREVLoans loans,
182
184
  address trustedForwarder,
183
185
  address owner
184
186
  )
@@ -193,20 +195,14 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
193
195
  HOOK_DEPLOYER = hookDeployer;
194
196
  PUBLISHER = publisher;
195
197
  BUYBACK_HOOK = buybackHook;
196
- // slither-disable-next-line missing-zero-check
197
198
  LOANS = loans;
198
199
  // slither-disable-next-line missing-zero-check
199
200
  OWNER = owner;
200
201
 
201
- // Give the sucker registry permission to map tokens for all revnets.
202
- _setPermission({
203
- operator: address(SUCKER_REGISTRY), revnetId: 0, permissionId: JBPermissionIds.MAP_SUCKER_TOKEN
204
- });
205
-
206
202
  // Give the loan contract permission to use the surplus allowance of all revnets.
207
203
  // Uses wildcard revnetId=0 intentionally — the loan contract is a singleton shared by all revnets,
208
204
  // and each revnet's surplus allowance limits already constrain how much can be drawn.
209
- _setPermission({operator: LOANS, revnetId: 0, permissionId: JBPermissionIds.USE_ALLOWANCE});
205
+ _setPermission({operator: address(LOANS), revnetId: 0, permissionId: JBPermissionIds.USE_ALLOWANCE});
210
206
 
211
207
  // Give the buyback hook (registry) permission to configure pools on all revnets.
212
208
  _setPermission({operator: address(BUYBACK_HOOK), revnetId: 0, permissionId: JBPermissionIds.SET_BUYBACK_POOL});
@@ -356,12 +352,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
356
352
  });
357
353
  }
358
354
 
359
- /// @notice Returns the next project ID.
360
- /// @return nextProjectId The next project ID.
361
- function _nextProjectId() internal view returns (uint256) {
362
- return PROJECTS.count() + 1;
363
- }
364
-
365
355
  /// @notice Returns the permissions that the split operator should be granted for a revnet.
366
356
  /// @param revnetId The ID of the revnet to get split operator permissions for.
367
357
  /// @return allOperatorPermissions The permissions that the split operator should be granted for the revnet,
@@ -375,22 +365,21 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
375
365
  uint256[] memory customSplitOperatorPermissionIndexes = _extraOperatorPermissions[revnetId];
376
366
 
377
367
  // Make the array that merges the default and custom operator permissions.
378
- allOperatorPermissions = new uint256[](11 + customSplitOperatorPermissionIndexes.length);
368
+ allOperatorPermissions = new uint256[](10 + customSplitOperatorPermissionIndexes.length);
379
369
  allOperatorPermissions[0] = JBPermissionIds.SET_SPLIT_GROUPS;
380
370
  allOperatorPermissions[1] = JBPermissionIds.SET_BUYBACK_POOL;
381
371
  allOperatorPermissions[2] = JBPermissionIds.SET_BUYBACK_TWAP;
382
372
  allOperatorPermissions[3] = JBPermissionIds.SET_PROJECT_URI;
383
- allOperatorPermissions[4] = JBPermissionIds.ADD_PRICE_FEED;
384
- allOperatorPermissions[5] = JBPermissionIds.SUCKER_SAFETY;
385
- allOperatorPermissions[6] = JBPermissionIds.SET_BUYBACK_HOOK;
386
- allOperatorPermissions[7] = JBPermissionIds.SET_ROUTER_TERMINAL;
387
- allOperatorPermissions[8] = JBPermissionIds.SET_TOKEN_METADATA;
388
- allOperatorPermissions[9] = JBPermissionIds.SIGN_FOR_ERC20;
389
- allOperatorPermissions[10] = JBPermissionIds.HIDE_TOKENS;
373
+ allOperatorPermissions[4] = JBPermissionIds.SUCKER_SAFETY;
374
+ allOperatorPermissions[5] = JBPermissionIds.SET_BUYBACK_HOOK;
375
+ allOperatorPermissions[6] = JBPermissionIds.SET_ROUTER_TERMINAL;
376
+ allOperatorPermissions[7] = JBPermissionIds.SET_TOKEN_METADATA;
377
+ allOperatorPermissions[8] = JBPermissionIds.SIGN_FOR_ERC20;
378
+ allOperatorPermissions[9] = JBPermissionIds.HIDE_TOKENS;
390
379
 
391
380
  // Copy the custom permissions into the array.
392
381
  for (uint256 i; i < customSplitOperatorPermissionIndexes.length;) {
393
- allOperatorPermissions[11 + i] = customSplitOperatorPermissionIndexes[i];
382
+ allOperatorPermissions[10 + i] = customSplitOperatorPermissionIndexes[i];
394
383
  unchecked {
395
384
  ++i;
396
385
  }
@@ -510,6 +499,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
510
499
  /// @param allowedPosts Restrictions on which croptop posts are allowed on the revnet's ERC-721 tiers.
511
500
  /// @return revnetId The ID of the newly created revnet.
512
501
  /// @return hook The address of the tiered ERC-721 hook that was deployed for the revnet.
502
+ // The deployment flow makes external setup calls, but any observed state is revnet-scoped and reverts atomically.
503
+ // slither-disable-next-line reentrancy-benign
513
504
  function deployFor(
514
505
  uint256 revnetId,
515
506
  REVConfig calldata configuration,
@@ -525,9 +516,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
525
516
  // Keep a reference to the revnet ID which was passed in.
526
517
  bool shouldDeployNewRevnet = revnetId == 0;
527
518
 
528
- // If the caller is deploying a new revnet, calculate its ID
529
- // (which will be 1 greater than the current count).
530
- if (shouldDeployNewRevnet) revnetId = _nextProjectId();
519
+ // If the caller is deploying a new revnet, reserve its project ID before deriving hook/sucker config.
520
+ if (shouldDeployNewRevnet) revnetId = PROJECTS.createFor(address(this));
531
521
 
532
522
  // Deploy the revnet with the specified tiered ERC-721 hook and croptop posting criteria.
533
523
  hook = _deploy721RevnetFor({
@@ -544,6 +534,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
544
534
  }
545
535
 
546
536
  /// @inheritdoc IREVDeployer
537
+ // The deployment flow makes external setup calls, but any observed state is revnet-scoped and reverts atomically.
538
+ // slither-disable-next-line reentrancy-benign
547
539
  function deployFor(
548
540
  uint256 revnetId,
549
541
  REVConfig calldata configuration,
@@ -555,7 +547,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
555
547
  returns (uint256, IJB721TiersHook hook)
556
548
  {
557
549
  bool shouldDeployNewRevnet = revnetId == 0;
558
- if (shouldDeployNewRevnet) revnetId = _nextProjectId();
550
+ if (shouldDeployNewRevnet) revnetId = PROJECTS.createFor(address(this));
559
551
 
560
552
  // Deploy the revnet (project, rulesets, ERC-20, suckers, etc.).
561
553
  bytes32 encodedConfigurationHash = _deployRevnetFor({
@@ -583,6 +575,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
583
575
  REVOwner(OWNER).setTiered721HookOf({revnetId: revnetId, hook: hook});
584
576
 
585
577
  // Grant the split operator all 721 permissions (no prevent* flags for default config).
578
+ // These permission IDs are only consumed by `_setSplitOperatorOf` below, after revnet setup has either
579
+ // completed or reverted atomically.
586
580
  _extraOperatorPermissions[revnetId].push(JBPermissionIds.ADJUST_721_TIERS);
587
581
  _extraOperatorPermissions[revnetId].push(JBPermissionIds.SET_721_METADATA);
588
582
  _extraOperatorPermissions[revnetId].push(JBPermissionIds.MINT_721);
@@ -656,6 +650,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
656
650
  //*********************************************************************//
657
651
 
658
652
  /// @notice Deploy a revnet which sells tiered ERC-721s and (optionally) allows croptop posts to its ERC-721 tiers.
653
+ // The helper performs external hook/post setup after core revnet setup; any failure reverts the whole deployment.
654
+ // slither-disable-next-line reentrancy-benign
659
655
  function _deploy721RevnetFor(
660
656
  uint256 revnetId,
661
657
  bool shouldDeployNewRevnet,
@@ -706,6 +702,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
706
702
  // Store the tiered ERC-721 hook in the owner contract.
707
703
  REVOwner(OWNER).setTiered721HookOf({revnetId: revnetId, hook: hook});
708
704
 
705
+ // These permission IDs are only consumed by `_setSplitOperatorOf` below, after revnet setup has either
706
+ // completed or reverted atomically.
707
+
709
708
  // Give the split operator permission to add and remove tiers unless prevented.
710
709
  if (!tiered721HookConfiguration.preventSplitOperatorAdjustingTiers) {
711
710
  _extraOperatorPermissions[revnetId].push(JBPermissionIds.ADJUST_721_TIERS);
@@ -773,8 +772,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
773
772
  /// uninitialized.
774
773
  /// - The project's JBProjects NFT is permanently transferred to this contract. This is irreversible.
775
774
  /// @param revnetId The ID of the Juicebox project to initialize as a revnet. Send 0 to deploy a new revnet.
776
- /// @param shouldDeployNewRevnet Whether to deploy a new revnet or convert an existing Juicebox project into a
777
- /// revnet.
775
+ /// @param shouldDeployNewRevnet Whether the revnet ID was reserved by this deployment call, or the caller is
776
+ /// converting an existing Juicebox project into a revnet.
778
777
  /// @param configuration Core revnet configuration. See `REVConfig`.
779
778
  /// @param terminalConfigurations The terminals to set up for the revnet. Used for payments and cash outs.
780
779
  /// @param suckerDeploymentConfiguration The suckers to set up for the revnet. Suckers facilitate cross-chain
@@ -795,43 +794,37 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
795
794
  (rulesetConfigurations, encodedConfigurationHash) = _makeRulesetConfigurations({
796
795
  revnetId: revnetId, configuration: configuration, terminalConfigurations: terminalConfigurations
797
796
  });
798
- if (shouldDeployNewRevnet) {
799
- // If we're deploying a new revnet, launch a Juicebox project for it.
800
- // Sanity check that we deployed the `revnetId` that we expected to deploy.
801
- // slither-disable-next-line incorrect-equality,reentrancy-benign,reentrancy-events
802
- assert(
803
- CONTROLLER.launchProjectFor({
804
- owner: address(this),
805
- projectUri: configuration.description.uri,
806
- rulesetConfigurations: rulesetConfigurations,
807
- terminalConfigurations: terminalConfigurations,
808
- memo: ""
809
- }) == revnetId
810
- );
811
- } else {
797
+
798
+ address owner;
799
+ if (!shouldDeployNewRevnet) {
812
800
  // Keep a reference to the Juicebox project's owner.
813
- address owner = PROJECTS.ownerOf(revnetId);
801
+ owner = PROJECTS.ownerOf(revnetId);
814
802
 
815
803
  // Make sure the caller is the owner of the Juicebox project.
816
804
  if (_msgSender() != owner) revert REVDeployer_Unauthorized(revnetId, _msgSender());
805
+ }
817
806
 
818
- // Initialize the existing Juicebox project as a revnet by
819
- // transferring the `JBProjects` NFT to this deployer. This is irreversible.
820
- IERC721(PROJECTS).safeTransferFrom({from: owner, to: address(this), tokenId: revnetId});
821
-
822
- // Launch the revnet rulesets for the pre-existing project.
823
- // slither-disable-next-line unused-return
824
- CONTROLLER.launchRulesetsFor({
825
- projectId: revnetId,
826
- rulesetConfigurations: rulesetConfigurations,
827
- terminalConfigurations: terminalConfigurations,
828
- memo: ""
829
- });
807
+ // Store the hash before setup callbacks so reentrant readers cannot observe a zero configuration hash. Any
808
+ // subsequent revert rolls this write back.
809
+ hashedEncodedConfigurationOf[revnetId] = encodedConfigurationHash;
830
810
 
831
- // Set the revnet's URI.
832
- CONTROLLER.setUriOf({projectId: revnetId, uri: configuration.description.uri});
811
+ if (!shouldDeployNewRevnet) {
812
+ // Initialize the existing Juicebox project as a revnet by transferring the `JBProjects` NFT to this
813
+ // deployer. This is irreversible.
814
+ // slither-disable-next-line reentrancy-benign
815
+ IERC721(PROJECTS).safeTransferFrom({from: owner, to: address(this), tokenId: revnetId});
833
816
  }
834
817
 
818
+ // Launch the revnet rulesets for the reserved or pre-existing blank project.
819
+ // slither-disable-next-line unused-return
820
+ CONTROLLER.launchRulesetsFor({
821
+ projectId: revnetId,
822
+ projectUri: configuration.description.uri,
823
+ rulesetConfigurations: rulesetConfigurations,
824
+ terminalConfigurations: terminalConfigurations,
825
+ memo: ""
826
+ });
827
+
835
828
  // Store the cash out delay of the revnet if its stages are already in progress.
836
829
  // This prevents cash out liquidity/arbitrage issues for existing revnets which
837
830
  // are deploying to a new chain.
@@ -875,9 +868,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
875
868
  });
876
869
  }
877
870
 
878
- // Store the hashed encoded configuration.
879
- hashedEncodedConfigurationOf[revnetId] = encodedConfigurationHash;
880
-
881
871
  emit DeployRevnet({
882
872
  revnetId: revnetId,
883
873
  configuration: configuration,
@@ -908,7 +898,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
908
898
  caller: _msgSender()
909
899
  });
910
900
 
911
- // Deploy the suckers.
901
+ // Include the caller so two revnets with identical configuration and user salt cannot collide. Same-address
902
+ // cross-chain deployment still works when the same operator calls this helper on each chain.
912
903
  // slither-disable-next-line unused-return
913
904
  suckers = SUCKER_REGISTRY.deploySuckersFor({
914
905
  projectId: revnetId,
@@ -1014,7 +1005,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
1014
1005
  fundAccessLimitGroups: fundAccessLimitGroups
1015
1006
  });
1016
1007
 
1017
- // Add the stage's properties to the byte-encoded configuration.
1008
+ // Add the stage's immutable economics to the byte-encoded configuration. `extraMetadata` is Juicebox
1009
+ // ruleset metadata forwarded to the revnet data hook, so it remains part of the revnet identity.
1010
+ // Reserved-token split recipients and individual weights are intentionally excluded below: the split
1011
+ // limit (`splitPercent`) is the economic commitment, while split routing can change over time.
1018
1012
  encodedConfiguration = abi.encode(
1019
1013
  encodedConfiguration,
1020
1014
  // Use the effective start time (normalized from 0 to block.timestamp for the first stage).
@@ -1024,7 +1018,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
1024
1018
  stageConfiguration.initialIssuance,
1025
1019
  stageConfiguration.issuanceCutFrequency,
1026
1020
  stageConfiguration.issuanceCutPercent,
1027
- stageConfiguration.cashOutTaxRate
1021
+ stageConfiguration.cashOutTaxRate,
1022
+ stageConfiguration.extraMetadata
1028
1023
  );
1029
1024
 
1030
1025
  // Add each auto-mint to the byte-encoded representation.
@@ -13,8 +13,8 @@ import {Context} from "@openzeppelin/contracts/utils/Context.sol";
13
13
  import {IREVHiddenTokens} from "./interfaces/IREVHiddenTokens.sol";
14
14
 
15
15
  /// @notice Allows authorized operators to hide (burn) revnet tokens on behalf of holders, excluding them from
16
- /// governance weight. Hidden tokens are burned from circulating supply, so they also stop contributing to
17
- /// cash-out and borrow valuations until revealed again.
16
+ /// governance weight. Hidden tokens are burned from live circulating supply, but `REVOwner` and `REVLoans` add the
17
+ /// hidden supply back into their economic denominators while those tokens remain revealable.
18
18
  /// Hidden tokens can be revealed (re-minted) at any time.
19
19
  contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
20
20
  //*********************************************************************//
package/src/REVLoans.sol CHANGED
@@ -359,10 +359,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
359
359
  // Get the total amount of tokens in circulation.
360
360
  uint256 totalSupply = CONTROLLER.totalTokenSupplyWithReservedTokensOf(revnetId);
361
361
 
362
- // Get a refeerence to the collateral being used to secure loans.
362
+ // Get a reference to the collateral being used to secure loans.
363
363
  uint256 totalCollateral = totalCollateralOf[revnetId];
364
364
 
365
- // The local supply includes both circulating tokens and tokens locked as loan collateral.
365
+ // Hidden tokens are intentionally excluded from borrowing math. Operators can hide tokens as a security
366
+ // handle without changing the fair loan market for visible token holders.
366
367
  uint256 localSupply = totalSupply + totalCollateral;
367
368
 
368
369
  // The local surplus includes both the treasury surplus and the outstanding borrowed amounts.
@@ -469,10 +470,13 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
469
470
  revert REVLoans_LoanExpired(timeSinceLoanCreated, LOAN_LIQUIDATION_DURATION);
470
471
  }
471
472
 
472
- // Get a reference to the amount prepaid for the full loan.
473
- uint256 prepaid = JBFees.feeAmountFrom({amountBeforeFee: loan.amount, feePercent: loan.prepaidFeePercent});
473
+ // Get a reference to the amount prepaid for the full loan. This is an app-level loan fee, so keep it
474
+ // floor-rounded instead of applying the protocol fee helper's dust minimum.
475
+ uint256 prepaid = JBFees.feeAmountFromFloor({amountBeforeFee: loan.amount, feePercent: loan.prepaidFeePercent});
474
476
 
475
- uint256 fullSourceFeeAmount = JBFees.feeAmountFrom({
477
+ // This source fee ramps with elapsed time. Use the floor-rounded fee helper so a one-second elapsed window
478
+ // with zero fee percent stays free instead of inheriting the protocol fee helper's anti-dust minimum.
479
+ uint256 fullSourceFeeAmount = JBFees.feeAmountFromFloor({
476
480
  amountBeforeFee: loan.amount - prepaid,
477
481
  feePercent: mulDiv({
478
482
  x: timeSinceLoanCreated - loan.prepaidDuration,
@@ -1017,10 +1021,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
1017
1021
  // Keep a reference to the fee terminal.
1018
1022
  IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: REV_ID, token: loan.source.token});
1019
1023
 
1020
- // Get the amount of additional fee to take for REV.
1024
+ // Get the amount of additional fee to take for REV. This is an app-level loan fee, not the terminal's
1025
+ // protocol fee, so keep it floor-rounded instead of applying the protocol fee helper's dust minimum.
1021
1026
  uint256 revFeeAmount = address(feeTerminal) == address(0)
1022
1027
  ? 0
1023
- : JBFees.feeAmountFrom({amountBeforeFee: addedBorrowAmount, feePercent: REV_PREPAID_FEE_PERCENT});
1028
+ : JBFees.feeAmountFromFloor({amountBeforeFee: addedBorrowAmount, feePercent: REV_PREPAID_FEE_PERCENT});
1024
1029
 
1025
1030
  // Try to pay the REV fee. If it fails, revFeeAmount is zeroed so the borrower receives it instead.
1026
1031
  if (revFeeAmount > 0) {
@@ -1238,9 +1243,11 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
1238
1243
  // Make sure the minimum borrow amount is met.
1239
1244
  if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
1240
1245
 
1241
- // Get the amount of additional fee to take for the revnet issuing the loan.
1242
- // Fee rounding may leave a few wei of dust economically insignificant relative to gas costs.
1243
- uint256 sourceFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
1246
+ // Get the amount of additional fee to take for the revnet issuing the loan. This is an app-level loan fee,
1247
+ // not the terminal's protocol fee, so keep it floor-rounded instead of applying the protocol fee helper's dust
1248
+ // minimum.
1249
+ uint256 sourceFeeAmount =
1250
+ JBFees.feeAmountFromFloor({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
1244
1251
 
1245
1252
  // Borrow the amount.
1246
1253
  _adjust({