@rev-net/core-v6 0.0.29 → 0.0.31

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 (77) hide show
  1. package/ADMINISTRATION.md +19 -9
  2. package/ARCHITECTURE.md +3 -0
  3. package/AUDIT_INSTRUCTIONS.md +11 -1
  4. package/CHANGELOG.md +26 -0
  5. package/README.md +1 -0
  6. package/RISKS.md +28 -4
  7. package/SKILLS.md +2 -1
  8. package/USER_JOURNEYS.md +28 -3
  9. package/package.json +8 -8
  10. package/references/operations.md +1 -1
  11. package/script/Deploy.s.sol +26 -4
  12. package/src/REVDeployer.sol +4 -2
  13. package/src/REVHiddenTokens.sol +149 -0
  14. package/src/REVLoans.sol +192 -199
  15. package/src/REVOwner.sol +51 -14
  16. package/src/interfaces/IREVHiddenTokens.sol +53 -0
  17. package/src/interfaces/IREVLoans.sol +8 -6
  18. package/test/REV.integrations.t.sol +12 -2
  19. package/test/REVAutoIssuanceFuzz.t.sol +12 -2
  20. package/test/REVDeployerRegressions.t.sol +14 -3
  21. package/test/REVInvincibility.t.sol +27 -8
  22. package/test/REVInvincibilityHandler.sol +1 -1
  23. package/test/REVLifecycle.t.sol +14 -3
  24. package/test/REVLoans.invariants.t.sol +15 -4
  25. package/test/REVLoansAttacks.t.sol +19 -7
  26. package/test/REVLoansFeeRecovery.t.sol +24 -13
  27. package/test/REVLoansFindings.t.sol +16 -5
  28. package/test/REVLoansRegressions.t.sol +15 -4
  29. package/test/REVLoansSourceFeeRecovery.t.sol +16 -5
  30. package/test/REVLoansSourced.t.sol +60 -25
  31. package/test/REVLoansUnSourced.t.sol +15 -4
  32. package/test/TestBurnHeldTokens.t.sol +14 -3
  33. package/test/TestCEIPattern.t.sol +19 -7
  34. package/test/TestCashOutCallerValidation.t.sol +15 -4
  35. package/test/TestConversionDocumentation.t.sol +14 -3
  36. package/test/TestCrossCurrencyReclaim.t.sol +14 -3
  37. package/test/TestCrossSourceReallocation.t.sol +15 -4
  38. package/test/TestERC2771MetaTx.t.sol +18 -5
  39. package/test/TestEmptyBuybackSpecs.t.sol +14 -3
  40. package/test/TestFlashLoanSurplus.t.sol +15 -4
  41. package/test/TestHiddenTokens.t.sol +431 -0
  42. package/test/TestHookArrayOOB.t.sol +14 -3
  43. package/test/TestLiquidationBehavior.t.sol +16 -5
  44. package/test/TestLoanSourceRotation.t.sol +20 -7
  45. package/test/TestLoansCashOutDelay.t.sol +18 -7
  46. package/test/TestLongTailEconomics.t.sol +14 -3
  47. package/test/TestLowFindings.t.sol +25 -9
  48. package/test/TestMixedFixes.t.sol +19 -8
  49. package/test/TestPermit2Signatures.t.sol +15 -4
  50. package/test/TestReallocationSandwich.t.sol +16 -4
  51. package/test/TestRevnetRegressions.t.sol +16 -5
  52. package/test/TestSplitWeightAdjustment.t.sol +16 -4
  53. package/test/TestSplitWeightE2E.t.sol +18 -4
  54. package/test/TestSplitWeightFork.t.sol +16 -3
  55. package/test/TestStageTransitionBorrowable.t.sol +14 -3
  56. package/test/TestSwapTerminalPermission.t.sol +14 -3
  57. package/test/TestUint112Overflow.t.sol +15 -4
  58. package/test/TestZeroAmountLoanGuard.t.sol +15 -4
  59. package/test/TestZeroRepayment.t.sol +15 -4
  60. package/test/audit/CodexPhantomSurplusTerminal.t.sol +367 -0
  61. package/test/audit/LoanIdOverflowGuard.t.sol +16 -5
  62. package/test/audit/NemesisOperatorDelegation.t.sol +289 -0
  63. package/test/fork/ForkTestBase.sol +18 -4
  64. package/test/fork/TestLoanBorrowFork.t.sol +2 -1
  65. package/test/fork/TestLoanERC20Fork.t.sol +4 -2
  66. package/test/fork/TestLoanTransferFork.t.sol +12 -2
  67. package/test/helpers/MaliciousContracts.sol +1 -1
  68. package/test/mock/MockBuybackCashOutRecorder.sol +2 -0
  69. package/test/mock/MockBuybackDataHook.sol +3 -1
  70. package/test/mock/MockBuybackDataHookMintPath.sol +2 -0
  71. package/test/mock/MockSuckerRegistry.sol +17 -0
  72. package/test/regression/TestBurnPermissionRequired.t.sol +16 -5
  73. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +16 -3
  74. package/test/regression/TestCrossRevnetLiquidation.t.sol +14 -3
  75. package/test/regression/TestCumulativeLoanCounter.t.sol +15 -4
  76. package/test/regression/TestLiquidateGapHandling.t.sol +15 -4
  77. package/test/regression/TestZeroPriceFeed.t.sol +17 -6
