@rev-net/core-v6 0.0.11 → 0.0.13

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 (81) hide show
  1. package/ADMINISTRATION.md +7 -7
  2. package/ARCHITECTURE.md +11 -11
  3. package/AUDIT_INSTRUCTIONS.md +295 -0
  4. package/CHANGE_LOG.md +316 -0
  5. package/README.md +9 -6
  6. package/RISKS.md +180 -35
  7. package/SKILLS.md +9 -11
  8. package/STYLE_GUIDE.md +14 -1
  9. package/USER_JOURNEYS.md +489 -0
  10. package/package.json +9 -9
  11. package/script/Deploy.s.sol +124 -40
  12. package/script/helpers/RevnetCoreDeploymentLib.sol +19 -6
  13. package/src/REVDeployer.sol +183 -175
  14. package/src/REVLoans.sol +65 -28
  15. package/src/interfaces/IREVDeployer.sol +25 -23
  16. package/src/structs/REV721TiersHookFlags.sol +1 -0
  17. package/src/structs/REVAutoIssuance.sol +1 -0
  18. package/src/structs/REVBaseline721HookConfig.sol +1 -0
  19. package/src/structs/REVConfig.sol +1 -0
  20. package/src/structs/REVCroptopAllowedPost.sol +1 -0
  21. package/src/structs/REVDeploy721TiersHookConfig.sol +13 -14
  22. package/src/structs/REVDescription.sol +1 -0
  23. package/src/structs/REVLoan.sol +1 -0
  24. package/src/structs/REVLoanSource.sol +1 -0
  25. package/src/structs/REVStageConfig.sol +1 -0
  26. package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
  27. package/test/REV.integrations.t.sol +148 -19
  28. package/test/REVAutoIssuanceFuzz.t.sol +31 -6
  29. package/test/REVDeployerRegressions.t.sol +47 -9
  30. package/test/REVInvincibility.t.sol +83 -19
  31. package/test/REVInvincibilityHandler.sol +29 -0
  32. package/test/REVLifecycle.t.sol +36 -6
  33. package/test/REVLoans.invariants.t.sol +64 -10
  34. package/test/REVLoansAttacks.t.sol +54 -9
  35. package/test/REVLoansFeeRecovery.t.sol +61 -15
  36. package/test/REVLoansFindings.t.sol +42 -9
  37. package/test/REVLoansRegressions.t.sol +33 -6
  38. package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
  39. package/test/REVLoansSourced.t.sol +79 -17
  40. package/test/REVLoansUnSourced.t.sol +61 -10
  41. package/test/TestBurnHeldTokens.t.sol +47 -11
  42. package/test/TestCEIPattern.t.sol +37 -6
  43. package/test/TestCashOutCallerValidation.t.sol +41 -8
  44. package/test/TestConversionDocumentation.t.sol +50 -13
  45. package/test/TestCrossCurrencyReclaim.t.sol +584 -0
  46. package/test/TestCrossSourceReallocation.t.sol +37 -6
  47. package/test/TestERC2771MetaTx.t.sol +557 -0
  48. package/test/TestEmptyBuybackSpecs.t.sol +45 -10
  49. package/test/TestFlashLoanSurplus.t.sol +39 -7
  50. package/test/TestHookArrayOOB.t.sol +42 -13
  51. package/test/TestLiquidationBehavior.t.sol +37 -7
  52. package/test/TestLoanSourceRotation.t.sol +525 -0
  53. package/test/TestLongTailEconomics.t.sol +651 -0
  54. package/test/TestLowFindings.t.sol +80 -8
  55. package/test/TestMixedFixes.t.sol +43 -9
  56. package/test/TestPermit2Signatures.t.sol +657 -0
  57. package/test/TestReallocationSandwich.t.sol +384 -0
  58. package/test/TestRevnetRegressions.t.sol +324 -0
  59. package/test/TestSplitWeightAdjustment.t.sol +52 -13
  60. package/test/TestSplitWeightE2E.t.sol +53 -18
  61. package/test/TestSplitWeightFork.t.sol +66 -21
  62. package/test/TestStageTransitionBorrowable.t.sol +38 -6
  63. package/test/TestSwapTerminalPermission.t.sol +37 -7
  64. package/test/TestUint112Overflow.t.sol +39 -6
  65. package/test/TestZeroRepayment.t.sol +37 -6
  66. package/test/fork/ForkTestBase.sol +66 -17
  67. package/test/fork/TestCashOutFork.t.sol +9 -3
  68. package/test/fork/TestLoanBorrowFork.t.sol +1 -0
  69. package/test/fork/TestLoanCrossRulesetFork.t.sol +11 -3
  70. package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
  71. package/test/fork/TestLoanReallocateFork.t.sol +1 -0
  72. package/test/fork/TestLoanRepayFork.t.sol +1 -0
  73. package/test/fork/TestLoanTransferFork.t.sol +133 -0
  74. package/test/fork/TestSplitWeightFork.t.sol +3 -0
  75. package/test/helpers/REVEmpty721Config.sol +46 -0
  76. package/test/mock/MockBuybackDataHook.sol +1 -0
  77. package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
  78. package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
  79. package/test/regression/TestCumulativeLoanCounter.t.sol +38 -8
  80. package/test/regression/TestLiquidateGapHandling.t.sol +40 -8
  81. package/test/regression/TestZeroPriceFeed.t.sol +396 -0
