@rev-net/core-v6 0.0.18 → 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 (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 +79 -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 +42 -4
  12. package/src/REVDeployer.sol +20 -305
  13. package/src/REVLoans.sol +24 -29
  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/test/REV.integrations.t.sol +14 -1
  18. package/test/REVAutoIssuanceFuzz.t.sol +14 -1
  19. package/test/REVDeployerRegressions.t.sol +17 -2
  20. package/test/REVInvincibility.t.sol +31 -3
  21. package/test/REVLifecycle.t.sol +16 -1
  22. package/test/REVLoans.invariants.t.sol +16 -1
  23. package/test/REVLoansAttacks.t.sol +16 -1
  24. package/test/REVLoansFeeRecovery.t.sol +16 -1
  25. package/test/REVLoansFindings.t.sol +16 -1
  26. package/test/REVLoansRegressions.t.sol +16 -1
  27. package/test/REVLoansSourceFeeRecovery.t.sol +16 -1
  28. package/test/REVLoansSourced.t.sol +16 -1
  29. package/test/REVLoansUnSourced.t.sol +16 -1
  30. package/test/TestBurnHeldTokens.t.sol +16 -1
  31. package/test/TestCEIPattern.t.sol +16 -1
  32. package/test/TestCashOutCallerValidation.t.sol +19 -4
  33. package/test/TestConversionDocumentation.t.sol +16 -1
  34. package/test/TestCrossCurrencyReclaim.t.sol +16 -1
  35. package/test/TestCrossSourceReallocation.t.sol +16 -1
  36. package/test/TestERC2771MetaTx.t.sol +16 -1
  37. package/test/TestEmptyBuybackSpecs.t.sol +18 -3
  38. package/test/TestFlashLoanSurplus.t.sol +16 -1
  39. package/test/TestHookArrayOOB.t.sol +17 -2
  40. package/test/TestLiquidationBehavior.t.sol +16 -1
  41. package/test/TestLoanSourceRotation.t.sol +16 -1
  42. package/test/TestLoansCashOutDelay.t.sol +21 -6
  43. package/test/TestLongTailEconomics.t.sol +16 -1
  44. package/test/TestLowFindings.t.sol +16 -1
  45. package/test/TestMixedFixes.t.sol +16 -1
  46. package/test/TestPermit2Signatures.t.sol +16 -1
  47. package/test/TestReallocationSandwich.t.sol +16 -1
  48. package/test/TestRevnetRegressions.t.sol +16 -1
  49. package/test/TestSplitWeightAdjustment.t.sol +43 -19
  50. package/test/TestSplitWeightE2E.t.sol +26 -2
  51. package/test/TestSplitWeightFork.t.sol +16 -1
  52. package/test/TestStageTransitionBorrowable.t.sol +16 -1
  53. package/test/TestSwapTerminalPermission.t.sol +16 -1
  54. package/test/TestUint112Overflow.t.sol +16 -1
  55. package/test/TestZeroRepayment.t.sol +16 -1
  56. package/test/audit/LoanIdOverflowGuard.t.sol +16 -1
  57. package/test/fork/ForkTestBase.sol +16 -1
  58. package/test/fork/TestPermit2PaymentFork.t.sol +4 -3
  59. package/test/regression/TestBurnPermissionRequired.t.sol +16 -1
  60. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +15 -1
  61. package/test/regression/TestCrossRevnetLiquidation.t.sol +16 -1
  62. package/test/regression/TestCumulativeLoanCounter.t.sol +16 -1
  63. package/test/regression/TestLiquidateGapHandling.t.sol +16 -1
  64. package/test/regression/TestZeroPriceFeed.t.sol +16 -1
@@ -6,26 +6,17 @@ import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB
6
6
  import {JB721TiersHookFlags} from "@bananapus/721-hook-v6/src/structs/JB721TiersHookFlags.sol";
7
7
  import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
8
8
  import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
9
- import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
10
9
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
11
10
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
12
11
  import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
13
12
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
14
13
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
15
14
  import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
16
- import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
17
- import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
18
- import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
19
15
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
20
16
  import {JBSplitGroupIds} from "@bananapus/core-v6/src/libraries/JBSplitGroupIds.sol";
21
17
  import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
22
- import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
23
- import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
24
- import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
25
- import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
26
18
  import {JBCurrencyAmount} from "@bananapus/core-v6/src/structs/JBCurrencyAmount.sol";
27
19
  import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
28
- import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
29
20
  import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
30
21
  import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
31
22
  import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
@@ -37,13 +28,12 @@ import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerR
37
28
  import {CTPublisher} from "@croptop/core-v6/src/CTPublisher.sol";
38
29
  import {CTAllowedPost} from "@croptop/core-v6/src/structs/CTAllowedPost.sol";
39
30
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
40
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
41
- import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
42
31
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
43
32
  import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
44
33
  import {mulDiv, sqrt} from "@prb/math/src/Common.sol";
45
34
 
46
35
  import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
36
+ import {REVOwner} from "./REVOwner.sol";
47
37
  import {REVAutoIssuance} from "./structs/REVAutoIssuance.sol";
48
38
  import {REVConfig} from "./structs/REVConfig.sol";
49
39
  import {REVCroptopAllowedPost} from "./structs/REVCroptopAllowedPost.sol";
@@ -53,16 +43,12 @@ import {REVSuckerDeploymentConfig} from "./structs/REVSuckerDeploymentConfig.sol
53
43
 
54
44
  /// @notice `REVDeployer` deploys, manages, and operates Revnets.
55
45
  /// @dev Revnets are unowned Juicebox projects which operate autonomously after deployment.
56
- contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCashOutHook, IERC721Receiver {
57
- // A library that adds default safety checks to ERC20 functionality.
58
- using SafeERC20 for IERC20;
59
-
46
+ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
60
47
  //*********************************************************************//
61
48
  // --------------------------- custom errors ------------------------- //
62
49
  //*********************************************************************//
63
50
 
64
51
  error REVDeployer_AutoIssuanceBeneficiaryZeroAddress();
65
- error REVDeployer_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
66
52
  error REVDeployer_CashOutsCantBeTurnedOffCompletely(uint256 cashOutTaxRate, uint256 maxCashOutTaxRate);
67
53
  error REVDeployer_MustHaveSplits();
68
54
  error REVDeployer_NothingToAutoIssue();
@@ -126,6 +112,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
126
112
  /// Participants can borrow up to the current cash out value of their tokens.
127
113
  address public immutable override LOANS;
128
114
 
115
+ /// @notice The runtime data hook contract that handles pay and cash out callbacks for revnets.
116
+ /// @dev Set as `dataHook` in each revnet's ruleset metadata. Implements `IJBRulesetDataHook` and `IJBCashOutHook`.
117
+ address public immutable override OWNER;
118
+
129
119
  /// @notice Stores Juicebox project (and revnet) access permissions.
130
120
  IJBPermissions public immutable override PERMISSIONS;
131
121
 
@@ -152,22 +142,12 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
152
142
  public
153
143
  override amountToAutoIssue;
154
144
 
155
- /// @notice The timestamp of when cashouts will become available to a specific revnet's participants.
156
- /// @dev Only applies to existing revnets which are deploying onto a new network.
157
- /// @custom:param revnetId The ID of the revnet to get the cash out delay for.
158
- mapping(uint256 revnetId => uint256 cashOutDelay) public override cashOutDelayOf;
159
-
160
145
  /// @notice The hashed encoded configuration of each revnet.
161
146
  /// @dev This is used to ensure that the encoded configuration of a revnet is the same when deploying suckers for
162
147
  /// omnichain operations.
163
148
  /// @custom:param revnetId The ID of the revnet to get the hashed encoded configuration for.
164
149
  mapping(uint256 revnetId => bytes32 hashedEncodedConfiguration) public override hashedEncodedConfigurationOf;
165
150
 
166
- /// @notice Each revnet's tiered ERC-721 hook.
167
- /// @custom:param revnetId The ID of the revnet to get the tiered ERC-721 hook for.
168
- // slither-disable-next-line uninitialized-state
169
- mapping(uint256 revnetId => IJB721TiersHook tiered721Hook) public override tiered721HookOf;
170
-
171
151
  //*********************************************************************//
172
152
  // ------------------- internal stored properties -------------------- //
173
153
  //*********************************************************************//
@@ -190,6 +170,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
190
170
  /// @param buybackHook The buyback hook used as a data hook to route payments through buyback pools.
191
171
  /// @param loans The loan contract used by all revnets.
192
172
  /// @param trustedForwarder The trusted forwarder for the ERC2771Context.
173
+ /// @param owner The runtime data hook contract (REVOwner) that handles pay and cash out callbacks.
193
174
  constructor(
194
175
  IJBController controller,
195
176
  IJBSuckerRegistry suckerRegistry,
@@ -198,7 +179,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
198
179
  CTPublisher publisher,
199
180
  IJBBuybackHookRegistry buybackHook,
200
181
  address loans,
201
- address trustedForwarder
182
+ address trustedForwarder,
183
+ address owner
202
184
  )
203
185
  ERC2771Context(trustedForwarder)
204
186
  {
@@ -213,6 +195,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
213
195
  BUYBACK_HOOK = buybackHook;
214
196
  // slither-disable-next-line missing-zero-check
215
197
  LOANS = loans;
198
+ // slither-disable-next-line missing-zero-check
199
+ OWNER = owner;
216
200
 
217
201
  // Give the sucker registry permission to map tokens for all revnets.
218
202
  _setPermission({
@@ -232,195 +216,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
232
216
  // ------------------------- external views -------------------------- //
233
217
  //*********************************************************************//
234
218
 
235
- /// @notice Determine how a cash out from a revnet should be processed.
236
- /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
237
- /// @dev If a sucker is cashing out, no taxes or fees are imposed.
238
- /// @dev REVDeployer is intentionally not registered as a feeless address. The protocol fee (2.5%) applies on top
239
- /// of the rev fee — this is by design. The fee hook spec amount sent to `afterCashOutRecordedWith` will have the
240
- /// protocol fee deducted by the terminal before reaching this contract, so the rev fee is computed on the
241
- /// post-protocol-fee amount.
242
- /// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
243
- /// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
244
- /// out.
245
- /// @return cashOutCount The number of revnet tokens that are cashed out.
246
- /// @return totalSupply The total revnet token supply.
247
- /// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
248
- function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
249
- external
250
- view
251
- override
252
- returns (
253
- uint256 cashOutTaxRate,
254
- uint256 cashOutCount,
255
- uint256 totalSupply,
256
- JBCashOutHookSpecification[] memory hookSpecifications
257
- )
258
- {
259
- // If the cash out is from a sucker, return the full cash out amount without taxes or fees.
260
- // This relies on the sucker registry to only contain trusted sucker contracts deployed via
261
- // the registry's own deploySuckersFor flow — external addresses cannot register as suckers.
262
- if (_isSuckerOf({revnetId: context.projectId, addr: context.holder})) {
263
- return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
264
- }
265
-
266
- // Keep a reference to the cash out delay of the revnet.
267
- uint256 cashOutDelay = cashOutDelayOf[context.projectId];
268
-
269
- // Enforce the cash out delay.
270
- if (cashOutDelay > block.timestamp) {
271
- revert REVDeployer_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
272
- }
273
-
274
- // Get the terminal that will receive the cash out fee.
275
- IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
276
-
277
- // If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
278
- // feeless (e.g. the router terminal routing value between projects), proxy directly to the buyback hook.
279
- if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
280
- // slither-disable-next-line unused-return
281
- return BUYBACK_HOOK.beforeCashOutRecordedWith(context);
282
- }
283
-
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.
288
- // Micro cash outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero, bypassing the fee.
289
- // Economically insignificant: the gas cost of the transaction far exceeds the bypassed fee. No fix needed.
290
- uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
291
- uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
292
-
293
- // Calculate how much surplus the non-fee tokens can reclaim via the bonding curve.
294
- uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
295
- surplus: context.surplus.value,
296
- cashOutCount: nonFeeCashOutCount,
297
- totalSupply: context.totalSupply,
298
- cashOutTaxRate: context.cashOutTaxRate
299
- });
300
-
301
- // Calculate how much the fee tokens reclaim from the remaining surplus after the non-fee reclaim.
302
- uint256 feeAmount = JBCashOuts.cashOutFrom({
303
- surplus: context.surplus.value - postFeeReclaimedAmount,
304
- cashOutCount: feeCashOutCount,
305
- totalSupply: context.totalSupply - nonFeeCashOutCount,
306
- cashOutTaxRate: context.cashOutTaxRate
307
- });
308
-
309
- // Build a context for the buyback hook using only the non-fee token count.
310
- JBBeforeCashOutRecordedContext memory buybackHookContext = context;
311
- buybackHookContext.cashOutCount = nonFeeCashOutCount;
312
-
313
- // Let the buyback hook adjust the cash out parameters and optionally return a hook specification.
314
- JBCashOutHookSpecification[] memory buybackHookSpecifications;
315
- (cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications) =
316
- BUYBACK_HOOK.beforeCashOutRecordedWith(buybackHookContext);
317
-
318
- // If the fee rounds down to zero, return the buyback hook's response directly — no fee to process.
319
- if (feeAmount == 0) return (cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications);
320
-
321
- // Build a hook spec that routes the fee amount to this contract's `afterCashOutRecordedWith` for processing.
322
- JBCashOutHookSpecification memory feeSpec = JBCashOutHookSpecification({
323
- hook: IJBCashOutHook(address(this)), noop: false, amount: feeAmount, metadata: abi.encode(feeTerminal)
324
- });
325
-
326
- // Compose the final hook specifications: buyback spec (if any) + fee spec.
327
- // NOTE: Only buybackHookSpecifications[0] is used. If the buyback hook returns multiple
328
- // specs, the additional ones are silently dropped. This is intentional — the buyback hook is
329
- // expected to return at most one spec for the cash-out buyback swap.
330
- if (buybackHookSpecifications.length > 0) {
331
- // The buyback hook returned a spec — include it before the fee spec.
332
- hookSpecifications = new JBCashOutHookSpecification[](2);
333
- hookSpecifications[0] = buybackHookSpecifications[0];
334
- hookSpecifications[1] = feeSpec;
335
- } else {
336
- // No buyback spec — only the fee spec.
337
- hookSpecifications = new JBCashOutHookSpecification[](1);
338
- hookSpecifications[0] = feeSpec;
339
- }
340
-
341
- return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
342
- }
343
-
344
- /// @notice Before a revnet processes an incoming payment, determine the weight and pay hooks to use.
345
- /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
346
- /// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
347
- /// @return weight The weight which revnet tokens are minted relative to. This can be used to customize how many
348
- /// tokens get minted by a payment.
349
- /// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
350
- /// into the revnet. Useful for automatically routing funds from a treasury as payments come in.
351
- function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
352
- external
353
- view
354
- override
355
- returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
356
- {
357
- // Get the 721 hook's spec and total split amount.
358
- IJB721TiersHook tiered721Hook = tiered721HookOf[context.projectId];
359
- JBPayHookSpecification memory tiered721HookSpec;
360
- uint256 totalSplitAmount;
361
- bool usesTiered721Hook = address(tiered721Hook) != address(0);
362
- if (usesTiered721Hook) {
363
- JBPayHookSpecification[] memory specs;
364
- // slither-disable-next-line unused-return
365
- (, specs) = IJBRulesetDataHook(address(tiered721Hook)).beforePayRecordedWith(context);
366
- // The 721 hook returns a single spec (itself) whose amount is the total split amount.
367
- if (specs.length > 0) {
368
- tiered721HookSpec = specs[0];
369
- totalSplitAmount = tiered721HookSpec.amount;
370
- }
371
- }
372
-
373
- // The amount entering the project after tier splits.
374
- uint256 projectAmount = totalSplitAmount >= context.amount.value ? 0 : context.amount.value - totalSplitAmount;
375
-
376
- // Get the buyback hook's weight and specs. Reduce the amount so it only considers funds entering the project.
377
- JBPayHookSpecification[] memory buybackHookSpecs;
378
- {
379
- JBBeforePayRecordedContext memory buybackHookContext = context;
380
- buybackHookContext.amount.value = projectAmount;
381
- (weight, buybackHookSpecs) = BUYBACK_HOOK.beforePayRecordedWith(buybackHookContext);
382
- }
383
-
384
- // Scale the buyback hook's weight for splits so the terminal mints tokens only for the project's share.
385
- // The terminal uses the full context.amount.value for minting (tokenCount = amount * weight / weightRatio),
386
- // but only projectAmount actually enters the project. Without scaling, payers get token credit for the split
387
- // portion too. Preserves weight=0 from the buyback hook (buying back, not minting).
388
- if (projectAmount == 0) {
389
- weight = 0;
390
- } else if (projectAmount < context.amount.value) {
391
- weight = mulDiv({x: weight, y: projectAmount, denominator: context.amount.value});
392
- }
393
-
394
- // Merge hook specifications: 721 hook spec first, then buyback hook spec.
395
- bool usesBuybackHook = buybackHookSpecs.length > 0;
396
- hookSpecifications = new JBPayHookSpecification[]((usesTiered721Hook ? 1 : 0) + (usesBuybackHook ? 1 : 0));
397
-
398
- if (usesTiered721Hook) hookSpecifications[0] = tiered721HookSpec;
399
- if (usesBuybackHook) hookSpecifications[usesTiered721Hook ? 1 : 0] = buybackHookSpecs[0];
400
- }
401
-
402
- /// @notice A flag indicating whether an address has permission to mint a revnet's tokens on-demand.
403
- /// @dev Required by the `IJBRulesetDataHook` interface.
404
- /// @param revnetId The ID of the revnet to check permissions for.
405
- /// @param ruleset The ruleset to check the mint permission for.
406
- /// @param addr The address to check the mint permission of.
407
- /// @return flag A flag indicating whether the address has permission to mint the revnet's tokens on-demand.
408
- function hasMintPermissionFor(
409
- uint256 revnetId,
410
- JBRuleset calldata ruleset,
411
- address addr
412
- )
413
- external
414
- view
415
- override
416
- returns (bool)
417
- {
418
- // The loans contract, buyback hook (and its delegates), and suckers are allowed to mint the revnet's tokens.
419
- return addr == LOANS || addr == address(BUYBACK_HOOK)
420
- || BUYBACK_HOOK.hasMintPermissionFor({projectId: revnetId, ruleset: ruleset, addr: addr})
421
- || _isSuckerOf({revnetId: revnetId, addr: addr});
422
- }
423
-
424
219
  /// @dev Make sure this contract can only receive project NFTs from `JBProjects`.
425
220
  function onERC721Received(address, address, uint256, bytes calldata) external view returns (bytes4) {
426
221
  // Make sure the 721 received is from the `JBProjects` contract.
@@ -451,9 +246,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
451
246
  /// @notice Indicates if this contract adheres to the specified interface.
452
247
  /// @dev See `IERC165.supportsInterface`.
453
248
  /// @return A flag indicating if the provided interface ID is supported.
454
- function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
455
- return interfaceId == type(IREVDeployer).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
456
- || interfaceId == type(IJBCashOutHook).interfaceId || interfaceId == type(IERC721Receiver).interfaceId;
249
+ function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
250
+ return interfaceId == type(IREVDeployer).interfaceId || interfaceId == type(IERC721Receiver).interfaceId;
457
251
  }
458
252
 
459
253
  //*********************************************************************//
@@ -469,14 +263,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
469
263
  }
470
264
  }
