@rev-net/core-v6 0.0.32 → 0.0.34

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.
package/RISKS.md CHANGED
@@ -1,247 +1,89 @@
1
1
  # Revnet Core Risk Register
2
2
 
3
- This file focuses on the risks created by autonomous treasury-backed projects with staged economics, composable hooks, cross-chain wiring, and token-collateralized loans.
4
-
5
- Read [ARCHITECTURE.md](./ARCHITECTURE.md) and [SKILLS.md](./SKILLS.md) for protocol context first.
3
+ This file focuses on the staged-economics, runtime-hook, hidden-supply, and loan risks that matter in Revnets. The main question is whether the deployed economic shape still holds under real runtime behavior.
6
4
 
7
5
  ## How to use this file
8
6
 
9
- - Read `Priority risks` first; they capture the highest-impact ways a revnet can misprice, misroute, or mis-govern itself.
10
- - Use the detailed sections for economics, loans, data-hook proxying, and liveness reasoning.
11
- - Treat `Accepted Behaviors` and `Invariants to Verify` as the operating contract for revnet deployments.
7
+ - Read `Priority risks` first.
8
+ - Use the detailed sections to separate stage design, hook composition, hidden supply, and loan accounting.
9
+ - Treat `Accepted Behaviors` and `Invariants to Verify` as the line between intended product tradeoffs and defects.
12
10
 
13
11
  ## Priority risks
14
12
 
15
13
  | Priority | Risk | Why it matters | Primary controls |
16
14
  |----------|------|----------------|------------------|
17
- | P0 | Loan mispricing from bad surplus or configuration | `REVLoans` depends on correct surplus, fee, and deployer-owned configuration; bad inputs can enable over-borrowing. | Surplus and price checks, deployer verification, and loan-specific invariant coverage. |
18
- | P0 | Deployer or data-hook proxy blast radius | `REVDeployer` sits in the pay and cash-out path for all revnets it launched. A bug here affects every attached project. | High-scrutiny review, composition testing, and cautious shared-deployer usage. |
19
- | P1 | Stage or split misconfiguration becomes permanent | Revnets are intentionally autonomous and hard to govern after launch; bad initial economics are difficult or impossible to fix. | Strong pre-launch review, deployment runbooks, and config simulation before production. |
15
+ | P0 | Borrowability drift from live surplus or cross-chain state | Loans can overextend or under-credit if revnet state is read incorrectly. | Borrowability tests, omnichain-state checks, and cash-out-delay gating. |
16
+ | P1 | Stage configuration mistakes | Revnet economics are hard to change after launch, so bad stages are expensive. | Deployment review, stage-transition tests, and launch-time validation. |
17
+ | P1 | Hidden-supply and burned-collateral misunderstandings | Hidden tokens and loans both change visible supply in non-obvious ways. | Explicit supply invariants and product-level review. |
20
18
 
21
19
  ## 1. Trust Assumptions
22
20
 
23
- ### What the system assumes to be correct
24
-
25
- - **REVOwner is a singleton data hook.** Every revnet shares one `beforePayRecordedWith` and `beforeCashOutRecordedWith` implementation in REVOwner. A bug in either function affects ALL revnets deployed by that deployer simultaneously. There is no per-project isolation and no circuit breaker.
26
- - **REVOwner deployer binding is precomputed.** REVOwner records the account that deployed it as an internal one-time binder, and only that account can bind `DEPLOYER`. Deployment tooling must precompute the canonical REVDeployer address and call `setDeployer(...)` before constructing REVDeployer. This removes the old frontrunnable ambient initializer, but it also means deployment scripts must not skip the prebind step.
27
- - **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.
28
- - **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.
29
- - **Juicebox core contracts are correct.** `JBController`, `JBMultiTerminal`, `JBTerminalStore`, `JBTokens`, `JBPrices` -- a bug in any of these is a bug in every revnet.
30
- - **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.
31
- - **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.
32
- - **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.
33
- - **REVLoans contract address is immutable per deployer.** `LOANS` is set once in the REVDeployer constructor (and shared as an immutable on REVOwner) with wildcard `USE_ALLOWANCE` permission (`projectId=0`). If the loans contract has a vulnerability, every revnet's surplus is exposed.
34
-
35
- ### What you do NOT need to trust
36
-
37
- - **Project owners.** There are none. REVDeployer permanently holds the project NFT.
38
- - **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.
39
- - **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.
40
-
41
- ---
21
+ - **`REVDeployer` and `REVOwner` are one design.** Misreading them independently is a review hazard.
22
+ - **Core protocol state is still upstream truth.** Revnet economics sit on top of `nana-core-v6`, not outside it.
23
+ - **Optional integrations matter.** Buybacks, 721 hooks, and suckers can materially change runtime behavior.
24
+ - **Price feeds and source accounting matter for loans.** Cross-currency debt aggregation depends on working feed assumptions.
42
25
 
