@rev-net/core-v6 0.0.16 → 0.0.18

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 (79) hide show
  1. package/ARCHITECTURE.md +1 -0
  2. package/AUDIT_INSTRUCTIONS.md +3 -1
  3. package/CHANGE_LOG.md +12 -3
  4. package/RISKS.md +4 -0
  5. package/SKILLS.md +5 -4
  6. package/STYLE_GUIDE.md +2 -2
  7. package/USER_JOURNEYS.md +3 -0
  8. package/foundry.toml +1 -1
  9. package/package.json +9 -9
  10. package/script/Deploy.s.sol +20 -17
  11. package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
  12. package/src/REVDeployer.sol +5 -2
  13. package/src/REVLoans.sol +37 -1
  14. package/src/structs/REVBaseline721HookConfig.sol +0 -2
  15. package/test/REV.integrations.t.sol +1 -1
  16. package/test/REVAutoIssuanceFuzz.t.sol +1 -1
  17. package/test/REVDeployerRegressions.t.sol +1 -1
  18. package/test/REVInvincibility.t.sol +1 -1
  19. package/test/REVInvincibilityHandler.sol +1 -1
  20. package/test/REVLifecycle.t.sol +1 -1
  21. package/test/REVLoans.invariants.t.sol +1 -1
  22. package/test/REVLoansAttacks.t.sol +1 -1
  23. package/test/REVLoansFeeRecovery.t.sol +1 -1
  24. package/test/REVLoansFindings.t.sol +1 -1
  25. package/test/REVLoansRegressions.t.sol +1 -1
  26. package/test/REVLoansSourceFeeRecovery.t.sol +1 -1
  27. package/test/REVLoansSourced.t.sol +1 -1
  28. package/test/REVLoansUnSourced.t.sol +1 -1
  29. package/test/TestBurnHeldTokens.t.sol +1 -1
  30. package/test/TestCEIPattern.t.sol +1 -1
  31. package/test/TestCashOutCallerValidation.t.sol +1 -1
  32. package/test/TestConversionDocumentation.t.sol +1 -1
  33. package/test/TestCrossCurrencyReclaim.t.sol +1 -1
  34. package/test/TestCrossSourceReallocation.t.sol +1 -1
  35. package/test/TestERC2771MetaTx.t.sol +1 -1
  36. package/test/TestEmptyBuybackSpecs.t.sol +1 -1
  37. package/test/TestFlashLoanSurplus.t.sol +1 -1
  38. package/test/TestHookArrayOOB.t.sol +1 -1
  39. package/test/TestLiquidationBehavior.t.sol +1 -1
  40. package/test/TestLoanSourceRotation.t.sol +1 -1
  41. package/test/TestLoansCashOutDelay.t.sol +467 -0
  42. package/test/TestLongTailEconomics.t.sol +1 -1
  43. package/test/TestLowFindings.t.sol +1 -1
  44. package/test/TestMixedFixes.t.sol +1 -1
  45. package/test/TestPermit2Signatures.t.sol +1 -1
  46. package/test/TestReallocationSandwich.t.sol +1 -1
  47. package/test/TestRevnetRegressions.t.sol +1 -1
  48. package/test/TestSplitWeightAdjustment.t.sol +1 -1
  49. package/test/TestSplitWeightE2E.t.sol +1 -2
  50. package/test/TestSplitWeightFork.t.sol +1 -2
  51. package/test/TestStageTransitionBorrowable.t.sol +1 -1
  52. package/test/TestSwapTerminalPermission.t.sol +1 -1
  53. package/test/TestUint112Overflow.t.sol +1 -1
  54. package/test/TestZeroRepayment.t.sol +1 -1
  55. package/test/audit/LoanIdOverflowGuard.t.sol +1 -1
  56. package/test/fork/ForkTestBase.sol +1 -2
  57. package/test/fork/TestAutoIssuanceFork.t.sol +1 -1
  58. package/test/fork/TestCashOutFork.t.sol +1 -1
  59. package/test/fork/TestIssuanceDecayFork.t.sol +1 -1
  60. package/test/fork/TestLoanBorrowFork.t.sol +1 -1
  61. package/test/fork/TestLoanCrossRulesetFork.t.sol +1 -1
  62. package/test/fork/TestLoanERC20Fork.t.sol +1 -1
  63. package/test/fork/TestLoanLiquidationFork.t.sol +1 -1
  64. package/test/fork/TestLoanReallocateFork.t.sol +1 -1
  65. package/test/fork/TestLoanRepayFork.t.sol +1 -1
  66. package/test/fork/TestLoanTransferFork.t.sol +1 -1
  67. package/test/fork/TestPermit2PaymentFork.t.sol +1 -1
  68. package/test/fork/TestSplitWeightFork.t.sol +1 -1
  69. package/test/helpers/MaliciousContracts.sol +1 -1
  70. package/test/helpers/REVEmpty721Config.sol +0 -1
  71. package/test/mock/MockBuybackCashOutRecorder.sol +1 -1
  72. package/test/mock/MockBuybackDataHook.sol +1 -1
  73. package/test/mock/MockBuybackDataHookMintPath.sol +1 -1
  74. package/test/regression/TestBurnPermissionRequired.t.sol +1 -1
  75. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +1 -1
  76. package/test/regression/TestCrossRevnetLiquidation.t.sol +1 -1
  77. package/test/regression/TestCumulativeLoanCounter.t.sol +1 -1
  78. package/test/regression/TestLiquidateGapHandling.t.sol +1 -1
  79. package/test/regression/TestZeroPriceFeed.t.sol +1 -1