471
265
 
472
- /// @notice A flag indicating whether an address is a revnet's sucker.
473
- /// @param revnetId The ID of the revnet to check sucker status for.
474
- /// @param addr The address being checked.
475
- /// @return isSucker A flag indicating whether the address is one of the revnet's suckers.
476
- function _isSuckerOf(uint256 revnetId, address addr) internal view returns (bool) {
477
- return SUCKER_REGISTRY.isSuckerOf({projectId: revnetId, addr: addr});
478
- }
479
-
480
266
  /// @notice Initialize fund access limits for the loan contract.
481
267
  /// @dev Returns an unlimited surplus allowance for each terminal+token pair derived from the terminal
482
268
  /// configurations.
@@ -542,7 +328,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
542
328
  metadata.useDataHookForPay = true; // Call this contract's `beforePayRecordedWith(…)` callback on payments.
543
329
  metadata.useDataHookForCashOut = true; // Call this contract's `beforeCashOutRecordedWith(…)` callback on cash
544
330
  // outs.
545
- metadata.dataHook = address(this); // This contract is the data hook.
331
+ metadata.dataHook = OWNER; // The REVOwner contract is the data hook.
546
332
  metadata.metadata = stageConfiguration.extraMetadata;
547
333
 
548
334
  // Package the reserved token splits.
