@rev-net/core-v6 0.0.29 → 0.0.30

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 (71) 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 +17 -3
  9. package/package.json +2 -2
  10. package/references/operations.md +1 -1
  11. package/script/Deploy.s.sol +25 -6
  12. package/src/REVHiddenTokens.sol +149 -0
  13. package/src/REVLoans.sol +115 -144
  14. package/src/REVOwner.sol +11 -3
  15. package/src/interfaces/IREVHiddenTokens.sol +53 -0
  16. package/src/interfaces/IREVLoans.sol +3 -6
  17. package/test/REV.integrations.t.sol +2 -1
  18. package/test/REVAutoIssuanceFuzz.t.sol +2 -1
  19. package/test/REVDeployerRegressions.t.sol +2 -2
  20. package/test/REVInvincibility.t.sol +6 -6
  21. package/test/REVInvincibilityHandler.sol +1 -1
  22. package/test/REVLifecycle.t.sol +2 -2
  23. package/test/REVLoans.invariants.t.sol +3 -3
  24. package/test/REVLoansAttacks.t.sol +7 -6
  25. package/test/REVLoansFeeRecovery.t.sol +12 -12
  26. package/test/REVLoansFindings.t.sol +4 -4
  27. package/test/REVLoansRegressions.t.sol +3 -3
  28. package/test/REVLoansSourceFeeRecovery.t.sol +4 -4
  29. package/test/REVLoansSourced.t.sol +48 -24
  30. package/test/REVLoansUnSourced.t.sol +3 -3
  31. package/test/TestBurnHeldTokens.t.sol +2 -2
  32. package/test/TestCEIPattern.t.sol +7 -6
  33. package/test/TestCashOutCallerValidation.t.sol +2 -2
  34. package/test/TestConversionDocumentation.t.sol +2 -2
  35. package/test/TestCrossCurrencyReclaim.t.sol +2 -2
  36. package/test/TestCrossSourceReallocation.t.sol +3 -3
  37. package/test/TestERC2771MetaTx.t.sol +6 -4
  38. package/test/TestEmptyBuybackSpecs.t.sol +2 -2
  39. package/test/TestFlashLoanSurplus.t.sol +3 -3
  40. package/test/TestHiddenTokens.t.sol +420 -0
  41. package/test/TestHookArrayOOB.t.sol +2 -2
  42. package/test/TestLiquidationBehavior.t.sol +4 -4
  43. package/test/TestLoanSourceRotation.t.sol +8 -6
  44. package/test/TestLoansCashOutDelay.t.sol +6 -6
  45. package/test/TestLongTailEconomics.t.sol +2 -2
  46. package/test/TestLowFindings.t.sol +13 -8
  47. package/test/TestMixedFixes.t.sol +7 -7
  48. package/test/TestPermit2Signatures.t.sol +3 -3
  49. package/test/TestReallocationSandwich.t.sol +4 -3
  50. package/test/TestRevnetRegressions.t.sol +3 -4
  51. package/test/TestSplitWeightAdjustment.t.sol +4 -3
  52. package/test/TestSplitWeightE2E.t.sol +4 -3
  53. package/test/TestSplitWeightFork.t.sol +2 -2
  54. package/test/TestStageTransitionBorrowable.t.sol +2 -2
  55. package/test/TestSwapTerminalPermission.t.sol +2 -2
  56. package/test/TestUint112Overflow.t.sol +3 -3
  57. package/test/TestZeroAmountLoanGuard.t.sol +3 -3
  58. package/test/TestZeroRepayment.t.sol +3 -3
  59. package/test/audit/LoanIdOverflowGuard.t.sol +4 -4
  60. package/test/audit/NemesisOperatorDelegation.t.sol +278 -0
  61. package/test/fork/ForkTestBase.sol +4 -3
  62. package/test/fork/TestLoanBorrowFork.t.sol +2 -1
  63. package/test/fork/TestLoanERC20Fork.t.sol +4 -2
  64. package/test/fork/TestLoanTransferFork.t.sol +12 -2
  65. package/test/helpers/MaliciousContracts.sol +1 -1
  66. package/test/regression/TestBurnPermissionRequired.t.sol +4 -4
  67. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +2 -2
  68. package/test/regression/TestCrossRevnetLiquidation.t.sol +2 -2
  69. package/test/regression/TestCumulativeLoanCounter.t.sol +3 -3
  70. package/test/regression/TestLiquidateGapHandling.t.sol +3 -3
  71. package/test/regression/TestZeroPriceFeed.t.sol +5 -5
