@rev-net/core-v6 0.0.12 → 0.0.13

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 (78) hide show
  1. package/AUDIT_INSTRUCTIONS.md +295 -0
  2. package/CHANGE_LOG.md +316 -0
  3. package/README.md +2 -2
  4. package/RISKS.md +180 -35
  5. package/SKILLS.md +1 -1
  6. package/USER_JOURNEYS.md +489 -0
  7. package/package.json +9 -9
  8. package/script/Deploy.s.sol +40 -6
  9. package/script/helpers/RevnetCoreDeploymentLib.sol +7 -1
  10. package/src/REVDeployer.sol +63 -47
  11. package/src/REVLoans.sol +51 -15
  12. package/src/interfaces/IREVDeployer.sol +0 -1
  13. package/src/structs/REV721TiersHookFlags.sol +1 -0
  14. package/src/structs/REVAutoIssuance.sol +1 -0
  15. package/src/structs/REVBaseline721HookConfig.sol +1 -0
  16. package/src/structs/REVConfig.sol +1 -0
  17. package/src/structs/REVCroptopAllowedPost.sol +1 -0
  18. package/src/structs/REVDeploy721TiersHookConfig.sol +1 -0
  19. package/src/structs/REVDescription.sol +1 -0
  20. package/src/structs/REVLoan.sol +1 -0
  21. package/src/structs/REVLoanSource.sol +1 -0
  22. package/src/structs/REVStageConfig.sol +1 -0
  23. package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
  24. package/test/REV.integrations.t.sol +132 -12
  25. package/test/REVAutoIssuanceFuzz.t.sol +23 -3
  26. package/test/REVDeployerRegressions.t.sol +35 -4
  27. package/test/REVInvincibility.t.sol +58 -8
  28. package/test/REVInvincibilityHandler.sol +29 -0
  29. package/test/REVLifecycle.t.sol +28 -3
  30. package/test/REVLoans.invariants.t.sol +52 -5
  31. package/test/REVLoansAttacks.t.sol +43 -5
  32. package/test/REVLoansFeeRecovery.t.sol +50 -11
  33. package/test/REVLoansFindings.t.sol +27 -3
  34. package/test/REVLoansRegressions.t.sol +25 -3
  35. package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
  36. package/test/REVLoansSourced.t.sol +56 -7
  37. package/test/REVLoansUnSourced.t.sol +49 -5
  38. package/test/TestBurnHeldTokens.t.sol +32 -5
  39. package/test/TestCEIPattern.t.sol +26 -2
  40. package/test/TestCashOutCallerValidation.t.sol +30 -4
  41. package/test/TestConversionDocumentation.t.sol +26 -5
  42. package/test/TestCrossCurrencyReclaim.t.sol +584 -0
  43. package/test/TestCrossSourceReallocation.t.sol +26 -2
  44. package/test/TestERC2771MetaTx.t.sol +557 -0
  45. package/test/TestEmptyBuybackSpecs.t.sol +23 -3
  46. package/test/TestFlashLoanSurplus.t.sol +28 -3
  47. package/test/TestHookArrayOOB.t.sol +24 -4
  48. package/test/TestLiquidationBehavior.t.sol +26 -3
  49. package/test/TestLoanSourceRotation.t.sol +525 -0
  50. package/test/TestLongTailEconomics.t.sol +651 -0
  51. package/test/TestLowFindings.t.sol +65 -2
  52. package/test/TestMixedFixes.t.sol +28 -3
  53. package/test/TestPermit2Signatures.t.sol +657 -0
  54. package/test/TestReallocationSandwich.t.sol +384 -0
  55. package/test/TestRevnetRegressions.t.sol +324 -0
  56. package/test/TestSplitWeightAdjustment.t.sol +24 -2
  57. package/test/TestSplitWeightE2E.t.sol +29 -2
  58. package/test/TestSplitWeightFork.t.sol +46 -7
  59. package/test/TestStageTransitionBorrowable.t.sol +24 -2
  60. package/test/TestSwapTerminalPermission.t.sol +23 -3
  61. package/test/TestUint112Overflow.t.sol +28 -2
  62. package/test/TestZeroRepayment.t.sol +26 -2
  63. package/test/fork/ForkTestBase.sol +46 -3
  64. package/test/fork/TestCashOutFork.t.sol +1 -1
  65. package/test/fork/TestLoanBorrowFork.t.sol +1 -0
  66. package/test/fork/TestLoanCrossRulesetFork.t.sol +3 -1
  67. package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
  68. package/test/fork/TestLoanReallocateFork.t.sol +1 -0
  69. package/test/fork/TestLoanRepayFork.t.sol +1 -0
  70. package/test/fork/TestLoanTransferFork.t.sol +133 -0
  71. package/test/fork/TestSplitWeightFork.t.sol +3 -0
  72. package/test/helpers/REVEmpty721Config.sol +1 -0
  73. package/test/mock/MockBuybackDataHook.sol +1 -0
  74. package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
  75. package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
  76. package/test/regression/TestCumulativeLoanCounter.t.sol +27 -4
  77. package/test/regression/TestLiquidateGapHandling.t.sol +29 -4
  78. package/test/regression/TestZeroPriceFeed.t.sol +396 -0
