@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
@@ -0,0 +1,289 @@
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/721-hook-v6/src/JB721CheckpointsDeployer.sol";
17
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
18
+ import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
19
+ import "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
20
+ import "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
21
+ import "@bananapus/core-v6/src/libraries/JBConstants.sol";
22
+ import "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
23
+ import "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
24
+ import "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
25
+ import "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
26
+ import "@bananapus/core-v6/src/structs/JBSplit.sol";
27
+ import "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
28
+
29
+ import {MockBuybackDataHook} from "../mock/MockBuybackDataHook.sol";
30
+ import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
31
+ import {REVDeployer} from "../../src/REVDeployer.sol";
32
+ import {REVHiddenTokens} from "../../src/REVHiddenTokens.sol";
33
+ import {REVLoans} from "../../src/REVLoans.sol";
34
+ import {REVOwner} from "../../src/REVOwner.sol";
35
+ import {IREVLoans} from "../../src/interfaces/IREVLoans.sol";
36
+ import {IREVHiddenTokens} from "../../src/interfaces/IREVHiddenTokens.sol";
37
+ import {REVConfig} from "../../src/structs/REVConfig.sol";
38
+ import {REVDescription} from "../../src/structs/REVDescription.sol";
39
+ import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
40
+ import {REVStageConfig} from "../../src/structs/REVStageConfig.sol";
41
+ import {REVAutoIssuance} from "../../src/structs/REVAutoIssuance.sol";
42
+ import {REVSuckerDeploymentConfig} from "../../src/structs/REVSuckerDeploymentConfig.sol";
43
+ import {IREVDeployer} from "../../src/interfaces/IREVDeployer.sol";
44
+ import {MockSuckerRegistry} from "../mock/MockSuckerRegistry.sol";
45
+
46
+ contract NemesisOperatorDelegationTest is TestBaseWorkflow {
47
+ bytes32 internal constant REV_DEPLOYER_SALT = "REVDeployer";
48
+ bytes32 internal constant ERC20_SALT = "REV_TOKEN";
49
+
50
+ address internal constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
51
+
52
+ address internal USER = makeAddr("user");
53
+ address internal OPERATOR = makeAddr("operator");
54
+
55
+ REVDeployer internal REV_DEPLOYER;
56
+ REVOwner internal REV_OWNER;
57
+ REVHiddenTokens internal HIDDEN_TOKENS;
58
+ REVLoans internal LOANS;
59
+ JB721TiersHook internal EXAMPLE_HOOK;
60
+ IJB721TiersHookDeployer internal HOOK_DEPLOYER;
61
+ IJB721TiersHookStore internal HOOK_STORE;
62
+ IJBAddressRegistry internal ADDRESS_REGISTRY;
63
+ IJBSuckerRegistry internal SUCKER_REGISTRY;
64
+ CTPublisher internal PUBLISHER;
65
+ MockBuybackDataHook internal MOCK_BUYBACK;
66
+
67
+ uint256 internal FEE_PROJECT_ID;
68
+ uint256 internal REVNET_ID;
69
+
70
+ function setUp() public override {
71
+ super.setUp();
72
+
73
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
74
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
75
+ HOOK_STORE = new JB721TiersHookStore();
76
+ EXAMPLE_HOOK = new JB721TiersHook(
77
+ jbDirectory(),
78
+ jbPermissions(),
79
+ jbPrices(),
80
+ jbRulesets(),
81
+ HOOK_STORE,
82
+ jbSplits(),
83
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
84
+ multisig()
85
+ );
86
+ ADDRESS_REGISTRY = new JBAddressRegistry();
87
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
88
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
89
+ MOCK_BUYBACK = new MockBuybackDataHook();
90
+
91
+ LOANS = new REVLoans({
92
+ controller: jbController(),
93
+ suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
94
+ revId: FEE_PROJECT_ID,
95
+ owner: address(this),
96
+ permit2: permit2(),
97
+ trustedForwarder: TRUSTED_FORWARDER
98
+ });
99
+ HIDDEN_TOKENS = new REVHiddenTokens(jbController(), TRUSTED_FORWARDER);
100
+ REV_OWNER = new REVOwner(
101
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
102
+ jbDirectory(),
103
+ FEE_PROJECT_ID,
104
+ SUCKER_REGISTRY,
105
+ address(LOANS),
106
+ address(HIDDEN_TOKENS)
107
+ );
108
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
109
+ jbController(),
110
+ SUCKER_REGISTRY,
111
+ FEE_PROJECT_ID,
112
+ HOOK_DEPLOYER,
113
+ PUBLISHER,
114
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
115
+ address(LOANS),
116
+ TRUSTED_FORWARDER,
117
+ address(REV_OWNER)
118
+ );
119
+
120
+ REV_OWNER.setDeployer(IREVDeployer(REV_DEPLOYER));
121
+
122
+ vm.prank(multisig());
123
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
124
+
125
+ _deployFeeProject();
126
+ REVNET_ID = _deployRevnet();
127
+
128
+ vm.deal(USER, 100e18);
129
+ }
130
+
131
+ function test_openLoanOperatorCanRedirectBorrowedFunds() public {
132
+ uint256 userTokens = _payUserIntoRevnet(10e18);
133
+ _grantPermission(USER, REVNET_ID, address(LOANS), JBPermissionIds.BURN_TOKENS);
134
+ _grantPermission(USER, REVNET_ID, OPERATOR, JBPermissionIds.OPEN_LOAN);
135
+
136
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
137
+ uint256 operatorBalanceBefore = OPERATOR.balance;
138
+
139
+ vm.prank(OPERATOR);
140
+ (uint256 loanId,) = LOANS.borrowFrom(REVNET_ID, source, 0, userTokens / 2, payable(OPERATOR), 25, USER);
141
+
142
+ assertEq(LOANS.ownerOf(loanId), USER, "loan NFT stays with the holder");
143
+ assertGt(OPERATOR.balance, operatorBalanceBefore, "operator receives the borrowed funds");
144
+ assertLt(
145
+ jbController().TOKENS().totalBalanceOf(USER, REVNET_ID),
146
+ userTokens,
147
+ "holder lost collateral even though proceeds were redirected"
148
+ );
149
+ }
150
+
151
+ function test_revealTokensOperatorCanRedirectHiddenTokens() public {
152
+ uint256 userTokens = _payUserIntoRevnet(10e18);
153
+ uint256 hiddenCount = userTokens / 2;
154
+
155
+ _grantPermission(USER, REVNET_ID, address(HIDDEN_TOKENS), JBPermissionIds.BURN_TOKENS);
156
+ _grantPermission(USER, REVNET_ID, OPERATOR, JBPermissionIds.REVEAL_TOKENS);
157
+
158
+ vm.prank(USER);
159
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hiddenCount, USER);
160
+
161
+ vm.prank(OPERATOR);
162
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hiddenCount, OPERATOR, USER);
163
+
164
+ assertEq(HIDDEN_TOKENS.hiddenBalanceOf(USER, REVNET_ID), 0, "holder hidden balance was consumed");
165
+ assertEq(
166
+ jbController().TOKENS().totalBalanceOf(OPERATOR, REVNET_ID),
167
+ hiddenCount,
168
+ "operator receives the holder's revealed tokens"
169
+ );
170
+ assertEq(
171
+ jbController().TOKENS().totalBalanceOf(USER, REVNET_ID),
172
+ userTokens - hiddenCount,
173
+ "holder does not get the revealed tokens back"
174
+ );
175
+ }
176
+
177
+ function _grantPermission(address account, uint256 revnetId, address operator, uint8 permissionId) internal {
178
+ uint8[] memory permissionIds = new uint8[](1);
179
+ permissionIds[0] = permissionId;
180
+
181
+ vm.prank(account);
182
+ jbPermissions()
183
+ .setPermissionsFor(
184
+ account,
185
+ JBPermissionsData({operator: operator, projectId: uint56(revnetId), permissionIds: permissionIds})
186
+ );
187
+ }
188
+
189
+ function _payUserIntoRevnet(uint256 amount) internal returns (uint256 tokenCount) {
190
+ vm.prank(USER);
191
+ tokenCount = jbMultiTerminal().pay{value: amount}({
192
+ projectId: REVNET_ID,
193
+ token: JBConstants.NATIVE_TOKEN,
194
+ amount: amount,
195
+ beneficiary: USER,
196
+ minReturnedTokens: 0,
197
+ memo: "",
198
+ metadata: ""
199
+ });
200
+ assertGt(tokenCount, 0, "payment should mint revnet tokens");
201
+ }
202
+
203
+ function _deployFeeProject() internal {
204
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
205
+ acc[0] = JBAccountingContext({
206
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
207
+ });
208
+
209
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
210
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
211
+
212
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
213
+ stages[0] = REVStageConfig({
214
+ startsAtOrAfter: uint40(block.timestamp),
215
+ autoIssuances: new REVAutoIssuance[](0),
216
+ splitPercent: 0,
217
+ splits: new JBSplit[](0),
218
+ initialIssuance: uint112(1000e18),
219
+ issuanceCutFrequency: 0,
220
+ issuanceCutPercent: 0,
221
+ cashOutTaxRate: 0,
222
+ extraMetadata: 0
223
+ });
224
+
225
+ REVConfig memory feeConfig = REVConfig({
226
+ description: REVDescription("Fee Revnet", "FEE", "", ERC20_SALT),
227
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
228
+ splitOperator: multisig(),
229
+ stageConfigurations: stages
230
+ });
231
+
232
+ vm.prank(multisig());
233
+ REV_DEPLOYER.deployFor({
234
+ revnetId: FEE_PROJECT_ID,
235
+ configuration: feeConfig,
236
+ terminalConfigurations: tc,
237
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
238
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
239
+ }),
240
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
241
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
242
+ });
243
+ }
244
+
245
+ function _deployRevnet() internal returns (uint256 revnetId) {
246
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
247
+ acc[0] = JBAccountingContext({
248
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
249
+ });
250
+
251
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
252
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
253
+
254
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
255
+ JBSplit[] memory splits = new JBSplit[](1);
256
+ splits[0].beneficiary = payable(multisig());
257
+ splits[0].percent = 10_000;
258
+
259
+ stages[0] = REVStageConfig({
260
+ startsAtOrAfter: uint40(block.timestamp),
261
+ autoIssuances: new REVAutoIssuance[](0),
262
+ splitPercent: 2000,
263
+ splits: splits,
264
+ initialIssuance: uint112(1000e18),
265
+ issuanceCutFrequency: 90 days,
266
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
267
+ cashOutTaxRate: 6000,
268
+ extraMetadata: 0
269
+ });
270
+
271
+ REVConfig memory config = REVConfig({
272
+ description: REVDescription("Revnet", "REV", "", bytes32("REV_TOKEN_2")),
273
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
274
+ splitOperator: multisig(),
275
+ stageConfigurations: stages
276
+ });
277
+
278
+ (revnetId,) = REV_DEPLOYER.deployFor({
279
+ revnetId: 0,
280
+ configuration: config,
281
+ terminalConfigurations: tc,
282
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
283
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("REV")
284
+ }),
285
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
286
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
287
+ });
288
+ }
289
+ }
@@ -34,6 +34,8 @@ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
34
34
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
35
35
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
36
36
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
37
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
38
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
37
39
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
38
40
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
39
41
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
@@ -44,6 +46,8 @@ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfi
44
46
  import {JB721TierConfigFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierConfigFlags.sol";
45
47
  import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
46
48
  import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
49
+ import "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
50
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
47
51
  import {REVDeploy721TiersHookConfig} from "../../src/structs/REVDeploy721TiersHookConfig.sol";
48
52
  import {REVBaseline721HookConfig} from "../../src/structs/REVBaseline721HookConfig.sol";
49
53
  import {REV721TiersHookFlags} from "../../src/structs/REV721TiersHookFlags.sol";
@@ -74,6 +78,7 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
74
78
  import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
75
79
  import {REVOwner} from "../../src/REVOwner.sol";
76
80
  import {IREVDeployer} from "../../src/interfaces/IREVDeployer.sol";
81
+ import {MockSuckerRegistry} from "../mock/MockSuckerRegistry.sol";
77
82
 
78
83
  /// @notice Helper that adds liquidity to a V4 pool via the unlock/callback pattern.
79
84
  contract LiquidityHelper is IUnlockCallback {
@@ -299,7 +304,14 @@ abstract contract ForkTestBase is TestBaseWorkflow {
299
304
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
300
305
  HOOK_STORE = new JB721TiersHookStore();
301
306
  EXAMPLE_HOOK = new JB721TiersHook(
302
- jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
307
+ jbDirectory(),
308
+ jbPermissions(),
309
+ jbPrices(),
310
+ jbRulesets(),
311
+ HOOK_STORE,
312
+ jbSplits(),
313
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
314
+ multisig()
303
315
  );
304
316
  ADDRESS_REGISTRY = new JBAddressRegistry();
305
317
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
@@ -328,7 +340,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
328
340
 
329
341
  LOANS_CONTRACT = new REVLoans({
330
342
  controller: jbController(),
331
- projects: jbProjects(),
343
+ suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
332
344
  revId: FEE_PROJECT_ID,
333
345
  owner: address(this),
334
346
  permit2: permit2(),
@@ -340,7 +352,8 @@ abstract contract ForkTestBase is TestBaseWorkflow {
340
352
  jbDirectory(),
341
353
  FEE_PROJECT_ID,
342
354
  SUCKER_REGISTRY,
343
- address(LOANS_CONTRACT)
355
+ address(LOANS_CONTRACT),
356
+ address(0)
344
357
  );
345
358
 
346
359
  REV_DEPLOYER = new REVDeployer{salt: "REVDeployer_Fork"}(
@@ -707,7 +720,8 @@ abstract contract ForkTestBase is TestBaseWorkflow {
707
720
  minBorrowAmount: 0,
708
721
  collateralCount: collateral,
709
722
  beneficiary: payable(borrower),
710
- prepaidFeePercent: prepaidFeePercent
723
+ prepaidFeePercent: prepaidFeePercent,
724
+ holder: borrower
711
725
  });
712
726
  }
713
727
  }
@@ -97,7 +97,8 @@ contract TestLoanBorrowFork is ForkTestBase {
97
97
  minBorrowAmount: 0,
98
98
  collateralCount: borrowerTokens,
99
99
  beneficiary: payable(BORROWER),
100
- prepaidFeePercent: prepaidFeePercent
100
+ prepaidFeePercent: prepaidFeePercent,
101
+ holder: BORROWER
101
102
  });
102
103
 
103
104
  uint256 borrowerReceived = BORROWER.balance - borrowerEthBefore;
@@ -184,7 +184,8 @@ contract TestLoanERC20Fork is ForkTestBase {
184
184
  minBorrowAmount: 0,
185
185
  collateralCount: collateral,
186
186
  beneficiary: payable(borrower),
187
- prepaidFeePercent: prepaidFeePercent
187
+ prepaidFeePercent: prepaidFeePercent,
188
+ holder: borrower
188
189
  });
189
190
  }
