@rev-net/core-v6 0.0.18 → 0.0.20

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 (64) hide show
  1. package/ADMINISTRATION.md +14 -4
  2. package/ARCHITECTURE.md +13 -10
  3. package/AUDIT_INSTRUCTIONS.md +39 -18
  4. package/CHANGE_LOG.md +78 -1
  5. package/README.md +10 -5
  6. package/RISKS.md +12 -11
  7. package/SKILLS.md +27 -12
  8. package/USER_JOURNEYS.md +15 -14
  9. package/foundry.toml +1 -1
  10. package/package.json +1 -1
  11. package/script/Deploy.s.sol +37 -4
  12. package/src/REVDeployer.sol +23 -305
  13. package/src/REVLoans.sol +24 -29
  14. package/src/REVOwner.sol +429 -0
  15. package/src/interfaces/IREVDeployer.sol +4 -10
  16. package/src/interfaces/IREVOwner.sol +10 -0
  17. package/test/REV.integrations.t.sol +12 -1
  18. package/test/REVAutoIssuanceFuzz.t.sol +12 -1
  19. package/test/REVDeployerRegressions.t.sol +15 -2
  20. package/test/REVInvincibility.t.sol +27 -3
  21. package/test/REVLifecycle.t.sol +14 -1
  22. package/test/REVLoans.invariants.t.sol +14 -1
  23. package/test/REVLoansAttacks.t.sol +14 -1
  24. package/test/REVLoansFeeRecovery.t.sol +14 -1
  25. package/test/REVLoansFindings.t.sol +14 -1
  26. package/test/REVLoansRegressions.t.sol +14 -1
  27. package/test/REVLoansSourceFeeRecovery.t.sol +14 -1
  28. package/test/REVLoansSourced.t.sol +14 -1
  29. package/test/REVLoansUnSourced.t.sol +14 -1
  30. package/test/TestBurnHeldTokens.t.sol +14 -1
  31. package/test/TestCEIPattern.t.sol +15 -1
  32. package/test/TestCashOutCallerValidation.t.sol +17 -4
  33. package/test/TestConversionDocumentation.t.sol +14 -1
  34. package/test/TestCrossCurrencyReclaim.t.sol +14 -1
  35. package/test/TestCrossSourceReallocation.t.sol +15 -1
  36. package/test/TestERC2771MetaTx.t.sol +14 -1
  37. package/test/TestEmptyBuybackSpecs.t.sol +17 -3
  38. package/test/TestFlashLoanSurplus.t.sol +15 -1
  39. package/test/TestHookArrayOOB.t.sol +16 -2
  40. package/test/TestLiquidationBehavior.t.sol +15 -1
  41. package/test/TestLoanSourceRotation.t.sol +14 -1
  42. package/test/TestLoansCashOutDelay.t.sol +19 -6
  43. package/test/TestLongTailEconomics.t.sol +14 -1
  44. package/test/TestLowFindings.t.sol +14 -1
  45. package/test/TestMixedFixes.t.sol +15 -1
  46. package/test/TestPermit2Signatures.t.sol +14 -1
  47. package/test/TestReallocationSandwich.t.sol +15 -1
  48. package/test/TestRevnetRegressions.t.sol +14 -1
  49. package/test/TestSplitWeightAdjustment.t.sol +41 -19
  50. package/test/TestSplitWeightE2E.t.sol +23 -2
  51. package/test/TestSplitWeightFork.t.sol +14 -1
  52. package/test/TestStageTransitionBorrowable.t.sol +15 -1
  53. package/test/TestSwapTerminalPermission.t.sol +15 -1
  54. package/test/TestUint112Overflow.t.sol +14 -1
  55. package/test/TestZeroRepayment.t.sol +15 -1
  56. package/test/audit/LoanIdOverflowGuard.t.sol +14 -1
  57. package/test/fork/ForkTestBase.sol +14 -1
  58. package/test/fork/TestPermit2PaymentFork.t.sol +4 -3
  59. package/test/regression/TestBurnPermissionRequired.t.sol +15 -1
  60. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +13 -1
  61. package/test/regression/TestCrossRevnetLiquidation.t.sol +15 -1
  62. package/test/regression/TestCumulativeLoanCounter.t.sol +15 -1
  63. package/test/regression/TestLiquidateGapHandling.t.sol +15 -1
  64. package/test/regression/TestZeroPriceFeed.t.sol +14 -1