43
26
  ## 2. Economic Risks
44
27
 
45
- ### Loan economics
46
-
47
- - **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.
48
- - **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.
49
- - **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).
50
-
51
- ### Stage transition edge cases
52
-
53
- - **`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.
54
- - **`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.
55
- - **Weight decay approaching zero over long periods.** With `issuanceCutPercent > 0` and `issuanceCutFrequency > 0`, issuance weight decays exponentially through successive rulesets. After long enough, 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. The current repo models decay through ruleset duration and `weightCutPercent`, so validation should focus on long-horizon ruleset progression and issuance-decay tests rather than any separate weight-cache updater.
56
- - **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.
57
-
58
- ### Cross-currency reclaim calculations
59
-
60
- - **`_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.
61
- - **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.
62
- - **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.
63
- - **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.
64
- - **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.
65
-
66
- ### Hidden token supply manipulation
67
-
68
- - **Hiding reduces totalSupply, inflating per-token cash-out value.** When a holder hides tokens via `REVHiddenTokens`, those tokens are burned and excluded from `totalSupply`. The bonding curve sees fewer tokens, so each remaining token is worth more on cash-out. A holder with a large position can hide tokens, have an accomplice cash out at the inflated rate, then reveal. The net effect depends on the `cashOutTaxRate` and the relative positions. Operator delegation (`HIDE_TOKENS`/`REVEAL_TOKENS` permissions) extends this to permissioned operators acting on behalf of holders.
69
- - **Hidden tokens must be revealed before use as loan collateral.** `REVHiddenTokens` and `REVLoans` are separate systems. A holder cannot borrow against hidden tokens — they must first reveal (re-mint) them, then borrow. This is by design but may confuse users.
70
- - **Reveal mints without reserved percent.** `revealTokensOf` calls `mintTokensOf` with `useReservedPercent: false`. This is correct because the tokens were previously burned and are being restored, not newly issued. But it means revealed tokens bypass the reserved-token mechanism entirely.
71
- - **REVHiddenTokens has mint permission via REVOwner.** The contract is added to `REVOwner.hasMintPermissionFor`. If `REVHiddenTokens` has a vulnerability, it could mint unbounded tokens for any revnet.
72
-
73
- ### Auto-issuance overflow potential
74
-
75
- - **`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.
76
- - **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.
77
-
78
- ---
79
-
80
- ## 3. Loan System Risks
81
-
82
- ### Collateral valuation during price volatility
83
-
84
- - **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.
85
- - **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.
86
-
87
- ### Liquidation concerns
88
-
89
- - **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.
90
- - **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.
91
- - **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.
92
-
93
- ### Loan source rotation after deployment
94
-
95
- - **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.
96
- - **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.
97
-
98
- ### `reallocateCollateralFromLoan` sandwich potential
99
-
100
- - **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.
101
- - **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.
102
- - **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.
103
-
104
- ### Cross-chain cash-out delay enforcement
105
-
106
- - **Loans enforce the same 30-day cash-out delay as direct cash outs.** When a revnet is deployed to a new chain where its first stage has already started, REVDeployer calls `REVOwner.setCashOutDelayOf()` to set a 30-day delay (stored on REVOwner). `borrowFrom` resolves the REVOwner from the current ruleset's `dataHook` and checks `IREVOwner.cashOutDelayOf(revnetId)` (read directly from REVOwner storage), reverting with `REVLoans_CashOutDelayNotFinished` if the delay hasn't passed. `borrowableAmountFrom` returns 0 during the delay. This prevents cross-chain arbitrage via the loan system (bridging tokens to a new chain and immediately borrowing against them before prices equilibrate).
107
-
108
- ### BURN_TOKENS permission prerequisite
109
-
110
- - **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.
111
-
112
- ---
113
-
114
- ## 4. Data Hook Proxy Risks
115
-
116
- REVOwner sits between the terminal and the actual hooks (buyback hook, 721 hook). This proxy pattern creates composition risks.
28
+ - **Stage immutability cuts both ways.** A bad stage schedule or bad cash-out tax choice is expensive to unwind.
29
+ - **Borrowability depends on live economics.** If surplus, supply, or cross-chain state are wrong, loan capacity becomes wrong.
30
+ - **Zero or degraded price feeds can undercount debt.** If a source becomes invisible to debt aggregation, later borrowing can become too permissive.
31
+ - **Hidden-token mechanics change visible supply.** That affects per-token cash-out value and can change the economics seen by other holders.
32
+ - **Auto-issuance dilutes holders predictably but still materially.** Timing is permissionless, even if the amounts are fixed at deployment.
33
+ - **Omnichain expansion can corrupt surplus aggregation.** Since borrowability aggregates surplus from all registered terminals across chains, a compromised or misconfigured terminal on a remote chain affects global surplus accounting.
117
34
 