@@ -641,64 +427,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
641
427
  // --------------------- external transactions ----------------------- //
642
428
  //*********************************************************************//
643
429
 
644
- /// @notice Processes the fee from a cash out.
645
- /// @param context Cash out context passed in by the terminal.
646
- function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context) external payable {
647
- // No caller validation needed — this hook only pays fees to the fee project using funds forwarded by the
648
- // caller. A non-terminal caller would just be donating their own funds as fees. There's nothing to exploit.
649
-
650
- // If there's sufficient approval, transfer normally.
651
- if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
652
- IERC20(context.forwardedAmount.token)
653
- .safeTransferFrom({from: msg.sender, to: address(this), value: context.forwardedAmount.value});
654
- }
655
-
656
- // Parse the metadata forwarded from the data hook to get the fee terminal.
657
- // See `beforeCashOutRecordedWith(…)`.
658
- (IJBTerminal feeTerminal) = abi.decode(context.hookMetadata, (IJBTerminal));
659
-
660
- // Determine how much to pay in `msg.value` (in the native currency).
661
- uint256 payValue = _beforeTransferTo({
662
- to: address(feeTerminal), token: context.forwardedAmount.token, amount: context.forwardedAmount.value
663
- });
664
-
665
- // Pay the fee.
666
- // slither-disable-next-line arbitrary-send-eth,unused-return
667
- try feeTerminal.pay{value: payValue}({
668
- projectId: FEE_REVNET_ID,
669
- token: context.forwardedAmount.token,
670
- amount: context.forwardedAmount.value,
671
- beneficiary: context.holder,
672
- minReturnedTokens: 0,
673
- memo: "",
674
- metadata: bytes(abi.encodePacked(context.projectId))
675
- }) {}
676
- catch (bytes memory) {
677
- // Decrease the allowance for the fee terminal if the token is not the native token.
678
- if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
679
- IERC20(context.forwardedAmount.token)
680
- .safeDecreaseAllowance({
681
- spender: address(feeTerminal), requestedDecrease: context.forwardedAmount.value
682
- });
683
- }
684
-
685
- // If the fee can't be processed, return the funds to the project.
686
- payValue = _beforeTransferTo({
687
- to: msg.sender, token: context.forwardedAmount.token, amount: context.forwardedAmount.value
688
- });
689
-
690
- // slither-disable-next-line arbitrary-send-eth
691
- IJBTerminal(msg.sender).addToBalanceOf{value: payValue}({
692
- projectId: context.projectId,
693
- token: context.forwardedAmount.token,
694
- amount: context.forwardedAmount.value,
695
- shouldReturnHeldFees: false,
696
- memo: "",
697
- metadata: bytes(abi.encodePacked(FEE_REVNET_ID))
698
- });
699
- }
700
- }
701
-
702
430
  /// @notice Auto-mint a revnet's tokens from a stage for a beneficiary.