package/CHANGE_LOG.md ADDED
@@ -0,0 +1,316 @@
1
+ # revnet-core-v6 Changelog (v5 -> v6)
2
+
3
+ This document describes all changes between `revnet-core` (v5, Solidity 0.8.23) and `revnet-core-v6` (v6, Solidity 0.8.26).
4
+
5
+ ---
6
+
7
+ ## 1. Breaking Changes
8
+
9
+ ### 1.1 Removed Structs
10
+
11
+ | Struct | Notes |
12
+ |--------|-------|
13
+ | `REVBuybackHookConfig` | Removed entirely. Buyback hook configuration is no longer passed by the caller. The deployer auto-configures buyback pools via an immutable `BUYBACK_HOOK` registry. |
14
+ | `REVBuybackPoolConfig` | Removed entirely. Was used within `REVBuybackHookConfig`. Buyback pools are now auto-initialized with default parameters. |
15
+
16
+ ### 1.2 Struct Field Changes
17
+
18
+ #### REVConfig
19
+
20
+ | Change | v5 | v6 |
21
+ |--------|----|----|
22
+ | `loanSources` field | `REVLoanSource[] loanSources` | Removed |
23
+ | `loans` field | `address loans` | Removed |
24
+
25
+ Loan sources and the loans contract address are no longer part of the per-revnet configuration. In v6, loans are managed via a single immutable `LOANS` address on the deployer, and fund access limits for loans are derived from terminal configurations rather than explicit loan sources.
26
+
27
+ #### REVDeploy721TiersHookConfig
28
+
29
+ | Change | v5 | v6 |
30
+ |--------|----|----|
31
+ | `baseline721HookConfiguration` type | `JBDeploy721TiersHookConfig` | `REVBaseline721HookConfig` |
32
+ | `splitOperatorCanAdjustTiers` | `bool splitOperatorCanAdjustTiers` | Renamed to `bool preventSplitOperatorAdjustingTiers` |
33
+ | `splitOperatorCanUpdateMetadata` | `bool splitOperatorCanUpdateMetadata` | Renamed to `bool preventSplitOperatorUpdatingMetadata` |
34
+ | `splitOperatorCanMint` | `bool splitOperatorCanMint` | Renamed to `bool preventSplitOperatorMinting` |
35
+ | `splitOperatorCanIncreaseDiscountPercent` | `bool splitOperatorCanIncreaseDiscountPercent` | Renamed to `bool preventSplitOperatorIncreasingDiscountPercent` |
36
+
37
+ The boolean semantics are **inverted**: v5 used opt-in flags (`splitOperatorCan*`), v6 uses opt-out flags (`preventSplitOperator*`). In v6, the permissions are granted by default unless explicitly prevented.
38
+
39
+ #### REVCroptopAllowedPost
40
+
41
+ | Change | v5 | v6 |
42
+ |--------|----|----|
43
+ | `maximumSplitPercent` field | Not present | Added: `uint32 maximumSplitPercent` |
44
+
45
+ ### 1.3 IREVDeployer Interface Changes
46
+
47
+ | Change | v5 | v6 |
48
+ |--------|----|----|
49
+ | `deployFor` (no 721s) return type | `returns (uint256)` | `returns (uint256, IJB721TiersHook)` |
50
+ | `deployFor` (no 721s) parameters | `(uint256, REVConfig, JBTerminalConfig[], REVBuybackHookConfig, REVSuckerDeploymentConfig)` | `(uint256, REVConfig, JBTerminalConfig[], REVSuckerDeploymentConfig)` |
51
+ | `deployWith721sFor` | `deployWith721sFor(uint256, REVConfig, JBTerminalConfig[], REVBuybackHookConfig, REVSuckerDeploymentConfig, REVDeploy721TiersHookConfig, REVCroptopAllowedPost[])` | Removed. Replaced by `deployFor` overload with 6 parameters. |
52
+ | `buybackHookOf` view | `buybackHookOf(uint256) returns (IJBRulesetDataHook)` | Removed. Replaced by immutable `BUYBACK_HOOK()`. |
53
+ | `loansOf` view | `loansOf(uint256) returns (address)` | Removed. Replaced by immutable `LOANS()`. |
54
+
55
+ ### 1.4 IREVLoans Interface Changes
56
+
57
+ | Change | v5 | v6 |
58
+ |--------|----|----|
59
+ | `REVNETS` view | `REVNETS() returns (IREVDeployer)` | Removed. The loans contract no longer stores a reference to the deployer. |
60
+ | `numberOfLoansFor` view | `numberOfLoansFor(uint256) returns (uint256)` | Renamed to `totalLoansBorrowedFor(uint256) returns (uint256)` |
61
+ | `reallocateCollateralFromLoan` mutability | `external payable` | `external` (not payable) |
62
+ | Constructor | `constructor(IREVDeployer, uint256, address, IPermit2, address)` | `constructor(IJBController, IJBProjects, uint256, address, IPermit2, address)` |
63
+
64
+ ### 1.5 Removed Errors
65
+
66
+ | Contract | v5 Error | v6 Replacement |
67
+ |----------|----------|----------------|
68
+ | `REVDeployer` | `REVDeployer_LoanSourceDoesntMatchTerminalConfigurations(address, address)` | Removed. Loan sources are no longer validated against terminal configurations. |
69
+ | `REVLoans` | `REVLoans_RevnetsMismatch(address, address)` | Replaced by `REVLoans_InvalidTerminal(address, uint256)`. Terminal validation replaces deployer ownership check. |
70
+
71
+ ---
72
+
73
+ ## 2. New Features
74
+
75
+ ### 2.1 New Functions
76
+
77
+ #### IREVDeployer / REVDeployer
78
+
79
+ | Function | Description |
80
+ |----------|-------------|
81
+ | `burnHeldTokensOf(uint256 revnetId)` | Burns any of the revnet's tokens held by the deployer contract. Project tokens can accumulate here from reserved token distribution when splits do not sum to 100%. |
82
+ | `deployFor` (4-arg overload) | Convenience overload that deploys a revnet with a default empty 721 hook. Constructs an empty 721 config internally. Returns `(uint256, IJB721TiersHook)`. |
83
+ | `BUYBACK_HOOK()` | Returns the immutable `IJBBuybackHookRegistry` used as a data hook to route payments through buyback pools. |
84
+ | `LOANS()` | Returns the immutable address of the single loan contract shared by all revnets. |
85
+ | `DEFAULT_BUYBACK_POOL_FEE()` | Returns the default Uniswap pool fee tier (`10_000` = 1%) for auto-configured buyback pools. |
86
+ | `DEFAULT_BUYBACK_TWAP_WINDOW()` | Returns the default TWAP window (`2 days`) for auto-configured buyback pools. |
87
+
88
+ ### 2.2 New Events
89
+
90
+ | Contract | Event |
91
+ |----------|-------|
92
+ | `IREVDeployer` | `BurnHeldTokens(uint256 indexed revnetId, uint256 count, address caller)` |
93
+
94
+ ### 2.3 New Errors
95
+
96
+ | Contract | Error |
97
+ |----------|-------|
98
+ | `REVDeployer` | `REVDeployer_NothingToBurn()` |
99
+ | `REVLoans` | `REVLoans_InvalidTerminal(address terminal, uint256 revnetId)` |
100
+ | `REVLoans` | `REVLoans_NothingToRepay()` |
101
+ | `REVLoans` | `REVLoans_ZeroBorrowAmount()` |
102
+ | `REVLoans` | `REVLoans_SourceMismatch()` |
103
+ | `REVLoans` | `REVLoans_LoanIdOverflow()` |
104
+
105
+ ### 2.4 New Constants
106
+
107
+ | Contract | Constant | Description |
108
+ |----------|----------|-------------|
109
+ | `REVDeployer` | `DEFAULT_BUYBACK_POOL_FEE = 10_000` | Default Uniswap pool fee tier (1%) for auto-configured buyback pools. |
110
+ | `REVDeployer` | `DEFAULT_BUYBACK_TICK_SPACING = 200` | Default tick spacing for buyback pools, aligned with `UniV4DeploymentSplitHook.TICK_SPACING`. |
111
+ | `REVDeployer` | `DEFAULT_BUYBACK_TWAP_WINDOW = 2 days` | Default TWAP window for buyback pools. |
112
+
113
+ ### 2.5 New Structs
114
+
115
+ | Struct | Description |
116
+ |--------|-------------|
117
+ | `REVBaseline721HookConfig` | Replaces `JBDeploy721TiersHookConfig` as the type for `REVDeploy721TiersHookConfig.baseline721HookConfiguration`. Contains `name`, `symbol`, `baseUri`, `tokenUriResolver`, `contractUri`, `tiersConfig`, `reserveBeneficiary`, and `flags` (`REV721TiersHookFlags`). |
118
+ | `REV721TiersHookFlags` | A subset of `JB721TiersHookFlags` that omits `issueTokensForSplits` (revnets always force it to `false`). Contains `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting`, `preventOverspending`. |
119
+
120
+ ---
121
+
122
+ ## 3. Event Changes
123
+
124
+ ### 3.1 Added Events
125
+
126
+ See section 2.2 above.
127
+
128
+ ### 3.2 Removed Events
129
+
130
+ | Contract | Event | Notes |
131
+ |----------|-------|-------|
132
+ | `IREVDeployer` | `SetAdditionalOperator(uint256 revnetId, address additionalOperator, uint256[] permissionIds, address caller)` | Removed entirely. |
133
+
134
+ ### 3.3 Modified Events
135
+
136
+ | Contract | Event | Change |
137
+ |----------|-------|--------|
138
+ | `IREVDeployer` | `DeployRevnet` | Removed `REVBuybackHookConfig buybackHookConfiguration` parameter. |
139
+ | `IREVLoans` | `ReallocateCollateral` | Typo fix: `removedcollateralCount` (lowercase 'c') renamed to `removedCollateralCount` (uppercase 'C'). |
140
+
141
+ ### 3.4 NatSpec Documentation
142
+
143
+ All events in v6 interfaces gained comprehensive NatSpec documentation (`@notice`, `@param`). This is a documentation-only change that does not affect the ABI.
144
+
145
+ ---
146
+
147
+ ## 4. Error Changes
148
+
149
+ ### 4.1 Removed Errors
150
+
151
+ | Contract | Error | Notes |
152
+ |----------|-------|-------|
153
+ | `REVDeployer` | `REVDeployer_LoanSourceDoesntMatchTerminalConfigurations(address, address)` | Loan sources removed from `REVConfig`. |
154
+ | `REVLoans` | `REVLoans_RevnetsMismatch(address, address)` | Replaced by terminal validation via `DIRECTORY.isTerminalOf`. |
155
+
156
+ ### 4.2 New Errors
157
+
158
+ See section 2.3 above.
159
+
160
+ ### 4.3 Unchanged Errors
161
+
162
+ The following errors are identical between v5 and v6:
163
+
164
+ **REVDeployer:**
165
+ - `REVDeployer_AutoIssuanceBeneficiaryZeroAddress()`
166
+ - `REVDeployer_CashOutDelayNotFinished(uint256, uint256)`
167
+ - `REVDeployer_CashOutsCantBeTurnedOffCompletely(uint256, uint256)`
168
+ - `REVDeployer_MustHaveSplits()`
169
+ - `REVDeployer_NothingToAutoIssue()`
170
+ - `REVDeployer_RulesetDoesNotAllowDeployingSuckers()`
171
+ - `REVDeployer_StageNotStarted(uint256)`
172
+ - `REVDeployer_StagesRequired()`
173
+ - `REVDeployer_StageTimesMustIncrease()`
174
+ - `REVDeployer_Unauthorized(uint256, address)`
175
+
176
+ **REVLoans:**
177
+ - `REVLoans_CollateralExceedsLoan(uint256, uint256)`
178
+ - `REVLoans_InvalidPrepaidFeePercent(uint256, uint256, uint256)`
179
+ - `REVLoans_LoanExpired(uint256, uint256)`
180
+ - `REVLoans_NewBorrowAmountGreaterThanLoanAmount(uint256, uint256)`
181
+ - `REVLoans_NoMsgValueAllowed()`
182
+ - `REVLoans_NotEnoughCollateral()`
183
+ - `REVLoans_OverflowAlert(uint256, uint256)`
184
+ - `REVLoans_OverMaxRepayBorrowAmount(uint256, uint256)`
185
+ - `REVLoans_PermitAllowanceNotEnough(uint256, uint256)`
186
+ - `REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(uint256, uint256)`
187
+ - `REVLoans_Unauthorized(address, address)`
188
+ - `REVLoans_UnderMinBorrowAmount(uint256, uint256)`
189
+ - `REVLoans_ZeroCollateralLoanIsInvalid()`
190
+
191
+ ---
192
+
193
+ ## 5. Struct Changes
194
+
195
+ ### 5.1 Removed Structs
196
+
197
+ | Struct | Notes |
198
+ |--------|-------|
199
+ | `REVBuybackHookConfig` | Buyback hook is now an immutable on the deployer; configuration is automatic. |
200
+ | `REVBuybackPoolConfig` | Was used within `REVBuybackHookConfig`. |
201
+
202
+ ### 5.2 New Structs
203
+
204
+ | Struct | Notes |
205
+ |--------|-------|
206
+ | `REVBaseline721HookConfig` | Replaces `JBDeploy721TiersHookConfig` in `REVDeploy721TiersHookConfig`. Provides a revnet-specific 721 config that uses `REV721TiersHookFlags` instead of `JB721TiersHookFlags`, omitting `issueTokensForSplits`. |
207
+ | `REV721TiersHookFlags` | A subset of `JB721TiersHookFlags` without `issueTokensForSplits` (always forced to `false` for revnets). |
208
+
209
+ ### 5.3 Modified Structs
210
+
211
+ | Struct | Field | v5 | v6 |
212
+ |--------|-------|----|----|
213
+ | `REVConfig` | `loanSources` | `REVLoanSource[] loanSources` | Removed |
214
+ | `REVConfig` | `loans` | `address loans` | Removed |
215
+ | `REVCroptopAllowedPost` | `maximumSplitPercent` | Not present | `uint32 maximumSplitPercent` |
216
+ | `REVDeploy721TiersHookConfig` | `baseline721HookConfiguration` | `JBDeploy721TiersHookConfig` | `REVBaseline721HookConfig` |
217
+ | `REVDeploy721TiersHookConfig` | `splitOperatorCanAdjustTiers` | `bool splitOperatorCanAdjustTiers` | Renamed/inverted: `bool preventSplitOperatorAdjustingTiers` |
218
+ | `REVDeploy721TiersHookConfig` | `splitOperatorCanUpdateMetadata` | `bool splitOperatorCanUpdateMetadata` | Renamed/inverted: `bool preventSplitOperatorUpdatingMetadata` |
219
+ | `REVDeploy721TiersHookConfig` | `splitOperatorCanMint` | `bool splitOperatorCanMint` | Renamed/inverted: `bool preventSplitOperatorMinting` |
220
+ | `REVDeploy721TiersHookConfig` | `splitOperatorCanIncreaseDiscountPercent` | `bool splitOperatorCanIncreaseDiscountPercent` | Renamed/inverted: `bool preventSplitOperatorIncreasingDiscountPercent` |
221
+
222
+ ### 5.4 Unchanged Structs
223
+
224
+ The following structs are identical between v5 and v6 (only `forge-lint` comments added):
225
+ - `REVAutoIssuance`
226
+ - `REVDescription`
227
+ - `REVLoan`
228
+ - `REVLoanSource`
229
+ - `REVStageConfig`
230
+ - `REVSuckerDeploymentConfig`
231
+
232
+ ---
233
+
234
+ ## 6. Implementation Changes
235
+
236
+ ### 6.1 REVDeployer
237
+
238
+ | Change | Description |
239
+ |--------|-------------|
240
+ | **Solidity version** | Upgraded from `0.8.23` to `0.8.26`. |
241
+ | **Buyback hook architecture** | Per-revnet `buybackHookOf` mapping replaced with a single immutable `BUYBACK_HOOK` (`IJBBuybackHookRegistry`). Pools are auto-initialized for each terminal token during deployment via `_tryInitializeBuybackPoolFor`. |
242
+ | **Loans architecture** | Per-revnet `loansOf` mapping replaced with a single immutable `LOANS` address. The deployer grants `USE_ALLOWANCE` permission to the loans contract for all revnets in the constructor (wildcard `revnetId=0`). |
243
+ | **Constructor permissions** | v6 constructor grants three wildcard permissions: `MAP_SUCKER_TOKEN` to the sucker registry, `USE_ALLOWANCE` to the loans contract, and `SET_BUYBACK_POOL` to the buyback hook. v5 only granted `MAP_SUCKER_TOKEN`. |
244
+ | **Deploy function consolidation** | `deployFor` and `deployWith721sFor` merged into two `deployFor` overloads: a 6-arg version (with 721 config and allowed posts) and a 4-arg convenience version (auto-creates empty 721 hook). Both return `(uint256, IJB721TiersHook)`. |
245
+ | **Every revnet gets a 721 hook** | The 4-arg `deployFor` overload auto-deploys a default empty 721 hook with all split operator permissions granted. In v5, the simple `deployFor` did not deploy any 721 hook. |
246
+ | **721 permission semantics inverted** | v5 used opt-in flags (`splitOperatorCanAdjustTiers` etc.) that conditionally pushed permissions. v6 uses opt-out flags (`preventSplitOperatorAdjustingTiers` etc.) that grant permissions by default unless prevented. |
247
+ | **`beforePayRecordedWith` rewrite** | v5 fetched the buyback hook from `buybackHookOf[revnetId]` and the 721 hook separately, passing the 721 hook as a zero-amount `JBPayHookSpecification`. v6 queries the 721 hook first as a data hook to determine its tier split amount, reduces the payment context amount for the buyback hook query, and scales the buyback weight proportionally (`weight * projectAmount / totalAmount`) to prevent minting tokens for the split portion of payments. |
248
+ | **`hasMintPermissionFor` updated** | v5 checked `loansOf[revnetId]`, `buybackHookOf[revnetId]`, and suckers. v6 checks the immutable `LOANS`, the immutable `BUYBACK_HOOK`, and delegates to `BUYBACK_HOOK.hasMintPermissionFor` for buyback delegates. |
249
+ | **Loan fund access limits simplified** | v5 derived fund access limits from `configuration.loanSources` and validated them against terminal configurations via `_matchingCurrencyOf`. v6 derives them from all terminal configurations directly (one unlimited surplus allowance per terminal+token pair). The `_matchingCurrencyOf` helper is removed. |
250
+ | **`burnHeldTokensOf` added** | New function to burn any project tokens held by the deployer. Reverts with `REVDeployer_NothingToBurn` if the balance is zero. |
251
+ | **Split operator permissions expanded** | Default permissions increased from 6 (v5) to 9 (v6). Added `SET_BUYBACK_HOOK`, `SET_ROUTER_TERMINAL`, and `SET_TOKEN_METADATA`. |
252
+ | **Encoded configuration hash** | v5 included `configuration.loans` in the encoded configuration. v6 does not, since loans are no longer per-revnet. |
253
+ | **Deploy ordering** | v6 `_deploy721RevnetFor` deploys the revnet first via `_deployRevnetFor`, then deploys the 721 hook and sets split operator permissions. v5 deployed the 721 hook then called `_deployRevnetFor`. |
254
+ | **Croptop `maximumSplitPercent`** | v6 passes the new `maximumSplitPercent` field from `REVCroptopAllowedPost` to `CTAllowedPost`. |
255
+ | **Auto-initialized buyback pools** | During deployment, `_tryInitializeBuybackPoolFor` is called for every terminal token to set up Uniswap V4 buyback pools at a generic 1:1 `sqrtPriceX96`. Failures (e.g., pool already initialized) are silently caught via try-catch. |
256
+
257
+ ### 6.2 REVLoans
258
+
259
+ | Change | Description |
260
+ |--------|-------------|
261
+ | **Solidity version** | Upgraded from `0.8.23` to `0.8.26`. |
262
+ | **Deployer dependency removed** | v5 stored `REVNETS` (`IREVDeployer`) and validated that the revnet was owned by the expected deployer via `RevnetsMismatch`. v6 does not reference the deployer at all. Validation now checks the terminal directly via `DIRECTORY.isTerminalOf`. |
263
+ | **Constructor refactored** | v5 accepted `IREVDeployer revnets` and derived `CONTROLLER`, `DIRECTORY`, etc. from it. v6 accepts `IJBController controller` and `IJBProjects projects` directly. |
264
+ | **Terminal validation** | `borrowFrom` now validates that the source terminal is registered in the directory for the revnet, reverting with `REVLoans_InvalidTerminal` if not. v5 validated deployer ownership instead. |
265
+ | **`numberOfLoansFor` renamed** | Renamed to `totalLoansBorrowedFor` to clarify that it is a monotonically increasing counter, not a count of active loans. |
266
+ | **`reallocateCollateralFromLoan` not payable** | v5 marked this function as `external payable`. v6 removes `payable` since the function only moves existing collateral between loans and does not accept new funds. |
267
+ | **Source mismatch check** | `reallocateCollateralFromLoan` now validates that the provided source matches the existing loan's source, reverting with `REVLoans_SourceMismatch()` if they differ. |
268
+ | **Zero borrow amount check** | `borrowFrom` now reverts with `REVLoans_ZeroBorrowAmount()` if the bonding curve returns zero. v5 did not have this check and would create a zero-amount loan. |
269
+ | **Nothing to repay check** | `repayLoan` now reverts with `REVLoans_NothingToRepay()` if both `repayBorrowAmount` and `collateralCountToReturn` are zero, preventing unbounded `totalLoansBorrowedFor` inflation. |
270
+ | **Liquidation loop behavior** | v5 broke out of the loop when encountering a loan with `createdAt == 0` (`break`). v6 continues iterating (`continue`), skipping gaps from repaid or previously liquidated loans. |
271
+ | **Liquidation cleanup** | v6 adds `delete _loanOf[loanId]` after burning a liquidated loan, clearing stale loan data for a gas refund. v5 did not clean up the loan data. |
272
+ | **`_totalBorrowedFrom` decimal normalization** | v6 normalizes token amounts from the source's native decimals to the target decimals before currency conversion. v5 did not perform decimal normalization, which could cause mixed-decimal arithmetic errors for tokens with non-18 decimals (e.g., USDC with 6 decimals). |
273
+ | **`_totalBorrowedFrom` zero-price safety** | v6 skips sources with a zero price to prevent division-by-zero panics that would DoS all loan operations. v5 did not handle this case. |
274
+ | **`_determineSourceFeeAmount` boundary** | v6 uses `>=` for the liquidation check (`timeSinceLoanCreated >= LOAN_LIQUIDATION_DURATION`). v5 used `>`. This means v6 considers a loan expired exactly at the boundary, while v5 allowed one more second. |
275
+ | **`ReallocateCollateral` event typo fix** | v5 used `removedcollateralCount` (lowercase 'c'). v6 fixes it to `removedCollateralCount` (uppercase 'C'). |
276
+ | **NatSpec documentation** | Extensive NatSpec added to all functions, views, and internal helpers. Flash loan safety analysis documented in `_borrowableAmountFrom`. |
277
+
278
+ ### 6.3 Named Arguments
279
+
280
+ Throughout the codebase, function calls were updated to use named argument syntax (e.g., `foo({bar: 1, baz: 2})`) for improved readability.
281
+
282
+ ---
283
+
284
+ ## 7. Migration Table
285
+
286
+ ### Interfaces
287
+
288
+ | v5 | v6 | Notes |
289
+ |----|----|-------|
290
+ | `IREVDeployer` | `IREVDeployer` | `deployWith721sFor` removed; two `deployFor` overloads (both return `IJB721TiersHook`). `buybackHookOf` and `loansOf` removed. `BUYBACK_HOOK`, `LOANS`, `DEFAULT_BUYBACK_POOL_FEE`, `DEFAULT_BUYBACK_TWAP_WINDOW`, `burnHeldTokensOf` added. `BurnHeldTokens` event added, `SetAdditionalOperator` event removed. `DeployRevnet` event lost `buybackHookConfiguration` param. NatSpec added. |
291
+ | `IREVLoans` | `IREVLoans` | `REVNETS` removed. `numberOfLoansFor` renamed to `totalLoansBorrowedFor`. `reallocateCollateralFromLoan` no longer payable. Constructor takes `IJBController` + `IJBProjects` instead of `IREVDeployer`. `ReallocateCollateral` event typo fixed. NatSpec added. |
292
+
293
+ ### Contracts
294
+
295
+ | v5 | v6 | Notes |
296
+ |----|----|-------|
297
+ | `REVDeployer` | `REVDeployer` | Buyback hook architecture changed from per-revnet mapping to immutable registry. Loans changed from per-revnet to single immutable. Deploy functions consolidated. Every revnet gets a 721 hook. 721 permission flags inverted. `beforePayRecordedWith` rewritten for split-aware weight scaling. `burnHeldTokensOf` added. Split operator gains 3 new default permissions. |
298
+ | `REVLoans` | `REVLoans` | Deployer dependency removed. Terminal validation replaces deployer ownership check. `numberOfLoansFor` renamed. `reallocateCollateralFromLoan` not payable. Source mismatch, zero borrow, and nothing-to-repay checks added. Liquidation loop uses `continue` instead of `break`. Stale loan data cleaned up on liquidation. Decimal normalization and zero-price safety in `_totalBorrowedFrom`. |
299
+
300
+ ### Structs
301
+
302
+ | v5 | v6 | Notes |
303
+ |----|----|-------|
304
+ | `REVAutoIssuance` | `REVAutoIssuance` | Identical (lint comment added) |
305
+ | `REVBuybackHookConfig` | (removed) | Buyback hook is now an immutable on the deployer |
306
+ | `REVBuybackPoolConfig` | (removed) | Was used within `REVBuybackHookConfig` |
307
+ | (not present) | `REVBaseline721HookConfig` | New struct for revnet-specific 721 hook configuration |
308
+ | (not present) | `REV721TiersHookFlags` | New subset of `JB721TiersHookFlags` without `issueTokensForSplits` |
309
+ | `REVConfig` | `REVConfig` | Removed `loanSources` and `loans` fields |
310
+ | `REVCroptopAllowedPost` | `REVCroptopAllowedPost` | Added `maximumSplitPercent` field |
311
+ | `REVDeploy721TiersHookConfig` | `REVDeploy721TiersHookConfig` | `baseline721HookConfiguration` type changed. Boolean flags inverted from opt-in to opt-out. |
312
+ | `REVDescription` | `REVDescription` | Identical (lint comment added) |
313
+ | `REVLoan` | `REVLoan` | Identical (lint comment added) |
314
+ | `REVLoanSource` | `REVLoanSource` | Identical (lint comment added) |
315
+ | `REVStageConfig` | `REVStageConfig` | Identical (lint comment added) |
316
+ | `REVSuckerDeploymentConfig` | `REVSuckerDeploymentConfig` | Identical (lint comment added) |
package/README.md CHANGED
@@ -47,15 +47,17 @@ Revnets are autonomous Juicebox projects with predetermined economic stages. Eac
47
47
 