package/SKILLS.md CHANGED
@@ -8,7 +8,8 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
8
8
 
9
9
  | Contract | Role |
10
10
  |----------|------|
11
- | `REVDeployer` | Deploys revnets, permanently owns the project NFT, acts as data hook and cash-out hook. Manages stages, splits, auto-issuance, buyback hooks, 721 hooks, suckers, and split operators. (~1,287 lines) |
11
+ | `REVDeployer` | Deploys revnets, permanently owns the project NFT. Manages stages, splits, auto-issuance, buyback hooks, suckers, split operators, and configuration state storage. Exposes `OWNER()` view returning the REVOwner address. Calls DEPLOYER-restricted setters on REVOwner during deployment to store `cashOutDelayOf` and `tiered721HookOf`. |
12
+ | `REVOwner` | Runtime hook contract for all revnets. Implements `IJBRulesetDataHook` + `IJBCashOutHook`. Set as the `dataHook` in each revnet's ruleset metadata. Handles pay hooks, cash-out hooks, mint permissions, and sucker verification. Stores `cashOutDelayOf` and `tiered721HookOf` mappings (set by REVDeployer via DEPLOYER-restricted setters `setCashOutDelayOf()` and `setTiered721HookOf()`). (~310 lines) |
12
13
  | `REVLoans` | Issues token-collateralized loans from revnet treasuries. Each loan is an ERC-721 NFT. Burns collateral on borrow, re-mints on repay. Charges tiered fees (REV protocol fee + source fee + prepaid fee). (~1,359 lines) |
13
14
 
14
15
  ## Key Functions
@@ -21,14 +22,15 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
21
22
  | `REVDeployer.deployFor(revnetId, config, terminals, suckerConfig, hookConfig, allowedPosts)` | Permissionless | Same as `deployFor` but deploys a tiered ERC-721 hook with pre-configured tiers. Optionally configures Croptop posting criteria and grants publisher permission to add tiers. |
22
23
  | `REVDeployer.deploySuckersFor(revnetId, suckerConfig)` | Split operator | Deploy new cross-chain suckers post-launch. Validates ruleset allows sucker deployment (bit 2 of `extraMetadata`). Uses stored config hash for cross-chain matching. |
23
24
 
24
- ### Data Hooks
25
+ ### Data Hooks (REVOwner)
25
26
 
26
27
  | Function | Permissions | What it does |
27
28
  |----------|------------|-------------|
28
- | `REVDeployer.beforePayRecordedWith(context)` | Terminal callback | Calls the 721 hook first for split specs, then calls the buyback hook with a reduced amount context (payment minus split amount). Adjusts the returned weight proportionally for splits (`weight = mulDiv(weight, amount - splitAmount, amount)`) so the terminal only mints tokens for the amount entering the project. Assembles pay hook specs (721 hook specs first, then buyback spec). |
29
- | `REVDeployer.beforeCashOutRecordedWith(context)` | Terminal callback | If sucker: returns full amount with 0 tax (fee exempt). Otherwise: calculates 2.5% fee, enforces 30-day cash-out delay, returns modified count + fee hook spec. |
30
- | `REVDeployer.afterCashOutRecordedWith(context)` | Permissionless | Cash-out hook callback. Receives fee amount and pays it to the fee revnet's terminal. Falls back to returning funds if fee payment fails. |
31
- | `REVDeployer.hasMintPermissionFor(revnetId, ruleset, addr)` | View | Returns `true` for: loans contract, buyback hook, buyback hook delegates, or suckers. |
29
+ | `REVOwner.beforePayRecordedWith(context)` | Terminal callback | Calls the 721 hook first for split specs, then calls the buyback hook with a reduced amount context (payment minus split amount). Adjusts the returned weight proportionally for splits (`weight = mulDiv(weight, amount - splitAmount, amount)`) so the terminal only mints tokens for the amount entering the project. Assembles pay hook specs (721 hook specs first, then buyback spec). Reads `tiered721HookOf` from REVOwner storage. |
30
+ | `REVOwner.beforeCashOutRecordedWith(context)` | Terminal callback | If sucker: returns full amount with 0 tax (fee exempt). Otherwise: calculates 2.5% fee, enforces 30-day cash-out delay (reads `cashOutDelayOf` from REVOwner storage), returns modified count + fee hook spec. |
31
+ | `REVOwner.afterCashOutRecordedWith(context)` | Permissionless | Cash-out hook callback. Receives fee amount and pays it to the fee revnet's terminal. Falls back to returning funds if fee payment fails. |
32
+ | `REVOwner.hasMintPermissionFor(revnetId, ruleset, addr)` | View | Returns `true` for: loans contract, buyback hook, buyback hook delegates, or suckers. |
33
+ | `REVOwner.cashOutDelayOf(revnetId)` | View | Returns the cash-out delay timestamp from REVOwner storage. Exposed for REVLoans compatibility (REVLoans imports IREVOwner for this). |
32
34
 
