@rev-net/core-v6 0.0.8 → 0.0.9

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 (58) hide show
  1. package/ADMINISTRATION.md +186 -0
  2. package/ARCHITECTURE.md +87 -0
  3. package/README.md +4 -2
  4. package/RISKS.md +49 -0
  5. package/SKILLS.md +22 -2
  6. package/STYLE_GUIDE.md +482 -0
  7. package/foundry.toml +6 -6
  8. package/package.json +8 -8
  9. package/script/Deploy.s.sol +3 -2
  10. package/src/REVDeployer.sol +75 -52
  11. package/src/REVLoans.sol +13 -4
  12. package/src/interfaces/IREVDeployer.sol +2 -1
  13. package/src/structs/REV721TiersHookFlags.sol +14 -0
  14. package/src/structs/REVBaseline721HookConfig.sol +27 -0
  15. package/src/structs/REVDeploy721TiersHookConfig.sol +2 -2
  16. package/test/REV.integrations.t.sol +4 -3
  17. package/test/REVAutoIssuanceFuzz.t.sol +12 -8
  18. package/test/REVDeployerAuditRegressions.t.sol +4 -3
  19. package/test/REVInvincibility.t.sol +8 -6
  20. package/test/REVInvincibilityHandler.sol +1 -0
  21. package/test/REVLifecycle.t.sol +4 -3
  22. package/test/REVLoans.invariants.t.sol +5 -3
  23. package/test/REVLoansAttacks.t.sol +4 -3
  24. package/test/REVLoansAuditRegressions.t.sol +13 -24
  25. package/test/REVLoansFeeRecovery.t.sol +4 -3
  26. package/test/REVLoansSourced.t.sol +4 -3
  27. package/test/REVLoansUnSourced.t.sol +4 -3
  28. package/test/REVLoans_AuditFindings.t.sol +644 -0
  29. package/test/TestEmptyBuybackSpecs.t.sol +4 -3
  30. package/test/TestPR09_ConversionDocumentation.t.sol +4 -3
  31. package/test/TestPR10_LiquidationBehavior.t.sol +4 -3
  32. package/test/TestPR11_LowFindings.t.sol +4 -3
  33. package/test/TestPR12_FlashLoanSurplus.t.sol +4 -3
  34. package/test/TestPR13_CrossSourceReallocation.t.sol +4 -3
  35. package/test/TestPR15_CashOutCallerValidation.t.sol +4 -3
  36. package/test/TestPR16_ZeroRepayment.t.sol +4 -3
  37. package/test/TestPR21_Uint112Overflow.t.sol +4 -3
  38. package/test/TestPR22_HookArrayOOB.t.sol +4 -3
  39. package/test/TestPR26_BurnHeldTokens.t.sol +4 -3
  40. package/test/TestPR27_CEIPattern.t.sol +4 -3
  41. package/test/TestPR29_SwapTerminalPermission.t.sol +4 -3
  42. package/test/TestPR32_MixedFixes.t.sol +4 -3
  43. package/test/TestSplitWeightAdjustment.t.sol +445 -0
  44. package/test/TestSplitWeightE2E.t.sol +528 -0
  45. package/test/TestSplitWeightFork.t.sol +821 -0
  46. package/test/TestStageTransitionBorrowable.t.sol +4 -3
  47. package/test/fork/ForkTestBase.sol +617 -0
  48. package/test/fork/TestCashOutFork.t.sol +245 -0
  49. package/test/fork/TestLoanBorrowFork.t.sol +163 -0
  50. package/test/fork/TestLoanLiquidationFork.t.sol +129 -0
  51. package/test/fork/TestLoanReallocateFork.t.sol +103 -0
  52. package/test/fork/TestLoanRepayFork.t.sol +184 -0
  53. package/test/fork/TestSplitWeightFork.t.sol +186 -0
  54. package/test/mock/MockBuybackDataHook.sol +7 -4
  55. package/test/mock/MockBuybackDataHookMintPath.sol +7 -3
  56. package/test/regression/TestI20_CumulativeLoanCounter.t.sol +6 -5
  57. package/test/regression/TestL27_LiquidateGapHandling.t.sol +7 -6
  58. package/SECURITY.md +0 -68
@@ -9,10 +9,9 @@ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Recei
9
9
  import {mulDiv} from "@prb/math/src/Common.sol";
10
10
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
11
11
  import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
