@rev-net/core-v6 0.0.17 → 0.0.19

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 (66) hide show
  1. package/ADMINISTRATION.md +14 -4
  2. package/ARCHITECTURE.md +14 -10
  3. package/AUDIT_INSTRUCTIONS.md +40 -17
  4. package/CHANGE_LOG.md +87 -0
  5. package/README.md +10 -5
  6. package/RISKS.md +15 -10
  7. package/SKILLS.md +31 -15
  8. package/USER_JOURNEYS.md +16 -12
  9. package/foundry.toml +1 -1
  10. package/package.json +8 -8
  11. package/script/Deploy.s.sol +60 -19
  12. package/src/REVDeployer.sol +21 -303
  13. package/src/REVLoans.sol +31 -0
  14. package/src/REVOwner.sol +430 -0
  15. package/src/interfaces/IREVDeployer.sol +4 -10
  16. package/src/interfaces/IREVOwner.sol +10 -0
  17. package/src/structs/REVBaseline721HookConfig.sol +0 -2
  18. package/test/REV.integrations.t.sol +14 -1
  19. package/test/REVAutoIssuanceFuzz.t.sol +14 -1
  20. package/test/REVDeployerRegressions.t.sol +17 -2
  21. package/test/REVInvincibility.t.sol +31 -3
  22. package/test/REVLifecycle.t.sol +16 -1
  23. package/test/REVLoans.invariants.t.sol +16 -1
  24. package/test/REVLoansAttacks.t.sol +16 -1
  25. package/test/REVLoansFeeRecovery.t.sol +16 -1
  26. package/test/REVLoansFindings.t.sol +16 -1
  27. package/test/REVLoansRegressions.t.sol +16 -1
  28. package/test/REVLoansSourceFeeRecovery.t.sol +16 -1
  29. package/test/REVLoansSourced.t.sol +16 -1
  30. package/test/REVLoansUnSourced.t.sol +16 -1
  31. package/test/TestBurnHeldTokens.t.sol +16 -1
  32. package/test/TestCEIPattern.t.sol +16 -1
  33. package/test/TestCashOutCallerValidation.t.sol +19 -4
  34. package/test/TestConversionDocumentation.t.sol +16 -1
  35. package/test/TestCrossCurrencyReclaim.t.sol +16 -1
  36. package/test/TestCrossSourceReallocation.t.sol +16 -1
  37. package/test/TestERC2771MetaTx.t.sol +16 -1
  38. package/test/TestEmptyBuybackSpecs.t.sol +18 -3
  39. package/test/TestFlashLoanSurplus.t.sol +16 -1
  40. package/test/TestHookArrayOOB.t.sol +17 -2
  41. package/test/TestLiquidationBehavior.t.sol +16 -1
  42. package/test/TestLoanSourceRotation.t.sol +16 -1
  43. package/test/TestLoansCashOutDelay.t.sol +482 -0
  44. package/test/TestLongTailEconomics.t.sol +16 -1
  45. package/test/TestLowFindings.t.sol +16 -1
  46. package/test/TestMixedFixes.t.sol +16 -1
  47. package/test/TestPermit2Signatures.t.sol +16 -1
  48. package/test/TestReallocationSandwich.t.sol +16 -1
  49. package/test/TestRevnetRegressions.t.sol +16 -1
  50. package/test/TestSplitWeightAdjustment.t.sol +43 -19
  51. package/test/TestSplitWeightE2E.t.sol +26 -3
  52. package/test/TestSplitWeightFork.t.sol +16 -2
  53. package/test/TestStageTransitionBorrowable.t.sol +16 -1
  54. package/test/TestSwapTerminalPermission.t.sol +16 -1
  55. package/test/TestUint112Overflow.t.sol +16 -1
  56. package/test/TestZeroRepayment.t.sol +16 -1
  57. package/test/audit/LoanIdOverflowGuard.t.sol +16 -1
  58. package/test/fork/ForkTestBase.sol +16 -2
  59. package/test/fork/TestPermit2PaymentFork.t.sol +4 -3
  60. package/test/helpers/REVEmpty721Config.sol +0 -1
  61. package/test/regression/TestBurnPermissionRequired.t.sol +16 -1
  62. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +15 -1
  63. package/test/regression/TestCrossRevnetLiquidation.t.sol +16 -1
  64. package/test/regression/TestCumulativeLoanCounter.t.sol +16 -1
  65. package/test/regression/TestLiquidateGapHandling.t.sol +16 -1
  66. package/test/regression/TestZeroPriceFeed.t.sol +16 -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
 
@@ -47,7 +49,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
47
49
 
48
50
  | Function | Permissions | What it does |
49
51
  |----------|------------|-------------|