190
191
 
@@ -263,7 +264,8 @@ contract TestLoanERC20Fork is ForkTestBase {
263
264
  minBorrowAmount: 0,
264
265
  collateralCount: borrowerTokens,
265
266
  beneficiary: payable(BORROWER),
266
- prepaidFeePercent: prepaidFeePercent
267
+ prepaidFeePercent: prepaidFeePercent,
268
+ holder: BORROWER
267
269
  });
268
270
 
269
271
  uint256 borrowerUsdcReceived = IERC20(USDC).balanceOf(BORROWER) - borrowerUsdcBefore;
@@ -3,6 +3,8 @@ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./ForkTestBase.sol";
6
+ import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
7
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
6
8
 
7
9
  /// @notice Fork tests for transferring loan NFTs and repaying from the new owner.
8
10
  ///
@@ -79,9 +81,17 @@ contract TestLoanTransferFork is ForkTestBase {
79
81
 
80
82
  JBSingleAllowance memory allowance;
81
83
 
82
- // Original borrower tries to repay — should revert with REVLoans_Unauthorized.
84
+ // Original borrower tries to repay — should revert with JBPermissioned_Unauthorized.
83
85
  vm.prank(BORROWER);
84
- vm.expectRevert(abi.encodeWithSelector(REVLoans.REVLoans_Unauthorized.selector, BORROWER, newOwner));
86
+ vm.expectRevert(
87
+ abi.encodeWithSelector(
88
+ JBPermissioned.JBPermissioned_Unauthorized.selector,
89
+ newOwner,
90
+ BORROWER,
91
+ revnetId,
92
+ JBPermissionIds.REPAY_LOAN
93
+ )
94
+ );
85
95
  LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
86
96
  loanId: loanId,
87
97
  maxRepayBorrowAmount: loan.amount * 2,
@@ -162,7 +162,7 @@ contract SurplusInflator is ERC165, IJBPayoutTerminal {
162
162
  shouldInflate = false;
163
163
  // Try to borrow at the inflated surplus
164
164
  REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: realTerminal});
