@rev-net/core-v6 0.0.23 → 0.0.25

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/USER_JOURNEYS.md CHANGED
@@ -1,508 +1,85 @@
1
- # User Journeys -- revnet-core-v6
1
+ # User Journeys
2
2
 
3
- Every user path through the Revnet + Loans system. For each journey: entry point, key parameters, state changes, events, and edge cases.
3
+ ## Who This Repo Serves
4
4
 
5
- Read [SKILLS.md](./SKILLS.md) for the complete function reference. Read [ARCHITECTURE.md](./ARCHITECTURE.md) for the system overview.
5
+ - teams launching autonomous Revnets with encoded stage transitions
6
+ - participants buying, holding, and cashing out Revnet exposure over time
7
+ - borrowers using Revnet tokens as collateral instead of selling them
8
+ - operators working inside the narrow post-launch envelope the deployer allows
6
9
 
7
- ---
10
+ ## Journey 1: Launch A Revnet With Its Long-Lived Rules Encoded Up Front
8
11
 
9
- ## 1. Deploy a New Revnet
12
+ **Starting state:** you know the stage schedule, issuance behavior, optional integrations, and runtime controls the Revnet should allow.
10
13
 
11
- **Entry point:** `REVDeployer.deployFor(revnetId=0, configuration, terminalConfigurations, suckerDeploymentConfiguration)` (4-arg version, deploys with default empty 721 hook)
14
+ **Success:** the Revnet launches as a Juicebox project whose deploy-time shape already encodes its economic envelope.
12
15
 
13
- Or: `REVDeployer.deployFor(revnetId=0, configuration, terminalConfigurations, suckerDeploymentConfiguration, tiered721HookConfiguration, allowedPosts)` (6-arg version, deploys with configured 721 tiers and optional croptop posts)
16
+ **Flow**
17
+ 1. Use `REVDeployer` with the staged config, split operators, and optional integrations such as 721 hooks, buyback routing, router terminal support, or suckers.
18
+ 2. The deployer launches the underlying project and keeps the ownership model aligned with the Revnet runtime contracts.
19
+ 3. Stage config, issuance behavior, and auxiliary surfaces are committed as part of the launch instead of being left to human operators later.
20
+ 4. The network can now accept payments and transition across stages without ordinary project-owner governance.
14
21
 
15
- **Key parameters:**
22
+ ## Journey 2: Participate In The Revnet Across Stage Transitions
16
23
 
17
- | Parameter | Type | Description |
18
- |-----------|------|-------------|
19
- | `revnetId` | `uint256` | Set to 0 to deploy a new revnet. |
20
- | `configuration.description` | `REVDescription` | `name`, `ticker`, `uri`, `salt` for the ERC-20 token. |
21
- | `configuration.baseCurrency` | `uint32` | 1 = ETH, 2 = USD. Determines the denomination for issuance weights. |
22
- | `configuration.splitOperator` | `address` | The address that will manage splits, 721 tiers, and suckers. |
23
- | `configuration.stageConfigurations` | `REVStageConfig[]` | One or more stages defining the revnet's economics. |
24
- | `terminalConfigurations` | `JBTerminalConfig[]` | Which terminals and tokens the revnet accepts. |
25
- | `suckerDeploymentConfiguration` | `REVSuckerDeploymentConfig` | Cross-chain sucker config. Set `salt = bytes32(0)` to skip. |
24
+ **Starting state:** the Revnet is live and a participant wants to pay in, hold exposure, or cash out later.
26
25
 
27
- **What happens (in order):**
26
+ **Success:** participation follows the current stage's rules without requiring the user to reason about every deploy-time parameter directly.
28
27
 
29
- 1. `revnetId = PROJECTS.count() + 1` (next available ID)
30
- 2. `_makeRulesetConfigurations` converts stages to JBRulesetConfigs:
31
- - Validates: at least one stage, `startsAtOrAfter` strictly increasing, `cashOutTaxRate < MAX`, splits required if `splitPercent > 0`
32
- - Each stage becomes a ruleset with: duration = `issuanceCutFrequency`, weight = `initialIssuance`, weightCutPercent = `issuanceCutPercent`, data hook = REVOwner address
33
- - Fund access limits: unlimited surplus allowance per terminal/token (for loans)
34
- - Encoded configuration hash computed from economic parameters
35
- - Auto-issuance amounts stored: `amountToAutoIssue[revnetId][block.timestamp + i][beneficiary] += count`
36
- 3. `CONTROLLER.launchProjectFor` creates the Juicebox project, minting the ERC-721 to REVDeployer
37
- 4. Cash-out delay set if first stage's `startsAtOrAfter` has already passed (existing revnet deploying to new chain)
38
- 5. `CONTROLLER.deployERC20For` deploys the project's ERC-20 token
39
- 6. Buyback pools initialized for each terminal token via `_tryInitializeBuybackPoolFor` (try-catch, silent failure OK)
40
- 7. Suckers deployed if `suckerDeploymentConfiguration.salt != bytes32(0)`
41
- 8. Config hash stored: `hashedEncodedConfigurationOf[revnetId] = encodedConfigurationHash`
42
- 9. **4-arg version only:** Default empty 721 hook deployed, split operator gets all 721 permissions
43
- 10. **6-arg version only:** Configured 721 hook deployed with prevention flags applied, croptop posts configured if any
28
+ **Flow**
29
+ 1. Users pay through the configured terminal or router surface.
30
+ 2. `REVOwner` enforces runtime behavior such as pay handling, cash-out rules, delayed exits, and other stage-sensitive constraints.
31
+ 3. As stages advance, later pays and cash outs follow the newly active parameters while the project identity stays constant.
44
32
 