50
- | `REVLoans.borrowFrom(revnetId, source, minBorrowAmount, collateralCount, beneficiary, prepaidFeePercent)` | Permissionless (caller must grant BURN_TOKENS to REVLoans) | Open a loan: burn collateral tokens, pull funds from revnet via `useAllowanceOf`, pay REV fee (1%) + terminal fee (2.5%), transfer remainder to beneficiary, mint loan NFT. |
52
+ | `REVLoans.borrowFrom(revnetId, source, minBorrowAmount, collateralCount, beneficiary, prepaidFeePercent)` | Permissionless (caller must grant BURN_TOKENS to REVLoans) | Open a loan: enforce cash-out delay if set (cross-chain deployment protection), burn collateral tokens, pull funds from revnet via `useAllowanceOf`, pay REV fee (1%) + terminal fee (2.5%), transfer remainder to beneficiary, mint loan NFT. |
51
53
  | `REVLoans.repayLoan(loanId, maxRepayBorrowAmount, collateralCountToReturn, beneficiary, allowance)` | Loan NFT owner | Repay fully or partially. Returns funds to revnet via `addToBalanceOf`, re-mints collateral tokens, burns/replaces the loan NFT. Supports permit2 signatures. |
52
54
  | `REVLoans.reallocateCollateralFromLoan(loanId, collateralCountToTransfer, source, minBorrowAmount, collateralCountToAdd, beneficiary, prepaidFeePercent)` | Loan NFT owner | Refinance: remove excess collateral from an existing loan and open a new loan with the freed collateral. Burns original, mints two replacements. |
53
55
  | `REVLoans.liquidateExpiredLoansFrom(revnetId, startingLoanId, count)` | Permissionless | Clean up loans past the 10-year liquidation duration. Burns NFTs and decrements accounting totals. Collateral is permanently lost. |
@@ -57,7 +59,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
57
59
 
58
60
  | Function | What it does |
59
61
  |----------|-------------|
60
- | `REVLoans.borrowableAmountFrom(revnetId, collateralCount, decimals, currency)` | Calculate how much can be borrowed for a given collateral amount. Aggregates surplus from all terminals, applies bonding curve. |
62
+ | `REVLoans.borrowableAmountFrom(revnetId, collateralCount, decimals, currency)` | Calculate how much can be borrowed for a given collateral amount. Returns 0 during the cash-out delay period. Aggregates surplus from all terminals, applies bonding curve. |
61
63
  | `REVLoans.determineSourceFeeAmount(loan, amount)` | Calculate the time-proportional source fee for a loan repayment. Zero during prepaid window, linear accrual after. |
62
64
  | `REVLoans.loanOf(loanId)` | Returns the full `REVLoan` struct for a loan. |
63
65
  | `REVLoans.loanSourcesOf(revnetId)` | Returns all `(terminal, token)` pairs used for loans by a revnet. |
@@ -139,6 +141,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
139
141
 
140
142
  | Error | When It Fires |
141
143
  |-------|---------------|
144
+ | `REVLoans_CashOutDelayNotFinished(cashOutDelay, blockTimestamp)` | When borrowing during the 30-day cash-out delay period (cross-chain deployment protection). |
142
145
  | `REVLoans_CollateralExceedsLoan(collateralToReturn, loanCollateral)` | When trying to return more collateral than the loan holds. |
143
146
  | `REVLoans_InvalidPrepaidFeePercent(prepaidFeePercent, min, max)` | When `prepaidFeePercent` is outside the allowed range (2.5%--50%). |
144
147
  | `REVLoans_InvalidTerminal(terminal, revnetId)` | When the specified terminal is not registered for the revnet. |
@@ -187,11 +190,17 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
187
190
  | Mapping | Visibility | Type | Purpose |
188
191
  |---------|-----------|------|---------|
189
192
  | `amountToAutoIssue` | `public` | `revnetId => stageId => beneficiary => uint256` | Premint tokens per stage per beneficiary |
190
- | `cashOutDelayOf` | `public` | `revnetId => uint256` | Timestamp when cash outs unlock (0 = no delay) |
191
193
  | `hashedEncodedConfigurationOf` | `public` | `revnetId => bytes32` | Config hash for cross-chain sucker validation |
192
- | `tiered721HookOf` | `public` | `revnetId => address` | Deployed 721 hook address (if any) |
193
194
  | `_extraOperatorPermissions` | `internal` | `revnetId => uint256[]` | Custom permissions for split operator (no auto-getter) |
194
195
 
196
+ ### REVOwner
197
+
198
+ | Mapping | Visibility | Type | Purpose |
199
+ |---------|-----------|------|---------|
200
+ | `DEPLOYER` | `public` | `address` | REVDeployer address (storage variable, set once via `initialize()`) |
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
+
195
204
  ### REVLoans