703
431
  /// @param revnetId The ID of the revnet to auto-mint tokens from.
704
432
  /// @param stageId The ID of the stage auto-mint tokens are available from.
@@ -837,8 +565,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
837
565
  });
838
566
  }
839
567
 
840
- // Store the tiered ERC-721 hook.
841
- tiered721HookOf[revnetId] = hook;
568
+ // Store the tiered ERC-721 hook in the owner contract.
569
+ REVOwner(OWNER).setTiered721HookOf(revnetId, hook);
842
570
 
843
571
  // Grant the split operator all 721 permissions (no prevent* flags for default config).
844
572
  _extraOperatorPermissions[revnetId].push(JBPermissionIds.ADJUST_721_TIERS);
@@ -913,19 +641,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
913
641
  // --------------------- internal transactions ----------------------- //
914
642
  //*********************************************************************//
915
643
 
916
- /// @notice Logic to be triggered before transferring tokens from this contract.
917
- /// @param to The address the transfer is going to.
918
- /// @param token The token being transferred.
919
- /// @param amount The number of tokens being transferred, as a fixed point number with the same number of decimals
920
- /// as the token specifies.
921
- /// @return payValue The value to attach to the transaction being sent.
922
- function _beforeTransferTo(address to, address token, uint256 amount) internal returns (uint256) {
923
- // If the token is the native token, no allowance needed.
924
- if (token == JBConstants.NATIVE_TOKEN) return amount;
925
- IERC20(token).safeIncreaseAllowance({spender: to, value: amount});
926
- return 0;
927
- }
928
-
929
644
  /// @notice Deploy a revnet which sells tiered ERC-721s and (optionally) allows croptop posts to its ERC-721 tiers.