165
- try loans.borrowFrom(revnetId, source, 0, 1e18, payable(address(this)), 25) {} catch {}
165
+ try loans.borrowFrom(revnetId, source, 0, 1e18, payable(address(this)), 25, address(this)) {} catch {}
166
166
  }
167
167
  return 0;
168
168
  }
@@ -38,12 +38,14 @@ contract MockBuybackCashOutRecorder is IJBRulesetDataHook, IJBPayHook, IJBCashOu
38
38
  uint256 cashOutTaxRate,
39
39
  uint256 cashOutCount,
40
40
  uint256 totalSupply,
41
+ uint256 effectiveSurplusValue,
41
42
  JBCashOutHookSpecification[] memory hookSpecifications
42
43
  )
43
44
  {
44
45
  cashOutTaxRate = JBConstants.MAX_CASH_OUT_TAX_RATE;
45
46
  cashOutCount = context.cashOutCount;
46
47
  totalSupply = context.totalSupply;
48
+ effectiveSurplusValue = 0;
47
49
  hookSpecifications = new JBCashOutHookSpecification[](1);
48
50
  hookSpecifications[0] = JBCashOutHookSpecification({
49
51
  hook: IJBCashOutHook(address(this)),
@@ -44,16 +44,18 @@ contract MockBuybackDataHook is IJBRulesetDataHook, IJBPayHook {
44
44
  uint256 cashOutTaxRate,
45
45
  uint256 cashOutCount,
46
46
  uint256 totalSupply,
47
+ uint256 effectiveSurplusValue,
47
48
  JBCashOutHookSpecification[] memory hookSpecifications
48
49
  )
49
50
  {
50
51
  cashOutTaxRate = cashOutTaxRateToReturn == 0 ? context.cashOutTaxRate : cashOutTaxRateToReturn;
51
52
  cashOutCount = cashOutCountToReturn == 0 ? context.cashOutCount : cashOutCountToReturn;
52
53
  totalSupply = totalSupplyToReturn == 0 ? context.totalSupply : totalSupplyToReturn;
54
+ effectiveSurplusValue = 0;
53
55
 
54
56
  if (!shouldReturnCashOutHookSpec) {
55
57
  hookSpecifications = new JBCashOutHookSpecification[](0);
56
- return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
58
+ return (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
57
59
  }
58
60
 
59
61
  hookSpecifications = new JBCashOutHookSpecification[](1);
@@ -35,12 +35,14 @@ contract MockBuybackDataHookMintPath is IJBRulesetDataHook, IJBPayHook {
35
35
  uint256 cashOutTaxRate,
36
36
  uint256 cashOutCount,
37
37
  uint256 totalSupply,
38
+ uint256 effectiveSurplusValue,
38
39
  JBCashOutHookSpecification[] memory hookSpecifications
39
40
  )
40
41
  {
41
42
  cashOutTaxRate = context.cashOutTaxRate;
42
43
  cashOutCount = context.cashOutCount;
43
44
  totalSupply = context.totalSupply;
45
+ effectiveSurplusValue = 0;
44
46
  hookSpecifications = new JBCashOutHookSpecification[](0);
45
47
  }
46
48
 
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ /// @notice Minimal mock that returns zeros for all cross-chain queries.
5
+ contract MockSuckerRegistry {
6
+ function isSuckerOf(uint256, address) external pure returns (bool) {
7
+ return false;
8
+ }
9
+
10
+ function remoteTotalSupplyOf(uint256) external pure returns (uint256) {
11
+ return 0;
12
+ }
13
+
14
+ function remoteSurplusOf(uint256, uint256, uint256) external pure returns (uint256) {
15
+ return 0;
16
+ }
17
+ }
@@ -35,6 +35,8 @@ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
35
35
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
36
36
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
37
37
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
38
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
39
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
38
40
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
39
41
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
40
42
  import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
@@ -42,6 +44,7 @@ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.
42
44
  import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
43
45
  import {REVOwner} from "../../src/REVOwner.sol";
44
46
  import {IREVDeployer} from "../../src/interfaces/IREVDeployer.sol";
47
+ import {MockSuckerRegistry} from "../mock/MockSuckerRegistry.sol";
45
48
 
46
49
  /// @notice Validates that borrowFrom() reverts with a clear error when the caller hasn't granted BURN_TOKENS
47
50
  /// permission to the REVLoans contract.
@@ -89,7 +92,14 @@ contract TestBurnPermissionRequired 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 TestBurnPermissionRequired 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 TestBurnPermissionRequired 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}(
@@ -247,7 +258,7 @@ contract TestBurnPermissionRequired is TestBaseWorkflow {
247
258
  JBPermissionIds.BURN_TOKENS // permissionId
248
259
  )
249
260
  );
250
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
261
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25, user);
251
262
  }
252
263
 
253
264
  /// @notice borrowFrom should succeed when the caller has granted BURN_TOKENS permission.
@@ -274,7 +285,7 @@ contract TestBurnPermissionRequired is TestBaseWorkflow {
274
285
  REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
275
286
  vm.prank(user);
276
287
  (uint256 loanId, REVLoan memory loan) =
277
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
288
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25, user);
278
289
 
279
290
  assertTrue(loanId > 0, "Loan ID should be non-zero");
280
291
  assertTrue(loan.createdAt > 0, "Loan should be created");
@@ -21,7 +21,11 @@ import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
21
21
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
22
22
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
23
23
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
24
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
25
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
24
26
  import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
27
+ import "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
28
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
25
29
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
26
30
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
27
31
  import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
@@ -37,6 +41,7 @@ import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
37
41
  import {MockBuybackCashOutRecorder} from "../mock/MockBuybackCashOutRecorder.sol";
38
42
  import {REVOwner} from "../../src/REVOwner.sol";
39
43
  import {IREVDeployer} from "../../src/interfaces/IREVDeployer.sol";
44
+ import {MockSuckerRegistry} from "../mock/MockSuckerRegistry.sol";
40
45
 
41
46
  /// @title TestCashOutBuybackFeeLeak
42
47
  /// @notice Proves the buyback hook callback receives only the non-fee cashOutCount (not the full count).
@@ -69,7 +74,14 @@ contract TestCashOutBuybackFeeLeak is TestBaseWorkflow {
69
74
  suckerRegistry = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
70
75
  hookStore = new JB721TiersHookStore();
71
76
  exampleHook = new JB721TiersHook(
72
- jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), hookStore, jbSplits(), multisig()
77
+ jbDirectory(),
78
+ jbPermissions(),
79
+ jbPrices(),
80
+ jbRulesets(),
81
+ hookStore,
82
+ jbSplits(),
83
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
84
+ multisig()
73
85
  );
74
86
  addressRegistry = new JBAddressRegistry();
75
87
  hookDeployer = new JB721TiersHookDeployer(exampleHook, hookStore, addressRegistry, multisig());
@@ -77,7 +89,7 @@ contract TestCashOutBuybackFeeLeak is TestBaseWorkflow {
77
89
  mockBuyback = new MockBuybackCashOutRecorder();
78
90
  loans = new REVLoans({
79
91
  controller: jbController(),
80
- projects: jbProjects(),
92
+ suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
81
93
  revId: feeProjectId,
82
94
  owner: address(this),
83
95
  permit2: permit2(),
@@ -89,7 +101,8 @@ contract TestCashOutBuybackFeeLeak is TestBaseWorkflow {
89
101
  jbDirectory(),
90
102
  feeProjectId,
91
103
  IJBSuckerRegistry(address(suckerRegistry)),
92
- address(loans)
104
+ address(loans),
105
+ address(0)
93
106
  );
94
107
 
95
108
  revDeployer = new REVDeployer{salt: REV_DEPLOYER_SALT}(