@@ -36,11 +36,14 @@ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
36
36
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
37
37
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
38
38
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
39
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
40
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
39
41
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
40
42
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
41
43
  import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
42
44
  import {REVOwner} from "../src/REVOwner.sol";
43
45
  import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
46
+ import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
44
47
 
45
48
  /// @notice Tests for PR #13: cross-source reallocation prevention.
46
49
  contract TestCrossSourceReallocation is TestBaseWorkflow {
@@ -86,7 +89,14 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
86
89
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
87
90
  HOOK_STORE = new JB721TiersHookStore();
88
91
  EXAMPLE_HOOK = new JB721TiersHook(
89
- jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
92
+ jbDirectory(),
93
+ jbPermissions(),
94
+ jbPrices(),
95
+ jbRulesets(),
96
+ HOOK_STORE,
97
+ jbSplits(),
98
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
99
+ multisig()
90
100
  );
91
101
  ADDRESS_REGISTRY = new JBAddressRegistry();
92
102
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
@@ -99,7 +109,7 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
99
109
  .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
100
110
  LOANS_CONTRACT = new REVLoans({
101
111
  controller: jbController(),
102
- projects: jbProjects(),
112
+ suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
103
113
  revId: FEE_PROJECT_ID,
104
114
  owner: address(this),
105
115
  permit2: permit2(),
@@ -110,7 +120,8 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
110
120
  jbDirectory(),
111
121
  FEE_PROJECT_ID,
112
122
  SUCKER_REGISTRY,
113
- address(LOANS_CONTRACT)
123
+ address(LOANS_CONTRACT),
124
+ address(0)
114
125
  );
115
126
 
116
127
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -246,7 +257,7 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
246
257
  );
247
258
  REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
248
259
  vm.prank(user);
249
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee);
260
+ (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
250
261
  }
251
262
 
252
263
  /// @notice Reallocating with the same source (token + terminal) should succeed.
@@ -38,6 +38,8 @@ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
38
38
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
39
39
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
40
40
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
41
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
42
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
41
43
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
42
44
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
43
45
  import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
@@ -45,6 +47,7 @@ import {ERC2771Forwarder} from "@openzeppelin/contracts/metatx/ERC2771Forwarder.
45
47
  import {ERC2771ForwarderMock, ForwardRequest} from "@bananapus/core-v6/test/mock/ERC2771ForwarderMock.sol";
46
48
  import {REVOwner} from "../src/REVOwner.sol";
47
49
  import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
50
+ import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
48
51
 