33
35
  ### Split Operator
34
36
 
@@ -188,11 +190,17 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
188
190
  | Mapping | Visibility | Type | Purpose |
189
191
  |---------|-----------|------|---------|
190
192
  | `amountToAutoIssue` | `public` | `revnetId => stageId => beneficiary => uint256` | Premint tokens per stage per beneficiary |
191
- | `cashOutDelayOf` | `public` | `revnetId => uint256` | Timestamp when cash outs unlock (0 = no delay) |
192
193
  | `hashedEncodedConfigurationOf` | `public` | `revnetId => bytes32` | Config hash for cross-chain sucker validation |
193
- | `tiered721HookOf` | `public` | `revnetId => address` | Deployed 721 hook address (if any) |
194
194
  | `_extraOperatorPermissions` | `internal` | `revnetId => uint256[]` | Custom permissions for split operator (no auto-getter) |
195
195
 
196
+ ### REVOwner
197
+
198
+ | Mapping | Visibility | Type | Purpose |
199
+ |---------|-----------|------|---------|
200
+ | `DEPLOYER` | `public` | `address` | REVDeployer address (storage variable, set once via `setDeployer()` called from REVDeployer's constructor) |
201
+ | `cashOutDelayOf` | `public` | `revnetId => uint256` | Timestamp when cash outs unlock (0 = no delay). Set by REVDeployer via `setCashOutDelayOf()`. |
202
+ | `tiered721HookOf` | `public` | `revnetId => address` | Deployed 721 hook address (if any). Set by REVDeployer via `setTiered721HookOf()`. |
203
+
196
204
  ### REVLoans
197
205
 
198
206
  | Mapping | Visibility | Type | Purpose |
@@ -214,19 +222,20 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
214
222
  5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
215
223
  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.
216
224
  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. The 2.5% fee is deducted from the TOKEN AMOUNT being cashed out, not from the reclaim value. 2.5% of the tokens are redirected to the fee revnet, which then redeems them at the bonding curve independently. The net reclaim to the caller is based on 97.5% of the tokens, not 97.5% of the computed ETH value. This is by design.
217
- 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. Enforced in both `beforeCashOutRecordedWith` (direct cash outs) and `REVLoans.borrowFrom` / `borrowableAmountFrom` (loans). The delay is resolved via the ruleset's `dataHook` (the REVDeployer) and `cashOutDelayOf(revnetId)`.
225
+ 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. Enforced in both `beforeCashOutRecordedWith` (direct cash outs) and `REVLoans.borrowFrom` / `borrowableAmountFrom` (loans). The delay is stored on REVOwner (`cashOutDelayOf(revnetId)`) and set by REVDeployer during deployment via `setCashOutDelayOf()`. REVLoans imports IREVOwner (not IREVDeployer) to read it.
218
226
  9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
219
227
  10. **Split operator is singular.** Only ONE address can be split operator at a time. The operator can replace itself via `setSplitOperatorOf` but cannot delegate or multi-sig.
220
228
  11. **NATIVE_TOKEN on non-ETH chains.** `JBConstants.NATIVE_TOKEN` on Celo means CELO, on Polygon means MATIC -- not ETH. Use ERC-20 WETH instead. The config matching hash does NOT catch terminal configuration differences.
221
229
  12. **Loan source array is unbounded.** `_loanSourcesOf[revnetId]` grows without limit. No validation that a terminal is actually registered for the project.
222
230
  13. **Flash-loan surplus exposure.** `borrowableAmountFrom` reads live surplus. A flash loan can temporarily inflate the treasury to borrow more than the sustained value supports.
223
231
  14. **Fee revnet must have terminals.** Cash-out fees and loan protocol fees are paid to `FEE_REVNET_ID`. If that project has no terminal for the token, the fee silently fails (try-catch).
224
- 15. **Buyback hook is immutable per deployer.** `REVDeployer.BUYBACK_HOOK` is set at construction time. All revnets deployed by the same deployer share the same buyback hook.
232
+ 15. **Buyback hook is immutable per deployer.** `BUYBACK_HOOK` is set at construction time on both REVDeployer and REVOwner. All revnets deployed by the same deployer share the same buyback hook.
225
233
  16. **Cross-chain config matching.** `hashedEncodedConfigurationOf` covers economic parameters (baseCurrency, stages, auto-issuances) but NOT terminal configurations, accounting contexts, or sucker token mappings. Two deployments with identical hashes can have different terminal setups.
226
234
  17. **Loan fee model has three layers.** See Constants table for exact values: REV protocol fee, terminal fee, and prepaid source fee (borrower-chosen, buys interest-free window). After the prepaid window, source fee accrues linearly over the remaining loan duration.
227
235
  18. **Permit2 fallback.** `REVLoans` uses permit2 for ERC-20 transfers as a fallback when standard allowance is insufficient. Wrapped in try-catch.
228
236
  19. **39.16% cash-out tax crossover.** Below ~39% cash-out tax, cashing out is more capital-efficient than borrowing. Above ~39%, loans become more efficient because they preserve upside while providing liquidity. Based on CryptoEconLab academic research. Design implication: revnets intended for active token trading should consider this threshold when setting `cashOutTaxRate`.
229
237
  20. **REVDeployer always deploys a 721 hook** via `HOOK_DEPLOYER.deployHookFor` — even if `baseline721HookConfiguration` has empty tiers. This is correct by design: it lets the split operator add and sell NFTs later without migration. Non-revnet projects should follow the same pattern by using `JB721TiersHookProjectDeployer.launchProjectFor` (or `JBOmnichainDeployer.launchProjectFor`) instead of bare `launchProjectFor`.
238
+ 21. **REVOwner circular dependency.** REVOwner and REVDeployer have a circular dependency broken by `setDeployer()`, called atomically from REVDeployer's constructor. `REVOwner.DEPLOYER` is a storage variable (not immutable). `setDeployer()` sets `msg.sender` as `DEPLOYER` if not already set -- reverts if already set. If `setDeployer()` is never called, all runtime hook behavior breaks. Deploy order: REVOwner first, then REVDeployer(owner=REVOwner) -- the constructor calls `REVOwner.setDeployer()` atomically. REVOwner stores `cashOutDelayOf` and `tiered721HookOf` mappings, which are set by REVDeployer via DEPLOYER-restricted setters (`setCashOutDelayOf()`, `setTiered721HookOf()`).
230
239
 
231
240
  ### NATIVE_TOKEN Accounting on Non-ETH Chains
232
241
 
@@ -301,8 +310,14 @@ Quick-reference for common read operations. All functions are `view`/`pure` and
301
310
  | What | Call | Returns |
302
311
  |------|------|---------|
303
312
  | Config hash (cross-chain matching) | `REVDeployer.hashedEncodedConfigurationOf(revnetId)` | `bytes32` |
304
- | 721 hook address | `REVDeployer.tiered721HookOf(revnetId)` | `IJB721TiersHook` |
305
- | Cash-out delay timestamp | `REVDeployer.cashOutDelayOf(revnetId)` | `uint256` (0 = no delay) |
313
+ | REVOwner address | `REVDeployer.OWNER()` | `address` |
314
+
315
+ ### REVOwner State
316
+
317
+ | What | Call | Returns |
318
+ |------|------|---------|
319
+ | 721 hook address | `REVOwner.tiered721HookOf(revnetId)` | `IJB721TiersHook` |
320
+ | Cash-out delay timestamp | `REVOwner.cashOutDelayOf(revnetId)` | `uint256` (0 = no delay) |
306
321
 
307
322
  ## Example Integration
308
323
 
package/USER_JOURNEYS.md CHANGED
@@ -29,7 +29,7 @@ Or: `REVDeployer.deployFor(revnetId=0, configuration, terminalConfigurations, su
29
29
  1. `revnetId = PROJECTS.count() + 1` (next available ID)
30
30
  2. `_makeRulesetConfigurations` converts stages to JBRulesetConfigs:
31
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 = REVDeployer address
32
+ - Each stage becomes a ruleset with: duration = `issuanceCutFrequency`, weight = `initialIssuance`, weightCutPercent = `issuanceCutPercent`, data hook = REVOwner address
33
33
  - Fund access limits: unlimited surplus allowance per terminal/token (for loans)
34
34
  - Encoded configuration hash computed from economic parameters
35
35
  - Auto-issuance amounts stored: `amountToAutoIssue[revnetId][block.timestamp + i][beneficiary] += count`
@@ -91,12 +91,13 @@ Or: `REVDeployer.deployFor(revnetId=0, configuration, terminalConfigurations, su
91
91
 
92
92
  **Entry point:** `JBMultiTerminal.pay(projectId, token, amount, beneficiary, minReturnedTokens, memo, metadata)`
93
93
 
94
- This is a standard Juicebox payment, but REVDeployer intervenes as the data hook.
94
+ This is a standard Juicebox payment, but REVOwner intervenes as the data hook.
95
95
 
96
96
  **What happens:**
97
97
 
98
98
  1. Terminal records payment in store
99
- 2. Store calls `REVDeployer.beforePayRecordedWith(context)`:
99
+ 2. Store calls `REVOwner.beforePayRecordedWith(context)`:
100
+ - Reads `tiered721HookOf` from REVOwner storage
100
101
  - Calls 721 hook's `beforePayRecordedWith` for split specs (tier purchases)
101
102
  - Computes `projectAmount = context.amount.value - totalSplitAmount`
102
103
  - Calls buyback hook's `beforePayRecordedWith` with reduced amount context
@@ -108,14 +109,14 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
108
109
  - 721 hook processes tier purchases
109
110
  - Buyback hook processes swap (if applicable)
110
111
 
111
- **Preview**: Call `JBMultiTerminal.previewPayFor(revnetId, token, amount, beneficiary, metadata)` to simulate the full payment including REVDeployer'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.
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.
112
113
 
113
- **Events:** No revnet-specific events. The payment is handled by `JBMultiTerminal` and `JBController` (see nana-core-v6). REVDeployer's `beforePayRecordedWith` is a `view` function and emits nothing.
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.
114
115
 
115
116
  **Edge cases:**
116
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.
117
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.
118
- - If no 721 hook is set (`tiered721HookOf[revnetId] == address(0)`), only the buyback hook is consulted.
119
+ - If no 721 hook is set (`tiered721HookOf[revnetId] == address(0)` on REVOwner), only the buyback hook is consulted.
119
120
 
120
121
  ---
121
122
 
@@ -126,9 +127,9 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
126
127
  **What happens:**
127
128
 
128
129
  1. Terminal records cash-out in store
129
- 2. Store calls `REVDeployer.beforeCashOutRecordedWith(context)`:
130
+ 2. Store calls `REVOwner.beforeCashOutRecordedWith(context)`:
130
131
  - **If sucker:** Returns 0% tax, full cash-out count, no hooks (fee exempt)
131
- - **If cash-out delay active:** Reverts with `REVDeployer_CashOutDelayNotFinished`
132
+ - **If cash-out delay active:** Reads `cashOutDelayOf` from REVOwner storage, reverts with `REVDeployer_CashOutDelayNotFinished`
132
133
  - **If no tax or no fee terminal:** Returns parameters unchanged
133
134
  - **Otherwise:** Splits cash-out into fee portion (2.5%) and non-fee portion:
134
135
  - `feeCashOutCount = mulDiv(cashOutCount, 25, 1000)`
@@ -138,17 +139,17 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
138
139
  - Returns `nonFeeCashOutCount` as the adjusted cash-out count + hook spec for fee
139
140
  3. Terminal burns ALL of the user's specified token count
140
141
  4. Terminal transfers the reclaimed amount to the beneficiary
141
- 5. Terminal calls `REVDeployer.afterCashOutRecordedWith(context)`:
142
+ 5. Terminal calls `REVOwner.afterCashOutRecordedWith(context)`:
142
143
  - Transfers fee amount from terminal to this contract
143
144
  - Pays fee to fee revnet's terminal via `feeTerminal.pay`
144
145
  - On failure: returns funds to the originating project via `addToBalanceOf`
145
146
 
146
- **Preview**: Call `JBMultiTerminal.previewCashOutFrom(holder, revnetId, cashOutCount, tokenToReclaim, beneficiary, metadata)` to simulate the full cash out including REVDeployer'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)`.
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)`.
147
148
 
148
- **Events:** No revnet-specific events. Cash-out events are emitted by `JBMultiTerminal` and `JBController`. REVDeployer's `beforeCashOutRecordedWith` is a `view` function. The `afterCashOutRecordedWith` hook processes fees but does not emit events.
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.
149
150
 
150
151
  **Edge cases:**
151
- - Suckers bypass both the cash-out fee AND the cash-out delay. The `_isSuckerOf` check is the only gate.
152
+ - Suckers bypass both the cash-out fee AND the cash-out delay. The `REVOwner._isSuckerOf` check is the only gate.
152
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.
153
154
  - Micro cash-outs (< 40 wei at 2.5%) round `feeCashOutCount` to 0, bypassing the fee. Gas cost far exceeds the bypassed fee.
154
155
  - The fee is paid to `FEE_REVNET_ID`, not `REV_ID`. These may be different projects.
@@ -182,7 +183,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
182
183
  - `collateralCount > 0` (no zero-collateral loans)
183
184
  - `source.terminal` is registered for the revnet in the directory
184
185
  - `prepaidFeePercent` in range [25, 500]
185
- - Cash-out delay has passed: resolves the `REVDeployer` from the current ruleset's `dataHook`, checks `cashOutDelayOf(revnetId)`. Reverts with `REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp)` if `cashOutDelay > block.timestamp`.
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`.
186
187
  2. **Loan ID generation:** `revnetId * 1_000_000_000_000 + (++totalLoansBorrowedFor[revnetId])`