48
48
  `REVLoans` lets participants borrow against their revnet tokens. Unlike traditional lending:
49
49
 
50
- - **Collateral is burned, not held.** Tokens are destroyed on borrow and re-minted on repay. This keeps the token supply accurate -- collateral tokens don't exist during the loan.
50
+ - **Collateral is burned, not held.** Tokens are destroyed on borrow and re-minted on repay. This keeps the token supply accurate -- collateral tokens don't exist during the loan. Callers must first grant `BURN_TOKENS` permission to the loans contract via `JBPermissions.setPermissionsFor()`.
51
51
  - **Borrowable amount = cash-out value.** The bonding curve determines how much you can borrow for a given amount of collateral.
52
52
  - **Prepaid fee model.** Borrowers choose a prepaid fee (2.5%-50%) that buys an interest-free window. After that window, a time-proportional source fee accrues.
53
53
  - **Each loan is an ERC-721 NFT.** Loans can be transferred, and expired loans (10 years) can be liquidated by anyone.
54
54
 
55
55
  ### Deployer Variants
56
56
 
57
- - **Basic revnet** -- `deployFor` with stage configurations mapped to Juicebox rulesets.
58
- - **Tiered 721 revnet** -- `deployWith721sFor` adds a tiered 721 pay hook that mints NFTs as people pay.
57
+ Every revnet gets a tiered ERC-721 hook deployed automatically even if no tiers are configured at launch. This lets the split operator add and sell NFTs later without migration.
58
+
59
+ - **Basic revnet** -- `deployFor` with stage configurations mapped to Juicebox rulesets and an empty 721 hook.
60
+ - **Tiered 721 revnet** -- `deployFor` adds a tiered 721 pay hook with pre-configured tiers that mint NFTs as people pay.
59
61
  - **Croptop revnet** -- A tiered 721 revnet with Croptop posting criteria, allowing the public to post content.