12
- import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
13
- import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
14
- import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
15
- import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
12
+ import {JB721TiersHookFlags} from "@bananapus/721-hook-v6/src/structs/JB721TiersHookFlags.sol";
13
+ import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
14
+ import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
16
15
  import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
17
16
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
18
17
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
@@ -40,6 +39,7 @@ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
40
39
  import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
41
40
  import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
42
41
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
42
+ import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
43
43
  import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
44
44
  import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
45
45
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
@@ -111,7 +111,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
111
111
  //*********************************************************************//
112
112
 
113
113
  /// @notice The buyback hook used as a data hook to route payments through buyback pools.
114
- IJBRulesetDataHook public immutable override BUYBACK_HOOK;
114
+ IJBBuybackHookRegistry public immutable override BUYBACK_HOOK;
115
115
 
116
116
  /// @notice The controller used to create and manage Juicebox projects for revnets.
117
117
  IJBController public immutable override CONTROLLER;
@@ -200,7 +200,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
200
200
  uint256 feeRevnetId,
201
201
  IJB721TiersHookDeployer hookDeployer,
202
202
  CTPublisher publisher,
203
- IJBRulesetDataHook buybackHook,
203
+ IJBBuybackHookRegistry buybackHook,
204
204
  address loans,
205
205
  address trustedForwarder
206
206
  )
@@ -225,6 +225,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
225
225
 
226
226
  // Give the loan contract permission to use the surplus allowance of all revnets.
227
227
  _setPermission({operator: LOANS, revnetId: 0, permissionId: JBPermissionIds.USE_ALLOWANCE});
228
+
229
+ // Give the buyback hook (registry) permission to configure pools on all revnets.
230
+ _setPermission({operator: address(BUYBACK_HOOK), revnetId: 0, permissionId: JBPermissionIds.SET_BUYBACK_POOL});
228
231
  }
229
232
 
230
233
  //*********************************************************************//
@@ -316,34 +319,48 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
316
319
  override
317
320
  returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