187
188
  3. **Loan creation in storage:**
188
189
  - `source`, `createdAt = block.timestamp`, `prepaidFeePercent`, `prepaidDuration = mulDiv(prepaidFeePercent, 3650 days, 500)`
@@ -212,7 +213,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
212
213
  - `prepaidDuration` at minimum (25): `25 * 3650 days / 500 = 182.5 days`. At maximum (500): `500 * 3650 days / 500 = 3650 days`.
213
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.
214
215
  - Loan NFT is minted to `_msgSender()`, not `beneficiary`. The caller owns the loan; the beneficiary receives the funds.
215
- - When a revnet deploys to a new chain with `startsAtOrAfter` in the past, `REVDeployer` sets a 30-day cash-out delay. Both `borrowFrom` and `borrowableAmountFrom` enforce this delay by resolving the deployer from the current ruleset's `dataHook` and checking `cashOutDelayOf`. This prevents cross-chain arbitrage via loans during the delay window.
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.
216
217
 
217
218
  ---
218
219
 
package/foundry.toml CHANGED
@@ -2,7 +2,7 @@
2
2
  solc = '0.8.28'
3
3
  evm_version = 'cancun'
4
4
  via_ir = true
5
- optimizer_runs = 100
5
+ optimizer_runs = 200
6
6
  libs = ["node_modules", "lib"]