118
- ### Underlying hook reverts
35
+ ## 3. Loan Risks
119
36
 
120
- - **721 hook revert in `beforePayRecordedWith`.** The call to `IJBRulesetDataHook(tiered721Hook).beforePayRecordedWith(context)` in REVOwner 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.
121
- - **Buyback hook is more resilient.** The `BUYBACK_HOOK.beforePayRecordedWith(buybackHookContext)` call in REVOwner 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.
122
- - **Cash-out fee terminal revert.** In `REVOwner.afterCashOutRecordedWith`, the fee payment to the fee terminal IS wrapped in try-catch with a fallback to `addToBalanceOf`. If the fallback also reverts, the entire cashout transaction reverts — no funds are stuck, but the cashout is blocked until the terminal is available.
37
+ - **Burned collateral is not escrow.** Reviewers and integrators who model it as escrow will misread liquidation and repayment behavior.
38
+ - **No short-term liquidation model.** Under-collateralized loans can persist until the long expiry model allows cleanup.
39
+ - **Loan sources grow over time.** Debt aggregation cost and complexity increase as new source pairs are used.
40
+ - **Reallocation still depends on live state.** Reallocate flows can change outcomes around stage boundaries.
123
41
 
124
- ### Sucker bypass path (0% cashout tax)
42
+ ## 4. Hidden-Token Risks
125
43
 