318
321
  {
319
- // Keep a reference to the specifications provided by the buyback hook.
320
- JBPayHookSpecification[] memory buybackHookSpecifications;
321
-
322
- // Read the weight and specifications from the buyback hook.
323
- (weight, buybackHookSpecifications) = BUYBACK_HOOK.beforePayRecordedWith(context);
324
-
325
- // Keep a reference to the revnet's tiered ERC-721 hook.
322
+ // Get the 721 hook's spec and total split amount.
326
323
  IJB721TiersHook tiered721Hook = tiered721HookOf[context.projectId];
327
-
328
- // Is there a tiered ERC-721 hook?
324
+ JBPayHookSpecification memory tiered721HookSpec;
325
+ uint256 totalSplitAmount;
329
326
  bool usesTiered721Hook = address(tiered721Hook) != address(0);
327
+ if (usesTiered721Hook) {
328
+ JBPayHookSpecification[] memory specs;
329
+ (, specs) = IJBRulesetDataHook(address(tiered721Hook)).beforePayRecordedWith(context);
330
+ // The 721 hook returns a single spec (itself) whose amount is the total split amount.
331
+ if (specs.length > 0) {
332
+ tiered721HookSpec = specs[0];
333
+ totalSplitAmount = tiered721HookSpec.amount;
334
+ }
335
+ }
330
336
 
331
- // Did the buyback hook return any specifications? (It won't when direct minting is cheaper than swapping.)
332
- bool usesBuybackHook = buybackHookSpecifications.length > 0;
333
-
334
- // Initialize the returned specification array with only the hooks that are present.
335
- hookSpecifications = new JBPayHookSpecification[]((usesTiered721Hook ? 1 : 0) + (usesBuybackHook ? 1 : 0));
337
+ // The amount entering the project after tier splits.
338
+ uint256 projectAmount = totalSplitAmount >= context.amount.value ? 0 : context.amount.value - totalSplitAmount;
336
339
 
337
- // If we have a tiered ERC-721 hook, add it to the array.
338
- if (usesTiered721Hook) {
339
- hookSpecifications[0] =
340
- JBPayHookSpecification({hook: IJBPayHook(address(tiered721Hook)), amount: 0, metadata: bytes("")});
340
+ // Get the buyback hook's weight and specs. Reduce the amount so it only considers funds entering the project.
341
+ JBPayHookSpecification[] memory buybackHookSpecs;
342
+ {
343
+ JBBeforePayRecordedContext memory buybackHookContext = context;
344
+ buybackHookContext.amount.value = projectAmount;
345
+ (weight, buybackHookSpecs) = BUYBACK_HOOK.beforePayRecordedWith(buybackHookContext);
341
346
  }
342
347
 
343
- // Add the buyback hook specification if present.
344
- if (usesBuybackHook) {
345
- hookSpecifications[usesTiered721Hook ? 1 : 0] = buybackHookSpecifications[0];
348
+ // Scale the buyback hook's weight for splits so the terminal mints tokens only for the project's share.
349
+ // The terminal uses the full context.amount.value for minting (tokenCount = amount * weight / weightRatio),
350
+ // but only projectAmount actually enters the project. Without scaling, payers get token credit for the split
351
+ // portion too. Preserves weight=0 from the buyback hook (buying back, not minting).
352
+ if (projectAmount == 0) {
353
+ weight = 0;
354
+ } else if (projectAmount < context.amount.value) {
355
+ weight = mulDiv(weight, projectAmount, context.amount.value);
346
356
  }
357
+
358
+ // Merge hook specifications: 721 hook spec first, then buyback hook spec.
359
+ bool usesBuybackHook = buybackHookSpecs.length > 0;
360
+ hookSpecifications = new JBPayHookSpecification[]((usesTiered721Hook ? 1 : 0) + (usesBuybackHook ? 1 : 0));
361
+
362
+ if (usesTiered721Hook) hookSpecifications[0] = tiered721HookSpec;
363
+ if (usesBuybackHook) hookSpecifications[usesTiered721Hook ? 1 : 0] = buybackHookSpecs[0];
347
364
  }
348
365
 
349
366
  /// @notice A flag indicating whether an address has permission to mint a revnet's tokens on-demand.
@@ -477,31 +494,16 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
477
494
  /// @param revnetId The ID of the revnet.
478
495
  /// @param terminalToken The terminal token to configure a buyback pool for.
479
496
  function _trySetBuybackPoolFor(uint256 revnetId, address terminalToken) internal {
480
- // Normalize the terminal token (use WETH for native) and get the project token.
481
- address normalizedTerminalToken = terminalToken == JBConstants.NATIVE_TOKEN
482
- ? address(IJBBuybackHook(address(BUYBACK_HOOK)).WETH())
483
- : terminalToken;
484
- address projectToken = address(CONTROLLER.TOKENS().tokenOf(revnetId));
485
-
486
- // Sort currencies numerically for the pool key (lower address = currency0).
487
- (Currency currency0, Currency currency1) = normalizedTerminalToken < projectToken
488
- ? (Currency.wrap(normalizedTerminalToken), Currency.wrap(projectToken))
489
- : (Currency.wrap(projectToken), Currency.wrap(normalizedTerminalToken));
490
-
491
497
  // Try to set the pool — if the pool isn't initialized in the PoolManager yet, this will revert and be caught.
492
- try IJBBuybackHook(address(BUYBACK_HOOK))
493
- .setPoolFor({
494
- projectId: revnetId,
495
- poolKey: PoolKey({
496
- currency0: currency0,
497
- currency1: currency1,
498
- fee: DEFAULT_BUYBACK_POOL_FEE,
499
- tickSpacing: DEFAULT_BUYBACK_TICK_SPACING,
500
- hooks: IHooks(address(0))
501
- }),
502
- twapWindow: DEFAULT_BUYBACK_TWAP_WINDOW,
503
- terminalToken: terminalToken
504
- }) {}
498
+ // The buyback hook constructs the PoolKey internally from the project token, terminal token, and pool params.
499
+ // slither-disable-next-line calls-loop
500
+ try BUYBACK_HOOK.setPoolFor({
501
+ projectId: revnetId,
502
+ fee: DEFAULT_BUYBACK_POOL_FEE,
503
+ tickSpacing: DEFAULT_BUYBACK_TICK_SPACING,
504
+ twapWindow: DEFAULT_BUYBACK_TWAP_WINDOW,
505
+ terminalToken: terminalToken
506
+ }) {}
505
507
  catch {} // Pool may not be initialized yet — that's OK.
506
508
  }
507
509
 
@@ -886,11 +888,32 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
886
888
  revnetId: revnetId, configuration: configuration, terminalConfigurations: terminalConfigurations
887
889
  });
888
890
 