@@ -98,7 +98,6 @@ contract TestMixedFixes is TestBaseWorkflow {
98
98
  .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
99
99
  LOANS_CONTRACT = new REVLoans({
100
100
  controller: jbController(),
101
- projects: jbProjects(),
102
101
  revId: FEE_PROJECT_ID,
103
102
  owner: address(this),
104
103
  permit2: permit2(),
@@ -109,7 +108,8 @@ contract TestMixedFixes is TestBaseWorkflow {
109
108
  jbDirectory(),
110
109
  FEE_PROJECT_ID,
111
110
  SUCKER_REGISTRY,
112
- address(LOANS_CONTRACT)
111
+ address(LOANS_CONTRACT),
112
+ address(0)
113
113
  );
114
114
 
115
115
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -245,7 +245,7 @@ contract TestMixedFixes is TestBaseWorkflow {
245
245
  );
246
246
  REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
247
247
  vm.prank(user);
248
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee);
248
+ (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
249
249
  }
250
250
 
251
251
  /// @notice At exactly LOAN_LIQUIDATION_DURATION, determineSourceFeeAmount should revert with LoanExpired (>=
@@ -431,7 +431,7 @@ contract TestMixedFixes is TestBaseWorkflow {
431
431
  REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
432
432
  vm.prank(USER);
433
433
  (uint256 loanId, REVLoan memory loan) =
434
- LOANS_CONTRACT.borrowFrom(MIXED_REVNET_ID, ethSource, 0, tokenCount, payable(USER), 25);
434
+ LOANS_CONTRACT.borrowFrom(MIXED_REVNET_ID, ethSource, 0, tokenCount, payable(USER), 25, USER);
435
435
 
436
436
  // Verify loan created and TOKEN source has zero borrowed.
437
437
  assertTrue(loanId != 0, "ETH loan should be created");
@@ -481,7 +481,7 @@ contract TestMixedFixes is TestBaseWorkflow {
481
481
  REVLoanSource memory tokenSource = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
482
482
  vm.prank(USER);
483
483
  (uint256 loanId, REVLoan memory loan) =
484
- LOANS_CONTRACT.borrowFrom(MIXED_REVNET_ID, tokenSource, 0, smallCollateral, payable(USER), 25);
484
+ LOANS_CONTRACT.borrowFrom(MIXED_REVNET_ID, tokenSource, 0, smallCollateral, payable(USER), 25, USER);
485
485
 
486
486
  assertTrue(loanId != 0, "TOKEN loan should be created");
487
487
  assertTrue(loan.amount > 0, "Loan amount should be nonzero");
@@ -537,7 +537,7 @@ contract TestMixedFixes is TestBaseWorkflow {
537
537
  REVLoanSource memory tokenSource = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
538
538
  vm.prank(USER);
539
539
  (uint256 tokenLoanId,) =
540
- LOANS_CONTRACT.borrowFrom(MIXED_REVNET_ID, tokenSource, 0, smallCollateral, payable(USER), 25);
540
+ LOANS_CONTRACT.borrowFrom(MIXED_REVNET_ID, tokenSource, 0, smallCollateral, payable(USER), 25, USER);
541
541
  assertTrue(tokenLoanId != 0, "TOKEN loan should be created");
542
542
 
543
543
  // STEP 3: Pay ETH to create ETH surplus.
@@ -558,7 +558,7 @@ contract TestMixedFixes is TestBaseWorkflow {
558
558
  REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
559
559
  vm.prank(USER);
560
560
  (uint256 ethLoanId,) =
561
- LOANS_CONTRACT.borrowFrom(MIXED_REVNET_ID, ethSource, 0, ethCollateral, payable(USER), 25);
561
+ LOANS_CONTRACT.borrowFrom(MIXED_REVNET_ID, ethSource, 0, ethCollateral, payable(USER), 25, USER);
562
562
  assertTrue(ethLoanId != 0, "ETH loan should be created");
563
563
 
564
564
  // Both sources should have tracked borrows.
@@ -258,7 +258,6 @@ contract TestPermit2Signatures is TestBaseWorkflow {
258
258
 
259
259
  LOANS_CONTRACT = new REVLoans({
260
260
  controller: jbController(),
261
- projects: jbProjects(),
262
261
  revId: FEE_PROJECT_ID,
263
262
  owner: address(this),
264
263
  permit2: permit2(),
@@ -270,7 +269,8 @@ contract TestPermit2Signatures is TestBaseWorkflow {
270
269
  jbDirectory(),
271
270
  FEE_PROJECT_ID,
272
271
  SUCKER_REGISTRY,
273
- address(LOANS_CONTRACT)
272
+ address(LOANS_CONTRACT),
273
+ address(0)
274
274
  );
275
275
 
276
276
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -398,7 +398,7 @@ contract TestPermit2Signatures is TestBaseWorkflow {
398
398
  REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
399
399
 
400
400
  vm.prank(user);
401
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee);
401
+ (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
402
402
  }
403
403
 
404
404
  // =========================================================================
@@ -153,7 +153,6 @@ contract TestReallocationSandwich is TestBaseWorkflow {
153
153
  MOCK_BUYBACK = new MockBuybackDataHook();
154
154
  LOANS_CONTRACT = new REVLoans({
155
155
  controller: jbController(),
156
- projects: jbProjects(),
157
156
  revId: FEE_PROJECT_ID,
158
157
  owner: address(this),
159
158
  permit2: permit2(),
@@ -164,7 +163,8 @@ contract TestReallocationSandwich is TestBaseWorkflow {
164
163
  jbDirectory(),
165
164
  FEE_PROJECT_ID,
166
165
  SUCKER_REGISTRY,
167
- address(LOANS_CONTRACT)
166
+ address(LOANS_CONTRACT),
167
+ address(0)
168
168
  );
169
169
 
170
170
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -286,7 +286,8 @@ contract TestReallocationSandwich is TestBaseWorkflow {
286
286
  minBorrowAmount: 0,
287
287
  collateralCount: borrowerTokens,
288
288
  beneficiary: payable(BORROWER),
289
- prepaidFeePercent: prepaidFeePercent
289
+ prepaidFeePercent: prepaidFeePercent,
290
+ holder: BORROWER
290
291
  });
291
292
 
292
293
  uint256 stage1BorrowedAmount = stage1Loan.amount;
@@ -48,13 +48,12 @@ import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
48
48
  contract REVLoansHarness is REVLoans {
49
49
  constructor(
50
50
  IJBController controller,
51
- IJBProjects projects,
52
51
  uint256 revId,
53
52
  address owner,
54
53
  IPermit2 permit2,
55
54
  address trustedForwarder
56
55
  )
57
- REVLoans(controller, projects, revId, owner, permit2, trustedForwarder)
56
+ REVLoans(controller, revId, owner, permit2, trustedForwarder)
58
57
  {}
59
58
 
60
59
  /// @notice Expose _totalBorrowedFrom for testing.
@@ -141,7 +140,6 @@ contract TestRevnetRegressions is TestBaseWorkflow {
141
140
 
142
141
  LOANS_CONTRACT = new REVLoansHarness({
143
142
  controller: jbController(),
144
- projects: jbProjects(),
145
143
  revId: FEE_PROJECT_ID,
146
144
  owner: address(this),
147
145
  permit2: permit2(),
@@ -153,7 +151,8 @@ contract TestRevnetRegressions is TestBaseWorkflow {
153
151
  jbDirectory(),
154
152
  FEE_PROJECT_ID,
155
153
  SUCKER_REGISTRY,
156
- address(LOANS_CONTRACT)
154
+ address(LOANS_CONTRACT),
155
+ address(0)
157
156
  );
158
157
 
159
158
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -91,7 +91,6 @@ contract TestSplitWeightAdjustment is TestBaseWorkflow {
91
91
  MOCK_BUYBACK = new MockBuybackDataHookMintPath();
92
92
  LOANS_CONTRACT = new REVLoans({
93
93
  controller: jbController(),
94
- projects: jbProjects(),
95
94
  revId: FEE_PROJECT_ID,
96
95
  owner: address(this),
97
96
  permit2: permit2(),
@@ -102,7 +101,8 @@ contract TestSplitWeightAdjustment is TestBaseWorkflow {
102
101
  jbDirectory(),
103
102
  FEE_PROJECT_ID,
104
103
  SUCKER_REGISTRY,
105
- address(LOANS_CONTRACT)
104
+ address(LOANS_CONTRACT),
105
+ address(0)
106
106
  );
107
107
 
108
108
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -329,7 +329,8 @@ contract TestSplitWeightAdjustment is TestBaseWorkflow {
329
329
  jbDirectory(),
330
330
  FEE_PROJECT_ID,
331
331
  SUCKER_REGISTRY,
332
- address(LOANS_CONTRACT)
332
+ address(LOANS_CONTRACT),
333
+ address(0)
333
334
  );
334
335
  REVDeployer ammDeployer = new REVDeployer{salt: "REVDeployer_AMM"}(
335
336
  jbController(),
@@ -111,7 +111,6 @@ contract TestSplitWeightE2E is TestBaseWorkflow {
111
111
 
112
112
  LOANS_CONTRACT = new REVLoans({
113
113
  controller: jbController(),
114
- projects: jbProjects(),
115
114
  revId: FEE_PROJECT_ID,
116
115
  owner: address(this),
117
116
  permit2: permit2(),
@@ -123,7 +122,8 @@ contract TestSplitWeightE2E is TestBaseWorkflow {
123
122
  jbDirectory(),
124
123
  FEE_PROJECT_ID,
125
124
  SUCKER_REGISTRY,
126
- address(LOANS_CONTRACT)
125
+ address(LOANS_CONTRACT),
126
+ address(0)
127
127
  );
128
128
 
129
129
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -429,7 +429,8 @@ contract TestSplitWeightE2E is TestBaseWorkflow {
429
429
  jbDirectory(),
430
430
  FEE_PROJECT_ID,
431
431
  SUCKER_REGISTRY,
432
- address(LOANS_CONTRACT)
432
+ address(LOANS_CONTRACT),
433
+ address(0)
433
434
  );
434
435
  REVDeployer ammDeployer = new REVDeployer{salt: "REVDeployer_AMM_E2E"}(
435
436
  jbController(),
@@ -323,7 +323,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
323
323
 
324
324
  LOANS_CONTRACT = new REVLoans({
325
325
  controller: jbController(),
326
- projects: jbProjects(),
327
326
  revId: FEE_PROJECT_ID,
328
327
  owner: address(this),
329
328
  permit2: permit2(),
@@ -335,7 +334,8 @@ contract TestSplitWeightFork is TestBaseWorkflow {
335
334
  jbDirectory(),
336
335
  FEE_PROJECT_ID,
337
336
  SUCKER_REGISTRY,
338
- address(LOANS_CONTRACT)
337
+ address(LOANS_CONTRACT),
338
+ address(0)
339
339
  );
340
340
 
341
341
  REV_DEPLOYER = new REVDeployer{salt: "REVDeployer_Fork"}(
@@ -146,7 +146,6 @@ contract TestStageTransitionBorrowable is TestBaseWorkflow {
146
146
  MOCK_BUYBACK = new MockBuybackDataHook();
147
147
  LOANS_CONTRACT = new REVLoans({
148
148
  controller: jbController(),
149
- projects: jbProjects(),
150
149
  revId: FEE_PROJECT_ID,
151
150
  owner: address(this),
152
151
  permit2: permit2(),
@@ -157,7 +156,8 @@ contract TestStageTransitionBorrowable is TestBaseWorkflow {
157
156
  jbDirectory(),
158
157
  FEE_PROJECT_ID,
159
158
  SUCKER_REGISTRY,
160
- address(LOANS_CONTRACT)
159
+ address(LOANS_CONTRACT),
160
+ address(0)
161
161
  );
162
162
 
163
163
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -87,7 +87,6 @@ contract TestSwapTerminalPermission is TestBaseWorkflow {
87
87
  MOCK_BUYBACK = new MockBuybackDataHook();
88
88
  LOANS_CONTRACT = new REVLoans({
89
89
  controller: jbController(),
90
- projects: jbProjects(),
91
90
  revId: FEE_PROJECT_ID,
92
91
  owner: address(this),
93
92
  permit2: permit2(),
@@ -98,7 +97,8 @@ contract TestSwapTerminalPermission is TestBaseWorkflow {
98
97
  jbDirectory(),
99
98
  FEE_PROJECT_ID,
100
99
  SUCKER_REGISTRY,
101
- address(LOANS_CONTRACT)
100
+ address(LOANS_CONTRACT),
101
+ address(0)
102
102
  );
103
103
 
104
104
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -106,7 +106,6 @@ contract TestUint112Overflow is TestBaseWorkflow {
106
106
 
107
107
  LOANS_CONTRACT = new REVLoans({
108
108
  controller: jbController(),
109
- projects: jbProjects(),
110
109
  revId: FEE_PROJECT_ID,
111
110
  owner: address(this),
112
111
  permit2: permit2(),
@@ -118,7 +117,8 @@ contract TestUint112Overflow is TestBaseWorkflow {
118
117
  jbDirectory(),
119
118
  FEE_PROJECT_ID,
120
119
  SUCKER_REGISTRY,
121
- address(LOANS_CONTRACT)
120
+ address(LOANS_CONTRACT),
121
+ address(0)
122
122
  );
123
123
 
124
124
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -265,7 +265,7 @@ contract TestUint112Overflow is TestBaseWorkflow {
265
265
 
266
266
  REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
267
267
  vm.prank(user);
268
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee);
268
+ (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
269
269
  }
270
270
 
271
271
  /// @notice Verify loan creation with a reasonable borrow amount succeeds.
@@ -100,7 +100,6 @@ contract TestZeroAmountLoanGuard is TestBaseWorkflow {
100
100
  .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
101
101
  LOANS_CONTRACT = new REVLoans({
102
102
  controller: jbController(),
103
- projects: jbProjects(),
104
103
  revId: FEE_PROJECT_ID,
105
104
  owner: address(this),
106
105
  permit2: permit2(),
@@ -111,7 +110,8 @@ contract TestZeroAmountLoanGuard is TestBaseWorkflow {
111
110
  jbDirectory(),
112
111
  FEE_PROJECT_ID,
113
112
  SUCKER_REGISTRY,
114
- address(LOANS_CONTRACT)
113
+ address(LOANS_CONTRACT),
114
+ address(0)
115
115
  );
116
116
 
117
117
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -247,7 +247,7 @@ contract TestZeroAmountLoanGuard is TestBaseWorkflow {
247
247
  );
248
248
  REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
249
249
  vm.prank(user);
250
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee);
250
+ (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
251
251
  }
252
252
 
253
253
  // -----------------------------------------------------------------------
@@ -100,7 +100,6 @@ contract TestZeroRepayment is TestBaseWorkflow {
100
100
  .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
101
101
  LOANS_CONTRACT = new REVLoans({
102
102
  controller: jbController(),
103
- projects: jbProjects(),
104
103
  revId: FEE_PROJECT_ID,
105
104
  owner: address(this),
106
105
  permit2: permit2(),
@@ -111,7 +110,8 @@ contract TestZeroRepayment is TestBaseWorkflow {
111
110
  jbDirectory(),
112
111
  FEE_PROJECT_ID,
113
112
  SUCKER_REGISTRY,
114
- address(LOANS_CONTRACT)
113
+ address(LOANS_CONTRACT),
114
+ address(0)
115
115
  );
116
116
 
117
117
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -247,7 +247,7 @@ contract TestZeroRepayment is TestBaseWorkflow {
247
247
  );
248
248
  REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
249
249
  vm.prank(user);
250
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee);
250
+ (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
251
251
  }
252
252
 
253
253
  /// @notice Repaying with zero borrow amount and zero collateral return should revert.
@@ -155,7 +155,6 @@ contract LoanIdOverflowGuard is TestBaseWorkflow {
155
155
  // Deploy the REVLoans contract.
156
156
  LOANS_CONTRACT = new REVLoans({
157
157
  controller: jbController(),
158
- projects: jbProjects(),
159
158
  revId: FEE_PROJECT_ID,
160
159
  owner: address(this),
161
160
  permit2: permit2(),
@@ -168,7 +167,8 @@ contract LoanIdOverflowGuard is TestBaseWorkflow {
168
167
  jbDirectory(),
169
168
  FEE_PROJECT_ID,
170
169
  SUCKER_REGISTRY,
171
- address(LOANS_CONTRACT)
170
+ address(LOANS_CONTRACT),
171
+ address(0)
172
172
  );
173
173
 
174
174
  REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
@@ -349,7 +349,7 @@ contract LoanIdOverflowGuard is TestBaseWorkflow {
349
349
 
350
350
  // Borrow with minimum fee percent (25 = 2.5%).
351
351
  vm.prank(user);
352
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
352
+ (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25, user);
353
353
  }
354
354
 
355
355
  /// @dev Computes the storage slot for totalLoansBorrowedFor[revnetId].
@@ -401,7 +401,7 @@ contract LoanIdOverflowGuard is TestBaseWorkflow {
401
401
 
402
402
  // Attempt to borrow -- should revert because the counter is at the limit.
403
403
  vm.prank(USER);
404
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25);
404
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25, USER);
405
405
  }
406
406
 
407
407
  // ---------------------------------------------------------------
@@ -0,0 +1,278 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import "forge-std/Test.sol";
5
+ import "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
6
+ import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
7
+ import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
8
+ import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
9
+ import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
10
+ import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
11
+ import "@croptop/core-v6/src/CTPublisher.sol";
12
+ import "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
13
+ import "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
14
+ import "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
15
+ import "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
16
+ import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
17
+ import "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
18
+ import "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
19
+ import "@bananapus/core-v6/src/libraries/JBConstants.sol";
20
+ import "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
21
+ import "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
22
+ import "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
23
+ import "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
24
+ import "@bananapus/core-v6/src/structs/JBSplit.sol";
25
+ import "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
26
+
27
+ import {MockBuybackDataHook} from "../mock/MockBuybackDataHook.sol";
28
+ import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
29
+ import {REVDeployer} from "../../src/REVDeployer.sol";
30
+ import {REVHiddenTokens} from "../../src/REVHiddenTokens.sol";
31
+ import {REVLoans} from "../../src/REVLoans.sol";
32
+ import {REVOwner} from "../../src/REVOwner.sol";
33
+ import {IREVLoans} from "../../src/interfaces/IREVLoans.sol";
34
+ import {IREVHiddenTokens} from "../../src/interfaces/IREVHiddenTokens.sol";
35
+ import {REVConfig} from "../../src/structs/REVConfig.sol";
36
+ import {REVDescription} from "../../src/structs/REVDescription.sol";
37
+ import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
38
+ import {REVStageConfig} from "../../src/structs/REVStageConfig.sol";
39
+ import {REVAutoIssuance} from "../../src/structs/REVAutoIssuance.sol";
40
+ import {REVSuckerDeploymentConfig} from "../../src/structs/REVSuckerDeploymentConfig.sol";
41
+ import {IREVDeployer} from "../../src/interfaces/IREVDeployer.sol";
42
+
43
+ contract NemesisOperatorDelegationTest is TestBaseWorkflow {
44
+ bytes32 internal constant REV_DEPLOYER_SALT = "REVDeployer";
45
+ bytes32 internal constant ERC20_SALT = "REV_TOKEN";
46
+
47
+ address internal constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
48
+
49
+ address internal USER = makeAddr("user");
50
+ address internal OPERATOR = makeAddr("operator");
51
+
52
+ REVDeployer internal REV_DEPLOYER;
53
+ REVOwner internal REV_OWNER;
54
+ REVHiddenTokens internal HIDDEN_TOKENS;
55
+ REVLoans internal LOANS;
56
+ JB721TiersHook internal EXAMPLE_HOOK;
57
+ IJB721TiersHookDeployer internal HOOK_DEPLOYER;
58
+ IJB721TiersHookStore internal HOOK_STORE;
59
+ IJBAddressRegistry internal ADDRESS_REGISTRY;
60
+ IJBSuckerRegistry internal SUCKER_REGISTRY;
61
+ CTPublisher internal PUBLISHER;
62
+ MockBuybackDataHook internal MOCK_BUYBACK;
63
+
64
+ uint256 internal FEE_PROJECT_ID;
65
+ uint256 internal REVNET_ID;
66
+
67
+ function setUp() public override {
68
+ super.setUp();
69
+
70
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
71
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
72
+ HOOK_STORE = new JB721TiersHookStore();
73
+ EXAMPLE_HOOK = new JB721TiersHook(
74
+ jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
75
+ );
76
+ ADDRESS_REGISTRY = new JBAddressRegistry();
77
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
78
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
79
+ MOCK_BUYBACK = new MockBuybackDataHook();
80
+
81
+ LOANS = new REVLoans({
82
+ controller: jbController(),
83
+ revId: FEE_PROJECT_ID,
84
+ owner: address(this),
85
+ permit2: permit2(),
86
+ trustedForwarder: TRUSTED_FORWARDER
87
+ });
88
+ HIDDEN_TOKENS = new REVHiddenTokens(jbController(), TRUSTED_FORWARDER);
89
+ REV_OWNER = new REVOwner(
90
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
91
+ jbDirectory(),
92
+ FEE_PROJECT_ID,
93
+ SUCKER_REGISTRY,
94
+ address(LOANS),
95
+ address(HIDDEN_TOKENS)
96
+ );
97
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
98
+ jbController(),
99
+ SUCKER_REGISTRY,
100
+ FEE_PROJECT_ID,
101
+ HOOK_DEPLOYER,
102
+ PUBLISHER,
103
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
104
+ address(LOANS),
105
+ TRUSTED_FORWARDER,
106
+ address(REV_OWNER)
107
+ );
108
+
109
+ REV_OWNER.setDeployer(IREVDeployer(REV_DEPLOYER));
110
+
111
+ vm.prank(multisig());
112
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
113
+
114
+ _deployFeeProject();
115
+ REVNET_ID = _deployRevnet();
116
+
117
+ vm.deal(USER, 100e18);
118
+ }
119
+
120
+ function test_openLoanOperatorCanRedirectBorrowedFunds() public {
121
+ uint256 userTokens = _payUserIntoRevnet(10e18);
122
+ _grantPermission(USER, REVNET_ID, address(LOANS), JBPermissionIds.BURN_TOKENS);
123
+ _grantPermission(USER, REVNET_ID, OPERATOR, JBPermissionIds.OPEN_LOAN);
124
+
125
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
126
+ uint256 operatorBalanceBefore = OPERATOR.balance;
127
+
128
+ vm.prank(OPERATOR);
129
+ (uint256 loanId,) = LOANS.borrowFrom(REVNET_ID, source, 0, userTokens / 2, payable(OPERATOR), 25, USER);
130
+
131
+ assertEq(LOANS.ownerOf(loanId), USER, "loan NFT stays with the holder");
132
+ assertGt(OPERATOR.balance, operatorBalanceBefore, "operator receives the borrowed funds");
133
+ assertLt(
134
+ jbController().TOKENS().totalBalanceOf(USER, REVNET_ID),
135
+ userTokens,
136
+ "holder lost collateral even though proceeds were redirected"
137
+ );
138
+ }
139
+
140
+ function test_revealTokensOperatorCanRedirectHiddenTokens() public {
141
+ uint256 userTokens = _payUserIntoRevnet(10e18);
142
+ uint256 hiddenCount = userTokens / 2;
143
+
144
+ _grantPermission(USER, REVNET_ID, address(HIDDEN_TOKENS), JBPermissionIds.BURN_TOKENS);
145
+ _grantPermission(USER, REVNET_ID, OPERATOR, JBPermissionIds.REVEAL_TOKENS);
146
+
147
+ vm.prank(USER);
148
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hiddenCount, USER);
149
+
150
+ vm.prank(OPERATOR);
151
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hiddenCount, OPERATOR, USER);
152
+
153
+ assertEq(HIDDEN_TOKENS.hiddenBalanceOf(USER, REVNET_ID), 0, "holder hidden balance was consumed");
154
+ assertEq(
155
+ jbController().TOKENS().totalBalanceOf(OPERATOR, REVNET_ID),
156
+ hiddenCount,
157
+ "operator receives the holder's revealed tokens"
158
+ );
159
+ assertEq(
160
+ jbController().TOKENS().totalBalanceOf(USER, REVNET_ID),
161
+ userTokens - hiddenCount,
162
+ "holder does not get the revealed tokens back"
163
+ );
164
+ }
165
+
166
+ function _grantPermission(address account, uint256 revnetId, address operator, uint8 permissionId) internal {
167
+ uint8[] memory permissionIds = new uint8[](1);
168
+ permissionIds[0] = permissionId;
169
+
170
+ vm.prank(account);
171
+ jbPermissions()
172
+ .setPermissionsFor(
173
+ account,
174
+ JBPermissionsData({operator: operator, projectId: uint56(revnetId), permissionIds: permissionIds})
175
+ );
176
+ }
177
+
178
+ function _payUserIntoRevnet(uint256 amount) internal returns (uint256 tokenCount) {
179
+ vm.prank(USER);
180
+ tokenCount = jbMultiTerminal().pay{value: amount}({
181
+ projectId: REVNET_ID,
182
+ token: JBConstants.NATIVE_TOKEN,
183
+ amount: amount,
184
+ beneficiary: USER,
185
+ minReturnedTokens: 0,
186
+ memo: "",
187
+ metadata: ""
188
+ });
189
+ assertGt(tokenCount, 0, "payment should mint revnet tokens");
190
+ }
191
+
192
+ function _deployFeeProject() internal {
193
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
194
+ acc[0] = JBAccountingContext({
195
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
196
+ });
197
+
198
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
199
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
200
+
201
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
202
+ stages[0] = REVStageConfig({
203
+ startsAtOrAfter: uint40(block.timestamp),
204
+ autoIssuances: new REVAutoIssuance[](0),
205
+ splitPercent: 0,
206
+ splits: new JBSplit[](0),
207
+ initialIssuance: uint112(1000e18),
208
+ issuanceCutFrequency: 0,
209
+ issuanceCutPercent: 0,
210
+ cashOutTaxRate: 0,
211
+ extraMetadata: 0
212
+ });
213
+
214
+ REVConfig memory feeConfig = REVConfig({
215
+ description: REVDescription("Fee Revnet", "FEE", "", ERC20_SALT),
216
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
217
+ splitOperator: multisig(),
218
+ stageConfigurations: stages
219
+ });
220
+
221
+ vm.prank(multisig());
222
+ REV_DEPLOYER.deployFor({
223
+ revnetId: FEE_PROJECT_ID,
224
+ configuration: feeConfig,
225
+ terminalConfigurations: tc,
226
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
227
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
228
+ }),
229
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
230
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
231
+ });
232
+ }
233
+
234
+ function _deployRevnet() internal returns (uint256 revnetId) {
235
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
236
+ acc[0] = JBAccountingContext({
237
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
238
+ });
239
+
240
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
241
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
242
+
243
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
244
+ JBSplit[] memory splits = new JBSplit[](1);
245
+ splits[0].beneficiary = payable(multisig());
246
+ splits[0].percent = 10_000;
247
+
248
+ stages[0] = REVStageConfig({
249
+ startsAtOrAfter: uint40(block.timestamp),
250
+ autoIssuances: new REVAutoIssuance[](0),
251
+ splitPercent: 2000,
252
+ splits: splits,
253
+ initialIssuance: uint112(1000e18),
254
+ issuanceCutFrequency: 90 days,
255
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
256
+ cashOutTaxRate: 6000,
257
+ extraMetadata: 0
258
+ });
259
+
260
+ REVConfig memory config = REVConfig({
261
+ description: REVDescription("Revnet", "REV", "", bytes32("REV_TOKEN_2")),
262
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
263
+ splitOperator: multisig(),
264
+ stageConfigurations: stages
265
+ });
266
+
267
+ (revnetId,) = REV_DEPLOYER.deployFor({
268
+ revnetId: 0,
269
+ configuration: config,
270
+ terminalConfigurations: tc,
271
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
272
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("REV")
273
+ }),
274
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
275
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
276
+ });
277
+ }
278
+ }