@@ -1,14 +1,23 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "forge-std/Test.sol";
6
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
7
  import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
8
+ // forge-lint: disable-next-line(unaliased-plain-import)
6
9
  import /* {*} from */ "../../src/REVDeployer.sol";
10
+ // forge-lint: disable-next-line(unaliased-plain-import)
7
11
  import "@croptop/core-v6/src/CTPublisher.sol";
12
+ // forge-lint: disable-next-line(unaliased-plain-import)
8
13
  import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
14
+ // forge-lint: disable-next-line(unaliased-plain-import)
9
15
  import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
16
+ // forge-lint: disable-next-line(unaliased-plain-import)
10
17
  import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
18
+ // forge-lint: disable-next-line(unaliased-plain-import)
11
19
  import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
20
+ // forge-lint: disable-next-line(unaliased-plain-import)
12
21
  import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
13
22
 
14
23
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
@@ -29,6 +38,7 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
29
38
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
30
39
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
31
40
  import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
41
+ // forge-lint: disable-next-line(unused-import)
32
42
  import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
33
43
  import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
34
44
  import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
@@ -42,6 +52,7 @@ import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
42
52
  // Buyback hook
43
53
  import {JBBuybackHook} from "@bananapus/buyback-hook-v6/src/JBBuybackHook.sol";
44
54
  import {JBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/JBBuybackHookRegistry.sol";
55
+ // forge-lint: disable-next-line(unused-import)
45
56
  import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
46
57
  import {IGeomeanOracle} from "@bananapus/buyback-hook-v6/src/interfaces/IGeomeanOracle.sol";
47
58
 
@@ -50,10 +61,12 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
50
61
  import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
51
62
  import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
52
63
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
64
+ // forge-lint: disable-next-line(unused-import)
53
65
  import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
54
66
  import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
55
67
  import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
56
68
  import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
69
+ // forge-lint: disable-next-line(unused-import)
57
70
  import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
58
71
  import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
59
72
 
@@ -61,6 +74,7 @@ import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
61
74
 
62
75
  /// @notice Helper that adds liquidity to a V4 pool via the unlock/callback pattern.
63
76
  contract LiquidityHelper is IUnlockCallback {
77
+ // forge-lint: disable-next-line(screaming-snake-case-immutable)
64
78
  IPoolManager public immutable poolManager;
65
79
 
66
80
  enum Action {
@@ -96,7 +110,8 @@ contract LiquidityHelper is IUnlockCallback {
96
110
  payable
97
111
  {
98
112
  bytes memory data =
99
- abi.encode(Action.ADD_LIQUIDITY, abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta)));
113
+ // forge-lint: disable-next-line(named-struct-fields)
114
+ abi.encode(Action.ADD_LIQUIDITY, abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta)));
100
115
  poolManager.unlock(data);
101
116
  }
102
117
 