60
62
 
61
63
  ## Architecture
@@ -102,8 +104,8 @@ If `forge install` has issues, try `git submodule update --init --recursive`.
102
104
 
103
105
  ```
104
106
  src/
105
- REVDeployer.sol # Revnet deployer + data hook (~1,256 lines)
106
- REVLoans.sol # Token-collateralized lending (~1,333 lines)
107
+ REVDeployer.sol # Revnet deployer + data hook (~1,287 lines)
108
+ REVLoans.sol # Token-collateralized lending (~1,359 lines)
107
109
  interfaces/
108
110
  IREVDeployer.sol # Deployer interface + events
109
111
  IREVLoans.sol # Loans interface + events
@@ -159,6 +161,7 @@ The split operator has these default permissions:
159
161
  | `SUCKER_SAFETY` | Emergency sucker functions |
160
162
  | `SET_BUYBACK_HOOK` | Swap buyback hook |
161
163
  | `SET_ROUTER_TERMINAL` | Swap terminal |
164
+ | `SET_TOKEN_METADATA` | Update token name and symbol |
162
165
 
163
166
  Plus optional from 721 hook config: `ADJUST_721_TIERS`, `SET_721_METADATA`, `MINT_721`, `SET_721_DISCOUNT_PERCENT`.
164
167
 
@@ -180,7 +183,7 @@ Plus optional from 721 hook config: `ADJUST_721_TIERS`, `SET_721_METADATA`, `MIN
180
183
  - **Loan flash-loan exposure.** `borrowableAmountFrom` reads live surplus, which can be inflated via flash loans. A borrower could temporarily inflate the treasury to borrow more than the sustained value would support.