package/ARCHITECTURE.md CHANGED
@@ -76,6 +76,7 @@ Cash Out → REVDeployer.beforeCashOutRecordedWith()
76
76
  ### Loan Flow
77
77
  ```
78
78
  Borrower → REVLoans.borrowFrom()
79
+ → Enforce cash-out delay if set (cross-chain deployment protection)
79
80
  → Burn borrower's revnet tokens as collateral
80
81
  → Calculate borrow amount from bonding curve value of collateral
81
82
  → Pull funds from treasury via USE_ALLOWANCE
@@ -93,6 +93,7 @@ User cashes out via terminal
93
93
  ```
94
94
  Borrower calls REVLoans.borrowFrom()
95
95
  -> Prerequisite: caller must have granted BURN_TOKENS permission to REVLoans via JBPermissions
96
+ -> Enforce cash-out delay: resolve deployer from ruleset dataHook, check cashOutDelayOf(revnetId)
96
97
  -> Validate: collateral > 0, terminal registered, prepaidFeePercent in range
97
98
  -> Generate loan ID: revnetId * 1T + loanNumber
98
99
  -> Create loan in storage
@@ -332,6 +333,7 @@ No prior formal audit with finding IDs has been conducted on this codebase. All
332
333
  | `REVDeployer_StagesRequired` | REVDeployer | `deployFor` / `launchChainsFor` called with empty `stageConfigurations` array |
333
334
  | `REVDeployer_StageTimesMustIncrease` | REVDeployer | Stage `startsAtOrAfter` timestamps are not strictly increasing |
334
335
  | `REVDeployer_Unauthorized` | REVDeployer | Caller is not the split operator (for operator-gated functions) or not the project owner (for `launchChainsFor`) |
336
+ | `REVLoans_CashOutDelayNotFinished` | REVLoans | `borrowFrom` called during the 30-day cash-out delay period (cross-chain deployment protection) |
335
337
  | `REVLoans_CollateralExceedsLoan` | REVLoans | `reallocateCollateralFromLoan` called with `collateralCountToReturn > loan.collateral` |
336
338
  | `REVLoans_InvalidPrepaidFeePercent` | REVLoans | `prepaidFeePercent` outside `[MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT]` range (25-500) |
337
339
  | `REVLoans_InvalidTerminal` | REVLoans | Loan source references a terminal not registered in `JBDirectory` for the revnet |
@@ -353,7 +355,7 @@ No prior formal audit with finding IDs has been conducted on this codebase. All
353
355
 
354
356
  ## Compiler and Version Info
355
357
 