930
645
  function _deploy721RevnetFor(
931
646
  uint256 revnetId,
@@ -974,8 +689,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
974
689
  salt: keccak256(abi.encode(tiered721HookConfiguration.salt, encodedConfigurationHash, _msgSender()))
975
690
  });
976
691
 
977
- // Store the tiered ERC-721 hook.
978
- tiered721HookOf[revnetId] = hook;
692
+ // Store the tiered ERC-721 hook in the owner contract.
693
+ REVOwner(OWNER).setTiered721HookOf(revnetId, hook);
979
694
 
980
695
  // Give the split operator permission to add and remove tiers unless prevented.
981
696
  if (!tiered721HookConfiguration.preventSplitOperatorAdjustingTiers) {
@@ -1323,8 +1038,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
1323
1038
  // Calculate the timestamp at which the cash out delay ends.
1324
1039
  uint256 cashOutDelay = block.timestamp + CASH_OUT_DELAY;
1325
1040
 
1326
- // Store the cash out delay.
1327
- cashOutDelayOf[revnetId] = cashOutDelay;
1041
+ // Store the cash out delay in the owner contract.
1042
+ REVOwner(OWNER).setCashOutDelayOf(revnetId, cashOutDelay);
1328
1043
 
1329
1044
  emit SetCashOutDelay({revnetId: revnetId, cashOutDelay: cashOutDelay, caller: _msgSender()});
1330
1045
  }