@@ -110,7 +125,8 @@ contract LiquidityHelper is IUnlockCallback {
110
125
  payable
111
126
  {
112
127
  bytes memory data =
113
- abi.encode(Action.SWAP, abi.encode(DoSwapParams(key, zeroForOne, amountSpecified, sqrtPriceLimitX96)));
128
+ // forge-lint: disable-next-line(named-struct-fields)
129
+ abi.encode(Action.SWAP, abi.encode(DoSwapParams(key, zeroForOne, amountSpecified, sqrtPriceLimitX96)));
114
130
  poolManager.unlock(data);
115
131
  }
116
132
 
@@ -152,7 +168,10 @@ contract LiquidityHelper is IUnlockCallback {
152
168
  DoSwapParams memory params = abi.decode(data, (DoSwapParams));
153
169
 
154
170
  BalanceDelta delta = poolManager.swap(
155
- params.key, SwapParams(params.zeroForOne, params.amountSpecified, params.sqrtPriceLimitX96), ""
171
+ params.key,
172
+ // forge-lint: disable-next-line(named-struct-fields)
173
+ SwapParams(params.zeroForOne, params.amountSpecified, params.sqrtPriceLimitX96),
174
+ ""
156
175
  );
157
176
 
158
177
  if (delta.amount0() < 0) {
@@ -171,12 +190,14 @@ contract LiquidityHelper is IUnlockCallback {
171
190
 
172
191
  function _settleIfNegative(Currency currency, int128 delta) internal {
173
192
  if (delta >= 0) return;
193
+ // forge-lint: disable-next-line(unsafe-typecast)
174
194
  uint256 amount = uint256(uint128(-delta));
175
195
 
176
196
  if (currency.isAddressZero()) {
177
197
  poolManager.settle{value: amount}();
178
198
  } else {
179
199
  poolManager.sync(currency);
200
+ // forge-lint: disable-next-line(erc20-unchecked-transfer)
180
201
  IERC20(Currency.unwrap(currency)).transfer(address(poolManager), amount);
181
202
  poolManager.settle();
182
203
  }
@@ -184,6 +205,7 @@ contract LiquidityHelper is IUnlockCallback {
184
205
 
185
206
  function _takeIfPositive(Currency currency, int128 delta) internal {
186
207
  if (delta <= 0) return;
208
+ // forge-lint: disable-next-line(unsafe-typecast)
187
209
  uint256 amount = uint256(uint128(delta));
188
210
  poolManager.take(currency, address(this), amount);
189
211
  }
@@ -212,24 +234,38 @@ abstract contract ForkTestBase is TestBaseWorkflow {
212
234
  // ───────────────────────── State
213
235
  // ─────────────────────────
214
236
 
237
+ // forge-lint: disable-next-line(mixed-case-variable)
215
238
  REVDeployer REV_DEPLOYER;
239
+ // forge-lint: disable-next-line(mixed-case-variable)
216
240
  JBBuybackHook BUYBACK_HOOK;
241
+ // forge-lint: disable-next-line(mixed-case-variable)
217
242
  JBBuybackHookRegistry BUYBACK_REGISTRY;
243
+ // forge-lint: disable-next-line(mixed-case-variable)
218
244
  JB721TiersHook EXAMPLE_HOOK;
245
+ // forge-lint: disable-next-line(mixed-case-variable)
219
246
  IJB721TiersHookDeployer HOOK_DEPLOYER;
247
+ // forge-lint: disable-next-line(mixed-case-variable)
220
248
  IJB721TiersHookStore HOOK_STORE;
249
+ // forge-lint: disable-next-line(mixed-case-variable)
221
250
  IJBAddressRegistry ADDRESS_REGISTRY;
251
+ // forge-lint: disable-next-line(mixed-case-variable)
222
252
  IREVLoans LOANS_CONTRACT;
253
+ // forge-lint: disable-next-line(mixed-case-variable)
223
254
  IJBSuckerRegistry SUCKER_REGISTRY;
255
+ // forge-lint: disable-next-line(mixed-case-variable)
224
256
  CTPublisher PUBLISHER;
225
257
  IPoolManager poolManager;
226
258
  LiquidityHelper liqHelper;
227
259
 
260
+ // forge-lint: disable-next-line(mixed-case-variable)
228
261
  uint256 FEE_PROJECT_ID;
229
262
 
230
263
  address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
264
+ // forge-lint: disable-next-line(mixed-case-variable)
231
265
  address PAYER = makeAddr("payer");
266
+ // forge-lint: disable-next-line(mixed-case-variable)
232
267
  address BORROWER = makeAddr("borrower");
268
+ // forge-lint: disable-next-line(mixed-case-variable)
233
269
  address SPLIT_BENEFICIARY = makeAddr("splitBeneficiary");
234
270
 
235
271
  // Tier configuration: 1 ETH tier with 30% split.
@@ -345,6 +381,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
345
381
  });
346
382
 
347
383
  cfg = REVConfig({
384
+ // forge-lint: disable-next-line(named-struct-fields)
348
385
  description: REVDescription("Fork Test", "FORK", "ipfs://fork", "FORK_SALT"),
349
386
  baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
350
387
  splitOperator: multisig(),
@@ -374,6 +411,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
374
411
  votingUnits: 0,
375
412
  reserveFrequency: 0,
376
413
  reserveBeneficiary: address(0),
414
+ // forge-lint: disable-next-line(unsafe-typecast)
377
415
  encodedIPFSUri: bytes32("tier1"),
378
416
  category: 1,
379
417
  discountPercent: 0,
@@ -405,6 +443,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
405
443
  preventOverspending: false
406
444
  })
407
445
  }),
446
+ // forge-lint: disable-next-line(unsafe-typecast)
408
447
  salt: bytes32("FORK_721"),
409
448
  preventSplitOperatorAdjustingTiers: false,
410
449
  preventSplitOperatorUpdatingMetadata: false,
@@ -420,6 +459,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
420
459
  function _deployFeeProject(uint16 cashOutTaxRate) internal {
421
460
  (REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
422
461
  _buildMinimalConfig(cashOutTaxRate);
462
+ // forge-lint: disable-next-line(named-struct-fields)
423
463
  feeCfg.description = REVDescription("Fee", "FEE", "ipfs://fee", "FEE_SALT");
424
464
 
425
465
  vm.prank(multisig());
@@ -500,6 +540,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
500
540
  IERC20(projectToken).approve(address(poolManager), type(uint256).max);
501
541
  vm.stopPrank();
502
542
 
543
+ // forge-lint: disable-next-line(unsafe-typecast)
503
544
  int256 liquidityDelta = int256(liquidityTokenAmount / 2);
504
545
  vm.prank(address(liqHelper));
505
546
  liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
@@ -513,12 +554,14 @@ abstract contract ForkTestBase is TestBaseWorkflow {
513
554
 
514
555
  int56[] memory tickCumulatives = new int56[](2);
515
556
  tickCumulatives[0] = 0;
557
+ // forge-lint: disable-next-line(unsafe-typecast)
516
558
  tickCumulatives[1] = int56(tick) * int56(int32(twapWindow));
517
559
 
518
560
  uint136[] memory secondsPerLiquidityCumulativeX128s = new uint136[](2);
519
561
  secondsPerLiquidityCumulativeX128s[0] = 0;
520
562
  uint256 liq = uint256(liquidity > 0 ? liquidity : -liquidity);
521
563
  if (liq == 0) liq = 1;
564
+ // forge-lint: disable-next-line(unsafe-typecast)
522
565
  secondsPerLiquidityCumulativeX128s[1] = uint136((uint256(twapWindow) << 128) / liq);
523
566
 
524
567
  vm.mockCall(
@@ -1,8 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "./ForkTestBase.sol";
5
- import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
6
6
  import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
7
7
 
8
8
  /// @notice Fork tests for revnet cash-out scenarios with real Uniswap V4 buyback hook.
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "./ForkTestBase.sol";
5
6
  import {JBFees} from "@bananapus/core-v6/src/libraries/JBFees.sol";
6
7
 
@@ -1,8 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "./ForkTestBase.sol";
5
- import {JBFees} from "@bananapus/core-v6/src/libraries/JBFees.sol";
6
6
  import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
7
7
 
8
8
  /// @notice Fork tests for loan lifecycle spanning multiple revnet stages (rulesets).
@@ -53,6 +53,7 @@ contract TestLoanCrossRulesetFork is ForkTestBase {
53
53
 
54
54
  // Stage 2: low tax — starts after STAGE_DURATION.
55
55
  stages[1] = REVStageConfig({
56
+ // forge-lint: disable-next-line(unsafe-typecast)
56
57
  startsAtOrAfter: uint40(block.timestamp + STAGE_DURATION),
57
58
  autoIssuances: new REVAutoIssuance[](0),
58
59
  splitPercent: 0,
@@ -65,6 +66,7 @@ contract TestLoanCrossRulesetFork is ForkTestBase {
65
66
  });
66
67
 
67
68
  cfg = REVConfig({
69
+ // forge-lint: disable-next-line(named-struct-fields)
68
70
  description: REVDescription("CrossStage", "XSTG", "ipfs://xstage", "XSTG_SALT"),
69
71
  baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
70
72
  splitOperator: multisig(),
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "./ForkTestBase.sol";
5
6
 
6
7
  /// @notice Fork tests for REVLoans.liquidateExpiredLoansFrom() with real Uniswap V4 buyback hook.
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "./ForkTestBase.sol";
5
6
 
6
7
  /// @notice Fork tests for REVLoans.reallocateCollateralFromLoan() with real Uniswap V4 buyback hook.
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "./ForkTestBase.sol";
5
6
 
6
7
  /// @notice Fork tests for REVLoans.repayLoan() with real Uniswap V4 buyback hook.
@@ -0,0 +1,133 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "./ForkTestBase.sol";
6
+
7
+ /// @notice Fork tests for transferring loan NFTs and repaying from the new owner.
8
+ ///
9
+ /// Covers: transfer + repay by new owner, original owner rejection after transfer, transfer + partial repay.
10
+ ///
11
+ /// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestLoanTransferFork -vvv
12
+ contract TestLoanTransferFork is ForkTestBase {
13
+ uint256 revnetId;
14
+ uint256 loanId;
15
+ REVLoan loan;
16
+ uint256 borrowerTokens;
17
+
18
+ address newOwner = makeAddr("newOwner");
19
+
20
+ function setUp() public override {
21
+ super.setUp();
22
+
23
+ // Deploy fee project + revnet.
24
+ _deployFeeProject(5000);
25
+ revnetId = _deployRevnet(5000);
26
+ _setupPool(revnetId, 10_000 ether);
27
+
28
+ // Pay to create surplus.
29
+ _payRevnet(revnetId, PAYER, 10 ether);
30
+ _payRevnet(revnetId, BORROWER, 5 ether);
31
+
32
+ borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
33
+
34
+ // Create a loan with min prepaid fee.
35
+ (loanId, loan) = _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
36
+ }
37
+
38
+ /// @notice Transfer loan NFT to a new owner, who then fully repays the loan.
39
+ function test_fork_transferLoan_newOwnerCanRepay() public {
40
+ // Transfer the loan NFT from BORROWER to newOwner.
41
+ vm.prank(BORROWER);
42
+ REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
43
+
44
+ // Verify newOwner is the loan NFT owner.
45
+ assertEq(_loanOwnerOf(loanId), newOwner, "newOwner should own the loan NFT after transfer");
46
+
47
+ // Fund newOwner with ETH for repayment.
48
+ vm.deal(newOwner, 100 ether);
49
+
50
+ JBSingleAllowance memory allowance;
51
+
52
+ // newOwner repays the loan in full.
53
+ vm.prank(newOwner);
54
+ LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
55
+ loanId: loanId,
56
+ maxRepayBorrowAmount: loan.amount * 2,
57
+ collateralCountToReturn: loan.collateral,
58
+ beneficiary: payable(newOwner),
59
+ allowance: allowance
60
+ });
61
+
62
+ // Loan NFT should be burned after full repay.
63
+ vm.expectRevert();
64
+ _loanOwnerOf(loanId);
65
+
66
+ // Collateral tokens should be minted to newOwner (the beneficiary).
67
+ uint256 newOwnerTokens = jbTokens().totalBalanceOf(newOwner, revnetId);
68
+ assertEq(newOwnerTokens, borrowerTokens, "collateral should be returned to newOwner");
69
+ }
70
+
71
+ /// @notice After transferring the loan NFT, the original borrower cannot repay.
72
+ function test_fork_transferLoan_originalBorrowerCannotRepay() public {
73
+ // Transfer the loan NFT from BORROWER to newOwner.
74
+ vm.prank(BORROWER);
75
+ REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
76
+
77
+ // Fund BORROWER with ETH for the attempted repayment.
78
+ vm.deal(BORROWER, 100 ether);
79
+
80
+ JBSingleAllowance memory allowance;
81
+
82
+ // Original borrower tries to repay — should revert with REVLoans_Unauthorized.
83
+ vm.prank(BORROWER);
84
+ vm.expectRevert(abi.encodeWithSelector(REVLoans.REVLoans_Unauthorized.selector, BORROWER, newOwner));
85
+ LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
86
+ loanId: loanId,
87
+ maxRepayBorrowAmount: loan.amount * 2,
88
+ collateralCountToReturn: loan.collateral,
89
+ beneficiary: payable(BORROWER),
90
+ allowance: allowance
91
+ });
92
+ }
93
+
94
+ /// @notice Transfer loan NFT, new owner does a partial repay — old loan burned, new loan minted to new owner.
95
+ function test_fork_transferLoan_newOwnerPartialRepay() public {
96
+ // Transfer the loan NFT from BORROWER to newOwner.
97
+ vm.prank(BORROWER);
98
+ REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
99
+
100
+ // Fund newOwner with ETH for repayment.
101
+ vm.deal(newOwner, 100 ether);
102
+
103
+ uint256 halfCollateral = loan.collateral / 2;
104
+
105
+ JBSingleAllowance memory allowance;
106
+
107
+ // newOwner partially repays the loan (return half the collateral).
108
+ vm.prank(newOwner);
109
+ (uint256 newLoanId, REVLoan memory newLoan) = LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
110
+ loanId: loanId,
111
+ maxRepayBorrowAmount: loan.amount * 2,
112
+ collateralCountToReturn: halfCollateral,
113
+ beneficiary: payable(newOwner),
114
+ allowance: allowance
115
+ });
116
+
117
+ // Original loan NFT should be burned.
118
+ vm.expectRevert();
119
+ _loanOwnerOf(loanId);
120
+
121
+ // New loan should exist with reduced collateral.
122
+ assertGt(newLoanId, 0, "new loan should be created");
123
+ assertEq(newLoan.collateral, loan.collateral - halfCollateral, "new loan collateral should be reduced");
124
+ assertLt(newLoan.amount, loan.amount, "new loan borrow amount should be less");
125
+
126
+ // New loan NFT should be owned by newOwner.
127
+ assertEq(_loanOwnerOf(newLoanId), newOwner, "new loan NFT should be owned by newOwner");
128
+
129
+ // Half collateral should be returned to newOwner.
130
+ uint256 newOwnerTokens = jbTokens().totalBalanceOf(newOwner, revnetId);
131
+ assertEq(newOwnerTokens, halfCollateral, "half collateral should be returned to newOwner");
132
+ }
133
+ }
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "./ForkTestBase.sol";
5
6
 
6
7
  /// @notice Fork tests verifying that revnet 721 tier splits + real Uniswap V4 buyback hook produce correct token
@@ -50,6 +51,7 @@ contract TestSplitWeightFork is ForkTestBase {
50
51
  vm.stopPrank();
51
52
 
52
53
  // Add full-range liquidity at tick 0 (1:1 price).
54
+ // forge-lint: disable-next-line(unsafe-typecast)
53
55
  int256 liquidityDelta = int256(ethLiq / 4);
54
56
  vm.prank(address(liqHelper));
55
57
  liqHelper.addLiquidity{value: ethLiq}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
@@ -66,6 +68,7 @@ contract TestSplitWeightFork is ForkTestBase {
66
68
  uint160 sqrtPriceLimit = TickMath.getSqrtPriceAtTick(76_000);
67
69
 
68
70
  vm.prank(address(liqHelper));
71
+ // forge-lint: disable-next-line(unsafe-typecast)
69
72
  liqHelper.swap(key, zeroForOne, -int256(swapAmount), sqrtPriceLimit);
70
73
 
71
74
  // Read the post-swap tick for the oracle mock.
@@ -2,6 +2,7 @@
2
2
  pragma solidity ^0.8.0;
3
3
 
4
4
  import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
5
+ // forge-lint: disable-next-line(unused-import)
5
6
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
6
7
  import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
7
8
  import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
@@ -3,6 +3,7 @@ pragma solidity 0.8.26;
3
3
 
4
4
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
5
5
  import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
6
+ // forge-lint: disable-next-line(unused-import)
6
7
  import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
7
8
  import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
8
9
  import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";