@rev-net/core-v6 0.0.12 → 0.0.14

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 (117) hide show
  1. package/AUDIT_INSTRUCTIONS.md +295 -0
  2. package/CHANGE_LOG.md +321 -0
  3. package/README.md +2 -2
  4. package/RISKS.md +180 -35
  5. package/SKILLS.md +1 -1
  6. package/USER_JOURNEYS.md +489 -0
  7. package/package.json +9 -9
  8. package/script/Deploy.s.sol +40 -6
  9. package/script/helpers/RevnetCoreDeploymentLib.sol +7 -1
  10. package/src/REVDeployer.sol +63 -47
  11. package/src/REVLoans.sol +51 -15
  12. package/src/interfaces/IREVDeployer.sol +0 -1
  13. package/src/structs/REV721TiersHookFlags.sol +1 -0
  14. package/src/structs/REVAutoIssuance.sol +1 -0
  15. package/src/structs/REVBaseline721HookConfig.sol +1 -0
  16. package/src/structs/REVConfig.sol +1 -0
  17. package/src/structs/REVCroptopAllowedPost.sol +1 -0
  18. package/src/structs/REVDeploy721TiersHookConfig.sol +1 -0
  19. package/src/structs/REVDescription.sol +1 -0
  20. package/src/structs/REVLoan.sol +1 -0
  21. package/src/structs/REVLoanSource.sol +1 -0
  22. package/src/structs/REVStageConfig.sol +1 -0
  23. package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
  24. package/test/REV.integrations.t.sol +132 -12
  25. package/test/REVAutoIssuanceFuzz.t.sol +23 -3
  26. package/test/REVDeployerRegressions.t.sol +35 -4
  27. package/test/REVInvincibility.t.sol +58 -8
  28. package/test/REVInvincibilityHandler.sol +29 -0
  29. package/test/REVLifecycle.t.sol +28 -3
  30. package/test/REVLoans.invariants.t.sol +52 -5
  31. package/test/REVLoansAttacks.t.sol +43 -5
  32. package/test/REVLoansFeeRecovery.t.sol +50 -11
  33. package/test/REVLoansFindings.t.sol +27 -3
  34. package/test/REVLoansRegressions.t.sol +25 -3
  35. package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
  36. package/test/REVLoansSourced.t.sol +56 -7
  37. package/test/REVLoansUnSourced.t.sol +49 -5
  38. package/test/TestBurnHeldTokens.t.sol +32 -5
  39. package/test/TestCEIPattern.t.sol +26 -2
  40. package/test/TestCashOutCallerValidation.t.sol +30 -4
  41. package/test/TestConversionDocumentation.t.sol +26 -5
  42. package/test/TestCrossCurrencyReclaim.t.sol +584 -0
  43. package/test/TestCrossSourceReallocation.t.sol +26 -2
  44. package/test/TestERC2771MetaTx.t.sol +557 -0
  45. package/test/TestEmptyBuybackSpecs.t.sol +23 -3
  46. package/test/TestFlashLoanSurplus.t.sol +28 -3
  47. package/test/TestHookArrayOOB.t.sol +24 -4
  48. package/test/TestLiquidationBehavior.t.sol +26 -3
  49. package/test/TestLoanSourceRotation.t.sol +525 -0
  50. package/test/TestLongTailEconomics.t.sol +651 -0
  51. package/test/TestLowFindings.t.sol +65 -2
  52. package/test/TestMixedFixes.t.sol +28 -3
  53. package/test/TestPermit2Signatures.t.sol +657 -0
  54. package/test/TestReallocationSandwich.t.sol +384 -0
  55. package/test/TestRevnetRegressions.t.sol +324 -0
  56. package/test/TestSplitWeightAdjustment.t.sol +24 -2
  57. package/test/TestSplitWeightE2E.t.sol +29 -2
  58. package/test/TestSplitWeightFork.t.sol +46 -7
  59. package/test/TestStageTransitionBorrowable.t.sol +24 -2
  60. package/test/TestSwapTerminalPermission.t.sol +23 -3
  61. package/test/TestUint112Overflow.t.sol +28 -2
  62. package/test/TestZeroRepayment.t.sol +26 -2
  63. package/test/fork/ForkTestBase.sol +46 -3
  64. package/test/fork/TestCashOutFork.t.sol +1 -1
  65. package/test/fork/TestLoanBorrowFork.t.sol +1 -0
  66. package/test/fork/TestLoanCrossRulesetFork.t.sol +3 -1
  67. package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
  68. package/test/fork/TestLoanReallocateFork.t.sol +1 -0
  69. package/test/fork/TestLoanRepayFork.t.sol +1 -0
  70. package/test/fork/TestLoanTransferFork.t.sol +133 -0
  71. package/test/fork/TestSplitWeightFork.t.sol +3 -0
  72. package/test/helpers/REVEmpty721Config.sol +1 -0
  73. package/test/mock/MockBuybackDataHook.sol +1 -0
  74. package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
  75. package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
  76. package/test/regression/TestCumulativeLoanCounter.t.sol +27 -4
  77. package/test/regression/TestLiquidateGapHandling.t.sol +29 -4
  78. package/test/regression/TestZeroPriceFeed.t.sol +396 -0
  79. package/deployments/revnet-core-v5/arbitrum/REVDeployer.json +0 -2821
  80. package/deployments/revnet-core-v5/arbitrum/REVLoans.json +0 -2260
  81. package/deployments/revnet-core-v5/arbitrum_sepolia/REVDeployer.json +0 -2821
  82. package/deployments/revnet-core-v5/arbitrum_sepolia/REVLoans.json +0 -2260
  83. package/deployments/revnet-core-v5/base/REVDeployer.json +0 -2825
  84. package/deployments/revnet-core-v5/base/REVLoans.json +0 -2264
  85. package/deployments/revnet-core-v5/base_sepolia/REVDeployer.json +0 -2825
  86. package/deployments/revnet-core-v5/base_sepolia/REVLoans.json +0 -2264
  87. package/deployments/revnet-core-v5/ethereum/REVDeployer.json +0 -2825
  88. package/deployments/revnet-core-v5/ethereum/REVLoans.json +0 -2264
  89. package/deployments/revnet-core-v5/optimism/REVDeployer.json +0 -2821
  90. package/deployments/revnet-core-v5/optimism/REVLoans.json +0 -2260
  91. package/deployments/revnet-core-v5/optimism_sepolia/REVDeployer.json +0 -2825
  92. package/deployments/revnet-core-v5/optimism_sepolia/REVLoans.json +0 -2264
  93. package/deployments/revnet-core-v5/sepolia/REVDeployer.json +0 -2825
  94. package/deployments/revnet-core-v5/sepolia/REVLoans.json +0 -2264
  95. package/docs/book.css +0 -13
  96. package/docs/book.toml +0 -13
  97. package/docs/solidity.min.js +0 -74
  98. package/docs/src/README.md +0 -185
  99. package/docs/src/SUMMARY.md +0 -18
  100. package/docs/src/src/README.md +0 -7
  101. package/docs/src/src/REVDeployer.sol/contract.REVDeployer.md +0 -999
  102. package/docs/src/src/REVLoans.sol/contract.REVLoans.md +0 -1108
  103. package/docs/src/src/interfaces/IREVDeployer.sol/interface.IREVDeployer.md +0 -525
  104. package/docs/src/src/interfaces/IREVLoans.sol/interface.IREVLoans.md +0 -598
  105. package/docs/src/src/interfaces/README.md +0 -5
  106. package/docs/src/src/structs/README.md +0 -12
  107. package/docs/src/src/structs/REVAutoIssuance.sol/struct.REVAutoIssuance.md +0 -19
  108. package/docs/src/src/structs/REVBuybackHookConfig.sol/struct.REVBuybackHookConfig.md +0 -19
  109. package/docs/src/src/structs/REVBuybackPoolConfig.sol/struct.REVBuybackPoolConfig.md +0 -21
  110. package/docs/src/src/structs/REVConfig.sol/struct.REVConfig.md +0 -23
  111. package/docs/src/src/structs/REVCroptopAllowedPost.sol/struct.REVCroptopAllowedPost.md +0 -32
  112. package/docs/src/src/structs/REVDeploy721TiersHookConfig.sol/struct.REVDeploy721TiersHookConfig.md +0 -34
  113. package/docs/src/src/structs/REVDescription.sol/struct.REVDescription.md +0 -23
  114. package/docs/src/src/structs/REVLoan.sol/struct.REVLoan.md +0 -28
  115. package/docs/src/src/structs/REVLoanSource.sol/struct.REVLoanSource.md +0 -16
  116. package/docs/src/src/structs/REVStageConfig.sol/struct.REVStageConfig.md +0 -44
  117. package/docs/src/src/structs/REVSuckerDeploymentConfig.sol/struct.REVSuckerDeploymentConfig.md +0 -16
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 burned BEFORE funds transferred | LOW |
48
- | `REVLoans.repayLoan` | Loan state cleared BEFORE collateral re-minted | 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.
package/SKILLS.md CHANGED
@@ -143,7 +143,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
143
143
  3. **100% LTV by design.** Borrowable amount equals the pro-rata cash-out value. No safety margin unless the stage has `cashOutTaxRate > 0`. A tax of 20% creates ~20% effective collateral buffer.
144
144
  4. **Loan ID encoding.** `loanId = revnetId * 1_000_000_000_000 + loanNumber`. Each revnet supports ~1 trillion loans. Use `revnetIdOfLoanWith(loanId)` to decode.
145
145
  5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
146
- 6. **Auto-issuance stage IDs.** Computed as `block.timestamp + i` during deployment, but actual Juicebox ruleset IDs depend on queuing logic. Stage 1+ auto-issuance may be unclaimed if IDs don't match exactly.
146
+ 6. **Auto-issuance stage IDs.** Computed as `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.
147
147
  7. **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.
148
148
  8. **30-day cash-out delay.** Applied when deploying an existing revnet to a new chain where the first stage has already started. Prevents cross-chain liquidity arbitrage.
149
149
  9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.