45
- **Events:**
46
- - `DeployRevnet(revnetId, configuration, terminalConfigurations, suckerDeploymentConfiguration, rulesetConfigurations, encodedConfigurationHash, caller)`
47
- - `StoreAutoIssuanceAmount(revnetId, stageId, beneficiary, count, caller)` for each auto-issuance on this chain
48
- - `DeploySuckers(...)` if suckers are deployed
49
- - `SetCashOutDelay(revnetId, cashOutDelay, caller)` if applicable
33
+ **Failure cases that matter:** assuming a stage parameter is mutable when it is fixed at launch, misreading delayed cash-out behavior, and forgetting that optional integrations materially change the participant experience.
50
34
 
51
- **Edge cases:**
52
- - If `startsAtOrAfter = 0` for the first stage, `block.timestamp` is used. The stage starts immediately.
53
- - Auto-issuances with `chainId != block.chainid` are included in the config hash but not stored on this chain.
54
- - Auto-issuances with `count = 0` are skipped (not stored, not included in config hash).
55
- - Buyback pool initialization silently fails if the pool already exists.
56
- - The `assert` on `launchProjectFor` return value catches project ID mismatches (should never happen).
35
+ ## Journey 3: Claim Stage-Based Auto-Issuance When It Becomes Available
57
36
 
58
- ---
37
+ **Starting state:** the Revnet was deployed with auto-issuance allocations for one or more beneficiaries.
59
38
 
60
- ## 2. Convert an Existing Juicebox Project to a Revnet
39
+ **Success:** the beneficiary claims the right amount for the right stage only after that stage is actually live.
61
40
 
62
- **Entry point:** `REVDeployer.deployFor(revnetId=<existingProjectId>, configuration, terminalConfigurations, suckerDeploymentConfiguration)`
41
+ **Flow**
42
+ 1. Check `amountToAutoIssue(...)` for the revnet, stage, and beneficiary.
43
+ 2. Call `autoIssueFor(...)` only once the target stage has started.
44
+ 3. The stored allocation is consumed so the same stage allocation cannot be claimed twice.
63
45
 
64
- **Prerequisites:**
65
- - Caller must own the project's ERC-721 NFT
66
- - Project must have no controller and no rulesets (blank project)
46
+ ## Journey 4: Borrow Against Revnet Tokens Instead Of Selling Them
67
47
 
68
- **What happens:**
48
+ **Starting state:** a holder wants liquidity but does not want to exit the Revnet position.
69
49
 
70
- 1. `_msgSender()` must equal `PROJECTS.ownerOf(revnetId)` (owner check)
71
- 2. Project NFT transferred from owner to REVDeployer via `safeTransferFrom` -- **irreversible**
72
- 3. `CONTROLLER.launchRulesetsFor` initializes rulesets for the existing project
73
- 4. `CONTROLLER.setUriOf` sets the project's metadata URI
74
- 5. Cash-out delay applied if first stage has already started
75
- 6. Same remaining steps as new deployment (ERC-20, buyback pools, suckers, 721 hook)
50
+ **Success:** the holder opens a loan, receives borrowed value, and keeps an NFT loan position representing the debt.
76
51
 
77
- **Events:**
78
- - `DeployRevnet(revnetId, configuration, terminalConfigurations, suckerDeploymentConfiguration, rulesetConfigurations, encodedConfigurationHash, caller)`
79
- - `StoreAutoIssuanceAmount(revnetId, stageId, beneficiary, count, caller)` for each auto-issuance on this chain
80
- - `DeploySuckers(...)` if suckers are deployed
81
- - `SetCashOutDelay(revnetId, cashOutDelay, caller)` if applicable
52
+ **Flow**
53
+ 1. The holder interacts with `REVLoans` using the eligible Revnet token exposure as collateral.
54
+ 2. The system burns or escrows the relevant token exposure and mints a loan-position NFT.
55
+ 3. Loan terms depend on the live Revnet economics rather than a static side system.
56
+ 4. The borrower can later repay, transfer the loan NFT, or face liquidation if conditions require it.
82
57
 
83
- **Edge cases:**
84
- - This is a **one-way operation**. The project NFT is permanently locked in REVDeployer.
85
- - `launchRulesetsFor` reverts if rulesets already exist. `setControllerOf` reverts if a controller is already set.
86
- - Useful in deploy scripts where the project ID is needed before configuration (e.g., for cross-chain sucker peer mappings).
58
+ ## Journey 5: Repay, Transfer, Or Liquidate A Loan Position
87
59
 
88
- ---
60
+ **Starting state:** a loan already exists and either the borrower or another actor needs to change its state.
89
61
 
90
- ## 3. Pay a Revnet
62
+ **Success:** the debt path settles cleanly and the collateral outcome matches the Revnet's current rules.
91
63
 
92
- **Entry point:** `JBMultiTerminal.pay(projectId, token, amount, beneficiary, minReturnedTokens, memo, metadata)`
64
+ **Flow**
65
+ 1. Repayment burns the debt and remints or releases the collateralized Revnet token position.
66
+ 2. Transfers move the loan NFT, not the original collateralized exposure.
67
+ 3. Liquidation consumes the loan under the rules encoded by `REVLoans` and the current Revnet state.
93
68
 
94
- This is a standard Juicebox payment, but REVOwner intervenes as the data hook.
69
+ **Edge conditions that change user experience:** cross-ruleset loan behavior, zero-amount or zero-price edge cases, and sourced versus unsourced loan paths.
95
70
 