7
7
  fs_permissions = [{ access = "read-write", path = "./"}]
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rev-net/core-v6",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,6 +30,8 @@ import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/
30
30
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
31
31
 
32
32
  import {REVDeployer} from "./../src/REVDeployer.sol";
33
+ import {REVOwner} from "./../src/REVOwner.sol";
34
+ import {IREVDeployer} from "./../src/interfaces/IREVDeployer.sol";
33
35
  import {REVAutoIssuance} from "../src/structs/REVAutoIssuance.sol";
34
36
  import {REVConfig} from "../src/structs/REVConfig.sol";
35
37
  import {REVDescription} from "../src/structs/REVDescription.sol";
@@ -91,6 +93,8 @@ contract DeployScript is Script, Sphinx {
91
93
  // forge-lint: disable-next-line(mixed-case-variable)
92
94
  bytes32 REVLOANS_SALT = "_REV_LOANS_SALT_V6_";
93
95
  // forge-lint: disable-next-line(mixed-case-variable)
96
+ bytes32 REVOWNER_SALT = "_REV_OWNER_SALT_V6_";
97
+ // forge-lint: disable-next-line(mixed-case-variable)
94
98
  address LOANS_OWNER;
95
99
  // forge-lint: disable-next-line(mixed-case-variable)
96
100
  address OPERATOR;
@@ -368,6 +372,8 @@ contract DeployScript is Script, Sphinx {
368
372
  bool _singletonsExist;
369
373
  // The address of the previously deployed REVLoans, if found.
370
374
  address _existingRevloansAddr;
375
+ // The address of the previously deployed REVOwner, if found.
376
+ address _existingOwnerAddr;
371
377
  // The address of the previously deployed REVDeployer, if found.
372
378
  address _existingDeployerAddr;
373
379
 
@@ -391,6 +397,19 @@ contract DeployScript is Script, Sphinx {
391
397
  // Flag that singletons were found.
392
398
  _singletonsExist = true;
393
399
 
400
+ // Also predict and verify the owner.
401
+ (_existingOwnerAddr,) = _isDeployed({
402
+ salt: REVOWNER_SALT,
403
+ creationCode: type(REVOwner).creationCode,
404
+ arguments: abi.encode(
405
+ IJBBuybackHookRegistry(address(buybackHook.registry)),
406
+ core.controller.DIRECTORY(),
407
+ _candidateId,
408
+ suckers.registry,
409
+ _candidateRevloansAddr
410
+ )
411
+ });
412
+
394
413
  // Also predict and verify the deployer.
395
414
  (_existingDeployerAddr,) = _isDeployed({
396
415
  salt: DEPLOYER_SALT,
@@ -403,7 +422,8 @@ contract DeployScript is Script, Sphinx {
403
422
  croptop.publisher,
404
423
  IJBBuybackHookRegistry(address(buybackHook.registry)),
405
424
  _candidateRevloansAddr,
406
- TRUSTED_FORWARDER
425
+ TRUSTED_FORWARDER,
426
+ _existingOwnerAddr
407
427
  )
408
428
  });
409
429
  // Stop searching — we found the deployed singletons.
@@ -430,7 +450,18 @@ contract DeployScript is Script, Sphinx {
430
450
  trustedForwarder: TRUSTED_FORWARDER
431
451
  });
432
452
 
433
- // Deploy REVDeployer with the REVLoans and buyback hook addresses.
453
+ // Deploy REVOwner the runtime data hook that handles pay and cash out callbacks.
454
+ REVOwner revOwner = _singletonsExist
455
+ ? REVOwner(_existingOwnerAddr)
456
+ : new REVOwner{salt: REVOWNER_SALT}({
457
+ buybackHook: IJBBuybackHookRegistry(address(buybackHook.registry)),
458
+ directory: core.controller.DIRECTORY(),
459
+ feeRevnetId: FEE_PROJECT_ID,
460
+ suckerRegistry: suckers.registry,
461
+ loans: address(revloans)
462
+ });
463
+
464
+ // Deploy REVDeployer with the REVLoans, buyback hook, and REVOwner addresses.
434
465
  (address _deployerAddr, bool _deployerIsDeployed) = _isDeployed({
435
466
  salt: DEPLOYER_SALT,
436
467
  creationCode: type(REVDeployer).creationCode,
@@ -442,7 +473,8 @@ contract DeployScript is Script, Sphinx {
442
473
  croptop.publisher,
443
474
  IJBBuybackHookRegistry(address(buybackHook.registry)),
444
475
  address(revloans),
445
- TRUSTED_FORWARDER
476
+ TRUSTED_FORWARDER,
477
+ address(revOwner)
446
478
  )
447
479
  });
448
480
  REVDeployer _basicDeployer = _deployerIsDeployed
@@ -455,7 +487,8 @@ contract DeployScript is Script, Sphinx {
455
487
  publisher: croptop.publisher,
456
488
  buybackHook: IJBBuybackHookRegistry(address(buybackHook.registry)),
457
489
  loans: address(revloans),
458
- trustedForwarder: TRUSTED_FORWARDER
490
+ trustedForwarder: TRUSTED_FORWARDER,
491
+ owner: address(revOwner)
459
492
  });
460
493
 
461
494
  // Only configure the fee project if singletons were freshly deployed. Re-running `deployFor` on an