126
- - **Suckers bypass all economic protections.** `REVOwner.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.
127
- - **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.
44
+ - **Visible-supply manipulation is intentional.** Hiding tokens changes visible supply and therefore changes redemption economics.
45
+ - **Hidden tokens are not usable collateral while hidden.** They must be revealed before borrowing.
46
+ - **Reveal is restoration, not fresh issuance.** It intentionally bypasses reserved-percent behavior.
128
47
 
129
- ### Permission escalation through proxy
48
+ ## 5. Hook-Composition Risks
130
49
 
131
- - **`hasMintPermissionFor` grants mint to five categories.** `REVOwner.hasMintPermissionFor` grants mint to: the loans contract, the hidden tokens contract, buyback hook, buyback hook delegates, and suckers. If any of these contracts have a vulnerability that allows arbitrary calls, they can mint unlimited revnet tokens for any revnet.
132
- - **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.
50
+ - **`REVOwner` is a real runtime authority surface.** It composes pay hooks, cash-out hooks, sucker exemptions, and fee logic.
51
+ - **Suckers can bypass tax and fee paths by design.** That privilege is safe only if registry and deployer assumptions are correct.
52
+ - **Mint-permission surfaces are broad enough to matter.** Loans, hidden tokens, buyback flows, and suckers all touch mint authority in some deployments.
133
53
 
134
- ---
54
+ ## 6. Access-Control Risks
135
55
 
136
- ## 5. Access Control
137
-
138
- ### Stage configuration immutability
139
-
140
- - **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.
141
- - **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.
142
-
143
- ### Who can deploy and modify
144
-
145
- - **`deployFor` is permissionless for new revnets** (`revnetId == 0`). Anyone can deploy a revnet with arbitrary configuration.
146
- - **`deployFor` with existing project requires ownership.** The caller must be `PROJECTS.ownerOf(revnetId)` and the project must be blank (no controller, no rulesets).
147
- - **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.
148
- - **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).
149
-
150
- ### Split operator trust boundaries
151
-
152
- - **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).
153
- - **SET_SPLIT_GROUPS.** The split operator controls where reserved tokens go. A compromised operator can redirect all reserved tokens to themselves.
154
- - **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.
155
- - **SET_ROUTER_TERMINAL.** The split operator can configure the router terminal, potentially redirecting payments.
156
-
157
- ---
158
-
159
- ## 6. DoS Vectors
160
-
161
- ### Long stage chains
162
-
163
- - **`_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.
164
- - **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.
165
-
166
- ### Many auto-issuances
167
-
168
- - **`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.
169
-
170
- ### Loan source enumeration
171
-
172
- - **`_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.
173
- - **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.
174
-
175
- ---
56
+ - **The deployer-held project NFT can be misunderstood.** Revnets are owner-minimized, but the deployer path still matters for the trust model.
57
+ - **Split operator mistakes are high-impact.** Narrow powers like price-feed installation, split updates, sucker deployment, or router setup still matter.
58
+ - **There is intentionally no broad admin recovery path.** Operational teams may try to reach for powers the design never intended to leave available.
176
59
 
177
60
  ## 7. Invariants to Verify
178
61
 
179
- These MUST hold. Breaking any of them is a finding.
180
-
181
- ### Loan accounting
182
-
183
- - **Collateral conservation.** `totalCollateralOf[revnetId]` == sum of `loan.collateral` for all active (non-liquidated, non-repaid) loans of that revnet.
184
- - **Borrowed amount conservation.** `totalBorrowedFrom[revnetId][terminal][token]` == sum of `loan.amount` for all active loans from that source.
185
- - **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).
186
- - **No zero-collateral loans.** Every active loan has `collateral > 0` and `amount > 0`. The `borrowFrom` function reverts on `collateralCount == 0` and `borrowAmount == 0`.
187
- - **Liquidation only after expiry.** `liquidateExpiredLoansFrom` skips loans where `block.timestamp <= loan.createdAt + LOAN_LIQUIDATION_DURATION`.
188
- - **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.
189
-
190
- ### Stage and deployment
191
-
192
- - **Stage immutability.** After `deployFor` completes, no function in REVDeployer calls `CONTROLLER.queueRulesetsOf` or modifies ruleset parameters.
193
- - **Stage progression monotonicity.** `startsAtOrAfter` values strictly increase between stages. The first stage can be 0 (mapped to `block.timestamp`).
194
- - **Auto-issuance single-claim.** Each `(revnetId, stageId, beneficiary)` can only be claimed once. `amountToAutoIssue` is zeroed BEFORE the external `mintTokensOf` call (CEI pattern).
195
- - **Split percentages.** Per-stage `splitPercent > 0` requires `splits.length > 0`. Split percentages are validated by `JBSplits` in core (must sum to <= `SPLITS_TOTAL_PERCENT`).
196
-
197
- ### Fee flows
198
-
199
- - **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.
200
- - **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.
201
-
202
- ### Hidden token accounting
203
-
204
- - **Hidden balance conservation.** `totalHiddenOf[revnetId]` == sum of `hiddenBalanceOf[holder][revnetId]` across all holders.
205
- - **Reveal bounded by hide.** No holder can reveal more tokens than they have hidden. `revealTokensOf` reverts with `REVHiddenTokens_InsufficientHiddenBalance` if `tokenCount > hiddenBalanceOf[caller][revnetId]`.
206
- - **No double-mint from reveal.** Each hidden token can only be revealed once. Decrementing `hiddenBalanceOf` before minting prevents double-reveal.
207
-
208
- ### Privilege isolation
209
-
210
- - **Sucker privilege.** Only addresses returning `true` from `SUCKER_REGISTRY.isSuckerOf(projectId, addr)` get 0% cashout tax. No other code path grants this exemption.
211
- - **Loan ownership.** Only `_ownerOf(loanId)` — or an operator with the relevant `JBPermissionIds` (`REPAY_LOAN` for repayment, `REALLOCATE_LOAN` for reallocation) — can call `repayLoan` and `reallocateCollateralFromLoan`. Similarly, `borrowFrom` requires the caller to be the `holder` or to have `OPEN_LOAN` permission. The loan NFT is burned before any state changes in repayment, preventing double-use. Replacement loan NFTs are minted to the original holder/owner, not the operator. However, delegated operators control the `beneficiary` parameter and can direct borrowed funds, returned collateral, or revealed tokens to any address (see §8.5).
212
- - **Mint permission.** Only `LOANS`, `HIDDEN_TOKENS`, `BUYBACK_HOOK`, buyback hook delegates (via `BUYBACK_HOOK.hasMintPermissionFor`), and suckers (via `REVOwner._isSuckerOf`) can mint tokens. No other address passes the `REVOwner.hasMintPermissionFor` check.
213
-
214
- ---
62
+ - Collateral and debt conservation across all active loans.
63
+ - Stage immutability after deployment.
64
+ - Borrowability dropping to zero when cash-out delay should still block borrowing.
65
+ - Hidden-balance conservation across hide and reveal flows.
66
+ - Sucker-only privileges staying restricted to real registered suckers.
67
+ - Mint permission remaining limited to the documented runtime surfaces.
215
68
 