891
+ // Convert the REVBaseline721HookConfig to JBDeploy721TiersHookConfig, forcing issueTokensForSplits to false.
892
+ // Revnets do their own weight adjustment for splits, so the 721 hook must not also adjust.
893
+ JBDeploy721TiersHookConfig memory hookConfig = JBDeploy721TiersHookConfig({
894
+ name: tiered721HookConfiguration.baseline721HookConfiguration.name,
895
+ symbol: tiered721HookConfiguration.baseline721HookConfiguration.symbol,
896
+ baseUri: tiered721HookConfiguration.baseline721HookConfiguration.baseUri,
897
+ tokenUriResolver: tiered721HookConfiguration.baseline721HookConfiguration.tokenUriResolver,
898
+ contractUri: tiered721HookConfiguration.baseline721HookConfiguration.contractUri,
899
+ tiersConfig: tiered721HookConfiguration.baseline721HookConfiguration.tiersConfig,
900
+ reserveBeneficiary: tiered721HookConfiguration.baseline721HookConfiguration.reserveBeneficiary,
901
+ flags: JB721TiersHookFlags({
902
+ noNewTiersWithReserves: tiered721HookConfiguration.baseline721HookConfiguration.flags
903
+ .noNewTiersWithReserves,
904
+ noNewTiersWithVotes: tiered721HookConfiguration.baseline721HookConfiguration.flags.noNewTiersWithVotes,
905
+ noNewTiersWithOwnerMinting: tiered721HookConfiguration.baseline721HookConfiguration.flags
906
+ .noNewTiersWithOwnerMinting,
907
+ preventOverspending: tiered721HookConfiguration.baseline721HookConfiguration.flags.preventOverspending,
908
+ issueTokensForSplits: false
909
+ })
910
+ });
911
+
889
912
  // Deploy the tiered ERC-721 hook contract.
890
913
  // slither-disable-next-line reentrancy-benign
891
914
  hook = HOOK_DEPLOYER.deployHookFor({
892
915
  projectId: revnetId,
893
- deployTiersHookConfig: tiered721HookConfiguration.baseline721HookConfiguration,
916
+ deployTiersHookConfig: hookConfig,
894
917
  salt: keccak256(abi.encode(tiered721HookConfiguration.salt, encodedConfigurationHash, _msgSender()))
895
918
  });
896
919
 
package/src/REVLoans.sol CHANGED
@@ -57,6 +57,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
57
57
 
58
58
  error REVLoans_CollateralExceedsLoan(uint256 collateralToReturn, uint256 loanCollateral);
59
59
  error REVLoans_InvalidPrepaidFeePercent(uint256 prepaidFeePercent, uint256 min, uint256 max);
60
+ error REVLoans_InvalidTerminal(address terminal, uint256 revnetId);
60
61
  error REVLoans_LoanExpired(uint256 timeSinceLoanCreated, uint256 loanLiquidationDuration);
61
62
  error REVLoans_NewBorrowAmountGreaterThanLoanAmount(uint256 newBorrowAmount, uint256 loanAmount);
62
63
  error REVLoans_NoMsgValueAllowed();
@@ -556,6 +557,11 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
556
557
  // A loan needs to have collateral.
557
558
  if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
558
559
 
560
+ // Make sure the source terminal is registered in the directory for this revnet.
561
+ if (!DIRECTORY.isTerminalOf(revnetId, IJBTerminal(address(source.terminal)))) {
562
+ revert REVLoans_InvalidTerminal(address(source.terminal), revnetId);
563
+ }
564
+
559
565
  // Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
560
566
  // an 16 year loan can be paid upfront with a
561
567
  // payment of 50% of the borrowed assets, the cheapest possible rate.
@@ -1218,6 +1224,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
1218
1224
  // If the loan will carry no more amount or collateral, store its changes directly.
1219
1225
  // slither-disable-next-line incorrect-equality