196
205
 
197
206
  | Mapping | Visibility | Type | Purpose |
@@ -212,20 +221,21 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
212
221
  4. **Loan ID encoding.** `loanId = revnetId * 1_000_000_000_000 + loanNumber`. Each revnet supports ~1 trillion loans. Use `revnetIdOfLoanWith(loanId)` to decode.
213
222
  5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
214
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.
215
- 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.
216
- 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.
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.
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.
217
226
  9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
218
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.
219
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.
220
229
  12. **Loan source array is unbounded.** `_loanSourcesOf[revnetId]` grows without limit. No validation that a terminal is actually registered for the project.
221
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.
222
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).
223
- 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.
224
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.
225
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.
226
235
  18. **Permit2 fallback.** `REVLoans` uses permit2 for ERC-20 transfers as a fallback when standard allowance is insufficient. Wrapped in try-catch.
227
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`.
228
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 a one-shot `initialize()` call. `REVOwner.DEPLOYER` is a storage variable (not immutable) set via `initialize()`. If `initialize()` is never called or called with the wrong address, all runtime hook behavior breaks. Deploy order: REVOwner first, then REVDeployer(owner=REVOwner), then REVOwner.initialize(deployer). REVOwner stores `cashOutDelayOf` and `tiered721HookOf` mappings, which are set by REVDeployer via DEPLOYER-restricted setters (`setCashOutDelayOf()`, `setTiered721HookOf()`).
229
239
 
230
240
  ### NATIVE_TOKEN Accounting on Non-ETH Chains
231
241
 
@@ -300,8 +310,14 @@ Quick-reference for common read operations. All functions are `view`/`pure` and
300
310
  | What | Call | Returns |
301
311
  |------|------|---------|
302
312
  | Config hash (cross-chain matching) | `REVDeployer.hashedEncodedConfigurationOf(revnetId)` | `bytes32` |
303
- | 721 hook address | `REVDeployer.tiered721HookOf(revnetId)` | `IJB721TiersHook` |
304
- | 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) |
305
321
 
306
322
  ## Example Integration
307
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.
@@ -163,6 +164,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
163
164
  **Prerequisites:**
164
165
  - Caller must hold `collateralCount` revnet ERC-20 tokens
165
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.
166
168
 
167
169
  **Key parameters:**
168
170
 
@@ -181,6 +183,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
181
183
  - `collateralCount > 0` (no zero-collateral loans)
182
184
  - `source.terminal` is registered for the revnet in the directory
183
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`.
184
187
  2. **Loan ID generation:** `revnetId * 1_000_000_000_000 + (++totalLoansBorrowedFor[revnetId])`
185
188
  3. **Loan creation in storage:**
186
189
  - `source`, `createdAt = block.timestamp`, `prepaidFeePercent`, `prepaidDuration = mulDiv(prepaidFeePercent, 3650 days, 500)`
@@ -210,6 +213,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
210
213
  - `prepaidDuration` at minimum (25): `25 * 3650 days / 500 = 182.5 days`. At maximum (500): `500 * 3650 days / 500 = 3650 days`.
211
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.
212
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.
213
217
 
214
218
  ---
215
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.17",
3
+ "version": "0.0.19",
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.21",
23
- "@bananapus/buyback-hook-v6": "^0.0.21",
24
- "@bananapus/core-v6": "^0.0.27",
25
- "@bananapus/ownable-v6": "^0.0.14",
22
+ "@bananapus/721-hook-v6": "^0.0.22",
23
+ "@bananapus/buyback-hook-v6": "^0.0.22",
24
+ "@bananapus/core-v6": "^0.0.28",
25
+ "@bananapus/ownable-v6": "^0.0.15",
26
26
  "@bananapus/permission-ids-v6": "^0.0.14",
27
- "@bananapus/router-terminal-v6": "^0.0.20",
28
- "@bananapus/suckers-v6": "^0.0.17",
29
- "@croptop/core-v6": "^0.0.22",
27
+ "@bananapus/router-terminal-v6": "^0.0.21",
28
+ "@bananapus/suckers-v6": "^0.0.18",
29
+ "@croptop/core-v6": "^0.0.23",
30
30
  "@openzeppelin/contracts": "^5.6.1",
31
31
  "@uniswap/v4-core": "^1.0.2",
32
32
  "@uniswap/v4-periphery": "^1.0.3"