package/src/REVLoans.sol CHANGED
@@ -27,8 +27,8 @@ 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";
31
30
  import {IREVLoans} from "./interfaces/IREVLoans.sol";
31
+ import {IREVOwner} from "./interfaces/IREVOwner.sol";
32
32
  import {REVLoan} from "./structs/REVLoan.sol";
33
33
  import {REVLoanSource} from "./structs/REVLoanSource.sol";
34
34
 
@@ -227,21 +227,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
227
227
  view
228
228
  returns (uint256)
229
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
- }
230
+ // If the cash out delay hasn't passed yet, no amount is borrowable.
231
+ if (_cashOutDelayOf(revnetId) > block.timestamp) return 0;
245
232
 
246
233
  return _borrowableAmountFrom({
247
234
  revnetId: revnetId,
@@ -303,6 +290,24 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
303
290
  // -------------------------- internal views ------------------------- //
304
291
  //*********************************************************************//
305
292
 
293
+ /// @notice Returns the cash out delay timestamp for a revnet by resolving the data hook from the current ruleset.
294
+ /// @param revnetId The ID of the revnet.
295
+ /// @return The cash out delay timestamp. Returns 0 if no data hook is set or no delay exists.
296
+ function _cashOutDelayOf(uint256 revnetId) internal view returns (uint256) {
297
+ // Get the revnet's current ruleset to find its data hook (the REVOwner contract).
298
+ // slither-disable-next-line unused-return
299
+ (JBRuleset memory currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
300
+
301
+ // Extract the data hook address from the ruleset's packed metadata.
302
+ address dataHook = currentRuleset.dataHook();
303
+
304
+ // If there's no data hook, this isn't a revnet — no cash out delay applies.
305
+ if (dataHook == address(0)) return 0;
306
+
307
+ // Read the cash out delay from the REVOwner contract (the data hook).
308
+ return IREVOwner(dataHook).cashOutDelayOf(revnetId);
309
+ }
310
+
306
311
  /// @notice Checks this contract's balance of a specific token.
307
312
  /// @param token The address of the token to get this contract's balance of.
308
313
  /// @return This contract's balance.
@@ -598,19 +603,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
598
603
  );
599
604
  }
600
605
 
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.
606
+ // Enforce the cash out delay.
607
+ {
608
+ uint256 cashOutDelay = _cashOutDelayOf(revnetId);
614
609
  if (cashOutDelay > block.timestamp) {
615
610
  revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
616
611
  }