96
- **What happens:**
71
+ ## Journey 6: Operate Inside The Bounded Post-Launch Control Envelope
97
72
 
98
- 1. Terminal records payment in store
99
- 2. Store calls `REVOwner.beforePayRecordedWith(context)`:
100
- - Reads `tiered721HookOf` from REVOwner storage
101
- - Calls 721 hook's `beforePayRecordedWith` for split specs (tier purchases)
102
- - Computes `projectAmount = context.amount.value - totalSplitAmount`
103
- - Calls buyback hook's `beforePayRecordedWith` with reduced amount context
104
- - Scales weight: `weight = mulDiv(weight, projectAmount, context.amount.value)` (or 0 if `projectAmount == 0`)
105
- - Returns merged hook specs: [721 hook spec, buyback hook spec]
106
- 3. Store calculates token count using the modified weight
107
- 4. Terminal mints tokens via controller
108
- 5. Terminal executes hook specs:
109
- - 721 hook processes tier purchases
110
- - Buyback hook processes swap (if applicable)
73
+ **Starting state:** the Revnet is live and an operator wants to use whatever ongoing controls the deployment allowed.
111
74
 
112
- **Preview**: Call `JBMultiTerminal.previewPayFor(revnetId, token, amount, beneficiary, metadata)` to simulate the full payment including REVOwner's data hook effects (buyback routing, 721 tier splits, weight adjustment). Returns the expected token count and hook specifications. When the buyback hook is active, noop specs may carry routing diagnostics (TWAP tick, liquidity, pool ID) even when the protocol mint path wins.
75
+ **Success:** the operator can use sanctioned controls without escaping the "autonomous after launch" model.
113
76
 
114
- **Events:** No revnet-specific events. The payment is handled by `JBMultiTerminal` and `JBController` (see nana-core-v6). REVOwner's `beforePayRecordedWith` is a `view` function and emits nothing.
77
+ **Flow**
78
+ 1. Review what `REVDeployer` allowed for split operators, stage evolution, and auxiliary integrations.
79
+ 2. Use only those surfaces rather than treating the project like a normal owner-governed Juicebox project.
80
+ 3. Audit cross-package behavior whenever the Revnet enabled buybacks, 721 hooks, router terminals, or suckers.
115
81
 
116
- **Edge cases:**
117
- - If the buyback hook determines a DEX swap is better, weight = 0 and the buyback hook spec receives the full project amount. The buyback hook buys tokens on the DEX and mints them to the payer.
118
- - If `totalSplitAmount >= context.amount.value`, `projectAmount = 0`, weight = 0, and no tokens are minted by the terminal. All funds go to 721 tier splits.
119
- - If no 721 hook is set (`tiered721HookOf[revnetId] == address(0)` on REVOwner), only the buyback hook is consulted.
82
+ ## Hand-Offs
120
83
 