@@ -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;
@@ -323,7 +327,6 @@ contract DeployScript is Script, Sphinx {
323
327
  tiersConfig: JB721InitTiersConfig({
324
328
  tiers: new JB721TierConfig[](0), currency: ETH_CURRENCY, decimals: 18
325
329
  }),
326
- reserveBeneficiary: address(0),
327
330
  flags: REV721TiersHookFlags({
328
331
  noNewTiersWithReserves: false,
329
332
  noNewTiersWithVotes: false,
@@ -369,6 +372,8 @@ contract DeployScript is Script, Sphinx {
369
372
  bool _singletonsExist;
370
373
  // The address of the previously deployed REVLoans, if found.
371
374
  address _existingRevloansAddr;
375
+ // The address of the previously deployed REVOwner, if found.
376
+ address _existingOwnerAddr;
372
377
  // The address of the previously deployed REVDeployer, if found.
373
378
  address _existingDeployerAddr;
374
379
 
@@ -392,6 +397,19 @@ contract DeployScript is Script, Sphinx {
392
397
  // Flag that singletons were found.
393
398
  _singletonsExist = true;
394
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
+
395
413
  // Also predict and verify the deployer.
396
414
  (_existingDeployerAddr,) = _isDeployed({
397
415
  salt: DEPLOYER_SALT,
@@ -404,7 +422,8 @@ contract DeployScript is Script, Sphinx {
404
422
  croptop.publisher,
405
423
  IJBBuybackHookRegistry(address(buybackHook.registry)),
406
424
  _candidateRevloansAddr,
407
- TRUSTED_FORWARDER
425
+ TRUSTED_FORWARDER,
426
+ _existingOwnerAddr
408
427
  )
409
428
  });
410
429
  // Stop searching — we found the deployed singletons.
@@ -431,7 +450,18 @@ contract DeployScript is Script, Sphinx {
431
450
  trustedForwarder: TRUSTED_FORWARDER
432
451
  });
433
452
 
434
- // 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.
435
465
  (address _deployerAddr, bool _deployerIsDeployed) = _isDeployed({
436
466
  salt: DEPLOYER_SALT,
437
467
  creationCode: type(REVDeployer).creationCode,
@@ -443,7 +473,8 @@ contract DeployScript is Script, Sphinx {
443
473
  croptop.publisher,
444
474
  IJBBuybackHookRegistry(address(buybackHook.registry)),
445
475
  address(revloans),
446
- TRUSTED_FORWARDER
476
+ TRUSTED_FORWARDER,
477
+ address(revOwner)
447
478
  )
448
479
  });
449
480
  REVDeployer _basicDeployer = _deployerIsDeployed
@@ -456,24 +487,34 @@ contract DeployScript is Script, Sphinx {
456
487
  publisher: croptop.publisher,
457
488
  buybackHook: IJBBuybackHookRegistry(address(buybackHook.registry)),
458
489
  loans: address(revloans),
459
- trustedForwarder: TRUSTED_FORWARDER
490
+ trustedForwarder: TRUSTED_FORWARDER,
491
+ owner: address(revOwner)
460
492
  });
461
493
 
462
- // Approve the basic deployer to configure the project.
463
- core.projects.approve({to: address(_basicDeployer), tokenId: FEE_PROJECT_ID});
464
-
465
- // Build the config.
466
- FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
494
+ // Link the REVOwner to the REVDeployer (can only be called once).
495
+ if (!_deployerIsDeployed) {
496
+ revOwner.initialize(IREVDeployer(address(_basicDeployer)));
497
+ }
467
498
 
468
- // Configure the project.
469
- _basicDeployer.deployFor({
470
- revnetId: FEE_PROJECT_ID,
471
- configuration: feeProjectConfig.configuration,
472
- terminalConfigurations: feeProjectConfig.terminalConfigurations,
473
- suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
474
- tiered721HookConfiguration: feeProjectConfig.tiered721HookConfiguration,
475
- allowedPosts: feeProjectConfig.allowedPosts
476
- });
499
+ // Only configure the fee project if singletons were freshly deployed. Re-running `deployFor` on an
500
+ // already-configured project would fail because the project is no longer blank.
501
+ if (!_singletonsExist) {
502
+ // Approve the basic deployer to configure the project.
503
+ core.projects.approve({to: address(_basicDeployer), tokenId: FEE_PROJECT_ID});
504
+
505
+ // Build the config.
506
+ FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
507
+
508
+ // Configure the project.
509
+ _basicDeployer.deployFor({
510
+ revnetId: FEE_PROJECT_ID,
511
+ configuration: feeProjectConfig.configuration,
512
+ terminalConfigurations: feeProjectConfig.terminalConfigurations,
513
+ suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
514
+ tiered721HookConfiguration: feeProjectConfig.tiered721HookConfiguration,
515
+ allowedPosts: feeProjectConfig.allowedPosts
516
+ });
517
+ }
477
518
  }
478
519
 
479
520
  /// @notice Check whether a contract has already been deployed at its deterministic address.