181
184
  - **uint112 truncation.** `REVLoan.amount` and `REVLoan.collateral` are `uint112` -- values above ~5.19e33 truncate silently.
182
185
  - **Cash-out fee stacking.** Cash outs incur both the Juicebox terminal fee (2.5%) and the revnet cash-out fee (2.5% to fee revnet). These compound.
183
- - **Auto-issuance stage ID mismatch.** Stage IDs are computed as `block.timestamp + i` during deployment, but actual Juicebox ruleset IDs depend on queuing logic. If timestamps don't align, auto-issuance for later stages may be unclaimed.
186
+ - **Auto-issuance stage IDs.** Stage IDs are `block.timestamp + i` during deployment. These match the Juicebox ruleset IDs because `JBRulesets` assigns IDs the same way (`latestId >= block.timestamp ? latestId + 1 : block.timestamp`), producing identical sequential IDs when all stages are queued in a single `deployFor()` call.
184
187
  - **NATIVE_TOKEN on non-ETH chains.** Using `JBConstants.NATIVE_TOKEN` on Celo or Polygon means CELO/MATIC, not ETH. Use ERC-20 WETH instead. The matching hash does NOT catch this -- it covers economic parameters but NOT terminal configurations.
185
188
  - **30-day cash-out delay.** When deploying an existing revnet to a new chain where the first stage has already started, a 30-day delay is imposed before cash outs are allowed, preventing cross-chain liquidity arbitrage.
186
189
  - **Loan source array growth.** `_loanSourcesOf[revnetId]` is unbounded. If an attacker creates loans from many different terminals/tokens, the array grows without limit.
package/RISKS.md CHANGED
@@ -1,49 +1,194 @@
1
- # revnet-core-v6 Risks
1
+ # revnet-core-v6 -- Active Risk Vectors
2
2
 
3
- ## Trust Assumptions
3
+ Forward-looking risk assessment for auditors. Covers trust assumptions, economic risks, loan system risks, data hook proxy concerns, access control, DoS vectors, and invariants.
4
4
 
5
- 1. **REVDeployer Contract** — Acts as data hook for all deployed revnets. A bug in REVDeployer affects every revnet's pay and cashout behavior.
6
- 2. **Immutable Stages** — Once deployed, stage parameters cannot be changed. If configured incorrectly, there is no fix (by design — this IS the trust model).
7
- 3. **Buyback Hook** — REVDeployer delegates to the buyback hook for swap-vs-mint decisions. Buyback hook failure falls back to direct minting.
8
- 4. **Suckers** — Cross-chain bridge implementations trusted for token transport. Bridge compromise = fund loss.
9
- 5. **Core Protocol** — Relies on JBController, JBMultiTerminal, JBTerminalStore for correct operation.
5
+ Read [ARCHITECTURE.md](./ARCHITECTURE.md) and [SKILLS.md](./SKILLS.md) for protocol context first.
10
6
 
11
- ## Known Risks
7
+ ---
12
8
 
13
- | Risk | Description | Mitigation |
14
- |------|-------------|------------|
15
- | Irreversible deployment | Stage parameters cannot be changed after deployment | Thorough testing before deploy; matching hash verification |
16
- | Loan collateral manipulation | Attacker inflates surplus to borrow more, then deflates | Borrow based on bonding curve value at time of borrow; existing loans unaffected by surplus changes |
17
- | 10-year liquidation drift | Collateral real value may diverge from loan over 10 years | Gradual liquidation schedule; early repayment available |
18
- | Loans beat cash-outs | Above ~39% cashOutTaxRate, borrowing is more capital-efficient than cashing out | By design (CryptoEconLab finding); creates natural demand for loans |
19
- | Matching hash gap | Hash covers economic parameters but NOT terminal configs, accounting contexts, or token mappings | Verify full configuration manually before cross-chain deploy |
9
+ ## 1. Trust Assumptions
20
10
 
21
- ## INTEROP-6: NATIVE_TOKEN on Non-ETH Chains
11
+ ### What the system assumes to be correct
22
12
 
23
- **Severity:** Medium
24
- **Status:** Acknowledged by design
13
+ - **REVDeployer is a singleton data hook.** Every revnet shares one `beforePayRecordedWith` and `beforeCashOutRecordedWith` implementation. A bug in either function affects ALL revnets deployed by that deployer simultaneously. There is no per-project isolation and no circuit breaker.
14
+ - **Stage immutability is the trust model.** Once `deployFor()` completes, stage parameters (issuance, `cashOutTaxRate`, splits, auto-issuances) are locked forever. No owner, no governance, no upgrade path. A misconfigured deployment is permanent. This is intentional -- the absence of admin keys IS the security property.
15
+ - **Bonding curve is the sole collateral oracle.** `REVLoans` uses `JBCashOuts.cashOutFrom` to value collateral. There is no external price oracle, no liquidation margin, and no health factor. The borrowable amount equals the cash-out value at the moment of borrowing.
16
+ - **Juicebox core contracts are correct.** `JBController`, `JBMultiTerminal`, `JBTerminalStore`, `JBTokens`, `JBPrices` -- a bug in any of these is a bug in every revnet.
17
+ - **Buyback hook operates correctly.** `BUYBACK_HOOK` handles swap-vs-mint routing. All revnets from the same deployer share one instance. Failure falls back to direct minting (not a revert), so the failure mode is economic inefficiency, not fund loss.
18
+ - **Suckers are honest bridges.** Suckers get 0% cashout tax in `beforeCashOutRecordedWith`. A compromised or malicious sucker registered in `SUCKER_REGISTRY` can extract funds from any revnet at zero cost.
19
+ - **Auto-issuance beneficiaries are set at deployment.** Beneficiary addresses are baked into the stage configuration. If a beneficiary address is a contract that becomes compromised, or an EOA whose keys are lost, those auto-issuance tokens are either captured or permanently unclaimable.
20
+ - **REVLoans contract address is immutable per deployer.** `LOANS` is set once in the REVDeployer constructor with wildcard `USE_ALLOWANCE` permission (`projectId=0`). If the loans contract has a vulnerability, every revnet's surplus is exposed.
25
21
 