49
52
  struct MetaTxProjectConfig {
50
53
  REVConfig configuration;
@@ -275,7 +278,14 @@ contract TestERC2771MetaTx is TestBaseWorkflow {
275
278
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
276
279
  HOOK_STORE = new JB721TiersHookStore();
277
280
  EXAMPLE_HOOK = new JB721TiersHook(
278
- jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
281
+ jbDirectory(),
282
+ jbPermissions(),
283
+ jbPrices(),
284
+ jbRulesets(),
285
+ HOOK_STORE,
286
+ jbSplits(),
287
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
288
+ multisig()
279
289
  );
280
290
  ADDRESS_REGISTRY = new JBAddressRegistry();
281
291
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
@@ -291,7 +301,7 @@ contract TestERC2771MetaTx is TestBaseWorkflow {
291
301
  // Deploy LOANS_CONTRACT with the forwarder as trusted forwarder.
292
302
  LOANS_CONTRACT = new REVLoans({
293
303
  controller: jbController(),
294
- projects: jbProjects(),
304
+ suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
295
305
  revId: FEE_PROJECT_ID,
296
306
  owner: address(this),
297
307
  permit2: permit2(),
@@ -303,7 +313,8 @@ contract TestERC2771MetaTx is TestBaseWorkflow {
303
313
  jbDirectory(),
304
314
  FEE_PROJECT_ID,
305
315
  SUCKER_REGISTRY,
306
- address(LOANS_CONTRACT)
316
+ address(LOANS_CONTRACT),
317
+ address(0)
307
318
  );
308
319
 
309
320
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -400,7 +411,8 @@ contract TestERC2771MetaTx is TestBaseWorkflow {
400
411
  0, // minBorrowAmount
401
412
  tokenCount,
402
413
  payable(signerAddr),
403
- uint256(25) // MIN_PREPAID_FEE_PERCENT
414
+ uint256(25), // MIN_PREPAID_FEE_PERCENT
415
+ signerAddr // holder
404
416
  );
405
417
 
406
418
  // Build the forwarded request signed by the signer.
@@ -448,7 +460,8 @@ contract TestERC2771MetaTx is TestBaseWorkflow {
448
460
  REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
449
461
 
450
462
  vm.prank(signerAddr);
451
- (uint256 loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(signerAddr), 25);
463
+ (uint256 loanId,) =
464
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(signerAddr), 25, signerAddr);
452
465
 
453
466
  REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
454
467
  assertTrue(loan.amount > 0, "Loan should exist");
@@ -31,6 +31,8 @@ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
31
31
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
32
32
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
33
33
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
34
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
35
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
34
36
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
35
37
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
36
38
  import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
@@ -39,6 +41,7 @@ import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
39
41
  import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
40
42
  import {REVOwner} from "../src/REVOwner.sol";
41
43
  import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
44
+ import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
42
45
 
43
46
  /// @notice Regression tests for the empty buyback hook specifications fix.
44
47
  /// When JBBuybackHook determines minting is cheaper than swapping, it returns an empty
@@ -82,7 +85,14 @@ contract TestEmptyBuybackSpecs is TestBaseWorkflow {
82
85
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
83
86
  HOOK_STORE = new JB721TiersHookStore();
84
87
  EXAMPLE_HOOK = new JB721TiersHook(
85
- jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
88
+ jbDirectory(),
89
+ jbPermissions(),
90
+ jbPrices(),
91
+ jbRulesets(),
92
+ HOOK_STORE,
93
+ jbSplits(),
94
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
95
+ multisig()
86
96
  );
87
97
  ADDRESS_REGISTRY = new JBAddressRegistry();
88
98
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
@@ -90,7 +100,7 @@ contract TestEmptyBuybackSpecs is TestBaseWorkflow {
90
100
  MOCK_BUYBACK_MINT_PATH = new MockBuybackDataHookMintPath();
91
101
  LOANS_CONTRACT = new REVLoans({
92
102
  controller: jbController(),
93
- projects: jbProjects(),
103
+ suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
94
104
  revId: FEE_PROJECT_ID,
95
105
  owner: address(this),
96
106
  permit2: permit2(),
@@ -101,7 +111,8 @@ contract TestEmptyBuybackSpecs is TestBaseWorkflow {
101
111
  jbDirectory(),
102
112
  FEE_PROJECT_ID,
103
113
  SUCKER_REGISTRY,
104
- address(LOANS_CONTRACT)
114
+ address(LOANS_CONTRACT),
115
+ address(0)
105
116
  );
106
117
 
107
118
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -36,10 +36,13 @@ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
36
36
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
37
37
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
38
38
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
39
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
40
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
39
41
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
40
42
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
41
43
  import {REVOwner} from "../src/REVOwner.sol";
42
44
  import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
45
+ import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
43
46
 
44
47
  /// @notice Tests showing that flash loan surplus manipulation is economically unprofitable.
45
48
  contract TestFlashLoanSurplus is TestBaseWorkflow {
@@ -89,7 +92,14 @@ contract TestFlashLoanSurplus is TestBaseWorkflow {
89
92
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
90
93
  HOOK_STORE = new JB721TiersHookStore();
91
94
  EXAMPLE_HOOK = new JB721TiersHook(
92
- jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
95
+ jbDirectory(),
96
+ jbPermissions(),
97
+ jbPrices(),
98
+ jbRulesets(),
99
+ HOOK_STORE,
100
+ jbSplits(),
101
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
102
+ multisig()
93
103
  );
94
104
  ADDRESS_REGISTRY = new JBAddressRegistry();
95
105
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
@@ -102,7 +112,7 @@ contract TestFlashLoanSurplus is TestBaseWorkflow {
102
112
  .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
103
113
  LOANS_CONTRACT = new REVLoans({
104
114
  controller: jbController(),
105
- projects: jbProjects(),
115
+ suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
106
116
  revId: FEE_PROJECT_ID,
107
117
  owner: address(this),
108
118
  permit2: permit2(),
@@ -113,7 +123,8 @@ contract TestFlashLoanSurplus is TestBaseWorkflow {
113
123
  jbDirectory(),
114
124
  FEE_PROJECT_ID,
115
125
  SUCKER_REGISTRY,
116
- address(LOANS_CONTRACT)
126
+ address(LOANS_CONTRACT),
127
+ address(0)
117
128
  );
118
129
 
119
130
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -250,7 +261,7 @@ contract TestFlashLoanSurplus is TestBaseWorkflow {
250
261
  );
251
262
  REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
252
263
  vm.prank(user);
253
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee);
264
+ (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
254
265
  }
255
266
 
256
267
  /// @notice Donate ETH via addToBalanceOf, then borrow. The donation costs more than the extra borrowable amount.
@@ -0,0 +1,431 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "forge-std/Test.sol";
6
+ // forge-lint: disable-next-line(unaliased-plain-import)
7
+ import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
8
+ // forge-lint: disable-next-line(unaliased-plain-import)
9
+ import /* {*} from */ "./../src/REVDeployer.sol";
10
+ // forge-lint: disable-next-line(unaliased-plain-import)
11
+ import "@croptop/core-v6/src/CTPublisher.sol";
12
+ import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
13
+ import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
14
+ // forge-lint: disable-next-line(unaliased-plain-import)
15
+ import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
16
+ // forge-lint: disable-next-line(unaliased-plain-import)
17
+ import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
18
+ // forge-lint: disable-next-line(unaliased-plain-import)
19
+ import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
20
+ // forge-lint: disable-next-line(unaliased-plain-import)
21
+ import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
22
+ // forge-lint: disable-next-line(unaliased-plain-import)
23
+ import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
24
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
25
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
26
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
27
+ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
28
+ import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
29
+ import {REVLoans} from "../src/REVLoans.sol";
30
+ import {REVHiddenTokens} from "../src/REVHiddenTokens.sol";
31
+ import {IREVHiddenTokens} from "../src/interfaces/IREVHiddenTokens.sol";
32
+ import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
33
+ import {REVDescription} from "../src/structs/REVDescription.sol";
34
+ import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
35
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
36
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
37
+ import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
38
+ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
39
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
40
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
41
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
42
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
43
+ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
44
+ import {REVOwner} from "../src/REVOwner.sol";
45
+ import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
46
+ import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
47
+
48
+ /// @notice Tests for the standalone REVHiddenTokens contract.
49
+ contract TestHiddenTokens is TestBaseWorkflow {
50
+ // forge-lint: disable-next-line(mixed-case-variable)
51
+ bytes32 REV_DEPLOYER_SALT = "REVDeployer";
52
+ // forge-lint: disable-next-line(mixed-case-variable)
53
+ bytes32 ERC20_SALT = "REV_TOKEN";
54
+
55
+ // forge-lint: disable-next-line(mixed-case-variable)
56
+ REVDeployer REV_DEPLOYER;
57
+ // forge-lint: disable-next-line(mixed-case-variable)
58
+ REVOwner REV_OWNER;
59
+ // forge-lint: disable-next-line(mixed-case-variable)
60
+ JB721TiersHook EXAMPLE_HOOK;
61
+ // forge-lint: disable-next-line(mixed-case-variable)
62
+ IJB721TiersHookDeployer HOOK_DEPLOYER;
63
+ // forge-lint: disable-next-line(mixed-case-variable)
64
+ IJB721TiersHookStore HOOK_STORE;
65
+ // forge-lint: disable-next-line(mixed-case-variable)
66
+ IJBAddressRegistry ADDRESS_REGISTRY;
67
+ // forge-lint: disable-next-line(mixed-case-variable)
68
+ IREVLoans LOANS_CONTRACT;
69
+ // forge-lint: disable-next-line(mixed-case-variable)
70
+ REVHiddenTokens HIDDEN_TOKENS;
71
+ // forge-lint: disable-next-line(mixed-case-variable)
72
+ IJBSuckerRegistry SUCKER_REGISTRY;
73
+ // forge-lint: disable-next-line(mixed-case-variable)
74
+ CTPublisher PUBLISHER;
75
+ // forge-lint: disable-next-line(mixed-case-variable)
76
+ MockBuybackDataHook MOCK_BUYBACK;
77
+
78
+ // forge-lint: disable-next-line(mixed-case-variable)
79
+ uint256 FEE_PROJECT_ID;
80
+ // forge-lint: disable-next-line(mixed-case-variable)
81
+ uint256 REVNET_ID;
82
+
83
+ // forge-lint: disable-next-line(mixed-case-variable)
84
+ address USER = makeAddr("user");
85
+ // forge-lint: disable-next-line(mixed-case-variable)
86
+ address BENEFICIARY = makeAddr("beneficiary");
87
+
88
+ address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
89
+
90
+ function setUp() public override {
91
+ super.setUp();
92
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
93
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
94
+ HOOK_STORE = new JB721TiersHookStore();
95
+ EXAMPLE_HOOK = new JB721TiersHook(
96
+ jbDirectory(),
97
+ jbPermissions(),
98
+ jbPrices(),
99
+ jbRulesets(),
100
+ HOOK_STORE,
101
+ jbSplits(),
102
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
103
+ multisig()
104
+ );
105
+ ADDRESS_REGISTRY = new JBAddressRegistry();
106
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
107
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
108
+ MOCK_BUYBACK = new MockBuybackDataHook();
109
+
110
+ LOANS_CONTRACT = new REVLoans({
111
+ controller: jbController(),
112
+ suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
113
+ revId: FEE_PROJECT_ID,
114
+ owner: address(this),
115
+ permit2: permit2(),
116
+ trustedForwarder: TRUSTED_FORWARDER
117
+ });
118
+
119
+ HIDDEN_TOKENS = new REVHiddenTokens(jbController(), TRUSTED_FORWARDER);
120
+
121
+ REV_OWNER = new REVOwner(
122
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
123
+ jbDirectory(),
124
+ FEE_PROJECT_ID,
125
+ SUCKER_REGISTRY,
126
+ address(LOANS_CONTRACT),
127
+ address(HIDDEN_TOKENS)
128
+ );
129
+
130
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
131
+ jbController(),
132
+ SUCKER_REGISTRY,
133
+ FEE_PROJECT_ID,
134
+ HOOK_DEPLOYER,
135
+ PUBLISHER,
136
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
137
+ address(LOANS_CONTRACT),
138
+ TRUSTED_FORWARDER,
139
+ address(REV_OWNER)
140
+ );
141
+
142
+ REV_OWNER.setDeployer(REV_DEPLOYER);
143
+
144
+ vm.prank(multisig());
145
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
146
+ _deployFeeProject();
147
+ REVNET_ID = _deployRevnet();
148
+ vm.deal(USER, 100e18);
149
+ _grantBurnPermission(USER, REVNET_ID);
150
+ }
151
+
152
+ // ──────────────────── Test: Hiding reduces totalSupply
153
+ // ────────────────────
154
+
155
+ function test_hideTokens_reducesTotalSupply() public {
156
+ // Pay to get tokens.
157
+ uint256 payAmount = 10e18;
158
+ vm.prank(USER);
159
+ jbMultiTerminal().pay{value: payAmount}({
160
+ projectId: REVNET_ID,
161
+ token: JBConstants.NATIVE_TOKEN,
162
+ amount: payAmount,
163
+ beneficiary: USER,
164
+ minReturnedTokens: 0,
165
+ memo: "",
166
+ metadata: ""
167
+ });
168
+
169
+ uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
170
+ assertGt(userTokens, 0, "User should have tokens after paying");
171
+
172
+ uint256 totalSupplyBefore = jbController().TOKENS().totalSupplyOf(REVNET_ID);
173
+
174
+ // Hide half the tokens.
175
+ uint256 hideCount = userTokens / 2;
176
+ vm.prank(USER);
177
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hideCount, USER);
178
+
179
+ uint256 totalSupplyAfter = jbController().TOKENS().totalSupplyOf(REVNET_ID);
180
+ assertEq(totalSupplyAfter, totalSupplyBefore - hideCount, "Total supply should decrease by hidden amount");
181
+ assertEq(HIDDEN_TOKENS.hiddenBalanceOf(USER, REVNET_ID), hideCount, "Hidden balance should match");
182
+ assertEq(HIDDEN_TOKENS.totalHiddenOf(REVNET_ID), hideCount, "Total hidden should match");
183
+ }
184
+
185
+ // ──────────────────── Test: Revealing restores tokens
186
+ // ────────────────────
187
+
188
+ function test_revealTokens_restoresTokens() public {
189
+ // Pay to get tokens.
190
+ uint256 payAmount = 10e18;
191
+ vm.prank(USER);
192
+ jbMultiTerminal().pay{value: payAmount}({
193
+ projectId: REVNET_ID,
194
+ token: JBConstants.NATIVE_TOKEN,
195
+ amount: payAmount,
196
+ beneficiary: USER,
197
+ minReturnedTokens: 0,
198
+ memo: "",
199
+ metadata: ""
200
+ });
201
+
202
+ uint256 userTokensBefore = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
203
+ uint256 totalSupplyBefore = jbController().TOKENS().totalSupplyOf(REVNET_ID);
204
+
205
+ // Hide tokens.
206
+ uint256 hideCount = userTokensBefore / 2;
207
+ vm.prank(USER);
208
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hideCount, USER);
209
+
210
+ // Reveal tokens to beneficiary.
211
+ vm.prank(USER);
212
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hideCount, BENEFICIARY, USER);
213
+
214
+ uint256 totalSupplyAfter = jbController().TOKENS().totalSupplyOf(REVNET_ID);
215
+ assertEq(totalSupplyAfter, totalSupplyBefore, "Total supply should be restored");
216
+ assertEq(HIDDEN_TOKENS.hiddenBalanceOf(USER, REVNET_ID), 0, "Hidden balance should be zero");
217
+ assertEq(HIDDEN_TOKENS.totalHiddenOf(REVNET_ID), 0, "Total hidden should be zero");
218
+ assertEq(
219
+ jbController().TOKENS().totalBalanceOf(BENEFICIARY, REVNET_ID),
220
+ hideCount,
221
+ "Beneficiary should receive tokens"
222
+ );
223
+ }
224
+
225
+ // ──────────────────── Test: Insufficient hidden balance reverts
226
+ // ────────────────────
227
+
228
+ function test_revealTokens_revertsOnInsufficientBalance() public {
229
+ // Pay to get tokens.
230
+ uint256 payAmount = 10e18;
231
+ vm.prank(USER);
232
+ jbMultiTerminal().pay{value: payAmount}({
233
+ projectId: REVNET_ID,
234
+ token: JBConstants.NATIVE_TOKEN,
235
+ amount: payAmount,
236
+ beneficiary: USER,
237
+ minReturnedTokens: 0,
238
+ memo: "",
239
+ metadata: ""
240
+ });
241
+
242
+ uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
243
+ uint256 hideCount = userTokens / 4;
244
+
245
+ // Hide some tokens.
246
+ vm.prank(USER);
247
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hideCount, USER);
248
+
249
+ // Try to reveal more than hidden — should revert.
250
+ vm.prank(USER);
251
+ vm.expectRevert(
252
+ abi.encodeWithSelector(
253
+ REVHiddenTokens.REVHiddenTokens_InsufficientHiddenBalance.selector, hideCount, hideCount + 1
254
+ )
255
+ );
256
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hideCount + 1, USER, USER);
257
+ }
258
+
259
+ // ──────────────────── Test: Hidden tokens inflate cash out rate
260
+ // ────────────────────
261
+
262
+ function test_hiddenTokens_inflateCashOutRate() public {
263
+ // Pay to get tokens for 2 users.
264
+ uint256 payAmount = 10e18;
265
+ vm.prank(USER);
266
+ jbMultiTerminal().pay{value: payAmount}({
267
+ projectId: REVNET_ID,
268
+ token: JBConstants.NATIVE_TOKEN,
269
+ amount: payAmount,
270
+ beneficiary: USER,
271
+ minReturnedTokens: 0,
272
+ memo: "",
273
+ metadata: ""
274
+ });
275
+
276
+ uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
277
+
278
+ // Hide half the user's tokens.
279
+ uint256 hideCount = userTokens / 2;
280
+ vm.prank(USER);
281
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hideCount, USER);
282
+
283
+ // The remaining tokens now represent a larger share of totalSupply.
284
+ uint256 totalSupply = jbController().TOKENS().totalSupplyOf(REVNET_ID);
285
+ uint256 remainingBalance = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
286
+ assertEq(remainingBalance, userTokens - hideCount, "Remaining balance should be half");
287
+ assertEq(totalSupply, userTokens - hideCount, "Total supply should equal remaining balance");
288
+ }
289
+
290
+ // ──────────────────── Test: Events emitted correctly
291
+ // ────────────────────
292
+
293
+ function test_hideTokens_emitsEvent() public {
294
+ uint256 payAmount = 10e18;
295
+ vm.prank(USER);
296
+ jbMultiTerminal().pay{value: payAmount}({
297
+ projectId: REVNET_ID,
298
+ token: JBConstants.NATIVE_TOKEN,
299
+ amount: payAmount,
300
+ beneficiary: USER,
301
+ minReturnedTokens: 0,
302
+ memo: "",
303
+ metadata: ""
304
+ });
305
+
306
+ uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
307
+
308
+ vm.prank(USER);
309
+ vm.expectEmit(true, false, false, true);
310
+ emit IREVHiddenTokens.HideTokens(REVNET_ID, userTokens, USER, USER);
311
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, userTokens, USER);
312
+ }
313
+
314
+ function test_revealTokens_emitsEvent() public {
315
+ uint256 payAmount = 10e18;
316
+ vm.prank(USER);
317
+ jbMultiTerminal().pay{value: payAmount}({
318
+ projectId: REVNET_ID,
319
+ token: JBConstants.NATIVE_TOKEN,
320
+ amount: payAmount,
321
+ beneficiary: USER,
322
+ minReturnedTokens: 0,
323
+ memo: "",
324
+ metadata: ""
325
+ });
326
+
327
+ uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
328
+
329
+ vm.prank(USER);
330
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, userTokens, USER);
331
+
332
+ vm.prank(USER);
333
+ vm.expectEmit(true, false, false, true);
334
+ emit IREVHiddenTokens.RevealTokens(REVNET_ID, userTokens, BENEFICIARY, USER, USER);
335
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, userTokens, BENEFICIARY, USER);
336
+ }
337
+
338
+ // ──────────────────── Internal helpers
339
+ // ────────────────────
340
+
341
+ function _grantBurnPermission(address account, uint256 revnetId) internal {
342
+ uint8[] memory permissionIds = new uint8[](1);
343
+ permissionIds[0] = JBPermissionIds.BURN_TOKENS;
344
+ JBPermissionsData memory permissionsData = JBPermissionsData({
345
+ operator: address(HIDDEN_TOKENS),
346
+ // forge-lint: disable-next-line(unsafe-typecast)
347
+ projectId: uint56(revnetId),
348
+ permissionIds: permissionIds
349
+ });
350
+ vm.prank(account);
351
+ jbPermissions().setPermissionsFor(account, permissionsData);
352
+ }
353
+
354
+ function _deployFeeProject() internal {
355
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
356
+ acc[0] = JBAccountingContext({
357
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
358
+ });
359
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
360
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
361
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
362
+ stages[0] = REVStageConfig({
363
+ startsAtOrAfter: uint48(block.timestamp),
364
+ autoIssuances: new REVAutoIssuance[](0),
365
+ splitPercent: 0,
366
+ splits: new JBSplit[](0),
367
+ initialIssuance: uint112(1000e18),
368
+ issuanceCutFrequency: 0,
369
+ issuanceCutPercent: 0,
370
+ cashOutTaxRate: 0,
371
+ extraMetadata: 0
372
+ });
373
+ // forge-lint: disable-next-line(named-struct-fields)
374
+ REVConfig memory feeConfig = REVConfig({
375
+ description: REVDescription("Fee Revnet", "FEE", "", ERC20_SALT),
376
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
377
+ splitOperator: multisig(),
378
+ stageConfigurations: stages
379
+ });
380
+ vm.prank(multisig());
381
+ REV_DEPLOYER.deployFor({
382
+ revnetId: FEE_PROJECT_ID,
383
+ configuration: feeConfig,
384
+ terminalConfigurations: tc,
385
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
386
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
387
+ }),
388
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
389
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
390
+ });
391
+ }
392
+
393
+ function _deployRevnet() internal returns (uint256) {
394
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
395
+ acc[0] = JBAccountingContext({
396
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
397
+ });
398
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
399
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
400
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
401
+ stages[0] = REVStageConfig({
402
+ startsAtOrAfter: uint48(block.timestamp),
403
+ autoIssuances: new REVAutoIssuance[](0),
404
+ splitPercent: 0,
405
+ splits: new JBSplit[](0),
406
+ initialIssuance: uint112(1000e18),
407
+ issuanceCutFrequency: 0,
408
+ issuanceCutPercent: 0,
409
+ cashOutTaxRate: 5000, // 50% cash out tax rate
410
+ extraMetadata: 0
411
+ });
412
+ // forge-lint: disable-next-line(named-struct-fields)
413
+ REVConfig memory revConfig = REVConfig({
414
+ description: REVDescription("Test Revnet", "TEST", "", bytes32("TEST_TOKEN")),
415
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
416
+ splitOperator: multisig(),
417
+ stageConfigurations: stages
418
+ });
419
+ (uint256 revnetId,) = REV_DEPLOYER.deployFor({
420
+ revnetId: 0,
421
+ configuration: revConfig,
422
+ terminalConfigurations: tc,
423
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
424
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
425
+ }),
426
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
427
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
428
+ });
429
+ return revnetId;
430
+ }
431
+ }