121
- ---
122
-
123
- ## 4. Cash Out from a Revnet
124
-
125
- **Entry point:** `JBMultiTerminal.cashOutTokensOf(holder, projectId, tokenCount, token, minTokensReclaimed, beneficiary, metadata)`
126
-
127
- **What happens:**
128
-
129
- 1. Terminal records cash-out in store
130
- 2. Store calls `REVOwner.beforeCashOutRecordedWith(context)`:
131
- - **If sucker:** Returns 0% tax, full cash-out count, no hooks (fee exempt)
132
- - **If cash-out delay active:** Reads `cashOutDelayOf` from REVOwner storage, reverts with `REVDeployer_CashOutDelayNotFinished`
133
- - **If no tax or no fee terminal:** Returns parameters unchanged
134
- - **Otherwise:** Splits cash-out into fee portion (2.5%) and non-fee portion:
135
- - `feeCashOutCount = mulDiv(cashOutCount, 25, 1000)`
136
- - `nonFeeCashOutCount = cashOutCount - feeCashOutCount`
137
- - Computes `postFeeReclaimedAmount` via bonding curve for non-fee tokens
138
- - Computes `feeAmount` via bonding curve for fee tokens (on remaining surplus)
139
- - Returns `nonFeeCashOutCount` as the adjusted cash-out count + hook spec for fee
140
- 3. Terminal burns ALL of the user's specified token count
141
- 4. Terminal transfers the reclaimed amount to the beneficiary
142
- 5. Terminal calls `REVOwner.afterCashOutRecordedWith(context)`:
143
- - Transfers fee amount from terminal to this contract
144
- - Pays fee to fee revnet's terminal via `feeTerminal.pay`
145
- - On failure: returns funds to the originating project via `addToBalanceOf`
146
-
147
- **Preview**: Call `JBMultiTerminal.previewCashOutFrom(holder, revnetId, cashOutCount, tokenToReclaim, beneficiary, metadata)` to simulate the full cash out including REVOwner's data hook effects (fee splitting, tax rate). Returns the expected reclaim amount and hook specifications. For a simpler estimate without data hook effects, use `JBTerminalStore.currentTotalReclaimableSurplusOf(revnetId, cashOutCount, decimals, currency)`.
148
-
149
- **Events:** No revnet-specific events. Cash-out events are emitted by `JBMultiTerminal` and `JBController`. REVOwner's `beforeCashOutRecordedWith` is a `view` function. The `afterCashOutRecordedWith` hook on REVOwner processes fees but does not emit events.
150
-
151
- **Edge cases:**
152
- - Suckers bypass both the cash-out fee AND the cash-out delay. The `REVOwner._isSuckerOf` check is the only gate.
153
- - `cashOutTaxRate == 0` means no tax and no revnet fee. The terminal's 2.5% protocol fee only applies up to the `feeFreeSurplusOf` amount (round-trip prevention), not the full reclaim.
154
- - Micro cash-outs (< 40 wei at 2.5%) round `feeCashOutCount` to 0, bypassing the fee. Gas cost far exceeds the bypassed fee.
155
- - The fee is paid to `FEE_REVNET_ID`, not `REV_ID`. These may be different projects.
156
- - Both the revnet fee and the terminal protocol fee apply. The revnet fee is computed first (at the data hook level, by splitting the cashout token count into fee and non-fee portions), then the terminal's 2.5% protocol fee is applied to all outbound fund amounts (both the beneficiary's reclaim and the hook-forwarded fee amount).
157
-
158
- ---
159
-
160
- ## 5. Borrow Against Revnet Tokens (REVLoans)
161
-
162
- **Entry point:** `REVLoans.borrowFrom(revnetId, source, minBorrowAmount, collateralCount, beneficiary, prepaidFeePercent)`
163
-
164
- **Prerequisites:**
165
- - Caller must hold `collateralCount` revnet ERC-20 tokens
166
- - Caller must grant `BURN_TOKENS` permission to the REVLoans contract for the revnet's project ID via `JBPermissions.setPermissionsFor()`. Without this, the transaction reverts in `JBController.burnTokensOf` with `JBPermissioned_Unauthorized`.
167
- - The revnet's cash-out delay must have passed (if one was set during cross-chain deployment). `borrowableAmountFrom` returns 0 and `borrowFrom` reverts with `REVLoans_CashOutDelayNotFinished` until the 30-day delay expires.
168
-
169
- **Key parameters:**
170
-
171
- | Parameter | Type | Description |
172
- |-----------|------|-------------|
173
- | `revnetId` | `uint256` | The revnet to borrow from. |
174
- | `source` | `REVLoanSource` | `{token, terminal}` -- which terminal and token to borrow. |
175
- | `minBorrowAmount` | `uint256` | Slippage protection -- revert if you'd get less. |
176
- | `collateralCount` | `uint256` | Number of revnet tokens to burn as collateral. |
177
- | `beneficiary` | `address payable` | Receives the borrowed funds and fee payment tokens. |
178
- | `prepaidFeePercent` | `uint256` | 25-500 (2.5%-50% of MAX_FEE=1000). Higher = longer interest-free window. |
179
-
180
- **What happens:**
181
-
182
- 1. **Validation:**
183
- - `collateralCount > 0` (no zero-collateral loans)
184
- - `source.terminal` is registered for the revnet in the directory
185
- - `prepaidFeePercent` in range [25, 500]
186
- - Cash-out delay has passed: resolves the `REVOwner` from the current ruleset's `dataHook`, checks `IREVOwner.cashOutDelayOf(revnetId)` (stored on REVOwner). Reverts with `REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp)` if `cashOutDelay > block.timestamp`.
187
- 2. **Loan ID generation:** `revnetId * 1_000_000_000_000 + (++totalLoansBorrowedFor[revnetId])`
188
- 3. **Loan creation in storage:**
189
- - `source`, `createdAt = block.timestamp`, `prepaidFeePercent`, `prepaidDuration = mulDiv(prepaidFeePercent, 3650 days, 500)`
190
- 4. **Borrow amount calculation:**
191
- - `totalSurplus` from all terminals (aggregated via `JBSurplus.currentSurplusOf`)
192
- - `totalBorrowed` from all loan sources (aggregated via `_totalBorrowedFrom`)
193
- - `borrowAmount = JBCashOuts.cashOutFrom(surplus + borrowed, collateral, supply + totalCollateral, cashOutTaxRate)`
194
- 5. **Validation:** `borrowAmount > 0`, `borrowAmount >= minBorrowAmount`
195
- 6. **Source fee:** `JBFees.feeAmountFrom(borrowAmount, prepaidFeePercent)`
196
- 7. **`_adjust` executes:**
197
- - Writes `loan.amount = borrowAmount` and `loan.collateral = collateralCount` to storage (CEI)
198
- - `_addTo`:
199
- - Registers the source if first time
200
- - Increments `totalBorrowedFrom`
201
- - Calls `terminal.useAllowanceOf` to pull funds (incurs 2.5% protocol fee automatically)
202
- - Pays REV fee (1%) to `REV_ID` via `feeTerminal.pay` (try-catch; zeroed on failure)
203
- - Transfers remaining: `netAmountPaidOut - revFeeAmount - sourceFeeAmount` to beneficiary
204
- - `_addCollateralTo`: increments `totalCollateralOf`, burns collateral via `CONTROLLER.burnTokensOf`
205
- - Pays source fee to revnet via `terminal.pay` (try-catch — on failure, returns fee amount to beneficiary)
206
- 8. **Mint loan ERC-721** to `_msgSender()`
207
-
208
- **Events:** `Borrow(loanId, revnetId, loan, source, borrowAmount, collateralCount, sourceFeeAmount, beneficiary, caller)`
209
-
210
- **Edge cases:**
211
- - Revnets always deploy an ERC-20 at creation, so collateral is always ERC-20 tokens (never credits).
212
- - The `minBorrowAmount` check is against the raw bonding curve output, BEFORE fees are deducted. The actual amount received is less.
213
- - `prepaidDuration` at minimum (25): `25 * 3650 days / 500 = 182.5 days`. At maximum (500): `500 * 3650 days / 500 = 3650 days`.
214
- - Both the REV fee payment and the source fee payment failures are non-fatal. If either `feeTerminal.pay` or `source.terminal.pay` reverts, the fee amount is transferred to the beneficiary instead.
215
- - Loan NFT is minted to `_msgSender()`, not `beneficiary`. The caller owns the loan; the beneficiary receives the funds.
216
- - When a revnet deploys to a new chain with `startsAtOrAfter` in the past, `REVDeployer` sets a 30-day cash-out delay via `REVOwner.setCashOutDelayOf()`. Both `borrowFrom` and `borrowableAmountFrom` enforce this delay by resolving the REVOwner from the current ruleset's `dataHook` and checking `IREVOwner.cashOutDelayOf(revnetId)` (stored on REVOwner). This prevents cross-chain arbitrage via loans during the delay window.
217
-
218
- ---
219
-
220
- ## 6. Repay a Loan
221
-
222
- **Entry point:** `REVLoans.repayLoan(loanId, maxRepayBorrowAmount, collateralCountToReturn, beneficiary, allowance)`
223
-
224
- **Key parameters:**
225
-
226
- | Parameter | Type | Description |
227
- |-----------|------|-------------|
228
- | `loanId` | `uint256` | The loan to repay (from the ERC-721). |
229
- | `maxRepayBorrowAmount` | `uint256` | Maximum amount willing to pay. Use `type(uint256).max` for "whatever it costs." |
230
- | `collateralCountToReturn` | `uint256` | How many collateral tokens to get back (up to `loan.collateral`). |
231
- | `beneficiary` | `address payable` | Receives the re-minted collateral tokens and fee payment tokens. |
232
- | `allowance` | `JBSingleAllowance` | Optional permit2 data. Set `amount = 0` to skip. |
233
-
234
- **What happens:**
235
-
236
- 1. **Authorization:** `_ownerOf(loanId) == _msgSender()` (only loan NFT owner can repay)
237
- 2. **Collateral check:** `collateralCountToReturn <= loan.collateral`
238
- 3. **Calculate new borrow amount** for remaining collateral via bonding curve:
239
- - `newBorrowAmount = _borrowAmountFrom(loan, revnetId, loan.collateral - collateralCountToReturn)`
240
- - Verify `newBorrowAmount <= loan.amount` (collateral value hasn't increased enough to over-collateralize)
241
- - `repayBorrowAmount = loan.amount - newBorrowAmount`
242
- 4. **Nothing-to-do check:** Reverts if `repayBorrowAmount == 0 && collateralCountToReturn == 0`
243
- 5. **Source fee calculation:**
244
- - If within prepaid window (`timeSinceCreated <= prepaidDuration`): fee = 0
245
- - If expired (`timeSinceCreated > LOAN_LIQUIDATION_DURATION`): revert `REVLoans_LoanExpired`
246
- - Otherwise: linear fee based on time elapsed beyond prepaid window
247
- - `repayBorrowAmount += sourceFeeAmount` (fee added to repayment)
248
- 6. **Accept funds:** `_acceptFundsFor` handles native token (uses `msg.value`) or ERC-20 (with optional permit2)
249
- 7. **Max repay check:** `repayBorrowAmount <= maxRepayBorrowAmount`
250
- 8. **Execute repay** via `_repayLoan`:
251
- - **Full repay** (`collateralCountToReturn == loan.collateral`): burns original NFT, calls `_adjust` with amounts zeroed, deletes loan, returns same loan ID
252
- - **Partial repay:** burns original NFT, creates new loan with reduced amount/collateral, copies `createdAt`/`prepaidFeePercent`/`prepaidDuration` from original, mints new NFT
253
- 9. **Refund excess:** If `maxRepayBorrowAmount > repayBorrowAmount`, transfers the difference back to `_msgSender()`
254
-
255
- **Events:** `RepayLoan(loanId, revnetId, paidOffLoanId, loan, paidOffLoan, repayBorrowAmount, sourceFeeAmount, collateralCountToReturn, beneficiary, caller)`
256
-
257
- **Edge cases:**
258
- - The source fee on repay is time-proportional. At the boundary of the prepaid window, it jumps from 0 to a small amount.
259
- - Partial repay creates a new loan NFT with a new loan ID but preserves `createdAt` and `prepaidFeePercent`. The prepaid window clock doesn't reset.
260
- - If the collateral has increased in value since borrowing (surplus grew), `newBorrowAmount > loan.amount` triggers `REVLoans_NewBorrowAmountGreaterThanLoanAmount`. The borrower should use `reallocateCollateralFromLoan` instead.
261
- - For ERC-20 repayments, the contract tries standard `transferFrom` first. If allowance is insufficient, it falls through to permit2.
262
- - `msg.value` is used directly for native token repayments (overrides `maxRepayBorrowAmount`).
263
-
264
- ---
265
-
266
- ## 7. Get Liquidated
267
-
268
- **Entry point:** `REVLoans.liquidateExpiredLoansFrom(revnetId, startingLoanId, count)`
269
-
270
- **Permissionless.** Anyone can call this to clean up expired loans.
271
-
272
- **Key parameters:**
273
-
274
- | Parameter | Type | Description |
275
- |-----------|------|-------------|
276
- | `revnetId` | `uint256` | The revnet to liquidate loans from. |
277
- | `startingLoanId` | `uint256` | The LOAN NUMBER (not full loan ID) to start iterating from. |
278
- | `count` | `uint256` | How many loan numbers to iterate over. |
279
-
280
- **What happens (per loan in range):**
281
-
282
- 1. Construct full loan ID: `revnetId * 1_000_000_000_000 + (startingLoanId + i)`
283
- 2. Read loan from storage. If `createdAt == 0`, skip (already repaid or liquidated).
284
- 3. Check ownership. If `_ownerOf(loanId) == address(0)`, skip (already burned).
285
- 4. Check expiry: `block.timestamp > loan.createdAt + LOAN_LIQUIDATION_DURATION` (strictly greater than)
286
- 5. Burn the loan NFT via `_burn(loanId)`
287
- 6. Delete loan data: `delete _loanOf[loanId]`
288
- 7. Decrement `totalCollateralOf[revnetId]` by `loan.collateral`
289
- 8. Decrement `totalBorrowedFrom[revnetId][terminal][token]` by `loan.amount`
290
-
291
- **Events:** `Liquidate(loanId, revnetId, loan, caller)` for each liquidated loan
292
-
293
- **Edge cases:**
294
- - The collateral was burned when the loan was created. There is nothing to "seize" -- liquidation is purely bookkeeping cleanup.
295
- - The borrower retains whatever funds they borrowed. The burned collateral tokens are permanently lost.
296
- - `startingLoanId` is the loan NUMBER within the revnet, not the full loan ID. The function constructs full IDs internally.
297
- - Gaps in the loan ID sequence (from repaid or already-liquidated loans) are skipped via the `createdAt == 0` check.
298
- - Gas cost scales linearly with `count`. Choose parameters carefully to avoid iterating over many empty slots.
299
- - The `>` comparison means a loan is liquidatable starting at `createdAt + LOAN_LIQUIDATION_DURATION + 1` second.
300
-
301
- ---
302
-
303
- ## 8. Reallocate Collateral Between Loans
304
-
305
- **Entry point:** `REVLoans.reallocateCollateralFromLoan(loanId, collateralCountToTransfer, source, minBorrowAmount, collateralCountToAdd, beneficiary, prepaidFeePercent)`
306
-
307
- **Purpose:** If a loan's collateral has appreciated (surplus grew, or tax rate decreased), extract excess collateral and use it to open a new loan.
308
-
309
- **Key parameters:**
310
-
311
- | Parameter | Type | Description |
312
- |-----------|------|-------------|
313
- | `loanId` | `uint256` | The existing loan to take collateral from. |
314
- | `collateralCountToTransfer` | `uint256` | Tokens to move from existing loan to new loan. |
315
- | `source` | `REVLoanSource` | Must match the existing loan's source (same terminal + token). |
316
- | `minBorrowAmount` | `uint256` | Slippage protection for the new loan. |
317
- | `collateralCountToAdd` | `uint256` | Additional fresh tokens to add to the new loan (from caller's balance). |
318
- | `beneficiary` | `address payable` | Receives proceeds from the new loan. |
319
- | `prepaidFeePercent` | `uint256` | For the new loan (25-500). |
320
-
321
- **What happens:**
322
-
323
- 1. **Authorization:** `_ownerOf(loanId) == _msgSender()`
324
- 2. **Expiry check:** If the loan has expired (`block.timestamp - createdAt > LOAN_LIQUIDATION_DURATION`), reverts with `REVLoans_LoanExpired`. Expired loans can only be liquidated, not reallocated.
325
- 3. **Source match:** New loan source must match existing loan source (prevents cross-source value extraction)
326
- 4. **`_reallocateCollateralFromLoan`:**
327
- - Burns original loan NFT
328
- - Validates `collateralCountToTransfer <= loan.collateral`
329
- - Computes `newCollateralCount = loan.collateral - collateralCountToTransfer`
330
- - Computes `borrowAmount = _borrowAmountFrom(loan, revnetId, newCollateralCount)`
331
- - Validates `borrowAmount >= loan.amount` (remaining collateral must still cover the original loan amount)
332
- - Creates replacement loan with original values
333
- - Calls `_adjust` to reduce collateral (returns excess tokens to caller via `_returnCollateralFrom`)
334
- - Mints replacement loan NFT to caller
335
- - Deletes original loan data
336
- 5. **`borrowFrom`:** Opens a new loan with `collateralCountToTransfer + collateralCountToAdd` as collateral
337
- - The `collateralCountToTransfer` tokens were just re-minted to the caller in step 4
338
- - The `collateralCountToAdd` tokens come from the caller's existing balance
339
- - Both are burned as collateral for the new loan
340
-
341
- **Events:**
342
- - `ReallocateCollateral(loanId, revnetId, reallocatedLoanId, reallocatedLoan, removedCollateralCount, caller)`
343
- - `Borrow(newLoanId, revnetId, ...)` from the `borrowFrom` call
344
-
345
- **Edge cases:**
346
- - This function is NOT payable. Any ETH sent with the call is rejected at the EVM level.
347
- - The original loan's `createdAt` and `prepaidFeePercent` are preserved on the replacement loan. The new loan gets fresh values.
348
- - If `collateralCountToTransfer == loan.collateral`, the replacement loan has 0 collateral and must have `borrowAmount >= loan.amount` (which requires the bonding curve to return 0 for 0 collateral, which it does). But `borrowAmount = 0 < loan.amount` would revert. So you can't transfer ALL collateral -- some must remain.
349
- - Between `_reallocateCollateralFromLoan` and `borrowFrom`, collateral tokens are minted to the caller and then immediately burned. If the caller is a contract with a `receive` function that re-enters, verify this is safe.
350
-
351
- ---
352
-
353
- ## 9. Claim Auto-Issuance
354
-
355
- **Entry point:** `REVDeployer.autoIssueFor(revnetId, stageId, beneficiary)`
356
-
357
- **Permissionless.** Anyone can call this on behalf of any beneficiary.
358
-
359
- **Key parameters:**
360
-
361
- | Parameter | Type | Description |
362
- |-----------|------|-------------|
363
- | `revnetId` | `uint256` | The revnet to claim from. |
364
- | `stageId` | `uint256` | The stage ID (= `block.timestamp + stageIndex` from deployment). |
365
- | `beneficiary` | `address` | The address to receive the tokens. |
366
-
367
- **What happens:**
368
-
369
- 1. `CONTROLLER.getRulesetOf(revnetId, stageId)` retrieves the ruleset
370
- 2. Validates `ruleset.start <= block.timestamp` (stage has started)
371
- 3. Reads `count = amountToAutoIssue[revnetId][stageId][beneficiary]`
372
- 4. Validates `count > 0`
373
- 5. **Zeroes the amount BEFORE minting** (CEI pattern): `amountToAutoIssue[...] = 0`
374
- 6. `CONTROLLER.mintTokensOf(revnetId, count, beneficiary, "", useReservedPercent=false)`
375
-
376
- **Events:** `AutoIssue(revnetId, stageId, beneficiary, count, caller)`
377
-
378
- **Edge cases:**
379
- - `useReservedPercent = false` means the FULL `count` goes to the beneficiary. No split percent is applied.
380
- - Auto-issuance is one-time per (revnetId, stageId, beneficiary). Once claimed, `amountToAutoIssue` is zero and calling again reverts with `REVDeployer_NothingToAutoIssue`.
381
- - The `stageId` must exactly match the value stored during deployment. If the deployment timestamp assumption doesn't hold (see RISKS.md S-3), the stage ID may be wrong and the claim will fail.
382
- - Auto-issuances for other chains (where `chainId != block.chainid`) are not stored on this chain and cannot be claimed here.
383
-
384
- ---
385
-
386
- ## 10. Burn Held Tokens
387
-
388
- **Entry point:** `REVDeployer.burnHeldTokensOf(revnetId)`
389
-
390
- **Permissionless.** Anyone can call this.
391
-
392
- **Purpose:** When reserved token splits don't sum to 100%, the remainder goes to the project owner (REVDeployer). This function burns those tokens to prevent them from sitting idle.
393
-
394
- **What happens:**
395
-
396
- 1. Reads REVDeployer's token balance for the revnet
397
- 2. Reverts with `REVDeployer_NothingToBurn` if balance is 0
398
- 3. Burns all held tokens via `CONTROLLER.burnTokensOf`
399
-
400
- **Events:** `BurnHeldTokens(revnetId, count, caller)`
401
-
402
- ---
403
-
404
- ## 11. Change Split Operator
405
-
406
- **Entry point:** `REVDeployer.setSplitOperatorOf(revnetId, newSplitOperator)`
407
-
408
- **Authorization:** Only the current split operator can call this.
409
-
410
- **What happens:**
411
-
412
- 1. `_checkIfIsSplitOperatorOf(revnetId, _msgSender())` verifies caller is current operator
413
- 2. Revokes all permissions from old operator: `_setPermissionsFor(this, _msgSender(), revnetId, empty)`
414
- 3. Grants all split operator permissions to new operator: `_setSplitOperatorOf(revnetId, newSplitOperator)`
415
-
416
- **Default permissions (9):** SET_SPLIT_GROUPS, SET_BUYBACK_POOL, SET_BUYBACK_TWAP, SET_PROJECT_URI, ADD_PRICE_FEED, SUCKER_SAFETY, SET_BUYBACK_HOOK, SET_ROUTER_TERMINAL, SET_TOKEN_METADATA
417
-
418
- **Additional permissions (if not prevented by 721 hook deployment flags):** ADJUST_721_TIERS, SET_721_METADATA, MINT_721, SET_721_DISCOUNT_PERCENT
419
-
420
- **Events:** `ReplaceSplitOperator(revnetId, newSplitOperator, caller)`
421
-
422
- **Edge cases:**
423
- - The split operator is singular. There can only be one at a time.
424
- - Setting `newSplitOperator = address(0)` effectively abandons the operator role. Nobody can change splits after that.
425
- - The old operator's permissions are fully revoked (set to empty array), not just the split-specific ones.
426
-
427
- ---
428
-
429
- ## 12. Deploy Suckers for an Existing Revnet
430
-
431
- **Entry point:** `REVDeployer.deploySuckersFor(revnetId, suckerDeploymentConfiguration)`
432
-
433
- **Authorization:** Only the split operator can call this. The current stage must allow sucker deployment (bit 2 of `extraMetadata`).
434
-
435
- **What happens:**
436
-
437
- 1. `_checkIfIsSplitOperatorOf(revnetId, _msgSender())`
438
- 2. Reads current ruleset metadata
439
- 3. Checks `(metadata.metadata >> 2) & 1 == 1` (third bit = allow suckers)
440
- 4. Deploys suckers via `SUCKER_REGISTRY.deploySuckersFor` using stored config hash
441
-
442
- **Events:** `DeploySuckers(revnetId, encodedConfigurationHash, suckerDeploymentConfiguration, caller)`
443
-
444
- **Edge cases:**
445
- - The `extraMetadata` bit check means sucker deployment can be disabled for specific stages. If the current stage doesn't allow it, the transaction reverts.
446
- - The `encodedConfigurationHash` used for the salt comes from the stored hash, not a newly computed one. This ensures cross-chain consistency.
447
-
448
- ---
449
-
450
- ## 13. Stage Transitions (Automatic)
451
-
452
- **No entry point.** Stage transitions happen automatically via the Juicebox rulesets system.
453
-
454
- **How it works:**
455
-
456
- Each stage is a JBRuleset with:
457
- - `duration = issuanceCutFrequency` (e.g., 30 days)
458
- - `weight = initialIssuance`
459
- - `weightCutPercent = issuanceCutPercent`
460
- - `mustStartAtOrAfter = startsAtOrAfter`
461
-
462
- When a stage's duration expires, it either:
463
- 1. **Cycles:** If no later stage is ready to start, the current stage repeats with decayed weight (`weight *= (1 - weightCutPercent/1e9)`)
464
- 2. **Transitions:** If the next stage's `startsAtOrAfter` has been reached, the next stage activates with its own `initialIssuance`
465
-
466
- **Impact on active loans:**
467
-
468
- When a stage transition changes `cashOutTaxRate`:
469
- - `_borrowableAmountFrom` uses the CURRENT stage's `cashOutTaxRate`
470
- - A higher tax rate reduces borrowable amount per unit of collateral
471
- - A lower tax rate increases borrowable amount per unit of collateral
472
- - Existing loans are NOT automatically adjusted -- they retain their original `amount`
473
- - A loan can become under-collateralized if the new tax rate is higher (collateral's cash-out value drops below `loan.amount`)
474
- - The protocol has no mechanism to force repayment -- only the 10-year expiry applies
475
-
476
- **Impact on payments:**
477
- - New payments use the new stage's issuance rate
478
- - The buyback hook compares DEX price vs. new issuance rate for swap-vs-mint decisions
479
-
480
- **Impact on cash-outs:**
481
- - The bonding curve uses the new stage's `cashOutTaxRate`
482
- - If the new stage has a higher tax rate, cash-outs return less
483
- - If the new stage has a lower tax rate, cash-outs return more
484
-
485
- **Edge cases:**
486
- - If `issuanceCutFrequency = 0` (no duration), the stage never expires. It must be replaced by a later stage's `startsAtOrAfter`.
487
- - Weight decay across many cycles (20,000+) requires progressive cache updates via `updateRulesetWeightCache()`. Without caching, operations revert with `WeightCacheRequired`.
488
- - The issuance cut is applied per cycle. After N cycles: `weight = initialIssuance * (1 - issuanceCutPercent/1e9)^N`.
489
-
490
- ---
491
-
492
- ## Summary: Entry Points by Actor
493
-
494
- | Actor | Function | Authorization |
495
- |-------|----------|---------------|
496
- | Anyone | `REVDeployer.deployFor(0, ...)` | None (deploys new revnet) |
497
- | Project owner | `REVDeployer.deployFor(existingId, ...)` | Must own project NFT |
498
- | Anyone | `JBMultiTerminal.pay(...)` | None |
499
- | Token holder | `JBMultiTerminal.cashOutTokensOf(...)` | Must hold tokens |
500
- | Token holder | `REVLoans.borrowFrom(...)` | Must hold tokens + grant `BURN_TOKENS` permission to REVLoans |
501
- | Loan owner | `REVLoans.repayLoan(...)` | Must own loan NFT |
502
- | Loan owner | `REVLoans.reallocateCollateralFromLoan(...)` | Must own loan NFT |
503
- | Anyone | `REVLoans.liquidateExpiredLoansFrom(...)` | None (permissionless after 10 years) |
504
- | Anyone | `REVDeployer.autoIssueFor(...)` | None (permissionless after stage starts) |
505
- | Anyone | `REVDeployer.burnHeldTokensOf(...)` | None |
506
- | Split operator | `REVDeployer.setSplitOperatorOf(...)` | Must be current split operator |
507
- | Split operator | `REVDeployer.deploySuckersFor(...)` | Must be current split operator + stage allows it |
508
- | Contract owner | `REVLoans.setTokenUriResolver(...)` | Must be Ownable owner of REVLoans |
84
+ - Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) for the underlying project, terminal, and ruleset mechanics that Revnets package and constrain.
85
+ - Use [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md), [nana-buyback-hook-v6](../nana-buyback-hook-v6/USER_JOURNEYS.md), and [nana-suckers-v6](../nana-suckers-v6/USER_JOURNEYS.md) when a Revnet deployment enables those optional features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rev-net/core-v6",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,14 +19,14 @@
19
19
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
20
20
  },
21
21
  "dependencies": {
22
- "@bananapus/721-hook-v6": "^0.0.25",
22
+ "@bananapus/721-hook-v6": "^0.0.30",
23
23
  "@bananapus/buyback-hook-v6": "^0.0.24",
24
24
  "@bananapus/core-v6": "^0.0.30",
25
25
  "@bananapus/ownable-v6": "^0.0.16",
26
26
  "@bananapus/permission-ids-v6": "^0.0.15",
27
27
  "@bananapus/router-terminal-v6": "^0.0.24",
28
28
  "@bananapus/suckers-v6": "^0.0.20",
29
- "@croptop/core-v6": "^0.0.25",
29
+ "@croptop/core-v6": "^0.0.28",
30
30
  "@openzeppelin/contracts": "^5.6.1",
31
31
  "@uniswap/v4-core": "^1.0.2",
32
32
  "@uniswap/v4-periphery": "^1.0.3"