26
- When a revnet expands to a chain where the native token is not ETH (e.g., Celo), using `NATIVE_TOKEN` as the accounting context creates a semantic mismatch — CELO payments priced as ETH without a price feed.
22
+ ### What you do NOT need to trust
27
23
 
28
- **Impact:** Issuance mispricing, surplus fragmentation, cross-chain arbitrage.
24
+ - **Project owners.** There are none. REVDeployer permanently holds the project NFT.
25
+ - **Split operators.** They can change splits, manage 721 tiers, deploy suckers, and set buyback pools, but cannot change stage parameters, issuance rates, cashout tax rates, or directly access treasury funds.
26
+ - **Token holders.** They can only cash out proportional to the bonding curve. Borrowers can only borrow up to the bonding curve value of their burned collateral.
29
27
 
30
- **Safe chains:** Ethereum, Optimism, Base, Arbitrum
31
- **Affected chains:** Celo, Polygon, Avalanche, BNB Chain
28
+ ---
32
29
 
33
- **Mitigation:** Use WETH ERC20 (not NATIVE_TOKEN) on non-ETH chains. Map `WETH → WETH` in sucker token mappings.
30
+ ## 2. Economic Risks
34
31
 
35
- ## Privileged Roles
32
+ ### Loan economics
36
33
 
37
- | Role | Capabilities | Notes |
38
- |------|-------------|-------|
39
- | Deployer (one-time) | Configures all stage parameters | Parameters immutable after deploy |
40
- | Auto-issuance beneficiaries | Receive pre-minted tokens per stage | Configured at deploy time |
41
- | Suckers | 0% cashout tax privilege | Enables cross-chain bridging without fee |
34
+ - **100% LTV with no safety margin.** Borrowable amount equals exact bonding curve cash-out value. When `cashOutTaxRate == 0`, this is true 100% LTV. Any decrease in surplus (other cash-outs, payouts, stage transitions) makes existing loans effectively under-collateralized. The protocol has no liquidation trigger for under-collateralized loans -- only the 10-year expiry.
35
+ - **Loans beat cash-outs above ~39% tax.** Above approximately 39.16% `cashOutTaxRate`, borrowing is more capital-efficient than cashing out because loans preserve upside while providing immediate liquidity. Based on CryptoEconLab research. This is by design but creates an incentive to borrow rather than cash out at higher tax rates, concentrating risk in the loan system.
36
+ - **10-year free put option.** Over the loan's lifetime, if the collateral's real value drops below the borrowed amount, the borrower has no incentive to repay. The borrower keeps the borrowed funds and forfeits worthless collateral. This is equivalent to a free put option with a 10-year expiry. The protocol absorbs this loss through permanent supply reduction (burned collateral never re-minted).
37
+ - **Surplus manipulation is economically irrational.** `_borrowableAmountFrom` reads live surplus. An attacker could inflate surplus via `addToBalanceOf`, but donations are permanent (no recovery), and the extra borrowable amount is always less than the donation. `pay` increases both surplus AND supply, neutralizing the effect. With non-zero `cashOutTaxRate`, the concave bonding curve makes this even worse for attackers.
42
38
 
43
- ## Reentrancy Considerations
39
+ ### Stage transition edge cases
44
40
 