356
- - **Solidity**: ^0.8.26
358
+ - **Solidity**: 0.8.28
357
359
  - **EVM target**: Cancun
358
360
  - **Optimizer**: via-IR, 100 runs
359
361
  - **Dependencies**: OpenZeppelin 5.x, PRBMath, Permit2, nana-core-v6, nana-721-hook-v6, nana-buyback-hook-v6, nana-suckers-v6
package/CHANGE_LOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # revnet-core-v6 Changelog (v5 -> v6)
2
2
 
3
- This document describes all changes between `revnet-core` (v5, Solidity 0.8.23) and `revnet-core-v6` (v6, Solidity ^0.8.26).
3
+ This document describes all changes between `revnet-core` (v5, Solidity 0.8.23) and `revnet-core-v6` (v6, Solidity 0.8.28).
4
4
 
5
5
  ## Summary
6
6
 
@@ -108,6 +108,7 @@ The boolean semantics are **inverted**: v5 used opt-in flags (`splitOperatorCan*
108
108
  | `REVLoans` | `REVLoans_NothingToRepay()` |
109
109
  | `REVLoans` | `REVLoans_ZeroBorrowAmount()` |
110
110
  | `REVLoans` | `REVLoans_SourceMismatch()` |
111
+ | `REVLoans` | `REVLoans_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp)` |
111
112
  | `REVLoans` | `REVLoans_LoanIdOverflow()` |
112
113
 
113
114
  ### 2.4 New Constants
@@ -129,6 +130,13 @@ The boolean semantics are **inverted**: v5 used opt-in flags (`splitOperatorCan*
129
130
 
130
131
  ## 3. Event Changes
131
132
 
133
+ ### 3.0 Indexer Notes
134
+
135
+ For revnet-focused subgraphs:
136
+ - both deployment flows now correlate to `deployFor` rather than a split `deployFor`/`deployWith721sFor` model;
137
+ - revnet deployment entities should expect an associated 721 hook by default;
138
+ - any entity that previously depended on caller-supplied buyback-hook config should be updated for the v6 auto-configured buyback path.
139
+
132
140
  ### 3.1 Added Events
133
141
 
134
142
  See section 2.2 above.