216
69
  ## 8. Accepted Behaviors
217
70
 
218
- ### 8.1 Suckers receive 0% cashout tax (by design)
71
+ ### 8.1 Suckers receive 0% cash-out tax
219
72
 
220
- `REVOwner.beforeCashOutRecordedWith` returns `cashOutTaxRate = 0` for any address where `SUCKER_REGISTRY.isSuckerOf(projectId, addr)` returns true. This grants suckers the full pro-rata reclaim with no tax retention. This is intentional: suckers burn tokens on the source chain and mint equivalent tokens on the destination chain. The zero-tax path ensures bridged tokens preserve their full economic value across chains. The security boundary is the sucker registry — only addresses registered by authorized deployers (gated by `DEPLOY_SUCKERS` permission and per-stage `extraMetadata` bit 2) receive this privilege.
73
+ Trusted suckers are intentionally exempt so bridged value preserves its economic meaning across chains.
221
74
 
222
- ### 8.2 No liquidation trigger for under-collateralized loans (by design)
75
+ ### 8.2 There is no short-horizon liquidation model
223
76
 
224
- `REVLoans` has no health factor, no margin call, and no keeper-triggered liquidation. The only liquidation path is `liquidateExpiredLoansFrom` after 10 years. This is a conscious design choice: the protocol treats under-collateralized loans as free put options where the borrower forfeits worthless collateral and keeps the borrowed funds. The protocol absorbs this "loss" through permanent supply reduction (burned collateral), which is deflationary for remaining holders. A liquidation mechanism would add complexity, require oracles, and introduce MEV extraction opportunities at liquidation boundaries — all of which conflict with the revnet's minimal-trust design philosophy.
77
+ Revnet loans are designed more like long-dated economic positions than instantly mark-to-market margin loans.
225
78
 
226
79
  ### 8.3 Auto-issuance dilution is permissionless but predictable
227
80
 