1220
1226
  if (collateralCountToReturn == loan.collateral) {
1227
+ // Snapshot the loan to memory BEFORE _adjust zeroes the storage pointer.
1228
+ REVLoan memory loanSnapshot = loan;
1229
+
1221
1230
  // Borrow in.
1222
1231
  _adjust({
1223
1232
  loan: loan,
@@ -1228,15 +1237,15 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
1228
1237
  beneficiary: beneficiary
1229
1238
  });
1230
1239
 
1231
- // Snapshot the loan to memory before deleting storage.
1232
- REVLoan memory loanSnapshot = loan;
1240
+ // Snapshot the zeroed loan for the return value (reflects post-repay state).
1241
+ REVLoan memory paidOffSnapshot = loan;
1233
1242
 
1234
1243
  emit RepayLoan({
1235
1244
  loanId: loanId,
1236
1245
  revnetId: revnetId,
1237
1246
  paidOffLoanId: loanId,
1238
1247
  loan: loanSnapshot,
1239
- paidOffLoan: loanSnapshot,
1248
+ paidOffLoan: paidOffSnapshot,
1240
1249
  repayBorrowAmount: repayBorrowAmount,
1241
1250
  sourceFeeAmount: sourceFeeAmount,
1242
1251
  collateralCountToReturn: collateralCountToReturn,
@@ -1247,7 +1256,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
1247
1256
  // Clear stale loan data for gas refund.
1248
1257
  delete _loanOf[loanId];
1249
1258
 
1250
- return (loanId, loanSnapshot);
1259
+ return (loanId, paidOffSnapshot);
1251
1260
  } else {
1252
1261
  // Make a new loan with the remaining amount and collateral.
1253
1262
  // Get a reference to the replacement loan ID.
@@ -7,6 +7,7 @@ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol
7
7
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
8
8
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
9
9
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
10
+ import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
10
11
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
11
12
  import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
12
13
  import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
@@ -96,7 +97,7 @@ interface IREVDeployer {
96
97
 
97
98
  /// @notice The buyback hook used as a data hook to route payments through buyback pools.
98
99
  /// @return The buyback hook contract.
99
- function BUYBACK_HOOK() external view returns (IJBRulesetDataHook);
100
+ function BUYBACK_HOOK() external view returns (IJBBuybackHookRegistry);
100
101
 
101
102
  /// @notice The number of seconds until a revnet's participants can cash out after deploying to a new network.
102
103
  /// @return The cash out delay in seconds.
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /// @custom:member noNewTiersWithReserves A flag indicating if new tiers with non-zero reserve frequency are forbidden.
5
+ /// @custom:member noNewTiersWithVotes A flag indicating if new tiers with voting units are forbidden.
6
+ /// @custom:member noNewTiersWithOwnerMinting A flag indicating if new tiers with owner minting are forbidden.
7
+ /// @custom:member preventOverspending A flag indicating if payments exceeding the price of minted NFTs should be
8
+ /// prevented.
9
+ struct REV721TiersHookFlags {
10
+ bool noNewTiersWithReserves;
11
+ bool noNewTiersWithVotes;
12
+ bool noNewTiersWithOwnerMinting;
13
+ bool preventOverspending;
14
+ }
@@ -0,0 +1,27 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
5
+ import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
6
+
7
+ import {REV721TiersHookFlags} from "./REV721TiersHookFlags.sol";
8
+
9
+ /// @custom:member name The name of the NFT collection.
10
+ /// @custom:member symbol The symbol of the NFT collection.
11
+ /// @custom:member baseUri The base URI for the NFT collection.
12
+ /// @custom:member tokenUriResolver The token URI resolver for the NFT collection.
13
+ /// @custom:member contractUri The contract URI for the NFT collection.
14
+ /// @custom:member tiersConfig The tier configuration for the NFT collection.
15
+ /// @custom:member reserveBeneficiary The default reserve beneficiary for the NFT collection.
16
+ /// @custom:member flags A set of flags that configure the 721 hook. Omits `issueTokensForSplits` since revnets
17
+ /// always force it to `false`.
18
+ struct REVBaseline721HookConfig {
19
+ string name;
20
+ string symbol;
21
+ string baseUri;
22
+ IJB721TokenUriResolver tokenUriResolver;
23
+ string contractUri;
24
+ JB721InitTiersConfig tiersConfig;
25
+ address reserveBeneficiary;
26
+ REV721TiersHookFlags flags;
27
+ }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
4
+ import {REVBaseline721HookConfig} from "./REVBaseline721HookConfig.sol";
5
5
 
6
6
  /// @custom:member baseline721HookConfiguration The baseline config.
7
7
  /// @custom:member salt The salt to base the collection's address on.
@@ -16,7 +16,7 @@ import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBD
16
16
  /// the
17
17
  /// discount of a tier.
18
18
  struct REVDeploy721TiersHookConfig {
19
- JBDeploy721TiersHookConfig baseline721HookConfiguration;
19
+ REVBaseline721HookConfig baseline721HookConfiguration;
20
20
  bytes32 salt;
21
21
  bool splitOperatorCanAdjustTiers;
22
22
  bool splitOperatorCanUpdateMetadata;
@@ -38,7 +38,7 @@ struct FeeProjectConfig {
38
38
  REVSuckerDeploymentConfig suckerDeploymentConfiguration;
39
39
  }
40
40
 
41
- contract REVnet_Integrations is TestBaseWorkflow, JBTest {
41
+ contract REVnet_Integrations is TestBaseWorkflow {
42
42
  /// @notice the salts that are used to deploy the contracts.
43
43
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
44
44
  bytes32 ERC20_SALT = "REV_TOKEN";
@@ -177,7 +177,8 @@ contract REVnet_Integrations is TestBaseWorkflow, JBTest {
177
177
 
178
178
  HOOK_STORE = new JB721TiersHookStore();
179
179
 
180
- EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
180
+ EXAMPLE_HOOK =
181
+ new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
181
182
 
182
183
  ADDRESS_REGISTRY = new JBAddressRegistry();
183
184
 
@@ -192,7 +193,7 @@ contract REVnet_Integrations is TestBaseWorkflow, JBTest {
192
193
  FEE_PROJECT_ID,
193
194
  HOOK_DEPLOYER,
194
195
  PUBLISHER,
195
- IJBRulesetDataHook(address(MOCK_BUYBACK)),
196
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
196
197
  makeAddr("loans"),
197
198
  TRUSTED_FORWARDER
198
199
  );
@@ -30,7 +30,7 @@ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/
30
30
  /// @notice Fuzz tests for REVDeployer multi-stage auto-issuance.
31
31
  /// Tests stage ID computation consistency and multi-stage claiming behavior.
32
32
  /// Stage IDs use block.timestamp + i which may mismatch actual ruleset IDs.
33
- contract REVAutoIssuanceFuzz_Local is TestBaseWorkflow, JBTest {
33
+ contract REVAutoIssuanceFuzz_Local is TestBaseWorkflow {
34
34
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
35
35
 
36
36
  REVDeployer REV_DEPLOYER;
@@ -55,7 +55,8 @@ contract REVAutoIssuanceFuzz_Local is TestBaseWorkflow, JBTest {
55
55
 
56
56
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
57
57
  HOOK_STORE = new JB721TiersHookStore();
58
- EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
58
+ EXAMPLE_HOOK =
59
+ new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
59
60
  ADDRESS_REGISTRY = new JBAddressRegistry();
60
61
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
61
62
  PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
@@ -67,7 +68,7 @@ contract REVAutoIssuanceFuzz_Local is TestBaseWorkflow, JBTest {
67
68
  FEE_PROJECT_ID,
68
69
  HOOK_DEPLOYER,
69
70
  PUBLISHER,
70
- IJBRulesetDataHook(address(MOCK_BUYBACK)),
71
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
71
72
  makeAddr("loans"),
72
73
  TRUSTED_FORWARDER
73
74
  );
@@ -165,9 +166,12 @@ contract REVAutoIssuanceFuzz_Local is TestBaseWorkflow, JBTest {
165
166
 
166
167
  /// @notice Deploy 3-stage revnet, advance time, claim auto-issuance from each stage.
167
168
  function test_multiStage_allStagesClaimable() external {
169
+ // Save the deploy timestamp for absolute warp targets.
170
+ uint256 deployTs = block.timestamp;
171
+
168
172
  (uint256 revnetId, uint256[] memory stageIds) = _deployMultiStageRevnet(3);
169
173
 
170
- // Stage 0 starts at block.timestamp — immediately claimable.
174
+ // Stage 0 starts at deploy time — immediately claimable.
171
175
  REV_DEPLOYER.autoIssueFor(revnetId, stageIds[0], multisig());
172
176
 
173
177
  uint256 stage0Amount = (10_000) * decimalMultiplier;
@@ -175,8 +179,8 @@ contract REVAutoIssuanceFuzz_Local is TestBaseWorkflow, JBTest {
175
179
  IJBToken(jbTokens().tokenOf(revnetId)).balanceOf(multisig()), stage0Amount, "Stage 0 auto-issuance claimed"
176
180
  );
177
181
 
178
- // Stage 1 starts at block.timestamp + 180 days.
179
- vm.warp(block.timestamp + 180 days);
182
+ // Stage 1 starts at deployTs + 180 days.
183
+ vm.warp(deployTs + 180 days);
180
184
  REV_DEPLOYER.autoIssueFor(revnetId, stageIds[1], multisig());
181
185
 
182
186
  uint256 stage1Amount = (11_000) * decimalMultiplier;
@@ -186,8 +190,8 @@ contract REVAutoIssuanceFuzz_Local is TestBaseWorkflow, JBTest {
186
190
  "Stage 1 auto-issuance claimed"
187
191
  );
188
192
 
189
- // Stage 2 starts at block.timestamp + 360 days.
190
- vm.warp(block.timestamp + 180 days);
193
+ // Stage 2 starts at deployTs + 360 days.
194
+ vm.warp(deployTs + 360 days);
191
195
  REV_DEPLOYER.autoIssueFor(revnetId, stageIds[2], multisig());
192
196
 
193
197
  uint256 stage2Amount = (12_000) * decimalMultiplier;
@@ -29,7 +29,7 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
29
29
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
30
30
 
31
31
  /// @notice Regression tests for REVDeployer.
32
- contract REVDeployerRegressions_Local is TestBaseWorkflow, JBTest {
32
+ contract REVDeployerRegressions_Local is TestBaseWorkflow {
33
33
  using JBRulesetMetadataResolver for JBRuleset;
34
34
 
35
35
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
@@ -56,7 +56,8 @@ contract REVDeployerRegressions_Local is TestBaseWorkflow, JBTest {
56
56
 
57
57
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
58
58
  HOOK_STORE = new JB721TiersHookStore();
59
- EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
59
+ EXAMPLE_HOOK =
60
+ new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
60
61
  ADDRESS_REGISTRY = new JBAddressRegistry();
61
62
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
62
63
  PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
@@ -77,7 +78,7 @@ contract REVDeployerRegressions_Local is TestBaseWorkflow, JBTest {
77
78
  FEE_PROJECT_ID,
78
79
  HOOK_DEPLOYER,
79
80
  PUBLISHER,
80
- IJBRulesetDataHook(address(MOCK_BUYBACK)),
81
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
81
82
  address(LOANS_CONTRACT),
82
83
  TRUSTED_FORWARDER
83
84
  );
@@ -53,7 +53,7 @@ struct InvincibilityProjectConfig {
53
53
  // =========================================================================
54
54
  // Section A + B: Property Verification & Economic Tests
55
55
  // =========================================================================
56
- contract REVInvincibility_PropertyTests is TestBaseWorkflow, JBTest {
56
+ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
57
57
  using JBRulesetMetadataResolver for JBRuleset;
58
58
 
59
59
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
@@ -202,7 +202,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow, JBTest {
202
202
 
203
203
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
204
204
  HOOK_STORE = new JB721TiersHookStore();
205
- EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
205
+ EXAMPLE_HOOK =
206
+ new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
206
207
  ADDRESS_REGISTRY = new JBAddressRegistry();
207
208
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
208
209
  PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
@@ -224,7 +225,7 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow, JBTest {
224
225
  FEE_PROJECT_ID,
225
226
  HOOK_DEPLOYER,
226
227
  PUBLISHER,
227
- IJBRulesetDataHook(address(MOCK_BUYBACK)),
228
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
228
229
  address(LOANS_CONTRACT),
229
230
  TRUSTED_FORWARDER
230
231
  );
@@ -922,7 +923,7 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow, JBTest {
922
923
  // =========================================================================
923
924
  // Section C: Invariant Properties (6 invariants)
924
925
  // =========================================================================
925
- contract REVInvincibility_Invariants is StdInvariant, TestBaseWorkflow, JBTest {
926
+ contract REVInvincibility_Invariants is StdInvariant, TestBaseWorkflow {
926
927
  using JBRulesetMetadataResolver for JBRuleset;
927
928
 
928
929
  bytes32 REV_DEPLOYER_SALT = "REVDeployer_INV";
@@ -956,7 +957,8 @@ contract REVInvincibility_Invariants is StdInvariant, TestBaseWorkflow, JBTest {
956
957
 
957
958
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
958
959
  HOOK_STORE = new JB721TiersHookStore();
959
- EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
960
+ EXAMPLE_HOOK =
961
+ new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
960
962
  ADDRESS_REGISTRY = new JBAddressRegistry();
961
963
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
962
964
  PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
@@ -977,7 +979,7 @@ contract REVInvincibility_Invariants is StdInvariant, TestBaseWorkflow, JBTest {
977
979
  FEE_PROJECT_ID,
978
980
  HOOK_DEPLOYER,
979
981
  PUBLISHER,
980
- IJBRulesetDataHook(address(MOCK_BUYBACK)),
982
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
981
983
  address(LOANS_CONTRACT),
982
984
  TRUSTED_FORWARDER
983
985
  );
@@ -11,6 +11,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
11
11
  import {IREVLoans} from "../src/interfaces/IREVLoans.sol";
12
12
  import {REVLoan} from "../src/structs/REVLoan.sol";
13
13
  import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
14
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
14
15
 
15
16
  /// @title REVInvincibilityHandler
16
17
  /// @notice Stateful fuzzing handler for the revnet + loans interaction surface.
@@ -30,7 +30,7 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
30
30
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
31
31
 
32
32
  /// @notice Full revnet lifecycle E2E: deploy 3-stage -> pay -> advance stages -> cash out.
33
- contract REVLifecycle_Local is TestBaseWorkflow, JBTest {
33
+ contract REVLifecycle_Local is TestBaseWorkflow {
34
34
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
35
35
  bytes32 ERC20_SALT = "REV_TOKEN";
36
36
 
@@ -61,7 +61,8 @@ contract REVLifecycle_Local is TestBaseWorkflow, JBTest {
61
61
 
62
62
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
63
63
  HOOK_STORE = new JB721TiersHookStore();
64
- EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
64
+ EXAMPLE_HOOK =
65
+ new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
65
66
  ADDRESS_REGISTRY = new JBAddressRegistry();
66
67
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
67
68
  PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
@@ -82,7 +83,7 @@ contract REVLifecycle_Local is TestBaseWorkflow, JBTest {
82
83
  FEE_PROJECT_ID,
83
84
  HOOK_DEPLOYER,
84
85
  PUBLISHER,
85
- IJBRulesetDataHook(address(MOCK_BUYBACK)),
86
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
86
87
  address(LOANS_CONTRACT),
87
88
  TRUSTED_FORWARDER
88
89
  );
@@ -31,6 +31,7 @@ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
31
31
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
32
32
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
33
33
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
34
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
34
35
 
35
36
  struct FeeProjectConfig {
36
37
  REVConfig configuration;
@@ -252,7 +253,7 @@ contract REVLoansCallHandler is JBTest {
252
253
  }
253
254
  }
254
255
 
255
- contract InvariantREVLoansTests is StdInvariant, TestBaseWorkflow, JBTest {
256
+ contract InvariantREVLoansTests is StdInvariant, TestBaseWorkflow {
256
257
  // A library that parses the packed ruleset metadata into a friendlier format.
257
258
  using JBRulesetMetadataResolver for JBRuleset;
258
259
 
@@ -475,7 +476,8 @@ contract InvariantREVLoansTests is StdInvariant, TestBaseWorkflow, JBTest {
475
476
 
476
477
  HOOK_STORE = new JB721TiersHookStore();
477
478
 
478
- EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
479
+ EXAMPLE_HOOK =
480
+ new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
479
481
 
480
482
  ADDRESS_REGISTRY = new JBAddressRegistry();
481
483
 
@@ -499,7 +501,7 @@ contract InvariantREVLoansTests is StdInvariant, TestBaseWorkflow, JBTest {
499
501
  FEE_PROJECT_ID,
500
502
  HOOK_DEPLOYER,
501
503
  PUBLISHER,
502
- IJBRulesetDataHook(address(MOCK_BUYBACK)),
504
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
503
505
  address(LOANS_CONTRACT),
504
506
  TRUSTED_FORWARDER
505
507
  );
@@ -176,7 +176,7 @@ struct AttackProjectConfig {
176
176
  /// @title REVLoansAttacks
177
177
  /// @notice Attack tests for REVLoans covering uint112 truncation, reentrancy,
178
178
  /// collateral race conditions, liquidation edge cases, and fuzz testing.
179
- contract REVLoansAttacks is TestBaseWorkflow, JBTest {
179
+ contract REVLoansAttacks is TestBaseWorkflow {
180
180
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
181
181
  bytes32 ERC20_SALT = "REV_TOKEN";
182
182
 
@@ -318,7 +318,8 @@ contract REVLoansAttacks is TestBaseWorkflow, JBTest {
318
318
 
319
319
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
320
320
  HOOK_STORE = new JB721TiersHookStore();
321
- EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
321
+ EXAMPLE_HOOK =
322
+ new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
322
323
  ADDRESS_REGISTRY = new JBAddressRegistry();
323
324
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
324
325
  PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
@@ -345,7 +346,7 @@ contract REVLoansAttacks is TestBaseWorkflow, JBTest {
345
346
  FEE_PROJECT_ID,
346
347
  HOOK_DEPLOYER,
347
348
  PUBLISHER,
348
- IJBRulesetDataHook(address(MOCK_BUYBACK)),
349
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
349
350
  address(LOANS_CONTRACT),
350
351
  TRUSTED_FORWARDER
351
352
  );