@@ -245,7 +253,7 @@ The following structs are identical between v5 and v6 (only `forge-lint` comment
245
253
 
246
254
  | Change | Description |
247
255
  |--------|-------------|
248
- | **Solidity version** | Upgraded from `0.8.23` to `^0.8.26`. |
256
+ | **Solidity version** | Upgraded from `0.8.23` to `0.8.28`. |
249
257
  | **Buyback hook architecture** | Per-revnet `buybackHookOf` mapping replaced with a single immutable `BUYBACK_HOOK` (`IJBBuybackHookRegistry`). Pools are auto-initialized for each terminal token during deployment via `_tryInitializeBuybackPoolFor`. |
250
258
  | **Loans architecture** | Per-revnet `loansOf` mapping replaced with a single immutable `LOANS` address. The deployer grants `USE_ALLOWANCE` permission to the loans contract for all revnets in the constructor (wildcard `revnetId=0`). |
251
259
  | **Constructor permissions** | v6 constructor grants three wildcard permissions: `MAP_SUCKER_TOKEN` to the sucker registry, `USE_ALLOWANCE` to the loans contract, and `SET_BUYBACK_POOL` to the buyback hook. v5 only granted `MAP_SUCKER_TOKEN`. |
@@ -270,7 +278,7 @@ The following structs are identical between v5 and v6 (only `forge-lint` comment
270
278
 
271
279
  | Change | Description |
272
280
  |--------|-------------|
273
- | **Solidity version** | Upgraded from `0.8.23` to `^0.8.26`. |
281
+ | **Solidity version** | Upgraded from `0.8.23` to `0.8.28`. |
274
282
  | **Deployer dependency removed** | v5 stored `REVNETS` (`IREVDeployer`) and validated that the revnet was owned by the expected deployer via `RevnetsMismatch`. v6 does not reference the deployer at all. Validation now checks the terminal directly via `DIRECTORY.isTerminalOf`. |
275
283
  | **Constructor refactored** | v5 accepted `IREVDeployer revnets` and derived `CONTROLLER`, `DIRECTORY`, etc. from it. v6 accepts `IJBController controller` and `IJBProjects projects` directly. |
276
284
  | **Terminal validation** | `borrowFrom` now validates that the source terminal is registered in the directory for the revnet, reverting with `REVLoans_InvalidTerminal` if not. v5 validated deployer ownership instead. |
@@ -289,6 +297,7 @@ The following structs are identical between v5 and v6 (only `forge-lint` comment
289
297
  | **Source fee try-catch hardening** | The source fee payment in `_adjust` is now wrapped in a try-catch block. If the source terminal's `pay` call reverts, the ERC-20 allowance is reclaimed and the fee amount is returned to the beneficiary instead of blocking the entire loan operation. v5 called `terminal.pay` directly without error handling. |
290
298
  | **Timestamp cast fix** | `borrowFrom` now casts `block.timestamp` to `uint48` when setting `loan.createdAt`, matching the `REVLoan.createdAt` field width. v5 used `uint40`, which would silently truncate timestamps after the year 36812. |
291
299
  | **`ReallocateCollateral` event typo fix** | v5 used `removedcollateralCount` (lowercase 'c'). v6 fixes it to `removedCollateralCount` (uppercase 'C'). |
300
+ | **Cash out delay enforced in loans** | `borrowFrom` now resolves the `REVDeployer` from the ruleset's `dataHook` and checks `cashOutDelayOf(revnetId)`. If the 30-day cross-chain deployment delay hasn't passed, `borrowFrom` reverts with `REVLoans_CashOutDelayNotFinished`. `borrowableAmountFrom` returns 0 during the delay so UIs reflect the restriction. v5 did not enforce the cash out delay in the loans contract. |
292
301
  | **NatSpec documentation** | Extensive NatSpec added to all functions, views, and internal helpers. Flash loan safety analysis documented in `_borrowableAmountFrom`. |
293
302
 
294
303
  ### 6.3 Named Arguments
package/RISKS.md CHANGED
@@ -81,6 +81,10 @@ Read [ARCHITECTURE.md](./ARCHITECTURE.md) and [SKILLS.md](./SKILLS.md) for proto
81
81
  - **Source mismatch check.** `reallocateCollateralFromLoan` enforces that the new loan's source matches the existing loan's source (`source.token == existingSource.token && source.terminal == existingSource.terminal`). This prevents cross-source value extraction.
82
82
  - **MEV opportunity at stage boundaries.** If a borrower knows a stage transition will decrease `cashOutTaxRate`, they can wait until just after the transition and `reallocateCollateralFromLoan` to extract more borrowed funds from the same collateral. This is predictable and not preventable by design.
83
83
 
84
+ ### Cross-chain cash-out delay enforcement
85
+
86
+ - **Loans enforce the same 30-day cash-out delay as direct cash outs.** When a revnet is deployed to a new chain where its first stage has already started, `REVDeployer._setCashOutDelayIfNeeded()` sets a 30-day delay. `borrowFrom` resolves the deployer from the current ruleset's `dataHook` and checks `cashOutDelayOf(revnetId)`, reverting with `REVLoans_CashOutDelayNotFinished` if the delay hasn't passed. `borrowableAmountFrom` returns 0 during the delay. This prevents cross-chain arbitrage via the loan system (bridging tokens to a new chain and immediately borrowing against them before prices equilibrate).
87
+
84
88
  ### BURN_TOKENS permission prerequisite
85
89
 
86
90
  - **Borrowers must grant BURN_TOKENS permission before calling `borrowFrom`.** The loans contract burns the caller's tokens as collateral via `JBController.burnTokensOf`, which requires the caller to have granted `BURN_TOKENS` permission to the loans contract for the revnet's project ID. Without this, the transaction reverts deep in `JBController` with `JBPermissioned_Unauthorized`. The prerequisite is documented in `borrowFrom`'s NatSpec.
package/SKILLS.md CHANGED
@@ -47,7 +47,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
47
47
 
48
48
  | Function | Permissions | What it does |
49
49
  |----------|------------|-------------|
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. |
50
+ | `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
51
  | `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
52
  | `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
53
  | `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 +57,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
57
57
 
58
58
  | Function | What it does |
59
59
  |----------|-------------|
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. |
60
+ | `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
61
  | `REVLoans.determineSourceFeeAmount(loan, amount)` | Calculate the time-proportional source fee for a loan repayment. Zero during prepaid window, linear accrual after. |
62
62
  | `REVLoans.loanOf(loanId)` | Returns the full `REVLoan` struct for a loan. |
63
63
  | `REVLoans.loanSourcesOf(revnetId)` | Returns all `(terminal, token)` pairs used for loans by a revnet. |
@@ -139,6 +139,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
139
139
 
140
140
  | Error | When It Fires |
141
141
  |-------|---------------|
142
+ | `REVLoans_CashOutDelayNotFinished(cashOutDelay, blockTimestamp)` | When borrowing during the 30-day cash-out delay period (cross-chain deployment protection). |
142
143
  | `REVLoans_CollateralExceedsLoan(collateralToReturn, loanCollateral)` | When trying to return more collateral than the loan holds. |
143
144
  | `REVLoans_InvalidPrepaidFeePercent(prepaidFeePercent, min, max)` | When `prepaidFeePercent` is outside the allowed range (2.5%--50%). |
144
145
  | `REVLoans_InvalidTerminal(terminal, revnetId)` | When the specified terminal is not registered for the revnet. |
@@ -212,8 +213,8 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
212
213
  4. **Loan ID encoding.** `loanId = revnetId * 1_000_000_000_000 + loanNumber`. Each revnet supports ~1 trillion loans. Use `revnetIdOfLoanWith(loanId)` to decode.
213
214
  5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
214
215
  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.
216
+ 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)`.
217
218
  9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
218
219
  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
220
  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.
package/STYLE_GUIDE.md CHANGED
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
21
21
 
22
22
  ```solidity
23
23
  // Contracts — pin to exact version
24
- pragma solidity ^0.8.26;
24
+ pragma solidity 0.8.28;
25
25
 
26
26
  // Interfaces, structs, enums — caret for forward compatibility
27
27
  pragma solidity ^0.8.0;
@@ -326,7 +326,7 @@ Standard config across all repos:
326
326
 
327
327
  ```toml
328
328
  [profile.default]
329
- solc = '0.8.26'
329
+ solc = '0.8.28'
330
330
  evm_version = 'cancun'
331
331
  optimizer_runs = 200
332
332
  libs = ["node_modules", "lib"]
package/USER_JOURNEYS.md CHANGED
@@ -163,6 +163,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
163
163
  **Prerequisites:**
164
164
  - Caller must hold `collateralCount` revnet ERC-20 tokens
165
165
  - 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`.
166
+ - 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
167
 
167
168
  **Key parameters:**
168
169
 
@@ -181,6 +182,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
181
182
  - `collateralCount > 0` (no zero-collateral loans)
182
183
  - `source.terminal` is registered for the revnet in the directory
183
184
  - `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`.
184
186
  2. **Loan ID generation:** `revnetId * 1_000_000_000_000 + (++totalLoansBorrowedFor[revnetId])`
185
187
  3. **Loan creation in storage:**
186
188
  - `source`, `createdAt = block.timestamp`, `prepaidFeePercent`, `prepaidDuration = mulDiv(prepaidFeePercent, 3650 days, 500)`
@@ -210,6 +212,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
210
212
  - `prepaidDuration` at minimum (25): `25 * 3650 days / 500 = 182.5 days`. At maximum (500): `500 * 3650 days / 500 = 3650 days`.
211
213
  - 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
214
  - 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.
213
216
 
214
217
  ---
215
218
 
package/foundry.toml CHANGED
@@ -1,5 +1,5 @@
1
1
  [profile.default]
2
- solc = '0.8.26'
2
+ solc = '0.8.28'
3
3
  evm_version = 'cancun'
4
4
  via_ir = true
5
5
  optimizer_runs = 100
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rev-net/core-v6",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
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.20",
23
- "@bananapus/buyback-hook-v6": "^0.0.19",
24
- "@bananapus/core-v6": "^0.0.26",
25
- "@bananapus/ownable-v6": "^0.0.13",
26
- "@bananapus/permission-ids-v6": "^0.0.12",
27
- "@bananapus/router-terminal-v6": "^0.0.19",
28
- "@bananapus/suckers-v6": "^0.0.16",
29
- "@croptop/core-v6": "^0.0.21",
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
+ "@bananapus/permission-ids-v6": "^0.0.14",
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"
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
@@ -323,7 +323,6 @@ contract DeployScript is Script, Sphinx {
323
323
  tiersConfig: JB721InitTiersConfig({
324
324
  tiers: new JB721TierConfig[](0), currency: ETH_CURRENCY, decimals: 18
325
325
  }),
326
- reserveBeneficiary: address(0),
327
326
  flags: REV721TiersHookFlags({
328
327
  noNewTiersWithReserves: false,
329
328
  noNewTiersWithVotes: false,
@@ -459,21 +458,25 @@ contract DeployScript is Script, Sphinx {
459
458
  trustedForwarder: TRUSTED_FORWARDER
460
459
  });
461
460
 
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();
467
-
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
- });
461
+ // Only configure the fee project if singletons were freshly deployed. Re-running `deployFor` on an
462
+ // already-configured project would fail because the project is no longer blank.
463
+ if (!_singletonsExist) {
464
+ // Approve the basic deployer to configure the project.
465
+ core.projects.approve({to: address(_basicDeployer), tokenId: FEE_PROJECT_ID});
466
+
467
+ // Build the config.
468
+ FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
469
+
470
+ // Configure the project.
471
+ _basicDeployer.deployFor({
472
+ revnetId: FEE_PROJECT_ID,
473
+ configuration: feeProjectConfig.configuration,
474
+ terminalConfigurations: feeProjectConfig.terminalConfigurations,
475
+ suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
476
+ tiered721HookConfiguration: feeProjectConfig.tiered721HookConfiguration,
477
+ allowedPosts: feeProjectConfig.allowedPosts
478
+ });
479
+ }
477
480
  }
478
481
 
479
482
  /// @notice Check whether a contract has already been deployed at its deterministic address.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {stdJson} from "forge-std/Script.sol";
5
5
  import {Vm} from "forge-std/Vm.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
5
5
  import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
@@ -282,6 +282,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
282
282
  }
283
283
 
284
284
  // Split the cashed-out tokens into a fee portion and a non-fee portion.
285
+ // The fee is applied to TOKEN COUNT (2.5% of tokens), not to value. The fee revnet receives the bonding-curve
286
+ // reclaim of its 2.5% token share regardless of whether the remaining 97.5% routes through a buyback pool at
287
+ // a better price. This is by design.
285
288
  // Micro cash outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero, bypassing the fee.
286
289
  // Economically insignificant: the gas cost of the transaction far exceeds the bypassed fee. No fix needed.
287
290
  uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
@@ -608,6 +611,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
608
611
  sqrtPriceX96 = uint160(1 << 96);
609
612
  } else {
610
613
  address normalizedTerminalToken = terminalToken == JBConstants.NATIVE_TOKEN ? address(0) : terminalToken;
614
+ // slither-disable-next-line calls-loop
611
615
  address projectToken = address(CONTROLLER.TOKENS().tokenOf(revnetId));
612
616
 
613
617
  if (projectToken == address(0) || projectToken == normalizedTerminalToken) {
@@ -955,7 +959,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
955
959
  tokenUriResolver: tiered721HookConfiguration.baseline721HookConfiguration.tokenUriResolver,
956
960
  contractUri: tiered721HookConfiguration.baseline721HookConfiguration.contractUri,
957
961
  tiersConfig: tiered721HookConfiguration.baseline721HookConfiguration.tiersConfig,
958
- reserveBeneficiary: tiered721HookConfiguration.baseline721HookConfiguration.reserveBeneficiary,
959
962
  flags: JB721TiersHookFlags({
960
963
  noNewTiersWithReserves: tiered721HookConfiguration.baseline721HookConfiguration.flags
961
964
  .noNewTiersWithReserves,
package/src/REVLoans.sol CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
5
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
@@ -27,6 +27,7 @@ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingCo
27
27
  import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
28
28
  import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
29
29
 
30
+ import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
30
31
  import {IREVLoans} from "./interfaces/IREVLoans.sol";
31
32
  import {REVLoan} from "./structs/REVLoan.sol";
32
33
  import {REVLoanSource} from "./structs/REVLoanSource.sol";
@@ -55,6 +56,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
55
56
  // --------------------------- custom errors ------------------------- //
56
57
  //*********************************************************************//
57
58
 
59
+ error REVLoans_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
58
60
  error REVLoans_CollateralExceedsLoan(uint256 collateralToReturn, uint256 loanCollateral);
59
61
  error REVLoans_InvalidPrepaidFeePercent(uint256 prepaidFeePercent, uint256 min, uint256 max);
60
62
  error REVLoans_InvalidTerminal(address terminal, uint256 revnetId);
@@ -225,6 +227,22 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
225
227
  view
226
228
  returns (uint256)
227
229
  {
230
+ // Get the current ruleset to resolve the deployer from its data hook.
231
+ // slither-disable-next-line unused-return
232
+ (JBRuleset memory currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
233
+
234
+ // The ruleset's data hook is the REVDeployer that configured this revnet.
235
+ address deployer = currentRuleset.dataHook();
236
+
237
+ // Only check the delay if a deployer is set.
238
+ if (deployer != address(0)) {
239
+ // Get the timestamp after which cash outs (and loans) are allowed.
240
+ uint256 cashOutDelay = IREVDeployer(deployer).cashOutDelayOf(revnetId);
241
+
242
+ // If the delay hasn't passed yet, no amount is borrowable.
243
+ if (cashOutDelay > block.timestamp) return 0;
244
+ }
245
+
228
246
  return _borrowableAmountFrom({
229
247
  revnetId: revnetId,
230
248
  collateralCount: collateralCount,
@@ -580,6 +598,24 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
580
598
  );
581
599
  }
582
600
 
601
+ // Get the current ruleset to resolve the deployer from its data hook.
602
+ // slither-disable-next-line unused-return
603
+ (JBRuleset memory currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
604
+
605
+ // The ruleset's data hook is the REVDeployer that configured this revnet.
606
+ address deployer = currentRuleset.dataHook();
607
+
608
+ // Only check the delay if a deployer is set.
609
+ if (deployer != address(0)) {
610
+ // Get the timestamp after which cash outs (and loans) are allowed.
611
+ uint256 cashOutDelay = IREVDeployer(deployer).cashOutDelayOf(revnetId);
612
+
613
+ // Revert if the delay hasn't passed yet.
614
+ if (cashOutDelay > block.timestamp) {
615
+ revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
616
+ }
617
+ }
618
+
583
619
  // Prevent the loan number from exceeding the ID namespace for this revnet.
584
620
  if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
585
621
 
@@ -12,7 +12,6 @@ import {REV721TiersHookFlags} from "./REV721TiersHookFlags.sol";
12
12
  /// @custom:member tokenUriResolver The token URI resolver for the NFT collection.
13
13
  /// @custom:member contractUri The contract URI for the NFT collection.
14
14
  /// @custom:member tiersConfig The tier configuration for the NFT collection.
15
- /// @custom:member reserveBeneficiary The default reserve beneficiary for the NFT collection.
16
15
  /// @custom:member flags A set of flags that configure the 721 hook. Omits `issueTokensForSplits` since revnets
17
16
  /// always force it to `false`.
18
17
  // forge-lint: disable-next-line(pascal-case-struct)
@@ -23,6 +22,5 @@ struct REVBaseline721HookConfig {
23
22
  IJB721TokenUriResolver tokenUriResolver;
24
23
  string contractUri;
25
24
  JB721InitTiersConfig tiersConfig;
26
- address reserveBeneficiary;
27
25
  REV721TiersHookFlags flags;
28
26
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";