45
- | Function | Protection | Risk |
46
- |----------|-----------|------|
47
- | `REVLoans.borrowFrom` | Collateral locked BEFORE funds transferred | LOW |
48
- | `REVLoans.repayLoan` | Loan state cleared BEFORE collateral returned | LOW |
49
- | `REVDeployer.beforePayRecordedWith` | View function, no state changes | NONE |
41
+ - **`cashOutTaxRate` increase destroys loan health.** Active loans use the CURRENT stage's `cashOutTaxRate`. When a stage transition increases this rate, existing loans become under-collateralized -- the collateral's cash-out value drops but the loan amount remains unchanged. No forced repayment or margin call mechanism exists. Over 10 years with multiple stage transitions, this compounds.
42
+ - **`cashOutTaxRate` decrease creates refinancing opportunity.** When a new stage lowers the tax rate, existing collateral becomes worth more. Borrowers can `reallocateCollateralFromLoan` to extract the surplus value. This creates a predictable, front-runnable event at every stage boundary.
43
+ - **Weight decay approaching zero over long periods.** With `issuanceCutPercent > 0` and `issuanceCutFrequency > 0`, issuance weight decays exponentially. After 10+ years, new payments mint negligibly few tokens, meaning the token supply effectively freezes. This concentrates cash-out value among existing holders and makes the bonding curve increasingly sensitive to individual cash-outs. Verify the weight cache mechanism (`updateRulesetWeightCache`) handles the 20,000-iteration threshold correctly when many cycles have elapsed.
44
+ - **Duration=0 stages never auto-expire.** A stage with `duration=0` (no issuance cut frequency) persists until explicitly replaced by a subsequent stage's `startsAtOrAfter`. If the next stage's `startsAtOrAfter` is far in the future, the current stage runs indefinitely at its configured parameters.
45
+
46
+ ### Cross-currency reclaim calculations
47
+
48
+ - **`_totalBorrowedFrom` aggregates across currencies via `JBPrices`.** Each loan source may be in a different token/currency. Aggregation normalizes decimals and converts via price feeds. If a price feed returns zero, that source is skipped (prevents division-by-zero DoS). But a stale or manipulated price feed silently over- or under-counts total borrowed amount, affecting all subsequent borrow operations for the revnet.
49
+ - **Undercount from zero-price feeds is unbounded.** When `PRICES.pricePerUnitOf` returns 0 for a source, that source's entire outstanding debt is excluded from the `_totalBorrowedFrom` total. There is no cap on the magnitude of the omission -- it equals the full borrowed amount from the affected source. Concretely: if a revnet has 100 ETH borrowed across 3 sources and one source's feed returns 0, that source's debt becomes invisible, potentially allowing the remaining borrowing capacity to be over-utilized. The more debt concentrated in the affected source, the larger the undercount.
50
+ - **Cascading effect on borrowing capacity.** Because `_totalBorrowedFrom` is called on every `borrowFrom` to enforce `_borrowableAmountFrom`, an undercount directly inflates the perceived remaining borrowing capacity for the entire revnet -- not just the affected source. New borrowers across all sources benefit from the artificially low total, and the over-extension compounds with each new loan taken while the feed is down.
51
+ - **No automatic recovery mechanism.** Once a price feed returns 0, the affected source's debt remains invisible to `_totalBorrowedFrom` until the feed recovers. There is no event emitted when a source is skipped, no fallback oracle, and no circuit breaker that pauses borrowing when feed health degrades. Operators should actively monitor price feed health and treat any feed returning 0 as a risk event for the revnet's loan system.
52
+ - **Decimal normalization truncation.** When converting from higher-decimal tokens (18) to lower-decimal targets (6), integer division truncates. For sources with large outstanding borrows in high-decimal tokens, this truncation can systematically undercount the total borrowed amount, allowing slightly more borrowing than intended. For example, converting 999,999,999,999 wei (18-decimal) to 6-decimal precision discards 12 digits of precision. Across many sources, these per-source truncation errors accumulate additively, further widening the gap between the reported and actual total debt.
53
+
54
+ ### Auto-issuance overflow potential
55
+
56
+ - **`REVAutoIssuance.count` is `uint104` (~2.03e31).** Multiple auto-issuances for the same beneficiary in the same stage are summed via `+=` in `_makeRulesetConfigurations`. If cumulative auto-issuances exceed `uint256`, this wraps. In practice, `uint104` inputs limit each addition, but verify no path allows the mapping value to overflow.
57
+ - **Auto-issuance dilutes existing holders.** Large auto-issuances at stage boundaries dilute the token supply, reducing per-token cash-out value. This is permissionless (`autoIssueFor` can be called by anyone). A griefing vector exists where someone calls `autoIssueFor` immediately before another user's cash-out to reduce their reclaim amount. However, the dilution is pre-configured and predictable.
58
+
59
+ ---
60
+
61
+ ## 3. Loan System Risks
62
+
63
+ ### Collateral valuation during price volatility
64
+
65
+ - **Bonding curve is internal, not market-price.** Collateral is valued by the bonding curve (`JBCashOuts.cashOutFrom`) which depends on surplus, total supply, and `cashOutTaxRate`. External market price (e.g., DEX price of the revnet token) is irrelevant to collateral valuation. If the market price diverges significantly from the bonding curve value, arbitrage opportunities arise between borrowing and trading.
66
+ - **Surplus is cross-terminal aggregate.** `_borrowableAmountFrom` calls `JBSurplus.currentSurplusOf` across all terminals. If one terminal holds tokens in a volatile asset that has crashed, the aggregate surplus drops, reducing collateral value for ALL borrowers regardless of which terminal their loan draws from.
67
+
68
+ ### Liquidation concerns
69
+
70
+ - **No cascading liquidation mechanism.** There is no health factor, no margin call, and no keeper-triggered liquidation for under-collateralized loans. The only liquidation path is `liquidateExpiredLoansFrom` after 10 years. Under-collateralized loans persist indefinitely within that window.
71
+ - **Liquidation iterates by loan number.** `liquidateExpiredLoansFrom` takes `startingLoanId` and `count`, iterating sequentially. Repaid and already-liquidated loans are skipped (`createdAt == 0`), but the caller pays gas for every skip. If a revnet has thousands of loans with sparse gaps (many repaid), liquidation becomes expensive. The `count` parameter bounds gas per call, but a malicious actor could create many small loans to increase cleanup costs. **Mitigated:** `startingLoanId + count` is now bounded to `_ONE_TRILLION` to prevent cross-revnet accounting corruption (reverts with `REVLoans_LoanIdOverflow`).
72
+ - **Liquidation permanently destroys collateral.** Collateral was burned at borrow time. Upon liquidation, `totalCollateralOf` is decremented but no tokens are minted or returned. The collateral is permanently removed from the token supply. This deflates the total supply, increasing per-token value for remaining holders -- a mild positive externality from defaults.
73
+
74
+ ### Loan source rotation after deployment
75
+
76
+ - **Loan sources grow monotonically.** `_loanSourcesOf[revnetId]` is append-only. Each new `(terminal, token)` pair used for borrowing adds an entry. Entries are never removed, even if all loans from that source are repaid. `_totalBorrowedFrom` iterates the entire array on every borrow/repay.
77
+ - **Removed terminals remain as loan sources.** If a terminal is de-registered from `JBDirectory` (via migration), existing loans from that terminal remain valid (the loan struct stores a direct reference to the terminal contract). New borrows against that terminal are blocked by `DIRECTORY.isTerminalOf` check in `borrowFrom`. But `_totalBorrowedFrom` still queries the de-registered terminal's `accountingContextForTokenOf` -- verify this doesn't revert.
78
+
79
+ ### `reallocateCollateralFromLoan` sandwich potential
80
+
81
+ - **Reallocation is two operations in one transaction.** `reallocateCollateralFromLoan` first reduces collateral on the existing loan (via `_reallocateCollateralFromLoan`), then opens a new loan (via `borrowFrom`). Between these two operations, the surplus and total supply have changed (collateral was returned to the caller, changing supply). The new loan's borrowable amount is computed with the post-reallocation state.
82
+ - **Source mismatch check.** `reallocateCollateralFromLoan` enforces that the new loan's source matches the existing loan's source (`source.token == existingSource.token && source.terminal == existingSource.terminal`). This prevents cross-source value extraction.
83
+ - **MEV opportunity at stage boundaries.** If a borrower knows a stage transition will decrease `cashOutTaxRate`, they can wait until just after the transition and `reallocateCollateralFromLoan` to extract more borrowed funds from the same collateral. This is predictable and not preventable by design.
84
+
85
+ ### BURN_TOKENS permission prerequisite
86
+
87
+ - **Borrowers must grant BURN_TOKENS permission before calling `borrowFrom`.** The loans contract burns the caller's tokens as collateral via `JBController.burnTokensOf`, which requires the caller to have granted `BURN_TOKENS` permission to the loans contract for the revnet's project ID. Without this, the transaction reverts deep in `JBController` with `JBPermissioned_Unauthorized`. The prerequisite is documented in `borrowFrom`'s NatSpec.
88
+
89
+ ### Borrow-repay arbitrage
90
+
91
+ - **Immediate repayment within prepaid window incurs zero source fee.** A borrower who pays the prepaid fee upfront can repay at any time within the prepaid duration with no additional cost. The prepaid fee is the minimum 2.5% + REV fee 1% = 3.5%. If the bonding curve value of the collateral increases (e.g., from new payments into the revnet) during the prepaid window, the borrower can repay, recover their collateral, and cash out at the higher value.
92
+ - **This is not profitable as a standalone strategy** because the 3.5% minimum fee exceeds the expected value gained from short-term surplus fluctuations. But for borrowers who need liquidity anyway, it provides free optionality.
93
+
94
+ ---
95
+
96
+ ## 4. Data Hook Proxy Risks
97
+
98
+ REVDeployer sits between the terminal and the actual hooks (buyback hook, 721 hook). This proxy pattern creates composition risks.
99
+
100
+ ### Underlying hook reverts
101
+
102
+ - **721 hook revert in `beforePayRecordedWith`.** The call to `IJBRulesetDataHook(tiered721Hook).beforePayRecordedWith(context)` is NOT wrapped in try-catch. If the 721 hook reverts (e.g., due to a storage corruption or out-of-gas), the entire payment reverts. This is a single point of failure for all payments to revnets with 721 hooks.
103
+ - **Buyback hook is more resilient.** The `BUYBACK_HOOK.beforePayRecordedWith(buybackHookContext)` call is also not try-caught, but the buyback hook is a shared singleton controlled by the protocol. If it reverts, all revnets from that deployer are affected.
104
+ - **Cash-out fee terminal revert.** In `afterCashOutRecordedWith`, the fee payment to the fee terminal IS wrapped in try-catch with a fallback to `addToBalanceOf`. If the fallback also fails, funds could be stuck in the deployer contract. However, the deployer has no withdrawal mechanism for arbitrary tokens -- only `burnHeldTokensOf` for project tokens.
105
+
106
+ ### Sucker bypass path (0% cashout tax)
107
+
108
+ - **Suckers bypass all economic protections.** `beforeCashOutRecordedWith` returns `(0, context.cashOutCount, context.totalSupply, hookSpecifications)` for suckers -- zero tax, no fee. A compromised sucker effectively has a backdoor to extract the full pro-rata surplus of any token it holds.
109
+ - **Sucker registration is controlled by `SUCKER_REGISTRY`.** The registry has `MAP_SUCKER_TOKEN` wildcard permission from the deployer. The split operator has `SUCKER_SAFETY` permission. Verify that `deploySuckersFor` correctly checks the `extraMetadata` bit (bit 2) for sucker deployment permission per stage.
110
+
111
+ ### Permission escalation through proxy
112
+
113
+ - **`hasMintPermissionFor` grants mint to four categories.** The loans contract, buyback hook, buyback hook delegates, and suckers all have unrestricted mint permission. If any of these contracts have a vulnerability that allows arbitrary calls, they can mint unlimited revnet tokens for any revnet.
114
+ - **Wildcard permissions.** REVDeployer grants `USE_ALLOWANCE` to `LOANS` with `projectId=0` (wildcard). This means the loans contract can drain surplus from ANY revnet deployed by this deployer, constrained only by the loan logic itself. A bug in `REVLoans._addTo` that miscalculates `addedBorrowAmount` could drain treasuries.
115
+
116
+ ---
117
+
118
+ ## 5. Access Control
119
+
120
+ ### Stage configuration immutability
121
+
122
+ - **No function modifies rulesets after deployment.** REVDeployer holds the project NFT and is the only entity that could call `CONTROLLER.queueRulesetsOf`. But REVDeployer has no function that does this. The only ruleset interaction after deployment is reading via `currentRulesetOf` and `getRulesetOf`. This is the core immutability guarantee -- verify no code path exists that calls `queueRulesetsOf` or `launchRulesetsFor` on an already-deployed revnet.
123
+ - **Project NFT cannot be recovered.** Once transferred to REVDeployer via `safeTransferFrom` or minted by `launchProjectFor`, the NFT is permanently held. REVDeployer implements `onERC721Received` but only accepts from `PROJECTS`. It has no `transferFrom` or equivalent for project NFTs.
124
+
125
+ ### Who can deploy and modify
126
+
127
+ - **`deployFor` is permissionless for new revnets** (`revnetId == 0`). Anyone can deploy a revnet with arbitrary configuration.
128
+ - **`deployFor` with existing project requires ownership.** The caller must be `PROJECTS.ownerOf(revnetId)` and the project must be blank (no controller, no rulesets).
129
+ - **Split operator is singular and self-replacing.** `setSplitOperatorOf` can only be called by the current split operator. If the split operator is set to address(0) or a contract with no ability to call `setSplitOperatorOf`, the role is permanently lost.
130
+ - **Split operator permissions are cumulative during deployment.** `_extraOperatorPermissions[revnetId]` is populated via `.push()` during deployment. If the same permission ID is pushed twice, the array has duplicates but this is harmless (the permission check uses `hasPermissions` which doesn't care about duplicates).
131
+
132
+ ### Split operator trust boundaries
133
+
134
+ - **ADD_PRICE_FEED.** The split operator can add price feeds for the revnet. A malicious price feed could return manipulated values, affecting cross-currency surplus calculations and loan collateral valuations. Price feeds are immutable once added (cannot be replaced).
135
+ - **SET_SPLIT_GROUPS.** The split operator controls where reserved tokens go. A compromised operator can redirect all reserved tokens to themselves.
136
+ - **DEPLOY_SUCKERS (via `deploySuckersFor`).** The split operator can deploy new suckers if the current stage allows it (`extraMetadata` bit 2). A malicious sucker gets 0% cashout tax privilege. This is the highest-impact split operator action.
137
+ - **SET_ROUTER_TERMINAL.** The split operator can configure the router terminal, potentially redirecting payments.
138
+
139
+ ---
140
+
141
+ ## 6. DoS Vectors
142
+
143
+ ### Long stage chains
144
+
145
+ - **`_makeRulesetConfigurations` iterates all stages.** Deployment cost scales linearly with stage count. There is no explicit cap on the number of stages. A deployment with hundreds of stages would be expensive but is not blocked.
146
+ - **Auto-issuance inner loop.** For each stage, the deployment iterates all `autoIssuances[]`. The combined iteration (stages x auto-issuances per stage) could hit the block gas limit for extreme configurations.
147
+
148
+ ### Many auto-issuances
149
+
150
+ - **`autoIssueFor` is one-per-call.** Each call processes a single `(revnetId, stageId, beneficiary)` tuple. If a stage has many beneficiaries, they must each be claimed individually. Not a DoS vector against the protocol, but a usability concern.
151
+
152
+ ### Loan source enumeration
153
+
154
+ - **`_totalBorrowedFrom` iterates ALL sources on every borrow and repay.** Gas cost: ~20k per source (external `accountingContextForTokenOf` call + storage read + potential price feed call). With 10 sources, this adds ~200k gas per loan operation. With 50+ sources (unlikely but possible), operations become prohibitively expensive.
155
+ - **Mitigation.** `borrowFrom` checks `DIRECTORY.isTerminalOf` before accepting a new source. The number of registered terminals per project is practically bounded. But nothing prevents a terminal from being registered, used for one loan, de-registered, and then a new terminal registered -- leaving stale entries in the source array.
156
+
157
+ ### Fee terminal unavailability
158
+
159
+ - **Source fee payment in `_adjust` IS try-caught.** If `loan.source.terminal.pay` reverts when paying the source fee, the fee amount is returned to the borrower instead. This prevents a reverting terminal from blocking all loan operations.
160
+ - **REV fee payment in `_addTo` IS try-caught.** If the REV fee terminal is unavailable, the fee is zeroed and the borrower receives it. This is graceful degradation.
161
+ - **Cash-out fee in `afterCashOutRecordedWith` IS try-caught.** Falls back to `addToBalanceOf`. If fallback also fails, the transaction still doesn't revert (the deployer absorbs the funds, which can only be recovered via `burnHeldTokensOf` for project tokens, not arbitrary tokens).
162
+
163
+ ---
164
+
165
+ ## 7. Invariants to Verify
166
+
167
+ These MUST hold. Breaking any of them is a finding.
168
+
169
+ ### Loan accounting
170
+
171
+ - **Collateral conservation.** `totalCollateralOf[revnetId]` == sum of `loan.collateral` for all active (non-liquidated, non-repaid) loans of that revnet.
172
+ - **Borrowed amount conservation.** `totalBorrowedFrom[revnetId][terminal][token]` == sum of `loan.amount` for all active loans from that source.
173
+ - **No double-mint collateral.** Repaying a loan mints collateral back exactly once. A repaid loan cannot be repaid again (NFT is burned, storage is deleted).
174
+ - **No zero-collateral loans.** Every active loan has `collateral > 0` and `amount > 0`. The `borrowFrom` function reverts on `collateralCount == 0` and `borrowAmount == 0`.
175
+ - **Liquidation only after expiry.** `liquidateExpiredLoansFrom` skips loans where `block.timestamp <= loan.createdAt + LOAN_LIQUIDATION_DURATION`.
176
+ - **Total loans counter monotonicity.** `totalLoansBorrowedFor[revnetId]` only increments (on borrow, repay-with-replacement, and reallocation). It never decrements. Loan IDs are unique and never reused.
177
+
178
+ ### Stage and deployment
179
+
180
+ - **Stage immutability.** After `deployFor` completes, no function in REVDeployer calls `CONTROLLER.queueRulesetsOf` or modifies ruleset parameters.
181
+ - **Stage progression monotonicity.** `startsAtOrAfter` values strictly increase between stages. The first stage can be 0 (mapped to `block.timestamp`).
182
+ - **Auto-issuance single-claim.** Each `(revnetId, stageId, beneficiary)` can only be claimed once. `amountToAutoIssue` is zeroed BEFORE the external `mintTokensOf` call (CEI pattern).
183
+ - **Split percentages.** Per-stage `splitPercent > 0` requires `splits.length > 0`. Split percentages are validated by `JBSplits` in core (must sum to <= `SPLITS_TOTAL_PERCENT`).
184
+
185
+ ### Fee flows
186
+
187
+ - **Cash-out fees flow to fee revnet.** If the fee terminal `pay` succeeds, the fee goes to `FEE_REVNET_ID`. If it fails, the fee returns to the originating project via `addToBalanceOf`. Funds are never lost and never kept by the caller.
188
+ - **Loan REV fees flow to REV revnet.** If `feeTerminal.pay` succeeds, the fee goes to `REV_ID`. If it fails (try-catch), the fee amount is zeroed and added to the borrower's payout. The fee is never lost -- it either reaches the REV revnet or goes to the borrower.
189
+
190
+ ### Privilege isolation
191
+
192
+ - **Sucker privilege.** Only addresses returning `true` from `SUCKER_REGISTRY.isSuckerOf(projectId, addr)` get 0% cashout tax. No other code path grants this exemption.
193
+ - **Loan ownership.** Only `_ownerOf(loanId)` can call `repayLoan` and `reallocateCollateralFromLoan`. The loan NFT is burned before any state changes in repayment, preventing double-use.
194
+ - **Mint permission.** Only `LOANS`, `BUYBACK_HOOK`, buyback hook delegates (via `BUYBACK_HOOK.hasMintPermissionFor`), and suckers (via `_isSuckerOf`) can mint tokens. No other address passes the `hasMintPermissionFor` check.