228
- `autoIssueFor` can be called by anyone, diluting existing holders by minting pre-configured token amounts to beneficiaries. This is accepted because: (1) auto-issuance amounts are set immutably at deployment, so dilution is fully predictable, (2) the dilution only occurs once per `(revnetId, stageId, beneficiary)` tuple (single-claim guarantee), and (3) delaying the call only delays the inevitable — the configured amounts will eventually be minted. A griefing vector exists where someone calls `autoIssueFor` immediately before another user's cash-out, but the dilution magnitude is deterministic and can be priced in.
229
-
230
- ### 8.4 Surplus manipulation via donations is economically irrational (by design)
231
-
232
- `_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. The attack is self-defeating by construction.
233
-
234
- ### 8.5 Delegated operators control beneficiary independently of holder/owner (by design)
235
-
236
- When a holder grants `OPEN_LOAN`, `REALLOCATE_LOAN`, `REPAY_LOAN`, or `REVEAL_TOKENS` permission to an operator, the operator can set the `beneficiary` parameter to any address — including themselves. This means:
81
+ Anyone can trigger a valid auto-issuance once a stage is live, but the amount was fixed at deployment.
237
82
 
238
- - **`borrowFrom`**: An operator with `OPEN_LOAN` permission burns the holder's tokens as collateral but can direct the borrowed funds to an arbitrary beneficiary.
239
- - **`reallocateCollateralFromLoan`**: An operator with `REALLOCATE_LOAN` permission can direct the borrowed funds from the new loan to an arbitrary beneficiary.
240
- - **`repayLoan`**: An operator with `REPAY_LOAN` permission can direct returned collateral tokens to an arbitrary beneficiary.
241
- - **`revealTokensOf`**: An operator with `REVEAL_TOKENS` permission can direct revealed (re-minted) tokens to an arbitrary beneficiary instead of the original holder.
83
+ ### 8.4 Surplus manipulation by pure donation is economically self-defeating
242
84
 
243
- This is accepted because the JBPermissions delegation model is opt-in: a holder explicitly grants permission to a specific operator for a specific project. The holder trusts the operator to act in their interest. Restricting `beneficiary` to the holder would break legitimate delegation use cases (e.g., automated vaults, multi-sig workflows, portfolio managers). Holders should only grant these permissions to operators they fully trust.
85
+ The model assumes that attempts to inflate surplus through donations are not profitable once the surrounding bonding-curve math is considered.
244
86
 
245
- ### 8.6 Borrow-repay arbitrage is unprofitable (by design)
87
+ ### 8.5 Omnichain terminal expansion inherits remote-chain trust
246
88
 
247
- A borrower who pays the prepaid fee upfront (minimum 2.5% + REV fee 1% = 3.5%) can repay at any time within the prepaid duration with no additional cost. If the bonding curve value of the collateral increases during the prepaid window, the borrower can repay, recover their collateral, and cash out at the higher value. This is not profitable as a standalone strategy because the 3.5% minimum fee exceeds the expected value gained from short-term surplus fluctuations. For borrowers who need liquidity anyway, it provides free optionality which is the intended use case.
89
+ A project that expands to a new chain can register additional terminals on that chain. Because borrowability calculations aggregate surplus from all registered terminals across all chains, a compromised or misconfigured terminal on a remote chain can corrupt the project's surplus accounting globally. This is accepted because revnet terminals are set and fixed on deploy for the initial chain, but expansion to new chains inherently requires trust in the new chain's terminal infrastructure, bridge integrity, and deployment configuration. Project operators should treat each chain expansion as a trust-boundary decision.
package/SKILLS.md CHANGED
@@ -2,20 +2,20 @@
2
2
 
3
3
  ## Use This File For
4
4
 
5
- - Use this file when the task involves revnet deployment, staged issuance, split operator logic, auto-issuance, or the revnet loan system built on top of Juicebox core.
6
- - Start here, then open the deployer, owner, or loans contract depending on whether the issue is launch-time config, runtime hook behavior, or debt accounting.
5
+ - Use this file when the task involves revnet deployment, staged issuance, split-operator logic, auto-issuance, hidden tokens, or the revnet loan system.
6
+ - Start here, then decide whether the issue is really in `REVDeployer`, `REVOwner`, `REVLoans`, or `REVHiddenTokens`.
7
7
 
8
8
  ## Read This Next
9
9
 
10
10
  | If you need... | Open this next |
11
11
  |---|---|
12
- | Repo overview and operator flow | [`README.md`](./README.md) |
12
+ | Repo overview and operator flow | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
13
13
  | Deployment and stage config | [`src/REVDeployer.sol`](./src/REVDeployer.sol), [`script/Deploy.s.sol`](./script/Deploy.s.sol) |
14
- | Runtime owner and data-hook behavior | [`src/REVOwner.sol`](./src/REVOwner.sol) |
14
+ | Runtime owner and data-hook behavior | [`src/REVOwner.sol`](./src/REVOwner.sol), [`references/runtime.md`](./references/runtime.md) |
15
15
  | Loan accounting and liquidation behavior | [`src/REVLoans.sol`](./src/REVLoans.sol) |
16
16
  | Temporary token hiding and supply exclusion | [`src/REVHiddenTokens.sol`](./src/REVHiddenTokens.sol) |
17
17
  | Types and helpers | [`src/structs/`](./src/structs/), [`src/interfaces/`](./src/interfaces/), [`test/helpers/`](./test/helpers/) |
18
- | Loan regressions, economics, and forks | [`test/REVLoansRegressions.t.sol`](./test/REVLoansRegressions.t.sol), [`test/TestLongTailEconomics.t.sol`](./test/TestLongTailEconomics.t.sol), [`test/fork/`](./test/fork/), [`test/regression/`](./test/regression/) |
18
+ | Lifecycle, loans, and economic proofs | [`test/REVLifecycle.t.sol`](./test/REVLifecycle.t.sol), [`test/REVLoansRegressions.t.sol`](./test/REVLoansRegressions.t.sol), [`test/REVLoans.invariants.t.sol`](./test/REVLoans.invariants.t.sol), [`test/TestLongTailEconomics.t.sol`](./test/TestLongTailEconomics.t.sol) |
19
19
 
20
20
  ## Repo Map
21
21
 
@@ -28,17 +28,19 @@
28
28
 
29
29
  ## Purpose
30
30
 
31
- Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged issuance schedules, built-in Uniswap buyback pools, cross-chain suckers, and token-collateralized lending.
31
+ Deploy and manage Revnets: autonomous Juicebox project shapes with staged issuance schedules, optional buyback pools, cross-chain suckers, hidden-token mechanics, and token-collateralized lending.
32
32
 
33
33
  ## Reference Files
34
34
 
35
- | If you need... | Open this next |
36
- |---|---|
37
- | Contract roles, deploy/runtime entrypoints, integration points, or key structs | [`references/runtime.md`](./references/runtime.md) |
38
- | Events, errors, constants, storage, gotchas, state-reading recipes, or examples | [`references/operations.md`](./references/operations.md) |
35
+ - Open [`references/runtime.md`](./references/runtime.md) for contract roles, deploy/runtime entrypoints, integration points, and key structs.
36
+ - Open [`references/operations.md`](./references/operations.md) for events, errors, constants, storage, gotchas, and state-reading recipes.
39
37
 
40
38
  ## Working Rules
41
39
 
42
- - Start in `REVDeployer` for launch-time behavior, `REVOwner` for runtime hook behavior, `REVLoans` for collateral and debt accounting, and `REVHiddenTokens` for temporary supply exclusion.
43
- - Verify any economic assumption in code or tests before relying on it. Revnet docs carry more economic interpretation than most repos.
44
- - For anything cross-chain or stage-related, check both the deployer path and the reading-state reference before editing.
40
+ - Start in `REVDeployer` for launch-time behavior, `REVOwner` for runtime hook behavior, `REVLoans` for debt accounting, and `REVHiddenTokens` for supply exclusion.
41
+ - Revnets are intentionally ownerless after deployment. Treat any admin-recovery instinct as suspect unless the code proves it.
42
+ - `REVOwner` is not a minor helper; it is a live runtime policy surface.
43
+ - Loan collateral is burned and re-minted, not escrowed. Any change that assumes escrow semantics is likely wrong.
44
+ - Cash-out delay is enforced in both runtime cash-outs and loan borrowing. If one path changes without the other, the protection is broken.
45
+ - Hidden tokens are supply exclusion, not a side balance.
46
+ - Loan behavior, stage transitions, hidden supply, and split-weight adjustments interact. Do not treat them as independent subsystems.